feat(push): a bound agent may push only its own branch (close cross-agent clobber)#19
Merged
Merged
Conversation
…gent clobber) Multi-agent scenario work surfaced a real gap: the push guards blocked protected refs (main/master/policy), `--mirror`/`--all`, trust-root files, and snapshot refs — but NOT a push to another agent's non-protected branch. An agent bound to feat/a could `git push origin +HEAD:feat/b` and force-clobber agent-b's branch on a shared remote (empirically reproduced: `forced update` succeeded). Add `push_cross_branch_violation`: a bound agent may push ONLY its assigned branch — the symmetric partner to the cross-branch checkout deny. Runs alongside the existing push guards (fugu: coexist; protected still gives better messages for main/master). Denies cross-branch pushes, deletes, force-pushes, `--all`/ `--mirror`, wildcards, and (per fugu review) an implicit push when the worktree HEAD has drifted off its binding or push.default=matching. Parsing is option-aware (`parse_push_argv`): it skips flags AND value-taking options (`-o`/`--push-option`/`--receive-pack`/`--exec`) and detects delete mode, so it doesn't mis-read `git push --delete feat/b` (no remote) or an option value as a remote/refspec — the exact holes fugu's design review flagged. Tag pushes (`--tags`, `refs/tags/…`, `tag <name>`) are exempt from the BRANCH guard. Tests: a full deny/allow table matrix (fugu's list) as unit tests, plus an end-to-end repro — agent-a's own-branch push succeeds, its force-push/delete of feat/b are denied, and feat/b on the shared origin is NOT clobbered. Full workspace suite green, clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
suzuke
added a commit
that referenced
this pull request
Jul 6, 2026
…is) (#20) * demo: multi-agent verification scenario (agents produce evidence, supervisor verifies) Two agents share one repo at once, each on its own branch. verify.sh (the deterministic supervisor) launches agent-a and agent-b CONCURRENTLY, each in its own guarded session running agent-run.sh; each agent does real work, pushes its own branch, tries to interfere with the other, and leaves a machine-recomputable evidence bundle (every command's raw stdout/stderr/exit-code + a state snapshot). Per the adversarial design review: an agent's own PASS is not proof — the supervisor RE-DERIVES the truth from git/home/audit state (never the agent's narrative) and checks nine invariants: distinct worktrees on bound branches, git-is-the-shim in both sessions, both branches on the shared origin, per-agent provenance (not mixed), isolated working trees, the shared source repo untouched, one integrity key with both signed bindings, every guarded step behaving (own push ok; cross-agent checkout/force-push/delete all DENIED), and each agent's self-verdict consistent with the re-derived state. Invariant 8 reads the real exit codes, so it FAILS if a guard doesn't fire — it is designed to catch a regression of exactly the cross-agent force-push clobber (PR #19) whose gap this scenario's design first surfaced. Verified end-to-end (9/9 invariants held with the installed 0.2.2 + PR #19). Throwaway world in a temp dir; only remote is a local bare repo. shellcheck clean (SC2015 excluded: pass/bad are non-failing printf helpers). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(multi-agent): synthesis re-derives ALL invariants from state (close forged-artifact hole) fugu review REJECTED: the synthesis still trusted agent-written artifacts for the canonical-checkout invariant. It only checked the shared project repo, never the stand-in real checkout, and took the agent's own rc/verdict files as the source of truth — so an agent could move the canonical checkout's HEAD, write 10.rc=1 + verdict=PASS, and the synthesis reported success (fugu reproduced it). Rewrite the synthesis to RE-DERIVE every invariant from independent state: - worktrees come from the shim-written, HMAC-signed bindings (not the agent's evidence.env); each worktree's current branch is read with real git (catches a cross-branch checkout that drifted HEAD). - cross-agent push/delete is caught by the origin state itself: both branches present, distinct, each trailered to its OWN agent. - NEW: the stand-in real checkout's HEAD must equal its pre-run value — this independently catches an agent touching your checkout regardless of what it reported (the exact hole). - the agent's rc/verdict becomes a CONSISTENCY cross-check only: it can flag a self-report that disagrees, but can never turn a real violation into a pass. Verified: normal run VERIFIED (11 invariants, re-derived); and fugu's forge attack — tamper the canonical HEAD + fake agent-a's 10.rc=1/verdict=PASS — is now caught by the independent canonical invariant. shellcheck: verify.sh fully clean; agent-run.sh clean except an intentional single-quoted sh -c (SC2016). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(multi-agent): derive worktrees from the owned project, not agent-writable HOME fugu review REJECTED again (deeper): the synthesis called binding.json authoritative but never verified its HMAC signature — I5 only checked that .sig FILES exist. A same-uid agent with $AGENTIC_GIT_HOME can rewrite its binding.json to point at a fake clean repo (sig stale but present), and the synthesis accepted the fake worktree. fugu reproduced it. Root cause: HOME is agent-writable at same-uid (the agent can even read the HMAC key and re-sign), so NO home-derived state is trustworthy against a malicious agent — the seatbelt-not-cage limit. Fix: the supervisor now derives the agents' worktrees from the SHARED PROJECT's own `git worktree list` (the supervisor owns the project repo; an agent can't redirect it by editing HOME). Dropped the cosmetic file-leak check (an agent can create any file in its own worktree — no signal). All the load-bearing invariants already key off supervisor-owned state (bare origin branches + provenance, the stand-in checkout's HEAD, the project's HEAD). README now states the honest boundary explicitly: this verifies the guards for agents going THROUGH the shim (the threat model); a malicious same-uid agent that forges its HOME state or bypasses the shim via /usr/bin/git is out of scope — which is exactly why the supervisor keys off repos it owns, not the agent's home. Verified: normal run VERIFIED; the binding-forgery no longer redirects the supervisor (it reads the project's worktree list → the real worktree). verify.sh shellcheck clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(multi-agent): README matches the supervisor-owned invariants (no stale claims) fugu review REJECTED (documentation): after moving the load-bearing checks to supervisor-owned state, the README still advertised invariants the code no longer has — a nine-item list including "working trees isolated (file-leak)" (dropped as cosmetic) and "invariant 8 reads exit codes to prove guarded steps behaved" (exit codes are now only the non-authoritative consistency cross-check). A reader was told the scenario proves properties it no longer checks — the opposite of the honest boundary this scenario is about. Rewrite the invariant list and the "why it's a real test" section to match the actual supervisor-owned checks: the shared project's own worktree list, origin branches + per-agent provenance (this is what catches a clobber), the shared repo's and the stand-in checkout's HEADs unchanged, one non-split-brain key, per-agent audit attribution, plus the agents' self-report as a consistency cross-check only. Also fix the intro's "re-derives from git/home/audit" to "state it owns … never the agent's home". Docs only; verify.sh unchanged (fugu confirmed the code-side forge fixes at 94589cc). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap (found while designing the multi-agent scenario)
The push guards blocked protected refs (main/master/policy),
--mirror/--all, trust-root files, and snapshot refs — but NOT a push to another agent's non-protected branch. An agent bound tofeat/acould:Empirically reproduced. Local isolation held (checkout/worktree denied), but cross-agent branch integrity on a shared remote did not.
The fix
push_cross_branch_violation: a bound agent may push only its assigned branch — the symmetric partner to the cross-branch checkout deny. Coexists with the existing push guards (protected still gives better messages for main/master). Denies cross-branch push / delete / force-push /--all/--mirror/ wildcard, and an implicit push when the worktree HEAD drifted off its binding orpush.default=matching.Parsing is option-aware (
parse_push_argv): skips flags AND value-taking options (-o/--push-option/--receive-pack/--exec) and detects delete mode, so it doesn't mis-readgit push --delete feat/b(no remote) or an option value as a remote/refspec — the exact holes the fugu design review flagged. Tag pushes (--tags,refs/tags/…,tag <name>) are exempt from the branch guard.Verification
--delete feat/b(no remote),HEAD:feat/b,:feat/b,+HEAD:feat/b, wildcard, drift, matching, tag forms,-ovalue.feat/bare denied;feat/bon the shared origin is not clobbered.clippy -D warningsclean.🤖 Generated with Claude Code