Living distillation of RFC #15 (Revision 2) and the Phase 0 spike findings, as actually shipped. For onboarding, see getting-started.md.
AI coding agents need GitHub access. Every conventional approach puts a GitHub credential inside the agent's environment:
| Approach | Weakness |
|---|---|
| PAT in the container | Long-lived, exfiltratable, org-wide blast radius, attribution collapses to the PAT owner |
| Token vending machine | Agent still holds a live token for its lifetime; misusable within scope |
| Per-agent fine-grained PATs | Management burden; still long-lived; single-org |
octobroker's position: the agent never holds any GitHub credential. It holds at most an octobroker API key — revocable centrally, bounded by policy, useless against GitHub directly.
octobroker is a credential-swapping reverse proxy with a default-deny policy engine, sitting between agents and two GitHub surfaces:
- MCP (
/mcp) -> GitHub's hosted MCP server (api.githubcopilot.com/mcp/), with a narrowly scoped octobroker-owned review-tool exception. Upstream schemas remain proxied verbatim;octobroker_*tools are explicit, default-deny, App-backed, repository-bound, and fail-closed audited. octobroker does not expose arbitrary GraphQL or a general custom-tool registry. - REST/GraphQL (
/{path},/graphql) →api.github.com, with PAT pooling (budget-aware selection) and in-memory read caching. GraphQL mutations pass through with the client's own token (full attribution).
agent → [authn: X-Octobroker-Key] → [session binding] → [tool allowlist]
→ [write classification] → [repo allowlist (deny-if-unresolvable)]
→ [in-flight cap] → [fail-closed audit] → forward with scoped token
→ [buffer+parse write outcomes] → audit result
Every layer is independent; a request must clear all of them.
GitHub App installation tokens are the primary credential (the PAT pool remains for REST reads and legacy setups). Decided in Phase 0 after the PAT-pooling compliance concern (GitHub ToS §H rate-limit aggregation) was raised in review:
- Minted by octobroker from the App private key (RS256 JWT → installation token), cached, auto-refreshed 5 minutes before the 1-hour expiry.
- Scoped at mint: an agent whose repo allowlist is exact entries under
one owner gets tokens minted with the API's
repositoriesparameter — GitHub itself enforces the repo boundary, independent of octobroker's argument parsing (which remains as defense-in-depth). One credential per policy envelope. - Writes never run on PATs — enforced by startup validation, not convention.
Sessions (Mcp-Session-Id) are pinned to (credential, agent) at
initialize:
- Identity never rotates mid-session; an unknown/expired session gets 404 (per MCP spec) and the client re-initializes transparently.
- A session presented by a different agent — even with a valid key — gets 403 (binding violation).
- A session cannot outlive its credential: expired App-token pins terminate the session. Provider refreshes don't disturb in-flight sessions.
- octobroker's pin cache is the sole session authority. Phase 0 measured the hosted endpoint's own session semantics as fail-open: upstream DELETE is a no-op (the session remains usable), and unknown sessions get 400 not 404. Nothing about session validity is delegated upstream.
- Pins are in-process memory → single replica while MCP is enabled; config change = restart = all sessions revoked (the current revocation story).
Per-agent, default-deny, enforced at the proxy and mirrored upstream:
[[mcp.agents]]
id = "my-bot"
keys = ["env:KEY_CURRENT", "env:KEY_NEXT"] # rotation: both valid
tools = ["issue_read", "create_issue"] # exact names, default-deny
repos = ["my-org/repo-a", "my-org/*"] # exact or owner wildcard- The tool allowlist is injected upstream as
X-MCP-Tools(exact per-tool filtering, discovered and verified in Phase 0). We deliberately do NOT useX-MCP-Toolsetsfor enforcement: Phase 0 found invalid toolset names are silently ignored — fail-open. - All client-supplied
X-MCP-*headers are stripped; the upstream header set is built from scratch (a client cannot widen its own permissions). - Repo authorization is deny-if-unresolvable: a repo-restricted agent's call whose arguments name no repository is rejected.
- Write classification is rule-based and conservative: only
get_*/list_*/search_*/*_readnames are reads; everything else, including unknown names, is a write.
enable_writes = true requires — validated at boot, or the process refuses
to start:
[[mcp.agents]](writes never exist in network-trust mode)[mcp.github_app](writes never run on PATs)[mcp.audit](writes are never unaudited)
The audit trail is fail-closed: a pre-flight JSONL record is fsync'd
before the call is forwarded; if it cannot be persisted, the write is
rejected (503) without side effects. The result record captures the MCP
tool outcome — result.isError arrives inside HTTP 200/SSE, so transport
status alone is never treated as success. Argument values are never logged
(key names + resolved repo only). octobroker never auto-retries a forwarded
call; ambiguous outcomes are recorded as undeterminable and surfaced to the
caller.
- Single replica (MCP): session pins are in-process. Horizontal scaling (shared session state) is Phase 3 (#18).
- No rate-limit headers on the hosted MCP endpoint (Phase 0 finding) — budget accounting stays REST-driven; per-agent quotas are Phase 3.
- GitHub-side write attribution is the App identity, not the individual agent. The octobroker audit log is the per-agent ledger; GraphQL mutation passthrough remains the right path when GitHub-side per-human attribution matters.
- Contract-drift risk: the hosted MCP surface (tool names, headers like
X-MCP-Tools) is partially undocumented. A daily e2e canary exercises the full flow — including real App-token minting — against the live endpoint. - Phase 3 (#18): SigV4/STS secretless agent auth via a stdio shim, horizontal scaling, quotas/circuit-breaking, cache authorization.
| Decision | Where | Why |
|---|---|---|
| Proxy official schemas, define no tools | RFC Rev 1 | Zero schema maintenance; GitHub owns the surface |
| GitHub App primary, PAT pool legacy | RFC Rev 2 / #22 | ToS compliance, short-lived creds, scoped mint |
X-MCP-Tools not X-MCP-Toolsets |
#22 finding F | Toolsets fail open on invalid names |
| octobroker is session authority | #22 finding I | Upstream DELETE is a no-op |
| 404 on unknown session, never rotate identity | #20 review | MCP spec; no silent actor switching |
| Buffer + parse write responses | #17 review | isError inside HTTP 200; audit must record tool outcome |
| Scoped installation tokens per policy envelope | #17 review | GitHub enforces the repo boundary, not just our parser |
| Writes: App + audit + agents required in code | #17 review | Hard rules, not documented hopes |
octobroker_* review tools as a narrow MCP exception |
#44 / PR #45 | Fill upstream capability gaps without arbitrary GraphQL; preserve default-deny, repo binding, App credentials, and audit |