Skip to content

Derive push operations for approval requests (approve request --push) - #22

Closed
Deep-Sixed wants to merge 3 commits into
claude/eloquent-galileo-xu0ah7-approversfrom
claude/eloquent-galileo-xu0ah7-push-request
Closed

Deep-Sixed wants to merge 3 commits into
claude/eloquent-galileo-xu0ah7-approversfrom
claude/eloquent-galileo-xu0ah7-push-request

Conversation

@Deep-Sixed

Copy link
Copy Markdown
Owner

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, newrev and, 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 of check_ref_update. It applies the branch reservations (handoff freeze, claim owner) and returns the approval-gated operation, or None. 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():
    • Reads the old revision from the hub's current ref and resolves --rev (default HEAD) in the pusher's clone.
    • Runs governed_operation against 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.
    • Supports --delete.
  • CLI: quorumgit approve request --repo R --push REF (--from CLONE [--rev C] | --delete) [--pusher AGENT].
    • It prints the derived operation and opens the approval. If the push is ungoverned, it says so and requests nothing.
    • The positional JSON form still works. Passing both forms, or neither, is a usage error (exit 2).
  • Hook rejections now end with the exact approve request --push … command to run.
  • README: updated the approval walkthrough and command reference, and documented the derivation.

Tests

tests/test_push_request.py adds 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:

  • a protected push (also asserts the new commit never reaches the hub during derivation);
  • an out-of-scope push;
  • a force push, then a deletion;
  • the full CLI flow.

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: pytest 154 passed, ruff clean, pyright 0 errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mc44WsAZx9hzZdWEihDk5a


Generated by Claude Code

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
@Deep-Sixed
Deep-Sixed force-pushed the claude/eloquent-galileo-xu0ah7-approvers branch from cd1b083 to 5a1146f Compare September 29, 2026 22:29

Copy link
Copy Markdown
Owner Author

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 (governed_operation), I'll rebase this PR and #25 onto main once #10 merges, rather than twice. The derivation will then be ported onto #10's reference-transaction flow, and the migration renumbered.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Closing as superseded by #23, now on main. Its quorumgit approve prepare --repo R --ref X [--new rev | --delete] [-C clone] [--request] [--json] does the same thing as this PR: it reads objects from the pusher's clone and runs the hook's own derivation, printing the exact operations and hashes a push will need. It also has plan-only and JSON output.


Generated by Claude Code

@Deep-Sixed Deep-Sixed closed this Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants