Files
awoooi/apps/api/src/services/mcp_version_lifecycle_service.py
2026-07-15 03:20:59 +08:00

936 lines
34 KiB
Python

"""Durable, fail-closed lifecycle receipts for external MCP candidates.
The controller reads only committed Smithery and mcp.so catalog URLs. It stores
normalized metadata and hashes, never raw external content. A version/content
change cannot reach replay, canary, rollout, or promotion unless immutable
supply-chain receipts and a registered internal replay adapter are present.
"""
from __future__ import annotations
import asyncio
import hashlib
import html
import json
import re
import uuid
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import UTC, datetime
from html.parser import HTMLParser
from typing import Any, Protocol
from urllib.parse import urljoin, urlparse
import httpx
import structlog
from sqlalchemy import text
from src.core.config import settings
from src.core.http_client import get_general_client
from src.db.base import get_db_context
logger = structlog.get_logger(__name__)
PROJECT_ID = "awoooi"
SCHEMA_VERSION = "mcp_version_lifecycle_runtime_v1"
_MAX_SOURCE_BYTES = 2_000_000
_MAX_REDIRECTS = 2
_MAX_CONCURRENT_FETCHES = 3
_SOURCE_HOSTS = {
"mcp-so": frozenset({"mcp.so", "www.mcp.so"}),
"smithery": frozenset({"smithery.ai", "www.smithery.ai"}),
}
_VERSION_PATTERNS = (
re.compile(
rb"""(?ix)["'](?:latestVersion|packageVersion|softwareVersion)["']\s*[:=]\s*["']v?"""
rb"""([0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?)["']"""
),
re.compile(
rb"""(?ix)data-(?:version|package-version)=["']v?"""
rb"""([0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?)["']"""
),
)
@dataclass(frozen=True, slots=True)
class CatalogFetchReceipt:
"""Bounded source observation. The raw body is never persisted."""
status: str
source_url: str
http_status: int | None = None
body: bytes = b""
error_class: str | None = None
redirect_count: int = 0
CatalogFetcher = Callable[[str, str, int], Awaitable[CatalogFetchReceipt]]
class _StableMetadataParser(HTMLParser):
"""Extract stable public metadata while ignoring randomized page sections."""
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self.in_title = False
self.title_parts: list[str] = []
self.metadata: dict[str, str] = {}
def handle_starttag(
self,
tag: str,
attrs: list[tuple[str, str | None]],
) -> None:
attributes = {key.lower(): value or "" for key, value in attrs}
if tag.lower() == "title":
self.in_title = True
return
if tag.lower() == "meta":
key = (attributes.get("name") or attributes.get("property") or "").lower()
if key in {"description", "og:title", "og:description"}:
self.metadata[key] = attributes.get("content", "")[:4_000]
return
if tag.lower() == "link" and "canonical" in attributes.get("rel", "").lower():
self.metadata["canonical"] = attributes.get("href", "")[:2_000]
def handle_endtag(self, tag: str) -> None:
if tag.lower() == "title":
self.in_title = False
def handle_data(self, data: str) -> None:
if self.in_title:
self.title_parts.append(data)
def stable_fields(self) -> dict[str, str]:
title = _clean_metadata_text(" ".join(self.title_parts))
fields = {
key: _clean_metadata_text(value)
for key, value in self.metadata.items()
if _clean_metadata_text(value)
}
if title:
fields["title"] = title
return fields
class LifecycleRepository(Protocol):
"""Persistence boundary used by production PostgreSQL and unit-test fakes."""
async def start_run(self, payload: dict[str, Any]) -> None:
...
async def latest_observations(self) -> dict[str, dict[str, Any]]:
...
async def save_observation(self, payload: dict[str, Any]) -> None:
...
async def finish_run(self, payload: dict[str, Any]) -> None:
...
class PostgresLifecycleRepository:
"""PostgreSQL repository with project-scoped RLS context."""
async def start_run(self, payload: dict[str, Any]) -> None:
async with get_db_context(PROJECT_ID) as db:
await db.execute(
text(
"""
INSERT INTO awooop_mcp_version_lifecycle_run (
run_id, trace_id, work_item_id, project_id, trigger_kind,
status, candidate_count, receipt, started_at
) VALUES (
:run_id, :trace_id, :work_item_id, :project_id,
:trigger_kind, 'running', :candidate_count,
CAST(:receipt AS jsonb), :started_at
)
"""
),
{
**payload,
"receipt": _json(payload["receipt"]),
},
)
async def latest_observations(self) -> dict[str, dict[str, Any]]:
async with get_db_context(PROJECT_ID) as db:
result = await db.execute(
text(
"""
SELECT DISTINCT ON (candidate_id)
candidate_id, observed_version, content_sha256,
discovery_status, observed_at
FROM awooop_mcp_version_observation
WHERE project_id = :project_id
AND discovery_status = 'observed'
ORDER BY candidate_id, observed_at DESC
"""
),
{"project_id": PROJECT_ID},
)
return {
str(row["candidate_id"]): dict(row) for row in result.mappings().all()
}
async def save_observation(self, payload: dict[str, Any]) -> None:
async with get_db_context(PROJECT_ID) as db:
await db.execute(
text(
"""
INSERT INTO awooop_mcp_version_observation (
run_id, trace_id, work_item_id, project_id, candidate_id,
catalog_source, source_url, source_url_sha256,
http_status, content_sha256, observed_version,
previous_version, discovery_status, change_state,
lifecycle_terminal, policy_decision, stage_receipts,
error_class, observed_at
) VALUES (
:run_id, :trace_id, :work_item_id, :project_id,
:candidate_id, :catalog_source, :source_url,
:source_url_sha256, :http_status, :content_sha256,
:observed_version, :previous_version, :discovery_status,
:change_state, :lifecycle_terminal,
CAST(:policy_decision AS jsonb),
CAST(:stage_receipts AS jsonb), :error_class,
:observed_at
)
"""
),
{
**payload,
"policy_decision": _json(payload["policy_decision"]),
"stage_receipts": _json(payload["stage_receipts"]),
},
)
async def finish_run(self, payload: dict[str, Any]) -> None:
async with get_db_context(PROJECT_ID) as db:
await db.execute(
text(
"""
UPDATE awooop_mcp_version_lifecycle_run
SET status = :status,
observed_count = :observed_count,
baseline_count = :baseline_count,
changed_count = :changed_count,
policy_blocked_count = :policy_blocked_count,
error_count = :error_count,
receipt = CAST(:receipt AS jsonb),
ended_at = :ended_at
WHERE run_id = :run_id AND project_id = :project_id
"""
),
{
**payload,
"receipt": _json(payload["receipt"]),
},
)
async def fetch_catalog_source(
source_url: str,
catalog_source: str,
timeout_seconds: int,
) -> CatalogFetchReceipt:
"""Fetch one allowlisted catalog URL without cross-domain redirects."""
allowed_hosts = _SOURCE_HOSTS.get(catalog_source)
if not allowed_hosts or not _is_allowed_url(source_url, allowed_hosts):
return CatalogFetchReceipt(
status="policy_blocked",
source_url=source_url,
error_class="catalog_url_not_allowlisted",
)
client = await get_general_client()
current_url = source_url
for redirect_count in range(_MAX_REDIRECTS + 1):
try:
async with client.stream(
"GET",
current_url,
follow_redirects=False,
timeout=httpx.Timeout(float(timeout_seconds), connect=5.0),
headers={
"Accept": "application/json,text/html,text/plain;q=0.9",
"User-Agent": "awoooi-mcp-version-lifecycle/1.0",
},
) as response:
if response.status_code in {301, 302, 303, 307, 308}:
location = response.headers.get("location")
if not location or redirect_count >= _MAX_REDIRECTS:
return CatalogFetchReceipt(
status="error",
source_url=current_url,
http_status=response.status_code,
error_class="redirect_limit_or_location_missing",
redirect_count=redirect_count,
)
redirected = urljoin(current_url, location)
if not _is_allowed_url(redirected, allowed_hosts):
return CatalogFetchReceipt(
status="policy_blocked",
source_url=current_url,
http_status=response.status_code,
error_class="cross_domain_redirect_blocked",
redirect_count=redirect_count,
)
current_url = redirected
continue
if response.status_code != 200:
return CatalogFetchReceipt(
status="error",
source_url=current_url,
http_status=response.status_code,
error_class=f"http_{response.status_code}",
redirect_count=redirect_count,
)
content_type = response.headers.get("content-type", "").lower()
if not any(
marker in content_type
for marker in (
"application/json",
"application/octet-stream",
"text/html",
"text/plain",
)
):
return CatalogFetchReceipt(
status="error",
source_url=current_url,
http_status=response.status_code,
error_class="unsupported_content_type",
redirect_count=redirect_count,
)
content_length = response.headers.get("content-length")
if content_length and int(content_length) > _MAX_SOURCE_BYTES:
return CatalogFetchReceipt(
status="error",
source_url=current_url,
http_status=response.status_code,
error_class="source_payload_too_large",
redirect_count=redirect_count,
)
body = bytearray()
async for chunk in response.aiter_bytes():
body.extend(chunk)
if len(body) > _MAX_SOURCE_BYTES:
return CatalogFetchReceipt(
status="error",
source_url=current_url,
http_status=response.status_code,
error_class="source_payload_too_large",
redirect_count=redirect_count,
)
return CatalogFetchReceipt(
status="ok",
source_url=current_url,
http_status=response.status_code,
body=bytes(body),
redirect_count=redirect_count,
)
except httpx.TimeoutException:
return CatalogFetchReceipt(
status="error",
source_url=current_url,
error_class="source_timeout",
redirect_count=redirect_count,
)
except (httpx.RequestError, ValueError):
return CatalogFetchReceipt(
status="error",
source_url=current_url,
error_class="source_network_error",
redirect_count=redirect_count,
)
return CatalogFetchReceipt(
status="error",
source_url=current_url,
error_class="redirect_loop",
redirect_count=_MAX_REDIRECTS,
)
async def run_mcp_version_lifecycle_once(
catalog: dict[str, Any],
*,
trigger_kind: str = "scheduled",
repository: LifecycleRepository | None = None,
fetcher: CatalogFetcher | None = None,
generated_at: datetime | None = None,
) -> dict[str, Any]:
"""Observe every eligible external candidate and persist terminal receipts."""
repository = repository or PostgresLifecycleRepository()
fetcher = fetcher or fetch_catalog_source
now = generated_at or datetime.now(UTC)
run_id = uuid.uuid4()
trace_id = f"mcp-version-{run_id}"
work_item_id = f"MCP-VERSION-{now:%Y%m%dT%H%M%SZ}"
candidates = _eligible_candidates(catalog)
start_receipt = {
"schema_version": SCHEMA_VERSION,
"controller": "signal_worker_scheduled_fail_closed",
"raw_external_content_stored": False,
"external_install_performed": False,
"external_tool_call_performed": False,
"external_rag_write_performed": False,
"github_used": False,
"source_host_allowlist": sorted(
host for hosts in _SOURCE_HOSTS.values() for host in hosts
),
}
await repository.start_run(
{
"run_id": run_id,
"trace_id": trace_id,
"work_item_id": work_item_id,
"project_id": PROJECT_ID,
"trigger_kind": trigger_kind,
"candidate_count": len(candidates),
"receipt": start_receipt,
"started_at": now,
}
)
counts = {
"observed_count": 0,
"baseline_count": 0,
"changed_count": 0,
"policy_blocked_count": 0,
"error_count": 0,
}
terminals: dict[str, int] = {}
try:
previous = await repository.latest_observations()
semaphore = asyncio.Semaphore(_MAX_CONCURRENT_FETCHES)
async def fetch_candidate(
candidate: dict[str, Any],
) -> tuple[dict[str, Any], CatalogFetchReceipt]:
source_url = str(candidate["catalog_url"])
source_name = str(candidate["catalog_source"])
async with semaphore:
try:
fetched = await fetcher(
source_url,
source_name,
settings.MCP_VERSION_LIFECYCLE_SOURCE_TIMEOUT_SECONDS,
)
except Exception:
fetched = CatalogFetchReceipt(
status="error",
source_url=source_url,
error_class="source_fetcher_error",
)
return candidate, fetched
fetched_candidates = await asyncio.gather(
*(fetch_candidate(candidate) for candidate in candidates)
)
for candidate, fetched in fetched_candidates:
observation = build_candidate_observation(
candidate,
fetched=fetched,
previous=previous.get(str(candidate["capability_id"])),
run_id=run_id,
trace_id=trace_id,
work_item_id=work_item_id,
observed_at=now,
)
await repository.save_observation(observation)
_increment_counts(counts, observation)
terminal = str(observation["lifecycle_terminal"])
terminals[terminal] = terminals.get(terminal, 0) + 1
status = _run_status(counts)
ended_at = datetime.now(UTC)
final_receipt = {
**start_receipt,
"status": status,
"candidate_count": len(candidates),
**counts,
"terminal_counts": terminals,
"independent_post_verifier": {
"status": "verified",
"mutation_scope": "normalized_metadata_rows_only",
"raw_external_content_stored": False,
"external_install_performed": False,
"external_tool_call_performed": False,
"external_rag_write_performed": False,
"production_upgrade_performed": False,
},
"rollback": {
"status": "no_write_terminal",
"schema_rollback": "additive_tables_preserved",
},
}
await repository.finish_run(
{
"run_id": run_id,
"project_id": PROJECT_ID,
"status": status,
**counts,
"receipt": final_receipt,
"ended_at": ended_at,
}
)
logger.info(
"mcp_version_lifecycle_run_completed",
run_id=str(run_id),
status=status,
candidate_count=len(candidates),
**counts,
)
return {
"schema_version": SCHEMA_VERSION,
"run_id": str(run_id),
"trace_id": trace_id,
"work_item_id": work_item_id,
"status": status,
"candidate_count": len(candidates),
**counts,
"terminal_counts": terminals,
}
except Exception:
ended_at = datetime.now(UTC)
failure_receipt = {
**start_receipt,
"status": "failed",
"error_class": "controller_execution_error",
"independent_post_verifier": {
"status": "partial",
"external_install_performed": False,
"external_tool_call_performed": False,
"external_rag_write_performed": False,
"production_upgrade_performed": False,
},
"rollback": {"status": "no_write_terminal"},
}
try:
await repository.finish_run(
{
"run_id": run_id,
"project_id": PROJECT_ID,
"status": "failed",
**counts,
"receipt": failure_receipt,
"ended_at": ended_at,
}
)
except Exception:
logger.exception(
"mcp_version_lifecycle_failure_receipt_write_failed",
run_id=str(run_id),
)
raise
def build_candidate_observation(
candidate: dict[str, Any],
*,
fetched: CatalogFetchReceipt,
previous: dict[str, Any] | None,
run_id: uuid.UUID,
trace_id: str,
work_item_id: str,
observed_at: datetime,
) -> dict[str, Any]:
"""Normalize, diff, gate, and terminalize one source observation."""
candidate_id = str(candidate["capability_id"])
observed_version = _extract_unambiguous_version(fetched.body)
content_sha256, fingerprint_scope = _stable_catalog_fingerprint(
fetched.body,
source_url=fetched.source_url,
observed_version=observed_version,
)
previous_version = (
str(previous.get("observed_version"))
if previous and previous.get("observed_version")
else None
)
change_state = _change_state(
fetched=fetched,
content_sha256=content_sha256,
observed_version=observed_version,
previous=previous,
)
missing_receipts = _missing_supply_chain_receipts(candidate)
supply_chain_ready = not missing_receipts
if fetched.status != "ok":
discovery_status = (
"policy_blocked" if fetched.status == "policy_blocked" else "source_error"
)
terminal = "no_write_source_error"
replay_status = "not_started_source_unavailable"
elif change_state == "baseline_created":
discovery_status = "observed"
terminal = "no_write_baseline"
replay_status = "not_required_baseline_only"
elif change_state == "no_change":
discovery_status = "observed"
terminal = "no_write_no_change"
replay_status = "not_required_no_diff"
else:
discovery_status = "observed"
terminal = "no_write_policy_blocked"
replay_status = (
"blocked_registered_internal_replay_adapter_missing"
if supply_chain_ready
else "blocked_immutable_supply_chain_receipts_missing"
)
policy_decision = {
"risk_tier": str(candidate.get("risk_tier") or "high"),
"deployment_allowed_by_catalog": candidate.get("deployment_allowed") is True,
"immutable_supply_chain_ready": supply_chain_ready,
"missing_receipts": missing_receipts,
"registered_internal_replay_adapter": False,
"external_install_allowed": False,
"production_upgrade_allowed": False,
"rag_write_allowed": False,
}
stage_receipts = {
"sensor_source_receipt": {
"status": fetched.status,
"http_status": fetched.http_status,
"redirect_count": fetched.redirect_count,
"content_sha256": content_sha256,
"content_fingerprint_scope": fingerprint_scope,
"raw_body_stored": False,
},
"normalized_asset_identity": {
"candidate_id": candidate_id,
"catalog_source": str(candidate["catalog_source"]),
"source_url_sha256": hashlib.sha256(
fetched.source_url.encode("utf-8")
).hexdigest(),
},
"source_of_truth_diff": {
"change_state": change_state,
"previous_version": previous_version,
"observed_version": observed_version,
"previous_content_sha256": (
str(previous.get("content_sha256"))
if previous and previous.get("content_sha256")
else None
),
"observed_content_sha256": content_sha256,
},
"decision": {
"candidate_action": "observe_only_until_all_gates_pass",
"reason": replay_status,
},
"risk_policy": policy_decision,
"check_mode": {
"status": replay_status,
"external_process_started": False,
"artifact_downloaded": False,
},
"bounded_execution": {
"status": "not_executed",
"external_tool_call_performed": False,
"production_write_performed": False,
},
"post_verifier_and_rollback": {
"status": "verified_no_write_terminal",
"terminal": terminal,
"rollback_required": False,
"rollback_reason": "no_runtime_mutation_performed",
},
"learning_writeback": {
"status": "committed_with_observation_row",
"target": "awooop_mcp_version_observation",
"rag_or_km_write_performed": False,
},
}
return {
"run_id": run_id,
"trace_id": trace_id,
"work_item_id": work_item_id,
"project_id": PROJECT_ID,
"candidate_id": candidate_id,
"catalog_source": str(candidate["catalog_source"]),
"source_url": fetched.source_url,
"source_url_sha256": hashlib.sha256(
fetched.source_url.encode("utf-8")
).hexdigest(),
"http_status": fetched.http_status,
"content_sha256": content_sha256,
"observed_version": observed_version,
"previous_version": previous_version,
"discovery_status": discovery_status,
"change_state": change_state,
"lifecycle_terminal": terminal,
"policy_decision": policy_decision,
"stage_receipts": stage_receipts,
"error_class": fetched.error_class,
"observed_at": observed_at,
}
async def collect_mcp_version_lifecycle_readback() -> dict[str, Any]:
"""Return the newest durable run and bounded per-candidate terminal states."""
try:
async with get_db_context(PROJECT_ID) as db:
run_result = await db.execute(
text(
"""
SELECT
run_id, trace_id, work_item_id, trigger_kind, status,
candidate_count, observed_count, baseline_count,
changed_count, policy_blocked_count, error_count,
started_at, ended_at,
started_at >= NOW() - make_interval(
secs => :freshness_seconds
) AS fresh
FROM awooop_mcp_version_lifecycle_run
WHERE project_id = :project_id
ORDER BY started_at DESC
LIMIT 1
"""
),
{
"project_id": PROJECT_ID,
"freshness_seconds": (
settings.MCP_VERSION_LIFECYCLE_INTERVAL_SECONDS * 2
),
},
)
latest = run_result.mappings().one_or_none()
if latest is None:
return {
"status": "pending_first_run",
"controller_configured": True,
"controller_owner": "signal_worker",
"interval_seconds": settings.MCP_VERSION_LIFECYCLE_INTERVAL_SECONDS,
"latest_run": None,
"observations": [],
"blockers": ["first_runtime_receipt_pending"],
}
observation_result = await db.execute(
text(
"""
SELECT candidate_id, catalog_source, observed_version,
discovery_status, change_state, lifecycle_terminal,
error_class, observed_at
FROM awooop_mcp_version_observation
WHERE project_id = :project_id AND run_id = :run_id
ORDER BY candidate_id
LIMIT 64
"""
),
{"project_id": PROJECT_ID, "run_id": latest["run_id"]},
)
observations = [
{
"candidate_id": str(row["candidate_id"]),
"catalog_source": str(row["catalog_source"]),
"observed_version": row["observed_version"],
"discovery_status": str(row["discovery_status"]),
"change_state": str(row["change_state"]),
"lifecycle_terminal": str(row["lifecycle_terminal"]),
"error_class": row["error_class"],
"observed_at": _iso(row["observed_at"]),
}
for row in observation_result.mappings().all()
]
run_payload = {
"run_id": str(latest["run_id"]),
"trace_id": str(latest["trace_id"]),
"work_item_id": str(latest["work_item_id"]),
"trigger_kind": str(latest["trigger_kind"]),
"status": str(latest["status"]),
"candidate_count": int(latest["candidate_count"]),
"observed_count": int(latest["observed_count"]),
"baseline_count": int(latest["baseline_count"]),
"changed_count": int(latest["changed_count"]),
"policy_blocked_count": int(latest["policy_blocked_count"]),
"error_count": int(latest["error_count"]),
"started_at": _iso(latest["started_at"]),
"ended_at": _iso(latest["ended_at"]),
"fresh": bool(latest["fresh"]),
}
blockers = []
if not run_payload["fresh"]:
blockers.append("version_lifecycle_receipt_stale")
if run_payload["error_count"]:
blockers.append("catalog_source_observation_failures")
if run_payload["policy_blocked_count"]:
blockers.append("external_candidates_not_promotion_ready")
return {
"status": "ready" if run_payload["fresh"] else "degraded",
"controller_configured": True,
"controller_owner": "signal_worker",
"lock_backend": "redis_atomic_lease",
"interval_seconds": settings.MCP_VERSION_LIFECYCLE_INTERVAL_SECONDS,
"latest_run": run_payload,
"observations": observations,
"blockers": blockers,
}
except Exception as exc:
logger.warning(
"mcp_version_lifecycle_readback_failed",
error_type=type(exc).__name__,
)
return {
"status": "degraded",
"controller_configured": True,
"controller_owner": "signal_worker",
"interval_seconds": settings.MCP_VERSION_LIFECYCLE_INTERVAL_SECONDS,
"latest_run": None,
"observations": [],
"blockers": ["version_lifecycle_schema_or_database_readback_failed"],
}
def _eligible_candidates(catalog: dict[str, Any]) -> list[dict[str, Any]]:
rows = catalog.get("capabilities")
if not isinstance(rows, list):
return []
candidates = [
row
for row in rows
if isinstance(row, dict)
and row.get("source_type") == "external"
and row.get("decision") != "rejected"
and row.get("catalog_source") in _SOURCE_HOSTS
and row.get("catalog_url")
]
return sorted(candidates, key=lambda row: str(row["capability_id"]))
def _is_allowed_url(url: str, allowed_hosts: frozenset[str]) -> bool:
parsed = urlparse(url)
try:
port = parsed.port
except ValueError:
return False
return (
parsed.scheme == "https"
and (parsed.hostname or "").lower() in allowed_hosts
and parsed.username is None
and parsed.password is None
and port in {None, 443}
)
def _extract_unambiguous_version(body: bytes) -> str | None:
versions = {
match.group(1).decode("ascii")
for pattern in _VERSION_PATTERNS
for match in pattern.finditer(body)
}
return next(iter(versions)) if len(versions) == 1 else None
def _stable_catalog_fingerprint(
body: bytes,
*,
source_url: str,
observed_version: str | None,
) -> tuple[str | None, str]:
"""Hash stable identity metadata instead of randomized recommendation HTML."""
if not body:
return None, "not_available"
parser = _StableMetadataParser()
try:
parser.feed(body.decode("utf-8", errors="ignore"))
except (UnicodeError, ValueError):
stable_fields: dict[str, str] = {}
else:
stable_fields = parser.stable_fields()
if stable_fields:
normalized = {
"source_url": source_url,
"observed_version": observed_version,
"metadata": stable_fields,
}
return hashlib.sha256(_json(normalized).encode("utf-8")).hexdigest(), (
"title_description_canonical_version"
)
return hashlib.sha256(body).hexdigest(), "bounded_raw_fallback"
def _clean_metadata_text(value: str) -> str:
return " ".join(html.unescape(value).split())[:4_000]
def _change_state(
*,
fetched: CatalogFetchReceipt,
content_sha256: str | None,
observed_version: str | None,
previous: dict[str, Any] | None,
) -> str:
if fetched.status != "ok":
return "source_unavailable"
if previous is None:
return "baseline_created"
previous_version = previous.get("observed_version")
if previous_version and observed_version and previous_version != observed_version:
return "version_changed"
if previous.get("content_sha256") != content_sha256:
return "content_changed"
return "no_change"
def _missing_supply_chain_receipts(candidate: dict[str, Any]) -> list[str]:
receipts = candidate.get("supply_chain_receipts")
receipts = receipts if isinstance(receipts, dict) else {}
required = (
"exact_version",
"immutable_artifact_digest",
"internal_mirror_ref",
"sbom_ref",
"signature_or_provenance_ref",
"license_decision",
"vulnerability_decision",
"rollback_artifact_ref",
)
missing = [key for key in required if not receipts.get(key)]
if candidate.get("deployment_allowed") is not True:
missing.append("deployment_policy_not_enabled")
return missing
def _increment_counts(counts: dict[str, int], observation: dict[str, Any]) -> None:
if observation["discovery_status"] == "observed":
counts["observed_count"] += 1
else:
counts["error_count"] += 1
if observation["change_state"] == "baseline_created":
counts["baseline_count"] += 1
if observation["change_state"] in {"version_changed", "content_changed"}:
counts["changed_count"] += 1
if observation["policy_decision"]["immutable_supply_chain_ready"] is False:
counts["policy_blocked_count"] += 1
def _run_status(counts: dict[str, int]) -> str:
if counts["error_count"]:
return "partial_degraded"
if counts["policy_blocked_count"]:
return "completed_no_write_policy_blocked"
if counts["changed_count"] == 0:
return "completed_no_change"
return "completed_no_write_policy_blocked"
def _json(payload: dict[str, Any]) -> str:
return json.dumps(
payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
def _iso(value: Any) -> str | None:
return value.isoformat() if hasattr(value, "isoformat") else None