feat: common anti-stomp preflight before mutation tools (Closes #604)

Add a shared fail-closed anti-stomp preflight that mutation tools invoke
before create/comment/lease/review/approve/request-changes/merge/cleanup/
label mutations. Composes repo/role/root/worktree/lease/terminal-lock/
head-SHA/stale-runtime/workflow-hash/contamination checks into a typed
blocker with an exact next action. No agent bypass flags.
This commit is contained in:
2026-07-12 05:10:30 -04:00
parent 22698c1b5f
commit ea8e186549
3 changed files with 1534 additions and 1 deletions
+263 -1
View File
@@ -619,18 +619,212 @@ def _enforce_branches_only_author_mutation(worktree_path: str | None = None) ->
)
def _anti_stomp_in_test_mode() -> bool:
"""Whether the #604 anti-stomp gate is skipped under pytest.
Mirrors remote-repo / stable-contamination test bypasses so the existing
suite (bare remotes, mocked APIs) stays green. Set
``GITEA_TEST_FORCE_ANTI_STOMP=1`` to exercise the live gate under tests.
"""
if not _preflight_in_test_mode():
return False
return not bool(os.environ.get("GITEA_TEST_FORCE_ANTI_STOMP"))
def _run_anti_stomp_preflight(
task: str | None,
*,
remote: str | None = None,
worktree_path: str | None = None,
host: str | None = None,
org: str | None = None,
repo: str | None = None,
expected_head_sha: str | None = None,
live_head_sha: str | None = None,
require_head_sha: bool = False,
lease_required: bool = False,
foreign_lease: bool | None = None,
lease_reasons: list[str] | None = None,
terminal_lock_blocks: bool | None = None,
terminal_lock_reasons: list[str] | None = None,
workflow_hash_valid: bool | None = None,
workflow_hash_reasons: list[str] | None = None,
raise_on_block: bool = True,
) -> dict | None:
"""Shared #604 anti-stomp preflight for MCP mutation entrypoints.
Gathers live workspace/parity/role/repo facts and invokes the pure
:func:`anti_stomp_preflight.assess_anti_stomp_preflight` assessor. On
block, raises ``RuntimeError`` (default) or returns a structured
:func:`anti_stomp_preflight.block_response` when *raise_on_block* is
False. Returns ``None`` when the mutation may proceed.
"""
if _anti_stomp_in_test_mode():
return None
# Only enforce when the caller names a known mutation task. Read-only
# paths and legacy verify_preflight_purity calls without a task stay
# on the pre-#604 gate chain (whoami/capability/root/worktree).
if not task or not anti_stomp_preflight.is_mutation_task(task):
return None
profile = get_profile()
profile_name = profile.get("profile_name")
profile_role = _actual_profile_role()
req_role = None
if task:
try:
req_role = task_capability_map.required_role(task)
except KeyError:
req_role = _preflight_resolved_role
ctx = _resolve_namespace_mutation_context(worktree_path)
workspace = ctx["workspace_path"]
canonical_root = ctx["canonical_repo_root"]
git_state = issue_lock_worktree.read_worktree_git_state(canonical_root)
remote_master_sha = root_checkout_guard.resolve_remote_master_sha(canonical_root)
# Repo/org facts (best-effort; explicit org/repo when provided).
resolved_org = org
resolved_repo = repo
local_remote_url = None
org_explicit = org is not None
repo_explicit = repo is not None
if remote:
try:
local_remote_url = _local_git_remote_url(remote)
except Exception:
local_remote_url = None
if resolved_org is None or resolved_repo is None:
try:
rem = _effective_remote(remote)
if rem in REMOTES:
if resolved_org is None:
resolved_org = REMOTES[rem]["org"]
if resolved_repo is None:
resolved_repo = REMOTES[rem]["repo"]
except Exception:
pass
parity = _current_master_parity()
startup_head = parity.get("startup_head")
current_code_head = parity.get("current_head")
# Source contamination from durable #671 marker (role-aware).
source_contaminated = None
contamination_reasons = None
try:
marker = _load_stable_contamination_marker(remote)
contaminated, cont_reasons = anti_stomp_preflight.contamination_from_stable_marker(
marker,
task=task,
actual_role=profile_role,
)
if marker is not None:
source_contaminated = contaminated
contamination_reasons = cont_reasons
except Exception:
# Fail soft on marker I/O: existing #671 enforcer still runs separately.
source_contaminated = None
# Workflow-hash facts when the task is review/merge oriented.
if workflow_hash_valid is None and task in {
"review_pr",
"submit_pr_review",
"approve_pr",
"request_changes_pr",
"merge_pr",
"mark_final_review_decision",
}:
try:
wf_reasons = _review_workflow_load_gate_reasons()
workflow_hash_valid = not bool(wf_reasons)
workflow_hash_reasons = list(wf_reasons) if wf_reasons else None
except Exception:
pass
# Mirror remote_repo_guard's pytest bypass: under the unit suite bare
# remote=prgs still defaults to Timesheet, and re-checking here would
# break happy-path reconciler/author tests that already rely on the
# #530 gate being opt-in via GITEA_FORCE_REMOTE_REPO_CHECK.
check_repo = True
if "pytest" in sys.modules and not os.environ.get(
"GITEA_FORCE_REMOTE_REPO_CHECK"
):
check_repo = False
assessment = anti_stomp_preflight.assess_anti_stomp_preflight(
task=task,
remote=remote,
resolved_org=resolved_org,
resolved_repo=resolved_repo,
local_remote_url=local_remote_url,
org_explicit=org_explicit,
repo_explicit=repo_explicit,
check_repo=check_repo,
profile_name=profile_name,
profile_role=profile_role,
required_role=req_role or _preflight_resolved_role,
workspace_path=workspace,
project_root=canonical_root,
current_branch=git_state.get("current_branch"),
root_head_sha=git_state.get("head_sha"),
root_porcelain=git_state.get("porcelain_status") or "",
remote_master_sha=remote_master_sha,
startup_head=startup_head,
current_code_head=current_code_head,
lease_required=lease_required,
foreign_lease=foreign_lease,
lease_reasons=lease_reasons,
terminal_lock_blocks=terminal_lock_blocks,
terminal_lock_reasons=terminal_lock_reasons,
require_head_sha=require_head_sha,
expected_head_sha=expected_head_sha,
live_head_sha=live_head_sha,
workflow_hash_valid=workflow_hash_valid,
workflow_hash_reasons=workflow_hash_reasons,
source_contaminated=source_contaminated,
contamination_reasons=contamination_reasons,
manual_bypass_attempted=False,
)
if not assessment.get("block"):
return None
if raise_on_block:
raise RuntimeError(anti_stomp_preflight.format_anti_stomp_error(assessment))
return anti_stomp_preflight.block_response(assessment)
def verify_preflight_purity(
remote: str | None = None,
worktree_path: str | None = None,
task: str | None = None,
*,
expected_head_sha: str | None = None,
live_head_sha: str | None = None,
require_head_sha: bool = False,
lease_required: bool = False,
foreign_lease: bool | None = None,
lease_reasons: list[str] | None = None,
terminal_lock_blocks: bool | None = None,
terminal_lock_reasons: list[str] | None = None,
workflow_hash_valid: bool | None = None,
workflow_hash_reasons: list[str] | None = None,
org: str | None = None,
repo: str | None = None,
):
"""Verify that identity and capability were verified prior to session edits."""
"""Verify that identity and capability were verified prior to session edits.
Also runs the shared #604 anti-stomp preflight for mutation tasks (typed
blocker + exact next action). Extended lease/head/terminal facts may be
supplied by review/merge callers.
"""
global _preflight_reviewer_violation_files
in_test = _preflight_in_test_mode()
if in_test and not (
os.environ.get("GITEA_TEST_FORCE_DIRTY")
or os.environ.get("GITEA_TEST_PORCELAIN") is not None
or os.environ.get("GITEA_TEST_FORCE_ANTI_STOMP")
):
return
@@ -717,6 +911,30 @@ def verify_preflight_purity(
_enforce_root_checkout_guard(worktree_path)
_enforce_branches_only_author_mutation(worktree_path)
# #604: common anti-stomp preflight (typed blocker + exact next action).
# Runs after the legacy enforcers so existing happy paths stay green and
# the shared module remains the single fail-closed composition point for
# repo/role/worktree/lease/terminal-lock/head/stale/workflow/contamination.
_run_anti_stomp_preflight(
task,
remote=remote,
worktree_path=worktree_path,
org=org,
repo=repo,
expected_head_sha=expected_head_sha,
live_head_sha=live_head_sha,
require_head_sha=require_head_sha,
lease_required=lease_required,
foreign_lease=foreign_lease,
lease_reasons=lease_reasons,
terminal_lock_blocks=terminal_lock_blocks,
terminal_lock_reasons=terminal_lock_reasons,
workflow_hash_valid=workflow_hash_valid,
workflow_hash_reasons=workflow_hash_reasons,
raise_on_block=True,
)
_clear_preflight_capability_state()
@@ -930,6 +1148,7 @@ import author_mutation_worktree # noqa: E402
import root_checkout_guard # noqa: E402
import stable_branch_push_guard # noqa: E402
import remote_repo_guard # noqa: E402
import anti_stomp_preflight # noqa: E402
import issue_claim_heartbeat # noqa: E402
import issue_work_duplicate_gate # noqa: E402
import issue_workflow_labels # noqa: E402
@@ -3833,6 +4052,30 @@ def _evaluate_pr_review_submission(
result["pr_work_lease"] = lease_block
return result
if live:
# #604: common anti-stomp with live head + lease proof (typed blocker).
anti_task = {
"approve": "approve_pr",
"request_changes": "request_changes_pr",
"comment": "submit_pr_review",
}.get(action, "submit_pr_review")
try:
_run_anti_stomp_preflight(
anti_task,
remote=remote,
worktree_path=worktree_path,
org=org,
repo=repo,
expected_head_sha=pinned_sha,
live_head_sha=actual_sha,
require_head_sha=True,
foreign_lease=False,
terminal_lock_blocks=False,
)
except RuntimeError as exc:
reasons.append(str(exc))
return result
if (body or "").strip():
gate = _canonical_comment_gate(body, context="pr_review")
if gate["blocked"]:
@@ -5269,6 +5512,25 @@ def gitea_merge_pr(
reasons.extend(lease_block.get("reasons") or [])
result["pr_work_lease"] = lease_block
return result
# #604: common anti-stomp with live head + lease proof (typed blocker).
try:
_run_anti_stomp_preflight(
"merge_pr",
remote=remote,
worktree_path=worktree_path,
org=org,
repo=repo,
expected_head_sha=expected_head_sha,
live_head_sha=actual_sha,
require_head_sha=True,
foreign_lease=False,
terminal_lock_blocks=False,
)
except RuntimeError as exc:
reasons.append(str(exc))
return result
if expected_head_sha and actual_sha and expected_head_sha != actual_sha:
reasons.append(
"expected head SHA does not match current PR head (fail closed)"