9.3 KiB
9.3 KiB
Remote SSH Living Spec
Last updated: February 23, 2026 Tracking issue: https://github.com/manaflow-ai/cmux/issues/151 Primary PR: https://github.com/manaflow-ai/cmux/pull/239 CLI relay PR: https://github.com/manaflow-ai/cmux/pull/374
This document is the working source of truth for:
- what is implemented now
- what is intentionally temporary
- what must be built next
1. Document Type
This is a living implementation spec (also called an execution spec): a spec-level document with status tracking (DONE, IN PROGRESS, TODO) and acceptance tests.
2. Objective
cmux ssh should provide:
- durable remote terminals with reconnect/reuse
- browser traffic that egresses from the remote host via proxying
- tmux-style PTY resize semantics (
smallest screen wins)
3. Current State (Implemented)
3.1 Remote Workspace + Reconnect UX
DONEcmux sshcreates remote-tagged workspaces and does not require--name.DONEscoped shell niceties are applied only forcmux sshlaunches.DONEcontext menu actions exist for remote workspaces (Reconnect Workspace(s),Disconnect Workspace(s)).DONEsocket API includesworkspace.remote.reconnect.
3.2 Bootstrap + Daemon
DONElocal app probes remote platform, builds/uploadscmuxd-remote, and runsserve --stdio.DONEdaemonhellohandshake is enforced.DONEbootstrap/probe failures surface actionable details.DONEbootstrap installs~/.cmux/bin/cmuxwrapper (also tries/usr/local/bin/cmux) socmuxis available in PATH on the remote.
3.5 CLI Relay (Running cmux Commands From Remote)
DONEcmuxd-remoteincludes a table-driven CLI relay (clisubcommand) that maps CLI args to v1 text or v2 JSON-RPC messages.DONEbusybox-style argv[0] detection: when invoked ascmuxvia wrapper/symlink, auto-dispatches to CLI relay.DONEbackgroundssh -N -R 127.0.0.1:PORT:/local/cmux.sockprocess reverse-forwards a TCP port to the local cmux socket. Uses TCP instead of Unix socket forwarding because many servers haveAllowStreamLocalForwardingdisabled.DONErelay process usesControlPath=none(avoids ControlMaster multiplexing and inheritedRemoteForwarddirectives) andExitOnForwardFailure=no(inherited forwards from user ssh config failing should not kill the relay).DONErelay address written to~/.cmux/socket_addron the remote with a 3s delay after the relay process starts, giving SSH time to establish the-Rforward.DONEGo CLI re-reads~/.cmux/socket_addron each TCP retry to pick up updated relay ports when multiple workspaces overwrite the file.DONEcmux sshstartup exports session-localCMUX_SOCKET_PATH=127.0.0.1:<relay_port>so parallel sessions pin to their own relay instead of racing on shared socket_addr.DONErelay startup writes~/.cmux/relay/<relay_port>.daemon_path; remotecmuxwrapper uses this to select the right daemon binary per session, including mixed local cmux versions.DONEephemeral port range (49152-65535) filtered from probe results to exclude relay ports from other workspaces.DONEmulti-workspace port conflict detection uses TCP connect check (isLoopbackPortReachable) so ports already forwarded by another workspace are silently skipped instead of flagged as conflicts.DONEorphaned relay SSH processes from previous app sessions are cleaned up before starting a new relay.
3.3 Error Surfacing
DONEremote errors are surfaced in sidebar status + logs + notifications.DONEreconnect retry count/time is included in surfaced error text (for example,retry 1 in 4s).
3.4 Existing Temporary Behavior (To Remove)
TEMPORARYcurrent implementation probes remote listening ports and mirrors them locally with SSH-L.TEMPORARYsidebar shows local bind conflicts (SSH port conflicts ...) caused by that mirroring path.TARGETbrowser path must no longer depend on per-port mirroring.
4. Target Architecture (No Port Mirroring)
4.1 Browser Networking Path
- One local proxy endpoint per SSH transport (not per workspace, not per detected port).
- Proxy endpoint supports SOCKS5 and HTTP CONNECT.
- Browser panels in remote workspaces are auto-wired to this proxy endpoint.
- Browser panels in local workspaces are not force-proxied.
4.2 WKWebView Wiring
- Use workspace/browser scoped
WKWebsiteDataStore.proxyConfigurations. - Prefer SOCKS5 proxy config.
- Keep HTTP CONNECT proxy config as fallback.
- Re-apply/validate proxy config after reconnect.
4.3 Remote Daemon + Transport
- Extend
cmuxd-remotebeyondhello/pingwith proxy stream RPC (proxy.open,proxy.close). - Local side runs a transport-scoped proxy broker and multiplexes proxy streams over SSH stdio transport.
- Remove remote service-port discovery/probing from browser routing path.
4.4 Explicit Non-Goal
- Automatic mirroring of every remote listening port to local loopback is not a goal for browser support.
5. PTY Resize Semantics (tmux-style)
5.1 Core Rule
For each session with multiple attachments, the effective PTY size is:
cols = min(cols_i over attached clients)rows = min(rows_i over attached clients)
This is the smallest screen wins rule.
5.2 State Model
Per session track:
- set of active attachments
{attachment_id -> cols, rows, updated_at} - effective size currently applied to PTY
- last-known size when temporarily unattached
5.3 Recompute Triggers
Recompute effective size on:
- attachment create
- attachment detach
- resize event from any attachment
- reconnect reattach
5.4 Correctness Requirements
- Never shrink history because of UI relayout noise; only PTY viewport changes.
- On reconnect, reuse persisted session and recompute from active attachments.
- If no attachments remain, keep last-known PTY size (do not force 80x24 reset).
6. Milestones (Living Status)
| ID | Milestone | Status | Notes |
|---|---|---|---|
| M-001 | cmux ssh workspace creation + metadata + optional --name |
DONE | Covered by tests_v2/test_ssh_remote_cli_metadata.py |
| M-002 | Remote bootstrap/upload/start + hello handshake | DONE | Current cmuxd-remote is minimal (hello, ping) |
| M-003 | Reconnect/disconnect UX + API + improved error surfacing | DONE | Includes retry count in surfaced errors |
| M-004 | Docker e2e for bootstrap/reconnect shell niceties | DONE | Existing docker tests currently validate mirroring-era path |
| M-004b | CLI relay: run cmux commands from within SSH sessions | DONE | Reverse TCP forward + Go CLI relay + bootstrap wrapper (PR #374) |
| M-005 | Remove automatic remote port mirroring path | TODO | Delete probe/listen mirror loop from WorkspaceRemoteSessionController |
| M-006 | Transport-scoped local proxy broker (SOCKS5 + CONNECT) | TODO | Local component in app/daemon layer |
| M-007 | Remote proxy stream RPC in cmuxd-remote |
TODO | Add proxy.open/close and multiplexed stream handling |
| M-008 | WebView proxy auto-wiring for remote workspaces | TODO | Use WKWebsiteDataStore.proxyConfigurations |
| M-009 | PTY resize coordinator (smallest screen wins) |
TODO | Session-level attachment-size aggregation |
| M-010 | Resize + proxy reconnect e2e test suites | TODO | Add dedicated docker cases for browser proxy + resize |
7. Acceptance Test Matrix (With Status)
7.1 Terminal + Reconnect
| ID | Scenario | Status |
|---|---|---|
| T-001 | baseline remote connect | DONE |
| T-002 | identical host reuse semantics | PARTIAL |
| T-003 | no --name |
DONE |
| T-004 | reconnect API success/error paths | DONE |
| T-005 | retry count visible in daemon error detail | DONE |
7.2 CLI Relay
| ID | Scenario | Status |
|---|---|---|
| C-001 | cmux ping from remote session |
DONE |
| C-002 | cmux list-workspaces --json from remote |
DONE |
| C-003 | cmux new-workspace from remote |
DONE |
| C-004 | cmux rpc system.capabilities passthrough |
DONE |
| C-005 | TCP retry handles relay not yet established | DONE |
| C-006 | multi-workspace port conflict silent skip | DONE |
| C-007 | ephemeral port filtering excludes relay ports | DONE |
7.3 Browser Proxy (Target)
| ID | Scenario | Status |
|---|---|---|
| W-001 | remote workspace browser auto-proxied | TODO |
| W-002 | browser egress IP equals remote host IP | TODO |
| W-003 | websocket via SOCKS5/CONNECT through remote daemon | TODO |
| W-004 | reconnect restores browser proxy path automatically | TODO |
| W-005 | local proxy bind conflict yields structured proxy_unavailable |
TODO |
7.4 Resize
| ID | Scenario | Status |
|---|---|---|
| RZ-001 | two attachments, smallest wins | TODO |
| RZ-002 | grow one attachment, PTY stays bounded by smallest | TODO |
| RZ-003 | detach smallest, PTY expands to next smallest | TODO |
| RZ-004 | reconnect preserves session + applies recomputed size | TODO |
8. Removal Checklist (Port Mirroring)
Before declaring browser proxying complete:
- remove remote port probe loop and
-Lauto-forward orchestration - remove mirror-specific sidebar conflict messaging as default remote behavior
- replace mirroring tests with browser-proxy e2e tests
- keep optional explicit user-driven forwarding as separate feature only if needed
9. Open Decisions
- Proxy auth policy for local broker (
nonevs optional credentials). - Reconnect backoff profile and max retry budget.
- Browser data-store isolation policy for remote vs local workspaces.