Skip to content

Bind push authorization to Git's ref transaction; scope enforcement and full schema contract - #10

Closed
Deep-Sixed wants to merge 10 commits into
mainfrom
claude/fervent-darwin-65ds9y
Closed

Deep-Sixed wants to merge 10 commits into
mainfrom
claude/fervent-darwin-65ds9y

Conversation

@Deep-Sixed

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

Copy link
Copy Markdown
Owner

Fixes findings from a code review of main at d73bb83, plus two raised afterwards: the pre-receive timing gap and verify_contract not checking indexes and triggers. Each fix comes with regression tests (tests/test_governance_fixes.py, tests/test_transaction_boundary.py). I ran the key tests against the previous code, and they fail there for the intended reason. Supersedes #9, which is now closed.

Changes

  1. Self-approval. Now provided by main's Move approval authority to the repository and continue retained worktrees #12: agent roles, a per-repository approval policy, and a rule that an agent who approved an operation can't be the one to carry it out. This PR's original fix and its tests were removed in favour of it. The push path below authorizes through Move approval authority to the repository and continue retained worktrees #12's rules, with the pusher as the agent using the approval.
  2. Reusing the old worktree when a task is re-claimed on the same branch. Also provided by Move approval authority to the repository and continue retained worktrees #12 (continue_or_create_worktree). This PR's duplicate code was removed.
  3. Scope globs are normalized (./, //, a leading /, backslashes, ..). Overlap is compared case-insensitively, and scopes that point outside the repository are refused.
  4. The hook checks pushed files against the claim's scopes. When a branch has a live claim, new commits may only touch files inside that claim's scopes. A merge commit only counts the files the merge itself changed.
  5. Common mistakes give a clean error instead of a traceback. Commit arguments accept short SHAs, refs and uppercase hex and are stored as full commit IDs. Duplicate registrations and malformed operation JSON produce normal errors.
  6. handoff create --last-commit is honored even when the claim has a worktree.
  7. Pushes are authorized at the moment Git updates the ref
    • Before: pre-receive consumed the approval and committed that before Git moved the ref. An update Git refused later (another hook, a lost ref lock) still used up its approval. And if a claim was released, taken over or handed off in that gap, the push landed on authority that no longer held.
    • pre-receive: validates the push and records each exact ref update in a new ref_updates table (migration 003_ref_updates.sql, between 002 and main's 004). It no longer consumes anything.
    • Transaction hook, prepared stage: Git's reference-transaction hook re-checks every recorded update against current state while Git holds the ref locks and can still abort. Approvals are consumed only here, and any failure aborts the update.
    • committed / aborted stages: these record the outcome. An abort after prepared returns the consumed approval to usable. If the same operation was requested again in the meantime, the old approval stays used up so there are never two live approvals for one operation.
    • Hook must be present: pre-receive refuses pushes if the transaction hook is missing, modified, or not executable.
    • Installation: hook install inspects both hook paths before writing either. It refuses to overwrite a hook QuorumGit didn't write and requires Git 2.28 or newer.
    • Only unidentified ref changes pass through. A transaction with an agent identity must match a fresh validation from pre-receive, and if the store has disappeared it fails closed. Only transactions without an identity (maintenance run inside the hub) pass through.
  8. verify_contract checks the whole schema. It replays the bundled migrations into an in-memory database and requires every table, index and trigger in the store to match (ignoring formatting). It rejects missing or altered objects, indexes or triggers that shouldn't be there, and stores migrated by a newer version.
  9. Latent bug fixed along the way. Repository identity lookups inherited the hook's GIT_DIR. With an absolute GIT_DIR, every registered path resolved to the hub.
  10. doctor settles ref updates stuck in prepared. It reads the outcome from the ref itself: at the new value → committed; still at the old value → aborted, and --repair restores the approval; moved elsewhere or still locked → reported, never guessed.

Notes for reviewers

Validation

  • pytest tests -q: 185 passed
  • pyright: 0 errors
  • ruff check (the pinned 0.16.9): no findings

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp

- Approvals: the agent that consumes an approval (pusher or takeover
  claimant) never counts toward its threshold, and a takeover's
  beneficiary cannot vote yes. Approved-but-unused instances still accept
  votes, so an independent approver can replace a self-vote and a deny
  revokes it before use.
- Claims: a reclaim of an expired lease or an approved takeover on the
  incumbent's branch inherits its managed worktree instead of failing
  because Git will not check the branch out twice.
- Scopes: globs are normalized (./, //, leading /, backslashes, ..) and
  overlap is compared case-insensitively; scopes escaping the repository
  are refused.
- Hook: pushes to a claimed branch are rejected when the new commits touch
  paths outside the claim's scopes (merge commits count only paths the
  merge itself changed).
- CLI: commit specs resolve to full OIDs (short SHAs, refs, uppercase);
  duplicate agent/protected-ref registration and malformed operation JSON
  are clean errors; task and agent creation take the write reservation.
- handoff create honors an explicit --last-commit and no longer crashes
  when the claim's worktree has been removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
Pre-receive used to consume approvals and commit before Git updated the
ref, so an update Git later refused still spent its approval, and a
claim release, takeover, or handoff between pre-receive and the ref
update let a push land on authority that no longer held.

- pre-receive now validates and records each exact ref update
  (ref_updates, migration 003) without consuming anything.
- A managed reference-transaction hook re-validates every recorded
  update at the `prepared` stage, while Git holds the ref locks and can
  still abort, and consumes approvals there. Failure aborts the update.
  `committed`/`aborted` record the outcome, and an abort after
  `prepared` restores the consumed approval.
- pre-receive refuses pushes when the transaction hook is missing or
  modified; hook install writes both hooks and inspects both before
  writing either. Ref transactions no push validated pass through.
- Repository identity lookups no longer inherit the hook's GIT_DIR, which
  resolved every registered path to the invoking repository.

verify_contract() now replays the bundled migrations in memory and
requires every table, index, and trigger to match, rejecting missing or
altered objects (append-only audit triggers, single-owner and single
live approval unique indexes, identity triggers), unexpected indexes or
triggers, and stores migrated by a newer version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
@Deep-Sixed Deep-Sixed changed the title Close self-approval, worktree reclaim, and scope enforcement gaps Close self-approval, push-timing, scope, and schema-contract gaps Sep 29, 2026
A governed ref update whose committed/aborted hook never ran (killed
process, unreachable store) stayed `prepared` forever, with its approval
consumed whether or not Git applied it.

`quorumgit doctor` now reports updates prepared for longer than the
validation window and reads the outcome from the ref: at the new value
(or gone, for a deletion) is committed; still at the old value is
aborted, and --repair restores the consumed approval. A ref that has
moved elsewhere or is still locked is reported, never guessed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
@Deep-Sixed
Deep-Sixed marked this pull request as ready for review September 29, 2026 08:57
Resolves README conflicts with the docs-alignment change on main, keeping
its added detail (identity over local remotes, hub handoff accept, command
flags, destroy semantics) while updating the statements this branch makes
untrue: an agent's own vote never counts toward an approval it uses, and
handoff create honors an explicit --last-commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp

@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: cc88d14851

ℹ️ 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/gate.py Resolved
Comment thread src/quorumgit/gate.py Resolved
Comment thread src/quorumgit/cli.py Resolved
Addresses review findings on the reference-transaction boundary:

- pre-receive also requires the transaction hook to be an executable
  regular file; Git silently skips a non-executable hook, which would
  have skipped re-validation and approval consumption.
- An identified transaction (every push carries QUORUMGIT_AGENT) whose
  pre-receive validation expired or is missing is rejected instead of
  passing through as local maintenance. Only unidentified transactions
  pass through. Classification happens before taking the write
  reservation, and symbolic refs such as HEAD, which Git lists beside
  the ref they point to, are left to that ref's own line.
- With the store missing, an identified `prepared` transaction fails
  closed instead of exiting 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
Resolves conflicts with the move from libSQL to Python's sqlite3 module:
the in-memory schema replay behind verify_contract() now uses sqlite3,
and restoring an approval catches sqlite3.IntegrityError, which is what
the standard library raises for the one-live-approval unique index. The
README keeps main's SQLite 3 requirements and adds this branch's Git
2.28+ requirement for the reference-transaction hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
When Git aborts a transaction after `prepared`, the consumed approval is
restored unless the same operation was re-requested meanwhile. Restoring
then would leave two live approvals for one operation, so the old one
stays consumed and `approval.restore_skipped` is recorded. The test
covers that path, which depends on catching sqlite3.IntegrityError: with
the libSQL-era ValueError catch the constraint error escapes and the
abort is never recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
…nce fixes

Resolves conflicts with #13 and the ruff cleanup on main:

- checkpoint, handoff create, and worktree inheritance use main's
  trees.active_worktree_for_claim(); the branch's duplicate CLI helper is
  dropped. handoff create keeps this branch's rule that an explicit
  --last-commit wins over the worktree's HEAD, and the README says so.
- Keeps main's explicit check= on subprocess calls and adds it to this
  branch's remaining call, so `ruff check` reports no findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
main's #12 now owns two things this branch also fixed, more completely:

- Approval authority: agent roles and per-repository policy, requesters
  and takeover beneficiaries barred from voting, and approvers barred
  from consuming. This branch's independent-vote rule, approve-after-
  approved voting, and their tests are dropped in favour of it. The
  push path's re-validation at the reference transaction's `prepared`
  stage now authorizes through main's approved_instance/approval_refusal
  with the pusher as consumer.
- Retained-worktree continuation: main's continue_or_create_worktree
  replaces this branch's inherit_worktree and its duplicate tests.

Everything else in this branch is kept: the reference-transaction
boundary, push-time scope enforcement, the full schema contract, doctor
reconciliation, scope normalization, commit resolution, and handoff
--last-commit precedence. CLI commands use main's store.session, and
migration 003_ref_updates sits between 002 and main's 004.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8BpCkArA34hB1aSyVcnZp
@Deep-Sixed Deep-Sixed changed the title Close self-approval, push-timing, scope, and schema-contract gaps Bind push authorization to Git's ref transaction; scope enforcement and full schema contract Sep 29, 2026

Copy link
Copy Markdown
Owner Author

Closing this in favour of smaller PRs. main has overtaken most of it:

The remaining pieces land separately:

This branch is left in place as the reference for that follow-up.


Generated by Claude Code

@Deep-Sixed Deep-Sixed closed this Sep 29, 2026
@Deep-Sixed Deep-Sixed changed the title Bind push authorization to Git's ref transaction; scope enforcement and full schema contract Bind push authorization to Git's ref transaction Sep 29, 2026
@Deep-Sixed Deep-Sixed changed the title Bind push authorization to Git's ref transaction Bind push authorization to Git's ref transaction; scope enforcement and full schema contract Sep 30, 2026
Deep-Sixed pushed a commit that referenced this pull request Sep 30, 2026
From the session that rebuilt this boundary on #10's branch (44a6b5f):

- A force push refused by receive.denyNonFastForwards, which Git checks
  after pre-receive, keeps its approval and the retry uses it. On main
  the approval is consumed and the branch does not move.
- An update needing two approvals (protected ref and protected path)
  consumes both at `prepared`, records both in approval_ids, and
  restores both on `aborted`.
- A non-atomic push of main and a claimed branch whose commits include
  main's still matches the out-of-scope operation that was approved.

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