feat: ssh tunnel + browser auto-open for OpenClaw web dashboard#2452
Conversation
OpenClaw runs a web dashboard on port 18791 of the remote VM. This change SSH-tunnels that port to localhost and auto-opens the browser, giving users a web UI with zero CLI knowledge needed. - Add TunnelConfig to AgentConfig interface (agents.ts) - Add startSshTunnel function with port-finding logic (ssh.ts) - Capture gateway token in closure so the same token is used for both the remote config and the browser URL (agent-setup.ts) - Wire tunnel into orchestration pipeline between preLaunch and interactiveSession (orchestrate.ts) - Add getConnectionInfo to CloudOrchestrator interface and implement in all SSH-based clouds (DO, Hetzner, AWS, GCP) - Local: opens browser directly at localhost:18791 - Sprite: gracefully skipped (no standard SSH) - Add USER.md bootstrap to guide OpenClaw users to web dashboard Closes OpenRouterTeam#2449 Supersedes OpenRouterTeam#2418 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
louisgv
left a comment
There was a problem hiding this comment.
Security Review
Verdict: APPROVED
Commit: d6279b1
Security Analysis
SSH Tunnel Implementation (packages/cli/src/shared/ssh.ts):
- ✅ Uses proper argument array for
Bun.spawn()- no shell interpolation risk - ✅ Port range validation:
remotePorttoremotePort + 10prevents port exhaustion - ✅ Hardcoded tunnel destination
127.0.0.1:${remotePort}- cannot be manipulated - ✅ Uses existing
SSH_BASE_OPTSsecurity flags (StrictHostKeyChecking, etc.) - ✅ Process cleanup via
killWithTimeout()prevents zombie processes - ✅ Graceful error handling with stderr capture
Browser URL Generation (packages/cli/src/shared/agent-setup.ts:666):
- ✅ Token generation:
crypto.randomUUID().replace(/-/g, "")- secure random 128-bit - ✅ Token is properly escaped via
jsonEscape()(JSON.stringify) in config files - ✅ URL construction:
http://localhost:${localPort}/?token=${dashboardToken}- no user-controlled components - ✅
localPortis validated integer from port scan - ✅
dashboardTokenis generated via crypto, not user-supplied
openBrowser Function (packages/cli/src/shared/ui.ts:130):
- ✅ Uses argument array for spawn:
Bun.spawnSync([cmd, ...args])- no shell injection - ✅ URL passed as array element, not interpolated into command string
- ✅ Falls back through platform-specific commands (open, xdg-open, termux-open-url)
Integration Points (packages/cli/src/shared/orchestrate.ts):
- ✅ Tunnel only started for SSH clouds with
getConnectionInfo() - ✅ Local cloud opens browser directly (no tunnel needed)
- ✅ Tunnel cleanup on exit:
tunnelHandle.stop()prevents port leaks - ✅ Try-catch with user-friendly fallback message
USER.md Documentation (packages/cli/src/shared/agent-setup.ts:391-406):
- ✅ Static markdown text, no injection risk
- ✅ Guides users to web dashboard for QR code scanning
Tests
- ✅
bash -n: N/A (no shell scripts modified) - ✅
bun test: 1497 pass, 0 fail (all tests green) - ✅
curl|bash: N/A (TypeScript changes only) - ✅ macOS compat: N/A (TypeScript changes only)
Code Quality
- ✅ Version bumped:
0.15.38→0.15.39(required for CLI changes) - ✅ Type safety: All new code uses proper TypeScript types
- ✅ No
asassertions introduced - ✅ Follows ESM-only pattern (no require/CJS)
Summary
This PR implements SSH tunneling and browser auto-open for OpenClaw's web dashboard. All security-critical operations use safe argument arrays instead of shell interpolation. The token is cryptographically random and properly escaped in all contexts. Port allocation is bounded and cleanup is guaranteed.
No security issues found.
-- security/pr-reviewer
la14-1
left a comment
There was a problem hiding this comment.
Reviewed: all CI checks pass (1497 tests, biome, shellcheck, macOS compat), security already approved. Implementation is clean:
- IIFE closure ensures consistent token between remote config and browser URL
- SSH tunnel uses argument array (no shell injection)
- Port range bounded (remotePort..remotePort+10) with graceful fallback
- Tunnel cleanup on session exit
getConnectionInfo()correctly implemented on all SSH clouds
Approving for merge.
-- refactor/issue-reviewer
Summary
TunnelConfigtoAgentConfiginterface andstartSshTunneltossh.tsgetConnectionInfo()on all SSH-based clouds (AWS, DO, GCP, Hetzner)Closes #2449
Supersedes #2418 (rebased cleanly onto current main)
Test plan
bunx @biomejs/biome check src/— 0 errors (114 files)bun test— 1497 tests passspawn run openclaw digitalocean— verify tunnel opens and browser navigates to dashboard🤖 Generated with Claude Code