Repository navigation
Derive push operations for approval requests (approve request --push) - #22
Deep-Sixed wants to merge 3 commits into
Conversation
Scopes were only compared against other claims when a claim was granted; the hook never looked at what a push actually changed, so an agent could claim src/** and push edits anywhere. The pre-receive hook now lists the paths changed by the commits a push introduces (rev-list newrev --not --all, combined diff for merges, renames split) and requires an approval bound to the exact update, including the sorted out_of_scope_paths, when the owner of a claimed branch touches anything outside its scopes. This restores the content-aware check the predecessor Codex_Kernel gate performed on incoming objects, applied to QuorumGit's claim scopes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mc44WsAZx9hzZdWEihDk5a
Approvals defaulted to one vote from any registered agent, including the agent that requested the approval or makes the push. Repositories can now opt into a stricter policy, restoring the membership-bound quorum from the predecessor Codex_Kernel gate: - `repo approver add/remove/list`: only roster members may vote on the repository's approvals, and the threshold is a 2/3 + 1 quorum of the roster (a request's --threshold may raise it, never lower it). - `repo policy --separate-duties`: the requester cannot vote, and the agent consuming an approval (pusher or takeover target) cannot count its own vote. Quorum is taken over the approvers eligible for that approval. - Quorum is re-verified at consumption against the current roster, so removing an approver withdraws their vote from unused approvals. - An approved-but-unused approval still accepts votes: a deny revokes it, and extra yes votes can restore quorum after a roster change. Repositories without a roster or policy keep the existing open behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mc44WsAZx9hzZdWEihDk5a
Requesting an approval for a push meant pushing, getting rejected, and hand-writing the operation JSON from the rejection. The predecessor Codex_Kernel had an offline proposal producer that derived the exact payload from the candidate commit before any push; this restores it. `quorumgit approve request --repo R --push REF --from CLONE [--rev C]` (or `--delete`) derives the operation with the hook's own logic: the ref-update governance is split out of check_ref_update into governed_operation(), shared by the hook and the new derive_push_operation(). The hub is inspected with the clone's object store attached as a read-only alternate, mirroring how Git exposes quarantined objects to pre-receive, so nothing is fetched or written and the result matches what the hook recomputes. Hook rejections now print the command to request approval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mc44WsAZx9hzZdWEihDk5a
cd1b083 to
5a1146f
Compare
|
Heads-up: the stack under this PR changed.
Until this PR is rebased, its diff against the old base branch shows unrelated changes. Because #10 rewrites the gate code this PR refactors ( Generated by Claude Code |
|
Closing as superseded by #23, now on Generated by Claude Code |
3 of 4 restoring ideas from the retired Codex_Kernel repo. This PR is stacked on #21, which is stacked on #20. Merge in order.
Problem
To get approval for a protected push today, you push, read the hash from the rejection, then hand-write the operation JSON (
type,refname,oldrev,newrevand, since #20,out_of_scope_paths). That's error-prone, and a single wrong field means the approval never matches.Codex_Kernel had
proposal_producer.py, which built the exact payload from the candidate commit before pushing, using the same code path as the gate. This PR brings that back.Change
gate.governed_operation()is split out ofcheck_ref_update. It applies the branch reservations (handoff freeze, claim owner) and returns the approval-gated operation, orNone. The hook and the new derivation both call it, so their results can't drift apart. Hook behaviour is unchanged, and all existing tests pass untouched.gate.derive_push_operation():--rev(defaultHEAD) in the pusher's clone.governed_operationagainst the hub with the clone's object store attached as a read-only alternate (GIT_ALTERNATE_OBJECT_DIRECTORIES). That mirrors how Git shows quarantined incoming objects to a real pre-receive hook, so nothing is fetched or written.--delete.quorumgit approve request --repo R --push REF (--from CLONE [--rev C] | --delete) [--pusher AGENT].approve request --push …command to run.Tests
tests/test_push_request.pyadds 8 tests. For each of these cases, the derived operation's hash equals the one the hook prints on rejection, and the push lands once that approval is granted:It also covers: an ungoverned push derives
None; branch reservations are enforced during derivation; bad inputs (nothing to push, unknown rev, nothing to delete); and usage errors.Local results:
pytest154 passed,ruffclean,pyright0 errors.🤖 Generated with Claude Code
https://claude.ai/code/session_01Mc44WsAZx9hzZdWEihDk5a
Generated by Claude Code