Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/OpenClaw.Shared/OpenClawGatewayClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ private enum SignatureTokenMode
private bool _nodeListUnsupported;
private bool _operatorReadScopeUnavailable;
private bool _pairingRequiredAwaitingApproval;
private bool _authFailed;
private IReadOnlyList<UserNotificationRule>? _userRules;
private bool _preferStructuredCategories = true;

Expand Down Expand Up @@ -103,7 +104,7 @@ protected override Task OnConnectedAsync()

protected override bool ShouldAutoReconnect()
{
return !_pairingRequiredAwaitingApproval;
return !_pairingRequiredAwaitingApproval && !_authFailed;
}

protected override void OnDisconnected()
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}

Expand All @@ -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")
{
Expand Down Expand Up @@ -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))
Expand Down
15 changes: 14 additions & 1 deletion src/OpenClaw.Shared/WebSocketClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ public abstract class WebSocketClientBase : IDisposable

// Events
public event EventHandler<ConnectionStatus>? StatusChanged;
public event EventHandler<string>? AuthenticationFailed;

/// <summary>Reset reconnect backoff counter. Call after successful application-level handshake.</summary>
protected void ResetReconnectAttempts() => _reconnectAttempts = 0;

/// <summary>Fire AuthenticationFailed event and stop auto-reconnect.</summary>
protected void RaiseAuthenticationFailed(string message)
{
_logger.Warn($"{ClientRole} authentication failed: {message}");
AuthenticationFailed?.Invoke(this, message);
}

// --- Abstract members (subclass MUST implement) ---

Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Shared/WindowsNodeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
19 changes: 19 additions & 0 deletions src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
private ActivityStreamWindow? _activityStreamWindow;
private TrayMenuWindow? _trayMenuWindow;
private QuickSendDialog? _quickSendDialog;
private string? _authFailureMessage;

// Node service (optional, enabled in settings)
private NodeService? _nodeService;
Expand Down Expand Up @@ -302,7 +303,7 @@
StartDeepLinkServer();

// Register global hotkey if enabled
if (_settings.GlobalHotkeyEnabled)

Check warning on line 306 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.

Check warning on line 306 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

Dereference of a possibly null reference.

Check warning on line 306 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

Dereference of a possibly null reference.

Check warning on line 306 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

Dereference of a possibly null reference.

Check warning on line 306 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

Dereference of a possibly null reference.

Check warning on line 306 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build-msix (win-arm64)

Dereference of a possibly null reference.

Check warning on line 306 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build-msix (win-x64)

Dereference of a possibly null reference.
{
_globalHotkey = new GlobalHotkeyService();
_globalHotkey.HotkeyPressed += OnGlobalHotkeyPressed;
Expand Down Expand Up @@ -753,6 +754,12 @@
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)
{
Expand Down Expand Up @@ -1108,6 +1115,7 @@
_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;
Expand All @@ -1126,6 +1134,7 @@
if (_gatewayClient != null)
{
_gatewayClient.StatusChanged -= OnConnectionStatusChanged;
_gatewayClient.AuthenticationFailed -= OnAuthenticationFailed;
_gatewayClient.ActivityChanged -= OnActivityChanged;
_gatewayClient.NotificationReceived -= OnNotificationReceived;
_gatewayClient.ChannelHealthUpdated -= OnChannelHealthUpdated;
Expand Down Expand Up @@ -1245,6 +1254,8 @@
private void OnConnectionStatusChanged(object? sender, ConnectionStatus status)
{
_currentStatus = status;
if (status == ConnectionStatus.Connected)
_authFailureMessage = null;
UpdateTrayIcon();

if (status == ConnectionStatus.Connected)
Expand All @@ -1253,6 +1264,14 @@
}
}

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)
Expand Down
Loading