acpmux: the GitHub broker's fixed actions and the Mac's checked gh runner
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, that includes GitHub working with no gh auth login there.
Summary
The core of the GitHub broker, split out of #24797 to keep each PR under about 800 lines. It adds src/github_broker.rs and its unit tests, with no caller yet. #24797 adds the peer link that serves it.
The stack: this PR, then #24797 (the Mac serves it on the peer link), #26784 (the added machine asks), #26785 (Add Machine turns it on; acpmux gh), #26914 (the app lends its cmux sign-in), #26909 (App tokens when gh has no keyring login) and #26910 (the Swift lender).
- The actions: the vocabulary is five fixed actions:
read(repository or pull request),issue,commentanddraft_pr. Unknown fields are refused (deny_unknown_fields). - The checks:
- The repo is
owner/name, with no leading-and no path parts. - The branch has no
:, no.., no@{and no leading-. - Titles and bodies are bounded to 64 KiB.
- A peer the Mac's config does not lend to is refused (
github.denied) before anything runs.
- The repo is
- The one
ghcommand each action runs:- Every caller value goes in
--flag=valueform. - It runs with null stdin and
GH_PROMPT_DISABLED=1, a 60 s limit andkill_on_drop. - It gets no GitHub token or host from the daemon's env (
login_env):GH_TOKEN,GITHUB_TOKEN,GH_ENTERPRISE_TOKEN,GITHUB_ENTERPRISE_TOKEN,GH_DEBUGandDEBUGare removed andGH_HOSTis github.com, so it uses the Mac's keyring login on github.com and nothing else. - stdout and stderr are each read up to a bound, and the output is capped at 64 KiB.
- Any GitHub token in the output (
github_pat_,ghp_,gho_,ghu_,ghs_,ghr_, whole or cut off) is replaced with[redacted]first, and then the output is cut to its limit, so it never grows past it.
- Every caller value goes in
- Slots per peer name: 4 at a time, shared across reconnects (
owner_slots). - The audit row: it holds the action, repo, branch, number and outcome, and never a title, body or output.
Threat model
This is the whole feature's threat model; the parts that need the link are in #24797.
- The token never leaves the Mac.
ghreads it from the Mac's keyring. The answer holds onlygh's bounded output, with any token in it cut out. - No other command can be asked for. Only these five
ghcommands can run, so there is no merge, close, delete,api,auth, push or workflow. - Caller values cannot become flags. Every caller value is checked again on the Mac and passed as a flag value, so it cannot become a flag or a different repository.
- A slow or chatty
ghcannot grow the daemon or run past 60 s. - Residual: a legacy unprefixed (40-hex) token is not redacted: it cannot be told from a commit SHA, which
gh pr viewprints. None of the five commands print the login's token, and GitHub has issued only prefixed tokens since 2021.GH_CONFIG_DIRis kept: it is the person's own pointer to theirghlogin, set on their Mac, which no peer can change. - No new trust: no GitHub App, secret or permission is added.
Tests (cmux-ci, Linux)
- Unit tests in
src/github_broker_tests.rscover:- the vocabulary and refusals;
- the fixed arguments;
- the bounded and redacted output (
every_github_token_is_cut_out_whole_or_cut_off); - a peer the Mac is off for gets nothing (
the_mac_lends_nothing_to_a_peer_it_is_off_for); - an audit row holding no title, body or output;
- a
ghenv with no daemon token or host (the_gh_the_broker_runs_gets_no_token_or_host_from_the_daemon).
- Before: there is no broker on the base. End to end, #24797's
tests/github_broker.rsfails there, because the Mac answersMethod not found(run https://github.com/manaflow-ai/cmux/actions/runs/40651696477).- For the redaction review fix,
f2e68a4's parent with the new test failed: agho_token on stderr and agithub_pat_token on stdout went back (fleet job7f032bd11937b8783cb7ebda).
- For the redaction review fix,
- Before, for review round 1 (the env):
939021ahad nologin_env, so the unit test does not build there. The behaviour is shown end to end by #24797's test with a decoyGITHUB_TOKENin the daemon's env: on the old core the stand-inghsaw the token and noGH_HOST(the_mac_runs_an_added_machines_action_with_its_own_gh_and_no_other_peersFAILED). Run: cmux-ci fleet job39ec024a998fbd5ccc41789a. - After, at
c0d5122:cargo fmt --check, clippy-D warningsandcargo test --lib -- github_broker(8) passed. Run: cmux-ci fleet job07701559dc78de54d78e69bb. - Before, for review round 2 (output growth):
c0d5122withoutput_full_of_tokens_stays_within_its_limitFAILED: an output of 64 Ki short tokens grew past the limit once they were replaced. Run: cmux-ci fleet jobd4d941cfce7361fa6ce4d1a3. - After, at this head
2867d0d: the same checks passed (9 unit tests). Run: cmux-ci fleet jobe4c1def704aa822b94851be3.
Changelog
- None (no caller yet).
No capture: nothing is drawn.
Risk
risk:high (runs gh with the person's login). This code was reviewed inside #24797 through nine Codex gpt-6-astra rounds. Round 1 on its own, at 939021a: BLOCK (the env, fixed above). Round 2, at c0d5122: BLOCK. Output could grow past its limit (fixed above); the legacy-token and GH_CONFIG_DIR points are answered under Residual. Round 3, at this head:
Review (codex gpt-6-astra, high, fast): SHIP at 2867d0d24f1c67f756c8406926d24f9210c0e0df ("No findings in BASE..HEAD.")
Agent session: agent session
š¤ Generated with Claude Code