Skip to content

Add per-operation approval policy overrides - #27

Merged
Deep-Sixed merged 5 commits into
mainfrom
claude/vigilant-darwin-1ebqfk
Sep 30, 2026
Merged

Deep-Sixed merged 5 commits into
mainfrom
claude/vigilant-darwin-1ebqfk

Conversation

@Deep-Sixed

@Deep-Sixed Deep-Sixed commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #23. A repository's approval policy becomes its default. Operators can now override it for a single operation type. For example, a force push or a branch deletion can need two votes while a lease takeover needs one.

What changes

  • Overridable operation types: protected_ref_update, force_update, ref_delete, out_of_scope_push, protected_path_update, lease_takeover.
  • What an override can set: the threshold, the approving roles, and whether the requester may vote. Any field an override leaves unset still follows the default, even if the default changes later. --inherit removes an override.
  • Migration 008_operation_policies.sql (renumbered from 006; 006 and 007 now hold the ref-update transaction tables from Bind push authorization to Git's reference transaction #30 and Measure claim scopes against the mainline, not every ref #29):
    • Adds operation_approval_policies, operation_approval_roles, and an approval_effective_policy view.
    • Recreates the two vote-eligibility triggers so they enforce the effective policy. Code that writes to the database directly is held to the overrides too.
    • Adds no overrides, so existing repositories behave exactly as before.
    • The store's contract check requires the view by name, before the full schema fingerprint (which covers views too), so a dropped view is reported as such.
  • Enforcement: registry.approval_policy(conn, repo_id, operation_type) returns the effective policy. Requests, votes, and consumption all use it, so raising an override invalidates approvals that no longer meet it.
  • Invalidated approvals can be topped up. The next vote reopens an approved instance that no longer meets the current policy, and the audit log records it as approval.reopened. Eligible agents can then add the missing votes, or deny it. Before this, such an approval was stuck until the policy was loosened. That also happened when the default threshold was raised, before this PR.
  • CLI: quorumgit repo policy <repo> --operation <type> [--threshold n] [--role r]… [--[no-]requester-may-vote] [--inherit]. The plain repo policy <repo> view now lists overrides.
  • What stays the same: an agent that approved an operation still can never carry it out, even where an override lets the requester vote.

Testing

  • New tests/test_operation_policies.py (9 tests):
    • an override's threshold applies only to its own operation type
    • override roles replace the default roles, enforced both in Python and by the database trigger
    • an override that lets the requester vote still doesn't let an approver carry out the operation
    • raising an override invalidates existing approvals, which a new vote reopens and brings back up to the threshold
    • an invalidated approval can be denied, while one that still meets the policy stays closed to further votes
    • unset fields inherit the default, --inherit removes an override, and changes are audited
    • input validation and operator-only changes
    • the contract check rejects a store missing the view
    • the CLI round trip
  • Updated the existing migration-list assertions for 006–008.
  • After merging main: pytest 240 passed. ruff check and pyright are clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_0142xh1CDDPxBvB1EHu6PN3m
https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp

A repository's approval policy becomes its default. Operators can override
the threshold, approving roles, or requester-may-vote for one operation
type (protected_ref_update, force_update, ref_delete, out_of_scope_push,
protected_path_update, lease_takeover), e.g. two votes for a force push
and one for a takeover. Unset override fields inherit the default;
--inherit removes the override.

- Migration 006: operation_approval_policies/roles tables and an
  approval_effective_policy view; the vote-eligibility triggers now
  enforce the effective policy, so direct writers are held to it too.
- registry.approval_policy takes the operation type; request, vote and
  consumption all use the effective policy, so raising an override
  invalidates approvals that no longer meet it.
- CLI: repo policy --operation <type> [--inherit]; the plain view lists
  overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142xh1CDDPxBvB1EHu6PN3m
@Deep-Sixed
Deep-Sixed marked this pull request as ready for review September 29, 2026 23:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ba93177fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/quorumgit/registry.py Resolved
Comment thread src/quorumgit/store.py Resolved
Review fixes:

- vote() reopens an approved instance that no longer meets the effective
  policy (threshold raised, approvers demoted), audited as
  approval.reopened, so eligible agents can top it up. Previously
  request_approval() kept returning that live instance while vote()
  refused every vote on it, leaving the exact operation unusable until
  the policy was weakened. Applies to default-policy changes too.
- verify_contract() requires the approval_effective_policy view the
  vote-eligibility triggers read from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142xh1CDDPxBvB1EHu6PN3m
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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
@Deep-Sixed
Deep-Sixed merged commit d4c9ae8 into main Sep 30, 2026
7 checks passed
Deep-Sixed pushed a commit that referenced this pull request Sep 30, 2026
Renumber the quorum migration to 009 and let a per-operation override
set quorum mode as well as threshold, roles and requester voting: a NULL
approval_quorum on an override inherits the repository default. The
repo policy CLI accepts --quorum with --operation and shows quorum on
overrides. Reopening a stale approval (#27) uses the same requirement
as voting and consumption, so a quorum raised by new approvers reopens
it for the missing votes.

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
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