Repository navigation
Move approval authority to the repository and continue retained worktrees - #12
Conversation
…rees Approvals previously proved only that the voter was a registered agent, and the requester chose the threshold (default 1), so a worker could request and approve its own protected push or takeover. Authority: - agents carry a role (worker, reviewer, operator; existing agents upgrade as workers) - each repository owns its approval policy: threshold, approving roles, and whether the requester may vote; `approve request --threshold` is removed - only approving roles may vote, the requester may not by default, and a takeover's beneficiary may never vote on it - an agent that approved an operation cannot consume it (pusher or taker), enforced in the gate and by a database trigger - eligible votes are re-derived from current roles and policy at use time, so a tightened policy or a demoted approver invalidates old approvals - role and policy changes require an operator after bootstrap, the last operator cannot be demoted, and both are audited - the store contract now requires the governance triggers by name Worktrees: - a claim that supersedes an earlier claim on the same task and branch (lease expiry, approved takeover, release that kept its checkout) verifies and transfers the retained worktree instead of failing on Git's one-checkout-per-branch rule; drifted or other-task checkouts are refused - managed worktree paths are built from IDs plus a random suffix, so agent and repository names can no longer place a checkout outside the managed directory Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SrukvuPz7kpbore3kkKpQ3
|
CI note on Why I don't think this is a logic failure in this PR:
I can't reproduce Windows from this environment and haven't found a root cause, so I'm not calling it a flake. I'll re-run the failed job once when the rest of the run finishes; GitHub refused the re-run while macOS 3.14 is still in progress. If the crash comes back, I'll treat it as real and make the CLI's libSQL connection lifecycle explicit (close each connection deterministically, not by garbage collection), then re-test. Generated by Claude Code |
Every CLI command used libSQL's connection context manager, which ends the transaction but leaves the native connection open, so each one was torn down implicitly when its last reference disappeared. On Windows CI the CLI child process intermittently died with 0xC0000005 and no output, in different commands on each run, as the number of CLI invocations in the suite grew. store.session() keeps the same commit-on-success / rollback-on-error semantics and then closes the connection deterministically. All CLI commands use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SrukvuPz7kpbore3kkKpQ3
|
Follow-up on the Windows crash: the one re-run of Pushed Generated by Claude Code |
…odm7 # Conflicts: # README.md
The Windows job intermittently loses a CLI child process to 0xC0000005 with no output, so the failing call is unknown. faulthandler prints the Python stack on a native crash, which the next occurrence will capture in the assertion message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SrukvuPz7kpbore3kkKpQ3
The intermittent Windows 0xC0000005 crashes are a libsql 0.1.11 bug, not
QuorumGit code. faulthandler put the crash in Connection.close(), and
Valgrind on Linux shows why for every connection teardown, on main too:
dropping the inner LibsqlConnection calls sqlite3Close and frees the
handle, then the outer Connection's drop calls sqlite3Close again on the
freed block ("Invalid read ... inside a block of size 848 free'd by
sqlite3Close"). Linux tolerates the stray read; Windows sometimes faults.
0.1.11 is the newest libsql release, so there is no upgrade to take.
CLI processes now retain their connections and end through os._exit
after flushing output (cli.run, used by the console script and
`python -m quorumgit`). store.release() closes normally everywhere else,
and when retaining it first rolls back any open transaction so no write
lock outlives the command. Valgrind reports zero invalid reads for read
and write commands with this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SrukvuPz7kpbore3kkKpQ3
This reverts commit c5ba900.
This reverts commit 8e55c9c.
…odm7 # Conflicts: # src/quorumgit/store.py # tests/test_authorization_binding.py # tests/test_gate.py
|
Root cause of the Windows
PR #14 has since replaced libSQL with the standard-library Generated by Claude Code |
…odm7 # Conflicts: # src/quorumgit/cli.py # src/quorumgit/trees.py
main now pins ruff 0.16.9 and gates CI on it. Drop three noqa comments for a rule that is not enabled, sort registry imports, merge a nested with in the session test, and mark an unused unpacked fixture value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SrukvuPz7kpbore3kkKpQ3
The predecessor Codex_Kernel gate derived its threshold from membership: a proposal needed floor(2N/3) + 1 votes from the N authorized nodes, so the bar grew with the approver set. Repository policy (#12) uses a fixed approval_threshold, which stays at 1 no matter how many operators exist. `quorumgit repo policy <repo> --quorum` makes an operation require 2/3 + 1 of the agents eligible to approve it: agents holding an approving role, minus the requester (unless the policy lets it vote) and a takeover's beneficiary, and at use time minus the consumer. The fixed threshold remains a floor. Counting only eligible agents keeps those exclusions from making approval impossible. Because eligibility is re-derived when an approval is used, adding approvers raises the bar for unused approvals the same way raising --threshold does. Migration 005 adds the flag; policy changes remain operator-only and audited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mc44WsAZx9hzZdWEihDk5a
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
Summary
Fixes the review items that decide who may authorize and what happens to filesystem state when ownership moves. PR #9 (the receive transaction boundary) and the scope enforcement in PR #10 are left to those PRs.
Approval authority
Before this change,
gate.vote()checked only that the voter was a registered agent, and the requester set--threshold(default 1). A worker could therefore request and approve its own push.worker(the default),reviewer, oroperator.approval_threshold, its approving roles, andrequester_may_vote. The defaults are 1,operator, and no.approve request --thresholdis removed; the threshold is recorded on the instance for display only.004_approval_authority.sqladds triggers for vote eligibility (on insert and update), approvals naming a registered repository, and consumer ≠ approver.verify_contractnow fails if any governance trigger is missing.agent add --role,agent role <name> <role>, andrepo policy <name> [--threshold] [--role]… [--[no-]requester-may-vote].agent listnow shows roles.Upgrade behavior (fails closed). Existing agents become workers and existing repositories get the default policy. Old approvals, self-approvals included, stop authorizing anything until an eligible operator votes on a new request. The README tells operators to run
quorumgit agent role <name> operatorafterinit.Worktree continuation
A claim that replaces an earlier claim on the same task and branch no longer tries to create a second checkout, which Git would refuse. This covers lease expiry, an approved takeover, and
releasewithout--remove-worktree. Instead,trees.continue_or_create_worktreechecks the retained checkout's repository, root, and branch (the same checkdoctoruses, now factored out), moves it to the new claim with any uncommitted work, and recordsworktree.continued.doctorreports the stale record.Worktree paths
Managed paths are now
worktrees/repo-<id>/claim-<id>-<random>. Agent and repository names no longer appear in paths, so a name like../../xcannot move a checkout outside the managed directory. The random suffix stops a leftover directory from blocking a later claim that reuses the rowid of a rolled-back one.CLI connection lifecycle
Every CLI command now runs inside
store.session(). It keeps the connection context manager's commit/rollback behavior and then closes the connection deterministically. (The Windows0xC0000005crashes seen earlier on this PR were a libSQL 0.1.11 double-close bug; PR #14's move tosqlite3removed it. Details are in the PR comments.)Interaction with other PRs
004to avoid Reserve exact Git mutations until receive completion #9's003_git_mutations.sql. Reserve exact Git mutations until receive completion #9'sreceive._policy_for_updateshould passconsumer=pusher["name"]togate.approved_instance. Otherwise an approver who pushes passes pre-receive, and post-receive then refuses to consume after Git has already moved the ref. Reserve exact Git mutations until receive completion #9's tests request and vote as the same agent, so they need this PR'stests/conftest.py::approvefixture. Its newhook post-receivecommand should usestore.session.Validation
python -m pytest -q: 122 passed on the current head (merged withmainthrough PRs Ignore removed worktrees in handoff and checkpoint #13, Use standard-library SQLite 3 for the store instead of libSQL #14 and Fix pre-existing ruff lint findings #15)mainsourcepyright: 0 errors, 0 warningsruff check src tests: cleanNew tests are in
tests/test_approval_authority.py,tests/test_worktree_continuation.py, andtests/test_store.py(session commit/rollback/close).🤖 Generated with Claude Code
https://claude.ai/code/session_01SrukvuPz7kpbore3kkKpQ3