Skip to content

Feature/ssh tunnel gateway#117

Merged
shanselman merged 5 commits intoopenclaw:masterfrom
sytone:feature/ssh-tunnel-gateway
Apr 1, 2026
Merged

Feature/ssh tunnel gateway#117
shanselman merged 5 commits intoopenclaw:masterfrom
sytone:feature/ssh-tunnel-gateway

Conversation

@sytone
Copy link
Copy Markdown
Contributor

@sytone sytone commented Mar 29, 2026

Summary

This PR delivers two primary outcomes:

  1. SSH tunnel support in the tray app so gateway traffic can run through a local loopback endpoint while forwarding to a remote host.
  2. Reliable Quick Chat (Quick Send) by treating the tray app as a paired device identity and aligning with gateway auth/approval behavior.

Key Additions

1) SSH Tunnel Feature

  • Added persistent SSH tunnel settings (enable flag, SSH user/host, local/remote ports).
  • Added effective gateway URL behavior so tunnel mode uses local loopback (ws://127.0.0.1:<localPort>) while preserving remote target configuration.
  • Added tunnel lifecycle management in the tray app (start/stop, restart behavior, and shutdown cleanup).
  • Added settings UI and validation/testing flow for tunnel configuration.

2) Quick Chat / Quick Send Working with Paired Tray Device

  • Added operator device identity handshake payload (signed device info on connect).
  • Added connect signature compatibility modes and fallback handling.
  • Persisted/rehydrated device token from successful handshake for reconnect continuity.
  • Added explicit pairing-required handling path (including reconnect suppression behavior where appropriate).
  • Hardened chat.send flow with tracked request/response handling, session key usage, and idempotency key.
  • Updated Quick Send error UX to provide targeted remediation for:
    • pairing required / not paired
    • missing operator scopes

Other Fixes and Updates (Complete Branch Coverage)

Connection and Reconnect Behavior

  • Improved shared websocket reconnect behavior to avoid getting stuck after transient connect failures.
  • Added reconnect-loop guarding and safer reconnect/dispose behavior in the websocket base client.
  • Added ShouldAutoReconnect() control path for terminal/approval states.

UI / UX Improvements

  • Improved Quick Send dialog behavior and focus handling.
  • Added richer Quick Send error details/copy guidance behavior.
  • Fixed canvas visibility by forcing canvas window to foreground on present.
  • Added canvas bring-to-front helper behavior and integrated it into node canvas present flow.

Tray App Lifecycle / Settings

  • Added persisted skipped-update tag behavior and startup prompt logic updates.
  • Expanded settings model + manager round-trip support for new fields.
  • Updated app startup/shutdown wiring to support new services and state transitions.

New Tooling

  • Added OpenClaw.Cli project for gateway websocket validation and quick send probing from tray-compatible settings.
  • Added solution/build integration for the CLI project.

Documentation and Repo Hygiene

  • Updated README to document new capabilities and troubleshooting context.
  • Added AGENTS.md validation expectations for build/test workflow.

Tests

  • Updated/expanded shared tests for gateway/device identity and websocket behavior changes.
  • Updated tray tests for settings round-trip coverage.

Code Audit Coverage

This PR description was verified against the full upstream/master..HEAD code diff (22 files, ~2043 insertions).

Additional notable changes that were not obvious from commit titles:

  • Canvas visibility fix: canvas windows are now explicitly brought to foreground on present (CanvasWindow + NodeService).
  • Reconnect resilience fix in shared websocket base to prevent getting stuck after transient connect failures.
  • Update UX persistence: SkippedUpdateTag added to shared/tray settings and wired into update prompt flow.
  • Settings UI expansion for SSH tunnel configuration and test-connection behavior.
  • New SshTunnelService for tunnel process lifecycle management.
  • Repo/build wiring for the new CLI validator (build.ps1, solution updates, README updates).
  • Validation/process documentation added via AGENTS.md.

sytone added 3 commits March 28, 2026 15:41
- 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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.send request/response tracking + UX remediation.
  • Add OpenClaw.Cli WebSocket 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.

Comment on lines 202 to +209
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");
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +94
var psi = new ProcessStartInfo
{
FileName = "ssh",
Arguments = BuildArguments(user, host, remotePort, localPort),
UseShellExecute = false,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +421 to +486
[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);
}

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
[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);
}

Copilot uses AI. Check for mistakes.
shanselman and others added 2 commits April 1, 2026 00:01
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>
@shanselman shanselman merged commit 1d83639 into openclaw:master Apr 1, 2026
@shanselman shanselman mentioned this pull request Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants