From 2b4c291f12bd3433cad5654d27ad6924f725cb20 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Thu, 9 Jul 2026 12:56:12 -0400 Subject: [PATCH] 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 --- final_report_validator.py | 20 +++++ gitea_mcp_server.py | 13 ++++ merger_lease_adoption.py | 110 +++++++++++++++++++++++++++ tests/test_final_report_validator.py | 97 +++++++++++++++++++++++ tests/test_merger_lease_adoption.py | 90 ++++++++++++++++++++++ 5 files changed, 330 insertions(+) diff --git a/final_report_validator.py b/final_report_validator.py index 4a5aa0b..143e1ae 100644 --- a/final_report_validator.py +++ b/final_report_validator.py @@ -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, diff --git a/gitea_mcp_server.py b/gitea_mcp_server.py index e41b202..5d094fb 100644 --- a/gitea_mcp_server.py +++ b/gitea_mcp_server.py @@ -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 diff --git a/merger_lease_adoption.py b/merger_lease_adoption.py index 8c05de1..821779a 100644 --- a/merger_lease_adoption.py +++ b/merger_lease_adoption.py @@ -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], *, diff --git a/tests/test_final_report_validator.py b/tests/test_final_report_validator.py index 123e828..fec1307 100644 --- a/tests/test_final_report_validator.py +++ b/tests/test_final_report_validator.py @@ -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", diff --git a/tests/test_merger_lease_adoption.py b/tests/test_merger_lease_adoption.py index 8cd681d..1227c0a 100644 --- a/tests/test_merger_lease_adoption.py +++ b/tests/test_merger_lease_adoption.py @@ -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()