fix(scope): wire author bootstrap scope into workflow scope guard (Closes #941)
PR #926 (#892) taught create_issue_bootstrap.bootstrap_permits_control_checkout to accept task_scope=author_issue_bootstrap and wired that canonical decision into the #274 branches-only enforcer and the #604 anti-stomp preflight. A third enforcement path was left unwired. workflow_scope_guard kept its own copy of the clean-root author decision, gated on create_issue_bootstrap.is_create_issue_task -- a task-name allowlist that never contained bootstrap_author_issue_worktree. The real call path gitea_bootstrap_author_issue_worktree -> verify_preflight_purity -> _enforce_issue_scope_guard -> workflow_scope_guard.assess_production_mutation_guards therefore raised ProductionGuardError(missing_issue_worktree) before assess_author_issue_bootstrap was ever consulted, leaving the #892 deadlock partially present and blocking issue #931. Changes: * workflow_scope_guard.assess_root_source_mutation accepts the server-derived bootstrap_assessment and, for the clean-root author case, consults the canonical bootstrap_permits_control_checkout decision instead of a local task-name allowlist. The create_issue arm is unchanged. * workflow_scope_guard.assess_production_mutation_guards forwards the evidence. * _enforce_issue_scope_guard accepts and threads the same assessment the #274 and #604 guards already consume, so all three judge identical evidence, and waives the pre-ownership issue-lock requirement for the bootstrap task via that same canonical decision rather than a second task-name allowlist. * verify_preflight_purity passes the once-computed assessment at both sites. The waiver cannot widen: the predicate fails closed on missing, malformed, cross-scope, dirty, drifted, or wrongly bound evidence, so ordinary author source and test mutation from the control checkout stays forbidden and the #274/#604/#618/#683 protections are unchanged. Regression: tests/test_issue_941_scope_guard_bootstrap_wiring.py drives the real enforcer rather than the authorization helper in isolation, which is why #892's own predicate tests passed while the live bootstrap stayed blocked. Closes #941 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+32
-1
@@ -1546,6 +1546,7 @@ def verify_preflight_purity(
|
||||
task=task,
|
||||
target_issue_number=target_issue_number,
|
||||
require_author_lock=require_author_lock,
|
||||
bootstrap_assessment=bootstrap_assessment,
|
||||
)
|
||||
# #604: common anti-stomp preflight after legacy + #683 enforcers.
|
||||
_run_anti_stomp_preflight(
|
||||
@@ -1585,6 +1586,7 @@ def verify_preflight_purity(
|
||||
task=task,
|
||||
target_issue_number=target_issue_number,
|
||||
require_author_lock=require_author_lock,
|
||||
bootstrap_assessment=bootstrap_assessment,
|
||||
)
|
||||
if force_anti_stomp:
|
||||
_run_anti_stomp_preflight(
|
||||
@@ -1652,8 +1654,15 @@ def _enforce_issue_scope_guard(
|
||||
task: str | None = None,
|
||||
target_issue_number: int | None = None,
|
||||
require_author_lock: bool = False,
|
||||
bootstrap_assessment: object = _BOOTSTRAP_UNSET,
|
||||
) -> None:
|
||||
"""#683: fail closed on missing/out-of-scope issue ownership for mutations."""
|
||||
"""#683: fail closed on missing/out-of-scope issue ownership for mutations.
|
||||
|
||||
#941: the shared bootstrap assessment is threaded in so this guard judges
|
||||
the author issue-worktree bootstrap on the same server-derived evidence as
|
||||
the #274 branches-only and #604 anti-stomp guards. Callers that supply
|
||||
none fall back to computing it here, which preserves behaviour.
|
||||
"""
|
||||
ctx = _resolve_namespace_mutation_context(worktree_path)
|
||||
workspace = ctx["workspace_path"]
|
||||
git_state = issue_lock_worktree.read_worktree_git_state(workspace)
|
||||
@@ -1703,11 +1712,32 @@ def _enforce_issue_scope_guard(
|
||||
import create_issue_bootstrap as _cib
|
||||
|
||||
is_create_issue = _cib.is_create_issue_task(task)
|
||||
# #941: consume the caller-computed bootstrap assessment when one was
|
||||
# threaded in, so this guard and the #274/#604 guards judge identical
|
||||
# evidence. Falling back preserves behaviour for callers that supply none.
|
||||
bootstrap = (
|
||||
_create_issue_bootstrap_assessment(task, worktree_path)
|
||||
if bootstrap_assessment is _BOOTSTRAP_UNSET
|
||||
else bootstrap_assessment
|
||||
)
|
||||
# #941: the author issue-worktree bootstrap is pre-ownership for the same
|
||||
# reason create_issue is — it exists to break the lock<->worktree cycle, so
|
||||
# no owning lock can exist yet. The exemption is granted by the canonical
|
||||
# shared decision over server-derived evidence, never by a task-name list,
|
||||
# and fails closed on missing, malformed, cross-scope, dirty, drifted, or
|
||||
# wrongly bound evidence.
|
||||
bootstrap_waives_ownership = _cib.bootstrap_permits_control_checkout(
|
||||
bootstrap,
|
||||
task=task,
|
||||
workspace_path=workspace,
|
||||
canonical_repo_root=ctx["canonical_repo_root"],
|
||||
)
|
||||
require_lock = bool(require_author_lock) or (
|
||||
authorish
|
||||
and workflow_scope_guard.production_guards_forced()
|
||||
and role == "author"
|
||||
and not is_create_issue
|
||||
and not bootstrap_waives_ownership
|
||||
)
|
||||
assessment = workflow_scope_guard.assess_production_mutation_guards(
|
||||
workspace_path=workspace,
|
||||
@@ -1720,6 +1750,7 @@ def _enforce_issue_scope_guard(
|
||||
require_author_lock=require_lock,
|
||||
in_test_mode=_preflight_in_test_mode(),
|
||||
mutation_task=task,
|
||||
bootstrap_assessment=bootstrap,
|
||||
)
|
||||
workflow_scope_guard.raise_if_blocked(assessment)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user