fix(mcp): honor the create-issue bootstrap in anti-stomp preflight (Closes #757)
The sanctioned create_issue bootstrap from #749/#750 was unreachable in
production. Two guards assessed the same workspace for the same task and
reached opposite conclusions: the #274 branches-only guard consulted the
bootstrap and permitted a clean canonical control checkout, then the #604
anti-stomp preflight -- which never consulted it -- rejected that same
checkout as wrong_worktree.
The defect was wiring, not policy: the bootstrap decision was computed in
one guard and discarded, while the other re-derived a conflicting answer
from a lower-level assessor with no notion of the bootstrap phase.
Fix: one computation site, one interpretation site.
* create_issue_bootstrap.bootstrap_permits_control_checkout() is the single
predicate both guards use to interpret an assessment. It is fail-closed by
construction: missing, malformed, refused, incomplete, or contradictory
evidence returns False and leaves the ordinary block in force. It also
verifies the assessment describes the exact workspace and canonical root
being guarded, so a stale or foreign assessment cannot be reused.
* _create_issue_bootstrap_assessment() computes the assessment once per
preflight from inspected repository state. verify_preflight_purity threads
that single result into both guards.
* The #604 assessor accepts the assessment and waives ONLY the wrong-worktree
verdict. Root checkout, repo, role, stale runtime, lease, head-SHA,
workflow-hash, and contamination checks are evaluated independently and
still apply.
Evidence is server-derived only and travels an internal path: no MCP tool
signature gains a bootstrap argument, and no caller-controlled boolean can
manufacture eligibility. Behavior is unchanged for callers that supply no
evidence, and for every non-create_issue author mutation.
No issue or PR number is special-cased in production behavior.
Tests: new tests/test_issue_757_bootstrap_guard_agreement.py (38 tests, 26
subtests) covering the shared predicate, the narrow waiver, guard agreement
across the full workspace-state matrix, non-forgeable eligibility, and an
end-to-end native gitea_create_issue run with the #604 gate LIVE. All 38
fail against unfixed sources; the e2e reproduces the production error text
verbatim ("Anti-stomp preflight (#604) blocked mutation [wrong_worktree]").
tests/test_reconciler_close_workspace_guard.py: one case asserted that
create_issue stays blocked on the control checkout, which only held because
the bootstrap-blind #604 guard was overriding #750 -- it encoded the defect.
Re-pointed to lock_issue, which is issue-backed and legitimately still
requires a branches/ worktree. Its teardown now restores the module-level
preflight task/role so test order cannot leak resolved state.
Full suite: 3624 passed, 2 failed, 6 skipped (426 subtests).
Baseline at bde5c5fb on a clean detached worktree: 3586 passed, 2 failed,
6 skipped (400 subtests). The same 2 failures reproduce identically on
pristine master and are unrelated to this change
(test_issue_702_review_findings_f1_f6 F1 worktree recovery;
test_reconciler_supersession_close org/repo forwarding).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -179,6 +179,76 @@ def assess_create_issue_bootstrap(
|
||||
)
|
||||
|
||||
|
||||
def bootstrap_permits_control_checkout(
|
||||
assessment: Any,
|
||||
*,
|
||||
task: str | None,
|
||||
workspace_path: str | None,
|
||||
canonical_repo_root: str | None,
|
||||
) -> bool:
|
||||
"""Single interpretation of a bootstrap assessment (#757).
|
||||
|
||||
Both author-mutation guards — the #274 branches-only enforcer and the #604
|
||||
anti-stomp preflight — route their "may this workspace mutate" decision
|
||||
through this predicate, so the two can never reach opposite conclusions
|
||||
about identical evidence.
|
||||
|
||||
Fail-closed by construction. Every proof obligation must be present and
|
||||
affirmative in *assessment*, and the assessment must describe the very
|
||||
workspace and canonical root being guarded. A missing, malformed, refused,
|
||||
incomplete, or contradictory assessment returns ``False``, which leaves the
|
||||
caller's ordinary block in force.
|
||||
|
||||
``assessment`` is server-derived only: it is produced by
|
||||
:func:`assess_create_issue_bootstrap` from inspected repository state. It is
|
||||
never accepted from an MCP tool argument, so no caller can assert
|
||||
eligibility it has not proven.
|
||||
"""
|
||||
if not isinstance(assessment, dict):
|
||||
return False
|
||||
if not is_create_issue_task(task):
|
||||
return False
|
||||
|
||||
# Positive proof: the assessment must affirmatively allow, with no
|
||||
# competing refusal or not-applicable disposition recorded alongside it.
|
||||
if assessment.get("allowed") is not True:
|
||||
return False
|
||||
if assessment.get("proven") is not True:
|
||||
return False
|
||||
if assessment.get("block") is not False:
|
||||
return False
|
||||
if assessment.get("not_applicable") is not False:
|
||||
return False
|
||||
if assessment.get("reasons"):
|
||||
return False
|
||||
|
||||
# Scope proof: only the create_issue bootstrap, only via the clean
|
||||
# canonical control checkout path.
|
||||
if assessment.get("task_scope") != "create_issue_only":
|
||||
return False
|
||||
if assessment.get("bootstrap_path") != "clean_canonical_control_checkout":
|
||||
return False
|
||||
|
||||
# State proof: clean, and not a branches/ worktree (those keep #274).
|
||||
if assessment.get("dirty_files"):
|
||||
return False
|
||||
if assessment.get("under_branches") is not False:
|
||||
return False
|
||||
|
||||
# Binding proof: the assessment must describe *this* workspace and root,
|
||||
# and that workspace must be exactly the canonical control checkout.
|
||||
root = os.path.realpath(canonical_repo_root or "")
|
||||
workspace = os.path.realpath(workspace_path or root or ".")
|
||||
if not root or workspace != root:
|
||||
return False
|
||||
if assessment.get("canonical_repo_root") != root:
|
||||
return False
|
||||
if assessment.get("workspace_path") != workspace:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def format_create_issue_bootstrap_error(assessment: dict[str, Any]) -> str:
|
||||
"""RuntimeError / typed-block message for a failed bootstrap assessment."""
|
||||
reasons = "; ".join(
|
||||
|
||||
Reference in New Issue
Block a user