feat(git-credential): add git_credentials_read_only flag - #56
Conversation
The /git-credential endpoint always minted contents:write tokens, so an agent granted git-over-HTTPS access could both clone and push — there was no way to grant clone-only access. This also meant the backing GitHub App had to hold Contents: read/write; a read-only App would fail the mint (422). Add [mcp] git_credentials_read_only (default false, behavior unchanged). When true, token_git downscopes the installation-token envelope to contents:read: git clone/fetch works, push is rejected by GitHub, and the App only needs Contents: read. Read tokens use a separate cache namespace so a read-only credential can never be served a write token (or vice versa). Docs (README, config.example.toml) and tests updated: token_git read-only mint asserts contents:read and a distinct cache namespace; git_credential endpoint test asserts read-only mode mints contents:read. Co-authored-by: rockexe0000 <rockexe0000@gmail.com>
chaodu-agent
left a comment
There was a problem hiding this comment.
Note
LGTM ✅ — correct token-level capability removal with proper cache isolation; one non-blocking follow-up recommended (per-agent granularity).
What This PR Does
Adds [mcp] git_credentials_read_only (default false, behavior unchanged). When set, /git-credential mints contents: read installation tokens instead of contents: write — agents can git clone/fetch private repos but cannot push.
How It Works
The permission envelope is selected in token_git(repo, read_only) and enforced by GitHub itself — no git proxying, consistent with octobroker's existing design. Read and write git tokens live under distinct cache purposes (git:contents=read / git:contents=write), so a read-only credential can never be satisfied by a cached write token or vice versa.
Findings
| # | Severity | Finding | Location |
|---|---|---|---|
| 1 | 🟢 | Cache namespace split done correctly — the one subtle security bug this feature could introduce (cross-namespace cache hits) is explicitly tested with a 3-namespace assertion | src/app_token.rs tests |
| 2 | 🟢 | Backward compatible: #[serde(default)], default false; docs + config.example.toml updated |
src/config.rs |
| 3 | 🟢 | Tests at both layers: token mint envelope (test_git_token_downscopes_permissions_and_isolates_cache) and endpoint (test_read_only_mode_mints_contents_read) |
src/app_token.rs, src/git_credential.rs |
| 4 | ℹ️ | Follow-up (non-blocking): the flag is global — every agent on the broker gets read-only git creds, or none do. Mixed fleets (one push-capable agent + read-only reviewer agents) need per-agent granularity, which octobroker's existing per-agent policy structure could carry. The cache-purpose split here already future-proofs for it. Recommend opening a follow-up issue so a later per-agent override composes cleanly with this global flag before the config surface ossifies. | src/config.rs |
Baseline Check
mainhas no read-only git credential support (only a doc-comment mention of Contents permissions) — PR is a single commitf56d386of net-new value on top of main.- Correctly complementary to #49 (ref-level push policy): #49 constrains where push-capable tokens push; this removes push capability entirely. Not a duplicate.
Verified
At PR HEAD f56d386 (fresh clone, macOS/arm64):
cargo clippy --all-targets -- -D warnings— cleancargo test— 133 passed; 0 failedcargo test read_only—git_credential::tests::test_read_only_mode_mints_contents_read✅cargo test downscopes—app_token::tests::test_git_token_downscopes_permissions_and_isolates_cache✅
5️⃣ Three Reasons We Might Not Need This PR
- "Just don't enable git credentials for read-only agents" — insufficient: the use case is agents that must clone private repos. Without this, the only options are no-clone or full-push.
- "#49 will cover it" — no: #49's ref-level policy requires git proxying and still leaves the token push-capable. Removing the capability at the token is strictly stronger and simpler for the never-push case.
- "Deploy a second octobroker with a read-only App" — works but doubles operational surface for a one-line permission difference; a config flag is the right cost. (The per-agent follow-up in F4 eventually removes even the two-instance need for mixed fleets.)
* feat(git-credential): per-agent git_credentials_read_only override Follow-up to #56: the global flag forces the whole fleet into one mode. A per-agent Option<bool> on [[mcp.agents]] now overrides the global default in either direction; None (omitted) inherits. Enables the mixed-fleet posture: read-only global default + one explicitly push-capable agent. Both values are operator-set config, never agent-controlled. Issuance log now records the minted mode. * feat(git-credential): record effective mode in durable audit + TOML roundtrip test Self-review findings: (1) the minted mode (contents read/write) was only in the tracing log, not the audit JSONL — forensic gap for mixed-mode fleets; now recorded on every git_credential_result, including failure paths. (2) add a TOML deserialization test guarding the tri-state Option<bool> per-agent key. --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Summary
Add
[mcp] git_credentials_read_only(defaultfalse, behavior unchanged).When
true,/git-credentialmints acontents: readinstallation tokeninstead of
contents: write: the credential cangit clone/ fetch a privaterepo but cannot push.
Problem
token_git(src/app_token.rs) hardcodescontents: write, so any agentgranted git-over-HTTPS access can both clone and push — there is no
clone-only option. It also forces the backing GitHub App to hold
Contents: read/write; installing it withContents: readonly would makethe mint fail (GitHub 422) and break the endpoint.
Use case
Read-only AI agents that must
git clonea private repo but nevercommit/push. Previously the only choices were "no git credential at all"
(can't clone) or "full write credential" (can also push). This fills the
missing "clone, no push" mode, so an agent can be read-only end to end
(MCP read tools + read-only clone).
Changes
src/config.rs— newMcpConfig::git_credentials_read_only: bool(
#[serde(default)], defaults tofalse).src/app_token.rs—token_git(repo, read_only)selectscontents: readvs
contents: write, under a distinct cache purpose(
git:contents=read/git:contents=write) so a read-only credential cannever be served a write token, or vice versa.
src/git_credential.rs— passesgit_credentials_read_onlyinto the mint.README.md+config.example.toml— documented.contents: readunder its owncache namespace; endpoint test asserts read-only mode mints
contents: read.Relationship to upstream #49
#49 (ref-level push
policy) constrains where a push-capable token may push, which needs
git-proxying that octobroker deliberately avoids. This PR covers the other
half — agents that should never push — by removing the push capability
entirely (
contents: read). It is not a duplicate and does not supersede#49; it fills the "no push at all / clone-only" option #49's list lacks.
Testing