fix(author): gate bootstrap-lock recovery on the namespace mutation wall (#953)

Review 632 F1. `gitea_recover_incomplete_bootstrap_lock` writes the same
durable author issue lock as `gitea_recover_dirty_orphaned_issue_worktree`
but gated only on the reviewer-stop router check and the profile permission
block. Every other author state-creating mutation carries a third gate,
`_namespace_mutation_block`, and this tool was the outlier.

The surviving gates did not cover the gap: the task's required permission is
`gitea.issue.comment`, which every configured role holds, and
`_ensure_matching_profile` returns a profile name both call sites discard, so
it refuses nothing. A reviewer-bound session reached the exact-owner claimant
comparison inside `assess_bootstrap_lock_recovery` and was refused there —
one layer too late, with no namespace evaluation and no BLOCKED audit record
of the attempt.

Adding the call alone would have been inert. `check_author_mutation_namespace`
routes through `role_session_router.required_role_for_task`, which reads
`TASK_REQUIRED_ROLE` — not `task_capability_map` — and that table had no entry
for this task, so the check short-circuited to "allowed" for every caller.
The task is therefore registered in `TASK_REQUIRED_ROLE` and `AUTHOR_TASKS`,
matching the role the capability map already records.

The reviewer-namespace check alone is also insufficient for a task gated on
`gitea.issue.comment`, since merger, controller, and reconciler profiles hold
it too. `role_namespace_gate.check_author_role_kind` is added as an opt-in,
additive wall requiring the active profile's derived role kind to be exactly
`author`; `mixed` is refused rather than admitted. `_namespace_mutation_block`
gains a keyword-only `author_role_exclusive` flag, default off, so the six
pre-existing call sites are byte-for-byte unchanged.

Refusals produce the standard structured denial (`namespace_block: true`,
`mcp_namespace`) and the standard BLOCKED audit record, and no lock, lease,
generation, branch, worktree, issue, or PR is touched. Valid `prgs-author`
execution is unaffected. No reviewer permission is broadened and no existing
role wall is weakened.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-28 02:08:21 -04:00
co-authored by Claude Opus 4.8
parent cdf0daefa9
commit 55d66c57e4
3 changed files with 80 additions and 2 deletions
+10
View File
@@ -75,6 +75,10 @@ AUTHOR_TASKS = frozenset({
"push_branch",
"bootstrap_author_issue_worktree",
"gitea_bootstrap_author_issue_worktree",
# #953: recovery of an incomplete bootstrap lock is an author-only durable
# state mutation and belongs to the same class as bootstrap itself.
"recover_incomplete_bootstrap_lock",
"gitea_recover_incomplete_bootstrap_lock",
"create_pr",
"comment_pr",
"address_pr_change_requests",
@@ -112,6 +116,12 @@ TASK_REQUIRED_ROLE = {
"claim_issue": "author",
"create_branch": "author",
"push_branch": "author",
# #953: without this entry ``required_role_for_task`` returns None and
# ``role_namespace_gate.check_author_mutation_namespace`` short-circuits to
# "allowed" — the namespace wall on the recovery tool would be inert. The
# capability map already records the same role; both tables must agree.
"recover_incomplete_bootstrap_lock": "author",
"gitea_recover_incomplete_bootstrap_lock": "author",
"create_pr": "author",
"comment_pr": "author",
"address_pr_change_requests": "author",