feat: git-over-HTTPS credentials via App tokens#43
Conversation
…p helper)
Closes the last long-lived GitHub credential gap for agents: git push
now authenticates with a short-lived, single-repo GitHub App installation
token instead of a PAT or user OAuth session.
Server (/git-credential, GET, opt-in via [mcp].enable_git_credentials):
- Same hard gate as MCP writes: [[mcp.agents]] + App backend + [mcp.audit],
validated at startup
- Policy stack, all fail-closed: X-Ghpool-Key auth → repository-scoped
agents only → repo allowlist → installation routing (multi-app by owner,
single-app owner match) → fail-closed audit record → single-repo token
mint (GitHub enforces the boundary)
- Response { username: x-access-token, password, expires_at } with
cache-control: no-store; token value never audited
Client (ghp git-credential):
- Standard git credential helper protocol (get/store/erase); get exchanges
GHPOOL_KEY for a token, store/erase are no-ops
- Declines quietly (exit 1) on missing key/path/non-github hosts so git
can fall through to other helpers; requires credential.useHttpPath
Verified: ghpool clippy -D warnings clean, 105 passed / 0 failed (6 new);
ghp clippy clean, 14 passed / 0 failed (2 new)
…fail closed everywhere Hardening from self-review of #43: - BLOCKER: verify_owner() binds explicit installation IDs to the actual installation account (GET /app/installations/{id}) before any issuance; mislabeled config entries are refused (403) and audited - MAJOR: git tokens mint with permissions={contents:write} and a git-specific cache namespace — never the App's full permission set, never shared with MCP token cache entries - MAJOR: ghp helper emits quit=true for every unservable recognized github.com request (missing key/path, policy denial, network failure) so git never falls through to broader stored helpers; non-GitHub hosts still decline quietly; gist.github.com unsupported - MAJOR: two-phase audit — durable request preflight BEFORE owner verification/mint/cache lookup, result record before the token is returned; either write failing rejects the credential (503) - singular [mcp.github_app] now requires owner when git credentials are enabled (startup validation + defense-in-depth in the handler) - /git-credential always registered before the /{*path} catch-all; disabled mode answers 404 locally instead of proxying to GitHub Tests: mint-envelope/cache-isolation, owner verification (unit + endpoint mismatch), two-phase audit records, no-mint-on-preflight- failure, production route precedence, helper plan/fetch fail-closed matrix, config validation gates. README/config.example document the --replace-all helper reset and quit=true semantics.
This comment has been minimized.
This comment has been minimized.
|
Important CHANGES REQUESTED What This PR DoesAdds Git-over-HTTPS credentials backed by GitHub App installation tokens: an authenticated, repository-scoped agent calls How It WorksPolicy stack, all fail-closed: key auth → strict repo parse → agent repo allowlist → installation routing (multi-App by owner, or singular App with required Findings
Finding Details🔴 F1: host matching misses legitimate github.com variantsGit's credential protocol passes the host from the remote URL: a remote configured as Fix: lowercase the host, split off the port; 🟡 F2: verify_owner result cached foreverThe verified-owner cache never expires. GitHub organizations can rename, and the released name can be claimed by another account; a long-running ghpool process would keep issuing tokens under a stale binding. Add a TTL (re-verify on the order of the token lifetime, e.g. hourly) so the binding is re-checked against GitHub periodically. 🟡 F3: no charset validation on repo owner/nameGitHub owners match 🟡 F4: ghp crate invisible to CI
🟡 F5: redundant concurrent mintsTwo simultaneous cache misses for the same namespace key both mint. Cheap fix: double-checked locking around the mint (async mutex per provider). Not a security issue — every mint is audited and expires — but it adds avoidable GitHub API calls and audit noise. Baseline Check
VerifiedAt head
5️⃣ Three Reasons We Might Not Need This PR
The approach stands. Fix F1 (and ideally F2–F5) before merge. |
…ification, strict charset, CI for ghp Round-2 review fixes: - BLOCKER: helper host matching is now case-insensitive and port-aware — host=GitHub.com and github.com:443 are recognized github.com requests (Fetch/Quit), never declined to a broader helper; github.com on a non-443 port quits (recognized but unsupported) - MAJOR: verify_owner caches with a 1h TTL and re-checks the installation account with GitHub after expiry (orgs can rename) - MAJOR: strict charset validation of repo owner/name on the server (400 before allowlist/preflight/mint) and in the helper's repo_from_path (quit) — percent-encoded/exotic input never reaches the audit log, the mint path, or an unencoded query string - MAJOR: CI now builds/lints/tests the ghp crate, plus an integration test suite that spawns the real binary and asserts quit=true on stdout for recognized-github failure modes and quiet decline (exit 1) for other hosts - MINOR: token mint is singleflight — concurrent cache misses for the same key mint exactly once (asserted by test) Tests: ghpool 111, ghp 20 unit + 5 integration, clippy clean both.
Fixes for review round 2 (commit 08c98c7)All findings from the review addressed:
✅ Fixed in 08c98c7.
✅ Fixed. Verification is cached with a timestamp and re-checked against
✅ Fixed. Server: owner must match
✅ Fixed.
✅ Fixed. Double-checked locking around the mint via a per-provider async mutex. Validation (macmini, arm64)
|
Round-3 review: the per-provider singleflight mutex was held across mint network I/O, serializing distinct cache keys (different repos, MCP vs git purposes) behind one another — and the reqwest client had no timeout, so one stalled mint could block all issuance for an owner indefinitely. - Singleflight is now per cache key (Arc<tokio::sync::Mutex> map): same-key misses still mint exactly once; distinct keys mint in parallel - App API client now has a 30s hard timeout (MINT_TIMEOUT) covering mint, installation resolution, and owner verification calls - New test: a stalled mint for one key must not delay another key (test_distinct_keys_mint_in_parallel); same-key singleflight test retained Tests: ghpool 112, ghp 20 unit + 5 integration, clippy clean both.
Fix for review round 3 (commit 508dcc3)Round-3 review verified all round-2 fixes (F1–F5) and found one regression introduced by the F5 singleflight:
✅ Fixed in 508dcc3.
Validation (macmini, arm64)
|
Summary
MCP now covers issues/PRs, but
git pushspeaks the Git protocol and needs a real credential at push time — today that means a long-lived PAT or user OAuth session in the agent container. This PR closes that gap:GET /git-credentialexchanges an agent'sX-Ghpool-Keyfor a short-lived, single-repo GitHub App installation token, andghp git-credentialplugs it into git as a standard credential helper.Design
[mcp].enable_git_credentials, hard-gated at startup exactly likeenable_writes: authenticated agents + App backend +[mcp.audit].repoparams, and owners without an installation are refused before any mint or audit write.github-app:<owner>), repo, and expiry — never the token value.store/eraseno-ops; quiet decline (exit 1) for non-github hosts, missingGHPOOL_KEY, or missing path so git falls through to other helpers.Tested
ghpool: clippy
-D warningsclean; 105 passed / 0 failed — 6 new: disabled→404, missing/bad key→401, single-repo mint + audit record (token absent from log), owner routing across installations, policy denial matrix (no mint/no audit on deny), audit fail-closed→503. ghp: clippy clean; 14 passed / 0 failed — credential-protocol parsing and owner/repo path extraction (.gitsuffix, extra segments, malformed).Not run: a live push against GitHub (needs Contents: R&W granted to an installed App); will validate during deployment.
Review Contract