acpmux: an added machine's agent asks the Mac for a GitHub action

Merged
TE
8d ago
+700-34

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_action on 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 is Origin::Peer with ConnAuth::PeerToken (not a paired device), its initialize said githubBroker: 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 carries deadlineMs, 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_action to the asking session with status allowed, denied, failed or unavailable. 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 gh keyring login on their Mac. It never leaves the Mac. The Mac runs gh itself 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. The gh that runs is the Mac's own, with null stdin, GH_PROMPT_DISABLED=1, a 60 s deadline and kill_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 is acpmux host enroll, which is what Add Machine runs. The other is a peer_add that 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 run gh themselves. host doctor --fix, a plain peer_add, a lease and a pairing never set it. Every other peer gets github.denied before 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 in remote_guard. A request goes up only a link that the Mac opened with a PeerToken and that said githubBroker: true.
  • What may be asked: five fixed gh commands: repo view, pr view N, issue create, issue comment N and pr create --draft. Every caller value goes in --flag=value form, so a title like --repo=evil/x stays a title. The repo is owner/name with 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 gh command, or reach repos the person's login cannot.
  • Limits: 4 actions at a time per peer (github.busy past that), 60 s each on the Mac, 64 KiB of output and of each body (acpmux gh reads no more of a body file than that). Each request carries a deadlineMs that the Mac checks before it runs anything (github.expired once 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_action event 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 push of 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_does FAILED. 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 05c989a with this head's test): a_mac_lends_from_its_watch_and_each_action_says_when_it_stops_waiting FAILED 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 1b8dc27 on the older base: clippy -D warnings was clean. Unit tests github_broker/github_owner/client_request/remote_guard (18) passed, and the integration tests github_broker (5), remote_guard (7), peer_integration (50), pairing (8) and remote_guard_peer (13) passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40659655824
  • After, at 21f99737, rebased onto #26756, where _acpmux/watch moved to list_reply::watch and 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 tests github_broker, remote_guard, remote_guard_peer, peer_integration, web_token_exposure, pairing and peer_lease. Run: https://github.com/manaflow-ai/cmux/actions/runs/40675852411
  • After, at 3cfadce (only part 1 below changed): the same checks passed, plus start_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: 0a57249 with this head's tests. a_request_in_order_never_passes_an_answer_waiting_for_room and an_action_the_mac_starts_ends_before_the_machine_stops_waiting_whichever_clock_is_ahead FAILED. 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_sent FAILED on the round-9 head. Run: cmux-ci fleet job bb08304ec56e20e7106b29a5.
  • 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 job 2966f854849a9fb61c31a86e.
  • After, at 2070e10 (a merge of part 1's round-10 fixes; this part's own changes are the same as at 39ecbad): cargo fmt --check, clippy -D warnings, the unit tests and the integration tests passed. Run: cmux-ci fleet job 482317e0106e4fb8ec511948.
  • After, at c772009 (a merge of part 1's round-11 fix): the same checks passed. Run: cmux-ci fleet job b425cba38d895111ec1a0b93.
  • After, at this head 3d035a1 (feat-cmux-next with #24797 merged in; this part's own changed lines are the same as at c772009): 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