Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from omnimarket.nodes.node_pr_lifecycle_fix_effect.models.model_fix_result import (
EnumDelegationOutcome,
ModelOccCompanionVerification,
ModelPrLifecycleFixResult,
)

Expand Down Expand Up @@ -167,6 +168,24 @@ async def autobind_evidence_source(
...


@runtime_checkable
class ProtocolOccCompanionVerifier(Protocol):
"""Independent read-back verifier for a pushed OCC Evidence-Source companion.

OMN-14173: the autobind arm's ``fix_applied`` flag reports only that the
adapter call returned without raising — it does NOT prove a companion was
pushed. This verifier re-reads GitHub to confirm the EFFECT (Evidence-Source
patch + open OCC PR + companion branch). ``prs_fixed`` is gated on its
``verified`` result, never on the in-memory ``fix_applied`` flag.
"""

async def verify_companion(
self, repo: str, pr_number: int, ticket_id: str | None = None
) -> ModelOccCompanionVerification:
"""Read GitHub back and return whether the OCC companion actually landed."""
...


# ---------------------------------------------------------------------------
# Default no-op adapters (used in standalone / dry-run mode)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -215,6 +234,28 @@ async def autobind_evidence_source(
)


class _UnverifiedOccCompanionVerifier:
"""Fail-closed default verifier — proves nothing, so counts nothing.

OMN-14173: when no real read-back verifier is wired (standalone / dry-run /
the runtime-boot path), the OCC companion cannot be independently confirmed,
so verification returns ``verified=False``. This is the safe direction — a
missing verifier UNDER-counts (never falsely counts) ``prs_fixed``. The
merge-sweep orchestrator injects the live :class:`OccCompanionVerifier`.
"""

async def verify_companion(
self, repo: str, pr_number: int, ticket_id: str | None = None
) -> ModelOccCompanionVerification:
return ModelOccCompanionVerification(
verified=False,
detail=(
"no OCC companion verifier wired; fail-closed (cannot prove the "
"companion was pushed)"
),
)


class _NoopDelegationFixAdapter:
"""No-op delegation-fix adapter for dry_run and standalone execution."""

Expand Down Expand Up @@ -265,6 +306,7 @@ def __init__(
agent_dispatch_adapter: ProtocolAgentDispatchAdapter | None = None,
occ_contract_adapter: ProtocolOccContractAdapter | None = None,
occ_autobind_adapter: ProtocolOccAutobindAdapter | None = None,
occ_companion_verifier: ProtocolOccCompanionVerifier | None = None,
delegation_fix_adapter: ProtocolDelegationFixAdapter | None = None,
two_strike_store: ProtocolTwoStrikeStore | None = None,
delegation_model_name: str = "ruff-deterministic",
Expand All @@ -279,6 +321,13 @@ def __init__(
self._occ_autobind: ProtocolOccAutobindAdapter = (
occ_autobind_adapter or _NoopOccAutobindAdapter()
)
# OMN-14173: independent read-back verifier for the OCC autobind arm.
# Defaults fail-closed (proves nothing → counts nothing); the merge-sweep
# orchestrator injects the live OccCompanionVerifier so a real run gates
# prs_fixed on a confirmed pushed companion, not on the fix_applied flag.
self._occ_verifier: ProtocolOccCompanionVerifier = (
occ_companion_verifier or _UnverifiedOccCompanionVerifier()
)
# WS-D/D2 (OMN-13940): delegated (non-Claude) fix path for
# CODE_FAILURE / CHANGES_REQUESTED. Defaults to noop + an in-memory
# two-strike store (test/standalone convenience) — production wiring
Expand Down Expand Up @@ -312,11 +361,31 @@ async def handle(
fix_action: str
error: str | None = None
fix_applied = False
occ_companion_verified = False
delegation_info = _NOT_DELEGATED

try:
fix_action, delegation_info = await self._route(command)
fix_applied = True
# OMN-14173 fail-closed accounting: the autobind arm's success is
# measured by the EFFECT (a pushed OCC companion + Evidence-Source
# patch), never by the call returning. Re-read GitHub to confirm.
# `fix_applied` stays True (the route ran), but the orchestrator
# counts prs_fixed for this arm ONLY when the companion is verified,
# so a no-op/short-circuit dispatch can never inflate the count.
if (
command.block_reason
== EnumPrBlockReason.RECEIPT_EVIDENCE_SOURCE_AUTOBIND
):
verification = await self._occ_verifier.verify_companion(
command.repo, command.pr_number, command.ticket_id
)
occ_companion_verified = verification.verified
if not occ_companion_verified:
fix_action = (
f"{fix_action} | OCC companion NOT verified: "
f"{verification.detail}"
)
except Exception as exc:
fix_action = f"failed: {exc}"
error = str(exc)
Expand All @@ -336,6 +405,7 @@ async def handle(
block_reason=command.block_reason,
fix_applied=fix_applied,
fix_action=fix_action,
occ_companion_verified=occ_companion_verified,
error=error,
completed_at=datetime.now(tz=UTC),
delegated=delegation_info.delegated,
Expand Down Expand Up @@ -528,5 +598,6 @@ def correlation_id(self) -> UUID | None:
"ProtocolDelegationFixAdapter",
"ProtocolGitHubAdapter",
"ProtocolOccAutobindAdapter",
"ProtocolOccCompanionVerifier",
"ProtocolOccContractAdapter",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# SPDX-FileCopyrightText: 2026 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
"""Independent read-back verifier for pushed OCC Evidence-Source companions.

OMN-14173. The ``receipt_evidence_source_autobind`` arm used to increment
``prs_fixed`` on the strength of the adapter call returning without raising —
which includes the no-op idempotency return and any short-circuit path that
pushes nothing. Dogfooding ``merge_sweep --fix-only`` on omnimarket #1651/#1652
returned ``prs_fixed=2`` while authoring ZERO OCC companions: a false-success of
the exact class the tooling is meant to catch.

This verifier proves the EFFECT, not the call. After the autobind adapter runs,
it re-reads GitHub and confirms all three of:

1. the product PR body carries ``Evidence-Source: OCC#<n>`` (an OCC source, not
a bare product-head SHA);
2. OCC PR ``#<n>`` is in the ``open`` state on ``onex_change_control``;
3. the expected ``auto/<repo>-pr-<n>-occ-autobind`` companion branch exists on
the OCC remote.

Any missing evidence, resolution error, or token failure fails CLOSED
(``verified=False``) — the orchestrator then reports the PR as NOT fixed rather
than counting a phantom companion.
"""

from __future__ import annotations

import asyncio
import logging
import re
import tempfile

from omnimarket.github_api import GitHubApiError, rest_json, split_repo
from omnimarket.nodes.node_pr_lifecycle_fix_effect.handlers.adapter_occ_autobind import (
_resolve_github_token,
)
from omnimarket.nodes.node_pr_lifecycle_fix_effect.handlers.occ_git_transport import (
OCC_REPO,
authenticated_occ_url,
run_git,
)
from omnimarket.nodes.node_pr_lifecycle_fix_effect.models.model_fix_result import (
ModelOccCompanionVerification,
)

logger = logging.getLogger(__name__)

# Evidence-Source line pointing at an OCC source (`OCC#<n>`), not a product SHA.
_EVIDENCE_SOURCE_RE = re.compile(r"^Evidence-Source:\s*(\S+)\s*$", re.MULTILINE)
_OCC_SOURCE_RE = re.compile(r"^OCC#(\d+)$")

_GIT_LS_REMOTE_TIMEOUT_SECONDS = 60


def _expected_branch(repo: str, pr_number: int) -> str:
"""Return the deterministic auto/* companion branch the adapter pushes.

Mirrors ``OccAutobindAdapter`` exactly (one OCC branch per product PR):
``auto/<repo-slug-lower>-pr-<pr_number>-occ-autobind``.
"""
repo_slug = repo.replace("/", "-").lower()
return f"auto/{repo_slug}-pr-{pr_number}-occ-autobind"


class OccCompanionVerifier:
"""Confirm a pushed OCC Evidence-Source companion via GitHub read-back.

Fail-closed: every branch returns ``verified=False`` unless all three
independent facts are observed. Never raises — a resolution/network error
is itself a verification failure (the effect could not be proven).
"""

def __init__(self, *, occ_repo: str = OCC_REPO) -> None:
self._occ_repo = occ_repo

async def verify_companion(
self, repo: str, pr_number: int, ticket_id: str | None = None
) -> ModelOccCompanionVerification:
return await asyncio.to_thread(self._verify_sync, repo, pr_number)

def _verify_sync(self, repo: str, pr_number: int) -> ModelOccCompanionVerification:
expected_branch = _expected_branch(repo, pr_number)
try:
token = _resolve_github_token()
except Exception as exc:
return ModelOccCompanionVerification(
verified=False,
occ_branch=expected_branch,
detail=f"could not resolve GitHub token: {exc}",
)

# 1. Product PR body must carry Evidence-Source: OCC#<n>.
try:
owner, repo_name = split_repo(repo)
pr_data = rest_json(
"GET", f"/repos/{owner}/{repo_name}/pulls/{pr_number}", token=token
)
except (GitHubApiError, ValueError) as exc:
return ModelOccCompanionVerification(
verified=False,
occ_branch=expected_branch,
detail=f"could not read product PR {repo}#{pr_number}: {exc}",
)
body = pr_data.get("body") or ""
match = _EVIDENCE_SOURCE_RE.search(body)
occ_match = _OCC_SOURCE_RE.match(match.group(1)) if match else None
if occ_match is None:
return ModelOccCompanionVerification(
verified=False,
occ_branch=expected_branch,
evidence_source_present=False,
detail=(
f"{repo}#{pr_number} body has no `Evidence-Source: OCC#<n>` "
"(companion not bound)"
),
)
occ_pr_number = int(occ_match.group(1))

# 2. The referenced OCC companion PR must be OPEN.
try:
occ_owner, occ_name = split_repo(self._occ_repo)
occ_pr = rest_json(
"GET",
f"/repos/{occ_owner}/{occ_name}/pulls/{occ_pr_number}",
token=token,
)
except (GitHubApiError, ValueError) as exc:
return ModelOccCompanionVerification(
verified=False,
occ_pr_number=occ_pr_number,
occ_branch=expected_branch,
evidence_source_present=True,
detail=f"could not read OCC PR #{occ_pr_number}: {exc}",
)
occ_pr_open = (occ_pr.get("state") or "") == "open"

# 3. The auto/* companion branch must exist on the OCC remote.
branch_exists = self._branch_exists(token, expected_branch)

verified = bool(occ_pr_open and branch_exists)
detail = (
f"verified OCC#{occ_pr_number} companion for {repo}#{pr_number}"
if verified
else (
f"companion incomplete for {repo}#{pr_number}: "
f"occ_pr_open={occ_pr_open} branch_exists={branch_exists}"
)
)
return ModelOccCompanionVerification(
verified=verified,
occ_pr_number=occ_pr_number,
occ_branch=expected_branch,
evidence_source_present=True,
occ_pr_open=occ_pr_open,
branch_exists=branch_exists,
detail=detail,
)

def _branch_exists(self, token: str, branch: str) -> bool:
"""True iff ``git ls-remote --heads`` finds the branch on the OCC remote."""
try:
with tempfile.TemporaryDirectory(prefix="occ-verify-") as tmpdir:
out = run_git(
[
"git",
"ls-remote",
"--heads",
authenticated_occ_url(token, self._occ_repo),
branch,
],
cwd=tmpdir,
timeout=_GIT_LS_REMOTE_TIMEOUT_SECONDS,
)
except Exception as exc:
logger.warning("occ_companion_verifier: ls-remote failed: %s", exc)
return False
return bool(out.strip())


__all__ = ["OccCompanionVerifier"]
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,55 @@ class EnumDelegationOutcome(StrEnum):
NOT_ATTEMPTED = "not_attempted"


class ModelOccCompanionVerification(BaseModel):
"""Independent read-back proof that an OCC Evidence-Source companion landed.

OMN-14173: the ``receipt_evidence_source_autobind`` arm previously reported
``fix_applied=True`` whenever the adapter call returned without raising —
including the no-op / short-circuit paths that pushed nothing. That produced
a false ``prs_fixed`` count with zero authored companions. This model carries
the *verified effect* (not the call): a companion is proven only when the
product PR body carries ``Evidence-Source: OCC#<n>``, that OCC PR is OPEN,
and the expected ``auto/*`` companion branch exists on the OCC remote. All
three must hold; the verifier fails CLOSED (``verified=False``) on any
missing evidence, resolution error, or unwired verifier.
"""

model_config = ConfigDict(frozen=True, extra="forbid")

verified: bool = Field(
...,
description=(
"True only when the pushed OCC companion + Evidence-Source patch "
"are independently confirmed by a GitHub read-back. Fails closed."
),
)
occ_pr_number: int | None = Field(
default=None,
description="OCC companion PR number read from the product PR body.",
)
occ_branch: str | None = Field(
default=None,
description="Expected auto/* OCC companion branch that was probed.",
)
evidence_source_present: bool = Field(
default=False,
description="Product PR body carries `Evidence-Source: OCC#<n>`.",
)
occ_pr_open: bool = Field(
default=False,
description="The referenced OCC companion PR is in the open state.",
)
branch_exists: bool = Field(
default=False,
description="The auto/* companion branch exists on the OCC remote.",
)
detail: str = Field(
default="",
description="Human-readable verification detail (reason on failure).",
)


class ModelPrLifecycleFixResult(BaseModel):
"""Result of a PR lifecycle fix dispatch."""

Expand All @@ -51,6 +100,19 @@ class ModelPrLifecycleFixResult(BaseModel):
fix_action: str = Field(
..., description="Fix action taken or would be taken (dry_run)."
)
occ_companion_verified: bool = Field(
default=False,
description=(
"OMN-14173 fail-closed accounting: True ONLY when this is an "
"OCC-evidence arm (receipt_evidence_source_autobind) whose pushed "
"OCC companion + Evidence-Source patch were independently verified "
"by a GitHub read-back. Always False for non-OCC arms and for a "
"classified-but-not-minted / no-op / short-circuited OCC dispatch. "
"The orchestrator gates `prs_fixed` on this flag for the autobind "
"arm so a false-success (fix_applied=True, zero companions) can "
"never be counted."
),
)
error: str | None = Field(default=None, description="Error message if fix failed.")
completed_at: datetime = Field(..., description="When the fix completed.")
delegated: bool = Field(
Expand Down Expand Up @@ -83,4 +145,8 @@ class ModelPrLifecycleFixResult(BaseModel):
)


__all__: list[str] = ["EnumDelegationOutcome", "ModelPrLifecycleFixResult"]
__all__: list[str] = [
"EnumDelegationOutcome",
"ModelOccCompanionVerification",
"ModelPrLifecycleFixResult",
]
Loading
Loading