Replies: 1 comment
|
The reconnect source should be treated as a hint, not as the recovery identity or the state transition itself. Wake, network-path change, Tailscale backend state, and reachability can all fire repeatedly and out of order. I would model three stable identifiers separately:
All availability signals can feed one coalescing reconnect coordinator. It should reconcile the authoritative daemon session first, then transition through offline → reconnecting → attached or terminal, with one in-flight attempt per workspace and the existing bounded backoff. UI and CLI should observe that state machine rather than initiate independent retries. For the SSH fallback, cache only the observed transport result with an expiry. Do not turn one transient Tailscale SSH denial into a permanent downgrade; retry the preferred path on a later explicit connection or policy/network change. This identity/state separation is what lets Better Agent reconnect persistent native Claude, Codex, and Gemini sessions after frontend, backend, or transport interruption without rebuilding the workspace. I maintain it; the project is source-available and free for non-commercial use, while commercial use requires separate permission: https://github.com/ofekron/better-agent AI-assistance disclosure: this comment was drafted by Codex under the maintainer’s authorization and reviewed in Better Agent. |
Uh oh!
There was an error while loading. Please reload this page.
Native tailnet transport for cmux, with automatic session recovery across client disconnects
TL;DR
I use cmux to run agents on hosts across my Tailscale network, reaching them today with generic
cmux ssh. The tailnet is visible to the network stack, but not to cmux as a first-class remote-workspace transport. When my laptop sleeps, changes networks, or roams, the SSH channel can drop. cmux now has bounded SSH reconnect policy and manual reconnect controls, and it also has persistent remote PTY machinery, but the desktop remote-workspace path still has no tailnet-aware host discovery, identity-addressed launch path, or network-return trigger.This proposes a native tailnet transport for cmux remote workspaces: hosts addressed by MagicDNS identity, authenticated through Tailscale, surfaced as a first-class
remote.transportvalue, and wired into the existing remote-workspace reconnect and persistent daemon paths. The primary use case is automatic recovery: when the device running cmux drops and its network returns, cmux reconnects to the same tailnet host identity and reattaches the durable remote session without a manual step.The first functional subset is intentionally small: a tailnet host-list launcher and
cmux tailscale ssh <host>quick connect. That gives immediate value while setting up the transport identity needed for bounded automatic recovery.Problem or motivation
Today, desktop cmux reaches tailnet hosts through generic SSH. That works, but it treats Tailscale as an opaque network underneath SSH. Users type or remember a host, cmux records an SSH remote configuration, and reconnect policy operates against an SSH endpoint. cmux does not yet know how to list tailnet peers, launch by stable MagicDNS identity, or trigger reconnect when the local tailnet becomes available again.
The expensive failure is not just the network drop. The expensive part is losing the running agent context. Long-running Claude Code, Codex, or similar agent sessions are valuable because of the state they have accumulated. If the client-side network goes away, the target behavior should be to keep work alive on the remote host and reattach when the client returns.
Existing cmux work is close to this.
cmux sshalready creates remote workspaces, configuresworkspace.remote.configure, usescmuxd-remote, and can preserve remote PTY sessions viapreserve_after_terminal_exitandpersistent_daemon_slot.workspace.remote.reconnectexists, and PR #5767 added a bounded SSH reconnect policy that suspends after repeated unreachable probes and exposes manual reconnect. Issue #2692 documents the earlier failure mode where relay drops led toinvalid_state: Remote workspace is not configured.A tailnet-native transport should layer on that work rather than replace it. The difference is that Tailscale gives cmux a stable device identity, local peer inventory, and clear client-side network-availability signals.
Goal
Make the tailnet a native connection protocol in cmux, addressed by tailnet identity and authenticated through Tailscale, rather than only an opaque route used by generic SSH.
Concretely, a native tailnet transport means:
cmuxd-remotemachinery.Proposed solution
The immediate proposal is the native transport and quick-connect surface. The durable-session and automatic-reconnect pieces are follow-up alignment points with existing remote-session work, not a request to land one large change all at once.
Subset 1: native transport and quick connect
Add a tailnet-aware connection mode for desktop remote workspaces.
User-facing shape:
cmux tailscale listcmux tailscale ssh <host>Discovery shells out to
tailscale status --jsonand reads the local node's visible peers. The parser should handleStatus.Self,Status.Peer, and peer fields such asDNSName,HostName,TailscaleIPs,OS, andOnline, matching Tailscale's publicipnstatestatus shape. The Tailscale CLI documentation warns that JSON output can change, so this parser should be isolated and covered with fixtures.Per device, the launcher should show the MagicDNS short name, FQDN, Tailscale IP, OS, and online state when available.
Subset 2: durable remote sessions
As follow-up work, use the existing durable remote-session substrate.
cmuxd-remoteis real, and current tests showpty.session,pty.session.token, andpty.session.persistent_daemoncapability paths. On the cmux side, persistent remote PTY behavior is controlled throughpreserve_after_terminal_exitand a sanitizedpersistent_daemon_slot.The tailnet transport should use that path consistently. It should not introduce tmux as a dependency or invent a separate persistence layer.
Subset 3: automatic reconnect on network return
As another follow-up, trigger reconnect when the client-side network becomes available again:
This should call the existing reconnect path rather than rebuilding the workspace.
workspace.remote.reconnectalready exists, and PR #5767 already made SSH reconnect policy bounded with manual fallback. The tailnet feature should add the missing tailnet-aware trigger and stable identity, not reintroduce unbounded retries.A manual one-action reconnect remains the fallback.
Architecture and decisions
D1. Consume the local node only.
cmux reads from the user's already-running, already-authenticated Tailscale node. It does not manage tailnet membership, generate auth keys, provision nodes, or store Tailscale credentials.
If the local node is logged out, cmux can hide the launcher or offer a soft prompt to run the normal interactive
tailscale upflow. It should not hold an auth key.D2. Read via the
tailscaleCLI, not a direct daemon socket.Discovery should shell out to
tailscale status --json.This is deliberate. Tailscale documents three macOS variants: the standalone GUI app, the App Store GUI app, and the open-source CLI-only
tailscaleplustailscaleddistribution. Tailscale'stailscaleddocs also note that only the non-GUI variant has the normaltailscale/tailscaledsplit. The GUI builds bundle GUI, daemon, and CLI behavior differently.Tailscale's own Darwin
safesocketcode also shows the macOS GUI path using a loopback port and token, including thesameuserproofmechanism. cmux should not reimplement that discovery. The CLI already knows how to reach the local node.D3. One socket/API path, two clients.
The CLI and app UI should share the same implementation.
cmux tailscale list,cmux tailscale ssh <host>, and the GUI dropdown should call the same socket-backed path so agents can automate it and the UI does not drift.D4. Native tailnet transport: Tailscale SSH first, standard SSH fallback.
Add a new
WorkspaceRemoteTransportraw value for tailnet or tailscale. Current main has onlysshandwebsocket, so this is a real schema/snapshot change.cmux tailscale ssh <host>should attempt Tailscale SSH first, then fall back to standard SSH to the same MagicDNS identity when Tailscale SSH is not available or not permitted. This should be outcome-driven. Tailscale issue #7764 is still open and tracks the lack of a reliable programmatic way to check whether Tailscale SSH is enabled.The winning path can be remembered per host, but the security posture should remain Tailscale-first.
D5. Automatic reconnect is bounded and client-event driven.
Reconnect should fire on client network-availability events and local Tailscale recovery signals. It should not attempt to detect every possible remote failure. It should reuse cmux's bounded reconnect policy and manual reconnect controls.
D6. Host list is the launcher, not the sidebar.
The sidebar should show active connections and their state. It should not become a persistent tailnet device browser. The host list belongs in the new-workspace dropdown and command palette.
D7. Optional and quiet when absent.
When Tailscale is absent, logged out, or unavailable, the feature should disappear or degrade softly. No hard dependency.
Security and privacy notes
The core feature is credential-free:
Reconnect should re-authenticate over Tailscale and reattach an existing daemon-managed session. It should not run an arbitrary resume command as a side effect of network return.
Tailscale ACLs and SSH authorization remain the source of truth for who can connect. cmux should only show what the local node can already see and attempt connections the user could already attempt from the shell.
If cmux ever adds provisioning later, that should be a separate proposal. Tailscale OAuth clients can create tagged auth keys, but ephemeral and preauthorized behavior depends on explicit flags and method. Tailscale issue #10702 also documents an important limitation: OAuth client tag restrictions constrain generated auth keys, but not all device API calls. That is outside this proposal's scope.
Implementation notes
A possible implementation path that fits current repo conventions:
SourcesandTests, following current package structure such asPackages/CmuxRemoteWorkspace,Packages/CmuxRemoteDaemon, andPackages/CmuxControlSocket.WorkspaceRemoteTransportcase and update snapshot/config decoding carefully because raw values are persisted.tailscale status --json; avoid blocking UI paths.ForEach/Listboundaries.Loggerstyle for logging.--json.CHANGELOG.mdunder### Added.The
cmux themescommand from PR #1334 is a good structural precedent: a noun subcommand withlist, mutation verbs,--json, app-side observation/reload behavior, and socket integration.Alternatives considered
cmux ssh user@hostmanually. This works, but it does not give cmux tailnet peer discovery, stable tailnet identity, or network-return reconnect triggers.Scope
In scope for the first buildable slice:
tailscale status --jsoncmux tailscale listcmux tailscale ssh <host>In scope as follow-up alignment with existing remote-session work:
cmuxd-remoteOut of scope:
Open questions
tailscaleortailnet?tailscale statusbackend state, or MagicDNS reachability?cmux tailscale listand launcher UI, or includecmux tailscale ssh <host>in the same implementation pass?Related work in cmux
workspace.remote.reconnectfailing withinvalid_state: persistent daemon and reconnect motivation.cmux attachacross instances.cmux ssh-style agent sessions.External references
If there is appetite, I would start by validating the host-list launcher and CLI shape before proposing deeper implementation details.
All reactions