6.8 KiB
6.8 KiB
Remote Daemon Spec (Concise)
Last updated: February 21, 2026
Tracking issue: https://github.com/manaflow-ai/cmux/issues/151
1. Scope
cmux ssh should support:
- one client connected to multiple daemons at once
- tmux-style persistent remote/local sessions
- SSH transport reuse for identical targets
- first-class web proxying (HTTP CONNECT + SOCKS5 + websocket)
Remote daemon is Go (cmuxd-remote) for portability.
2. Core Invariants
- Daemon owns non-layout state: PTYs, process lifecycle, scrollback, cwd/title, service/port discovery, proxy channels, persistence.
- Client owns layout: windows/workspaces/panes/focus/reorder remain in Swift app.
- Session is durable; attachment is disposable: UI panes attach/detach from daemon sessions.
- Transport is separate from session: one SSH transport can carry many sessions.
- Reuse key is normalized config: not raw alias text.
- One protocol for local and remote: unix socket and SSH stdio are transport adapters for the same RPC/stream contract.
3. Multi-Daemon Model
- Client has a daemon router keyed by
daemon_id. - Any workspace pane may point to any daemon.
- Attachment identity:
pane_id -> daemon_id + session_id + stream_id
- Handles exposed in APIs include daemon scope where relevant:
daemon_id,session_id,transport_id,connection_key_hash
- Cross-daemon "move pane" is modeled as attach/create on target daemon, not live PTY migration.
4. Connection Reuse
Connection reuse key (ConnectionKey) is derived from ssh -G plus cmux flags:
- hostname, user, port
- identity files +
IdentitiesOnly ProxyJump/ProxyCommand- host-key policy options that change trust/auth semantics
- auth-impacting
--ssh-optionvalues
Reuse rule:
- identical normalized key => reuse same SSH transport
- any key difference => new transport
5. Bootstrap + Protocol
Bootstrap:
- ensure remote binary at
~/.cmux/bin/cmuxd-remote/<version>/<os>-<arch>/cmuxd-remote - checksum-verify before exec
- run
cmuxd-remote serve --stdio - negotiate version/capabilities
- if bootstrap fails, fail
cmux sshwith actionable error (no silent fallback to plain ssh mode)
Minimum RPC surface:
hellosession.create|attach|detach|close|resize|signalservice.watchproxy.open|closeheartbeat
Protocol requirement:
- multiplexed framed streams (control + PTY + proxy data)
6. Web Proxying (Browser-First)
Goal: remote workspaces browse from the remote host network, without per-service local port forwards.
Model:
cmux sshcreates/uses one proxy endpoint per SSH transport (not per workspace, not per destination port).- Browser panels opened in remote workspaces are auto-wired to that endpoint.
- Terminal/service port forwarding is not the browser path; keep it opt-in for explicit localhost workflows only.
Implementation:
- local
cmuxdruns a transport-scoped proxy broker (127.0.0.1:<ephemeral>), supporting:- HTTP CONNECT
- SOCKS5
- broker opens multiplexed proxy streams to
cmuxd-remote; remote daemon performs outbound dials. - browser wiring uses workspace-scoped
WKWebsiteDataStore.proxyConfigurations:- primary: SOCKS5 (
ProxyConfiguration(socksv5Proxy:)) - fallback: HTTP CONNECT (
ProxyConfiguration(httpCONNECTProxy:))
- primary: SOCKS5 (
- browser panels in non-remote workspaces use no forced proxy config.
Failure + reconnect:
- if proxy endpoint bind fails, return structured
proxy_unavailablewith actionable detail. - if transport drops, browser requests fail fast, workspace status shows reconnect + retry count.
- after reconnect, proxy broker and WKWebView proxy config are revalidated automatically.
7. Reconnect Semantics
States:
connecteddegradedreconnectingdisconnectedfatal
Rules:
- transport loss moves all attached sessions to
reconnecting - successful reattach must keep same
session_id(no duplicate shells) cmux sshdefaults to persistent sessions- persistent sessions survive app restart/disconnect
- ephemeral sessions can be GC'd after TTL when explicitly requested
8. Test Matrix
All cases require deterministic MUST assertions.
8.1 Terminal
| ID | Scenario | MUST Assertions |
|---|---|---|
| T-001 | baseline connect | one transport, one session, connected state |
| T-002 | identical host twice | same transport_id, refcount 2, one SSH process |
| T-003 | different identity/options | different connection_key_hash, separate transports |
| T-004 | no --name |
workspace created with non-empty title |
| T-005 | scoped niceties | only cmux ssh command metadata includes scoped GHOSTTY_SHELL_FEATURES SSH additions |
| T-006 | detach/reattach | same session_id, state/history preserved |
| T-007 | shell integration e2e | in fresh docker host, cmux ssh yields TERM/terminfo behavior and propagated SSH env vars per ssh-env/ssh-terminfo |
8.2 Web Proxy
| ID | Scenario | MUST Assertions |
|---|---|---|
| W-001 | browser auto wiring | remote workspace browser gets daemon-backed proxy automatically |
| W-002 | remote egress proof | remote workspace browser egress IP matches remote host, not local host |
| W-003 | websocket via CONNECT | echo integrity, no unexpected close |
| W-004 | websocket via SOCKS5 | echo integrity |
| W-005 | proxy listener conflict | structured proxy_unavailable + fallback bind behavior |
| W-006 | concurrent PTY + proxy load | no PTY stall; proxy latency/error budget met |
| W-007 | reconnect continuity | after transport reconnect, browser traffic resumes without manual proxy reconfiguration |
8.3 Reconnect
| ID | Scenario | MUST Assertions |
|---|---|---|
| R-001 | kill transport | sessions enter reconnecting, retries begin |
| R-002 | reconnect success | return to connected, same session_ids |
| R-003 | reconnect exhausted | transition to disconnected with actionable error |
| R-004 | daemon restart | client reattaches per policy without duplicate sessions |
| R-005 | app restart (persistent) | session continuity retained |
8.4 Multi-Daemon
| ID | Scenario | MUST Assertions |
|---|---|---|
| M-001 | one client, two daemons | panes/workspaces may attach to different daemon_ids simultaneously |
| M-002 | per-daemon failure isolation | daemon A outage does not impact daemon B sessions |
| M-003 | mixed local+remote | local cmuxd and remote cmuxd-remote coexist under same client layout |
| M-004 | reconnect with mixed daemons | only affected daemon’s panes transition state; others remain connected |
9. CI Gates
remote-terminal-core: T-001..T-005, T-007remote-proxy-core: W-001..W-004, W-007remote-reconnect-core: R-001..R-003remote-multidaemon-core: M-001..M-002
10. Open Decisions
- reconnect retry budget and backoff profile
- proxy auth policy (none vs optional credentials for local broker)