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:
+30
-1
@@ -279,6 +279,7 @@ def assess_root_source_mutation(
|
||||
locked_issue_number: int | None = None,
|
||||
role_kind: str | None = None,
|
||||
mutation_task: str | None = None,
|
||||
bootstrap_assessment: Any | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Fail closed for diagnostic/source edits on the control/root checkout.
|
||||
|
||||
@@ -286,6 +287,13 @@ def assess_root_source_mutation(
|
||||
tracked source/test files on the control checkout always block, including
|
||||
temporary/diagnostic/test-only intent.
|
||||
|
||||
#941: ``bootstrap_author_issue_worktree`` is judged by the canonical
|
||||
``create_issue_bootstrap.bootstrap_permits_control_checkout`` decision over
|
||||
*bootstrap_assessment* — the same server-derived evidence the #274 and
|
||||
#604 guards consume — instead of a task-name allowlist local to this
|
||||
module. Evidence that is absent, malformed, wrongly scoped, or bound to
|
||||
another workspace leaves the ordinary block in force.
|
||||
|
||||
#749: ``create_issue`` is a pure remote mutation with no local tree write.
|
||||
When *mutation_task* is create_issue and the control checkout has no dirty
|
||||
source/test files, the missing-worktree signal is suppressed so the
|
||||
@@ -336,6 +344,19 @@ def assess_root_source_mutation(
|
||||
if _cib is not None and _cib.is_create_issue_task(mutation_task):
|
||||
# #749: clean-root create_issue is the sanctioned bootstrap path.
|
||||
create_issue_bootstrap = True
|
||||
elif _cib is not None and _cib.bootstrap_permits_control_checkout(
|
||||
bootstrap_assessment,
|
||||
task=mutation_task,
|
||||
workspace_path=workspace,
|
||||
canonical_repo_root=root,
|
||||
):
|
||||
# #941: the author issue-worktree bootstrap is authorized by the
|
||||
# canonical shared decision over server-derived task-scope
|
||||
# evidence, never by a task-name allowlist kept in this module.
|
||||
# The predicate fails closed on missing, malformed, cross-scope,
|
||||
# dirty, drifted, or wrongly bound evidence, so this arm cannot
|
||||
# widen the waiver beyond the one sanctioned bootstrap task.
|
||||
create_issue_bootstrap = True
|
||||
else:
|
||||
# Explicit missing-worktree signal for force-on author entrypoints.
|
||||
reasons.append(
|
||||
@@ -393,8 +414,15 @@ def assess_production_mutation_guards(
|
||||
require_author_lock: bool = False,
|
||||
in_test_mode: bool = False,
|
||||
mutation_task: str | None = None,
|
||||
bootstrap_assessment: Any | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Compose root + scope production guards when they must be active (#683)."""
|
||||
"""Compose root + scope production guards when they must be active (#683).
|
||||
|
||||
#941: *bootstrap_assessment* is the server-derived author-bootstrap
|
||||
evidence, forwarded unchanged to :func:`assess_root_source_mutation` so
|
||||
this guard reaches the same canonical decision as the #274 and #604
|
||||
guards. Omitting it preserves the pre-existing behaviour.
|
||||
"""
|
||||
if not production_guards_active(in_test_mode=in_test_mode):
|
||||
return {
|
||||
"proven": True,
|
||||
@@ -414,6 +442,7 @@ def assess_production_mutation_guards(
|
||||
locked_issue_number=locked_issue_number,
|
||||
role_kind=role_kind,
|
||||
mutation_task=mutation_task,
|
||||
bootstrap_assessment=bootstrap_assessment,
|
||||
)
|
||||
if root_assess["block"]:
|
||||
return {**root_assess, "skipped": False}
|
||||
|
||||
Reference in New Issue
Block a user