acpmux: Add Machine lends the Mac's GitHub login; `acpmux gh` asks for it

Merged
TE
8d ago
+541-20

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 with githubBroker: 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@host is not alice@host), on the same host in any case, and the same port. host doctor --fix shares the peer step but never lends GitHub and never names the flag. Peers added any other way (peer_add by 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 of gh syntax, refuses anything else, and sends the request to the local daemon:
    • repo view [R]
    • pr view N
    • pr 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 the origin remote (one trailing .git dropped) and the head from the current branch. Each git question is bounded: 8 KiB of output and its exit within 10 s, or the child is killed. A --body-file that 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/zero or a FIFO cannot hang it. An unknown flag, an extra word or a repeated flag is refused, not dropped. When the Mac's daemon runs gh (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 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)

Changelog

  • Machines added with Add Machine can use GitHub through your Mac's gh login (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