acpmux: Add Machine lends the Mac's GitHub login; `acpmux gh` asks for it
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." On an added machine, GitHub now works with the Mac's login and no gh auth login there.
Summary
This is part 3 of 3, which turns the broker on by default for Add Machine. It builds on #24797 and #26784 (both merged).
acpmux host enroll(what Add Machine runs) adds the peer withgithubBroker: true. An Add Machine peer that is already connected but does not lend yet (added before this change) is re-added at its own URL with the flag on, keeping its saved tokens. It must be the same ssh user exactly (Alice@hostis notalice@host), on the same host in any case, and the same port.host doctor --fixshares the peer step but never lends GitHub and never names the flag. Peers added any other way (peer_addby hand, config, leases, pairing) keep their current behaviour, which is off.acpmux gh(hidden) is the agent's way in. It parses a strict subset ofghsyntax, refuses anything else, and sends the request to the local daemon:repo view [R]pr view Npr create --draft --title T [--head B] [--base B] [--body|--body-file]issue create --title T [--body|--body-file]issue comment N (--body|--body-file)- each of these also takes
--repo/-R.
The repo comes from theoriginremote (one trailing.gitdropped) and the head from the current branch. Eachgitquestion is bounded: 8 KiB of output and its exit within 10 s, or the child is killed. A--body-filethat does not arrive within 30 s (a FIFO with no writer, a stalled pipe) is refused. A--body-file(or-for stdin) is read only up to the 64 KiB limit and refused past it, so/dev/zeroor a FIFO cannot hang it. An unknown flag, an extra word or a repeated flag is refused, not dropped. When the Mac's daemon runsgh(ACPMUX_GITHUB_BROKER_OWNER=1), this command refuses to run, so it can never loop.
Threat model (the whole feature: #24797, #26784 and this PR)
- 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 2's head with only this PR's
host_peering_tests.rs):add_machine_lends_github_and_turns_it_on_for_an_older_peerFAILED withleft: Null, meaning Add Machine sent nogithubBroker. Run: https://github.com/manaflow-ai/cmux/actions/runs/40651703988 - Before, for the parser: the old shim at
c3634e0(src/bin/gh.rs) with this PR's refusal cases.a_flag_or_word_of_another_command_is_refused_not_droppedFAILED withaccepted: issue comment 42 --repo o/r --body x --edit-last. Run: https://github.com/manaflow-ai/cmux/actions/runs/40651705924 - Before, for review round 1 (doctor):
doctor_never_lends_githubFAILED on the earlier head. Run: https://github.com/manaflow-ai/cmux/actions/runs/40656450023 - Before, for review round 2 (body size): the shim before
read_boundedread a 262144-byte body whole. Run: https://github.com/manaflow-ai/cmux/actions/runs/40662427534 - Before, for review round 4 (
.git):ef20912with this head'sgh_shim_tests.rs:github_remote_urls_name_their_repositoryFAILED,o/r.git.gitgaveSome("o/r"), notSome("o/r.git"). Run: https://github.com/manaflow-ai/cmux/actions/runs/40664763467 - Before, for review round 5 (ssh user case):
b68845ewith this head'shost_peering_tests.rs.a_peer_url_matches_its_user_exactly_and_its_host_in_any_caseFAILED:!same_ssh_url("ssh://Alice@host.example", "ssh://alice@host.example"). Run: https://github.com/manaflow-ai/cmux/actions/runs/40670443483 - Before, for review round 5 (unbounded
git):b68845ewith this head'sgh_shim_tests.rsdoes not compile (bounded_outputdoes not exist;git()usedCommand::output()with no bound). Run: https://github.com/manaflow-ai/cmux/actions/runs/40670436236 - Before, for review round 6 (a
gitthat closes stdout and runs on):1fdce1awith a test whose child prints, closes stdout and sleeps 30 s.a_git_that_closes_its_output_and_runs_on_is_refusedFAILED after waiting the whole 30 s. Run: https://github.com/manaflow-ai/cmux/actions/runs/40675857139 - After, at
8b4e0c5: clippy-D warningswas clean. Unit testsgithub_broker gh_shim host_peering client_request remote_guard peer_lease catalog_reload list_replypassed, includinga_git_answer_past_its_limit_or_time_is_refused,a_git_that_closes_its_output_and_runs_on_or_a_body_that_never_comes_is_refused(a FIFO with no writer) anda_peer_url_matches_its_user_exactly_and_its_host_in_any_case. The integration testsgithub_broker,remote_guard,remote_guard_peer,peer_integration,web_token_exposure,pairingandpeer_leasepassed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40675847257 - After, at
e642fba(only part 1 below changed): the same checks passed, plusstart_mode. Run: cmux-ci fleet jobb93858e6e4e8fff653931025(linux-pool). - After, at
33db8a0: the same checks passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40706111699 - After, at
2b67b3e: the same checks passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40718978999 - After, at
6d6f4c6andb17af6c(only parts 1 and 2 changed): the same checks passed. Runs: https://github.com/manaflow-ai/cmux/actions/runs/40727850186 and https://github.com/manaflow-ai/cmux/actions/runs/40734796403 - After, at
a1494e4: the same checks passed. Run: cmux-ci fleet jobca2c07568fc0035e57213bff. - After, at
b09d9f4(a merge of parts 1 and 2's latest fixes; this part's own changes are the same as atb17af6c):cargo fmt --check, clippy-D warnings, the unit tests and the integration tests passed. Run: cmux-ci fleet job1d4028d29b510dad4e8e5e51. - After, at
0ef9bd3anddc87e7a(merges of parts 1 and 2's later heads; this part's own changed lines unchanged): the same checks passed. Runs: cmux-ci fleet joba7a15a316ed8eab9f7fdc987; https://github.com/manaflow-ai/cmux/actions/runs/40789577371 - After, at this head
f3c0871(feat-cmux-next with #26784 merged in):cargo fmt --check, clippy-D warnings, the unit tests and the integration tests passed. Run: https://github.com/manaflow-ai/cmux/actions/runs/40792849519
Changelog
- Machines added with Add Machine can use GitHub through your Mac's
ghlogin (read, issue, comment, draft PR) with no sign-in on the machine.
No capture: nothing is drawn; only the daemon and its CLI change. 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.
Review rounds 1 to 6 (codex gpt-6-astra, high, fast) each found something, and every finding is fixed above. Round 7, at 8b4e0c5: SHIP. Round 8, at e642fba: SHIP. Round 9, at 33db8a0: SHIP. Round 10, at 2b67b3e: SHIP. Round 11, at 6d6f4c6: SHIP. Round 12, at b17af6c: SHIP. Round 13, at a1494e4: SHIP. Round 14, at b09d9f4: SHIP. Round 15, at 0ef9bd3: SHIP. Round 16, at dc87e7a: SHIP. Round 17, at this head f3c0871 (feat-cmux-next with #26784 merged in; only parts 1 and 2 below it changed):
Review (codex gpt-6-astra, high, fast): SHIP at f3c0871630a624769b2df13b06567f524126c760. "No findings. The BASE..HEAD numstat matches 3d035a1..dc87e7a exactly across all changed paths; the README merge retained both sides without loss or duplication."
Agent session: agent session
š¤ Generated with Claude Code