feat: merge-report lease proof fields and handoff audit (Closes #535) #585
@@ -14,6 +14,7 @@ from typing import Any, Callable
|
|||||||
import branch_cleanup_guard
|
import branch_cleanup_guard
|
||||||
import issue_acceptance_gate
|
import issue_acceptance_gate
|
||||||
import issue_lock_provenance
|
import issue_lock_provenance
|
||||||
|
import merger_lease_adoption
|
||||||
import reviewer_handoff_consistency
|
import reviewer_handoff_consistency
|
||||||
import thread_state_ledger_validator
|
import thread_state_ledger_validator
|
||||||
from mcp_native_cleanup_proof import assess_mcp_native_cleanup_proof
|
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]]:
|
def _rule_shared_issue_acceptance_gate(report_text: str) -> list[dict[str, str]]:
|
||||||
result = issue_acceptance_gate.validate_final_report_issue_acceptance(report_text)
|
result = issue_acceptance_gate.validate_final_report_issue_acceptance(report_text)
|
||||||
if not result.get("applicable") or result.get("valid"):
|
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 = (
|
_SHARED_ISSUE_LOCK_RULES = (
|
||||||
_rule_shared_issue_lock_external_state,
|
_rule_shared_issue_lock_external_state,
|
||||||
_rule_shared_manual_lock_pr_override,
|
_rule_shared_manual_lock_pr_override,
|
||||||
|
_rule_shared_manual_lease_proof_handoff,
|
||||||
_rule_shared_author_reviewer_same_run,
|
_rule_shared_author_reviewer_same_run,
|
||||||
_rule_shared_raw_branch_delete_bypass,
|
_rule_shared_raw_branch_delete_bypass,
|
||||||
_rule_shared_canonical_state_update,
|
_rule_shared_canonical_state_update,
|
||||||
|
|||||||
@@ -4367,6 +4367,13 @@ def gitea_merge_pr(
|
|||||||
live_head_sha=result.get("head_sha"),
|
live_head_sha=result.get("head_sha"),
|
||||||
pinned_head_sha=expected_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:
|
if reasons:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -4524,6 +4531,12 @@ def gitea_merge_pr(
|
|||||||
|
|
||||||
result["performed"] = True
|
result["performed"] = True
|
||||||
result["merge_result"] = f"PR #{pr_number} merged via '{do}'."
|
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}'")
|
reasons.append(f"all gates passed; merged PR #{pr_number} via '{do}'")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ after formal approval at the current PR head.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -133,6 +134,115 @@ def is_sanctioned_session_lease(session: dict[str, Any] | None) -> bool:
|
|||||||
return False
|
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(
|
def assess_adopt_merger_lease(
|
||||||
comments: list[dict],
|
comments: list[dict],
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -781,6 +781,103 @@ class TestMergePrRules(unittest.TestCase):
|
|||||||
self.assertEqual(result["grade"], "A")
|
self.assertEqual(result["grade"], "A")
|
||||||
self.assertFalse(result["blocked"])
|
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):
|
def test_missing_merger_fields_blocks(self):
|
||||||
lines = [
|
lines = [
|
||||||
"## Controller Handoff",
|
"## Controller Handoff",
|
||||||
|
|||||||
@@ -42,6 +42,96 @@ def _lease_comment(
|
|||||||
HEAD = "f" * 40
|
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):
|
class TestSanctionedProvenance(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
leases.clear_session_lease()
|
leases.clear_session_lease()
|
||||||
|
|||||||
Reference in New Issue
Block a user