Repository navigation
Measure claim scopes against the mainline, not every ref - #29
Merged
Merged
Conversation
The hook computed a push's changed paths as the commits reachable from no existing ref, and used that one list for both protected paths and claim scopes. For scopes that was a bypass: unclaimed branches carry no scopes, so an agent could push an out-of-scope commit to an unclaimed branch (accepted) and then fast-forward its claimed branch to it, which then introduced "nothing new" and passed. Scope checks now use their own baseline: the branch's previous value, the hub's default branch (HEAD) and the protected refs. Anything else a push brings into a claimed branch counts against the claim, including another agent's branch. Merging the mainline in is still not charged. Protected-path checks keep the every-ref baseline, which is sound because they apply to every push on every ref. `approve prepare` uses the same two baselines, so its hashes still match the hook's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014R4wfmENrMnt36KN1Kj6Fx
Deep-Sixed
pushed a commit
that referenced
this pull request
Sep 29, 2026
…ransaction binding pre-receive now records both path lists it derives, the every-ref `paths` for protected paths and the mainline `scope_paths` for claim scopes, and the `prepared` stage replays both. The ref_updates migration gains a scope_paths column accordingly. New tests: - An approved force-push that Git denies by config (receive.denyNonFastForwards, set by `git init --shared`) keeps its approval, and the retry consumes it. - An update needing several approvals (protected ref + protected path) consumes all of them at `prepared` and restores all of them on abort. - In a non-atomic push where main lands before a claimed branch, the claimed branch's `prepared` stage uses the paths pre-receive derived. With recomputed paths this push is aborted: the out-of-scope set shrinks once main has moved, so the approved hash no longer matches. The branch's own copies of the scope tests (findings 3 and 4 in test_governance_fixes.py) are removed: main carries them in test_scope_enforcement.py, with #29's corrected expectation that merging another agent's branch counts against the claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014R4wfmENrMnt36KN1Kj6Fx
This was referenced Sep 29, 2026
Closed
#30 records each validated ref update's paths at pre-receive and replays them at the reference transaction's `prepared` stage. This PR adds a second list, scope_paths, measured against the mainline, so both are now recorded and replayed: migration 007 adds ref_updates.scope_paths, and evaluate_ref_update() computes whichever list it is not given. An update validated before 007 (NULL) recomputes its scope paths. test_prepared_uses_the_paths_pre_receive_derived pushes main and a claimed branch together; it fails if `prepared` recomputes scope_paths, because main landing first moves the mainline baseline. README keeps main's "hooks" wording and this PR's mainline-baseline note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
Deep-Sixed
marked this pull request as ready for review
September 30, 2026 01:30
Deep-Sixed
pushed a commit
that referenced
this pull request
Sep 30, 2026
Renumber 006_operation_policies.sql to 008, since 006 and 007 now hold the ref-update transaction tables. Check the required views by name before the schema fingerprint, as tables and triggers already are, so a dropped policy view is reported as such. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
Deep-Sixed
pushed a commit
that referenced
this pull request
Sep 30, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
Deep-Sixed
pushed a commit
that referenced
this pull request
Sep 30, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
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.
Summary
This closes a way to get around claim scopes, found in review and reproduced with real pushes through the hook. An agent whose claim covers
docs/**could landsrc/auth.pyon its claimed branch:out_of_scope_push.scratch: accepted, because unclaimed branches have no scopes.The cause is that
changed_pathstreated every commit reachable from any existing ref as already governed, and one path list was used for two different checks.Changes
git_objects.mainline_tips(new): the commits the hub's default branch (HEAD) and the protected refs point to. A missing ref is skipped, which only makes the check stricter.gate.governed_operationsnow takes two path lists:paths, measured against every ref as before, is used for protected paths. That baseline is sound because protected paths are checked on every push to every ref.scope_paths, measured against the branch's previous value plus the mainline, is used for claim scopes. Mergingmainin still isn't charged to the claim. Merging another agent's branch, or first pushing to an unclaimed branch, now is. Counting another agent's branch against your claim was a deliberate choice.prepare_pushcomputes both lists the same way the hook does, soapprove preparestill produces the hash the hook asks for. The existing check that the clone has every hub tip covers the mainline tips too.Integration with the reference-transaction hook (#30)
After
maingained the two-phase push check (pre-receive records each update inref_updates; the reference-transaction hook re-validates it atprepared),scope_pathshas to be replayed likepathsso operation hashes stay stable between the two phases:007_ref_update_scope_paths.sqladds a nullableref_updates.scope_pathsJSON array.scope_paths, andpreparedre-validates with both recorded lists. ANULL(a row validated before the migration) is recomputed.test_prepared_uses_the_paths_pre_receive_derivedcoversscope_pathstoo, and the migration-list tests include006and007.Tests
test_staging_work_on_another_branch_does_not_escape_the_claim. The staged commit is rejected, andapprove preparegives the same hash as the hook.test_new_claimed_branch_is_measured_from_the_default_branch. With no protected refs, a new claimed branch is charged only for its own commits.test_hook_holds_claimed_branch_to_its_scopesnow expects that merging another agent's unprotected branch needsout_of_scope_push.src/change reverted, the first and third tests fail. The default-branch test passes either way; it guards against the fix being too strict.Validation
pytest tests -q: 231 passed (after mergingmain).pyright: 0 errors.ruff check(the pinned 0.16.9): clean.🤖 Generated with Claude Code
https://claude.ai/code/session_014R4wfmENrMnt36KN1Kj6Fx
https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp