Root cause (confirmed on PR #740, session 33780-7168cbeeba58, marker 12354): merger_lease_adoption.SANCTIONED_PROVENANCE_SOURCES already contained SOURCE_ACQUIRE_MERGER, so the membership check passed, but is_sanctioned_session_lease() branched only for SOURCE_ADOPT and for {SOURCE_ACQUIRE, SOURCE_HEARTBEAT} and fell through to `return False` for a lease minted by gitea_acquire_merger_pr_lease. assess_mutation_lease_gate() therefore appended "in-session lease lacks sanctioned provenance" and gitea_merge_pr fail-closed on the merger's own freshly acquired lease. describe_session_lease_proof() likewise had no branch for that source and reported the lease as lease_proof_kind=unsanctioned. Separately, no merger-role owner-session operation existed to end that lease, so a failed merger lease could only expire. A. Acquired merger provenance is_sanctioned_session_lease() now accepts SOURCE_ACQUIRE_MERGER, but only via the new assess_acquired_merger_lease_integrity(), which fails closed unless the in-session record is complete and self-consistent: comment marker present and matching between provenance and session, non-empty session id, holder identity, merger profile, repository, valid PR number, and a normalized 40-hex candidate_head. describe_session_lease_proof() reports the explicit kind sanctioned_acquire_merger. Manual _SESSION_LEASE seeding, forged or incomplete records, mismatched fields, and unknown provenance all remain rejected; reviewer-acquire, reviewer-heartbeat, and merger-adoption paths are untouched. B. Merger owner-session finalization New native tool gitea_release_merger_pr_lease terminally releases or abandons a merger's own comment-backed lease when the merge does not occur. It is merger-only (gitea.read + gitea.pr.comment + gitea.pr.merge; reviewer profiles lack pr.merge) with an explicit capability-map task release_merger_pr_lease / gitea_release_merger_pr_lease bound to gitea.pr.comment + role merger, and it is listed as role-exclusive in the resolver. assess_merger_lease_finalization() verifies exact session ownership, repository, PR, candidate head vs live head, profile, identity, marker presence on the thread, and the native runtime token fingerprint recorded at acquisition; foreign-session release, reviewer-profile use, and any mismatch fail closed. Finalization appends a terminal lease marker and never edits or deletes ledger history; an already-terminal lease returns already_terminal and posts nothing. The PR, approval, decision lock, branch, and worktree are all preserved. gitea_release_reviewer_pr_lease is unchanged and is not repurposed for merger sessions. "abandoned" is added to reviewer_pr_lease._TERMINAL_PHASES; without it an abandoned marker would fall through the generic non-empty phase branch and keep re-arming the lease as active. Tests: new tests/test_merger_lease_finalization.py (38 tests, 11 subtests) covers all twelve acceptance criteria — provenance sanctioning, explicit proof kind, merge-gate acceptance, manual-seed and unknown-provenance rejection, profile/role/session/head/repository/fingerprint mismatches, owner release, foreign-session refusal, reviewer refusal, append-only idempotent finalization, no surviving lease after finalization, and no regression in adoption or reviewer-lease behavior. The defect was reproduced on clean baselinea8d2087first (is_sanctioned=False, proof_kind=unsanctioned, no finalization path). Validation: targeted lease/capability suites 272 passed. Full suite 3305 passed, 6 skipped, 2 failed — both failures (test_issue_702_review_findings_f1_f6 F1 recovery, reconciler supersession close) reproduce identically on clean baseline mastera8d2087in a throwaway worktree and are pre-existing #737 org/repo-forwarding drift, unrelated to this change. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
717 lines
26 KiB
Python
717 lines
26 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_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 = "<!-- mcp-merger-lease-final:v1 -->"
|
|
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,
|
|
} |