Skip to content

Normalize claim scopes and enforce them at push time - #19

Merged
Deep-Sixed merged 2 commits into
mainfrom
claude/scope-enforcement
Sep 29, 2026
Merged

Deep-Sixed merged 2 commits into
mainfrom
claude/scope-enforcement

Conversation

@Deep-Sixed

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

Copy link
Copy Markdown
Owner

Summary

Carries the scope fixes from #10 (review findings 3 and 4) onto main, which now includes the approval-authority work from #12.

#10 itself is left untouched. Its self-approval and worktree-reclaim changes are not carried over because #12 covers them. Its later commits (a reference-transaction hook protocol overlapping #9, a stricter schema check, and doctor reconciliation) aren't included either.

Changes

Scopes are canonical at claim time (work.normalize_scope, validate_scope, scopes_overlap)

  • ./src/**, /src/**, src//**, src\** and src/../src/** all normalize to src/** before being stored and compared.
  • Overlap compares normalized literal prefixes case-insensitively. That's correct for case-insensitive checkouts (macOS, Windows); elsewhere it only adds conservative flags.
  • Scopes that escape the repository root (../other/**) are refused.

Scopes are enforced at push time (gate.enforce_claim_scopes, work.path_in_scopes)

  • When a branch has a live claim, every path changed by the pushed commits (newrev --not --all) must match one of the claim's scopes, or pre-receive rejects the push and lists the offending paths.
  • --cc means a merge commit counts only the paths it changed itself, so merging the base branch in is not blamed on the claim holder.
  • Glob semantics: ** spans directories, * and ? stay within one segment, [...] is a character class, and a scope without wildcards covers that file or directory.
  • In the local model there is no hook, so scopes stay coordination metadata there. The README says so.

The scope and hook functions are identical to #10's, compared by AST. The one change is check=False on _pushed_paths' subprocess.run, to match main's pinned-ruff lint gate.

Not included from #10

  • Commit specs resolving to full OIDs, clean errors for duplicate registrations and malformed JSON, the write lock for create_task/add_agent, and handoff create --last-commit semantics. These are independent of scopes and could follow as a separate small PR.

Validation

  • python -m pytest -q: 148 passed (122 on main + 26 new in tests/test_scope_enforcement.py)
  • All 26 new tests fail without this change
  • pyright: 0 errors, 0 warnings
  • ruff check with the pinned ruff 0.16.9: clean

The new tests are #10's scope tests: normalization, overlap across spellings, escape refusal, ./ scopes classified as overlapping and stored normalized, glob matching, and two real-push hook tests (out-of-scope commits rejected, merge commits not attributed, new claimed branches covered).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SrukvuPz7kpbore3kkKpQ3

Ports the scope fixes from PR #10 (review findings 3 and 4) onto the
approval-authority work in #12. PR #10's self-approval and worktree
reclaim changes are not carried over; #12 covers those.

- Scope globs are normalized when a claim is made (./, //, leading /,
  backslashes, ..) and stored in that form. Overlap compares normalized
  prefixes case-insensitively. Scopes that escape the repository root are
  refused.
- The pre-receive hook rejects a push to a claimed branch when the new
  commits (newrev --not --all) touch paths outside the claim's scopes.
  --cc means a merge commit counts only the paths it changed itself, so
  merging the base branch in is not blamed on the claim holder.

The scope and hook functions are identical to PR #10's; _pushed_paths
adds check=False to match main's lint baseline. Tests are PR #10's scope
tests, and all 26 fail without the port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SrukvuPz7kpbore3kkKpQ3
@Deep-Sixed
Deep-Sixed changed the base branch from claude/kind-cannon-6yodm7 to main September 29, 2026 13:27
@Deep-Sixed
Deep-Sixed marked this pull request as ready for review September 29, 2026 22:35
@Deep-Sixed
Deep-Sixed merged commit 0fef70a into main Sep 29, 2026
7 checks passed
Deep-Sixed pushed a commit that referenced this pull request Sep 29, 2026
…rned scopes

main now carries #19, which rejects pushes that touch paths outside the
claim's scopes. This branch makes those pushes approval-governed instead.
Resolution keeps both:

- #19's scope normalization/validation, case-insensitive overlap, and
  path_in_scopes matcher replace this branch's duplicate glob matcher.
- This branch's git_objects path derivation (shared with approve prepare)
  replaces #19's _pushed_paths, so the hook and the planner agree on hashes.
- Out-of-scope pushes are still rejected by default, with #19's wording, but
  can now proceed with an out_of_scope_push approval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142xh1CDDPxBvB1EHu6PN3m
Deep-Sixed pushed a commit that referenced this pull request Sep 29, 2026
…e ref-transaction binding

main's content governance (#23) can require several approvals for one
ref update, where this branch recorded one operation and one approval.
Resolved by keeping main's derivation (governed_operations over
git_objects.changed_paths) and binding all of it to the transaction:

- pre-receive derives the update's paths from the received objects,
  records them with the update, and consumes nothing.
- The reference-transaction `prepared` stage re-derives the operations
  from those recorded paths against current governance state and
  consumes one approval per operation. Recording the paths matters in a
  non-atomic multi-ref push: by `prepared`, earlier refs have moved,
  which would change which commits look new and so the hashes.
- Every consumed approval is linked in ref_update_approvals, so an abort
  (or `doctor --repair` of an aborted update) restores all of them.

This branch's own scope enforcement and path helpers are dropped in
favour of main's (#19, #23). The ref_updates migration is renumbered
from 003 to 006: main already ships 004 and 005, and a gap-filling 003
would apply after them on existing stores but before them on new ones.
The README now requires Git 2.28 (for the reference-transaction hook)
instead of #18's 2.17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014R4wfmENrMnt36KN1Kj6Fx
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