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
363 lines
13 KiB
Python
363 lines
13 KiB
Python
"""Guarded merger adoption of an existing reviewer PR lease (#536).
|
|
|
|
Replaces manual in-process ``_SESSION_LEASE`` seeding with an auditable,
|
|
comment-backed adoption path for merger sessions that inherit a reviewer lease
|
|
after formal approval at the current PR head.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
import reviewer_pr_lease as leases
|
|
|
|
ADOPTION_MARKER = "<!-- mcp-review-lease-adoption:v1 -->"
|
|
DEFAULT_ADOPTION_REASON = "merger-handoff-approved-head"
|
|
|
|
SOURCE_ADOPT = "gitea_adopt_merger_pr_lease"
|
|
SOURCE_ACQUIRE = "gitea_acquire_reviewer_pr_lease"
|
|
SOURCE_HEARTBEAT = "gitea_heartbeat_reviewer_pr_lease"
|
|
|
|
SANCTIONED_PROVENANCE_SOURCES = frozenset({
|
|
SOURCE_ADOPT,
|
|
SOURCE_ACQUIRE,
|
|
SOURCE_HEARTBEAT,
|
|
})
|
|
|
|
_MERGER_ADOPTABLE_FRESHNESS = frozenset({"active", "stale_warning"})
|
|
|
|
|
|
def format_adoption_body(
|
|
*,
|
|
repo: str,
|
|
pr_number: int,
|
|
issue_number: int | None,
|
|
adopter_identity: str,
|
|
adopter_profile: str,
|
|
adopter_session_id: str,
|
|
worktree: str,
|
|
candidate_head: str | None,
|
|
target_branch: str,
|
|
target_branch_sha: str | None,
|
|
adopted_from_session_id: str,
|
|
adopted_from_profile: str,
|
|
adopted_from_reviewer_identity: str,
|
|
adopted_from_comment_id: int | None,
|
|
adoption_reason: str = DEFAULT_ADOPTION_REASON,
|
|
adopted_at: datetime | None = None,
|
|
) -> str:
|
|
"""Render a durable merger adoption proof comment."""
|
|
adopted_at = adopted_at or datetime.now(timezone.utc)
|
|
adopted_text = adopted_at.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace(
|
|
"+00:00", "Z"
|
|
)
|
|
lease_body = leases.format_lease_body(
|
|
repo=repo,
|
|
pr_number=pr_number,
|
|
issue_number=issue_number,
|
|
reviewer_identity=adopter_identity,
|
|
profile=adopter_profile,
|
|
session_id=adopter_session_id,
|
|
worktree=worktree,
|
|
phase="adopted",
|
|
candidate_head=candidate_head,
|
|
target_branch=target_branch,
|
|
target_branch_sha=target_branch_sha,
|
|
last_activity=adopted_at,
|
|
blocker="none",
|
|
)
|
|
lines = [
|
|
ADOPTION_MARKER,
|
|
f"adopted_at: {adopted_text}",
|
|
f"adopted_by_identity: {adopter_identity}",
|
|
f"adopted_by_profile: {adopter_profile}",
|
|
f"adopted_from_session_id: {adopted_from_session_id}",
|
|
f"adopted_from_profile: {adopted_from_profile}",
|
|
f"adopted_from_reviewer_identity: {adopted_from_reviewer_identity}",
|
|
f"adopted_from_comment_id: {adopted_from_comment_id or 'none'}",
|
|
f"adoption_reason: {adoption_reason}",
|
|
lease_body,
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def is_adoption_comment(body: str) -> bool:
|
|
return ADOPTION_MARKER in (body or "")
|
|
|
|
|
|
def build_lease_provenance(
|
|
*,
|
|
source: str,
|
|
comment_id: int | None = None,
|
|
adopted_from_session_id: str | None = None,
|
|
adopted_from_profile: str | None = None,
|
|
adopted_from_reviewer_identity: str | None = None,
|
|
adoption_reason: str | None = None,
|
|
recorded_at: datetime | None = None,
|
|
) -> dict[str, Any]:
|
|
recorded_at = recorded_at or datetime.now(timezone.utc)
|
|
recorded_text = recorded_at.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace(
|
|
"+00:00", "Z"
|
|
)
|
|
proof = {
|
|
"source": source,
|
|
"recorded_at": recorded_text,
|
|
}
|
|
if comment_id is not None:
|
|
proof["comment_id"] = comment_id
|
|
if adopted_from_session_id:
|
|
proof["adopted_from_session_id"] = adopted_from_session_id
|
|
if adopted_from_profile:
|
|
proof["adopted_from_profile"] = adopted_from_profile
|
|
if adopted_from_reviewer_identity:
|
|
proof["adopted_from_reviewer_identity"] = adopted_from_reviewer_identity
|
|
if adoption_reason:
|
|
proof["adoption_reason"] = adoption_reason
|
|
return proof
|
|
|
|
|
|
def is_sanctioned_session_lease(session: dict[str, Any] | None) -> bool:
|
|
if not session:
|
|
return False
|
|
provenance = session.get("lease_provenance") or {}
|
|
source = (provenance.get("source") or "").strip()
|
|
if source not in SANCTIONED_PROVENANCE_SOURCES:
|
|
return False
|
|
if source == SOURCE_ADOPT:
|
|
return bool(provenance.get("comment_id")) and bool(
|
|
provenance.get("adopted_from_session_id")
|
|
)
|
|
if source in {SOURCE_ACQUIRE, SOURCE_HEARTBEAT}:
|
|
return bool(session.get("comment_id") or provenance.get("comment_id"))
|
|
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],
|
|
*,
|
|
pr_number: int,
|
|
adopter_identity: str,
|
|
adopter_profile: str,
|
|
adopter_session_id: str,
|
|
repo: str,
|
|
issue_number: int | None,
|
|
worktree: str,
|
|
expected_head_sha: str | None,
|
|
live_head_sha: str | None,
|
|
approval_at_head: bool,
|
|
pr_open: bool = True,
|
|
now: datetime | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Decide whether a merger may adopt the active reviewer lease (#536)."""
|
|
now = now or datetime.now(timezone.utc)
|
|
reasons: list[str] = []
|
|
pinned = leases._normalize_sha(expected_head_sha)
|
|
live = leases._normalize_sha(live_head_sha)
|
|
|
|
if not pr_open:
|
|
reasons.append(
|
|
f"PR #{pr_number} is not open; merger lease adoption is only for open PRs"
|
|
)
|
|
if not approval_at_head:
|
|
reasons.append(
|
|
"formal APPROVED review at the current PR head is required before "
|
|
"merger lease adoption (fail closed)"
|
|
)
|
|
if not pinned:
|
|
reasons.append("expected_head_sha is required for merger lease adoption")
|
|
if not live:
|
|
reasons.append("live PR head SHA unavailable (fail closed)")
|
|
elif pinned and live and pinned != live:
|
|
reasons.append(
|
|
"expected_head_sha does not match live PR head; refresh approval before adoption"
|
|
)
|
|
|
|
active = leases.find_active_reviewer_lease(
|
|
comments, pr_number=pr_number, now=now
|
|
)
|
|
if not active:
|
|
reasons.append(
|
|
f"no active reviewer lease on PR #{pr_number} to adopt; reviewer "
|
|
"must acquire first"
|
|
)
|
|
else:
|
|
owner_session = (active.get("session_id") or "").strip()
|
|
freshness = active.get("freshness") or leases.classify_lease_freshness(
|
|
active, now=now
|
|
)
|
|
if freshness not in _MERGER_ADOPTABLE_FRESHNESS:
|
|
reasons.append(
|
|
f"active reviewer lease freshness is '{freshness}'; explicit "
|
|
"reclaim is not implemented (fail closed)"
|
|
)
|
|
lease_head = active.get("candidate_head")
|
|
if lease_head and live and lease_head != live:
|
|
reasons.append(
|
|
"active reviewer lease candidate_head differs from live PR head"
|
|
)
|
|
if owner_session and owner_session == adopter_session_id:
|
|
# Same session already holds the thread lease — allow idempotent adopt
|
|
# only when the newest entry is not yet an adoption by this session.
|
|
entries = leases._lease_entries(comments, pr_number=pr_number)
|
|
newest = entries[-1] if entries else None
|
|
if newest and (newest.get("phase") or "") == "adopted":
|
|
reasons.append(
|
|
"merger session already recorded an adoption lease on this PR"
|
|
)
|
|
elif owner_session and owner_session != adopter_session_id:
|
|
# Cross-session merger handoff: adopt the foreign reviewer lease.
|
|
pass
|
|
|
|
if not (adopter_identity or "").strip():
|
|
reasons.append("adopter identity required")
|
|
if not (adopter_session_id or "").strip():
|
|
reasons.append("adopter session_id required")
|
|
if not (worktree or "").strip():
|
|
reasons.append("merger worktree path required")
|
|
if "merger" not in (adopter_profile or "").lower():
|
|
reasons.append(
|
|
f"profile '{adopter_profile}' is not a merger profile; adoption is "
|
|
"merger-only (fail closed)"
|
|
)
|
|
|
|
adopt_allowed = not reasons
|
|
adoption_body = None
|
|
if adopt_allowed and active:
|
|
adoption_body = format_adoption_body(
|
|
repo=repo,
|
|
pr_number=pr_number,
|
|
issue_number=issue_number or active.get("issue_number"),
|
|
adopter_identity=adopter_identity,
|
|
adopter_profile=adopter_profile,
|
|
adopter_session_id=adopter_session_id,
|
|
worktree=worktree,
|
|
candidate_head=live,
|
|
target_branch=active.get("target_branch") or "master",
|
|
target_branch_sha=active.get("target_branch_sha"),
|
|
adopted_from_session_id=active.get("session_id") or "",
|
|
adopted_from_profile=active.get("profile") or "unknown",
|
|
adopted_from_reviewer_identity=active.get("reviewer_identity") or "",
|
|
adopted_from_comment_id=active.get("comment_id"),
|
|
adopted_at=now,
|
|
)
|
|
|
|
return {
|
|
"adopt_allowed": adopt_allowed,
|
|
"reasons": reasons,
|
|
"active_lease": active,
|
|
"adoption_body": adoption_body,
|
|
"adopter_session_id": adopter_session_id,
|
|
"expected_head_sha": pinned,
|
|
"live_head_sha": live,
|
|
} |