acpmux: an ssh peer that connected before the remote wrote its peer token no longer stays served as Web
Summary
deputy-2 (cosmic-gray), verbatim: "from cosmic-gray, acpmux send air-blue/gate-rules ... is refused with "This chat runs with your Mac's Claude permissions. Start a new chat from this device to control it remotely." The session was imported onto air-blue (a Mac) by acpmux session import and runs bypassPermissions; sending through air-blue's own daemon works. Coordinators on cosmic-gray have to steer lanes on other hosts, so a peer that the host lists in acpmux host (same user, ssh-authenticated) should be allowed to send, or the import should record the session as remotely controllable."
Cause. The policy is already what deputy-2 asks for. A proven peer daemon (Control::Peer) is exempt from the Mac-permissions refusal (web_control_verdict returns early for anything but Control::Web). The link from cosmic-gray to air-blue wasn't proven: acpmux host ls on cosmic-gray showed air-blue ⦠servedAs: remote while cmux16 and huge-blue showed peer. air-blue's daemon had just restarted for the #23880 rollout, and cosmic-gray reconnected inside the window where the new daemon is listening but hasn't yet written run/peer.token; the old daemon had already removed its own. In the air-blue log, the listener came up at 02:22:05 and the token was written at 02:22:08, and cosmic-gray connected at 02:22:10 with an older timestamp still in flight. The ssh token read came back empty, so the connection was served as Web. It stays Web, holding every send to the Web's rules, until the link next drops.
Fix (narrow, no policy change). Two parts, both on the ssh:// path only:
- Before connecting: if the token read finds no peer token, the peer reads again after 1 s, 2 s and 4 s, then connects with whatever it has. This covers the window where a starting daemon is listening but hasn't written
run/peer.tokenyet. - Right after
initialize, before any watch or attach: if the remote served this link as Web (remote), the peer reads the token once more. If the remote now has a token this connection didn't present (it restarted between the read and the handshake), the link is dropped as an ordinary error. The normal disconnect path then runs (pending requests fail,Disconnectednotice, backoff), and the next connect presents the new token. Because nothing has been watched, attached or sent on the link yet, nothing is lost or replayed.
A remote that is too old to make a token, or one that serves this daemon as Web for another reason (it has the same token we presented), stays Web, as before. That costs at most five ssh reads per connection and never loops. peer_auth::is_peer still needs the launch's token and a loopback transport, and the Web refusal (#23792 / D13) is untouched.
I rejected deputy-2's option (b), recording imported sessions as remotely controllable. It would widen the Web rule for every Web client (paired devices and the dashboard), not just peers.
Remote relay analysis (skills/cmux-socket-policy/references/remote-relay-authorization.md)
- Local command or content execution: none added. The extra token reads are the same
read_config_argvssh command the peer already runs at every connect (fixed argv, the destination parsed byssh_target, which refuses anything option-shaped). There are at most four extra reads per connection: three while the remote has no token, and one if the remote served the link as Web. - Access to unowned objects: none added. A link becomes Peer only by presenting the remote's own launch token over a loopback/ssh transport, which is the existing proof. The fix makes the daemon present that proof sooner; it can't create one.
- Local-state exposure: none. The token is read over the user's own ssh access, as before. It's never logged or returned, and it never crosses plain ws:// (
carries_peer_tokenis unchanged; this path is ssh-only). - Policy tests: the new
tests/peer_token_race.rsdrives a real remote daemon through a stand-insshwhose first token read finds none. The existingpeer_origin.rs(token plus dashboard token makes Peer; the dashboard token alone stays Web; no RPC returns the token),peer_transport.rs(the token never crosses plain ws:// to a non-loopback host),ssh_peer_injection.rsandremote_guard_modes.rs(a Web connection is still refused on a Mac Claude session) all pass on the head.
Testing
Focused command, run on huge-blue:
cargo test -p acpmux --test peer_token_race. It has three cases, each a real remote daemon behind a stand-in ssh: the remote has no token on the first read (late), the remote shows the previous run's token on the first read (stale), and the remote never has a token (never).
- Base
peer.rsat merge-base 986cfe36476, with this test file: FAILED, 3 of 3.lateandstale:the peer is not served as peer: {⦠"servedAs":"remote" ā¦}after 60 s.never:reads of a remote with no token left: 1 right: 5. - Head: ok, 3 passed in 12.6 s.
Also run on the head: cargo test -p acpmux --test peer_origin --test peer_transport --test ssh_peer_injection --test remote_guard_modes --test peer_degraded --test peer_lease --test peer_canonical_endpoint (all ok), cargo test -p acpmux --lib peer (57 passed), cargo clippy -p acpmux --all-targets and cargo fmt --check (clean).
Review: Codex at xhigh reviewed 6e48a42 and returned FIX. Its points were that a relink after the handshake could lose or replay queued RPCs and skipped the disconnect notice, and that a token rotated after a non-empty read was never retried. Both are addressed in 0c0b6e1 (above). Re-review is on the final head.
Observed / Not run
- Observed: the red then green above, and
servedAs: remotefor air-blue on cosmic-gray's livehost ls. - Not run: the fix on the live hosts. It reaches air-blue and cosmic-gray once their daemons run a build that includes it. Until then, a reconnect (restarting cosmic-gray's daemon, or the link dropping) clears the stuck link.
Changelog
Fixed: a cmux machine whose peer restarted no longer stays held to remote-device rules, which refused sends to chats running with the Mac's own Claude permissions.
risk:high (peer authentication path).
Agent session: huge-blue
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.