"""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 = "" DEFAULT_ADOPTION_REASON = "merger-handoff-approved-head" SOURCE_ADOPT = "gitea_adopt_merger_pr_lease" SOURCE_ACQUIRE = "gitea_acquire_reviewer_pr_lease" SOURCE_ACQUIRE_MERGER = "gitea_acquire_merger_pr_lease" SOURCE_HEARTBEAT = "gitea_heartbeat_reviewer_pr_lease" SOURCE_RELEASE_MERGER = "gitea_release_merger_pr_lease" SANCTIONED_PROVENANCE_SOURCES = frozenset({ SOURCE_ADOPT, SOURCE_ACQUIRE, SOURCE_ACQUIRE_MERGER, SOURCE_HEARTBEAT, }) _MERGER_ADOPTABLE_FRESHNESS = frozenset({"active", "stale_warning"}) # #742: owner-session terminal finalization of a merger-held lease. Append-only # marker; ledger history is never edited or deleted. MERGER_FINALIZATION_MARKER = "" OUTCOME_RELEASED = "released" OUTCOME_ABANDONED = "abandoned" MERGER_FINALIZATION_OUTCOMES = frozenset({OUTCOME_RELEASED, OUTCOME_ABANDONED}) DEFAULT_MERGER_FINALIZATION_REASON = "merge-not-performed" # Provenance sources whose in-session lease is merger-owned and therefore # finalizable by its owning merger session. MERGER_OWNED_PROVENANCE_SOURCES = frozenset({ SOURCE_ACQUIRE_MERGER, SOURCE_ADOPT, }) 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, native_token_fingerprint: 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 if native_token_fingerprint: proof["native_token_fingerprint"] = native_token_fingerprint return proof def assess_acquired_merger_lease_integrity( session: dict[str, Any] | None, ) -> list[str]: """Ownership/integrity reasons blocking a ``SOURCE_ACQUIRE_MERGER`` lease (#742). A merger lease minted by ``gitea_acquire_merger_pr_lease`` authorizes an irreversible merge, so it is sanctioned only when the in-session record is complete and self-consistent: comment marker, exact session identity, merger profile/role, repository, PR number, and pinned candidate head. Any missing or contradictory field fails closed — an incomplete record is not proof. """ if not session: return ["no in-session lease recorded"] provenance = session.get("lease_provenance") or {} if not isinstance(provenance, dict): provenance = {} reasons: list[str] = [] provenance_comment_id = provenance.get("comment_id") session_comment_id = session.get("comment_id") if not (provenance_comment_id or session_comment_id): reasons.append( "acquired merger lease has no comment marker id (comment-backed " "proof required)" ) elif ( provenance_comment_id is not None and session_comment_id is not None and provenance_comment_id != session_comment_id ): reasons.append( "acquired merger lease provenance comment_id does not match the " "session lease comment_id" ) if not (session.get("session_id") or "").strip(): reasons.append("acquired merger lease has no session_id") if not (session.get("reviewer_identity") or "").strip(): reasons.append("acquired merger lease has no holder identity") profile = (session.get("profile") or "").strip() if not profile: reasons.append("acquired merger lease has no profile") elif "merger" not in profile.lower(): reasons.append( f"acquired merger lease profile '{profile}' is not a merger profile " "(merger-only; fail closed)" ) if not (session.get("repo") or "").strip(): reasons.append("acquired merger lease has no repository") pr_number = session.get("pr_number") if not isinstance(pr_number, int) or isinstance(pr_number, bool) or pr_number <= 0: reasons.append("acquired merger lease has no valid PR number") if not leases._normalize_sha(session.get("candidate_head")): reasons.append( "acquired merger lease has no pinned candidate_head (exact-head " "scoping required)" ) return reasons 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 == SOURCE_ACQUIRE_MERGER: return not assess_acquired_merger_lease_integrity(session) 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_MERGER and sanctioned: kind = "sanctioned_acquire_merger" 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"gitea_acquire_merger_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_source\s*[:=]\s*gitea_acquire_merger_pr_lease|" r"lease_proof_kind\s*[:=]\s*sanctioned_adoption|" r"lease_proof_kind\s*[:=]\s*sanctioned_acquire_merger|" 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 format_merger_finalization_body( *, repo: str, pr_number: int, issue_number: int | None, merger_identity: str, merger_profile: str, merger_session_id: str, worktree: str, candidate_head: str | None, target_branch: str, target_branch_sha: str | None, outcome: str, reason: str, lease_comment_id: int | None, finalized_at: datetime | None = None, ) -> str: """Render the append-only terminal marker for a merger-owned lease (#742). The body carries a standard lease marker in a terminal phase, so the existing newest-wins ledger (#577) ends the lease without editing or deleting any prior comment. """ finalized_at = finalized_at or datetime.now(timezone.utc) finalized_text = finalized_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=merger_identity, profile=merger_profile, session_id=merger_session_id, worktree=worktree, phase=outcome, candidate_head=candidate_head, target_branch=target_branch, target_branch_sha=target_branch_sha, last_activity=finalized_at, blocker=reason, ) lines = [ MERGER_FINALIZATION_MARKER, f"finalized_at: {finalized_text}", f"finalized_by_identity: {merger_identity}", f"finalized_by_profile: {merger_profile}", f"finalized_by_session_id: {merger_session_id}", f"finalization_outcome: {outcome}", f"finalization_reason: {reason}", f"finalized_lease_comment_id: {lease_comment_id or 'none'}", lease_body, ] return "\n".join(lines) def is_merger_finalization_comment(body: str) -> bool: return MERGER_FINALIZATION_MARKER in (body or "") def assess_merger_lease_finalization( comments: list[dict], *, pr_number: int, session: dict[str, Any] | None, actor_identity: str, actor_profile: str, actor_session_id: str | None, repo: str, worktree: str, candidate_head: str | None, live_head_sha: str | None = None, outcome: str = OUTCOME_RELEASED, reason: str | None = None, runtime_token_fingerprint: str | None = None, issue_number: int | None = None, target_branch: str = "master", target_branch_sha: str | None = None, now: datetime | None = None, ) -> dict[str, Any]: """Decide whether a merger may terminally finalize its own lease (#742). Owner-session only: the caller must hold the exact comment-backed merger lease it is finalizing. Foreign sessions, reviewer profiles, and mismatched repository/PR/head/token-fingerprint callers fail closed. Already-terminal leases return ``already_terminal`` so repeat calls are idempotent and post no second marker. """ now = now or datetime.now(timezone.utc) reasons: list[str] = [] outcome = (outcome or "").strip().lower() finalization_reason = (reason or "").strip() or DEFAULT_MERGER_FINALIZATION_REASON if outcome not in MERGER_FINALIZATION_OUTCOMES: reasons.append( f"outcome '{outcome or 'none'}' is not a terminal merger " f"finalization outcome ({sorted(MERGER_FINALIZATION_OUTCOMES)})" ) if "merger" not in (actor_profile or "").lower(): reasons.append( f"profile '{actor_profile or 'unknown'}' is not a merger profile; " "merger lease finalization is merger-only (fail closed). Reviewer " "sessions use gitea_release_reviewer_pr_lease." ) session = session or None provenance = (session or {}).get("lease_provenance") or {} if not isinstance(provenance, dict): provenance = {} source = (provenance.get("source") or "").strip() if not session: reasons.append( "no in-session merger lease recorded; only the owning session may " "finalize a merger lease" ) elif source not in MERGER_OWNED_PROVENANCE_SOURCES: reasons.append( f"in-session lease provenance '{source or 'none'}' is not a " "merger-owned lease; refusing to finalize" ) elif not is_sanctioned_session_lease(session): reasons.extend( assess_acquired_merger_lease_integrity(session) if source == SOURCE_ACQUIRE_MERGER else ["in-session merger lease lacks sanctioned provenance"] ) pinned = leases._normalize_sha(candidate_head) live = leases._normalize_sha(live_head_sha) if not pinned: reasons.append( "candidate_head is required for merger lease finalization " "(exact-head scoping; fail closed)" ) if live and pinned and live != pinned: reasons.append( "candidate_head does not match live PR head (fail closed)" ) if session: session_sid = (session.get("session_id") or "").strip() actor_sid = (actor_session_id or "").strip() if not actor_sid or session_sid != actor_sid: reasons.append( "session_id does not match the in-session merger lease owner; " "foreign-session release is not permitted (fail closed)" ) if session.get("pr_number") != pr_number: reasons.append( f"in-session merger lease is for PR #{session.get('pr_number')}, " f"not #{pr_number}" ) session_repo = (session.get("repo") or "").strip() if session_repo and session_repo != (repo or "").strip(): reasons.append( f"in-session merger lease repository '{session_repo}' does not " f"match '{repo}' (fail closed)" ) session_head = leases._normalize_sha(session.get("candidate_head")) if pinned and session_head and session_head != pinned: reasons.append( "in-session merger lease candidate_head does not match the " "supplied candidate_head (fail closed)" ) session_identity = (session.get("reviewer_identity") or "").strip() if session_identity and session_identity != (actor_identity or "").strip(): reasons.append( "authenticated identity does not match the in-session merger " "lease holder (fail closed)" ) session_profile = (session.get("profile") or "").strip() if session_profile and session_profile != (actor_profile or "").strip(): reasons.append( "active profile does not match the in-session merger lease " "profile (fail closed)" ) recorded_fingerprint = ( provenance.get("native_token_fingerprint") or "" ).strip() live_fingerprint = (runtime_token_fingerprint or "").strip() if ( recorded_fingerprint and live_fingerprint and recorded_fingerprint != live_fingerprint ): reasons.append( "native runtime token fingerprint does not match the one " "recorded when the merger lease was acquired (fail closed)" ) lease_comment_id = (session or {}).get("comment_id") or provenance.get("comment_id") entries = leases._lease_entries(comments, pr_number=pr_number) if session and lease_comment_id is not None: if not any(entry.get("comment_id") == lease_comment_id for entry in entries): reasons.append( f"lease marker comment {lease_comment_id} is not present on PR " f"#{pr_number} (fail closed)" ) active = leases.find_active_reviewer_lease(comments, pr_number=pr_number, now=now) already_terminal = False if active: owner_session = (active.get("session_id") or "").strip() if owner_session and owner_session != (actor_session_id or "").strip(): reasons.append( f"active PR lease is owned by session_id={owner_session}; a " "merger may only finalize its own lease (fail closed)" ) else: newest = entries[-1] if entries else None newest_phase = ((newest or {}).get("phase") or "").strip().lower() if newest and newest_phase in leases._TERMINAL_PHASES: already_terminal = True elif not entries: reasons.append( f"no comment-backed lease marker found on PR #{pr_number}" ) finalize_allowed = not reasons and not already_terminal body = None if finalize_allowed and session: body = format_merger_finalization_body( repo=(session.get("repo") or repo), pr_number=pr_number, issue_number=( issue_number if issue_number is not None else session.get("issue_number") ), merger_identity=actor_identity, merger_profile=actor_profile, merger_session_id=(actor_session_id or ""), worktree=worktree or (session.get("worktree") or ""), candidate_head=pinned, target_branch=( session.get("target_branch") or target_branch or "master" ), target_branch_sha=( session.get("target_branch_sha") or target_branch_sha ), outcome=outcome, reason=finalization_reason, lease_comment_id=lease_comment_id, finalized_at=now, ) return { "finalize_allowed": finalize_allowed, "already_terminal": already_terminal, "reasons": reasons, "outcome": outcome, "finalization_reason": finalization_reason, "active_lease": active, "finalization_body": body, "lease_comment_id": lease_comment_id, "candidate_head": pinned, } 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, }