# 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: 1. one client connected to multiple daemons at once 2. tmux-style persistent remote/local sessions 3. SSH transport reuse for identical targets 4. first-class web proxying (HTTP CONNECT + SOCKS5 + websocket) Remote daemon is Go (`cmuxd-remote`) for portability. ## 2. Core Invariants 1. **Daemon owns non-layout state**: PTYs, process lifecycle, scrollback, cwd/title, service/port discovery, proxy channels, persistence. 2. **Client owns layout**: windows/workspaces/panes/focus/reorder remain in Swift app. 3. **Session is durable; attachment is disposable**: UI panes attach/detach from daemon sessions. 4. **Transport is separate from session**: one SSH transport can carry many sessions. 5. **Reuse key is normalized config**: not raw alias text. 6. **One protocol for local and remote**: unix socket and SSH stdio are transport adapters for the same RPC/stream contract. ## 3. Multi-Daemon Model 1. Client has a daemon router keyed by `daemon_id`. 2. Any workspace pane may point to any daemon. 3. Attachment identity: - `pane_id -> daemon_id + session_id + stream_id` 4. Handles exposed in APIs include daemon scope where relevant: - `daemon_id`, `session_id`, `transport_id`, `connection_key_hash` 5. 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: 1. hostname, user, port 2. identity files + `IdentitiesOnly` 3. `ProxyJump` / `ProxyCommand` 4. host-key policy options that change trust/auth semantics 5. auth-impacting `--ssh-option` values Reuse rule: 1. identical normalized key => reuse same SSH transport 2. any key difference => new transport ## 5. Bootstrap + Protocol Bootstrap: 1. ensure remote binary at `~/.cmux/bin/cmuxd-remote//-/cmuxd-remote` 2. checksum-verify before exec 3. run `cmuxd-remote serve --stdio` 4. negotiate version/capabilities 5. if bootstrap fails, fail `cmux ssh` with actionable error (no silent fallback to plain ssh mode) Minimum RPC surface: 1. `hello` 2. `session.create|attach|detach|close|resize|signal` 3. `service.watch` 4. `proxy.open|close` 5. `heartbeat` Protocol requirement: 1. multiplexed framed streams (control + PTY + proxy data) ## 6. Proxying Proxy endpoints (loopback only by default): 1. HTTP CONNECT 2. SOCKS5 Behavior: 1. requests tunnel to daemon, daemon dials destinations 2. websocket must work in both proxy modes 3. local bind conflicts return structured errors (+ optional next-port fallback) ## 7. Reconnect Semantics States: 1. `connected` 2. `degraded` 3. `reconnecting` 4. `disconnected` 5. `fatal` Rules: 1. transport loss moves all attached sessions to `reconnecting` 2. successful reattach must keep same `session_id` (no duplicate shells) 3. `cmux ssh` defaults to persistent sessions 4. persistent sessions survive app restart/disconnect 5. 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 | HTTP CONNECT | fixture response matches expected body | | W-002 | SOCKS5 | response parity with direct remote | | W-003 | websocket via CONNECT | echo integrity, no unexpected close | | W-004 | websocket via SOCKS5 | echo integrity | | W-005 | port conflict | structured conflict error + fallback behavior | | W-006 | concurrent PTY + proxy load | no PTY stall; proxy latency/error budget met | | W-007 | browser auto wiring | browser workflow uses daemon-backed proxy automatically when remote session is active | ### 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_id`s | | 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_id`s 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 1. `remote-terminal-core`: T-001..T-005, T-007 2. `remote-proxy-core`: W-001..W-004, W-007 3. `remote-reconnect-core`: R-001..R-003 4. `remote-multidaemon-core`: M-001..M-002 ## 10. Open Decisions 1. proxy endpoint scope: per daemon transport vs per workspace 2. reconnect retry budget and backoff profile