acpmux: the GitHub broker's fixed actions and the Mac's checked gh runner

Merged
TE
8d ago
+522-0

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, comment and draft_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 one gh command each action runs:
    • Every caller value goes in --flag=value form.
    • It runs with null stdin and GH_PROMPT_DISABLED=1, a 60 s limit and kill_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_DEBUG and DEBUG are removed and GH_HOST is 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.
  • 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. gh reads it from the Mac's keyring. The answer holds only gh's bounded output, with any token in it cut out.
  • No other command can be asked for. Only these five gh commands 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 gh cannot 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 view prints. None of the five commands print the login's token, and GitHub has issued only prefixed tokens since 2021. GH_CONFIG_DIR is kept: it is the person's own pointer to their gh login, 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.rs cover:
    • 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 gh env 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.rs fails there, because the Mac answers Method 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: a gho_ token on stderr and a github_pat_ token on stdout went back (fleet job 7f032bd11937b8783cb7ebda).
  • Before, for review round 1 (the env): 939021a had no login_env, so the unit test does not build there. The behaviour is shown end to end by #24797's test with a decoy GITHUB_TOKEN in the daemon's env: on the old core the stand-in gh saw the token and no GH_HOST (the_mac_runs_an_added_machines_action_with_its_own_gh_and_no_other_peers FAILED). Run: cmux-ci fleet job 39ec024a998fbd5ccc41789a.
  • After, at c0d5122: cargo fmt --check, clippy -D warnings and cargo test --lib -- github_broker (8) passed. Run: cmux-ci fleet job 07701559dc78de54d78e69bb.
  • Before, for review round 2 (output growth): c0d5122 with output_full_of_tokens_stays_within_its_limit FAILED: an output of 64 Ki short tokens grew past the limit once they were replaced. Run: cmux-ci fleet job d4d941cfce7361fa6ce4d1a3.
  • After, at this head 2867d0d: the same checks passed (9 unit tests). Run: cmux-ci fleet job e4c1def704aa822b94851be3.

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