feat: merge-report lease proof fields and handoff audit (#535)
Surface lease_proof_source / recovery reason from sanctioned session provenance on gitea_merge_pr results, and block final reports that claim manual/seeded/injected lease proof without MCP adoption evidence. Closes #535
This commit is contained in:
@@ -14,6 +14,7 @@ from typing import Any, Callable
|
||||
import branch_cleanup_guard
|
||||
import issue_acceptance_gate
|
||||
import issue_lock_provenance
|
||||
import merger_lease_adoption
|
||||
import reviewer_handoff_consistency
|
||||
import thread_state_ledger_validator
|
||||
from mcp_native_cleanup_proof import assess_mcp_native_cleanup_proof
|
||||
@@ -1263,6 +1264,24 @@ def _rule_shared_manual_lock_pr_override(report_text: str) -> list[dict[str, str
|
||||
)
|
||||
|
||||
|
||||
def _rule_shared_manual_lease_proof_handoff(report_text: str) -> list[dict[str, str]]:
|
||||
"""Block merge/review handoffs that claim unsafe lease seeding (#535)."""
|
||||
result = merger_lease_adoption.assess_manual_lease_proof_handoff(report_text)
|
||||
if result.get("proven"):
|
||||
return []
|
||||
return _findings_from_reasons(
|
||||
"shared.manual_lease_proof_handoff",
|
||||
result.get("reasons") or [],
|
||||
field="Merge mutations",
|
||||
severity="block",
|
||||
safe_next_action=(
|
||||
"use gitea_adopt_merger_pr_lease or gitea_acquire_reviewer_pr_lease; "
|
||||
"cite lease_proof_source / adoption_comment_id; never claim manual "
|
||||
"seeded/injected lease proof"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _rule_shared_issue_acceptance_gate(report_text: str) -> list[dict[str, str]]:
|
||||
result = issue_acceptance_gate.validate_final_report_issue_acceptance(report_text)
|
||||
if not result.get("applicable") or result.get("valid"):
|
||||
@@ -1459,6 +1478,7 @@ def _rule_shared_mcp_native_cleanup_proof(report_text: str) -> list[dict[str, st
|
||||
_SHARED_ISSUE_LOCK_RULES = (
|
||||
_rule_shared_issue_lock_external_state,
|
||||
_rule_shared_manual_lock_pr_override,
|
||||
_rule_shared_manual_lease_proof_handoff,
|
||||
_rule_shared_author_reviewer_same_run,
|
||||
_rule_shared_raw_branch_delete_bypass,
|
||||
_rule_shared_canonical_state_update,
|
||||
|
||||
@@ -4367,6 +4367,13 @@ def gitea_merge_pr(
|
||||
live_head_sha=result.get("head_sha"),
|
||||
pinned_head_sha=expected_head_sha,
|
||||
))
|
||||
# Always surface how the in-session lease was established (#535), even when
|
||||
# the lease gate blocks (e.g. unsanctioned manual _SESSION_LEASE seed).
|
||||
result.update(
|
||||
merger_lease_adoption.describe_session_lease_proof(
|
||||
reviewer_pr_lease.get_session_lease()
|
||||
)
|
||||
)
|
||||
if reasons:
|
||||
return result
|
||||
|
||||
@@ -4524,6 +4531,12 @@ def gitea_merge_pr(
|
||||
|
||||
result["performed"] = True
|
||||
result["merge_result"] = f"PR #{pr_number} merged via '{do}'."
|
||||
# Re-attach after success in case session lease was heartbeated mid-path.
|
||||
result.update(
|
||||
merger_lease_adoption.describe_session_lease_proof(
|
||||
reviewer_pr_lease.get_session_lease()
|
||||
)
|
||||
)
|
||||
reasons.append(f"all gates passed; merged PR #{pr_number} via '{do}'")
|
||||
return result
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ after formal approval at the current PR head.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
@@ -133,6 +134,115 @@ def is_sanctioned_session_lease(session: dict[str, Any] | None) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def describe_session_lease_proof(
|
||||
session: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Canonical merge-report lease proof fields (#535).
|
||||
|
||||
Surfaces how the in-session lease was established so merge handoffs can
|
||||
distinguish sanctioned MCP acquire/adopt paths from bare manual seeding.
|
||||
"""
|
||||
if not session:
|
||||
return {
|
||||
"lease_proof_source": None,
|
||||
"lease_recovery_reason": None,
|
||||
"lease_proof_kind": "none",
|
||||
"lease_proof_sanctioned": False,
|
||||
"lease_proof_comment_id": None,
|
||||
"lease_adopted_from_session_id": None,
|
||||
}
|
||||
|
||||
provenance = session.get("lease_provenance") or {}
|
||||
if not isinstance(provenance, dict):
|
||||
provenance = {}
|
||||
source = (provenance.get("source") or "").strip() or None
|
||||
sanctioned = is_sanctioned_session_lease(session)
|
||||
reason = provenance.get("adoption_reason")
|
||||
if isinstance(reason, str):
|
||||
reason = reason.strip() or None
|
||||
else:
|
||||
reason = None
|
||||
|
||||
if source == SOURCE_ADOPT and sanctioned:
|
||||
kind = "sanctioned_adoption"
|
||||
reason = reason or DEFAULT_ADOPTION_REASON
|
||||
elif source == SOURCE_ACQUIRE and sanctioned:
|
||||
kind = "sanctioned_acquire"
|
||||
elif source == SOURCE_HEARTBEAT and sanctioned:
|
||||
kind = "sanctioned_heartbeat"
|
||||
elif source:
|
||||
kind = "unsanctioned"
|
||||
else:
|
||||
# Session present without provenance = classic manual _SESSION_LEASE seed.
|
||||
kind = "unsanctioned_manual_seed"
|
||||
|
||||
comment_id = provenance.get("comment_id")
|
||||
if comment_id is None:
|
||||
comment_id = session.get("comment_id")
|
||||
|
||||
return {
|
||||
"lease_proof_source": source,
|
||||
"lease_recovery_reason": reason,
|
||||
"lease_proof_kind": kind,
|
||||
"lease_proof_sanctioned": sanctioned,
|
||||
"lease_proof_comment_id": comment_id,
|
||||
"lease_adopted_from_session_id": provenance.get("adopted_from_session_id"),
|
||||
}
|
||||
|
||||
|
||||
# Phrases that claim non-MCP / fabricated lease proof in handoffs (#535).
|
||||
_UNSAFE_LEASE_PROOF_CLAIM_RE = re.compile(
|
||||
r"(?is)\b("
|
||||
r"manually\s+seeded\s+lease|"
|
||||
r"manual(?:ly)?\s+seed(?:ed)?\s+(?:lease|proof)|"
|
||||
r"seeded\s+lease\s+proof|"
|
||||
r"injected\s+lease|"
|
||||
r"lease\s+proof\s+injected|"
|
||||
r"manual\s+_?SESSION_LEASE|"
|
||||
r"_SESSION_LEASE\s+seed|"
|
||||
r"unsanctioned\s+lease\s+proof"
|
||||
r")\b"
|
||||
)
|
||||
|
||||
# Evidence that the report used the sanctioned MCP adoption/acquire path.
|
||||
_SANCTIONED_LEASE_EVIDENCE_RE = re.compile(
|
||||
r"(?is)\b("
|
||||
r"gitea_adopt_merger_pr_lease|"
|
||||
r"gitea_acquire_reviewer_pr_lease|"
|
||||
r"lease_proof_source\s*[:=]\s*gitea_adopt_merger_pr_lease|"
|
||||
r"lease_proof_source\s*[:=]\s*gitea_acquire_reviewer_pr_lease|"
|
||||
r"lease_proof_kind\s*[:=]\s*sanctioned_adoption|"
|
||||
r"lease_proof_kind\s*[:=]\s*sanctioned_acquire|"
|
||||
r"adoption_comment_id\s*[:=]|"
|
||||
r"sanctioned_adoption|"
|
||||
r"mcp-review-lease-adoption"
|
||||
r")\b"
|
||||
)
|
||||
|
||||
|
||||
def assess_manual_lease_proof_handoff(report_text: str) -> dict[str, Any]:
|
||||
"""Controller audit: block handoffs that claim unsafe lease seeding (#535).
|
||||
|
||||
Reports may discuss manual seeding as a blocked/historical anti-pattern only
|
||||
when they also cite sanctioned MCP adoption/acquire evidence. Bare claims of
|
||||
manual/seeded/injected lease proof without that evidence fail closed.
|
||||
"""
|
||||
text = report_text or ""
|
||||
if not _UNSAFE_LEASE_PROOF_CLAIM_RE.search(text):
|
||||
return {"proven": True, "block": False, "reasons": []}
|
||||
if _SANCTIONED_LEASE_EVIDENCE_RE.search(text):
|
||||
return {"proven": True, "block": False, "reasons": []}
|
||||
return {
|
||||
"proven": False,
|
||||
"block": True,
|
||||
"reasons": [
|
||||
"report claims manual/seeded/injected/unsanctioned lease proof without "
|
||||
"sanctioned MCP adoption evidence (use gitea_adopt_merger_pr_lease / "
|
||||
"gitea_acquire_reviewer_pr_lease and cite lease_proof_source; #535)"
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def assess_adopt_merger_lease(
|
||||
comments: list[dict],
|
||||
*,
|
||||
|
||||
@@ -781,6 +781,103 @@ class TestMergePrRules(unittest.TestCase):
|
||||
self.assertEqual(result["grade"], "A")
|
||||
self.assertFalse(result["blocked"])
|
||||
|
||||
def test_manual_seeded_lease_claim_blocks_without_mcp_evidence(self):
|
||||
lines = [
|
||||
"## Controller Handoff",
|
||||
"",
|
||||
"- Task: merge PR #203",
|
||||
"- Repo: Scaled-Tech-Consulting/Gitea-Tools",
|
||||
"- Role: merger",
|
||||
"- Identity: sysadmin / prgs-merger",
|
||||
"- Issue/PR: #182 / PR #203",
|
||||
"- Branch/SHA: feat/x @ 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Files changed: review_proofs.py",
|
||||
"- Validation: pytest in branches/review-203",
|
||||
"- Mutations: merge completed",
|
||||
"- File edits by reviewer: none",
|
||||
"- Worktree/index mutations: none",
|
||||
"- Git ref mutations: git fetch prgs master",
|
||||
"- MCP/Gitea mutations: merge completed",
|
||||
"- Review mutations: none",
|
||||
"- Merge mutations: merged using manually seeded lease proof",
|
||||
"- Cleanup mutations: none",
|
||||
"- External-state mutations: none",
|
||||
"- Read-only diagnostics: metadata check",
|
||||
"- Current status: PR merged",
|
||||
"- Blockers: none",
|
||||
"- Next: none",
|
||||
"- Safety: review and merge are separate roles",
|
||||
"- Selected PR: #203",
|
||||
"- Pinned reviewed head: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Reviewed head SHA: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Final live head SHA before approval: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Final live head SHA before merge: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Push occurred during validation: no",
|
||||
"- Active profile: prgs-merger",
|
||||
"- Role kind: merger",
|
||||
"- Merge capability source: profile",
|
||||
"- Explicit operator authorization: MERGE PR 203",
|
||||
"- Expected head SHA: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Approval at current head: yes",
|
||||
"- Merge result: PR merged successfully",
|
||||
"- Cleanup status: complete",
|
||||
]
|
||||
report = "\n".join(lines)
|
||||
result = assess_final_report_validator(report, "merge_pr")
|
||||
self.assertTrue(result["blocked"])
|
||||
codes = {f.get("rule_id") for f in result.get("findings") or []}
|
||||
self.assertIn("shared.manual_lease_proof_handoff", codes)
|
||||
|
||||
def test_manual_seeded_claim_allowed_when_adoption_cited(self):
|
||||
lines = [
|
||||
"## Controller Handoff",
|
||||
"",
|
||||
"- Task: merge PR #203",
|
||||
"- Repo: Scaled-Tech-Consulting/Gitea-Tools",
|
||||
"- Role: merger",
|
||||
"- Identity: sysadmin / prgs-merger",
|
||||
"- Issue/PR: #182 / PR #203",
|
||||
"- Branch/SHA: feat/x @ 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Files changed: review_proofs.py",
|
||||
"- Validation: pytest in branches/review-203",
|
||||
"- Mutations: merge completed",
|
||||
"- File edits by reviewer: none",
|
||||
"- Worktree/index mutations: none",
|
||||
"- Git ref mutations: git fetch prgs master",
|
||||
"- MCP/Gitea mutations: gitea_adopt_merger_pr_lease then merge",
|
||||
"- Review mutations: none",
|
||||
"- Merge mutations: historical manual seed replaced by sanctioned adoption",
|
||||
"- Cleanup mutations: none",
|
||||
"- External-state mutations: none",
|
||||
"- Read-only diagnostics: metadata check",
|
||||
"- Current status: PR merged",
|
||||
"- Blockers: none",
|
||||
"- Next: none",
|
||||
"- Safety: review and merge are separate roles",
|
||||
"- Selected PR: #203",
|
||||
"- Pinned reviewed head: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Reviewed head SHA: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Final live head SHA before approval: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Final live head SHA before merge: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Push occurred during validation: no",
|
||||
"- Active profile: prgs-merger",
|
||||
"- Role kind: merger",
|
||||
"- Merge capability source: profile",
|
||||
"- Explicit operator authorization: MERGE PR 203",
|
||||
"- Expected head SHA: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Approval at current head: yes",
|
||||
"- lease_proof_source: gitea_adopt_merger_pr_lease",
|
||||
"- lease_proof_kind: sanctioned_adoption",
|
||||
"- adoption_comment_id: 8288",
|
||||
"- Merge result: PR merged successfully",
|
||||
"- Cleanup status: complete",
|
||||
]
|
||||
report = "\n".join(lines)
|
||||
result = assess_final_report_validator(report, "merge_pr")
|
||||
self.assertFalse(result["blocked"])
|
||||
codes = {f.get("rule_id") for f in result.get("findings") or []}
|
||||
self.assertNotIn("shared.manual_lease_proof_handoff", codes)
|
||||
|
||||
def test_missing_merger_fields_blocks(self):
|
||||
lines = [
|
||||
"## Controller Handoff",
|
||||
|
||||
@@ -42,6 +42,96 @@ def _lease_comment(
|
||||
HEAD = "f" * 40
|
||||
|
||||
|
||||
class TestDescribeSessionLeaseProof(unittest.TestCase):
|
||||
def setUp(self):
|
||||
leases.clear_session_lease()
|
||||
|
||||
def test_empty_session_is_none_kind(self):
|
||||
proof = mla.describe_session_lease_proof(None)
|
||||
self.assertIsNone(proof["lease_proof_source"])
|
||||
self.assertEqual(proof["lease_proof_kind"], "none")
|
||||
self.assertFalse(proof["lease_proof_sanctioned"])
|
||||
|
||||
def test_manual_seed_without_provenance(self):
|
||||
leases.record_session_lease({
|
||||
"pr_number": 536,
|
||||
"session_id": "manual",
|
||||
"comment_id": 1,
|
||||
})
|
||||
proof = mla.describe_session_lease_proof(leases.get_session_lease())
|
||||
self.assertIsNone(proof["lease_proof_source"])
|
||||
self.assertEqual(proof["lease_proof_kind"], "unsanctioned_manual_seed")
|
||||
self.assertFalse(proof["lease_proof_sanctioned"])
|
||||
|
||||
def test_sanctioned_adopt_fields(self):
|
||||
leases.record_session_lease(
|
||||
{
|
||||
"pr_number": 536,
|
||||
"session_id": "merger-sess",
|
||||
"comment_id": 700,
|
||||
},
|
||||
lease_provenance=mla.build_lease_provenance(
|
||||
source=mla.SOURCE_ADOPT,
|
||||
comment_id=700,
|
||||
adopted_from_session_id="reviewer-sess",
|
||||
adoption_reason=mla.DEFAULT_ADOPTION_REASON,
|
||||
),
|
||||
)
|
||||
proof = mla.describe_session_lease_proof(leases.get_session_lease())
|
||||
self.assertEqual(proof["lease_proof_source"], mla.SOURCE_ADOPT)
|
||||
self.assertEqual(proof["lease_proof_kind"], "sanctioned_adoption")
|
||||
self.assertTrue(proof["lease_proof_sanctioned"])
|
||||
self.assertEqual(
|
||||
proof["lease_recovery_reason"], mla.DEFAULT_ADOPTION_REASON
|
||||
)
|
||||
self.assertEqual(proof["lease_proof_comment_id"], 700)
|
||||
self.assertEqual(proof["lease_adopted_from_session_id"], "reviewer-sess")
|
||||
|
||||
def test_sanctioned_acquire_fields(self):
|
||||
leases.record_session_lease(
|
||||
{
|
||||
"pr_number": 536,
|
||||
"session_id": "rev",
|
||||
"comment_id": 50,
|
||||
},
|
||||
lease_provenance=mla.build_lease_provenance(
|
||||
source=mla.SOURCE_ACQUIRE,
|
||||
comment_id=50,
|
||||
),
|
||||
)
|
||||
proof = mla.describe_session_lease_proof(leases.get_session_lease())
|
||||
self.assertEqual(proof["lease_proof_source"], mla.SOURCE_ACQUIRE)
|
||||
self.assertEqual(proof["lease_proof_kind"], "sanctioned_acquire")
|
||||
self.assertTrue(proof["lease_proof_sanctioned"])
|
||||
|
||||
|
||||
class TestManualLeaseProofHandoffAudit(unittest.TestCase):
|
||||
def test_clean_merge_report_passes(self):
|
||||
text = (
|
||||
"Merged via gitea_merge_pr after gitea_adopt_merger_pr_lease. "
|
||||
"lease_proof_source: gitea_adopt_merger_pr_lease"
|
||||
)
|
||||
result = mla.assess_manual_lease_proof_handoff(text)
|
||||
self.assertTrue(result["proven"])
|
||||
self.assertFalse(result["block"])
|
||||
|
||||
def test_manual_seeded_claim_without_mcp_evidence_blocks(self):
|
||||
text = "Merged using manually seeded lease proof from prior session."
|
||||
result = mla.assess_manual_lease_proof_handoff(text)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(result["block"])
|
||||
self.assertTrue(any("#535" in r for r in result["reasons"]))
|
||||
|
||||
def test_historical_manual_mention_allowed_with_adoption_evidence(self):
|
||||
text = (
|
||||
"Historical incident used manually seeded lease; this merge used "
|
||||
"gitea_adopt_merger_pr_lease with adoption_comment_id: 8288."
|
||||
)
|
||||
result = mla.assess_manual_lease_proof_handoff(text)
|
||||
self.assertTrue(result["proven"])
|
||||
self.assertFalse(result["block"])
|
||||
|
||||
|
||||
class TestSanctionedProvenance(unittest.TestCase):
|
||||
def setUp(self):
|
||||
leases.clear_session_lease()
|
||||
|
||||
Reference in New Issue
Block a user