Conversation
- Introduced SkippedUpdateTag property in SettingsData and SettingsManager to remember skipped updates. - Updated App.xaml.cs to initialize settings before update checks and handle skipped updates. - Enhanced QuickSendDialog to provide detailed error messages and focus handling. - Improved WebSocketClientBase with better auto-reconnect logic and error handling. - Added integration tests for DeviceIdentity payload formats and OpenClawGatewayClient response handling. - Updated SettingsRoundTripTests to validate SkippedUpdateTag persistence.
There was a problem hiding this comment.
Pull request overview
This PR adds SSH-tunnel support and improves gateway authentication/handshake behavior so the tray app can operate as a paired operator device identity (notably to make Quick Send reliable), along with a new CLI validator to test tray-compatible connectivity outside the UI.
Changes:
- Add SSH tunnel settings + UI + runtime tunnel lifecycle management, and route gateway connections via loopback when enabled.
- Implement operator device identity handshake/signature compatibility, persist device token, and harden
chat.sendrequest/response tracking + UX remediation. - Add
OpenClaw.CliWebSocket validator and update build/docs/test expectations accordingly.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/OpenClaw.Tray.Tests/SettingsRoundTripTests.cs | Extends settings JSON round-trip coverage for SSH tunnel fields and skipped update tag. |
| tests/OpenClaw.Shared.Tests/WebSocketClientBaseTests.cs | Updates reconnect/status expectations to match more resilient connect/cancel behavior. |
| tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs | Adds tests for pending chat.send completion and handshake payload parsing helpers. |
| tests/OpenClaw.Shared.Tests/DeviceIdentityTests.cs | Adds integration tests for v2/v3 connect payload formatting. |
| src/OpenClaw.Tray.WinUI/Windows/SettingsWindow.xaml.cs | Wires SSH tunnel settings to the UI, validation, and test-connection flow. |
| src/OpenClaw.Tray.WinUI/Windows/SettingsWindow.xaml | Adds SSH tunnel toggle + configuration fields to settings UI. |
| src/OpenClaw.Tray.WinUI/Windows/CanvasWindow.xaml.cs | Adds best-effort bring-to-front helpers to ensure canvas visibility. |
| src/OpenClaw.Tray.WinUI/Services/SshTunnelService.cs | Introduces SSH port-forward process lifecycle management service. |
| src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs | Persists new SSH tunnel settings and adds effective gateway URL selection. |
| src/OpenClaw.Tray.WinUI/Services/NodeService.cs | Ensures presented canvas is brought to foreground for visibility. |
| src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs | Improves focus behavior and provides richer error details/remediation for pairing/scope issues. |
| src/OpenClaw.Tray.WinUI/App.xaml.cs | Starts/stops tunnel service as needed; routes gateway/node/web UI through effective URL; adds update-skip persistence; hardens shutdown. |
| src/OpenClaw.Shared/WebSocketClientBase.cs | Adds reconnect-loop guarding and ShouldAutoReconnect() hook for terminal states. |
| src/OpenClaw.Shared/SettingsData.cs | Adds persisted fields for SSH tunnel settings and skipped update tag. |
| src/OpenClaw.Shared/OpenClawGatewayClient.cs | Adds operator device identity handshake/signing, token persistence, missing-scope handling, and tracked chat.send flow. |
| src/OpenClaw.Shared/DeviceIdentity.cs | Adds v2/v3 connect payload building/signing helpers. |
| src/OpenClaw.Cli/Program.cs | Adds CLI tool to validate connect + chat.send using tray settings/overrides. |
| src/OpenClaw.Cli/OpenClaw.Cli.csproj | Adds new CLI project referencing the shared library. |
| README.md | Documents CLI validator and clarifies Quick Send scope/pairing requirements. |
| moltbot-windows-hub.slnx | Adds the CLI project to the solution. |
| build.ps1 | Adds CLI to build targets and includes it in the “All” build. |
| AGENTS.md | Documents required build/test validation steps for repository contributors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| await SendRawAsync(JsonSerializer.Serialize(req)); | ||
|
|
||
| var completedTask = await Task.WhenAny(completion.Task, Task.Delay(5000, CancellationToken)); | ||
| if (completedTask != completion.Task) | ||
| { | ||
| RemovePendingChatSend(requestId); | ||
| throw new TimeoutException("Timed out waiting for chat.send response from gateway"); | ||
| } |
There was a problem hiding this comment.
SendChatMessageAsync waits with Task.Delay(5000, CancellationToken). If the client cancellation token is triggered (disconnect/shutdown), the delay task completes as Canceled and WhenAny will treat it as a timeout, throwing TimeoutException instead of propagating cancellation. Consider using an uncancelable timeout task (CancellationToken.None) and then separately handling CancellationToken.IsCancellationRequested / awaiting completion.Task to preserve the correct exception semantics.
| var psi = new ProcessStartInfo | ||
| { | ||
| FileName = "ssh", | ||
| Arguments = BuildArguments(user, host, remotePort, localPort), | ||
| UseShellExecute = false, |
There was a problem hiding this comment.
SshTunnelService builds a single ProcessStartInfo.Arguments string from user/host without robust escaping. This can break for usernames/hosts containing spaces and enables argument injection (e.g., settings including extra flags). Prefer ProcessStartInfo.ArgumentList (or equivalent structured argv building) and validate/normalize user+host inputs (e.g., disallow whitespace) before launching ssh.
| [Fact] | ||
| public void ParseHandshakeMainSessionKey_ReturnsMainKey_WhenPresent() | ||
| { | ||
| var helper = new GatewayClientTestHelper(); | ||
| var key = helper.ParseHandshakeMainSessionKey(""" | ||
| { | ||
| "type": "hello-ok", | ||
| "snapshot": { | ||
| "sessionDefaults": { | ||
| "mainKey": "agent:main:123" | ||
| } | ||
| } | ||
| } | ||
| """); | ||
|
|
||
| Assert.Equal("agent:main:123", key); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseHandshakeMainSessionKey_ReturnsNull_WhenMissing() | ||
| { | ||
| var helper = new GatewayClientTestHelper(); | ||
| var key = helper.ParseHandshakeMainSessionKey(""" | ||
| { | ||
| "type": "hello-ok", | ||
| "snapshot": { | ||
| "sessionDefaults": { | ||
| } | ||
| } | ||
| } | ||
| """); | ||
|
|
||
| Assert.Null(key); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseHandshakeDeviceToken_ReturnsValue_WhenPresent() | ||
| { | ||
| var helper = new GatewayClientTestHelper(); | ||
| var token = helper.ParseHandshakeDeviceToken(""" | ||
| { | ||
| "type": "hello-ok", | ||
| "auth": { | ||
| "deviceToken": "device-token-123" | ||
| } | ||
| } | ||
| """); | ||
|
|
||
| Assert.Equal("device-token-123", token); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseHandshakeDeviceToken_ReturnsNull_WhenMissing() | ||
| { | ||
| var helper = new GatewayClientTestHelper(); | ||
| var token = helper.ParseHandshakeDeviceToken(""" | ||
| { | ||
| "type": "hello-ok", | ||
| "auth": { | ||
| } | ||
| } | ||
| """); | ||
|
|
||
| Assert.Null(token); | ||
| } | ||
|
|
There was a problem hiding this comment.
The new handshake parsing tests appear to be accidentally indented (attributes/method bodies shifted right), which makes the file harder to read and inconsistent with the surrounding test style. Please reformat these blocks (e.g., run dotnet format) so the [Fact] methods align with the rest of the class.
| [Fact] | |
| public void ParseHandshakeMainSessionKey_ReturnsMainKey_WhenPresent() | |
| { | |
| var helper = new GatewayClientTestHelper(); | |
| var key = helper.ParseHandshakeMainSessionKey(""" | |
| { | |
| "type": "hello-ok", | |
| "snapshot": { | |
| "sessionDefaults": { | |
| "mainKey": "agent:main:123" | |
| } | |
| } | |
| } | |
| """); | |
| Assert.Equal("agent:main:123", key); | |
| } | |
| [Fact] | |
| public void ParseHandshakeMainSessionKey_ReturnsNull_WhenMissing() | |
| { | |
| var helper = new GatewayClientTestHelper(); | |
| var key = helper.ParseHandshakeMainSessionKey(""" | |
| { | |
| "type": "hello-ok", | |
| "snapshot": { | |
| "sessionDefaults": { | |
| } | |
| } | |
| } | |
| """); | |
| Assert.Null(key); | |
| } | |
| [Fact] | |
| public void ParseHandshakeDeviceToken_ReturnsValue_WhenPresent() | |
| { | |
| var helper = new GatewayClientTestHelper(); | |
| var token = helper.ParseHandshakeDeviceToken(""" | |
| { | |
| "type": "hello-ok", | |
| "auth": { | |
| "deviceToken": "device-token-123" | |
| } | |
| } | |
| """); | |
| Assert.Equal("device-token-123", token); | |
| } | |
| [Fact] | |
| public void ParseHandshakeDeviceToken_ReturnsNull_WhenMissing() | |
| { | |
| var helper = new GatewayClientTestHelper(); | |
| var token = helper.ParseHandshakeDeviceToken(""" | |
| { | |
| "type": "hello-ok", | |
| "auth": { | |
| } | |
| } | |
| """); | |
| Assert.Null(token); | |
| } | |
| [Fact] | |
| public void ParseHandshakeMainSessionKey_ReturnsMainKey_WhenPresent() | |
| { | |
| var helper = new GatewayClientTestHelper(); | |
| var key = helper.ParseHandshakeMainSessionKey(""" | |
| { | |
| "type": "hello-ok", | |
| "snapshot": { | |
| "sessionDefaults": { | |
| "mainKey": "agent:main:123" | |
| } | |
| } | |
| } | |
| """); | |
| Assert.Equal("agent:main:123", key); | |
| } | |
| [Fact] | |
| public void ParseHandshakeMainSessionKey_ReturnsNull_WhenMissing() | |
| { | |
| var helper = new GatewayClientTestHelper(); | |
| var key = helper.ParseHandshakeMainSessionKey(""" | |
| { | |
| "type": "hello-ok", | |
| "snapshot": { | |
| "sessionDefaults": { | |
| } | |
| } | |
| } | |
| """); | |
| Assert.Null(key); | |
| } | |
| [Fact] | |
| public void ParseHandshakeDeviceToken_ReturnsValue_WhenPresent() | |
| { | |
| var helper = new GatewayClientTestHelper(); | |
| var token = helper.ParseHandshakeDeviceToken(""" | |
| { | |
| "type": "hello-ok", | |
| "auth": { | |
| "deviceToken": "device-token-123" | |
| } | |
| } | |
| """); | |
| Assert.Equal("device-token-123", token); | |
| } | |
| [Fact] | |
| public void ParseHandshakeDeviceToken_ReturnsNull_WhenMissing() | |
| { | |
| var helper = new GatewayClientTestHelper(); | |
| var token = helper.ParseHandshakeDeviceToken(""" | |
| { | |
| "type": "hello-ok", | |
| "auth": { | |
| } | |
| } | |
| """); | |
| Assert.Null(token); | |
| } |
Add strict regex validation (alphanumeric, dots, hyphens, underscores) to BuildArguments() before concatenating into SSH command line. Prevents injection of additional SSH flags or shell metacharacters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve conflicts: - GatewayClient: keep both PR fields and master's _userRules + operator.write scope - App.xaml.cs: use PR's GetEffectiveGatewayUrl + master's SetUserRules + SSH stop + status reset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
This PR delivers two primary outcomes:
Key Additions
1) SSH Tunnel Feature
ws://127.0.0.1:<localPort>) while preserving remote target configuration.2) Quick Chat / Quick Send Working with Paired Tray Device
chat.sendflow with tracked request/response handling, session key usage, and idempotency key.Other Fixes and Updates (Complete Branch Coverage)
Connection and Reconnect Behavior
ShouldAutoReconnect()control path for terminal/approval states.UI / UX Improvements
Tray App Lifecycle / Settings
New Tooling
OpenClaw.Cliproject for gateway websocket validation and quick send probing from tray-compatible settings.Documentation and Repo Hygiene
AGENTS.mdvalidation expectations for build/test workflow.Tests
Code Audit Coverage
This PR description was verified against the full
upstream/master..HEADcode diff (22 files, ~2043 insertions).Additional notable changes that were not obvious from commit titles:
CanvasWindow+NodeService).SkippedUpdateTagadded to shared/tray settings and wired into update prompt flow.SshTunnelServicefor tunnel process lifecycle management.build.ps1, solution updates, README updates).AGENTS.md.