diff --git a/src/OpenClaw.Shared/OpenClawGatewayClient.cs b/src/OpenClaw.Shared/OpenClawGatewayClient.cs index f6515950..35d89baa 100644 --- a/src/OpenClaw.Shared/OpenClawGatewayClient.cs +++ b/src/OpenClaw.Shared/OpenClawGatewayClient.cs @@ -58,6 +58,7 @@ private enum SignatureTokenMode private bool _nodeListUnsupported; private bool _operatorReadScopeUnavailable; private bool _pairingRequiredAwaitingApproval; + private bool _authFailed; private IReadOnlyList? _userRules; private bool _preferStructuredCategories = true; @@ -103,7 +104,7 @@ protected override Task OnConnectedAsync() protected override bool ShouldAutoReconnect() { - return !_pairingRequiredAwaitingApproval; + return !_pairingRequiredAwaitingApproval && !_authFailed; } protected override void OnDisconnected() @@ -665,6 +666,8 @@ private void HandleResponse(JsonElement root) if (payload.TryGetProperty("type", out var t) && t.GetString() == "hello-ok") { _pairingRequiredAwaitingApproval = false; + _authFailed = false; + ResetReconnectAttempts(); _operatorDeviceId = TryGetHandshakeDeviceId(payload); _grantedOperatorScopes = TryGetHandshakeScopes(payload); _mainSessionKey = TryGetHandshakeMainSessionKey(payload) ?? "main"; @@ -792,6 +795,9 @@ private void HandleRequestError(string? method, JsonElement root) } _logger.Warn("Gateway rejected device signature in all supported payload modes"); + _authFailed = true; + RaiseAuthenticationFailed("device signature rejected in all modes — the gateway may require a different auth protocol version"); + RaiseStatusChanged(ConnectionStatus.Error); return; } @@ -804,6 +810,15 @@ private void HandleRequestError(string? method, JsonElement root) return; } + // Permanent auth failures — stop retrying and notify the app + if (method == "connect" && IsTerminalAuthError(message)) + { + _authFailed = true; + RaiseAuthenticationFailed(message); + RaiseStatusChanged(ConnectionStatus.Error); + return; + } + if (IsMissingScopeError(message, "operator.read") && method is "sessions.list" or "usage.status" or "usage.cost" or "node.list") { @@ -904,6 +919,13 @@ private static bool IsUnknownMethodError(string errorMessage) return errorMessage.Contains("unknown method", StringComparison.OrdinalIgnoreCase); } + private static bool IsTerminalAuthError(string errorMessage) + { + return errorMessage.Contains("token mismatch", StringComparison.OrdinalIgnoreCase) || + errorMessage.Contains("origin not allowed", StringComparison.OrdinalIgnoreCase) || + errorMessage.Contains("too many failed", StringComparison.OrdinalIgnoreCase); + } + private static bool IsMissingScopeError(string errorMessage, string scope) { if (string.IsNullOrWhiteSpace(errorMessage) || string.IsNullOrWhiteSpace(scope)) diff --git a/src/OpenClaw.Shared/WebSocketClientBase.cs b/src/OpenClaw.Shared/WebSocketClientBase.cs index 74829b49..aecde0a0 100644 --- a/src/OpenClaw.Shared/WebSocketClientBase.cs +++ b/src/OpenClaw.Shared/WebSocketClientBase.cs @@ -40,6 +40,17 @@ public abstract class WebSocketClientBase : IDisposable // Events public event EventHandler? StatusChanged; + public event EventHandler? AuthenticationFailed; + + /// Reset reconnect backoff counter. Call after successful application-level handshake. + protected void ResetReconnectAttempts() => _reconnectAttempts = 0; + + /// Fire AuthenticationFailed event and stop auto-reconnect. + protected void RaiseAuthenticationFailed(string message) + { + _logger.Warn($"{ClientRole} authentication failed: {message}"); + AuthenticationFailed?.Invoke(this, message); + } // --- Abstract members (subclass MUST implement) --- @@ -123,7 +134,9 @@ public async Task ConnectAsync() await _webSocket.ConnectAsync(uri, _cts.Token); - _reconnectAttempts = 0; + // Don't reset _reconnectAttempts here — TCP connect succeeding doesn't mean + // auth will succeed. Reset only after the full application-level handshake + // completes (subclass calls ResetReconnectAttempts after hello-ok). _logger.Info($"{ClientRole} connected, waiting for challenge..."); await OnConnectedAsync(); diff --git a/src/OpenClaw.Shared/WindowsNodeClient.cs b/src/OpenClaw.Shared/WindowsNodeClient.cs index 9e7fdfd3..91b8e09c 100644 --- a/src/OpenClaw.Shared/WindowsNodeClient.cs +++ b/src/OpenClaw.Shared/WindowsNodeClient.cs @@ -530,6 +530,7 @@ private void HandleResponse(JsonElement root) { var reconnectingAfterApproval = _pairingApprovedAwaitingReconnect; _isConnected = true; + ResetReconnectAttempts(); // Extract node ID if returned if (payload.TryGetProperty("nodeId", out var nodeIdProp)) diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index 589eff04..60d7b4c0 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -87,6 +87,7 @@ public partial class App : Application private ActivityStreamWindow? _activityStreamWindow; private TrayMenuWindow? _trayMenuWindow; private QuickSendDialog? _quickSendDialog; + private string? _authFailureMessage; // Node service (optional, enabled in settings) private NodeService? _nodeService; @@ -753,6 +754,12 @@ private void BuildTrayMenuPopup(TrayMenuWindow menu) var statusIcon = MenuDisplayHelper.GetStatusIcon(_currentStatus); menu.AddMenuItem(string.Format(LocalizationHelper.GetString("Menu_StatusFormat"), LocalizationHelper.GetConnectionStatusText(_currentStatus)), statusIcon, "status"); + // Auth failure nudge + if (!string.IsNullOrEmpty(_authFailureMessage)) + { + menu.AddMenuItem("⚠️ Auth failed — Run Setup", "🔧", "setup"); + } + // Activity (if any) if (_currentActivity != null && _currentActivity.Kind != OpenClaw.Shared.ActivityKind.Idle) { @@ -1108,6 +1115,7 @@ private void InitializeGatewayClient() _gatewayClient.SetUserRules(_settings.UserRules.Count > 0 ? _settings.UserRules : null); _gatewayClient.SetPreferStructuredCategories(_settings.PreferStructuredCategories); _gatewayClient.StatusChanged += OnConnectionStatusChanged; + _gatewayClient.AuthenticationFailed += OnAuthenticationFailed; _gatewayClient.ActivityChanged += OnActivityChanged; _gatewayClient.NotificationReceived += OnNotificationReceived; _gatewayClient.ChannelHealthUpdated += OnChannelHealthUpdated; @@ -1126,6 +1134,7 @@ private void UnsubscribeGatewayEvents() if (_gatewayClient != null) { _gatewayClient.StatusChanged -= OnConnectionStatusChanged; + _gatewayClient.AuthenticationFailed -= OnAuthenticationFailed; _gatewayClient.ActivityChanged -= OnActivityChanged; _gatewayClient.NotificationReceived -= OnNotificationReceived; _gatewayClient.ChannelHealthUpdated -= OnChannelHealthUpdated; @@ -1245,6 +1254,8 @@ private void OnNodeNotificationRequested(object? sender, OpenClaw.Shared.Capabil private void OnConnectionStatusChanged(object? sender, ConnectionStatus status) { _currentStatus = status; + if (status == ConnectionStatus.Connected) + _authFailureMessage = null; UpdateTrayIcon(); if (status == ConnectionStatus.Connected) @@ -1253,6 +1264,14 @@ private void OnConnectionStatusChanged(object? sender, ConnectionStatus status) } } + private void OnAuthenticationFailed(object? sender, string message) + { + _authFailureMessage = message; + Logger.Error($"Authentication failed: {message}"); + AddRecentActivity($"Auth failed: {message}", category: "error"); + UpdateTrayIcon(); + } + private void OnActivityChanged(object? sender, AgentActivity? activity) { if (activity == null)