acpmux: an added machine's agent asks the Mac for a GitHub action
Leo's ask (cmuxterm-hq#4770): "you can just not choose a project ... you just get a little green dot ... as if you were working on your own computer, but now it's remote."
Summary
This is part 2 of 3, the added machine. An agent session on an added machine asks for a GitHub action, and its daemon sends the request up the link of a Mac that lends GitHub to it (part 1, #24797, merged). It builds on #24797.
_acpmux/github_actionon the added machine's local socket. The caller must be a caller-bound acpmux session (_meta.acpmux.callerSession). Its id is filled in from the caller, and the request is checked against the same vocabulary as on the Mac.hub/github_owner.rs: a link counts as an owner only when the connection isOrigin::PeerwithConnAuth::PeerToken(not a paired device), itsinitializesaidgithubBroker: true, and this machine has queued its answer to the Mac's_acpmux/watch. The Mac opens its side when it reads that answer (part 1), so every action goes out after it. The newest open one is used; a link is listed once and at most 16 are kept.server/client_request.rs: the daemon can now send a request to a connected client and wait for its reply, within a budget, until the connection ends. Responses from the client settle it. At most 8 wait on one connection. Each GitHub request carriesdeadlineMs, and the Mac refuses one that has passed.- With no lending Mac connected, the answer is
github.no_owner("no Mac that lends GitHub to this machine is connected (add this machine from the Mac with Add Machine)"). - Each action appends
github_broker_actionto the asking session with statusallowed,denied,failedorunavailable. The event holds no title, body or output.
Threat model (the whole feature: this PR and its two stacked follow-ups)
- What is lent: the person's
ghkeyring login on their Mac. It never leaves the Mac. The Mac runsghitself and sends back only the command's output, capped at 64 KiB. No token goes into a request, a reply, the peer's env, argv, disk or logs. Theghthat runs is the Mac's own, with null stdin,GH_PROMPT_DISABLED=1, a 60 s deadline andkill_on_drop. - Who may ask: on the Mac, only a peer whose config has
githubBroker: true, and only on a link where the peer has answered the handshake's watch while serving the Mac as a peer (not as Web). Anything sent before that, or on any other link, is refused. Two things set the flag. One isacpmux host enroll, which is what Add Machine runs. The other is apeer_addthat names it, or the config file. Both go over the Mac's unix socket (0600, in a 0700 dir), so only the person's own processes can reach them, and those can already runghthemselves.host doctor --fix, a plainpeer_add, a lease and a pairing never set it. Every other peer getsgithub.deniedbefore anything is spawned. On the added machine, only a caller-bound agent session on the local unix socket may ask. Web, LocalApp and Peer origins are refused inremote_guard. A request goes up only a link that the Mac opened with a PeerToken and that saidgithubBroker: true. - What may be asked: five fixed
ghcommands:repo view,pr view N,issue create,issue comment Nandpr create --draft. Every caller value goes in--flag=valueform, so a title like--repo=evil/xstays a title. The repo isowner/namewith no leading-and no path parts. The branch has no:(a fork's branch), no.., no@{and no leading-. Unknown fields are refused (deny_unknown_fields), not ignored. The Mac checks every request again itself and does not trust the added machine's check. There is no merge, close, delete,api,auth, push or workflow action. - What an attacker on the added machine gets: root or any agent there can ask for those five actions as the person, for as long as the Mac keeps the link open. This is the same trust as ssh-agent forwarding to a machine you added. It cannot get the token, run any other
ghcommand, or reach repos the person's login cannot. - Limits: 4 actions at a time per peer (
github.busypast that), 60 s each on the Mac, 64 KiB of output and of each body (acpmux ghreads no more of a body file than that). Each request carries adeadlineMsthat the Mac checks before it runs anything (github.expiredonce passed), the added machine waits at most 95 s, and at most 8 requests wait on one link. At most 16 lending links are remembered. - Records: each action appends a
github_broker_actionevent to the asking session on the added machine. It holds the action, repo, branch, issue number, status and error code, and never the title, body or output. The Mac logs one tracing line per action with the same fields. - Residual:
gh's stderr is relayed back, bounded, so a failed action can show a path from the Mac. If two Macs lend to one machine, the newest link is used.git pushof local commits is not brokered: the agent's branch must reach GitHub some other way before a draft PR can name it.
Tests (cmux-ci, Linux)
- Before (part 1's head with only this PR's
tests/github_broker.rs):an_added_machines_agent_uses_the_macs_gh_login_and_no_other_peers_doesFAILED. The added machine answered{"code":-32601,"message":"Method not found: _acpmux/github_action"}. Run: https://github.com/manaflow-ai/cmux/actions/runs/40651699484 - Before, for the review fixes (the lane part at
05c989awith this head's test):a_mac_lends_from_its_watch_and_each_action_says_when_it_stops_waitingFAILED after 30 s, because the action went to a Mac whose handshake had not finished. Run: https://github.com/manaflow-ai/cmux/actions/runs/40659661424 - After, at
1b8dc27on the older base: clippy-D warningswas clean. Unit testsgithub_broker/github_owner/client_request/remote_guard(18) passed, and the integration testsgithub_broker(5),remote_guard(7),peer_integration(50),pairing(8) andremote_guard_peer(13) passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40659655824 - After, at
21f99737, rebased onto #26756, where_acpmux/watchmoved tolist_reply::watchand its reply is built asynchronously. The owner is now registered after that call, and the test waits for the watch's reply before asking. Clippy was clean, and the unit tests passed, as did the integration testsgithub_broker,remote_guard,remote_guard_peer,peer_integration,web_token_exposure,pairingandpeer_lease. Run: https://github.com/manaflow-ai/cmux/actions/runs/40675852411 - After, at
3cfadce(only part 1 below changed): the same checks passed, plusstart_mode. Run: https://github.com/manaflow-ai/cmux/actions/runs/40694528381 - After, at
38c8b3c: the same checks passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40706046323 - After, at
0a57249: the same checks passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40720644836 - Before, for review round 8:
0a57249with this head's tests.a_request_in_order_never_passes_an_answer_waiting_for_roomandan_action_the_mac_starts_ends_before_the_machine_stops_waiting_whichever_clock_is_aheadFAILED. Run: https://github.com/manaflow-ai/cmux/actions/runs/40727383874 - Before, for review round 9:
a_request_in_order_to_a_reader_that_is_gone_is_not_sentFAILED on the round-9 head. Run: cmux-ci fleet jobbb08304ec56e20e7106b29a5. - After, at
39ecbad: the same checks passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40734793852 - After, at
40d1349(a merge of part 1's round-9 fixes): the same checks passed. Run: cmux-ci fleet job2966f854849a9fb61c31a86e. - After, at
2070e10(a merge of part 1's round-10 fixes; this part's own changes are the same as at39ecbad):cargo fmt --check, clippy-D warnings, the unit tests and the integration tests passed. Run: cmux-ci fleet job482317e0106e4fb8ec511948. - After, at
c772009(a merge of part 1's round-11 fix): the same checks passed. Run: cmux-ci fleet jobb425cba38d895111ec1a0b93. - After, at this head
3d035a1(feat-cmux-next with #24797 merged in; this part's own changed lines are the same as atc772009):cargo fmt --check, clippy-D warnings, the unit tests and the integration tests passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40789554757
Changelog
none (nothing is lent until part 3)
No capture: nothing is drawn; no webview or Packages/macOS file changes.
Risk
risk:high (socket and relay policy). An independent Codex gpt-6-astra review (high, fast) covers this head.
Round 3, at 1b8dc27: SHIP. Round 4, at 9d5876b after the rebase: SHIP. Round 5, at 21f9973: SHIP. Round 6, at 3cfadce: SHIP. Round 7, at 38c8b3c: SHIP. Round 8, at 0a57249: BLOCK (a GitHub request could pass a watch answer still waiting for room, and a Mac clock behind the machine's cut the wait short); both fixed. Round 9, at d0b90b0: BLOCK (a watch answer or request to a reader that is gone counted as sent); fixed. Round 10, at 39ecbad: SHIP. Round 11, at 40d1349: SHIP. Round 12, at 2070e10: BLOCK (it said host_str() drops IPv6 brackets, which it does not; part 1's check is bracket-agnostic now anyway). Round 13 of this part, at c772009: SHIP. Round 14, at this head 3d035a1:
Review (codex gpt-6-astra, high, fast): SHIP at 3d035a19833d1cad2f7c5e61dbced44ba2e461d2. "No findings. The full BASE..HEAD diff has identical added/removed lines to 9d7fd4d..c772009; the merge dropped or duplicated nothing."
Agent session: agent session
š¤ Generated with Claude Code