acpmux: a peer's dispatch runs here and names none of this host's peers
A peer daemon's connection (Origin::Peer, paired devices included) learns nothing about this host's other peers (#26823, #26860, #26881, #26887). _acpmux/dispatch still leaked them in three ways:
- Over budget. When this host was over its CPU/RAM budget, the reply and the brief row carried
suggestedHost: the connected peer with room. host: auto. The placement engine could put the brief on one of this host's peers. The reply's brief then named that peer, and when no host had room the error repeated the engine'swhy.- A named host. The answer was
no peer "X",peer "X" is not connected,dispatch to X failed: ā¦, a role refusal from the fleet registry, or the forwarded reply. Each one told the caller whether this host has that peer and whether it is up.
For a peer connection, the brief now runs on this host, as a forwarded brief already does:
auto(the default) weighs no peer. The brief runs here.local, or this host's own name, runs here.- Any other host gets one refusal:
invalid_params,reason: dispatch.peer_host, "a peer's dispatch runs on this host only". It is the same answer whether that peer exists, is up, or is down. Nothing is looked up or forwarded. - Over budget, the reply keeps
warning(host-budget: <this host> over its CPU/RAM budget: ā¦), which covers this host only.suggestedHostis null, and the brief row is stored without one. - A replayed brief (same
briefIdoridempotencyKey) comes back only if this host ran it, meaning its session is one of this host's, open or closed (Hub::brief_ran_here). It comes back withoutsuggestedHost. A row for a brief that one of this host's peers ran is kept here with that peer's name inhost,targetandto, so it is refused with one answer (reason: dispatch.brief_taken) whichever peer ran it.
Local, app and Web connections keep the full answer: placement, forwarding and suggestedHost. This follows the admission pattern from #26887 (Ask::peers).
Not changed here: the brief ledger's other methods, _acpmux/briefs, _acpmux/steer and _acpmux/report, still list and act on rows whose host is one of this host's peers when a peer connection calls them. That is a separate surface, left for its own PR.
Test
tests/peer_dispatch_origin.rs. The hub runs in process. Its budget is set through ACPMUX_HOST_BUDGET (any load and less than all RAM free counts as over), so this host is over budget. A fake peer reports a fixed health reading with room, so the peer is not. The test checks:
- the unix socket gets
suggestedHost: "elsewhere"; - a peer connection's
localandautodispatches run here (brief.host: "local"), keep this host's warning, and never mentionelsewhere; - a replay of the local brief over the peer connection never mentions
elsewhere; - replaying a brief the unix socket dispatched to
elsewhereis refused as taken, and the refusal never mentionselsewhere; host: "elsewhere"andhost: "nosuch-peer"get the same refusal.
Before the fix (this PR's final test file, with requests.rs and hub/dispatch.rs as on base; cmux-ci run https://github.com/manaflow-ai/cmux/actions/runs/40730753333), it fails:
panicked at crates/acpmux/tests/peer_dispatch_origin.rs:176:9:
a peer was told of this host's peer: {"brief":{... "host":"local", ... "suggestedHost":"elsewhere", ... "warning":"host-budget: ip-17... over its CPU/RAM budget: ..."}, ...
At the head (f9681211fbac; cmux-ci run https://github.com/manaflow-ai/cmux/actions/runs/40731429127), it passes, along with the neighbouring suites:
tests/inherit_start.rs test result: ok. 13 passed; 0 failed
tests/peer_admission_origin.rs test result: ok. 1 passed; 0 failed
tests/peer_dispatch_origin.rs test result: ok. 1 passed; 0 failed
tests/peer_state_origin.rs test result: ok. 2 passed; 0 failed
acpmux --lib dispatch test result: ok. 3 passed; 0 failed
Risk: low. The change only affects peer-origin _acpmux/dispatch. Replaying a brief whose local session has since been deleted from the store is also refused as taken. A peer that used to send a non-forwarded dispatch naming one of this host's peers is now refused. Daemons always mark what they forward (forwarded: true), which still runs here as before. Nothing in the tree sends that request.
Agent session: agent session
š¤ Generated with Claude Code