docs/remote-daemon-spec.md
Last updated: March 12, 2026 Tracking issue: https://github.com/manaflow-ai/cmux/issues/151 Primary PR: https://github.com/manaflow-ai/cmux/pull/1296 CLI relay PR: https://github.com/manaflow-ai/cmux/pull/374
This document is the working source of truth for:
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.
cmux ssh should provide:
smallest screen wins)DONE cmux ssh creates remote-tagged workspaces and does not require --name.DONE scoped shell niceties are applied only for cmux ssh launches.DONE context menu actions exist for remote workspaces (Reconnect Workspace(s), Disconnect Workspace(s)).DONE socket API includes workspace.remote.reconnect.DONE local app probes remote platform, verifies a release-pinned cmuxd-remote artifact by embedded manifest SHA-256, uploads it when missing, and runs serve --stdio.DONE daemon hello handshake is enforced.DONE daemon now exposes proxy stream RPC (proxy.open, proxy.close, proxy.write, proxy.stream.subscribe) plus pushed proxy.stream.* events.DONE local proxy broker now tunnels SOCKS5/CONNECT traffic over daemon stream RPC instead of ssh -D.DONE daemon now exposes session resize-coordinator RPC (session.open, session.attach, session.resize, session.detach, session.status, session.close).DONE transport-level proxy failures now escalate from broker retry to full daemon re-bootstrap/reconnect in the session controller.DONE SOCKS handshake parsing now preserves pipelined post-connect payload bytes instead of dropping request-prefix bytes.DONE workspace.remote.configure.local_proxy_port exists as an internal deterministic test hook for bind-conflict regression coverage.DONE bootstrap/probe failures surface actionable details.DONE bootstrap installs ~/.cmux/bin/cmux wrapper (also tries /usr/local/bin/cmux) so cmux is available in PATH on the remote.DONE cmuxd-remote includes a table-driven CLI relay (cli subcommand) that maps CLI args to v1 text or v2 JSON-RPC messages.DONE busybox-style argv[0] detection: when invoked as cmux via wrapper/symlink, auto-dispatches to CLI relay.DONE background ssh -N -R 127.0.0.1:PORT:127.0.0.1:LOCAL_RELAY_PORT process reverse-forwards a TCP port to a dedicated authenticated local relay server. Uses TCP instead of Unix socket forwarding because many servers have AllowStreamLocalForwarding disabled.DONE relay process uses -S none / standalone SSH transport (avoids ControlMaster multiplexing and inherited RemoteForward directives) and ExitOnForwardFailure=yes so dead reverse binds fail fast instead of publishing bad relay metadata.DONE relay address written to ~/.cmux/socket_addr on the remote only after the reverse forward survives startup validation.DONE Go CLI no longer polls for relay readiness. It dials the published relay once and only refreshes ~/.cmux/socket_addr a single time to recover from a stale shared address rewrite.DONE cmux ssh startup exports session-local CMUX_SOCKET_PATH=127.0.0.1:<relay_port> so parallel sessions pin to their own relay instead of racing on shared socket_addr.DONE relay startup writes ~/.cmux/relay/<relay_port>.daemon_path; remote cmux wrapper uses this to select the right daemon binary per session, including mixed local cmux versions.DONE relay startup writes ~/.cmux/relay/<relay_port>.auth with a relay ID and token; the local relay requires HMAC-SHA256 challenge-response before forwarding any command to the real local socket.DONE ephemeral port range (49152-65535) filtered from probe results to exclude relay ports from other workspaces.DONE multi-workspace port conflict detection uses TCP connect check (isLoopbackPortReachable) so ports already forwarded by another workspace are silently skipped instead of flagged as conflicts.DONE orphaned relay SSH processes from previous app sessions are cleaned up before starting a new relay.DONE release and nightly workflows publish cmuxd-remote assets for darwin/linux × arm64/amd64.DONE release and nightly apps embed a compact CMUXRemoteDaemonManifestJSON in Info.plist with exact asset URLs and SHA-256 digests.DONE cmux remote-daemon-status exposes the current manifest entry, local cache verification state, release download command, and GitHub attestation verification command.DONE remote errors are surfaced in sidebar status + logs + notifications.DONE reconnect retry count/time is included in surfaced error text (for example, retry 1 in 4s).DONE removed remote listening-port probe loop and per-port SSH -L mirroring.DONE remote browser routing now uses a single shared local proxy endpoint instead of detected-port mirroring.DONE remote status now includes structured proxy metadata (remote.proxy) and proxy_unavailable error code when proxy setup fails.DONE one local proxy endpoint is created per SSH transport/session key (not per detected port).DONE endpoint is provided by a local broker that supports SOCKS5 + HTTP CONNECT and tunnels via daemon stream RPC.DONE browser panels in remote workspaces are auto-wired to the workspace proxy endpoint.DONE browser panels in local workspaces are not force-proxied.DONE identical SSH transports share one endpoint via a transport-scoped broker.DONE use workspace-scoped WKWebsiteDataStore(forIdentifier:).DONE apply workspace/browser scoped proxyConfigurations.DONE prefer SOCKS5 proxy config.DONE keep HTTP CONNECT proxy config as fallback.DONE re-apply proxy config on reconnect/state updates.DONE cmuxd-remote now supports proxy stream RPC (proxy.open, proxy.close, proxy.write, proxy.stream.subscribe) with pushed proxy.stream.data/eof/error events.DONE local side now runs a shared local broker that serves SOCKS5/CONNECT and tunnels each stream over persistent daemon stdio RPC without polling reads.DONE removed remote service-port discovery/probing from browser routing path.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.
Per session track:
{attachment_id -> cols, rows, updated_at}Recompute effective size on:
| 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 | Includes daemon capability handshake + status surfacing |
| 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 | Docker suites validate proxy-path bootstrap and reconnect behavior |
| M-004b | CLI relay: run cmux commands from within SSH sessions | DONE | Reverse TCP forward + Go CLI relay + bootstrap wrapper |
| M-005 | Remove automatic remote port mirroring path | DONE | WorkspaceRemoteSessionController now uses one shared daemon-backed proxy endpoint |
| M-006 | Transport-scoped local proxy broker (SOCKS5 + CONNECT) | DONE | Identical SSH transports now reuse one local proxy endpoint |
| M-007 | Remote proxy stream RPC in cmuxd-remote | DONE | proxy.open/close/write/proxy.stream.subscribe plus pushed stream events implemented |
| M-008 | WebView proxy auto-wiring for remote workspaces | DONE | Workspace-scoped WKWebsiteDataStore.proxyConfigurations wiring is active |
| M-009 | PTY resize coordinator (smallest screen wins) | DONE | Daemon session RPC now tracks attachments and applies min cols/rows semantics with unit tests |
| M-010 | Resize + proxy reconnect e2e test suites | DONE | tests_v2/test_ssh_remote_docker_forwarding.py validates HTTP/websocket egress plus SOCKS pipelined-payload handling; tests_v2/test_ssh_remote_docker_reconnect.py verifies reconnect recovery and repeats SOCKS pipelined-payload checks after host restart; tests_v2/test_ssh_remote_proxy_bind_conflict.py validates structured proxy_unavailable bind-conflict surfacing and local_proxy_port status retention under bind conflict; tests_v2/test_ssh_remote_daemon_resize_stdio.py validates session resize semantics over real stdio RPC process boundaries; tests_v2/test_ssh_remote_cli_metadata.py validates workspace.remote.configure numeric-string compatibility, explicit null clear semantics (including workspace.remote.status reflection), strict port/local_proxy_port validation (bounds/type), case-insensitive SSH option override precedence for StrictHostKeyChecking/control-socket keys, and local_proxy_port payload echo for deterministic bind-conflict test hook behavior |
| ID | Scenario | Status |
|---|---|---|
| T-001 | baseline remote connect | DONE |
| T-002 | identical host reuse semantics | DONE |
| T-003 | no --name | DONE |
| T-004 | reconnect API success/error paths | DONE |
| T-005 | retry count visible in daemon error detail | DONE |
| 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 |
| ID | Scenario | Status |
|---|---|---|
| W-001 | remote workspace browser auto-proxied | DONE |
| W-002 | browser egress equals remote network path | DONE |
| W-003 | websocket via SOCKS5/CONNECT through remote daemon | DONE |
| W-004 | reconnect restores browser proxy path automatically | DONE |
| W-005 | local proxy bind conflict yields structured proxy_unavailable | DONE |
| W-006 | proxy transport failure triggers daemon re-bootstrap and recovers after host recreation | DONE |
| W-007 | SOCKS greeting/connect + immediate pipelined payload in same write remains intact | DONE |
| ID | Scenario | Status |
|---|---|---|
| RZ-001 | two attachments, smallest wins | DONE |
| RZ-002 | grow one attachment, PTY stays bounded by smallest | DONE |
| RZ-003 | detach smallest, PTY expands to next smallest | DONE |
| RZ-004 | reconnect preserves session + applies recomputed size | DONE |
| RZ-005 | daemon stdio RPC round-trip enforces resize semantics end-to-end | DONE |
Before declaring browser proxying complete:
DONE remove remote port probe loop and -L auto-forward orchestrationDONE remove mirror-specific routing behavior as default remote behaviorDONE replace mirroring docker assertions with proxy egress assertionsDONE keep optional explicit user-driven forwarding out of this path; no automatic mirroring remains in browser routingnone vs optional credentials).workspace.remote.configure Port Fieldsport and local_proxy_port accept integer values and numeric strings.null clears each field.invalid_params.local_proxy_port is an internal deterministic test hook to force local bind conflicts in regression coverage.StrictHostKeyChecking default (accept-new) is only injected when no user override is present.ControlMaster, ControlPersist, ControlPath) are only injected when missing.CMUX_SSH_TEST_DOCKER_HOST sets the SSH destination host/IP used by docker-backed SSH fixtures (default 127.0.0.1).CMUX_SSH_TEST_DOCKER_BIND_ADDR sets the bind address used in fixture container publish mappings (default 127.0.0.1).