5227 lines
226 KiB
Python
5227 lines
226 KiB
Python
#!/usr/bin/env python3
|
|
"""Prepare and verify the offline RC-06 asset and Prompt publication batch.
|
|
|
|
This tool is intentionally local-only. ``prepare`` writes one JSON artifact;
|
|
all validation commands are read-only and none of the commands call an HTTP
|
|
API, a database, a model provider, or a publication endpoint.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import csv
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path, PurePath
|
|
from typing import Any, Iterable, Mapping
|
|
|
|
from cryptography.exceptions import InvalidSignature
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa, utils
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
|
|
|
|
SCHEMA_VERSION = "yuqei.rc06-publication-batch.v4"
|
|
DISPOSITION_SCHEMA_VERSION = "yuqei.rc06-golden-dispositions.v2"
|
|
IMAGE_SCHEMA_VERSION = "yuqei.rc06-image-identities.v1"
|
|
APPROVAL_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval.v5"
|
|
APPROVAL_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval-payload.v4"
|
|
APPROVAL_REVIEW_SUMMARY_SCHEMA_VERSION = "yuqei.rc06-single-reviewer-summary.v2"
|
|
APPROVAL_KEYRING_SCHEMA_VERSION = "yuqei.rc06-publication-approval-keyring.v4"
|
|
APPLY_SCHEMA_VERSION = "yuqei.rc06-publication-system-item-apply-receipt.v6"
|
|
APPLY_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-publication-system-item-apply-payload.v4"
|
|
ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION = "yuqei.rc06-online-transaction-receipt.v5"
|
|
ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-online-transaction-payload.v4"
|
|
RUNTIME_RECEIPT_SCHEMA_VERSION = "yuqei.rc06-runtime-snapshot-receipt.v3"
|
|
RUNTIME_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-runtime-snapshot-payload.v2"
|
|
KMS_REQUEST_ID_ATTESTATION_SCHEMA_VERSION = "yuqei.rc06-kms-request-id-attestation.v1"
|
|
KMS_REQUEST_ID_ATTESTATION_PAYLOAD_SCHEMA_VERSION = (
|
|
"yuqei.rc06-kms-request-id-attestation-payload.v1"
|
|
)
|
|
APPLY_CHALLENGE_SCHEMA_VERSION = "yuqei.rc06-apply-challenge.v2"
|
|
APPLY_CHALLENGE_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-apply-challenge-payload.v2"
|
|
APPROVAL_SIGNATURE_DOMAIN = b"YUQEI_RC06_PUBLICATION_BATCH_APPROVAL_V1\0"
|
|
APPROVAL_KEYRING_SIGNATURE_DOMAIN = b"YUQEI_RC06_APPROVAL_KEYRING_V1\0"
|
|
APPLY_SIGNATURE_DOMAIN = b"YUQEI_RC06_SYSTEM_APPLY_RECEIPT_V2\0"
|
|
ONLINE_TRANSACTION_SIGNATURE_DOMAIN = b"YUQEI_RC06_ONLINE_TRANSACTION_RECEIPT_V2\0"
|
|
RUNTIME_SIGNATURE_DOMAIN = b"YUQEI_RC06_RUNTIME_SNAPSHOT_RECEIPT_V2\0"
|
|
ONLINE_REQUEST_ID_ATTESTATION_DOMAIN = (
|
|
b"YUQEI_RC06_ONLINE_TRANSACTION_REQUEST_ID_ATTESTATION_V1\0"
|
|
)
|
|
RUNTIME_REQUEST_ID_ATTESTATION_DOMAIN = (
|
|
b"YUQEI_RC06_RUNTIME_SNAPSHOT_REQUEST_ID_ATTESTATION_V1\0"
|
|
)
|
|
APPLY_CHALLENGE_SIGNATURE_DOMAIN = b"YUQEI_RC06_APPLY_CHALLENGE_V2\0"
|
|
APPLY_OPERATION = "apply_rc06_publication_batch"
|
|
TRANSACTION_MODE = "durable_saga"
|
|
MAX_APPLY_CHALLENGE_TTL = timedelta(minutes=10)
|
|
TRUSTED_APPROVAL_ROOT_KEY_ID = "yuqei-rc06-approval-root-v1"
|
|
# Provision the long-lived root/apply pins and exact candidate keyring epoch
|
|
# before source freeze. A root-signed keyring then authorizes the exact frozen
|
|
# manifest raw SHA without creating a source/keyring hash cycle. Rotation or
|
|
# revocation requires a new epoch and therefore a new frozen candidate.
|
|
TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256 = ""
|
|
FROZEN_APPROVAL_KEYRING_EPOCH = 0
|
|
TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256 = ""
|
|
# These values must be provisioned from the real publication service before
|
|
# source freeze. Empty defaults deliberately make every formal apply receipt
|
|
# fail closed; a caller-provided environment name or key cannot override them.
|
|
TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER = ""
|
|
TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE = ""
|
|
TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION = ""
|
|
TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM = ""
|
|
TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 = ""
|
|
TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT = "rc06-publication-service"
|
|
TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS: tuple[str, ...] = ()
|
|
# Pin the exact canonical legal-review keyring bytes before candidate freeze.
|
|
# Empty deliberately rejects caller-supplied/self-signed reviewer keyrings.
|
|
TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256 = ""
|
|
RC05_SUMMARY_SCHEMA_VERSION = "multiscene-evaluation-run.v1"
|
|
RC05_PLAN_SCHEMA_VERSION = "multiscene-evaluation-plan.v2"
|
|
RC05_CANDIDATE_SCHEMA_VERSION = "multiscene-candidate-binding.v1"
|
|
RC05_RELEASE_CONTROLS_SCHEMA_VERSION = "multiscene-release-controls.v2"
|
|
RC05_LEDGER_SCHEMA_VERSION = "multiscene-call-ledger.v2"
|
|
RC05_LEDGER_SNAPSHOT_SCHEMA_VERSION = "multiscene-call-ledger-snapshot.v2"
|
|
RC05_CAMPAIGN_SCHEMA_VERSION = "multiscene-call-campaign.v2"
|
|
RC05_CONTROL_STATE_SCHEMA_VERSION = "multiscene-release-control-state.v1"
|
|
EXPECTED_RC05_CANARY_CASE_IDS = (
|
|
"base--TPL-AGENCY-DISTRIBUTION-001--normal",
|
|
"base--TPL-DEMAND-LETTER-001--normal",
|
|
"base--TPL-PURCHASE-GOODS-001--normal",
|
|
"scene--TPL-AMENDMENT-GENERAL-001--opposite-adversarial",
|
|
"specialist-trade_procurement-first",
|
|
"intake-tpl-purchase-goods-001",
|
|
)
|
|
|
|
# Each tuple entry is missing evidence, not a request for another human reviewer.
|
|
# Validation removes the first two entries as the single-reviewer approval and
|
|
# system-generated per-item apply evidence are supplied. Runtime Prompt snapshots
|
|
# remain a separate UAT gate.
|
|
BLOCKED_CONTROLS = (
|
|
"single_reviewer_batch_approval_receipt",
|
|
"system_generated_item_apply_receipts_and_online_snapshots",
|
|
"runtime_executable_prompt_snapshots_1_drafting_plus_13_review",
|
|
)
|
|
# Only the stable key owner identity is frozen in source. A title, organization,
|
|
# and authorization scope are human assertions for one exact approval and must
|
|
# be supplied and confirmed during the interactive signing ceremony.
|
|
SOLE_RELEASE_REVIEWER = {
|
|
"approver_id": "chen-yuqi",
|
|
"name": "陈钰麒",
|
|
}
|
|
RELEASE_REVIEWER_IDENTITY_FIELDS = (
|
|
"approver_id",
|
|
"name",
|
|
"role",
|
|
"organization",
|
|
"authorization_scope",
|
|
)
|
|
MAX_APPROVAL_WINDOW = timedelta(days=7)
|
|
RC05_HISTORICAL_ATTEMPT_KEYS = {
|
|
"attempt_id", "plan_sha256", "case_id", "operation",
|
|
"provider_status", "business_status", "elapsed_seconds", "timed_out",
|
|
"request_sha256", "response_sha256", "provider", "model",
|
|
"job_id", "audit_run_id", "identity_path", "identity_file_sha256",
|
|
"ledger_identity_sha256", "claim_path", "claim_sha256", "finish_path",
|
|
"finish_sha256", "before_path", "before_sha256", "after_path",
|
|
"after_sha256", "disposition",
|
|
}
|
|
|
|
EXPECTED_ASSET_COUNT = 60
|
|
EXPECTED_DRAFTING_MODULE_COUNT = 6
|
|
EXPECTED_REVIEW_MODULE_COUNT = 8
|
|
EXPECTED_MODULE_COUNT = EXPECTED_DRAFTING_MODULE_COUNT + EXPECTED_REVIEW_MODULE_COUNT
|
|
EXPECTED_DEFAULT_PROMPT_COUNT = 46
|
|
EXPECTED_RUNTIME_PROMPT_COUNT = 14
|
|
EXPECTED_GOLDEN_COUNT = 31
|
|
EXPECTED_GOLDEN_GENERATION_COUNT = 23
|
|
EXPECTED_GOLDEN_CLAUSE_COUNT = 8
|
|
EXPECTED_RC05_OPERATION_LIMITS = {
|
|
"draft": 120,
|
|
"semantic_review": 30,
|
|
"repair_draft": 8,
|
|
"repair_review": 8,
|
|
"specialist_review": 106,
|
|
"intake": 8,
|
|
}
|
|
REQUIRED_IMAGE_ROLES = (
|
|
"contract-web",
|
|
"contract-api",
|
|
"ai-platform-api",
|
|
"ai-worker",
|
|
)
|
|
|
|
HEX64_RE = re.compile(r"[0-9a-f]{64}")
|
|
COMMIT_RE = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})")
|
|
TAG_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]{0,127}")
|
|
STABLE_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,191}")
|
|
RFC3339_RE = re.compile(
|
|
r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}"
|
|
r"(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})"
|
|
)
|
|
|
|
ASSET_ROOT = (
|
|
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/"
|
|
"template_assets/v1_contract_templates"
|
|
)
|
|
DRAFTING_MODULE_PATH = (
|
|
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/"
|
|
"drafting_prompt_modules.json"
|
|
)
|
|
REVIEW_MODULE_PATH = (
|
|
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/"
|
|
"review_scene_modules.json"
|
|
)
|
|
DEFAULT_PROMPT_PATH = (
|
|
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/"
|
|
"prompt_assets/v1_phase15/default-prompt-assets.json"
|
|
)
|
|
RUNTIME_EXISTING_DOMAIN_PROMPTS = (
|
|
"contract_review_channel_franchise",
|
|
"contract_review_employment_hr",
|
|
"contract_review_investment_finance",
|
|
"contract_review_real_estate_property",
|
|
"contract_review_tech_saas_data_compliance",
|
|
)
|
|
DRAFTING_RUNTIME_MODULE_ORDER = (
|
|
"draft_intake",
|
|
"template_match",
|
|
"missing_facts_questions",
|
|
"clause_assembly_plan",
|
|
"draft_consistency_check",
|
|
"candidate_revision_explanation",
|
|
)
|
|
GOLDEN_INVENTORY_PATH = (
|
|
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/"
|
|
"golden_assets/v1_phase13_legal_knowledge_template_library/inventories/"
|
|
"generation-golden-sample-inventory.csv"
|
|
)
|
|
FROZEN_IMPLEMENTATION_PATHS = (
|
|
"yuqei-infra/scripts/build-rc06-publication-batch.py",
|
|
"yuqei-infra/scripts/apply-rc06-publication-batch.py",
|
|
"yuqei-infra/scripts/build-scene-source-legal-review.py",
|
|
"yuqei-infra/scripts/rc06-single-reviewer-signing.py",
|
|
"yuqei-infra/scripts/rc06-release-trust-bootstrap.py",
|
|
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/contract_templates.py",
|
|
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/contract_scene_assets.py",
|
|
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/template_inputs.py",
|
|
"yuqei-ai-sdk-python/src/yuqei_sdk/review_routing.py",
|
|
DRAFTING_MODULE_PATH,
|
|
REVIEW_MODULE_PATH,
|
|
DEFAULT_PROMPT_PATH,
|
|
GOLDEN_INVENTORY_PATH,
|
|
)
|
|
|
|
CONTRACT_RUNTIME_PACKAGE_ROOT = (
|
|
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api"
|
|
)
|
|
SDK_RUNTIME_PACKAGE_ROOT = "yuqei-ai-sdk-python/src/yuqei_sdk"
|
|
AI_RUNTIME_PACKAGE_ROOT = (
|
|
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api"
|
|
)
|
|
|
|
DISPOSITION_STATUSES = {
|
|
"named_legal_approval",
|
|
"scoped_internal_risk_acceptance",
|
|
"unresolved",
|
|
}
|
|
SECRET_KEYS = {
|
|
"api_key",
|
|
"apikey",
|
|
"authorization",
|
|
"client_secret",
|
|
"credential",
|
|
"credentials",
|
|
"password",
|
|
"private_key",
|
|
"secret",
|
|
"token",
|
|
}
|
|
SECRET_MARKERS = (
|
|
"-----BEGIN PRIVATE KEY-----",
|
|
"-----BEGIN RSA PRIVATE KEY-----",
|
|
"-----BEGIN OPENSSH PRIVATE KEY-----",
|
|
)
|
|
|
|
|
|
class PublicationBatchError(RuntimeError):
|
|
"""An RC-06 batch input or receipt is incomplete, unsafe, or inconsistent."""
|
|
|
|
|
|
def _reject_duplicate_json_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
result: dict[str, Any] = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise PublicationBatchError(f"duplicate JSON key is forbidden: {key}")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _reject_json_constant(value: str) -> None:
|
|
raise PublicationBatchError(f"non-finite JSON value is forbidden: {value}")
|
|
|
|
|
|
def _canonical(value: Any) -> bytes:
|
|
try:
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
except (TypeError, ValueError) as exc:
|
|
raise PublicationBatchError(f"value is not canonical JSON: {exc}") from exc
|
|
|
|
|
|
def _sha256_bytes(value: bytes) -> str:
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
def _sha256_json(value: Any) -> str:
|
|
return _sha256_bytes(_canonical(value))
|
|
|
|
|
|
def _verify_embedded_hash(value: Mapping[str, Any], field: str, label: str) -> str:
|
|
digest = _require_sha(value.get(field), f"{label} {field}")
|
|
projection = {key: child for key, child in value.items() if key != field}
|
|
if _sha256_json(projection) != digest:
|
|
raise PublicationBatchError(f"{label} embedded hash drift")
|
|
return digest
|
|
|
|
|
|
def _require_sha(value: Any, label: str) -> str:
|
|
if not isinstance(value, str) or not HEX64_RE.fullmatch(value):
|
|
raise PublicationBatchError(f"{label} must be a lowercase SHA-256 hex digest")
|
|
return value
|
|
|
|
|
|
def _require_string(value: Any, label: str, *, max_length: int = 4096) -> str:
|
|
if not isinstance(value, str) or not value.strip() or len(value) > max_length:
|
|
raise PublicationBatchError(f"{label} must be a non-empty string")
|
|
return value.strip()
|
|
|
|
|
|
def _require_keys(
|
|
value: Any,
|
|
label: str,
|
|
required: Iterable[str],
|
|
optional: Iterable[str] = (),
|
|
) -> dict[str, Any]:
|
|
if not isinstance(value, dict):
|
|
raise PublicationBatchError(f"{label} must be an object")
|
|
required_set = set(required)
|
|
allowed = required_set | set(optional)
|
|
missing = sorted(required_set - set(value))
|
|
extra = sorted(set(value) - allowed)
|
|
if missing or extra:
|
|
raise PublicationBatchError(f"{label} keys invalid; missing={missing}, extra={extra}")
|
|
return value
|
|
|
|
|
|
def _parse_time(value: Any, label: str) -> datetime:
|
|
if not isinstance(value, str) or not RFC3339_RE.fullmatch(value):
|
|
raise PublicationBatchError(f"{label} must be an RFC3339 timestamp with timezone")
|
|
try:
|
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise PublicationBatchError(f"{label} is not a valid timestamp") from exc
|
|
if parsed.tzinfo is None:
|
|
raise PublicationBatchError(f"{label} must include a timezone")
|
|
return parsed
|
|
|
|
|
|
def _require_count(value: Any, label: str, *, maximum: int | None = None) -> int:
|
|
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
raise PublicationBatchError(f"{label} must be a non-negative integer")
|
|
if maximum is not None and value > maximum:
|
|
raise PublicationBatchError(f"{label} exceeds its frozen limit {maximum}")
|
|
return value
|
|
|
|
|
|
def _require_nonnegative_number(value: Any, label: str) -> float:
|
|
if (
|
|
not isinstance(value, (int, float))
|
|
or isinstance(value, bool)
|
|
or value < 0
|
|
or value != value
|
|
or value in {float("inf"), float("-inf")}
|
|
):
|
|
raise PublicationBatchError(f"{label} must be a finite non-negative number")
|
|
return float(value)
|
|
|
|
|
|
def _validate_elapsed_statistics(
|
|
value: Any,
|
|
label: str,
|
|
*,
|
|
expected_observed: int,
|
|
) -> None:
|
|
row = _require_keys(value, label, ("observed", "p50", "p95"))
|
|
observed = _require_count(row["observed"], f"{label}.observed")
|
|
if observed != expected_observed:
|
|
raise PublicationBatchError(f"{label}.observed does not match its call count")
|
|
if observed == 0:
|
|
if row["p50"] is not None or row["p95"] is not None:
|
|
raise PublicationBatchError(f"{label} percentiles must be null without observations")
|
|
return
|
|
p50 = _require_nonnegative_number(row["p50"], f"{label}.p50")
|
|
p95 = _require_nonnegative_number(row["p95"], f"{label}.p95")
|
|
if p50 > p95:
|
|
raise PublicationBatchError(f"{label} percentiles are not monotonic")
|
|
|
|
|
|
def _assert_no_secret_material(value: Any, label: str = "document") -> None:
|
|
if isinstance(value, dict):
|
|
for key, child in value.items():
|
|
normalized = str(key).strip().casefold().replace("-", "_")
|
|
if normalized in SECRET_KEYS:
|
|
raise PublicationBatchError(f"{label} contains forbidden secret field: {key}")
|
|
_assert_no_secret_material(child, f"{label}.{key}")
|
|
elif isinstance(value, list):
|
|
for index, child in enumerate(value):
|
|
_assert_no_secret_material(child, f"{label}[{index}]")
|
|
elif isinstance(value, str) and any(marker in value for marker in SECRET_MARKERS):
|
|
raise PublicationBatchError(f"{label} contains private-key material")
|
|
|
|
|
|
def _path_has_parent_traversal(path: Path) -> bool:
|
|
return any(part == ".." for part in PurePath(os.fspath(path)).parts)
|
|
|
|
|
|
def _assert_no_symlink_components(path: Path, label: str) -> None:
|
|
absolute = path.absolute()
|
|
parts = absolute.parts
|
|
if not parts:
|
|
raise PublicationBatchError(f"{label} is empty")
|
|
current = Path(parts[0])
|
|
for part in parts[1:]:
|
|
current = current / part
|
|
if not current.exists() and not current.is_symlink():
|
|
continue
|
|
try:
|
|
mode = current.lstat().st_mode
|
|
except OSError as exc:
|
|
raise PublicationBatchError(f"cannot inspect {label}: {exc}") from exc
|
|
file_attributes = getattr(current.lstat(), "st_file_attributes", 0)
|
|
reparse_flag = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
|
|
if stat.S_ISLNK(mode) or bool(reparse_flag and file_attributes & reparse_flag):
|
|
raise PublicationBatchError(f"{label} must not traverse a symbolic link: {current}")
|
|
|
|
|
|
def secure_read_bytes(path: Path, label: str, *, maximum_bytes: int = 128 * 1024 * 1024) -> bytes:
|
|
if _path_has_parent_traversal(path):
|
|
raise PublicationBatchError(f"{label} must not contain parent traversal")
|
|
_assert_no_symlink_components(path, label)
|
|
lexical = path.absolute()
|
|
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
try:
|
|
descriptor = os.open(lexical, flags)
|
|
except OSError as exc:
|
|
raise PublicationBatchError(f"cannot open {label}: {exc}") from exc
|
|
try:
|
|
opened = os.fstat(descriptor)
|
|
named = os.lstat(lexical)
|
|
if (
|
|
not stat.S_ISREG(opened.st_mode)
|
|
or not stat.S_ISREG(named.st_mode)
|
|
or not os.path.samestat(opened, named)
|
|
):
|
|
raise PublicationBatchError(f"{label} must be a stable regular file")
|
|
if opened.st_size < 0 or opened.st_size > maximum_bytes:
|
|
raise PublicationBatchError(f"{label} exceeds the {maximum_bytes}-byte limit")
|
|
chunks: list[bytes] = []
|
|
remaining = opened.st_size
|
|
while remaining:
|
|
chunk = os.read(descriptor, min(1024 * 1024, remaining))
|
|
if not chunk:
|
|
raise PublicationBatchError(f"{label} changed while it was being read")
|
|
chunks.append(chunk)
|
|
remaining -= len(chunk)
|
|
if os.read(descriptor, 1):
|
|
raise PublicationBatchError(f"{label} changed while it was being read")
|
|
after = os.fstat(descriptor)
|
|
try:
|
|
named_after = os.lstat(lexical)
|
|
except OSError as exc:
|
|
raise PublicationBatchError(f"{label} changed while it was being read") from exc
|
|
if (
|
|
after.st_size != opened.st_size
|
|
or after.st_mtime_ns != opened.st_mtime_ns
|
|
or after.st_ctime_ns != opened.st_ctime_ns
|
|
or not os.path.samestat(after, named_after)
|
|
):
|
|
raise PublicationBatchError(f"{label} changed while it was being read")
|
|
return b"".join(chunks)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def secure_read_json(path: Path, label: str, expected_sha256: str | None = None) -> tuple[Any, str]:
|
|
raw = secure_read_bytes(path, label)
|
|
actual = _sha256_bytes(raw)
|
|
if expected_sha256 is not None and actual != _require_sha(expected_sha256, f"{label} expected SHA-256"):
|
|
raise PublicationBatchError(f"{label} raw SHA-256 mismatch")
|
|
try:
|
|
payload = json.loads(
|
|
raw,
|
|
object_pairs_hook=_reject_duplicate_json_pairs,
|
|
parse_constant=_reject_json_constant,
|
|
)
|
|
except PublicationBatchError:
|
|
raise
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise PublicationBatchError(f"{label} is not valid UTF-8 JSON") from exc
|
|
return payload, actual
|
|
|
|
|
|
def _safe_output_path(path: Path) -> None:
|
|
if _path_has_parent_traversal(path):
|
|
raise PublicationBatchError("output path must not contain parent traversal")
|
|
if path.exists() or path.is_symlink():
|
|
raise PublicationBatchError("output already exists; refusing to overwrite immutable evidence")
|
|
_assert_no_symlink_components(path.parent, "output parent")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
_assert_no_symlink_components(path.parent, "output parent")
|
|
|
|
|
|
def _run_git(repo: Path, *args: str, binary: bool = False) -> str | bytes:
|
|
completed = subprocess.run(
|
|
["git", "-c", f"safe.directory={repo.resolve()}", "-C", str(repo), *args],
|
|
check=False,
|
|
capture_output=True,
|
|
timeout=30,
|
|
env={
|
|
**{key: os.environ[key] for key in ("SYSTEMROOT", "WINDIR", "PATH") if key in os.environ},
|
|
"GIT_CONFIG_NOSYSTEM": "1",
|
|
"GIT_NO_REPLACE_OBJECTS": "1",
|
|
"GIT_OPTIONAL_LOCKS": "0",
|
|
"GIT_TERMINAL_PROMPT": "0",
|
|
"LANG": "C.UTF-8",
|
|
"LC_ALL": "C.UTF-8",
|
|
},
|
|
)
|
|
if completed.returncode:
|
|
message = completed.stderr.decode("utf-8", "replace").strip()
|
|
raise PublicationBatchError(f"git {' '.join(args)} failed: {message}")
|
|
return completed.stdout if binary else completed.stdout.decode("utf-8", "strict").strip()
|
|
|
|
|
|
def validate_candidate(repo: Path, commit: str, tag: str) -> None:
|
|
if not COMMIT_RE.fullmatch(commit):
|
|
raise PublicationBatchError("commit must be a full lowercase Git object id")
|
|
if not TAG_RE.fullmatch(tag) or ".." in tag or tag.endswith("/"):
|
|
raise PublicationBatchError("tag is invalid or ambiguous")
|
|
resolved_commit = str(_run_git(repo, "rev-parse", "--verify", f"{commit}^{{commit}}"))
|
|
if resolved_commit != commit:
|
|
raise PublicationBatchError("commit does not resolve exactly")
|
|
resolved_tag = str(_run_git(repo, "rev-parse", "--verify", f"refs/tags/{tag}^{{commit}}"))
|
|
if resolved_tag != commit:
|
|
raise PublicationBatchError("tag does not resolve to the requested commit")
|
|
|
|
|
|
def _relative_source_path(repo: Path, path: Path) -> str:
|
|
try:
|
|
relative = path.resolve(strict=True).relative_to(repo.resolve(strict=True))
|
|
except (OSError, ValueError) as exc:
|
|
raise PublicationBatchError(f"source escapes the repository: {path}") from exc
|
|
return relative.as_posix()
|
|
|
|
|
|
def _inventory_source_paths(repo: Path, commit: str | None = None) -> list[str]:
|
|
paths: list[str] = []
|
|
if commit is None:
|
|
asset_root = repo / ASSET_ROOT
|
|
_assert_no_symlink_components(asset_root, "template asset root")
|
|
if not asset_root.is_dir():
|
|
raise PublicationBatchError("template asset root is missing")
|
|
for path in asset_root.rglob("*"):
|
|
if path.is_symlink():
|
|
raise PublicationBatchError(f"template source must not be a symbolic link: {path}")
|
|
if path.is_file() and path.suffix.lower() in {".json", ".md"}:
|
|
paths.append(_relative_source_path(repo, path))
|
|
for package_root in (
|
|
CONTRACT_RUNTIME_PACKAGE_ROOT,
|
|
SDK_RUNTIME_PACKAGE_ROOT,
|
|
AI_RUNTIME_PACKAGE_ROOT,
|
|
):
|
|
root = repo / package_root
|
|
_assert_no_symlink_components(root, f"runtime package {package_root}")
|
|
paths.extend(
|
|
_relative_source_path(repo, path)
|
|
for path in root.rglob("*.py")
|
|
if path.is_file() and not path.is_symlink()
|
|
)
|
|
else:
|
|
listing = str(
|
|
_run_git(
|
|
repo,
|
|
"ls-tree",
|
|
"-r",
|
|
"--name-only",
|
|
commit,
|
|
"--",
|
|
ASSET_ROOT,
|
|
CONTRACT_RUNTIME_PACKAGE_ROOT,
|
|
SDK_RUNTIME_PACKAGE_ROOT,
|
|
AI_RUNTIME_PACKAGE_ROOT,
|
|
)
|
|
)
|
|
for relative in listing.splitlines():
|
|
# Freeze every tracked package resource, not only Python modules.
|
|
# Runtime code resolves template and other data files relative to
|
|
# ``__file__``; omitting those transitive inputs would allow the
|
|
# inventory to consume mutable worktree data after code validation.
|
|
if (
|
|
relative.startswith(f"{CONTRACT_RUNTIME_PACKAGE_ROOT}/")
|
|
or relative.startswith(f"{SDK_RUNTIME_PACKAGE_ROOT}/")
|
|
or relative.startswith(f"{AI_RUNTIME_PACKAGE_ROOT}/")
|
|
):
|
|
paths.append(relative)
|
|
paths.extend(FROZEN_IMPLEMENTATION_PATHS)
|
|
return sorted(set(paths))
|
|
|
|
|
|
def _source_manifest_index(payload: Any, commit: str) -> tuple[dict[str, str], str | None]:
|
|
manifest = _require_keys(
|
|
payload,
|
|
"source manifest",
|
|
("schema_version", "candidate", "files", "summaries", "claims_not_made", "manifest_payload_sha256"),
|
|
)
|
|
if manifest["schema_version"] != "yuqei.release-source-manifest.v1":
|
|
raise PublicationBatchError("unsupported source manifest schema")
|
|
_verify_embedded_hash(manifest, "manifest_payload_sha256", "source manifest")
|
|
candidate = _require_keys(
|
|
manifest["candidate"],
|
|
"source manifest candidate",
|
|
("commit", "commit_timestamp", "tree", "worktree_was_clean", "identity_policy"),
|
|
)
|
|
if candidate["commit"] != commit:
|
|
raise PublicationBatchError("source manifest commit does not match the batch")
|
|
if candidate["worktree_was_clean"] is not True:
|
|
raise PublicationBatchError("source manifest is not from a clean candidate")
|
|
files = _require_keys(
|
|
manifest["files"],
|
|
"source manifest files",
|
|
("count", "total_size", "content_index_sha256", "items"),
|
|
)
|
|
if not isinstance(files["items"], list):
|
|
raise PublicationBatchError("source manifest file inventory is missing")
|
|
if files["count"] != len(files["items"]):
|
|
raise PublicationBatchError("source manifest file count is inconsistent")
|
|
_require_sha(files["content_index_sha256"], "source manifest content index SHA-256")
|
|
index: dict[str, str] = {}
|
|
for number, item in enumerate(files["items"]):
|
|
row = _require_keys(
|
|
item,
|
|
f"source manifest file {number}",
|
|
("path", "mode", "git_blob_oid", "size", "sha256"),
|
|
)
|
|
path = row["path"]
|
|
digest = row["sha256"]
|
|
if not isinstance(path, str) or path.startswith(("/", "\\")) or ".." in PurePath(path).parts:
|
|
raise PublicationBatchError("source manifest contains an unsafe path")
|
|
_require_sha(digest, f"source manifest {path}")
|
|
if path in index:
|
|
raise PublicationBatchError(f"duplicate source manifest path: {path}")
|
|
index[path] = digest
|
|
return index, manifest["manifest_payload_sha256"]
|
|
|
|
|
|
def verify_frozen_sources(
|
|
repo: Path,
|
|
commit: str,
|
|
source_manifest: Any,
|
|
required_paths: Iterable[str],
|
|
) -> None:
|
|
index, _ = _source_manifest_index(source_manifest, commit)
|
|
for relative in required_paths:
|
|
if relative not in index:
|
|
raise PublicationBatchError(f"frozen source is absent from source manifest: {relative}")
|
|
path = repo / Path(relative)
|
|
working = secure_read_bytes(path, f"frozen source {relative}")
|
|
committed = bytes(_run_git(repo, "show", f"{commit}:{relative}", binary=True))
|
|
if _sha256_bytes(committed) != index[relative]:
|
|
raise PublicationBatchError(f"commit source differs from source manifest: {relative}")
|
|
# Windows may check a text file out with CRLF while the frozen blob is
|
|
# LF. Accept only that transparent transform; never accept other
|
|
# filtered or semantic changes.
|
|
checkout_equivalent = working == committed
|
|
if not checkout_equivalent and b"\0" not in working and b"\0" not in committed:
|
|
checkout_equivalent = working.replace(b"\r\n", b"\n") == committed.replace(b"\r\n", b"\n")
|
|
if not checkout_equivalent:
|
|
raise PublicationBatchError(f"working source differs from frozen commit: {relative}")
|
|
|
|
|
|
def _write_frozen_source(path: Path, raw: bytes, label: str) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
_assert_no_symlink_components(path.parent, f"{label} parent")
|
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
|
|
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
descriptor: int | None = None
|
|
try:
|
|
descriptor = os.open(path, flags, 0o600)
|
|
offset = 0
|
|
while offset < len(raw):
|
|
written = os.write(descriptor, raw[offset:])
|
|
if written <= 0:
|
|
raise PublicationBatchError(f"cannot stage complete {label}")
|
|
offset += written
|
|
os.fsync(descriptor)
|
|
except OSError as exc:
|
|
raise PublicationBatchError(f"cannot stage {label}: {exc}") from exc
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if secure_read_bytes(path, label) != raw:
|
|
raise PublicationBatchError(f"staged {label} differs from the frozen Git object")
|
|
|
|
|
|
def stage_frozen_sources(
|
|
repo: Path,
|
|
commit: str,
|
|
source_manifest: Any,
|
|
required_paths: Iterable[str],
|
|
destination: Path,
|
|
) -> None:
|
|
"""Materialize verified Git-object bytes into a private, isolated source tree."""
|
|
|
|
index, _ = _source_manifest_index(source_manifest, commit)
|
|
destination.mkdir(mode=0o700, parents=True, exist_ok=False)
|
|
_assert_no_symlink_components(destination, "frozen staging root")
|
|
for relative in required_paths:
|
|
if relative not in index:
|
|
raise PublicationBatchError(f"frozen source is absent from source manifest: {relative}")
|
|
raw = bytes(_run_git(repo, "show", f"{commit}:{relative}", binary=True))
|
|
if _sha256_bytes(raw) != index[relative]:
|
|
raise PublicationBatchError(f"commit source differs from source manifest: {relative}")
|
|
_write_frozen_source(
|
|
destination / Path(relative),
|
|
raw,
|
|
f"frozen staged source {relative}",
|
|
)
|
|
|
|
|
|
def _project_imports(repo: Path) -> tuple[list[str], dict[str, Any]]:
|
|
paths = (
|
|
repo / "yuqei-contract-platform/services/contract-api/src",
|
|
repo / "yuqei-ai-sdk-python/src",
|
|
)
|
|
inserted: list[str] = []
|
|
for path in reversed(paths):
|
|
value = str(path)
|
|
if value not in sys.path:
|
|
sys.path.insert(0, value)
|
|
inserted.append(value)
|
|
prefixes = ("yuqei_contract_api", "yuqei_sdk")
|
|
previous = {
|
|
name: module
|
|
for name, module in list(sys.modules.items())
|
|
if name == prefixes[0]
|
|
or name.startswith(f"{prefixes[0]}.")
|
|
or name == prefixes[1]
|
|
or name.startswith(f"{prefixes[1]}.")
|
|
}
|
|
for name in previous:
|
|
sys.modules.pop(name, None)
|
|
return inserted, previous
|
|
|
|
|
|
def _load_inventory_symbols(repo: Path) -> tuple[Any, Any, Any, Any, Any]:
|
|
inserted, previous = _project_imports(repo)
|
|
try:
|
|
from yuqei_contract_api.contract_templates import ( # type: ignore
|
|
_load_template_body,
|
|
list_contract_templates,
|
|
template_document_kind,
|
|
)
|
|
from yuqei_sdk.review_routing import ( # type: ignore
|
|
REVIEW_ROUTING_VERSION,
|
|
review_prompt_routes,
|
|
)
|
|
return (
|
|
_load_template_body,
|
|
list_contract_templates,
|
|
template_document_kind,
|
|
REVIEW_ROUTING_VERSION,
|
|
review_prompt_routes,
|
|
)
|
|
except ImportError as exc:
|
|
raise PublicationBatchError(f"cannot import frozen template inventory: {exc}") from exc
|
|
finally:
|
|
prefixes = ("yuqei_contract_api", "yuqei_sdk")
|
|
for name in list(sys.modules):
|
|
if (
|
|
name == prefixes[0]
|
|
or name.startswith(f"{prefixes[0]}.")
|
|
or name == prefixes[1]
|
|
or name.startswith(f"{prefixes[1]}.")
|
|
):
|
|
sys.modules.pop(name, None)
|
|
sys.modules.update(previous)
|
|
for value in inserted:
|
|
try:
|
|
sys.path.remove(value)
|
|
except ValueError: # pragma: no cover - defensive cleanup.
|
|
pass
|
|
|
|
|
|
def _load_executable_prompt_dependencies(repo: Path) -> list[dict[str, Any]]:
|
|
"""Render the candidate's actual runtime Prompt content for 14 identities."""
|
|
|
|
paths = (
|
|
repo / "yuqei-ai-platform/services/ai-platform-api/src",
|
|
repo / "yuqei-ai-sdk-python/src",
|
|
)
|
|
inserted: list[str] = []
|
|
for path in reversed(paths):
|
|
value = str(path)
|
|
if value not in sys.path:
|
|
sys.path.insert(0, value)
|
|
inserted.append(value)
|
|
prefixes = ("yuqei_ai_platform_api", "yuqei_sdk")
|
|
previous = {
|
|
name: module
|
|
for name, module in list(sys.modules.items())
|
|
if any(name == prefix or name.startswith(f"{prefix}.") for prefix in prefixes)
|
|
}
|
|
for name in previous:
|
|
sys.modules.pop(name, None)
|
|
try:
|
|
from yuqei_ai_platform_api.contract_drafting import ( # type: ignore
|
|
DRAFT_PROMPT_CONTENT,
|
|
DRAFT_PROMPT_NAMES,
|
|
DRAFT_PROMPT_VERSION,
|
|
)
|
|
from yuqei_ai_platform_api.drafting_prompt_modules import ( # type: ignore
|
|
drafting_module_evidence,
|
|
)
|
|
from yuqei_ai_platform_api.packaged_prompt_assets_import import ( # type: ignore
|
|
DEFAULT_VERSION,
|
|
load_packaged_prompt_assets,
|
|
)
|
|
from yuqei_ai_platform_api.review_scene_prompts import ( # type: ignore
|
|
review_module_evidence,
|
|
scene_review_template_for_route,
|
|
)
|
|
|
|
drafting_evidence = drafting_module_evidence(DRAFT_PROMPT_CONTENT)
|
|
results: list[dict[str, Any]] = [
|
|
{
|
|
"kind": "composed_drafting",
|
|
"runtime_key": DRAFT_PROMPT_NAMES[0],
|
|
"version": DRAFT_PROMPT_VERSION,
|
|
"content_sha256": _sha256_bytes(DRAFT_PROMPT_CONTENT.encode("utf-8")),
|
|
"publication_dependency": "six_module_composition",
|
|
"publication_module_dependencies": drafting_evidence,
|
|
}
|
|
]
|
|
review_routes = sorted(
|
|
item["key"]
|
|
for item in json.loads((repo / REVIEW_MODULE_PATH).read_text(encoding="utf-8"))[
|
|
"modules"
|
|
]
|
|
)
|
|
for route in review_routes:
|
|
template = scene_review_template_for_route(
|
|
route,
|
|
review_profile="scene_evaluation_v4",
|
|
scene_template_id="rc06-runtime-snapshot",
|
|
)
|
|
module_evidence = review_module_evidence(template)
|
|
if len(module_evidence) != 1 or module_evidence[0]["key"] != route:
|
|
raise PublicationBatchError(
|
|
f"runtime review Prompt does not contain its exact module: {route}"
|
|
)
|
|
results.append(
|
|
{
|
|
"kind": "new_review_module",
|
|
"runtime_key": route,
|
|
"version": template.version,
|
|
"content_sha256": template.content_hash,
|
|
"publication_dependency": "published_module",
|
|
"publication_module_dependencies": module_evidence,
|
|
}
|
|
)
|
|
|
|
packaged, _, duplicates = load_packaged_prompt_assets(repo / DEFAULT_PROMPT_PATH)
|
|
if duplicates:
|
|
raise PublicationBatchError("default Prompt runtime inventory contains duplicates")
|
|
by_key = {item.prompt_key: item for item in packaged}
|
|
for key in RUNTIME_EXISTING_DOMAIN_PROMPTS:
|
|
if key not in by_key:
|
|
raise PublicationBatchError(f"existing runtime domain Prompt is absent: {key}")
|
|
results.append(
|
|
{
|
|
"kind": "existing_domain_prompt",
|
|
"runtime_key": key,
|
|
"version": DEFAULT_VERSION,
|
|
"content_sha256": _sha256_bytes(by_key[key].content.encode("utf-8")),
|
|
"publication_dependency": "preexisting_domain_prompt",
|
|
"publication_module_dependencies": [],
|
|
}
|
|
)
|
|
results.sort(key=lambda item: (item["kind"], item["runtime_key"]))
|
|
if len(results) != EXPECTED_RUNTIME_PROMPT_COUNT or len(
|
|
{item["runtime_key"] for item in results}
|
|
) != EXPECTED_RUNTIME_PROMPT_COUNT:
|
|
raise PublicationBatchError("runtime Prompt dependency inventory must contain 14 identities")
|
|
return results
|
|
except (ImportError, OSError, ValueError, KeyError, TypeError) as exc:
|
|
raise PublicationBatchError(f"cannot render executable runtime Prompts: {exc}") from exc
|
|
finally:
|
|
for name in list(sys.modules):
|
|
if any(name == prefix or name.startswith(f"{prefix}.") for prefix in prefixes):
|
|
sys.modules.pop(name, None)
|
|
sys.modules.update(previous)
|
|
for value in inserted:
|
|
try:
|
|
sys.path.remove(value)
|
|
except ValueError: # pragma: no cover
|
|
pass
|
|
|
|
|
|
def _asset_hash_projection(asset: Mapping[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"id": asset["template_id"],
|
|
"contract_type": asset["contract_type"],
|
|
"required_variables": asset["required_variables"],
|
|
"optional_variables": asset["optional_variables"],
|
|
"validation_rules": asset["validation_rules"],
|
|
"input_schema": asset["input_schema"],
|
|
"clause_ids": asset["clause_ids"],
|
|
"review_rule_ids": asset["review_rule_ids"],
|
|
"body_content": asset["body_content"],
|
|
}
|
|
|
|
|
|
def _finish_asset(asset: dict[str, Any]) -> dict[str, Any]:
|
|
asset["body_sha256"] = _sha256_bytes(asset["body_content"].encode("utf-8"))
|
|
asset["input_schema_sha256"] = (
|
|
_sha256_json(asset["input_schema"]) if asset["input_schema"] is not None else None
|
|
)
|
|
asset["variables_sha256"] = _sha256_json(
|
|
{"required": asset["required_variables"], "optional": asset["optional_variables"]}
|
|
)
|
|
asset["review_identity_sha256"] = _sha256_json(asset["review_identity"])
|
|
calculated_content = _sha256_json(_asset_hash_projection(asset))
|
|
if asset.get("content_sha256") != calculated_content:
|
|
raise PublicationBatchError(f"runtime content hash drift for {asset.get('template_id')}")
|
|
payload = {key: value for key, value in asset.items() if key != "asset_payload_sha256"}
|
|
asset["asset_payload_sha256"] = _sha256_json(payload)
|
|
return asset
|
|
|
|
|
|
def load_runtime_inventory(
|
|
repo: Path,
|
|
*,
|
|
frozen_source_paths: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Load the real source catalog without consulting a service or database."""
|
|
(
|
|
_load_template_body,
|
|
list_contract_templates,
|
|
template_document_kind,
|
|
REVIEW_ROUTING_VERSION,
|
|
review_prompt_routes,
|
|
) = _load_inventory_symbols(repo)
|
|
|
|
page = list_contract_templates()
|
|
if page.total != EXPECTED_ASSET_COUNT or len(page.items) != EXPECTED_ASSET_COUNT:
|
|
raise PublicationBatchError(
|
|
f"source catalog must contain exactly {EXPECTED_ASSET_COUNT} templates; got {page.total}"
|
|
)
|
|
assets: list[dict[str, Any]] = []
|
|
seen_assets: set[str] = set()
|
|
for template in page.items:
|
|
if template.id in seen_assets:
|
|
raise PublicationBatchError(f"duplicate template ID: {template.id}")
|
|
seen_assets.add(template.id)
|
|
body = _load_template_body(template.id)
|
|
schema = template.input_schema.model_dump(mode="json") if template.input_schema else None
|
|
scene = schema.get("scene") if isinstance(schema, dict) else None
|
|
document_kind = (
|
|
scene.get("document_kind")
|
|
if isinstance(scene, dict) and isinstance(scene.get("document_kind"), str)
|
|
else template_document_kind(template.contract_type)
|
|
)
|
|
required = [item.model_dump(mode="json") for item in template.required_variables]
|
|
optional = [item.model_dump(mode="json") for item in template.optional_variables]
|
|
routes = list(review_prompt_routes(template.contract_type, template.id))
|
|
review_identity = {
|
|
"routing_version": REVIEW_ROUTING_VERSION,
|
|
"prompt_routes": routes,
|
|
"review_rule_ids": list(template.review_rule_ids),
|
|
}
|
|
assets.append(
|
|
_finish_asset(
|
|
{
|
|
"template_id": template.id,
|
|
"name": template.name,
|
|
"version": template.lifecycle_version or template.version,
|
|
"document_kind": document_kind,
|
|
"contract_type": template.contract_type,
|
|
"source": template.source,
|
|
"source_lifecycle_status": template.lifecycle_status,
|
|
"body_content": body,
|
|
"content_sha256": template.content_sha256,
|
|
"input_schema": schema,
|
|
"required_variables": required,
|
|
"optional_variables": optional,
|
|
"validation_rules": list(template.validation_rules),
|
|
"clause_ids": list(template.clause_ids),
|
|
"review_rule_ids": list(template.review_rule_ids),
|
|
"review_identity": review_identity,
|
|
}
|
|
)
|
|
)
|
|
assets.sort(key=lambda item: item["template_id"])
|
|
|
|
drafting_raw, _ = secure_read_json(repo / DRAFTING_MODULE_PATH, "drafting module source")
|
|
review_raw, _ = secure_read_json(repo / REVIEW_MODULE_PATH, "review module source")
|
|
if not isinstance(drafting_raw, list) or len(drafting_raw) != EXPECTED_DRAFTING_MODULE_COUNT:
|
|
raise PublicationBatchError("drafting module source must contain exactly 6 modules")
|
|
if not isinstance(review_raw, dict) or review_raw.get("schema_version") != "review-scene-modules.v1":
|
|
raise PublicationBatchError("review module source schema is invalid")
|
|
review_modules = review_raw.get("modules")
|
|
if not isinstance(review_modules, list) or len(review_modules) != EXPECTED_REVIEW_MODULE_COUNT:
|
|
raise PublicationBatchError("review module source must contain exactly 8 modules")
|
|
|
|
modules: list[dict[str, Any]] = []
|
|
for source in drafting_raw:
|
|
row = _require_keys(source, "drafting module", ("key", "version", "instructions"))
|
|
key = _require_string(row["key"], "drafting module key", max_length=128)
|
|
content = _require_string(row["instructions"], f"{key} content", max_length=200_000)
|
|
module = {
|
|
"category": "drafting",
|
|
"key": key,
|
|
"version": _require_string(row["version"], f"{key} version", max_length=128),
|
|
"content": content,
|
|
"content_sha256": _sha256_bytes(content.encode("utf-8")),
|
|
"publication_mapping": {
|
|
"prompt_key": "contract_drafting",
|
|
"route": "contract_drafting",
|
|
},
|
|
}
|
|
module["module_payload_sha256"] = _sha256_json(module)
|
|
modules.append(module)
|
|
for source in review_modules:
|
|
row = _require_keys(source, "review module", ("key", "version", "text"))
|
|
key = _require_string(row["key"], "review module key", max_length=128)
|
|
content = _require_string(row["text"], f"{key} content", max_length=200_000)
|
|
module = {
|
|
"category": "review",
|
|
"key": key,
|
|
"version": _require_string(row["version"], f"{key} version", max_length=128),
|
|
"content": content,
|
|
"content_sha256": _sha256_bytes(content.encode("utf-8")),
|
|
"publication_mapping": {"prompt_key": key, "route": key},
|
|
}
|
|
module["module_payload_sha256"] = _sha256_json(module)
|
|
modules.append(module)
|
|
modules.sort(key=lambda item: (item["category"], item["key"]))
|
|
module_keys = [item["key"] for item in modules]
|
|
if len(module_keys) != EXPECTED_MODULE_COUNT or len(set(module_keys)) != EXPECTED_MODULE_COUNT:
|
|
raise PublicationBatchError("the 14 Prompt modules must have unique keys")
|
|
|
|
default_raw, default_raw_sha = secure_read_json(repo / DEFAULT_PROMPT_PATH, "default Prompt source")
|
|
if not isinstance(default_raw, dict) or not isinstance(default_raw.get("assets"), list):
|
|
raise PublicationBatchError("default Prompt source is invalid")
|
|
default_assets = default_raw["assets"]
|
|
if len(default_assets) != EXPECTED_DEFAULT_PROMPT_COUNT:
|
|
raise PublicationBatchError("default Prompt inventory must contain exactly 46 assets")
|
|
default_inventory = []
|
|
for asset in default_assets:
|
|
if not isinstance(asset, dict):
|
|
raise PublicationBatchError("default Prompt asset is invalid")
|
|
key = _require_string(asset.get("prompt_key"), "default Prompt key", max_length=128)
|
|
default_inventory.append(
|
|
{
|
|
"prompt_key": key,
|
|
"prompt_type": _require_string(asset.get("prompt_type"), f"{key} type", max_length=128),
|
|
"asset_sha256": _sha256_json(asset),
|
|
}
|
|
)
|
|
if len({item["prompt_key"] for item in default_inventory}) != EXPECTED_DEFAULT_PROMPT_COUNT:
|
|
raise PublicationBatchError("default Prompt inventory contains duplicate keys")
|
|
if set(module_keys).intersection(item["prompt_key"] for item in default_inventory):
|
|
raise PublicationBatchError("14-module batch must remain distinct from 46 default Prompt assets")
|
|
default_inventory.sort(key=lambda item: item["prompt_key"])
|
|
|
|
golden_raw = secure_read_bytes(repo / GOLDEN_INVENTORY_PATH, "Golden inventory")
|
|
try:
|
|
rows = list(csv.DictReader(golden_raw.decode("utf-8-sig").splitlines()))
|
|
except UnicodeDecodeError as exc:
|
|
raise PublicationBatchError("Golden inventory is not valid UTF-8") from exc
|
|
if len(rows) != EXPECTED_GOLDEN_COUNT:
|
|
raise PublicationBatchError("Golden inventory must contain exactly 31 rows")
|
|
golden_cases: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
sample_id = _require_string(row.get("sample_id"), "Golden sample ID", max_length=128)
|
|
asset_id = _require_string(row.get("template_id"), f"{sample_id} asset ID", max_length=128)
|
|
suite = "clause_recommendation" if asset_id.startswith("CLAUSE-") else "contract_generation"
|
|
golden_cases.append(
|
|
{
|
|
"sample_id": sample_id,
|
|
"suite": suite,
|
|
"asset_id": asset_id,
|
|
"source_case_sha256": _sha256_json(row),
|
|
}
|
|
)
|
|
golden_cases.sort(key=lambda item: item["sample_id"])
|
|
if len({item["sample_id"] for item in golden_cases}) != EXPECTED_GOLDEN_COUNT:
|
|
raise PublicationBatchError("Golden inventory contains duplicate sample IDs")
|
|
if sum(item["suite"] == "contract_generation" for item in golden_cases) != EXPECTED_GOLDEN_GENERATION_COUNT:
|
|
raise PublicationBatchError("Golden generation inventory must contain exactly 23 cases")
|
|
if sum(item["suite"] == "clause_recommendation" for item in golden_cases) != EXPECTED_GOLDEN_CLAUSE_COUNT:
|
|
raise PublicationBatchError("Golden clause inventory must contain exactly 8 cases")
|
|
|
|
return {
|
|
"assets": assets,
|
|
"prompt_modules": modules,
|
|
"runtime_prompt_dependencies": _load_executable_prompt_dependencies(repo),
|
|
"default_prompt_boundary": {
|
|
"included_in_publication_batch": False,
|
|
"count": len(default_inventory),
|
|
"source_path": DEFAULT_PROMPT_PATH,
|
|
"source_raw_sha256": default_raw_sha,
|
|
"inventory": default_inventory,
|
|
"inventory_sha256": _sha256_json(default_inventory),
|
|
"boundary_note": (
|
|
"These 46 default Prompt assets are an existing, separate inventory; "
|
|
"they are not the 14 RC-06 authored modules."
|
|
),
|
|
},
|
|
"golden_cases": golden_cases,
|
|
"source_paths": (
|
|
list(frozen_source_paths)
|
|
if frozen_source_paths is not None
|
|
else _inventory_source_paths(repo)
|
|
),
|
|
}
|
|
|
|
|
|
def validate_image_identities(payload: Any, commit: str, tag: str) -> dict[str, Any]:
|
|
if not isinstance(payload, dict):
|
|
raise PublicationBatchError("image identities must be an object")
|
|
if payload.get("schema_version") == IMAGE_SCHEMA_VERSION:
|
|
document = _require_keys(payload, "image identities", ("schema_version", "commit", "tag", "images"))
|
|
document_commit = document["commit"]
|
|
document_tag = document["tag"]
|
|
images = document["images"]
|
|
source_schema = IMAGE_SCHEMA_VERSION
|
|
elif payload.get("schema_version") == "yuqei.installable-release-manifest.v1":
|
|
candidate = payload.get("candidate")
|
|
if not isinstance(candidate, dict):
|
|
raise PublicationBatchError("installable release candidate identity is missing")
|
|
document_commit = candidate.get("commit")
|
|
document_tag = candidate.get("tag")
|
|
images = payload.get("images")
|
|
source_schema = "yuqei.installable-release-manifest.v1"
|
|
else:
|
|
raise PublicationBatchError("unsupported image identities schema")
|
|
if document_commit != commit or document_tag != tag:
|
|
raise PublicationBatchError("image identities candidate does not match commit/tag")
|
|
if not isinstance(images, list) or len(images) != len(REQUIRED_IMAGE_ROLES):
|
|
raise PublicationBatchError("image identities must contain the four application images")
|
|
normalized = []
|
|
roles: set[str] = set()
|
|
manifest_digests: set[str] = set()
|
|
for number, item in enumerate(images):
|
|
if not isinstance(item, dict):
|
|
raise PublicationBatchError(f"image {number} is invalid")
|
|
if source_schema == IMAGE_SCHEMA_VERSION:
|
|
row = _require_keys(
|
|
item,
|
|
f"image {number}",
|
|
("role", "repository", "manifest_digest", "config_digest"),
|
|
)
|
|
repository_value = row["repository"]
|
|
else:
|
|
row = item
|
|
repository_value = row.get("image_name") or row.get("registry_reference")
|
|
role = _require_string(row["role"], f"image {number} role", max_length=64)
|
|
repository = _require_string(repository_value, f"image {role} repository", max_length=256)
|
|
manifest_digest = row["manifest_digest"]
|
|
config_digest = row["config_digest"]
|
|
if not isinstance(manifest_digest, str) or not manifest_digest.startswith("sha256:"):
|
|
raise PublicationBatchError(f"image {role} manifest digest is invalid")
|
|
if not isinstance(config_digest, str) or not config_digest.startswith("sha256:"):
|
|
raise PublicationBatchError(f"image {role} config digest is invalid")
|
|
_require_sha(manifest_digest.removeprefix("sha256:"), f"image {role} manifest digest")
|
|
_require_sha(config_digest.removeprefix("sha256:"), f"image {role} config digest")
|
|
if role in roles or manifest_digest in manifest_digests:
|
|
raise PublicationBatchError("duplicate image role or manifest digest")
|
|
roles.add(role)
|
|
manifest_digests.add(manifest_digest)
|
|
normalized.append(
|
|
{
|
|
"role": role,
|
|
"repository": repository,
|
|
"manifest_digest": manifest_digest,
|
|
"config_digest": config_digest,
|
|
}
|
|
)
|
|
if roles != set(REQUIRED_IMAGE_ROLES):
|
|
raise PublicationBatchError(f"image roles mismatch: {sorted(roles)}")
|
|
normalized.sort(key=lambda item: REQUIRED_IMAGE_ROLES.index(item["role"]))
|
|
return {
|
|
"images": normalized,
|
|
"identity_set_sha256": _sha256_json(normalized),
|
|
"source_schema": source_schema,
|
|
}
|
|
|
|
|
|
def _validate_rc05_candidate_binding(value: Any) -> dict[str, Any]:
|
|
candidate = _require_keys(
|
|
value,
|
|
"RC-05 candidate binding",
|
|
("schema_version", "commit", "tag", "source_manifest", "image_identities"),
|
|
)
|
|
if (
|
|
candidate["schema_version"] != RC05_CANDIDATE_SCHEMA_VERSION
|
|
or not COMMIT_RE.fullmatch(str(candidate["commit"]))
|
|
or not TAG_RE.fullmatch(str(candidate["tag"]))
|
|
or ".." in candidate["tag"]
|
|
or "//" in candidate["tag"]
|
|
):
|
|
raise PublicationBatchError("RC-05 candidate binding identity is invalid")
|
|
source = _require_keys(
|
|
candidate["source_manifest"],
|
|
"RC-05 candidate source manifest binding",
|
|
("schema_version", "raw_sha256", "payload_sha256"),
|
|
)
|
|
if source["schema_version"] != "yuqei.release-source-manifest.v1":
|
|
raise PublicationBatchError("RC-05 candidate source manifest schema is invalid")
|
|
_require_sha(source["raw_sha256"], "RC-05 candidate source manifest raw SHA-256")
|
|
_require_sha(
|
|
source["payload_sha256"], "RC-05 candidate source manifest payload SHA-256"
|
|
)
|
|
image_binding = _require_keys(
|
|
candidate["image_identities"],
|
|
"RC-05 candidate image binding",
|
|
("schema_version", "raw_sha256", "identity_set_sha256", "images"),
|
|
)
|
|
if image_binding["schema_version"] not in {
|
|
IMAGE_SCHEMA_VERSION,
|
|
"yuqei.installable-release-manifest.v1",
|
|
}:
|
|
raise PublicationBatchError("RC-05 candidate image binding schema is invalid")
|
|
_require_sha(image_binding["raw_sha256"], "RC-05 candidate image raw SHA-256")
|
|
identity_set_sha = _require_sha(
|
|
image_binding["identity_set_sha256"], "RC-05 candidate image set SHA-256"
|
|
)
|
|
images = image_binding["images"]
|
|
if not isinstance(images, list) or len(images) != len(REQUIRED_IMAGE_ROLES):
|
|
raise PublicationBatchError("RC-05 candidate must bind all four release images")
|
|
normalized: list[dict[str, Any]] = []
|
|
roles: set[str] = set()
|
|
manifest_digests: set[str] = set()
|
|
for index, value_row in enumerate(images):
|
|
row = _require_keys(
|
|
value_row,
|
|
f"RC-05 candidate image {index}",
|
|
("role", "repository", "manifest_digest", "config_digest"),
|
|
)
|
|
role = row["role"]
|
|
repository = _require_string(
|
|
row["repository"], f"RC-05 candidate image {index} repository", max_length=256
|
|
)
|
|
if role not in REQUIRED_IMAGE_ROLES or role in roles:
|
|
raise PublicationBatchError("RC-05 candidate image role inventory is invalid")
|
|
manifest_digest = row["manifest_digest"]
|
|
config_digest = row["config_digest"]
|
|
if (
|
|
not isinstance(manifest_digest, str)
|
|
or not manifest_digest.startswith("sha256:")
|
|
or not isinstance(config_digest, str)
|
|
or not config_digest.startswith("sha256:")
|
|
):
|
|
raise PublicationBatchError("RC-05 candidate image digest is invalid")
|
|
_require_sha(
|
|
manifest_digest.removeprefix("sha256:"),
|
|
f"RC-05 candidate image {role} manifest digest",
|
|
)
|
|
_require_sha(
|
|
config_digest.removeprefix("sha256:"),
|
|
f"RC-05 candidate image {role} config digest",
|
|
)
|
|
if manifest_digest in manifest_digests:
|
|
raise PublicationBatchError("RC-05 candidate image manifest digests must be unique")
|
|
roles.add(role)
|
|
manifest_digests.add(manifest_digest)
|
|
normalized.append(
|
|
{
|
|
"role": role,
|
|
"repository": repository,
|
|
"manifest_digest": manifest_digest,
|
|
"config_digest": config_digest,
|
|
}
|
|
)
|
|
normalized.sort(key=lambda item: REQUIRED_IMAGE_ROLES.index(item["role"]))
|
|
if images != normalized or roles != set(REQUIRED_IMAGE_ROLES):
|
|
raise PublicationBatchError("RC-05 candidate images are not canonically ordered")
|
|
if _sha256_json(normalized) != identity_set_sha:
|
|
raise PublicationBatchError("RC-05 candidate image identity-set digest drift")
|
|
return dict(candidate)
|
|
|
|
|
|
def _validate_rc05_release_controls(
|
|
value: Any, *, cases: list[dict[str, Any]]
|
|
) -> dict[str, Any]:
|
|
controls = _require_keys(
|
|
value,
|
|
"RC-05 release controls",
|
|
(
|
|
"schema_version", "canary_case_ids", "failure_classes",
|
|
"expected_operations",
|
|
),
|
|
)
|
|
canaries = controls["canary_case_ids"]
|
|
failure_classes = controls["failure_classes"]
|
|
expected_operations = controls["expected_operations"]
|
|
case_ids = [case["case_id"] for case in cases]
|
|
expected_operation_map = {
|
|
case["case_id"]: list(case["operations"])
|
|
for case in cases
|
|
}
|
|
if (
|
|
controls["schema_version"] != RC05_RELEASE_CONTROLS_SCHEMA_VERSION
|
|
or canaries != list(EXPECTED_RC05_CANARY_CASE_IDS)
|
|
or not isinstance(failure_classes, dict)
|
|
or set(failure_classes) != set(case_ids)
|
|
or not isinstance(expected_operations, dict)
|
|
or expected_operations != expected_operation_map
|
|
):
|
|
raise PublicationBatchError("RC-05 release controls identity or coverage drift")
|
|
for case in cases:
|
|
failure_class = _require_string(
|
|
case.get("failure_class"),
|
|
f"RC-05 {case['case_id']} failure class",
|
|
max_length=128,
|
|
)
|
|
if (
|
|
not STABLE_ID_RE.fullmatch(failure_class)
|
|
or failure_classes[case["case_id"]] != failure_class
|
|
):
|
|
raise PublicationBatchError("RC-05 failure-class circuit binding drift")
|
|
return dict(controls)
|
|
|
|
|
|
def validate_rc05_plan(
|
|
payload: Any, *, expected_candidate: Mapping[str, Any] | None = None
|
|
) -> dict[str, Any]:
|
|
plan = _require_keys(
|
|
payload,
|
|
"RC-05 plan",
|
|
(
|
|
"schema_version",
|
|
"prepared_at",
|
|
"model",
|
|
"reasoning_effort",
|
|
"timeout_seconds",
|
|
"case_count",
|
|
"drafting_case_count",
|
|
"supplemental_case_count",
|
|
"operation_limits",
|
|
"total_provider_call_budget",
|
|
"candidate",
|
|
"release_controls",
|
|
"source_pins",
|
|
"cases",
|
|
"limitations",
|
|
),
|
|
)
|
|
if plan["schema_version"] != RC05_PLAN_SCHEMA_VERSION:
|
|
raise PublicationBatchError(
|
|
"legacy or unsupported RC-05 plan schema is read-only diagnostic material"
|
|
)
|
|
_parse_time(plan["prepared_at"], "RC-05 plan prepared_at")
|
|
candidate_binding = _validate_rc05_candidate_binding(plan["candidate"])
|
|
if expected_candidate is not None and candidate_binding != dict(expected_candidate):
|
|
raise PublicationBatchError("RC-05 plan candidate binding differs from the frozen candidate")
|
|
payload = plan
|
|
if payload.get("model") != "gpt-5.6-sol" or payload.get("reasoning_effort") != "medium":
|
|
raise PublicationBatchError("RC-05 plan model identity is not the frozen release configuration")
|
|
if payload.get("timeout_seconds") != 300 or payload.get("case_count") != 144:
|
|
raise PublicationBatchError("RC-05 plan timeout or case count drift")
|
|
if (
|
|
payload.get("drafting_case_count") != 120
|
|
or payload.get("supplemental_case_count") != 24
|
|
or payload.get("total_provider_call_budget") != 280
|
|
or payload.get("operation_limits") != EXPECTED_RC05_OPERATION_LIMITS
|
|
):
|
|
raise PublicationBatchError("RC-05 plan inventory, operation limits, or budget drift")
|
|
cases = payload.get("cases")
|
|
if not isinstance(cases, list) or len(cases) != 144:
|
|
raise PublicationBatchError("RC-05 plan case inventory is incomplete")
|
|
case_ids: list[str] = []
|
|
operation_counts = {operation: 0 for operation in EXPECTED_RC05_OPERATION_LIMITS}
|
|
total_case_budget = 0
|
|
drafting_count = 0
|
|
supplemental_count = 0
|
|
for number, case in enumerate(cases):
|
|
if not isinstance(case, dict):
|
|
raise PublicationBatchError(f"RC-05 plan case {number} must be an object")
|
|
case_id = case.get("case_id")
|
|
if not isinstance(case_id, str) or not STABLE_ID_RE.fullmatch(case_id):
|
|
raise PublicationBatchError("RC-05 plan contains an invalid case identity")
|
|
case_ids.append(case_id)
|
|
operations = case.get("operations")
|
|
budget = case.get("provider_call_budget")
|
|
if (
|
|
not isinstance(operations, list)
|
|
or not operations
|
|
or len(operations) != len(set(operations))
|
|
or any(operation not in EXPECTED_RC05_OPERATION_LIMITS for operation in operations)
|
|
):
|
|
raise PublicationBatchError(f"RC-05 case operations are invalid: {case_id}")
|
|
if not isinstance(budget, int) or isinstance(budget, bool) or budget != len(operations):
|
|
raise PublicationBatchError(f"RC-05 case provider budget is invalid: {case_id}")
|
|
for operation in operations:
|
|
operation_counts[operation] += 1
|
|
total_case_budget += budget
|
|
|
|
kind = case.get("kind")
|
|
if kind == "drafting":
|
|
drafting_count += 1
|
|
required = {
|
|
"source_case_id", "mode", "template_id", "document_kind", "title",
|
|
"instructions", "values", "values_sha256", "candidate_policy",
|
|
"expectation_profile", "template_content_sha256",
|
|
"template_input_schema_sha256", "draft_request_projection", "auto_repair",
|
|
}
|
|
if not required.issubset(case):
|
|
raise PublicationBatchError(f"RC-05 drafting case is incomplete: {case_id}")
|
|
if (
|
|
not isinstance(case["source_case_id"], str)
|
|
or not STABLE_ID_RE.fullmatch(case["source_case_id"])
|
|
or not isinstance(case["values"], dict)
|
|
):
|
|
raise PublicationBatchError(f"RC-05 drafting case identity or values drift: {case_id}")
|
|
if _sha256_json(case["values"]) != _require_sha(
|
|
case["values_sha256"], f"RC-05 {case_id} values SHA-256"
|
|
):
|
|
raise PublicationBatchError(f"RC-05 drafting values hash drift: {case_id}")
|
|
for field in ("template_content_sha256", "template_input_schema_sha256"):
|
|
if case[field] is not None:
|
|
_require_sha(case[field], f"RC-05 {case_id} {field}")
|
|
policy = case["candidate_policy"]
|
|
if not isinstance(policy, dict):
|
|
raise PublicationBatchError(f"RC-05 drafting policy is invalid: {case_id}")
|
|
expected_semantic = "semantic_review" in operations
|
|
expected_repair = {"repair_draft", "repair_review"}.issubset(operations)
|
|
if (
|
|
policy.get("semantic_review") is not expected_semantic
|
|
or policy.get("auto_repair") is not expected_repair
|
|
or case["auto_repair"] is not expected_repair
|
|
):
|
|
raise PublicationBatchError(f"RC-05 drafting operation policy drift: {case_id}")
|
|
elif kind in {"specialist_review", "intake"}:
|
|
supplemental_count += 1
|
|
if (
|
|
operations != [kind]
|
|
or case.get("operation") != kind
|
|
or case.get("mode") != kind
|
|
or case.get("source_case_id") != case_id
|
|
or not isinstance(case.get("request"), dict)
|
|
or _sha256_json(case["request"]) != _require_sha(
|
|
case.get("request_sha256"), f"RC-05 {case_id} request SHA-256"
|
|
)
|
|
):
|
|
raise PublicationBatchError(f"RC-05 supplemental case drift: {case_id}")
|
|
else:
|
|
raise PublicationBatchError(f"RC-05 case kind is invalid: {case_id}")
|
|
if len(set(case_ids)) != 144:
|
|
raise PublicationBatchError("RC-05 plan contains duplicate case identities")
|
|
if (
|
|
drafting_count != 120
|
|
or supplemental_count != 24
|
|
or operation_counts != EXPECTED_RC05_OPERATION_LIMITS
|
|
or total_case_budget != 280
|
|
):
|
|
raise PublicationBatchError("RC-05 case operations or provider budgets do not reconcile")
|
|
release_controls = _validate_rc05_release_controls(
|
|
payload["release_controls"], cases=cases
|
|
)
|
|
source_pins = payload.get("source_pins")
|
|
if not isinstance(source_pins, dict) or not source_pins:
|
|
raise PublicationBatchError("RC-05 plan source pins are missing")
|
|
for relative, digest in source_pins.items():
|
|
if (
|
|
not isinstance(relative, str)
|
|
or not relative
|
|
or relative.startswith(("/", "\\"))
|
|
or ".." in PurePath(relative).parts
|
|
or "\\" in relative
|
|
):
|
|
raise PublicationBatchError("RC-05 plan contains an unsafe source pin path")
|
|
_require_sha(digest, f"RC-05 source pin {relative}")
|
|
return {
|
|
"schema_version": payload["schema_version"],
|
|
"model": payload["model"],
|
|
"reasoning_effort": payload["reasoning_effort"],
|
|
"timeout_seconds": payload["timeout_seconds"],
|
|
"case_count": payload["case_count"],
|
|
"drafting_case_count": payload["drafting_case_count"],
|
|
"supplemental_case_count": payload["supplemental_case_count"],
|
|
"operation_limits": payload["operation_limits"],
|
|
"total_provider_call_budget": payload["total_provider_call_budget"],
|
|
"case_inventory_sha256": _sha256_json(cases),
|
|
"source_pins_sha256": _sha256_json(source_pins),
|
|
"candidate": candidate_binding,
|
|
"release_controls": release_controls,
|
|
}
|
|
|
|
|
|
def verify_rc05_source_pins(
|
|
repo: Path,
|
|
commit: str,
|
|
source_manifest: Any,
|
|
plan: Mapping[str, Any],
|
|
) -> None:
|
|
index, _ = _source_manifest_index(source_manifest, commit)
|
|
pins = plan["source_pins"]
|
|
for relative, digest in pins.items():
|
|
if index.get(relative) != digest:
|
|
raise PublicationBatchError(
|
|
f"RC-05 source pin differs from candidate source manifest: {relative}"
|
|
)
|
|
committed = bytes(_run_git(repo, "show", f"{commit}:{relative}", binary=True))
|
|
if _sha256_bytes(committed) != digest:
|
|
raise PublicationBatchError(
|
|
f"RC-05 source pin differs from candidate commit: {relative}"
|
|
)
|
|
|
|
|
|
def validate_rc05_plan_identity(value: Any) -> dict[str, Any]:
|
|
identity = _require_keys(
|
|
value,
|
|
"RC-05 plan identity",
|
|
(
|
|
"schema_version",
|
|
"model",
|
|
"reasoning_effort",
|
|
"timeout_seconds",
|
|
"case_count",
|
|
"drafting_case_count",
|
|
"supplemental_case_count",
|
|
"operation_limits",
|
|
"total_provider_call_budget",
|
|
"case_inventory_sha256",
|
|
"source_pins_sha256",
|
|
"candidate",
|
|
"release_controls",
|
|
),
|
|
)
|
|
if (
|
|
identity["schema_version"] != RC05_PLAN_SCHEMA_VERSION
|
|
or identity["model"] != "gpt-5.6-sol"
|
|
or identity["reasoning_effort"] != "medium"
|
|
or identity["timeout_seconds"] != 300
|
|
or identity["case_count"] != 144
|
|
or identity["drafting_case_count"] != 120
|
|
or identity["supplemental_case_count"] != 24
|
|
or identity["operation_limits"] != EXPECTED_RC05_OPERATION_LIMITS
|
|
or identity["total_provider_call_budget"] != 280
|
|
):
|
|
raise PublicationBatchError("RC-05 plan identity drift")
|
|
_require_sha(identity["case_inventory_sha256"], "RC-05 case inventory SHA-256")
|
|
_require_sha(identity["source_pins_sha256"], "RC-05 source pins SHA-256")
|
|
_validate_rc05_candidate_binding(identity["candidate"])
|
|
controls = _require_keys(
|
|
identity["release_controls"],
|
|
"RC-05 plan identity release controls",
|
|
(
|
|
"schema_version", "canary_case_ids", "failure_classes",
|
|
"expected_operations",
|
|
),
|
|
)
|
|
failure_classes = controls["failure_classes"]
|
|
expected_operations = controls["expected_operations"]
|
|
operation_counts = {
|
|
operation: 0 for operation in EXPECTED_RC05_OPERATION_LIMITS
|
|
}
|
|
expected_operations_valid = (
|
|
isinstance(expected_operations, dict)
|
|
and isinstance(failure_classes, dict)
|
|
and set(expected_operations) == set(failure_classes)
|
|
)
|
|
if expected_operations_valid:
|
|
for case_id, operations in expected_operations.items():
|
|
if (
|
|
not isinstance(case_id, str)
|
|
or not STABLE_ID_RE.fullmatch(case_id)
|
|
or not isinstance(operations, list)
|
|
or not operations
|
|
or len(operations) != len(set(operations))
|
|
or any(
|
|
operation not in EXPECTED_RC05_OPERATION_LIMITS
|
|
for operation in operations
|
|
)
|
|
):
|
|
expected_operations_valid = False
|
|
break
|
|
for operation in operations:
|
|
operation_counts[operation] += 1
|
|
if (
|
|
controls["schema_version"] != RC05_RELEASE_CONTROLS_SCHEMA_VERSION
|
|
or controls["canary_case_ids"] != list(EXPECTED_RC05_CANARY_CASE_IDS)
|
|
or not isinstance(failure_classes, dict)
|
|
or len(failure_classes) != 144
|
|
or any(
|
|
not isinstance(key, str)
|
|
or not STABLE_ID_RE.fullmatch(key)
|
|
or not isinstance(value, str)
|
|
or not STABLE_ID_RE.fullmatch(value)
|
|
for key, value in failure_classes.items()
|
|
)
|
|
or not expected_operations_valid
|
|
or operation_counts != EXPECTED_RC05_OPERATION_LIMITS
|
|
):
|
|
raise PublicationBatchError("RC-05 plan identity release controls drift")
|
|
return dict(identity)
|
|
|
|
|
|
def _validate_rc05_case_operation_coverage(
|
|
value: Any,
|
|
*,
|
|
plan_cases: Mapping[str, Mapping[str, Any]],
|
|
result_cases: Mapping[str, Mapping[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
if not isinstance(value, list) or len(value) != len(plan_cases):
|
|
raise PublicationBatchError(
|
|
"RC-05 case-operation coverage must contain all 144 cases"
|
|
)
|
|
repair_pair = {"repair_draft", "repair_review"}
|
|
coverage: dict[str, dict[str, Any]] = {}
|
|
for number, raw in enumerate(value):
|
|
row = _require_keys(
|
|
raw,
|
|
f"RC-05 case-operation coverage {number}",
|
|
(
|
|
"case_id", "declared_operations", "mandatory_operations",
|
|
"actual_operations", "provider_call_count", "passed",
|
|
),
|
|
)
|
|
case_id = row["case_id"]
|
|
if not isinstance(case_id, str) or case_id not in plan_cases or case_id in coverage:
|
|
raise PublicationBatchError("RC-05 case-operation coverage identity drift")
|
|
declared = row["declared_operations"]
|
|
mandatory = row["mandatory_operations"]
|
|
actual = row["actual_operations"]
|
|
if not all(isinstance(items, list) for items in (declared, mandatory, actual)):
|
|
raise PublicationBatchError(
|
|
f"RC-05 case-operation coverage lists are invalid: {case_id}"
|
|
)
|
|
if any(
|
|
not isinstance(operation, str)
|
|
for items in (declared, mandatory, actual)
|
|
for operation in items
|
|
):
|
|
raise PublicationBatchError(
|
|
f"RC-05 case-operation coverage operation is invalid: {case_id}"
|
|
)
|
|
plan_operations = plan_cases[case_id]["operations"]
|
|
declared_set = set(declared)
|
|
actual_set = set(actual)
|
|
expected_mandatory = set(plan_operations) - repair_pair
|
|
missing = expected_mandatory - actual_set
|
|
extra = actual_set - set(plan_operations)
|
|
repair_actual = actual_set & repair_pair
|
|
expected_calls = result_cases[case_id]["provider_call_count"]
|
|
if (
|
|
declared != plan_operations
|
|
or len(declared) != len(declared_set)
|
|
or mandatory != sorted(expected_mandatory)
|
|
or actual != sorted(actual)
|
|
or len(actual) != len(actual_set)
|
|
or missing
|
|
or extra
|
|
or repair_actual not in (set(), repair_pair)
|
|
or row["provider_call_count"] != expected_calls
|
|
or len(actual) != expected_calls
|
|
or row["passed"] is not True
|
|
):
|
|
raise PublicationBatchError(
|
|
f"RC-05 case-operation coverage is not release-green: {case_id}"
|
|
)
|
|
coverage[case_id] = dict(row)
|
|
if set(coverage) != set(plan_cases):
|
|
raise PublicationBatchError("RC-05 case-operation coverage inventory is incomplete")
|
|
return [coverage[case_id] for case_id in sorted(coverage)]
|
|
|
|
|
|
def validate_rc05_terminal_evidence(
|
|
plan: Mapping[str, Any],
|
|
plan_raw_sha256: str,
|
|
summary: Any,
|
|
summary_raw_sha256: str,
|
|
ledger_identity: Any,
|
|
ledger_identity_raw_sha256: str,
|
|
campaign_identity: Any,
|
|
campaign_identity_raw_sha256: str,
|
|
) -> dict[str, Any]:
|
|
"""Validate a complete real-provider RC-05 terminal evidence chain.
|
|
|
|
This is an integrity and cross-document consistency gate. It deliberately
|
|
does not turn RC-06 into a publication command or claim authentication.
|
|
"""
|
|
plan_sha = _require_sha(plan_raw_sha256, "RC-05 plan raw SHA-256")
|
|
for digest, label in (
|
|
(summary_raw_sha256, "RC-05 summary raw SHA-256"),
|
|
(ledger_identity_raw_sha256, "RC-05 ledger identity raw SHA-256"),
|
|
(campaign_identity_raw_sha256, "RC-05 campaign identity raw SHA-256"),
|
|
):
|
|
_require_sha(digest, label)
|
|
|
|
ledger = _require_keys(
|
|
ledger_identity,
|
|
"RC-05 ledger identity",
|
|
(
|
|
"schema_version", "plan_sha256", "operation_limits", "case_limits",
|
|
"source_pins", "real_provider", "candidate_binding", "release_controls", "authority",
|
|
"integrity_not_authentication", "historical_usage",
|
|
),
|
|
)
|
|
if (
|
|
ledger["schema_version"] != RC05_LEDGER_SCHEMA_VERSION
|
|
or ledger["plan_sha256"] != plan_sha
|
|
or ledger["operation_limits"] != plan["operation_limits"]
|
|
or ledger["source_pins"] != dict(sorted(plan["source_pins"].items()))
|
|
or ledger["candidate_binding"] != plan["candidate"]
|
|
or ledger["release_controls"] != plan["release_controls"]
|
|
or ledger["real_provider"] is not True
|
|
or ledger["integrity_not_authentication"] is not True
|
|
):
|
|
raise PublicationBatchError("RC-05 ledger identity does not match the frozen real plan")
|
|
expected_case_limits = {
|
|
case["case_id"]: case["provider_call_budget"] for case in sorted(
|
|
plan["cases"], key=lambda item: item["case_id"]
|
|
)
|
|
}
|
|
if ledger["case_limits"] != expected_case_limits:
|
|
raise PublicationBatchError("RC-05 ledger case budgets do not match the plan")
|
|
authority = _require_keys(
|
|
ledger["authority"],
|
|
"RC-05 ledger authority",
|
|
("mode", "registry_root", "run_key", "bound_output_dir"),
|
|
)
|
|
if authority["mode"] != "global_registry":
|
|
raise PublicationBatchError("RC-05 real ledger must use the global campaign registry")
|
|
for key in ("registry_root", "bound_output_dir"):
|
|
_require_string(authority[key], f"RC-05 ledger authority {key}", max_length=4096)
|
|
_require_sha(authority["run_key"], "RC-05 ledger run key")
|
|
ledger_identity_sha256 = _sha256_json(ledger)
|
|
|
|
historical = ledger["historical_usage"]
|
|
if not isinstance(historical, dict):
|
|
raise PublicationBatchError("RC-05 ledger historical usage is missing")
|
|
historical_count = historical.get("provider_call_count")
|
|
repair_count = historical.get("repair_budget_charged")
|
|
if (
|
|
not isinstance(historical_count, int)
|
|
or isinstance(historical_count, bool)
|
|
or historical_count < 0
|
|
or historical_count > 280
|
|
or not isinstance(repair_count, int)
|
|
or isinstance(repair_count, bool)
|
|
or repair_count < 0
|
|
or repair_count > 16
|
|
or repair_count > historical_count
|
|
):
|
|
raise PublicationBatchError("RC-05 historical call counts are invalid")
|
|
historical_receipt_sha = _require_sha(
|
|
historical.get("receipt_sha256"), "RC-05 historical receipt SHA-256"
|
|
)
|
|
historical_attempts_sha = _require_sha(
|
|
historical.get("attempts_sha256"), "RC-05 historical attempts SHA-256"
|
|
)
|
|
historical_attempts = historical.get("attempts")
|
|
if (
|
|
historical_count != 4
|
|
or not isinstance(historical_attempts, list)
|
|
or len(historical_attempts) != historical_count
|
|
or _sha256_json(historical_attempts) != historical_attempts_sha
|
|
):
|
|
raise PublicationBatchError("RC-05 historical attempt inventory or digest drift")
|
|
attempt_index: dict[str, dict[str, Any]] = {}
|
|
for attempt in historical_attempts:
|
|
if (
|
|
not isinstance(attempt, dict)
|
|
or set(attempt) != RC05_HISTORICAL_ATTEMPT_KEYS
|
|
or not STABLE_ID_RE.fullmatch(str(attempt.get("attempt_id", "")))
|
|
):
|
|
raise PublicationBatchError("RC-05 historical attempt identity is invalid")
|
|
attempt_id = attempt["attempt_id"]
|
|
if attempt_id in attempt_index:
|
|
raise PublicationBatchError("RC-05 historical attempt identities must be unique")
|
|
disposition = attempt.get("disposition")
|
|
if (
|
|
not isinstance(disposition, dict)
|
|
or set(disposition) != {
|
|
"resolution_status", "release_eligible", "reason",
|
|
"replacement_case_id", "replacement_operation",
|
|
}
|
|
or not isinstance(disposition.get("release_eligible"), bool)
|
|
):
|
|
raise PublicationBatchError("RC-05 historical attempt disposition is invalid")
|
|
for field in (
|
|
"plan_sha256", "request_sha256", "response_sha256", "identity_file_sha256",
|
|
"ledger_identity_sha256", "claim_sha256", "finish_sha256", "before_sha256",
|
|
"after_sha256",
|
|
):
|
|
_require_sha(attempt[field], f"RC-05 historical {field}")
|
|
if (
|
|
attempt["operation"] not in EXPECTED_RC05_OPERATION_LIMITS
|
|
or attempt["provider_status"] not in {"succeeded", "failed", "cancelled"}
|
|
or attempt["business_status"] not in {
|
|
"succeeded", "failed", "cancelled", "awaiting_human", "resolved",
|
|
}
|
|
or attempt["provider"] != "codex-cli-development"
|
|
or attempt["model"] != "gpt-5.6-sol"
|
|
or not isinstance(attempt["timed_out"], bool)
|
|
):
|
|
raise PublicationBatchError("RC-05 historical provider outcome drift")
|
|
_require_nonnegative_number(
|
|
attempt["elapsed_seconds"], "RC-05 historical elapsed_seconds"
|
|
)
|
|
if disposition["release_eligible"]:
|
|
if (
|
|
disposition.get("resolution_status") != "resolved"
|
|
or disposition.get("replacement_case_id") is not None
|
|
or disposition.get("replacement_operation") is not None
|
|
or not _historical_attempt_can_be_release_eligible(attempt)
|
|
):
|
|
raise PublicationBatchError("RC-05 resolved historical disposition drift")
|
|
elif (
|
|
disposition.get("resolution_status") != "requires_replacement"
|
|
or disposition.get("replacement_case_id") not in {
|
|
case["case_id"] for case in plan["cases"]
|
|
}
|
|
or disposition.get("replacement_operation") not in EXPECTED_RC05_OPERATION_LIMITS
|
|
):
|
|
raise PublicationBatchError("RC-05 replacement disposition is not bound to the plan")
|
|
attempt_index[attempt_id] = attempt
|
|
|
|
campaign = _require_keys(
|
|
campaign_identity,
|
|
"RC-05 campaign identity",
|
|
(
|
|
"schema_version", "campaign_id", "historical_receipt_sha256",
|
|
"historical_attempts_sha256", "historical_provider_call_count",
|
|
"historical_repair_budget_charged", "provider_call_limit",
|
|
"repair_call_limit",
|
|
),
|
|
)
|
|
if (
|
|
campaign["schema_version"] != RC05_CAMPAIGN_SCHEMA_VERSION
|
|
or campaign["campaign_id"] != "rc05-real-usage-v2"
|
|
or campaign["historical_receipt_sha256"] != historical_receipt_sha
|
|
or campaign["historical_attempts_sha256"] != historical_attempts_sha
|
|
or campaign["historical_provider_call_count"] != historical_count
|
|
or campaign["historical_repair_budget_charged"] != repair_count
|
|
or campaign["provider_call_limit"] != 280
|
|
or campaign["repair_call_limit"] != 16
|
|
):
|
|
raise PublicationBatchError("RC-05 campaign identity does not match ledger history")
|
|
campaign_identity_sha256 = _sha256_json(campaign)
|
|
|
|
run = _require_keys(
|
|
summary,
|
|
"RC-05 terminal summary",
|
|
(
|
|
"schema_version", "plan_raw_sha256", "real_provider", "execution_mode",
|
|
"case_count", "drafting_case_count", "asset_count", "supplemental_case_count",
|
|
"cases", "complete", "passed", "matrix_execution_passed",
|
|
"release_quality_gate_passed", "automated_model_quality_release_passed",
|
|
"human_legal_approval_in_scope", "current_run_passed",
|
|
"current_selection_passed", "historical_release_eligible",
|
|
"historical_remediation", "overall_release_passed", "selected_release_quality_passed",
|
|
"selected_passed", "provider_call_count", "provider_call_count_scope",
|
|
"current_plan_provider_call_count", "campaign_new_provider_call_count",
|
|
"ledger", "provider_call_count_including_historical", "historical_usage",
|
|
"operation_statistics", "historical_by_operation", "combined_call_counts",
|
|
"case_operation_coverage", "case_operation_coverage_passed",
|
|
"selected_operation_coverage_passed",
|
|
"operation_statistics_complete", "operation_evidence_errors",
|
|
"case_elapsed_seconds", "live_database_writes", "live_service_calls",
|
|
"member_release_acceptance_completed", "not_legal_approval",
|
|
),
|
|
)
|
|
required_green = (
|
|
"complete", "passed", "matrix_execution_passed", "release_quality_gate_passed",
|
|
"automated_model_quality_release_passed",
|
|
"current_run_passed", "current_selection_passed", "historical_release_eligible",
|
|
"overall_release_passed", "selected_release_quality_passed", "selected_passed",
|
|
"case_operation_coverage_passed", "selected_operation_coverage_passed",
|
|
"operation_statistics_complete",
|
|
)
|
|
if (
|
|
run["schema_version"] != RC05_SUMMARY_SCHEMA_VERSION
|
|
or run["plan_raw_sha256"] != plan_sha
|
|
or run["real_provider"] is not True
|
|
or run["execution_mode"] != "real"
|
|
or any(run[field] is not True for field in required_green)
|
|
or run["case_count"] != 144
|
|
or run["drafting_case_count"] != 120
|
|
or run["asset_count"] != 60
|
|
or run["supplemental_case_count"] != 24
|
|
or run["live_database_writes"] != 0
|
|
or run["live_service_calls"] != 0
|
|
or run["operation_evidence_errors"] != []
|
|
or run["human_legal_approval_in_scope"] is not False
|
|
or run["member_release_acceptance_completed"] is not False
|
|
or run["not_legal_approval"] is not True
|
|
):
|
|
raise PublicationBatchError("RC-05 terminal summary is not a complete real green run")
|
|
expected_summary_history = {
|
|
"provider_call_count": historical_count,
|
|
"repair_budget_charged": repair_count,
|
|
"attempts": historical_attempts,
|
|
"receipt_sha256": historical_receipt_sha,
|
|
}
|
|
if run["historical_usage"] != expected_summary_history:
|
|
raise PublicationBatchError("RC-05 terminal summary historical usage drift")
|
|
remediation = run["historical_remediation"]
|
|
if not isinstance(remediation, list) or len(remediation) != 4:
|
|
raise PublicationBatchError("RC-05 historical remediation must cover all four attempts")
|
|
remediation_index: dict[str, dict[str, Any]] = {}
|
|
for row in remediation:
|
|
if not isinstance(row, dict) or row.get("attempt_id") not in attempt_index:
|
|
raise PublicationBatchError("RC-05 historical remediation identity drift")
|
|
attempt_id = row["attempt_id"]
|
|
if attempt_id in remediation_index or row.get("passed") is not True:
|
|
raise PublicationBatchError("RC-05 historical remediation is duplicate or not passed")
|
|
disposition = attempt_index[attempt_id]["disposition"]
|
|
if disposition["release_eligible"]:
|
|
if (
|
|
row.get("status") != "historically_resolved"
|
|
or row.get("replacement_case_id") is not None
|
|
or row.get("replacement_operation") is not None
|
|
):
|
|
raise PublicationBatchError("RC-05 resolved remediation drift")
|
|
elif (
|
|
row.get("status") != "replacement_verified"
|
|
or row.get("replacement_case_id") != disposition["replacement_case_id"]
|
|
or row.get("replacement_operation") != disposition["replacement_operation"]
|
|
or row.get("provider_passed") is not True
|
|
or row.get("business_passed") is not True
|
|
or row.get("quality_passed") is not True
|
|
or row.get("evidence_error") is not None
|
|
):
|
|
raise PublicationBatchError("RC-05 replacement remediation is not verified")
|
|
remediation_index[attempt_id] = row
|
|
if (
|
|
set(remediation_index) != set(attempt_index)
|
|
or sum(row["status"] == "replacement_verified" for row in remediation) != 3
|
|
or sum(row["status"] == "historically_resolved" for row in remediation) != 1
|
|
):
|
|
raise PublicationBatchError("RC-05 historical remediation status counts drift")
|
|
|
|
result_rows = run["cases"]
|
|
if not isinstance(result_rows, list) or len(result_rows) != 144:
|
|
raise PublicationBatchError("RC-05 terminal summary must contain all 144 case results")
|
|
plan_cases = {case["case_id"]: case for case in plan["cases"]}
|
|
result_cases: dict[str, dict[str, Any]] = {}
|
|
for row in result_rows:
|
|
if not isinstance(row, dict) or not isinstance(row.get("case_id"), str):
|
|
raise PublicationBatchError("RC-05 terminal case result is invalid")
|
|
case_id = row["case_id"]
|
|
if case_id in result_cases or case_id not in plan_cases:
|
|
raise PublicationBatchError("RC-05 terminal case inventory drift")
|
|
calls = row.get("provider_call_count")
|
|
plan_case = plan_cases[case_id]
|
|
minimum_calls = sum(
|
|
operation not in {"repair_draft", "repair_review"}
|
|
for operation in plan_case["operations"]
|
|
)
|
|
repair_operations = {
|
|
operation for operation in plan_case["operations"]
|
|
if operation in {"repair_draft", "repair_review"}
|
|
}
|
|
allowed_calls = {minimum_calls}
|
|
if repair_operations:
|
|
if repair_operations != {"repair_draft", "repair_review"}:
|
|
raise PublicationBatchError(
|
|
f"RC-05 repair operations must be paired: {case_id}"
|
|
)
|
|
allowed_calls.add(minimum_calls + 2)
|
|
if (
|
|
row.get("status") != "completed"
|
|
or row.get("passed") is not True
|
|
or not isinstance(calls, int)
|
|
or isinstance(calls, bool)
|
|
or calls < 1
|
|
or calls > plan_case["provider_call_budget"]
|
|
or calls not in allowed_calls
|
|
or row.get("automated_quality_passed") is not True
|
|
or row.get("automated_model_quality_release_passed") is not True
|
|
or row.get("content_quality_release_passed") is not True
|
|
or row.get("human_calibration_completed") is not False
|
|
or row.get("legal_approval_completed") is not False
|
|
):
|
|
raise PublicationBatchError(f"RC-05 terminal case is not release-green: {case_id}")
|
|
_require_nonnegative_number(
|
|
row.get("elapsed_seconds"), f"RC-05 {case_id} elapsed_seconds"
|
|
)
|
|
result_cases[case_id] = row
|
|
if set(result_cases) != set(plan_cases):
|
|
raise PublicationBatchError("RC-05 terminal case inventory is incomplete")
|
|
coverage = _validate_rc05_case_operation_coverage(
|
|
run["case_operation_coverage"],
|
|
plan_cases=plan_cases,
|
|
result_cases=result_cases,
|
|
)
|
|
_validate_elapsed_statistics(
|
|
run["case_elapsed_seconds"],
|
|
"RC-05 case elapsed statistics",
|
|
expected_observed=len(result_cases),
|
|
)
|
|
|
|
snapshot = _require_keys(
|
|
run["ledger"],
|
|
"RC-05 terminal ledger snapshot",
|
|
(
|
|
"schema_version", "ledger_identity_sha256", "real_provider",
|
|
"authority", "budget", "operations", "cases", "campaign",
|
|
"attempted", "terminal", "unknown", "provider_terminal",
|
|
"release_controls",
|
|
),
|
|
)
|
|
snapshot_attempted = _require_count(
|
|
snapshot["attempted"], "RC-05 ledger attempted", maximum=280
|
|
)
|
|
snapshot_terminal = _require_count(
|
|
snapshot["terminal"], "RC-05 ledger terminal", maximum=280
|
|
)
|
|
snapshot_unknown = _require_count(
|
|
snapshot["unknown"], "RC-05 ledger unknown", maximum=280
|
|
)
|
|
if (
|
|
snapshot.get("schema_version") != RC05_LEDGER_SNAPSHOT_SCHEMA_VERSION
|
|
or snapshot.get("ledger_identity_sha256") != ledger_identity_sha256
|
|
or snapshot.get("real_provider") is not True
|
|
or snapshot.get("authority") != authority
|
|
or snapshot_unknown != 0
|
|
or snapshot_terminal != snapshot_attempted
|
|
):
|
|
raise PublicationBatchError("RC-05 terminal ledger snapshot identity or terminal counts drift")
|
|
control_state = _require_keys(
|
|
snapshot.get("release_controls"),
|
|
"RC-05 terminal release-control state",
|
|
(
|
|
"schema_version",
|
|
"phase",
|
|
"canary_case_ids",
|
|
"blocked_failure_classes",
|
|
"canary_transition_sha256",
|
|
),
|
|
)
|
|
if (
|
|
control_state["schema_version"] != RC05_CONTROL_STATE_SCHEMA_VERSION
|
|
or control_state["phase"] != "full_authorized"
|
|
or control_state["canary_case_ids"] != list(EXPECTED_RC05_CANARY_CASE_IDS)
|
|
or control_state["blocked_failure_classes"] != []
|
|
):
|
|
raise PublicationBatchError("RC-05 terminal release controls are not green")
|
|
_require_sha(
|
|
control_state["canary_transition_sha256"],
|
|
"RC-05 canary transition SHA-256",
|
|
)
|
|
case_snapshot = snapshot.get("cases")
|
|
operation_snapshot = snapshot.get("operations")
|
|
if not isinstance(case_snapshot, dict) or set(case_snapshot) != set(plan_cases):
|
|
raise PublicationBatchError("RC-05 ledger case snapshot is incomplete")
|
|
if not isinstance(operation_snapshot, dict) or set(operation_snapshot) != set(EXPECTED_RC05_OPERATION_LIMITS):
|
|
raise PublicationBatchError("RC-05 ledger operation snapshot is incomplete")
|
|
for case_id, plan_case in plan_cases.items():
|
|
counts = case_snapshot[case_id]
|
|
expected_calls = result_cases[case_id]["provider_call_count"]
|
|
if counts != {
|
|
"limit": plan_case["provider_call_budget"],
|
|
"attempted": expected_calls,
|
|
"terminal": expected_calls,
|
|
"unknown": 0,
|
|
}:
|
|
raise PublicationBatchError(f"RC-05 ledger case counts drift: {case_id}")
|
|
current_attempted = sum(row["provider_call_count"] for row in result_cases.values())
|
|
provider_terminal = _require_count(
|
|
snapshot["provider_terminal"],
|
|
"RC-05 ledger provider-terminal count",
|
|
maximum=280,
|
|
)
|
|
repair_case_calls = sum(
|
|
result_cases[case_id]["provider_call_count"]
|
|
- sum(
|
|
operation not in {"repair_draft", "repair_review"}
|
|
for operation in plan_case["operations"]
|
|
)
|
|
for case_id, plan_case in plan_cases.items()
|
|
)
|
|
if (
|
|
snapshot_attempted != current_attempted
|
|
or provider_terminal != current_attempted
|
|
or run["provider_call_count"] != current_attempted
|
|
or run["current_plan_provider_call_count"] != current_attempted
|
|
or run["provider_call_count_scope"] != "current_plan"
|
|
):
|
|
raise PublicationBatchError("RC-05 current-plan call count drift")
|
|
|
|
statistics = run["operation_statistics"]
|
|
if not isinstance(statistics, dict) or set(statistics) != set(EXPECTED_RC05_OPERATION_LIMITS):
|
|
raise PublicationBatchError("RC-05 operation statistics are incomplete")
|
|
historical_attempted = 0
|
|
combined_attempted = 0
|
|
historical_operation_counts = {operation: 0 for operation in EXPECTED_RC05_OPERATION_LIMITS}
|
|
historical_provider_outcomes = {
|
|
operation: {"succeeded": 0, "failed": 0, "cancelled": 0, "timeout": 0}
|
|
for operation in EXPECTED_RC05_OPERATION_LIMITS
|
|
}
|
|
historical_business_outcomes = {
|
|
operation: {
|
|
"succeeded": 0, "failed": 0, "cancelled": 0,
|
|
"awaiting_human": 0, "other": 0,
|
|
}
|
|
for operation in EXPECTED_RC05_OPERATION_LIMITS
|
|
}
|
|
for attempt in historical_attempts:
|
|
operation = attempt["operation"]
|
|
historical_operation_counts[operation] += 1
|
|
historical_provider_outcomes[operation][attempt["provider_status"]] += 1
|
|
if attempt["timed_out"]:
|
|
historical_provider_outcomes[operation]["timeout"] += 1
|
|
business_status = attempt["business_status"]
|
|
business_key = (
|
|
"succeeded" if business_status in {"succeeded", "resolved"}
|
|
else business_status
|
|
if business_status in historical_business_outcomes[operation]
|
|
else "other"
|
|
)
|
|
historical_business_outcomes[operation][business_key] += 1
|
|
for operation, limit in EXPECTED_RC05_OPERATION_LIMITS.items():
|
|
ledger_counts = operation_snapshot[operation]
|
|
stat = _require_keys(
|
|
statistics[operation],
|
|
f"RC-05 operation statistics {operation}",
|
|
(
|
|
"limit", "attempted", "terminal", "unknown", "succeeded",
|
|
"failed", "cancelled", "awaiting_human", "other", "timeout",
|
|
"business_outcomes", "provider_outcomes", "elapsed_seconds",
|
|
"historical", "combined",
|
|
),
|
|
)
|
|
attempted = _require_count(
|
|
stat.get("attempted"), f"RC-05 {operation} attempted", maximum=limit
|
|
)
|
|
terminal = _require_count(
|
|
stat.get("terminal"), f"RC-05 {operation} terminal", maximum=limit
|
|
)
|
|
unknown = _require_count(
|
|
stat.get("unknown"), f"RC-05 {operation} unknown", maximum=limit
|
|
)
|
|
succeeded = _require_count(
|
|
stat.get("succeeded"), f"RC-05 {operation} succeeded", maximum=limit
|
|
)
|
|
for field in ("failed", "cancelled", "awaiting_human", "other", "timeout"):
|
|
_require_count(stat.get(field), f"RC-05 {operation} {field}", maximum=limit)
|
|
for section, keys, maximum in (
|
|
(stat.get("business_outcomes"),
|
|
("succeeded", "failed", "cancelled", "awaiting_human", "other"), limit),
|
|
(stat.get("provider_outcomes"),
|
|
("succeeded", "failed", "cancelled", "timeout"), limit),
|
|
):
|
|
if not isinstance(section, dict) or set(section) != set(keys):
|
|
raise PublicationBatchError(
|
|
f"RC-05 {operation} outcome fields are incomplete"
|
|
)
|
|
for key in keys:
|
|
_require_count(
|
|
section[key], f"RC-05 {operation} outcome {key}", maximum=maximum
|
|
)
|
|
expected_minimum = 0 if operation in {"repair_draft", "repair_review"} else limit
|
|
if attempted < expected_minimum:
|
|
raise PublicationBatchError(
|
|
f"RC-05 mandatory operation count is incomplete: {operation}"
|
|
)
|
|
_validate_elapsed_statistics(
|
|
stat["elapsed_seconds"],
|
|
f"RC-05 {operation} elapsed statistics",
|
|
expected_observed=attempted,
|
|
)
|
|
if not isinstance(stat, dict) or ledger_counts != {
|
|
"limit": limit,
|
|
"attempted": attempted,
|
|
"terminal": terminal,
|
|
"unknown": unknown,
|
|
}:
|
|
raise PublicationBatchError(f"RC-05 operation ledger/statistics drift: {operation}")
|
|
if (
|
|
stat.get("limit") != limit
|
|
or terminal != attempted
|
|
or unknown != 0
|
|
or succeeded != attempted
|
|
or any(stat.get(field) != 0 for field in (
|
|
"failed", "cancelled", "awaiting_human", "other", "timeout"
|
|
))
|
|
or stat.get("business_outcomes") != {
|
|
"succeeded": attempted, "failed": 0, "cancelled": 0,
|
|
"awaiting_human": 0, "other": 0,
|
|
}
|
|
or stat.get("provider_outcomes") != {
|
|
"succeeded": attempted, "failed": 0, "cancelled": 0, "timeout": 0,
|
|
}
|
|
):
|
|
raise PublicationBatchError(f"RC-05 current operation is not release-green: {operation}")
|
|
prior = _require_keys(
|
|
stat.get("historical"),
|
|
f"RC-05 historical operation statistics {operation}",
|
|
(
|
|
"attempted", "terminal", "unknown", "provider_outcomes",
|
|
"business_outcomes", "elapsed_seconds",
|
|
),
|
|
)
|
|
combined = _require_keys(
|
|
stat.get("combined"),
|
|
f"RC-05 combined operation statistics {operation}",
|
|
(
|
|
"attempted", "terminal", "unknown", "provider_outcomes",
|
|
"business_outcomes", "elapsed_seconds",
|
|
),
|
|
)
|
|
prior_attempted = _require_count(
|
|
prior.get("attempted"), f"RC-05 historical {operation} attempted",
|
|
maximum=historical_count,
|
|
)
|
|
prior_terminal = _require_count(
|
|
prior.get("terminal"), f"RC-05 historical {operation} terminal",
|
|
maximum=historical_count,
|
|
)
|
|
prior_unknown = _require_count(
|
|
prior.get("unknown"), f"RC-05 historical {operation} unknown",
|
|
maximum=historical_count,
|
|
)
|
|
combined_attempted_value = _require_count(
|
|
combined.get("attempted"), f"RC-05 combined {operation} attempted",
|
|
maximum=280,
|
|
)
|
|
combined_terminal = _require_count(
|
|
combined.get("terminal"), f"RC-05 combined {operation} terminal",
|
|
maximum=280,
|
|
)
|
|
combined_unknown = _require_count(
|
|
combined.get("unknown"), f"RC-05 combined {operation} unknown",
|
|
maximum=280,
|
|
)
|
|
_validate_elapsed_statistics(
|
|
prior["elapsed_seconds"],
|
|
f"RC-05 historical {operation} elapsed statistics",
|
|
expected_observed=prior_attempted,
|
|
)
|
|
_validate_elapsed_statistics(
|
|
combined["elapsed_seconds"],
|
|
f"RC-05 combined {operation} elapsed statistics",
|
|
expected_observed=combined_attempted_value,
|
|
)
|
|
for section_name, section, keys in (
|
|
("historical provider", prior.get("provider_outcomes"),
|
|
("succeeded", "failed", "cancelled", "timeout")),
|
|
("historical business", prior.get("business_outcomes"),
|
|
("succeeded", "failed", "cancelled", "awaiting_human", "other")),
|
|
("combined provider", combined.get("provider_outcomes"),
|
|
("succeeded", "failed", "cancelled", "timeout")),
|
|
("combined business", combined.get("business_outcomes"),
|
|
("succeeded", "failed", "cancelled", "awaiting_human", "other")),
|
|
):
|
|
if not isinstance(section, dict) or set(section) != set(keys):
|
|
raise PublicationBatchError(
|
|
f"RC-05 {section_name} outcomes are incomplete: {operation}"
|
|
)
|
|
for key in keys:
|
|
_require_count(
|
|
section[key], f"RC-05 {section_name} {operation} {key}", maximum=280
|
|
)
|
|
if (
|
|
prior_attempted != historical_operation_counts[operation]
|
|
or prior_terminal != prior_attempted
|
|
or prior_unknown != 0
|
|
or prior.get("provider_outcomes") != historical_provider_outcomes[operation]
|
|
or prior.get("business_outcomes") != historical_business_outcomes[operation]
|
|
or combined_attempted_value != attempted + prior_attempted
|
|
or combined_terminal != combined_attempted_value
|
|
or combined_unknown != 0
|
|
or combined.get("provider_outcomes") != {
|
|
key: stat["provider_outcomes"][key]
|
|
+ historical_provider_outcomes[operation][key]
|
|
for key in ("succeeded", "failed", "cancelled", "timeout")
|
|
}
|
|
or combined.get("business_outcomes") != {
|
|
key: stat["business_outcomes"][key]
|
|
+ historical_business_outcomes[operation][key]
|
|
for key in ("succeeded", "failed", "cancelled", "awaiting_human", "other")
|
|
}
|
|
):
|
|
raise PublicationBatchError(f"RC-05 combined operation counts drift: {operation}")
|
|
historical_attempted += prior_attempted
|
|
combined_attempted += combined_attempted_value
|
|
if historical_attempted != historical_count:
|
|
raise PublicationBatchError("RC-05 historical operation statistics are incomplete")
|
|
if (
|
|
statistics["repair_draft"]["attempted"]
|
|
!= statistics["repair_review"]["attempted"]
|
|
or repair_case_calls != 2 * statistics["repair_draft"]["attempted"]
|
|
):
|
|
raise PublicationBatchError("RC-05 repair operation/case counts do not reconcile")
|
|
if run["historical_by_operation"] != {
|
|
operation: statistics[operation]["historical"]
|
|
for operation in EXPECTED_RC05_OPERATION_LIMITS
|
|
}:
|
|
raise PublicationBatchError("RC-05 historical_by_operation projection drift")
|
|
|
|
campaign_counts = _require_keys(
|
|
snapshot["campaign"],
|
|
"RC-05 terminal campaign counts",
|
|
(
|
|
"attempted", "terminal", "unknown", "provider_terminal",
|
|
"current_plan_attempted", "foreign_plan_attempted",
|
|
"provider_outcomes",
|
|
),
|
|
)
|
|
campaign_attempted = _require_count(
|
|
campaign_counts["attempted"], "RC-05 campaign attempted", maximum=280
|
|
)
|
|
campaign_terminal = _require_count(
|
|
campaign_counts["terminal"], "RC-05 campaign terminal", maximum=280
|
|
)
|
|
campaign_unknown = _require_count(
|
|
campaign_counts["unknown"], "RC-05 campaign unknown", maximum=280
|
|
)
|
|
campaign_provider_terminal = _require_count(
|
|
campaign_counts["provider_terminal"],
|
|
"RC-05 campaign provider-terminal count",
|
|
maximum=280,
|
|
)
|
|
campaign_current_plan = _require_count(
|
|
campaign_counts["current_plan_attempted"],
|
|
"RC-05 campaign current-plan attempted",
|
|
maximum=280,
|
|
)
|
|
campaign_foreign_plan = _require_count(
|
|
campaign_counts["foreign_plan_attempted"],
|
|
"RC-05 campaign foreign-plan attempted",
|
|
maximum=280,
|
|
)
|
|
campaign_provider_outcomes = _require_keys(
|
|
campaign_counts["provider_outcomes"],
|
|
"RC-05 campaign provider outcomes",
|
|
("succeeded", "failed", "cancelled"),
|
|
)
|
|
for status, value in campaign_provider_outcomes.items():
|
|
_require_count(
|
|
value, f"RC-05 campaign provider outcome {status}", maximum=280
|
|
)
|
|
combined_counts = run["combined_call_counts"]
|
|
if (
|
|
campaign_attempted != current_attempted
|
|
or campaign_terminal != current_attempted
|
|
or campaign_unknown != 0
|
|
or campaign_provider_terminal != current_attempted
|
|
or campaign_provider_terminal != provider_terminal
|
|
or campaign_current_plan != current_attempted
|
|
or campaign_foreign_plan != 0
|
|
or campaign_current_plan + campaign_foreign_plan != campaign_attempted
|
|
or campaign_provider_outcomes != {
|
|
"succeeded": current_attempted,
|
|
"failed": 0,
|
|
"cancelled": 0,
|
|
}
|
|
or run["campaign_new_provider_call_count"] != current_attempted
|
|
or combined_counts != {
|
|
"attempted": combined_attempted,
|
|
"terminal": combined_attempted,
|
|
"unknown": 0,
|
|
}
|
|
or combined_attempted != current_attempted + historical_count
|
|
or run["provider_call_count_including_historical"] != combined_attempted
|
|
or combined_attempted > 280
|
|
):
|
|
raise PublicationBatchError(
|
|
"RC-05 campaign or combined call statistics drift; the final frozen plan must be "
|
|
"the campaign's only new execution plan"
|
|
)
|
|
budget = snapshot.get("budget")
|
|
if (
|
|
not isinstance(budget, dict)
|
|
or budget.get("provider_call_limit") != 280
|
|
or budget.get("historical_provider_call_count") != historical_count
|
|
or budget.get("historical_repair_budget_charged") != repair_count
|
|
or budget.get("historical_receipt_sha256") != historical_receipt_sha
|
|
or budget.get("historical_attempts_sha256") != historical_attempts_sha
|
|
):
|
|
raise PublicationBatchError("RC-05 ledger budget evidence drift")
|
|
|
|
return {
|
|
"summary_schema_version": RC05_SUMMARY_SCHEMA_VERSION,
|
|
"summary_raw_sha256": summary_raw_sha256,
|
|
"ledger_identity_raw_sha256": ledger_identity_raw_sha256,
|
|
"ledger_identity_sha256": ledger_identity_sha256,
|
|
"campaign_identity_raw_sha256": campaign_identity_raw_sha256,
|
|
"campaign_identity_sha256": campaign_identity_sha256,
|
|
"plan_raw_sha256": plan_sha,
|
|
"case_results_sha256": _sha256_json(result_rows),
|
|
"operation_statistics_sha256": _sha256_json(statistics),
|
|
"case_operation_coverage_sha256": _sha256_json(coverage),
|
|
"historical_remediation_sha256": _sha256_json(remediation),
|
|
"current_plan_provider_call_count": current_attempted,
|
|
"historical_provider_call_count": historical_count,
|
|
"combined_provider_call_count": combined_attempted,
|
|
"complete_real_green": True,
|
|
}
|
|
|
|
|
|
def _historical_attempt_can_be_release_eligible(attempt: Mapping[str, Any]) -> bool:
|
|
return (
|
|
attempt.get("provider_status") == "succeeded"
|
|
and attempt.get("timed_out") is False
|
|
and attempt.get("business_status") in {"succeeded", "resolved"}
|
|
)
|
|
|
|
|
|
def _empty_disposition(case: Mapping[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
**case,
|
|
"status": "unresolved",
|
|
"human_confirmed": False,
|
|
"decision_source": None,
|
|
"decision_maker": None,
|
|
"decided_at": None,
|
|
"evidence_sha256": None,
|
|
"risk_acceptance": None,
|
|
"notes": "",
|
|
}
|
|
|
|
|
|
def _validate_decision_maker(value: Any, label: str) -> dict[str, str]:
|
|
row = _require_keys(value, label, ("approver_id", "name", "role", "organization"))
|
|
normalized = {
|
|
field: _require_string(row[field], f"{label}.{field}", max_length=256)
|
|
for field in ("approver_id", "name", "role", "organization")
|
|
}
|
|
stable_identity = {
|
|
field: normalized[field]
|
|
for field in ("approver_id", "name")
|
|
}
|
|
if stable_identity != SOLE_RELEASE_REVIEWER:
|
|
raise PublicationBatchError("Golden dispositions must use the configured sole release reviewer")
|
|
return normalized
|
|
|
|
|
|
def _require_release_reviewer_identity(value: Any, label: str) -> dict[str, str]:
|
|
"""Validate human-supplied identity assertions for one exact approval."""
|
|
|
|
row = _require_keys(value, label, RELEASE_REVIEWER_IDENTITY_FIELDS)
|
|
normalized = {
|
|
field: _require_string(
|
|
row[field],
|
|
f"{label}.{field}",
|
|
max_length=4000 if field == "authorization_scope" else 256,
|
|
)
|
|
for field in RELEASE_REVIEWER_IDENTITY_FIELDS
|
|
}
|
|
stable_identity = {
|
|
field: normalized[field]
|
|
for field in ("approver_id", "name")
|
|
}
|
|
if stable_identity != SOLE_RELEASE_REVIEWER:
|
|
raise PublicationBatchError(f"{label} must identify the registered sole release reviewer")
|
|
return normalized
|
|
|
|
|
|
def validate_disposition(value: Any, case: Mapping[str, Any]) -> dict[str, Any]:
|
|
required = (
|
|
"sample_id",
|
|
"suite",
|
|
"asset_id",
|
|
"source_case_sha256",
|
|
"status",
|
|
"human_confirmed",
|
|
"decision_source",
|
|
"decision_maker",
|
|
"decided_at",
|
|
"evidence_sha256",
|
|
"risk_acceptance",
|
|
"notes",
|
|
)
|
|
row = _require_keys(value, f"Golden disposition {case['sample_id']}", required)
|
|
for key in ("sample_id", "suite", "asset_id", "source_case_sha256"):
|
|
if row[key] != case[key]:
|
|
raise PublicationBatchError(f"Golden disposition identity drift for {case['sample_id']}: {key}")
|
|
status_value = row["status"]
|
|
if status_value not in DISPOSITION_STATUSES:
|
|
raise PublicationBatchError(f"invalid Golden disposition status for {case['sample_id']}")
|
|
if not isinstance(row["notes"], str) or len(row["notes"]) > 4000:
|
|
raise PublicationBatchError("Golden disposition notes are invalid")
|
|
|
|
if status_value == "unresolved":
|
|
if row["human_confirmed"] is not False:
|
|
raise PublicationBatchError("unresolved disposition cannot claim human confirmation")
|
|
if any(row[key] is not None for key in (
|
|
"decision_source", "decision_maker", "decided_at", "evidence_sha256", "risk_acceptance"
|
|
)):
|
|
raise PublicationBatchError("unresolved disposition must not contain a decision or evidence")
|
|
return dict(row)
|
|
|
|
if row["human_confirmed"] is not True or row["decision_source"] != "single_reviewer_batch":
|
|
raise PublicationBatchError("resolved disposition must be included in the sole reviewer batch")
|
|
maker = _validate_decision_maker(row["decision_maker"], "decision maker")
|
|
decided = _parse_time(row["decided_at"], "decided_at")
|
|
evidence = _require_sha(row["evidence_sha256"], "disposition evidence SHA-256")
|
|
|
|
if status_value == "named_legal_approval":
|
|
if row["risk_acceptance"] is not None:
|
|
raise PublicationBatchError("named legal approval must not contain risk acceptance fields")
|
|
return {**row, "decision_maker": maker, "evidence_sha256": evidence}
|
|
|
|
risk = _require_keys(
|
|
row["risk_acceptance"],
|
|
"risk acceptance",
|
|
("owner", "scope", "expires_at", "controls", "use_limits"),
|
|
)
|
|
owner = _require_string(risk["owner"], "risk acceptance owner", max_length=256)
|
|
if owner != SOLE_RELEASE_REVIEWER["name"]:
|
|
raise PublicationBatchError("risk acceptance owner must be the configured sole release reviewer")
|
|
scope = _require_string(risk["scope"], "risk acceptance scope", max_length=4000)
|
|
expires = _parse_time(risk["expires_at"], "risk acceptance expires_at")
|
|
if expires <= decided:
|
|
raise PublicationBatchError("risk acceptance expiry must be after its decision time")
|
|
controls = risk["controls"]
|
|
limits = risk["use_limits"]
|
|
if not isinstance(controls, list) or not controls or any(not isinstance(item, str) or not item.strip() for item in controls):
|
|
raise PublicationBatchError("risk acceptance controls must be a non-empty string list")
|
|
if not isinstance(limits, list) or not limits or any(not isinstance(item, str) or not item.strip() for item in limits):
|
|
raise PublicationBatchError("risk acceptance use_limits must be a non-empty string list")
|
|
return {
|
|
**row,
|
|
"decision_maker": maker,
|
|
"evidence_sha256": evidence,
|
|
"risk_acceptance": {
|
|
"owner": owner,
|
|
"scope": scope,
|
|
"expires_at": risk["expires_at"],
|
|
"controls": [item.strip() for item in controls],
|
|
"use_limits": [item.strip() for item in limits],
|
|
},
|
|
}
|
|
|
|
|
|
def build_dispositions(golden_cases: list[dict[str, Any]], source: Any | None) -> list[dict[str, Any]]:
|
|
expected = {item["sample_id"]: item for item in golden_cases}
|
|
if source is None:
|
|
return [_empty_disposition(expected[key]) for key in sorted(expected)]
|
|
document = _require_keys(source, "Golden dispositions", ("schema_version", "items"))
|
|
if document["schema_version"] != DISPOSITION_SCHEMA_VERSION or not isinstance(document["items"], list):
|
|
raise PublicationBatchError("unsupported Golden disposition document")
|
|
supplied: dict[str, Any] = {}
|
|
for row in document["items"]:
|
|
if not isinstance(row, dict) or not isinstance(row.get("sample_id"), str):
|
|
raise PublicationBatchError("Golden disposition item is invalid")
|
|
if row["sample_id"] in supplied:
|
|
raise PublicationBatchError(f"duplicate Golden disposition: {row['sample_id']}")
|
|
supplied[row["sample_id"]] = row
|
|
if set(supplied) != set(expected):
|
|
raise PublicationBatchError(
|
|
f"Golden disposition inventory mismatch; missing={sorted(set(expected)-set(supplied))}, "
|
|
f"extra={sorted(set(supplied)-set(expected))}"
|
|
)
|
|
return [validate_disposition(supplied[key], expected[key]) for key in sorted(expected)]
|
|
|
|
|
|
def assemble_manifest(
|
|
*,
|
|
commit: str,
|
|
tag: str,
|
|
source_manifest_sha256: str,
|
|
source_manifest_payload_sha256: str | None,
|
|
image_identities_sha256: str,
|
|
image_identity: dict[str, Any],
|
|
rc05_plan_sha256: str,
|
|
rc05_plan_identity: dict[str, Any],
|
|
rc05_terminal_evidence: dict[str, Any],
|
|
inventory: dict[str, Any],
|
|
dispositions: list[dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
unresolved = sum(item["status"] == "unresolved" for item in dispositions)
|
|
disposition_counts = {
|
|
status_value: sum(item["status"] == status_value for item in dispositions)
|
|
for status_value in sorted(DISPOSITION_STATUSES)
|
|
}
|
|
manifest: dict[str, Any] = {
|
|
"schema_version": SCHEMA_VERSION,
|
|
"candidate": {
|
|
"commit": commit,
|
|
"tag": tag,
|
|
"source_manifest_sha256": _require_sha(source_manifest_sha256, "source manifest SHA-256"),
|
|
"source_manifest_payload_sha256": (
|
|
_require_sha(source_manifest_payload_sha256, "source manifest payload SHA-256")
|
|
if source_manifest_payload_sha256 is not None
|
|
else None
|
|
),
|
|
"image_identities_sha256": _require_sha(image_identities_sha256, "image identities SHA-256"),
|
|
"image_identity_set_sha256": image_identity["identity_set_sha256"],
|
|
"images": image_identity["images"],
|
|
"rc05_plan_sha256": _require_sha(rc05_plan_sha256, "RC-05 plan SHA-256"),
|
|
"rc05_plan_identity": rc05_plan_identity,
|
|
"rc05_terminal_evidence": rc05_terminal_evidence,
|
|
},
|
|
"asset_batch": {
|
|
"count": len(inventory["assets"]),
|
|
"items": inventory["assets"],
|
|
"inventory_sha256": _sha256_json(inventory["assets"]),
|
|
},
|
|
"prompt_module_batch": {
|
|
"count": len(inventory["prompt_modules"]),
|
|
"drafting_count": sum(item["category"] == "drafting" for item in inventory["prompt_modules"]),
|
|
"review_count": sum(item["category"] == "review" for item in inventory["prompt_modules"]),
|
|
"drafting_prompt_keys": ["contract_drafting"],
|
|
"review_routes": sorted(
|
|
item["publication_mapping"]["route"]
|
|
for item in inventory["prompt_modules"]
|
|
if item["category"] == "review"
|
|
),
|
|
"items": inventory["prompt_modules"],
|
|
"inventory_sha256": _sha256_json(inventory["prompt_modules"]),
|
|
},
|
|
"runtime_prompt_dependencies": {
|
|
"publication_item_count": EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT,
|
|
"count": len(inventory["runtime_prompt_dependencies"]),
|
|
"items": inventory["runtime_prompt_dependencies"],
|
|
"inventory_sha256": _sha256_json(inventory["runtime_prompt_dependencies"]),
|
|
},
|
|
"default_prompt_asset_boundary": inventory["default_prompt_boundary"],
|
|
"golden_dispositions": {
|
|
"count": len(dispositions),
|
|
"counts_by_status": disposition_counts,
|
|
"items": dispositions,
|
|
"inventory_sha256": _sha256_json(dispositions),
|
|
},
|
|
"publication_control": {
|
|
"rc05_terminal_evidence_complete": rc05_terminal_evidence.get("complete_real_green") is True,
|
|
"golden_dispositions_complete": unresolved == 0,
|
|
"approval_receipt_attached": False,
|
|
"apply_receipt_attached": False,
|
|
"single_reviewer_batch_and_system_item_receipts_complete": False,
|
|
"runtime_executable_prompt_snapshots_complete": False,
|
|
"blocked_controls": list(BLOCKED_CONTROLS),
|
|
"release_ready": False,
|
|
"approval_receipt_schema": APPROVAL_SCHEMA_VERSION,
|
|
"apply_receipt_schema": APPLY_SCHEMA_VERSION,
|
|
"apply_performed_by_this_tool": False,
|
|
},
|
|
"side_effects": {
|
|
"http_calls": 0,
|
|
"database_writes": 0,
|
|
"model_calls": 0,
|
|
"publication_api_calls": 0,
|
|
},
|
|
"claims_not_made": [
|
|
"This offline manifest is not legal approval and does not fabricate a signature.",
|
|
"This offline manifest does not authorize or perform publication.",
|
|
"The separate 46 default Prompt assets are not included in the 14-module batch.",
|
|
"No reviewer approval or apply evidence is embedded in this offline manifest.",
|
|
"One signed batch approval must bind all 60 assets, 14 Prompt modules, and 31 dispositions.",
|
|
"The apply service must emit 74 machine-verifiable item results and online hashes.",
|
|
"One composed drafting plus thirteen review runtime Prompt snapshots are not implemented and remain blocking.",
|
|
],
|
|
}
|
|
_assert_no_secret_material(manifest)
|
|
manifest["manifest_payload_sha256"] = _sha256_json(manifest)
|
|
return manifest
|
|
|
|
|
|
def _validate_asset(asset: Any) -> None:
|
|
if not isinstance(asset, dict):
|
|
raise PublicationBatchError("asset item must be an object")
|
|
required = {
|
|
"template_id", "name", "version", "document_kind", "contract_type", "source",
|
|
"source_lifecycle_status", "body_content", "content_sha256", "input_schema",
|
|
"required_variables", "optional_variables", "validation_rules", "clause_ids",
|
|
"review_rule_ids", "review_identity", "body_sha256", "input_schema_sha256",
|
|
"variables_sha256", "review_identity_sha256", "asset_payload_sha256",
|
|
}
|
|
if set(asset) != required:
|
|
raise PublicationBatchError(f"asset {asset.get('template_id')} field set drift")
|
|
template_id = _require_string(asset["template_id"], "template ID", max_length=128)
|
|
if not STABLE_ID_RE.fullmatch(template_id):
|
|
raise PublicationBatchError(f"asset ID is invalid: {template_id}")
|
|
if not isinstance(asset["body_content"], str) or not asset["body_content"]:
|
|
raise PublicationBatchError(f"asset body is empty: {template_id}")
|
|
if _sha256_bytes(asset["body_content"].encode("utf-8")) != asset["body_sha256"]:
|
|
raise PublicationBatchError(f"asset body hash drift: {template_id}")
|
|
schema_hash = _sha256_json(asset["input_schema"]) if asset["input_schema"] is not None else None
|
|
if schema_hash != asset["input_schema_sha256"]:
|
|
raise PublicationBatchError(f"asset input schema hash drift: {template_id}")
|
|
variable_hash = _sha256_json(
|
|
{"required": asset["required_variables"], "optional": asset["optional_variables"]}
|
|
)
|
|
if variable_hash != asset["variables_sha256"]:
|
|
raise PublicationBatchError(f"asset variable hash drift: {template_id}")
|
|
if _sha256_json(asset["review_identity"]) != asset["review_identity_sha256"]:
|
|
raise PublicationBatchError(f"asset review identity hash drift: {template_id}")
|
|
if _sha256_json(_asset_hash_projection(asset)) != asset["content_sha256"]:
|
|
raise PublicationBatchError(f"asset content hash drift: {template_id}")
|
|
payload = {key: value for key, value in asset.items() if key != "asset_payload_sha256"}
|
|
if _sha256_json(payload) != asset["asset_payload_sha256"]:
|
|
raise PublicationBatchError(f"asset payload hash drift: {template_id}")
|
|
|
|
|
|
def _validate_prompt_module(module: Any) -> None:
|
|
row = _require_keys(
|
|
module,
|
|
"Prompt module",
|
|
("category", "key", "version", "content", "content_sha256", "publication_mapping", "module_payload_sha256"),
|
|
)
|
|
if row["category"] not in {"drafting", "review"}:
|
|
raise PublicationBatchError("Prompt module category is invalid")
|
|
key = _require_string(row["key"], "Prompt module key", max_length=128)
|
|
if not isinstance(row["content"], str) or not row["content"]:
|
|
raise PublicationBatchError(f"Prompt module content is empty: {key}")
|
|
if _sha256_bytes(row["content"].encode("utf-8")) != row["content_sha256"]:
|
|
raise PublicationBatchError(f"Prompt module content hash drift: {key}")
|
|
mapping = _require_keys(row["publication_mapping"], "Prompt module mapping", ("prompt_key", "route"))
|
|
if row["category"] == "drafting" and mapping != {"prompt_key": "contract_drafting", "route": "contract_drafting"}:
|
|
raise PublicationBatchError("all drafting modules must map to the one contract_drafting Prompt")
|
|
if row["category"] == "review" and mapping != {"prompt_key": key, "route": key}:
|
|
raise PublicationBatchError("review module must map one-to-one to its review route")
|
|
payload = {name: value for name, value in row.items() if name != "module_payload_sha256"}
|
|
if _sha256_json(payload) != row["module_payload_sha256"]:
|
|
raise PublicationBatchError(f"Prompt module payload hash drift: {key}")
|
|
|
|
|
|
def validate_manifest(payload: Any) -> dict[str, Any]:
|
|
manifest = _require_keys(
|
|
payload,
|
|
"publication batch",
|
|
(
|
|
"schema_version", "candidate", "asset_batch", "prompt_module_batch",
|
|
"runtime_prompt_dependencies", "default_prompt_asset_boundary",
|
|
"golden_dispositions", "publication_control",
|
|
"side_effects", "claims_not_made", "manifest_payload_sha256",
|
|
),
|
|
)
|
|
if manifest["schema_version"] != SCHEMA_VERSION:
|
|
raise PublicationBatchError("unsupported publication batch schema")
|
|
expected_payload_hash = manifest["manifest_payload_sha256"]
|
|
_require_sha(expected_payload_hash, "publication batch payload SHA-256")
|
|
projection = {key: value for key, value in manifest.items() if key != "manifest_payload_sha256"}
|
|
if _sha256_json(projection) != expected_payload_hash:
|
|
raise PublicationBatchError("publication batch canonical hash drift")
|
|
|
|
candidate = _require_keys(
|
|
manifest["candidate"],
|
|
"candidate",
|
|
(
|
|
"commit", "tag", "source_manifest_sha256", "source_manifest_payload_sha256",
|
|
"image_identities_sha256", "image_identity_set_sha256", "images",
|
|
"rc05_plan_sha256", "rc05_plan_identity", "rc05_terminal_evidence",
|
|
),
|
|
)
|
|
if not COMMIT_RE.fullmatch(str(candidate["commit"])) or not TAG_RE.fullmatch(str(candidate["tag"])):
|
|
raise PublicationBatchError("candidate commit or tag is invalid")
|
|
for key in ("source_manifest_sha256", "image_identities_sha256", "image_identity_set_sha256", "rc05_plan_sha256"):
|
|
_require_sha(candidate[key], f"candidate.{key}")
|
|
if candidate["source_manifest_payload_sha256"] is not None:
|
|
_require_sha(candidate["source_manifest_payload_sha256"], "candidate.source_manifest_payload_sha256")
|
|
image_check = validate_image_identities(
|
|
{
|
|
"schema_version": IMAGE_SCHEMA_VERSION,
|
|
"commit": candidate["commit"],
|
|
"tag": candidate["tag"],
|
|
"images": candidate["images"],
|
|
},
|
|
candidate["commit"],
|
|
candidate["tag"],
|
|
)
|
|
if image_check["identity_set_sha256"] != candidate["image_identity_set_sha256"]:
|
|
raise PublicationBatchError("image identity set hash drift")
|
|
validate_rc05_plan_identity(candidate["rc05_plan_identity"])
|
|
terminal = _require_keys(
|
|
candidate["rc05_terminal_evidence"],
|
|
"RC-05 terminal evidence identity",
|
|
(
|
|
"summary_schema_version", "summary_raw_sha256",
|
|
"ledger_identity_raw_sha256", "ledger_identity_sha256",
|
|
"campaign_identity_raw_sha256", "campaign_identity_sha256",
|
|
"plan_raw_sha256", "case_results_sha256", "operation_statistics_sha256",
|
|
"case_operation_coverage_sha256", "historical_remediation_sha256",
|
|
"current_plan_provider_call_count", "historical_provider_call_count",
|
|
"combined_provider_call_count", "complete_real_green",
|
|
),
|
|
)
|
|
for key in (
|
|
"summary_raw_sha256", "ledger_identity_raw_sha256", "ledger_identity_sha256",
|
|
"campaign_identity_raw_sha256", "campaign_identity_sha256", "plan_raw_sha256",
|
|
"case_results_sha256", "operation_statistics_sha256",
|
|
"case_operation_coverage_sha256", "historical_remediation_sha256",
|
|
):
|
|
_require_sha(terminal[key], f"RC-05 terminal evidence {key}")
|
|
current_plan_calls = _require_count(
|
|
terminal["current_plan_provider_call_count"],
|
|
"RC-05 terminal current-plan provider calls",
|
|
maximum=276,
|
|
)
|
|
historical_calls = _require_count(
|
|
terminal["historical_provider_call_count"],
|
|
"RC-05 terminal historical provider calls",
|
|
maximum=280,
|
|
)
|
|
combined_calls = _require_count(
|
|
terminal["combined_provider_call_count"],
|
|
"RC-05 terminal combined provider calls",
|
|
maximum=280,
|
|
)
|
|
if (
|
|
terminal["summary_schema_version"] != RC05_SUMMARY_SCHEMA_VERSION
|
|
or terminal["plan_raw_sha256"] != candidate["rc05_plan_sha256"]
|
|
or terminal["complete_real_green"] is not True
|
|
or current_plan_calls < 264
|
|
or historical_calls != 4
|
|
or combined_calls != current_plan_calls + historical_calls
|
|
):
|
|
raise PublicationBatchError("RC-05 terminal evidence identity drift")
|
|
|
|
assets = manifest["asset_batch"]
|
|
if not isinstance(assets, dict) or set(assets) != {"count", "items", "inventory_sha256"}:
|
|
raise PublicationBatchError("asset batch shape is invalid")
|
|
if assets["count"] != EXPECTED_ASSET_COUNT or not isinstance(assets["items"], list) or len(assets["items"]) != EXPECTED_ASSET_COUNT:
|
|
raise PublicationBatchError("asset batch must contain exactly 60 assets")
|
|
for item in assets["items"]:
|
|
_validate_asset(item)
|
|
asset_ids = [item["template_id"] for item in assets["items"]]
|
|
if len(set(asset_ids)) != EXPECTED_ASSET_COUNT:
|
|
raise PublicationBatchError("asset batch contains duplicate template IDs")
|
|
if _sha256_json(assets["items"]) != assets["inventory_sha256"]:
|
|
raise PublicationBatchError("asset inventory hash drift")
|
|
|
|
modules = manifest["prompt_module_batch"]
|
|
expected_module_keys = {
|
|
"count", "drafting_count", "review_count", "drafting_prompt_keys",
|
|
"review_routes", "items", "inventory_sha256",
|
|
}
|
|
if not isinstance(modules, dict) or set(modules) != expected_module_keys:
|
|
raise PublicationBatchError("Prompt module batch shape is invalid")
|
|
if (
|
|
modules["count"] != EXPECTED_MODULE_COUNT
|
|
or modules["drafting_count"] != EXPECTED_DRAFTING_MODULE_COUNT
|
|
or modules["review_count"] != EXPECTED_REVIEW_MODULE_COUNT
|
|
or modules["drafting_prompt_keys"] != ["contract_drafting"]
|
|
or not isinstance(modules["items"], list)
|
|
or len(modules["items"]) != EXPECTED_MODULE_COUNT
|
|
):
|
|
raise PublicationBatchError("Prompt module counts or drafting mapping drift")
|
|
for item in modules["items"]:
|
|
_validate_prompt_module(item)
|
|
module_keys = [item["key"] for item in modules["items"]]
|
|
if len(set(module_keys)) != EXPECTED_MODULE_COUNT:
|
|
raise PublicationBatchError("Prompt module batch contains duplicate keys")
|
|
review_routes = sorted(
|
|
item["key"] for item in modules["items"] if item["category"] == "review"
|
|
)
|
|
if modules["review_routes"] != review_routes or len(review_routes) != EXPECTED_REVIEW_MODULE_COUNT:
|
|
raise PublicationBatchError("review route inventory drift")
|
|
if _sha256_json(modules["items"]) != modules["inventory_sha256"]:
|
|
raise PublicationBatchError("Prompt module inventory hash drift")
|
|
|
|
runtime = _require_keys(
|
|
manifest["runtime_prompt_dependencies"],
|
|
"runtime Prompt dependencies",
|
|
("publication_item_count", "count", "items", "inventory_sha256"),
|
|
)
|
|
if (
|
|
runtime["publication_item_count"] != EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT
|
|
or runtime["count"] != EXPECTED_RUNTIME_PROMPT_COUNT
|
|
or not isinstance(runtime["items"], list)
|
|
or len(runtime["items"]) != EXPECTED_RUNTIME_PROMPT_COUNT
|
|
):
|
|
raise PublicationBatchError(
|
|
"runtime Prompt dependencies must distinguish 74 publication items from 14 snapshots"
|
|
)
|
|
runtime_keys: set[str] = set()
|
|
runtime_kinds: list[str] = []
|
|
runtime_keys_by_kind: dict[str, set[str]] = {}
|
|
publication_modules = {
|
|
item["key"]: {
|
|
"key": item["key"],
|
|
"version": item["version"],
|
|
"content_sha256": item["content_sha256"],
|
|
}
|
|
for item in modules["items"]
|
|
}
|
|
for item in runtime["items"]:
|
|
row = _require_keys(
|
|
item,
|
|
"runtime Prompt dependency",
|
|
(
|
|
"kind",
|
|
"runtime_key",
|
|
"version",
|
|
"content_sha256",
|
|
"publication_dependency",
|
|
"publication_module_dependencies",
|
|
),
|
|
)
|
|
key = _require_string(row["runtime_key"], "runtime Prompt key", max_length=128)
|
|
_require_string(row["version"], f"runtime Prompt {key} version", max_length=128)
|
|
_require_sha(row["content_sha256"], f"runtime Prompt {key} content SHA-256")
|
|
if key in runtime_keys:
|
|
raise PublicationBatchError("runtime Prompt dependency keys must be unique")
|
|
runtime_keys.add(key)
|
|
runtime_kinds.append(row["kind"])
|
|
runtime_keys_by_kind.setdefault(row["kind"], set()).add(key)
|
|
expected_dependency = {
|
|
"composed_drafting": "six_module_composition",
|
|
"new_review_module": "published_module",
|
|
"existing_domain_prompt": "preexisting_domain_prompt",
|
|
}.get(row["kind"])
|
|
if row["publication_dependency"] != expected_dependency:
|
|
raise PublicationBatchError(f"runtime Prompt dependency kind drift: {key}")
|
|
dependency_rows = row["publication_module_dependencies"]
|
|
if not isinstance(dependency_rows, list):
|
|
raise PublicationBatchError(
|
|
f"runtime Prompt module dependencies must be a list: {key}"
|
|
)
|
|
if row["kind"] == "composed_drafting":
|
|
expected_module_keys = list(DRAFTING_RUNTIME_MODULE_ORDER)
|
|
elif row["kind"] == "new_review_module":
|
|
expected_module_keys = [key]
|
|
else:
|
|
expected_module_keys = []
|
|
expected_dependency_rows = [
|
|
publication_modules[module_key] for module_key in expected_module_keys
|
|
]
|
|
if dependency_rows != expected_dependency_rows:
|
|
raise PublicationBatchError(
|
|
f"runtime Prompt is not bound to exact publication module identities: {key}"
|
|
)
|
|
if (
|
|
runtime_kinds.count("composed_drafting") != 1
|
|
or runtime_kinds.count("new_review_module") != 8
|
|
or runtime_kinds.count("existing_domain_prompt") != 5
|
|
or runtime_keys_by_kind.get("composed_drafting") != {"contract_drafting_v1"}
|
|
or runtime_keys_by_kind.get("new_review_module") != set(review_routes)
|
|
or runtime_keys_by_kind.get("existing_domain_prompt")
|
|
!= set(RUNTIME_EXISTING_DOMAIN_PROMPTS)
|
|
):
|
|
raise PublicationBatchError("runtime Prompt dependency 1+8+5 inventory drift")
|
|
if _sha256_json(runtime["items"]) != runtime["inventory_sha256"]:
|
|
raise PublicationBatchError("runtime Prompt dependency inventory hash drift")
|
|
|
|
boundary = manifest["default_prompt_asset_boundary"]
|
|
required_boundary = {
|
|
"included_in_publication_batch", "count", "source_path", "source_raw_sha256",
|
|
"inventory", "inventory_sha256", "boundary_note",
|
|
}
|
|
if not isinstance(boundary, dict) or set(boundary) != required_boundary:
|
|
raise PublicationBatchError("default Prompt boundary shape is invalid")
|
|
if boundary["included_in_publication_batch"] is not False or boundary["count"] != EXPECTED_DEFAULT_PROMPT_COUNT:
|
|
raise PublicationBatchError("46 default Prompt assets must remain outside this batch")
|
|
if not isinstance(boundary["inventory"], list) or len(boundary["inventory"]) != EXPECTED_DEFAULT_PROMPT_COUNT:
|
|
raise PublicationBatchError("default Prompt boundary inventory count drift")
|
|
default_keys = [item.get("prompt_key") for item in boundary["inventory"] if isinstance(item, dict)]
|
|
if len(default_keys) != EXPECTED_DEFAULT_PROMPT_COUNT or len(set(default_keys)) != EXPECTED_DEFAULT_PROMPT_COUNT:
|
|
raise PublicationBatchError("default Prompt boundary contains duplicate or invalid keys")
|
|
if set(default_keys).intersection(module_keys):
|
|
raise PublicationBatchError("14 modules overlap the separate 46 default Prompt assets")
|
|
if _sha256_json(boundary["inventory"]) != boundary["inventory_sha256"]:
|
|
raise PublicationBatchError("default Prompt boundary hash drift")
|
|
|
|
golden = manifest["golden_dispositions"]
|
|
if not isinstance(golden, dict) or set(golden) != {"count", "counts_by_status", "items", "inventory_sha256"}:
|
|
raise PublicationBatchError("Golden disposition section is invalid")
|
|
if golden["count"] != EXPECTED_GOLDEN_COUNT or not isinstance(golden["items"], list) or len(golden["items"]) != EXPECTED_GOLDEN_COUNT:
|
|
raise PublicationBatchError("all 31 Golden dispositions must be present")
|
|
cases = {
|
|
item["sample_id"]: {
|
|
"sample_id": item["sample_id"],
|
|
"suite": item["suite"],
|
|
"asset_id": item["asset_id"],
|
|
"source_case_sha256": item["source_case_sha256"],
|
|
}
|
|
for item in golden["items"]
|
|
if isinstance(item, dict) and isinstance(item.get("sample_id"), str)
|
|
}
|
|
if len(cases) != EXPECTED_GOLDEN_COUNT:
|
|
raise PublicationBatchError("Golden dispositions contain duplicate sample IDs")
|
|
validated = [validate_disposition(item, cases[item["sample_id"]]) for item in golden["items"]]
|
|
if validated != golden["items"]:
|
|
raise PublicationBatchError("Golden dispositions are not normalized")
|
|
actual_counts = {
|
|
status_value: sum(item["status"] == status_value for item in golden["items"])
|
|
for status_value in sorted(DISPOSITION_STATUSES)
|
|
}
|
|
if golden["counts_by_status"] != actual_counts or _sha256_json(golden["items"]) != golden["inventory_sha256"]:
|
|
raise PublicationBatchError("Golden disposition count or hash drift")
|
|
suite_counts = {
|
|
"contract_generation": sum(item["suite"] == "contract_generation" for item in golden["items"]),
|
|
"clause_recommendation": sum(item["suite"] == "clause_recommendation" for item in golden["items"]),
|
|
}
|
|
if suite_counts != {
|
|
"contract_generation": EXPECTED_GOLDEN_GENERATION_COUNT,
|
|
"clause_recommendation": EXPECTED_GOLDEN_CLAUSE_COUNT,
|
|
}:
|
|
raise PublicationBatchError("Golden suite counts drift")
|
|
|
|
control = manifest["publication_control"]
|
|
expected_control_keys = {
|
|
"rc05_terminal_evidence_complete", "golden_dispositions_complete",
|
|
"approval_receipt_attached", "apply_receipt_attached",
|
|
"single_reviewer_batch_and_system_item_receipts_complete",
|
|
"runtime_executable_prompt_snapshots_complete", "blocked_controls",
|
|
"release_ready", "approval_receipt_schema", "apply_receipt_schema",
|
|
"apply_performed_by_this_tool",
|
|
}
|
|
if not isinstance(control, dict) or set(control) != expected_control_keys:
|
|
raise PublicationBatchError("publication control shape is invalid")
|
|
complete = actual_counts["unresolved"] == 0
|
|
if (
|
|
control["golden_dispositions_complete"] is not complete
|
|
or control["rc05_terminal_evidence_complete"] is not True
|
|
or control["approval_receipt_attached"] is not False
|
|
or control["apply_receipt_attached"] is not False
|
|
or control["single_reviewer_batch_and_system_item_receipts_complete"] is not False
|
|
or control["runtime_executable_prompt_snapshots_complete"] is not False
|
|
or control["blocked_controls"] != list(BLOCKED_CONTROLS)
|
|
or control["release_ready"] is not False
|
|
or control["apply_performed_by_this_tool"] is not False
|
|
or control["approval_receipt_schema"] != APPROVAL_SCHEMA_VERSION
|
|
or control["apply_receipt_schema"] != APPLY_SCHEMA_VERSION
|
|
):
|
|
raise PublicationBatchError("offline manifest must remain fail-closed and unapplied")
|
|
if manifest["side_effects"] != {
|
|
"http_calls": 0,
|
|
"database_writes": 0,
|
|
"model_calls": 0,
|
|
"publication_api_calls": 0,
|
|
}:
|
|
raise PublicationBatchError("offline side-effect boundary drift")
|
|
_assert_no_secret_material(manifest)
|
|
return manifest
|
|
|
|
|
|
def build_approval_review_summary(
|
|
payload: Any,
|
|
*,
|
|
manifest_raw_sha256: str,
|
|
reviewer: Any,
|
|
) -> dict[str, Any]:
|
|
"""Build the exact, human-readable projection approved by the sole reviewer."""
|
|
|
|
manifest = validate_manifest(payload)
|
|
reviewer_identity = _require_release_reviewer_identity(
|
|
reviewer,
|
|
"approval review identity",
|
|
)
|
|
candidate = manifest["candidate"]
|
|
terminal = candidate["rc05_terminal_evidence"]
|
|
manifest_raw_sha256 = _require_sha(
|
|
manifest_raw_sha256,
|
|
"publication batch raw SHA-256",
|
|
)
|
|
summary = {
|
|
"schema_version": APPROVAL_REVIEW_SUMMARY_SCHEMA_VERSION,
|
|
"decision_scope": "approve_exact_rc06_publication_batch",
|
|
"reviewer": reviewer_identity,
|
|
"candidate": {
|
|
"commit": candidate["commit"],
|
|
"tag": candidate["tag"],
|
|
"source_manifest_sha256": candidate["source_manifest_sha256"],
|
|
"source_manifest_payload_sha256": candidate["source_manifest_payload_sha256"],
|
|
"image_identities_sha256": candidate["image_identities_sha256"],
|
|
"image_identity_set_sha256": candidate["image_identity_set_sha256"],
|
|
"rc05_plan_sha256": candidate["rc05_plan_sha256"],
|
|
"rc05_summary_raw_sha256": terminal["summary_raw_sha256"],
|
|
"rc05_ledger_identity_raw_sha256": terminal["ledger_identity_raw_sha256"],
|
|
"rc05_campaign_identity_raw_sha256": terminal["campaign_identity_raw_sha256"],
|
|
"rc05_complete_real_green": terminal["complete_real_green"],
|
|
},
|
|
"batch": {
|
|
"asset_count": manifest["asset_batch"]["count"],
|
|
"asset_inventory_sha256": manifest["asset_batch"]["inventory_sha256"],
|
|
"prompt_module_count": manifest["prompt_module_batch"]["count"],
|
|
"prompt_module_inventory_sha256": manifest["prompt_module_batch"]["inventory_sha256"],
|
|
"golden_disposition_count": manifest["golden_dispositions"]["count"],
|
|
"golden_disposition_counts_by_status": manifest["golden_dispositions"]["counts_by_status"],
|
|
"golden_disposition_inventory_sha256": manifest["golden_dispositions"]["inventory_sha256"],
|
|
},
|
|
"manifest_raw_sha256": manifest_raw_sha256,
|
|
"manifest_payload_sha256": manifest["manifest_payload_sha256"],
|
|
"approval_validity_max_hours": int(MAX_APPROVAL_WINDOW.total_seconds() // 3600),
|
|
"remaining_controls_after_approval": list(BLOCKED_CONTROLS[1:]),
|
|
"claims_not_made": [
|
|
"This approval does not itself publish any asset or Prompt module.",
|
|
"This approval does not authorize public production traffic cutover.",
|
|
"System apply evidence and executable runtime Prompt snapshots remain separate gates.",
|
|
],
|
|
}
|
|
_assert_no_secret_material(summary, "approval review summary")
|
|
return summary
|
|
|
|
|
|
def _approval_risk_acceptance_deadline(manifest: Mapping[str, Any]) -> datetime | None:
|
|
"""Return the earliest expiry across scoped Golden risk acceptances."""
|
|
|
|
deadlines = [
|
|
_parse_time(
|
|
item["risk_acceptance"]["expires_at"],
|
|
f"Golden disposition {item['sample_id']} risk acceptance expires_at",
|
|
)
|
|
for item in manifest["golden_dispositions"]["items"]
|
|
if item["status"] == "scoped_internal_risk_acceptance"
|
|
]
|
|
return min(deadlines) if deadlines else None
|
|
|
|
|
|
def _validate_disposition_approval_window(
|
|
manifest: Mapping[str, Any],
|
|
*,
|
|
approved_at: datetime,
|
|
expires_at: datetime,
|
|
) -> None:
|
|
"""Ensure every disposition decision and exception covers the approval window."""
|
|
|
|
for item in manifest["golden_dispositions"]["items"]:
|
|
if item["status"] == "unresolved":
|
|
raise PublicationBatchError(
|
|
"approval cannot release a batch with unresolved Golden dispositions"
|
|
)
|
|
decided_at = _parse_time(
|
|
item["decided_at"],
|
|
f"Golden disposition {item['sample_id']} decided_at",
|
|
)
|
|
if decided_at > approved_at:
|
|
raise PublicationBatchError(
|
|
f"Golden disposition {item['sample_id']} was decided after the approval time"
|
|
)
|
|
if item["status"] != "scoped_internal_risk_acceptance":
|
|
continue
|
|
risk_expires_at = _parse_time(
|
|
item["risk_acceptance"]["expires_at"],
|
|
f"Golden disposition {item['sample_id']} risk acceptance expires_at",
|
|
)
|
|
if approved_at >= risk_expires_at:
|
|
raise PublicationBatchError(
|
|
f"Golden disposition {item['sample_id']} risk acceptance expired before approval"
|
|
)
|
|
if expires_at > risk_expires_at:
|
|
raise PublicationBatchError(
|
|
f"approval validity exceeds Golden disposition {item['sample_id']} risk acceptance"
|
|
)
|
|
|
|
|
|
def build_approval_payload(
|
|
manifest: Any,
|
|
*,
|
|
manifest_raw_sha256: str,
|
|
key_id: str,
|
|
approval_keyring_epoch: int,
|
|
approval_keyring_payload_sha256: str,
|
|
approved_at: datetime,
|
|
expires_at: datetime,
|
|
approver: Any,
|
|
) -> dict[str, Any]:
|
|
"""Build the only approval payload accepted for the exact validated batch."""
|
|
|
|
validated = validate_manifest(manifest)
|
|
normalized_key_id = _require_string(key_id, "approval key_id", max_length=128)
|
|
if not STABLE_ID_RE.fullmatch(normalized_key_id):
|
|
raise PublicationBatchError("approval key_id is invalid")
|
|
if (
|
|
not isinstance(approval_keyring_epoch, int)
|
|
or isinstance(approval_keyring_epoch, bool)
|
|
or approval_keyring_epoch < 1
|
|
):
|
|
raise PublicationBatchError("approval keyring epoch is invalid")
|
|
normalized_keyring_payload_sha256 = _require_sha(
|
|
approval_keyring_payload_sha256,
|
|
"approval keyring payload SHA-256",
|
|
)
|
|
if approved_at.tzinfo is None or approved_at.utcoffset() is None:
|
|
raise PublicationBatchError("approval approved_at must include a timezone")
|
|
if expires_at.tzinfo is None or expires_at.utcoffset() is None:
|
|
raise PublicationBatchError("approval expires_at must include a timezone")
|
|
if expires_at <= approved_at or expires_at - approved_at > MAX_APPROVAL_WINDOW:
|
|
raise PublicationBatchError(
|
|
"approval validity must be greater than zero and no longer than seven days"
|
|
)
|
|
if not validated["publication_control"]["golden_dispositions_complete"]:
|
|
raise PublicationBatchError("approval cannot release a batch with unresolved Golden dispositions")
|
|
_validate_disposition_approval_window(
|
|
validated,
|
|
approved_at=approved_at,
|
|
expires_at=expires_at,
|
|
)
|
|
manifest_raw_sha256 = _require_sha(
|
|
manifest_raw_sha256,
|
|
"publication batch raw SHA-256",
|
|
)
|
|
confirmed_approver = _require_sole_release_reviewer(
|
|
approver,
|
|
"approval approver",
|
|
)
|
|
reviewer_identity = {
|
|
field: confirmed_approver[field]
|
|
for field in RELEASE_REVIEWER_IDENTITY_FIELDS
|
|
}
|
|
review_summary = build_approval_review_summary(
|
|
validated,
|
|
manifest_raw_sha256=manifest_raw_sha256,
|
|
reviewer=reviewer_identity,
|
|
)
|
|
return {
|
|
"schema_version": APPROVAL_PAYLOAD_SCHEMA_VERSION,
|
|
"batch_manifest_raw_sha256": manifest_raw_sha256,
|
|
"batch_manifest_payload_sha256": validated["manifest_payload_sha256"],
|
|
"decision": "approved",
|
|
"approved_at": approved_at.isoformat(timespec="seconds"),
|
|
"expires_at": expires_at.isoformat(timespec="seconds"),
|
|
"key_id": normalized_key_id,
|
|
"approval_keyring_epoch": approval_keyring_epoch,
|
|
"approval_keyring_payload_sha256": normalized_keyring_payload_sha256,
|
|
"approver": confirmed_approver,
|
|
"asset_payload_sha256s": {
|
|
item["template_id"]: item["asset_payload_sha256"]
|
|
for item in validated["asset_batch"]["items"]
|
|
},
|
|
"prompt_module_payload_sha256s": {
|
|
item["key"]: item["module_payload_sha256"]
|
|
for item in validated["prompt_module_batch"]["items"]
|
|
},
|
|
"golden_disposition_inventory_sha256": validated["golden_dispositions"][
|
|
"inventory_sha256"
|
|
],
|
|
"review_summary_sha256": _sha256_json(review_summary),
|
|
}
|
|
|
|
|
|
def _require_sole_release_reviewer(value: Any, label: str) -> dict[str, Any]:
|
|
reviewer = _require_keys(
|
|
value,
|
|
label,
|
|
(*RELEASE_REVIEWER_IDENTITY_FIELDS, "human_confirmed"),
|
|
)
|
|
identity = _require_release_reviewer_identity(
|
|
{field: reviewer[field] for field in RELEASE_REVIEWER_IDENTITY_FIELDS},
|
|
label,
|
|
)
|
|
if reviewer["human_confirmed"] is not True:
|
|
raise PublicationBatchError("AI or automated self-approval is forbidden")
|
|
return {**identity, "human_confirmed": reviewer["human_confirmed"]}
|
|
|
|
|
|
def _public_key_raw(public_key: Ed25519PublicKey) -> bytes:
|
|
return public_key.public_bytes(
|
|
serialization.Encoding.Raw,
|
|
serialization.PublicFormat.Raw,
|
|
)
|
|
|
|
|
|
def _public_key_spki(public_key: Any) -> bytes:
|
|
try:
|
|
return public_key.public_bytes(
|
|
serialization.Encoding.DER,
|
|
serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
)
|
|
except (AttributeError, TypeError, ValueError) as exc:
|
|
raise PublicationBatchError("online signing public key is invalid") from exc
|
|
|
|
|
|
def _read_pinned_spki_public_key(path: Path, *, expected_sha256: str, label: str) -> Any:
|
|
if not HEX64_RE.fullmatch(expected_sha256):
|
|
raise PublicationBatchError(f"{label} SPKI pin is not provisioned in the frozen source")
|
|
raw = secure_read_bytes(path, label, maximum_bytes=64 * 1024)
|
|
try:
|
|
public_key = serialization.load_pem_public_key(raw)
|
|
except (TypeError, ValueError) as exc:
|
|
raise PublicationBatchError(f"{label} is not a valid PEM public key") from exc
|
|
if not isinstance(public_key, (rsa.RSAPublicKey, ec.EllipticCurvePublicKey)):
|
|
raise PublicationBatchError(f"{label} must be an RSA or EC KMS signing key")
|
|
if _sha256_bytes(_public_key_spki(public_key)) != expected_sha256:
|
|
raise PublicationBatchError(f"{label} does not match the frozen SPKI trust anchor")
|
|
return public_key
|
|
|
|
|
|
def _verify_frozen_kms_signature(
|
|
public_key: Any,
|
|
signature_block: Any,
|
|
*,
|
|
domain: bytes,
|
|
payload: Mapping[str, Any],
|
|
label: str,
|
|
include_request_id: bool,
|
|
) -> tuple[dict[str, Any], bytes]:
|
|
"""Verify one KMS digest signature against exact frozen signer metadata."""
|
|
|
|
fields = (
|
|
"provider",
|
|
"key_reference",
|
|
"key_version",
|
|
"algorithm",
|
|
"value_base64",
|
|
) + (("request_id",) if include_request_id else ())
|
|
signature = _require_keys(
|
|
signature_block,
|
|
f"{label} signature",
|
|
fields,
|
|
)
|
|
required_trust = (
|
|
TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER,
|
|
TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE,
|
|
TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION,
|
|
TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM,
|
|
TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256,
|
|
)
|
|
if (
|
|
not all(isinstance(value, str) and value for value in required_trust)
|
|
or not HEX64_RE.fullmatch(
|
|
TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256
|
|
)
|
|
):
|
|
raise PublicationBatchError("online KMS signer trust is not fully provisioned")
|
|
if (
|
|
signature["provider"] != TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER
|
|
or signature["key_reference"]
|
|
!= TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE
|
|
or signature["key_version"] != TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION
|
|
or signature["algorithm"] != TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM
|
|
):
|
|
raise PublicationBatchError(f"{label} KMS signer identity drift")
|
|
if include_request_id:
|
|
_require_string(
|
|
signature["request_id"], f"{label} KMS request_id", max_length=512
|
|
)
|
|
if (
|
|
_sha256_bytes(_public_key_spki(public_key))
|
|
!= TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256
|
|
):
|
|
raise PublicationBatchError(f"{label} signer does not match the frozen SPKI pin")
|
|
try:
|
|
signature_bytes = base64.b64decode(
|
|
_require_string(
|
|
signature["value_base64"], f"{label} signature value", max_length=16384
|
|
),
|
|
validate=True,
|
|
)
|
|
digest = hashlib.sha256(domain + _canonical(payload)).digest()
|
|
algorithm = signature["algorithm"]
|
|
if algorithm == "RSA_PSS_SHA_256" and isinstance(public_key, rsa.RSAPublicKey):
|
|
public_key.verify(
|
|
signature_bytes,
|
|
digest,
|
|
padding.PSS(
|
|
mgf=padding.MGF1(hashes.SHA256()),
|
|
salt_length=hashes.SHA256().digest_size,
|
|
),
|
|
utils.Prehashed(hashes.SHA256()),
|
|
)
|
|
elif algorithm == "RSA_PKCS1_SHA_256" and isinstance(
|
|
public_key, rsa.RSAPublicKey
|
|
):
|
|
public_key.verify(
|
|
signature_bytes,
|
|
digest,
|
|
padding.PKCS1v15(),
|
|
utils.Prehashed(hashes.SHA256()),
|
|
)
|
|
elif algorithm == "ECDSA_SHA_256" and isinstance(
|
|
public_key, ec.EllipticCurvePublicKey
|
|
):
|
|
public_key.verify(
|
|
signature_bytes,
|
|
digest,
|
|
ec.ECDSA(utils.Prehashed(hashes.SHA256())),
|
|
)
|
|
else:
|
|
raise PublicationBatchError(f"{label} KMS algorithm/key type is unsupported")
|
|
except (TypeError, ValueError, InvalidSignature) as exc:
|
|
raise PublicationBatchError(f"{label} KMS signature verification failed") from exc
|
|
return signature, signature_bytes
|
|
|
|
|
|
def _verify_online_kms_signature(
|
|
public_key: Any,
|
|
signature_block: Any,
|
|
*,
|
|
domain: bytes,
|
|
payload: Mapping[str, Any],
|
|
label: str,
|
|
) -> dict[str, Any]:
|
|
signature, _ = _verify_frozen_kms_signature(
|
|
public_key,
|
|
signature_block,
|
|
domain=domain,
|
|
payload=payload,
|
|
label=label,
|
|
include_request_id=True,
|
|
)
|
|
return signature
|
|
|
|
|
|
def _verify_kms_request_id_attestation(
|
|
public_key: Any,
|
|
attestation_value: Any,
|
|
*,
|
|
receipt_payload: Mapping[str, Any],
|
|
primary_signature: Mapping[str, Any],
|
|
attestation_domain: bytes,
|
|
label: str,
|
|
) -> dict[str, Any]:
|
|
attestation = _require_keys(
|
|
attestation_value,
|
|
f"{label} request_id attestation",
|
|
("schema_version", "payload", "signature"),
|
|
)
|
|
if attestation["schema_version"] != KMS_REQUEST_ID_ATTESTATION_SCHEMA_VERSION:
|
|
raise PublicationBatchError(f"{label} request_id attestation schema is unsupported")
|
|
try:
|
|
primary_signature_bytes = base64.b64decode(
|
|
_require_string(
|
|
primary_signature["value_base64"],
|
|
f"{label} primary signature value",
|
|
max_length=16384,
|
|
),
|
|
validate=True,
|
|
)
|
|
except (TypeError, ValueError) as exc:
|
|
raise PublicationBatchError(f"{label} primary signature encoding is invalid") from exc
|
|
expected_payload = {
|
|
"schema_version": KMS_REQUEST_ID_ATTESTATION_PAYLOAD_SCHEMA_VERSION,
|
|
"receipt_payload_sha256": _sha256_json(receipt_payload),
|
|
"primary_signature_sha256": _sha256_bytes(primary_signature_bytes),
|
|
"primary_provider": primary_signature["provider"],
|
|
"primary_key_reference": primary_signature["key_reference"],
|
|
"primary_key_version": primary_signature["key_version"],
|
|
"primary_algorithm": primary_signature["algorithm"],
|
|
"primary_request_id": primary_signature["request_id"],
|
|
}
|
|
payload = _require_keys(
|
|
attestation["payload"],
|
|
f"{label} request_id attestation payload",
|
|
tuple(expected_payload),
|
|
)
|
|
if payload != expected_payload:
|
|
raise PublicationBatchError(f"{label} request_id attestation binding drift")
|
|
_verify_frozen_kms_signature(
|
|
public_key,
|
|
attestation["signature"],
|
|
domain=attestation_domain,
|
|
payload=payload,
|
|
label=f"{label} request_id attestation",
|
|
include_request_id=False,
|
|
)
|
|
return attestation
|
|
|
|
|
|
def _read_pinned_raw_public_key(path: Path, *, expected_sha256: str, label: str) -> Ed25519PublicKey:
|
|
if not HEX64_RE.fullmatch(expected_sha256):
|
|
raise PublicationBatchError(f"{label} pin is not provisioned in the frozen source")
|
|
raw = secure_read_bytes(path, label, maximum_bytes=32)
|
|
if len(raw) != 32 or _sha256_bytes(raw) != expected_sha256:
|
|
raise PublicationBatchError(f"{label} does not match the frozen trust anchor")
|
|
try:
|
|
return Ed25519PublicKey.from_public_bytes(raw)
|
|
except ValueError as exc:
|
|
raise PublicationBatchError(f"{label} is not a valid Ed25519 public key") from exc
|
|
|
|
|
|
def _validate_approval_keyring(
|
|
keyring: Any,
|
|
*,
|
|
root_public_key: Ed25519PublicKey,
|
|
approved_at: datetime,
|
|
) -> list[dict[str, Any]]:
|
|
trusted = _require_keys(
|
|
keyring,
|
|
"approval keyring",
|
|
(
|
|
"schema_version", "root_key_id", "epoch", "issued_at", "expires_at",
|
|
"authorized_batch_manifest_raw_sha256", "keys",
|
|
"keyring_payload_sha256", "root_signature_base64",
|
|
),
|
|
)
|
|
if trusted["schema_version"] != APPROVAL_KEYRING_SCHEMA_VERSION:
|
|
raise PublicationBatchError("approval keyring schema is unsupported")
|
|
unsigned = {
|
|
field: trusted[field]
|
|
for field in (
|
|
"schema_version", "root_key_id", "epoch", "issued_at", "expires_at",
|
|
"authorized_batch_manifest_raw_sha256", "keys",
|
|
)
|
|
}
|
|
_require_sha(
|
|
trusted["authorized_batch_manifest_raw_sha256"],
|
|
"approval keyring authorized batch manifest raw SHA-256",
|
|
)
|
|
payload_sha256 = _require_sha(
|
|
trusted["keyring_payload_sha256"],
|
|
"approval keyring payload SHA-256",
|
|
)
|
|
if _sha256_json(unsigned) != payload_sha256:
|
|
raise PublicationBatchError("approval keyring payload hash drift")
|
|
if not HEX64_RE.fullmatch(TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256):
|
|
raise PublicationBatchError("approval root public key pin is not provisioned")
|
|
if _sha256_bytes(_public_key_raw(root_public_key)) != TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256:
|
|
raise PublicationBatchError("approval root public key does not match the frozen pin")
|
|
if trusted["root_key_id"] != TRUSTED_APPROVAL_ROOT_KEY_ID:
|
|
raise PublicationBatchError("approval keyring uses the wrong root key")
|
|
epoch = trusted["epoch"]
|
|
if (
|
|
not isinstance(epoch, int)
|
|
or isinstance(epoch, bool)
|
|
or epoch < 1
|
|
or not isinstance(FROZEN_APPROVAL_KEYRING_EPOCH, int)
|
|
or isinstance(FROZEN_APPROVAL_KEYRING_EPOCH, bool)
|
|
or FROZEN_APPROVAL_KEYRING_EPOCH < 1
|
|
):
|
|
raise PublicationBatchError("approval keyring epoch policy is not provisioned")
|
|
if epoch != FROZEN_APPROVAL_KEYRING_EPOCH:
|
|
raise PublicationBatchError("approval keyring epoch does not match the frozen candidate")
|
|
issued_at = _parse_time(trusted["issued_at"], "approval keyring issued_at")
|
|
expires_at = _parse_time(trusted["expires_at"], "approval keyring expires_at")
|
|
if expires_at <= issued_at or approved_at < issued_at or approved_at >= expires_at:
|
|
raise PublicationBatchError("approval keyring is not valid at approval time")
|
|
try:
|
|
signature = base64.b64decode(trusted["root_signature_base64"], validate=True)
|
|
root_public_key.verify(
|
|
signature,
|
|
APPROVAL_KEYRING_SIGNATURE_DOMAIN + _canonical(unsigned),
|
|
)
|
|
except (TypeError, ValueError, InvalidSignature) as exc:
|
|
raise PublicationBatchError("approval keyring root signature is invalid") from exc
|
|
keys = trusted["keys"]
|
|
if not isinstance(keys, list) or not keys:
|
|
raise PublicationBatchError("approval keyring keys are invalid")
|
|
root_raw = _public_key_raw(root_public_key)
|
|
validated: list[dict[str, Any]] = []
|
|
seen_ids: set[str] = set()
|
|
seen_public_keys: set[bytes] = set()
|
|
for value in keys:
|
|
key = _require_keys(
|
|
value,
|
|
"approval key",
|
|
(
|
|
"key_id", "algorithm", "public_key_pem", "approver_id", "name",
|
|
"not_before", "not_after", "revoked",
|
|
),
|
|
)
|
|
key_id = _require_string(key["key_id"], "approval key_id", max_length=128)
|
|
if key_id in seen_ids:
|
|
raise PublicationBatchError("approval keyring repeats a key_id")
|
|
seen_ids.add(key_id)
|
|
if key["algorithm"] != "Ed25519":
|
|
raise PublicationBatchError("approval key uses an unsupported algorithm")
|
|
if key["revoked"] is not False:
|
|
raise PublicationBatchError("revoked approval keys are forbidden")
|
|
key_reviewer = {
|
|
field: key[field]
|
|
for field in ("approver_id", "name")
|
|
}
|
|
if key_reviewer != SOLE_RELEASE_REVIEWER:
|
|
raise PublicationBatchError("approval key must belong to the configured sole release reviewer")
|
|
public_pem = _require_string(key["public_key_pem"], "approval public key", max_length=16384)
|
|
try:
|
|
public_key = serialization.load_pem_public_key(public_pem.encode("ascii"))
|
|
except (UnicodeError, ValueError) as exc:
|
|
raise PublicationBatchError("approval public key is invalid") from exc
|
|
if not isinstance(public_key, Ed25519PublicKey):
|
|
raise PublicationBatchError("approval public key must be Ed25519")
|
|
public_raw = _public_key_raw(public_key)
|
|
if public_raw == root_raw:
|
|
raise PublicationBatchError("approval key must differ from the offline root key")
|
|
if public_raw in seen_public_keys:
|
|
raise PublicationBatchError("approval keyring reuses an Ed25519 public key")
|
|
seen_public_keys.add(public_raw)
|
|
not_before = _parse_time(key["not_before"], "approval key not_before")
|
|
not_after = _parse_time(key["not_after"], "approval key not_after")
|
|
if not_after <= not_before:
|
|
raise PublicationBatchError("approval key validity window is invalid")
|
|
validated.append(
|
|
{
|
|
**key,
|
|
"public_key": public_key,
|
|
"public_key_raw": public_raw,
|
|
"not_before_parsed": not_before,
|
|
"not_after_parsed": not_after,
|
|
}
|
|
)
|
|
return validated
|
|
|
|
|
|
def validate_approval_receipt(
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
receipt: Any,
|
|
keyring: Any,
|
|
root_public_key: Ed25519PublicKey,
|
|
) -> dict[str, Any]:
|
|
envelope = _require_keys(
|
|
receipt,
|
|
"approval receipt",
|
|
("schema_version", "approval_id", "payload", "signature"),
|
|
)
|
|
if envelope["schema_version"] != APPROVAL_SCHEMA_VERSION:
|
|
raise PublicationBatchError("approval receipt schema is unsupported")
|
|
payload = _require_keys(
|
|
envelope["payload"],
|
|
"approval payload",
|
|
(
|
|
"schema_version", "batch_manifest_raw_sha256", "batch_manifest_payload_sha256",
|
|
"decision", "approved_at", "expires_at", "key_id",
|
|
"approval_keyring_epoch", "approval_keyring_payload_sha256", "approver",
|
|
"asset_payload_sha256s",
|
|
"prompt_module_payload_sha256s", "golden_disposition_inventory_sha256",
|
|
"review_summary_sha256",
|
|
),
|
|
)
|
|
if payload["schema_version"] != APPROVAL_PAYLOAD_SCHEMA_VERSION or payload["decision"] != "approved":
|
|
raise PublicationBatchError("approval receipt is not an approved RC-06 batch receipt")
|
|
if payload["batch_manifest_payload_sha256"] != manifest["manifest_payload_sha256"]:
|
|
raise PublicationBatchError("approval receipt is not bound to this batch")
|
|
normalized_manifest_raw_sha256 = _require_sha(
|
|
manifest_raw_sha256,
|
|
"publication batch raw SHA-256",
|
|
)
|
|
if payload["batch_manifest_raw_sha256"] != normalized_manifest_raw_sha256:
|
|
raise PublicationBatchError("approval receipt is not bound to the exact manifest file")
|
|
approved_at = _parse_time(payload["approved_at"], "approval approved_at")
|
|
expires_at = _parse_time(payload["expires_at"], "approval expires_at")
|
|
if expires_at <= approved_at or expires_at - approved_at > MAX_APPROVAL_WINDOW:
|
|
raise PublicationBatchError("approval validity must be greater than zero and no longer than seven days")
|
|
approver = _require_sole_release_reviewer(payload["approver"], "approval approver")
|
|
expected_assets = {
|
|
item["template_id"]: item["asset_payload_sha256"]
|
|
for item in manifest["asset_batch"]["items"]
|
|
}
|
|
expected_modules = {
|
|
item["key"]: item["module_payload_sha256"]
|
|
for item in manifest["prompt_module_batch"]["items"]
|
|
}
|
|
if payload["asset_payload_sha256s"] != expected_assets:
|
|
raise PublicationBatchError("approval receipt asset identities drift")
|
|
if payload["prompt_module_payload_sha256s"] != expected_modules:
|
|
raise PublicationBatchError("approval receipt Prompt identities drift")
|
|
if payload["golden_disposition_inventory_sha256"] != manifest["golden_dispositions"]["inventory_sha256"]:
|
|
raise PublicationBatchError("approval receipt Golden disposition identity drift")
|
|
review_summary_sha256 = _require_sha(
|
|
payload["review_summary_sha256"],
|
|
"approval review summary SHA-256",
|
|
)
|
|
expected_review_summary_sha256 = _sha256_json(
|
|
build_approval_review_summary(
|
|
manifest,
|
|
manifest_raw_sha256=normalized_manifest_raw_sha256,
|
|
reviewer={
|
|
field: approver[field]
|
|
for field in RELEASE_REVIEWER_IDENTITY_FIELDS
|
|
},
|
|
)
|
|
)
|
|
if review_summary_sha256 != expected_review_summary_sha256:
|
|
raise PublicationBatchError("approval receipt does not match the exact review summary")
|
|
if not manifest["publication_control"]["golden_dispositions_complete"]:
|
|
raise PublicationBatchError("approval cannot release a batch with unresolved Golden dispositions")
|
|
_validate_disposition_approval_window(
|
|
manifest,
|
|
approved_at=approved_at,
|
|
expires_at=expires_at,
|
|
)
|
|
|
|
trusted_keys = _validate_approval_keyring(
|
|
keyring,
|
|
root_public_key=root_public_key,
|
|
approved_at=approved_at,
|
|
)
|
|
if payload["approval_keyring_epoch"] != keyring["epoch"]:
|
|
raise PublicationBatchError("approval receipt keyring epoch drift")
|
|
if _require_sha(
|
|
payload["approval_keyring_payload_sha256"],
|
|
"approval receipt keyring payload SHA-256",
|
|
) != keyring["keyring_payload_sha256"]:
|
|
raise PublicationBatchError("approval receipt keyring payload identity drift")
|
|
if keyring["authorized_batch_manifest_raw_sha256"] != normalized_manifest_raw_sha256:
|
|
raise PublicationBatchError("approval keyring does not authorize the exact batch manifest")
|
|
keyring_expires_at = _parse_time(keyring["expires_at"], "approval keyring expires_at")
|
|
if expires_at > keyring_expires_at:
|
|
raise PublicationBatchError("approval validity exceeds the trusted keyring window")
|
|
key_id = _require_string(payload["key_id"], "approval key_id", max_length=128)
|
|
matches = [
|
|
item
|
|
for item in trusted_keys
|
|
if item["key_id"] == key_id
|
|
]
|
|
if len(matches) != 1:
|
|
raise PublicationBatchError("approval key must resolve uniquely")
|
|
key = matches[0]
|
|
if approved_at < key["not_before_parsed"] or expires_at > key["not_after_parsed"]:
|
|
raise PublicationBatchError("approval validity is outside the trusted key window")
|
|
public_key = key["public_key"]
|
|
try:
|
|
signature = base64.b64decode(
|
|
_require_string(envelope["signature"], "approval signature", max_length=1024),
|
|
validate=True,
|
|
)
|
|
public_key.verify(signature, APPROVAL_SIGNATURE_DOMAIN + _canonical(payload))
|
|
except Exception as exc:
|
|
raise PublicationBatchError("approval signature verification failed") from exc
|
|
expected_id = f"rc06-batch-approval-{_sha256_bytes(_canonical(payload) + signature)[:24]}"
|
|
if envelope["approval_id"] != expected_id:
|
|
raise PublicationBatchError("approval_id does not match its signed material")
|
|
_assert_no_secret_material(envelope, "approval receipt")
|
|
return envelope
|
|
|
|
|
|
def _expected_apply_item_results(manifest: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
results = [
|
|
{
|
|
"kind": "asset",
|
|
"item_id": item["template_id"],
|
|
"expected_content_sha256": item["content_sha256"],
|
|
"online_content_sha256": item["content_sha256"],
|
|
"status": "applied_and_verified",
|
|
}
|
|
for item in manifest["asset_batch"]["items"]
|
|
] + [
|
|
{
|
|
"kind": "prompt_module",
|
|
"item_id": item["key"],
|
|
"expected_content_sha256": item["content_sha256"],
|
|
"online_content_sha256": item["content_sha256"],
|
|
"status": "applied_and_verified",
|
|
}
|
|
for item in manifest["prompt_module_batch"]["items"]
|
|
]
|
|
results.sort(key=lambda item: (item["kind"], item["item_id"]))
|
|
return results
|
|
|
|
|
|
def expected_runtime_snapshot_results(manifest: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
"""Project the fourteen executable Prompt identities into read-back evidence."""
|
|
|
|
dependencies = manifest["runtime_prompt_dependencies"]
|
|
if dependencies.get("publication_item_count") != 74 or dependencies.get("count") != 14:
|
|
raise PublicationBatchError(
|
|
"manifest must distinguish 74 publication items from 14 runtime snapshots"
|
|
)
|
|
results = [
|
|
{
|
|
"kind": item["kind"],
|
|
"runtime_key": item["runtime_key"],
|
|
"version": item["version"],
|
|
"expected_content_sha256": item["content_sha256"],
|
|
"online_content_sha256": item["content_sha256"],
|
|
"status": "published_and_resolved",
|
|
"publication_dependency": item["publication_dependency"],
|
|
"publication_module_dependencies": item["publication_module_dependencies"],
|
|
}
|
|
for item in dependencies["items"]
|
|
]
|
|
if len(results) != EXPECTED_RUNTIME_PROMPT_COUNT or len(
|
|
{item["runtime_key"] for item in results}
|
|
) != EXPECTED_RUNTIME_PROMPT_COUNT:
|
|
raise PublicationBatchError("runtime snapshot inventory must contain 14 identities")
|
|
return results
|
|
|
|
|
|
def publication_idempotency_key(
|
|
manifest_raw_sha256: str,
|
|
approval_raw_sha256: str,
|
|
legal_review_binding_sha256: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> str:
|
|
identity = {
|
|
"operation": APPLY_OPERATION,
|
|
"manifest_raw_sha256": _require_sha(
|
|
manifest_raw_sha256, "publication manifest raw SHA-256"
|
|
),
|
|
"approval_raw_sha256": _require_sha(
|
|
approval_raw_sha256, "publication approval raw SHA-256"
|
|
),
|
|
"legal_review_binding_sha256": _require_sha(
|
|
legal_review_binding_sha256, "legal review binding SHA-256"
|
|
),
|
|
"environment_id": _require_string(
|
|
environment_id, "publication environment_id", max_length=256
|
|
),
|
|
"tenant_id": _require_string(tenant_id, "publication tenant_id", max_length=64),
|
|
}
|
|
return _sha256_json(identity)
|
|
|
|
|
|
def publication_transaction_id(idempotency_key: str) -> str:
|
|
"""Return a stable transaction identifier that fits the 64-character DB contract."""
|
|
|
|
normalized = _require_sha(idempotency_key, "publication idempotency key")
|
|
return "rc06-" + normalized[:59]
|
|
|
|
|
|
def validate_scene_legal_review_keyring(
|
|
keyring: Any,
|
|
keyring_raw_sha256: str,
|
|
*,
|
|
expected_sha256: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Require the exact frozen canonical legal-review trust anchor.
|
|
|
|
The legal decision verifier checks the selected key's identity and validity.
|
|
This additional check prevents a caller from supplying an arbitrary self-signed
|
|
keyring to that verifier.
|
|
"""
|
|
|
|
trust_pin = (
|
|
TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256
|
|
if expected_sha256 is None
|
|
else expected_sha256
|
|
)
|
|
if not HEX64_RE.fullmatch(trust_pin):
|
|
raise PublicationBatchError("scene legal review keyring trust is not provisioned")
|
|
normalized_raw_sha = _require_sha(
|
|
keyring_raw_sha256, "scene legal review keyring raw SHA-256"
|
|
)
|
|
if (
|
|
normalized_raw_sha != trust_pin
|
|
or _sha256_json(keyring) != trust_pin
|
|
):
|
|
raise PublicationBatchError(
|
|
"scene legal review keyring does not match the frozen canonical pin"
|
|
)
|
|
row = _require_keys(
|
|
keyring,
|
|
"scene legal review keyring",
|
|
("schema_version", "keys"),
|
|
)
|
|
if row["schema_version"] != "yuqei.scene-source-legal-review-keyring.v1":
|
|
raise PublicationBatchError("scene legal review keyring schema is unsupported")
|
|
keys = row["keys"]
|
|
if not isinstance(keys, list) or not keys:
|
|
raise PublicationBatchError("scene legal review keyring must contain trusted keys")
|
|
seen: set[str] = set()
|
|
for index, value in enumerate(keys):
|
|
key = _require_keys(
|
|
value,
|
|
f"scene legal review key {index}",
|
|
(
|
|
"key_id",
|
|
"status",
|
|
"revoked_at",
|
|
"valid_from",
|
|
"valid_until",
|
|
"reviewer_name",
|
|
"reviewer_organization",
|
|
"reviewer_qualification",
|
|
"credential_reference",
|
|
"public_key_raw_base64",
|
|
),
|
|
)
|
|
key_id = _require_string(key["key_id"], "scene legal review key_id", max_length=128)
|
|
if key_id in seen:
|
|
raise PublicationBatchError("scene legal review keyring contains duplicate key ids")
|
|
seen.add(key_id)
|
|
if key["status"] != "trusted" or key["revoked_at"] is not None:
|
|
raise PublicationBatchError("scene legal review keyring contains an inactive key")
|
|
valid_from = _parse_time(key["valid_from"], "scene legal review key valid_from")
|
|
valid_until = _parse_time(key["valid_until"], "scene legal review key valid_until")
|
|
if valid_until <= valid_from:
|
|
raise PublicationBatchError("scene legal review key validity window is invalid")
|
|
for field in (
|
|
"reviewer_name",
|
|
"reviewer_organization",
|
|
"reviewer_qualification",
|
|
"credential_reference",
|
|
):
|
|
_require_string(key[field], f"scene legal review key {field}", max_length=512)
|
|
try:
|
|
public_raw = base64.b64decode(
|
|
_require_string(
|
|
key["public_key_raw_base64"],
|
|
"scene legal review public key",
|
|
max_length=128,
|
|
),
|
|
validate=True,
|
|
)
|
|
Ed25519PublicKey.from_public_bytes(public_raw)
|
|
except (TypeError, ValueError) as exc:
|
|
raise PublicationBatchError("scene legal review public key is invalid") from exc
|
|
return row
|
|
|
|
|
|
def _load_scene_legal_review_module() -> Any:
|
|
path = Path(__file__).resolve().with_name("build-scene-source-legal-review.py")
|
|
if not path.is_file():
|
|
raise PublicationBatchError("scene legal review validator is absent")
|
|
spec = importlib.util.spec_from_file_location("rc06_scene_source_legal_review", path)
|
|
if spec is None or spec.loader is None:
|
|
raise PublicationBatchError("cannot load scene legal review validator")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def validate_scene_legal_review_evidence(
|
|
repo: Path,
|
|
manifest: Mapping[str, Any],
|
|
approval: Mapping[str, Any],
|
|
request: Any,
|
|
decision: Any,
|
|
keyring: Any,
|
|
keyring_raw_sha256: str,
|
|
*,
|
|
now: datetime,
|
|
validator: Any | None = None,
|
|
) -> tuple[dict[str, Any], str]:
|
|
"""Validate the qualified-human 30-scene/43-occurrence proof and trust pin."""
|
|
|
|
if not isinstance(request, dict) or not isinstance(decision, dict):
|
|
raise PublicationBatchError("scene legal review request and decision are required")
|
|
validate_scene_legal_review_keyring(keyring, keyring_raw_sha256)
|
|
if request.get("candidate_commit") != manifest["candidate"]["commit"]:
|
|
raise PublicationBatchError("scene legal review candidate does not match the batch")
|
|
scope = request.get("scope")
|
|
if not isinstance(scope, dict) or scope.get("scene_count") != 30 or scope.get(
|
|
"reference_occurrence_count"
|
|
) != 43:
|
|
raise PublicationBatchError("scene legal review must cover 30 scenes and 43 references")
|
|
request_sha256 = request.get("request_sha256")
|
|
if not isinstance(request_sha256, str) or not HEX64_RE.fullmatch(request_sha256):
|
|
raise PublicationBatchError("scene legal review request_sha256 is invalid")
|
|
if decision.get("request_sha256") != request_sha256:
|
|
raise PublicationBatchError("scene legal review decision is not bound to its request")
|
|
verifier = validator or _load_scene_legal_review_module()
|
|
try:
|
|
verified_request = verifier.verify_request(request, repo)
|
|
summary = verifier.verify_signed_decision(
|
|
verified_request,
|
|
decision,
|
|
keyring,
|
|
checked_at=now.astimezone(UTC).isoformat(timespec="seconds"),
|
|
)
|
|
except Exception as exc:
|
|
raise PublicationBatchError(f"scene legal review validation failed: {exc}") from exc
|
|
if (
|
|
summary.get("status") != "verified_qualified_human_review"
|
|
or summary.get("request_sha256") != request_sha256
|
|
or summary.get("reviewed_occurrence_count") != 43
|
|
or summary.get("all_current_effectiveness_verified") is not True
|
|
or summary.get("all_transaction_applicability_verified") is not True
|
|
or summary.get("authorizes_publication_by_itself") is not False
|
|
):
|
|
raise PublicationBatchError("scene legal review verification summary is incomplete")
|
|
approval_expires_at = _parse_time(approval["payload"]["expires_at"], "approval expires_at")
|
|
legal_expires_at = _parse_time(summary.get("expires_at"), "legal review expires_at")
|
|
if legal_expires_at > approval_expires_at:
|
|
raise PublicationBatchError("scene legal review validity exceeds the approval window")
|
|
binding = {
|
|
"request_sha256": request_sha256,
|
|
"decision_sha256": _sha256_json(decision),
|
|
"keyring_raw_sha256": keyring_raw_sha256,
|
|
}
|
|
return summary, _sha256_json(binding)
|
|
|
|
|
|
def validate_scene_legal_review_evidence_online(
|
|
manifest: Mapping[str, Any],
|
|
approval: Mapping[str, Any],
|
|
request: Any,
|
|
decision: Any,
|
|
keyring: Any,
|
|
keyring_raw_sha256: str,
|
|
*,
|
|
now: datetime,
|
|
validator: Any | None = None,
|
|
) -> tuple[dict[str, Any], str]:
|
|
"""Verify frozen legal evidence without Git/worktree access.
|
|
|
|
The offline authority must still call ``validate_scene_legal_review_evidence``
|
|
so the request is rebuilt from the immutable Git candidate. This narrower
|
|
verifier is for the production facade, whose image has no ``.git``: it
|
|
verifies the request's embedded hash/30-by-43 structure, the frozen keyring,
|
|
and the qualified-reviewer signature and validity window.
|
|
"""
|
|
|
|
validate_scene_legal_review_keyring(keyring, keyring_raw_sha256)
|
|
row = _require_keys(
|
|
request,
|
|
"scene legal review request",
|
|
(
|
|
"schema_version",
|
|
"status",
|
|
"candidate_commit",
|
|
"created_at",
|
|
"scope",
|
|
"source_files",
|
|
"occurrences",
|
|
"guardrails",
|
|
"request_sha256",
|
|
),
|
|
)
|
|
request_sha256 = _require_sha(
|
|
row["request_sha256"], "scene legal review request_sha256"
|
|
)
|
|
if _sha256_json({key: value for key, value in row.items() if key != "request_sha256"}) != request_sha256:
|
|
raise PublicationBatchError("scene legal review request embedded hash drift")
|
|
if (
|
|
row["schema_version"] != "yuqei.scene-source-legal-review-request.v1"
|
|
or row["status"] != "pending_qualified_human_review"
|
|
or row["candidate_commit"] != manifest["candidate"]["commit"]
|
|
):
|
|
raise PublicationBatchError("scene legal review request identity drift")
|
|
_parse_time(row["created_at"], "scene legal review request created_at")
|
|
scope = _require_keys(
|
|
row["scope"],
|
|
"scene legal review scope",
|
|
(
|
|
"scene_count",
|
|
"reference_occurrence_count",
|
|
"distinct_source_record_count",
|
|
"distinct_url_count",
|
|
),
|
|
)
|
|
if scope["scene_count"] != 30 or scope["reference_occurrence_count"] != 43:
|
|
raise PublicationBatchError("scene legal review must cover 30 scenes and 43 references")
|
|
occurrences = row["occurrences"]
|
|
if not isinstance(occurrences, list) or len(occurrences) != 43:
|
|
raise PublicationBatchError("scene legal review occurrence inventory is incomplete")
|
|
occurrence_ids: list[str] = []
|
|
for index, occurrence in enumerate(occurrences):
|
|
if not isinstance(occurrence, dict):
|
|
raise PublicationBatchError(f"scene legal review occurrence {index} is invalid")
|
|
occurrence_id = _require_string(
|
|
occurrence.get("occurrence_id"),
|
|
f"scene legal review occurrence {index} id",
|
|
max_length=512,
|
|
)
|
|
_require_sha(
|
|
occurrence.get("source_record_sha256"),
|
|
f"scene legal review occurrence {index} source record SHA-256",
|
|
)
|
|
occurrence_ids.append(occurrence_id)
|
|
if occurrence_ids != sorted(occurrence_ids) or len(set(occurrence_ids)) != 43:
|
|
raise PublicationBatchError("scene legal review occurrence identities drift")
|
|
if row["guardrails"] != {
|
|
"not_legal_approval": True,
|
|
"authorizes_publication": False,
|
|
"ai_must_not_supply_human_decisions": True,
|
|
"all_occurrences_require_qualified_human_review": True,
|
|
}:
|
|
raise PublicationBatchError("scene legal review guardrails drift")
|
|
if not isinstance(row["source_files"], list) or not row["source_files"]:
|
|
raise PublicationBatchError("scene legal review source file inventory is empty")
|
|
verifier = validator or _load_scene_legal_review_module()
|
|
try:
|
|
summary = verifier.verify_signed_decision(
|
|
row,
|
|
decision,
|
|
keyring,
|
|
checked_at=now.astimezone(UTC).isoformat(timespec="seconds"),
|
|
)
|
|
except Exception as exc:
|
|
raise PublicationBatchError(f"scene legal review validation failed: {exc}") from exc
|
|
if (
|
|
summary.get("status") != "verified_qualified_human_review"
|
|
or summary.get("request_sha256") != request_sha256
|
|
or summary.get("reviewed_occurrence_count") != 43
|
|
or summary.get("all_current_effectiveness_verified") is not True
|
|
or summary.get("all_transaction_applicability_verified") is not True
|
|
or summary.get("authorizes_publication_by_itself") is not False
|
|
):
|
|
raise PublicationBatchError("scene legal review verification summary is incomplete")
|
|
legal_expires_at = _parse_time(summary.get("expires_at"), "legal review expires_at")
|
|
if legal_expires_at > _parse_time(
|
|
approval["payload"]["expires_at"], "approval expires_at"
|
|
):
|
|
raise PublicationBatchError("scene legal review validity exceeds the approval window")
|
|
binding = {
|
|
"request_sha256": request_sha256,
|
|
"decision_sha256": _sha256_json(decision),
|
|
"keyring_raw_sha256": keyring_raw_sha256,
|
|
}
|
|
return summary, _sha256_json(binding)
|
|
|
|
|
|
def validate_apply_challenge(
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval_raw_sha256: str,
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
challenge: Any,
|
|
*,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> dict[str, Any]:
|
|
if not HEX64_RE.fullmatch(TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256):
|
|
raise PublicationBatchError("apply service public key pin is not provisioned")
|
|
apply_public_raw = _public_key_raw(apply_service_public_key)
|
|
if _sha256_bytes(apply_public_raw) != TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256:
|
|
raise PublicationBatchError("apply challenge key does not match the frozen pin")
|
|
envelope = _require_keys(
|
|
challenge,
|
|
"apply challenge",
|
|
("schema_version", "challenge_id", "payload", "signature"),
|
|
)
|
|
if envelope["schema_version"] != APPLY_CHALLENGE_SCHEMA_VERSION:
|
|
raise PublicationBatchError("apply challenge schema is unsupported")
|
|
payload = _require_keys(
|
|
envelope["payload"],
|
|
"apply challenge payload",
|
|
(
|
|
"schema_version",
|
|
"nonce_base64",
|
|
"candidate_commit",
|
|
"candidate_tag",
|
|
"batch_manifest_raw_sha256",
|
|
"batch_manifest_payload_sha256",
|
|
"approval_receipt_sha256",
|
|
"legal_source_review_binding_sha256",
|
|
"legal_source_review_expires_at",
|
|
"operation",
|
|
"environment_id",
|
|
"tenant_id",
|
|
"idempotency_key",
|
|
"transaction_id",
|
|
"actor",
|
|
"issued_at",
|
|
"expires_at",
|
|
),
|
|
)
|
|
if (
|
|
payload["schema_version"] != APPLY_CHALLENGE_PAYLOAD_SCHEMA_VERSION
|
|
or payload["candidate_commit"] != manifest["candidate"]["commit"]
|
|
or payload["candidate_tag"] != manifest["candidate"]["tag"]
|
|
or payload["batch_manifest_payload_sha256"]
|
|
!= manifest["manifest_payload_sha256"]
|
|
or payload["approval_receipt_sha256"]
|
|
!= _require_sha(approval_raw_sha256, "approval raw SHA-256")
|
|
or payload["legal_source_review_binding_sha256"]
|
|
!= _require_sha(
|
|
legal_review_binding_sha256,
|
|
"expected legal review binding SHA-256",
|
|
)
|
|
or payload["legal_source_review_expires_at"] != legal_review_expires_at
|
|
or payload["operation"] != APPLY_OPERATION
|
|
):
|
|
raise PublicationBatchError("apply challenge candidate or operation binding drift")
|
|
if payload["batch_manifest_raw_sha256"] != _require_sha(
|
|
manifest_raw_sha256, "apply challenge manifest raw SHA-256"
|
|
):
|
|
raise PublicationBatchError("apply challenge manifest raw binding drift")
|
|
normalized_environment_id = _require_string(
|
|
payload["environment_id"], "apply challenge environment_id", max_length=256
|
|
)
|
|
normalized_expected_environment = _require_string(
|
|
environment_id, "expected apply challenge environment_id", max_length=256
|
|
)
|
|
normalized_tenant_id = _require_string(
|
|
payload["tenant_id"], "apply challenge tenant_id", max_length=64
|
|
)
|
|
normalized_expected_tenant = _require_string(
|
|
tenant_id, "expected apply challenge tenant_id", max_length=64
|
|
)
|
|
if (
|
|
normalized_environment_id != payload["environment_id"]
|
|
or normalized_environment_id != normalized_expected_environment
|
|
or normalized_tenant_id != payload["tenant_id"]
|
|
or normalized_tenant_id != normalized_expected_tenant
|
|
):
|
|
raise PublicationBatchError("apply challenge environment_id must be canonical")
|
|
expected_idempotency_key = publication_idempotency_key(
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
normalized_environment_id,
|
|
normalized_tenant_id,
|
|
)
|
|
if payload["idempotency_key"] != expected_idempotency_key:
|
|
raise PublicationBatchError("apply challenge idempotency binding drift")
|
|
if payload["transaction_id"] != publication_transaction_id(expected_idempotency_key):
|
|
raise PublicationBatchError("apply challenge transaction binding drift")
|
|
actor = _require_keys(payload["actor"], "apply challenge actor", ("type", "component"))
|
|
if actor != {
|
|
"type": "system",
|
|
"component": TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT,
|
|
}:
|
|
raise PublicationBatchError("apply challenge actor is not the trusted online service")
|
|
issued_at = _parse_time(payload["issued_at"], "apply challenge issued_at")
|
|
expires_at = _parse_time(payload["expires_at"], "apply challenge expires_at")
|
|
legal_expires_at = _parse_time(
|
|
payload["legal_source_review_expires_at"],
|
|
"apply challenge legal review expires_at",
|
|
)
|
|
if (
|
|
expires_at <= issued_at
|
|
or expires_at - issued_at > MAX_APPLY_CHALLENGE_TTL
|
|
or expires_at > legal_expires_at
|
|
):
|
|
raise PublicationBatchError("apply challenge TTL is invalid")
|
|
try:
|
|
nonce = base64.b64decode(
|
|
_require_string(
|
|
payload["nonce_base64"], "apply challenge nonce", max_length=128
|
|
),
|
|
validate=True,
|
|
)
|
|
signature = base64.b64decode(
|
|
_require_string(
|
|
envelope["signature"], "apply challenge signature", max_length=1024
|
|
),
|
|
validate=True,
|
|
)
|
|
if len(nonce) != 32:
|
|
raise ValueError("challenge nonce must contain 32 bytes")
|
|
apply_service_public_key.verify(
|
|
signature,
|
|
APPLY_CHALLENGE_SIGNATURE_DOMAIN + _canonical(payload),
|
|
)
|
|
except (TypeError, ValueError, InvalidSignature) as exc:
|
|
raise PublicationBatchError("apply challenge signature verification failed") from exc
|
|
expected_id = (
|
|
"rc06-apply-challenge-"
|
|
+ _sha256_bytes(_canonical(payload) + signature)[:24]
|
|
)
|
|
if envelope["challenge_id"] != expected_id:
|
|
raise PublicationBatchError("apply challenge_id does not match its signed material")
|
|
_assert_no_secret_material(envelope, "apply challenge")
|
|
return envelope
|
|
|
|
|
|
def validate_online_transaction_receipt(
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval_raw_sha256: str,
|
|
approval: Mapping[str, Any],
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
online_service_public_key: Any,
|
|
receipt: Any,
|
|
*,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> dict[str, Any]:
|
|
"""Verify server-originated database transaction evidence.
|
|
|
|
The public-key hash and environment allowlist are frozen source values.
|
|
The caller therefore cannot turn a locally fabricated payload into online
|
|
evidence merely by supplying another key or environment identifier.
|
|
"""
|
|
|
|
if (
|
|
not TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS
|
|
or len(set(TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS))
|
|
!= len(TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS)
|
|
or any(
|
|
not isinstance(value, str) or not value.strip() or value != value.strip()
|
|
for value in TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS
|
|
)
|
|
):
|
|
raise PublicationBatchError(
|
|
"online publication environment allowlist is not provisioned"
|
|
)
|
|
if not HEX64_RE.fullmatch(TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256):
|
|
raise PublicationBatchError(
|
|
"online publication service SPKI pin is not provisioned"
|
|
)
|
|
online_public_spki = _public_key_spki(online_service_public_key)
|
|
if (
|
|
_sha256_bytes(online_public_spki)
|
|
!= TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256
|
|
):
|
|
raise PublicationBatchError(
|
|
"online publication service public key does not match the frozen SPKI pin"
|
|
)
|
|
envelope = _require_keys(
|
|
receipt,
|
|
"online transaction receipt",
|
|
(
|
|
"schema_version",
|
|
"receipt_id",
|
|
"payload",
|
|
"signature",
|
|
"request_id_attestation",
|
|
),
|
|
)
|
|
if envelope["schema_version"] != ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION:
|
|
raise PublicationBatchError("online transaction receipt schema is unsupported")
|
|
payload = _require_keys(
|
|
envelope["payload"],
|
|
"online transaction receipt payload",
|
|
(
|
|
"schema_version",
|
|
"candidate_commit",
|
|
"candidate_tag",
|
|
"batch_manifest_raw_sha256",
|
|
"batch_manifest_payload_sha256",
|
|
"approval_receipt_sha256",
|
|
"legal_source_review_binding_sha256",
|
|
"environment_id",
|
|
"tenant_id",
|
|
"idempotency_key",
|
|
"transaction_id",
|
|
"transaction_mode",
|
|
"cross_database_atomicity",
|
|
"coordinator_deployment",
|
|
"participant_evidence",
|
|
"committed_at",
|
|
"authority_challenge",
|
|
"generated_by",
|
|
"item_results",
|
|
"write_count",
|
|
"status",
|
|
),
|
|
)
|
|
if (
|
|
payload["schema_version"] != ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION
|
|
or payload["candidate_commit"] != manifest["candidate"]["commit"]
|
|
or payload["candidate_tag"] != manifest["candidate"]["tag"]
|
|
or payload["status"] != "committed_and_verified"
|
|
or payload["transaction_mode"] != TRANSACTION_MODE
|
|
or payload["cross_database_atomicity"] is not False
|
|
):
|
|
raise PublicationBatchError("online transaction receipt identity is invalid")
|
|
if payload["batch_manifest_raw_sha256"] != _require_sha(
|
|
manifest_raw_sha256, "online transaction manifest raw SHA-256"
|
|
):
|
|
raise PublicationBatchError("online transaction raw manifest binding drift")
|
|
if payload["batch_manifest_payload_sha256"] != manifest["manifest_payload_sha256"]:
|
|
raise PublicationBatchError("online transaction receipt is not bound to this batch")
|
|
if payload["approval_receipt_sha256"] != _require_sha(
|
|
approval_raw_sha256, "approval raw SHA-256"
|
|
):
|
|
raise PublicationBatchError(
|
|
"online transaction receipt is not bound to the approval receipt"
|
|
)
|
|
if payload["legal_source_review_binding_sha256"] != _require_sha(
|
|
legal_review_binding_sha256, "expected legal review binding SHA-256"
|
|
):
|
|
raise PublicationBatchError(
|
|
"online transaction receipt is not bound to the legal review"
|
|
)
|
|
normalized_environment_id = _require_string(
|
|
payload["environment_id"], "online transaction environment_id", max_length=256
|
|
)
|
|
normalized_expected_environment = _require_string(
|
|
environment_id, "expected online transaction environment_id", max_length=256
|
|
)
|
|
normalized_tenant_id = _require_string(
|
|
payload["tenant_id"], "online transaction tenant_id", max_length=64
|
|
)
|
|
normalized_expected_tenant = _require_string(
|
|
tenant_id, "expected online transaction tenant_id", max_length=64
|
|
)
|
|
if (
|
|
normalized_environment_id != payload["environment_id"]
|
|
or normalized_environment_id != normalized_expected_environment
|
|
or normalized_tenant_id != payload["tenant_id"]
|
|
or normalized_tenant_id != normalized_expected_tenant
|
|
):
|
|
raise PublicationBatchError(
|
|
"online transaction environment or tenant binding drift"
|
|
)
|
|
if normalized_environment_id not in TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS:
|
|
raise PublicationBatchError(
|
|
"online transaction environment is not in the frozen allowlist"
|
|
)
|
|
expected_idempotency_key = publication_idempotency_key(
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
normalized_environment_id,
|
|
normalized_tenant_id,
|
|
)
|
|
if payload["idempotency_key"] != expected_idempotency_key:
|
|
raise PublicationBatchError("online transaction idempotency binding drift")
|
|
normalized_transaction_id = _require_string(
|
|
payload["transaction_id"], "online transaction_id", max_length=256
|
|
)
|
|
if (
|
|
normalized_transaction_id != payload["transaction_id"]
|
|
or normalized_transaction_id
|
|
!= publication_transaction_id(expected_idempotency_key)
|
|
):
|
|
raise PublicationBatchError("online transaction identifier binding drift")
|
|
committed_at = _parse_time(payload["committed_at"], "online committed_at")
|
|
approval_payload = approval["payload"]
|
|
if committed_at < _parse_time(
|
|
approval_payload["approved_at"], "approval approved_at"
|
|
) or committed_at >= _parse_time(
|
|
approval_payload["expires_at"], "approval expires_at"
|
|
):
|
|
raise PublicationBatchError(
|
|
"online transaction receipt was generated outside the approval window"
|
|
)
|
|
challenge = validate_apply_challenge(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
apply_service_public_key,
|
|
payload["authority_challenge"],
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=normalized_environment_id,
|
|
tenant_id=normalized_tenant_id,
|
|
)
|
|
challenge_payload = challenge["payload"]
|
|
challenge_issued_at = _parse_time(
|
|
challenge_payload["issued_at"], "apply challenge issued_at"
|
|
)
|
|
challenge_expires_at = _parse_time(
|
|
challenge_payload["expires_at"], "apply challenge expires_at"
|
|
)
|
|
approval_approved_at = _parse_time(
|
|
approval_payload["approved_at"], "approval approved_at"
|
|
)
|
|
approval_expires_at = _parse_time(
|
|
approval_payload["expires_at"], "approval expires_at"
|
|
)
|
|
if (
|
|
challenge_payload["environment_id"] != normalized_environment_id
|
|
or challenge_payload["tenant_id"] != normalized_tenant_id
|
|
or challenge_payload["idempotency_key"] != expected_idempotency_key
|
|
or challenge_payload["transaction_id"] != normalized_transaction_id
|
|
or challenge_issued_at < approval_approved_at
|
|
or challenge_expires_at > approval_expires_at
|
|
or committed_at < challenge_issued_at
|
|
or committed_at >= challenge_expires_at
|
|
):
|
|
raise PublicationBatchError(
|
|
"online transaction receipt does not satisfy its apply challenge"
|
|
)
|
|
actor = _require_keys(
|
|
payload["generated_by"], "online transaction generator", ("type", "component")
|
|
)
|
|
if actor != {
|
|
"type": "system",
|
|
"component": TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT,
|
|
}:
|
|
raise PublicationBatchError(
|
|
"online transaction receipt was not generated by the trusted service"
|
|
)
|
|
expected_item_results = _expected_apply_item_results(manifest)
|
|
if payload["item_results"] != expected_item_results:
|
|
raise PublicationBatchError("online transaction 74-item evidence drift")
|
|
if payload["write_count"] != EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT:
|
|
raise PublicationBatchError("online transaction write count must be exactly 74")
|
|
image_digests = {
|
|
item["role"]: item["manifest_digest"]
|
|
for item in manifest["candidate"]["images"]
|
|
}
|
|
coordinator_deployment = _require_keys(
|
|
payload["coordinator_deployment"],
|
|
"online transaction coordinator deployment",
|
|
("component", "candidate_commit", "release_image_digest"),
|
|
)
|
|
if coordinator_deployment != {
|
|
"component": TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT,
|
|
"candidate_commit": manifest["candidate"]["commit"],
|
|
"release_image_digest": image_digests["contract-api"],
|
|
}:
|
|
raise PublicationBatchError("online transaction coordinator deployment drift")
|
|
participant_evidence = payload["participant_evidence"]
|
|
if not isinstance(participant_evidence, list) or len(participant_evidence) != 2:
|
|
raise PublicationBatchError(
|
|
"online transaction must contain exactly two participant commit proofs"
|
|
)
|
|
expected_participants = {
|
|
"ai-platform": (
|
|
EXPECTED_MODULE_COUNT,
|
|
EXPECTED_RUNTIME_PROMPT_COUNT,
|
|
image_digests["ai-platform-api"],
|
|
_sha256_json(
|
|
[item for item in expected_item_results if item["kind"] == "prompt_module"]
|
|
),
|
|
),
|
|
"contract-platform": (
|
|
EXPECTED_ASSET_COUNT,
|
|
0,
|
|
image_digests["contract-api"],
|
|
_sha256_json([item for item in expected_item_results if item["kind"] == "asset"]),
|
|
),
|
|
}
|
|
if [item.get("participant") for item in participant_evidence] != sorted(
|
|
expected_participants
|
|
):
|
|
raise PublicationBatchError("online participant evidence ordering or identity drift")
|
|
for index, value in enumerate(participant_evidence):
|
|
participant = _require_keys(
|
|
value,
|
|
f"online participant evidence {index}",
|
|
(
|
|
"participant",
|
|
"state",
|
|
"transaction_id",
|
|
"candidate_commit",
|
|
"release_image_digest",
|
|
"committed_at",
|
|
"write_count",
|
|
"runtime_snapshot_count",
|
|
"committed_item_results_sha256",
|
|
"durable_commit_evidence_sha256",
|
|
),
|
|
)
|
|
(
|
|
expected_write_count,
|
|
expected_runtime_count,
|
|
expected_image_digest,
|
|
expected_results_sha,
|
|
) = expected_participants[
|
|
participant["participant"]
|
|
]
|
|
participant_committed_at = _parse_time(
|
|
participant["committed_at"],
|
|
f"online participant {participant['participant']} committed_at",
|
|
)
|
|
if (
|
|
participant["state"] != "committed"
|
|
or participant["transaction_id"] != normalized_transaction_id
|
|
or participant["candidate_commit"] != manifest["candidate"]["commit"]
|
|
or participant["release_image_digest"] != expected_image_digest
|
|
or participant_committed_at < challenge_issued_at
|
|
or participant_committed_at > committed_at
|
|
or participant_committed_at >= challenge_expires_at
|
|
or participant_committed_at >= approval_expires_at
|
|
or participant["write_count"] != expected_write_count
|
|
or participant["runtime_snapshot_count"] != expected_runtime_count
|
|
or participant["committed_item_results_sha256"] != expected_results_sha
|
|
):
|
|
raise PublicationBatchError("online participant commit evidence drift")
|
|
supplied_durable_sha = _require_sha(
|
|
participant["durable_commit_evidence_sha256"],
|
|
"online participant durable commit evidence SHA-256",
|
|
)
|
|
durable_projection = {
|
|
key: child
|
|
for key, child in participant.items()
|
|
if key != "durable_commit_evidence_sha256"
|
|
}
|
|
if supplied_durable_sha != _sha256_json(durable_projection):
|
|
raise PublicationBatchError(
|
|
"online participant durable commit evidence hash drift"
|
|
)
|
|
signature = _verify_online_kms_signature(
|
|
online_service_public_key,
|
|
envelope["signature"],
|
|
domain=ONLINE_TRANSACTION_SIGNATURE_DOMAIN,
|
|
payload=payload,
|
|
label="online transaction receipt",
|
|
)
|
|
request_id_attestation = _verify_kms_request_id_attestation(
|
|
online_service_public_key,
|
|
envelope["request_id_attestation"],
|
|
receipt_payload=payload,
|
|
primary_signature=signature,
|
|
attestation_domain=ONLINE_REQUEST_ID_ATTESTATION_DOMAIN,
|
|
label="online transaction receipt",
|
|
)
|
|
expected_id = (
|
|
"rc06-online-transaction-"
|
|
+ _sha256_bytes(
|
|
_canonical(payload)
|
|
+ _canonical(signature)
|
|
+ _canonical(request_id_attestation)
|
|
)[:24]
|
|
)
|
|
if envelope["receipt_id"] != expected_id:
|
|
raise PublicationBatchError(
|
|
"online transaction receipt_id does not match its signed material"
|
|
)
|
|
_assert_no_secret_material(envelope, "online transaction receipt")
|
|
return envelope
|
|
|
|
|
|
def validate_runtime_snapshot_receipt(
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval_raw_sha256: str,
|
|
approval: Mapping[str, Any],
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
online_service_public_key: Any,
|
|
online_transaction_receipt: Any,
|
|
receipt: Any,
|
|
*,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> dict[str, Any]:
|
|
"""Verify read-back proof bound to the exact committed online receipt."""
|
|
|
|
online_receipt = validate_online_transaction_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
approval,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
online_transaction_receipt,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
envelope = _require_keys(
|
|
receipt,
|
|
"runtime snapshot receipt",
|
|
(
|
|
"schema_version",
|
|
"receipt_id",
|
|
"payload",
|
|
"signature",
|
|
"request_id_attestation",
|
|
),
|
|
)
|
|
if envelope["schema_version"] != RUNTIME_RECEIPT_SCHEMA_VERSION:
|
|
raise PublicationBatchError("runtime snapshot receipt schema is unsupported")
|
|
payload = _require_keys(
|
|
envelope["payload"],
|
|
"runtime snapshot receipt payload",
|
|
(
|
|
"schema_version",
|
|
"batch_manifest_raw_sha256",
|
|
"batch_manifest_payload_sha256",
|
|
"approval_receipt_sha256",
|
|
"legal_source_review_binding_sha256",
|
|
"environment_id",
|
|
"tenant_id",
|
|
"idempotency_key",
|
|
"transaction_id",
|
|
"online_transaction_receipt_sha256",
|
|
"captured_at",
|
|
"generated_by",
|
|
"transaction_mode",
|
|
"cross_database_atomicity",
|
|
"publication_item_count",
|
|
"runtime_snapshot_dependencies",
|
|
"runtime_snapshot_dependency_count",
|
|
"status",
|
|
),
|
|
)
|
|
online_payload = online_receipt["payload"]
|
|
expected_online_sha = _sha256_json(online_receipt)
|
|
if (
|
|
payload["schema_version"] != RUNTIME_PAYLOAD_SCHEMA_VERSION
|
|
or payload["batch_manifest_raw_sha256"] != manifest_raw_sha256
|
|
or payload["batch_manifest_payload_sha256"]
|
|
!= manifest["manifest_payload_sha256"]
|
|
or payload["approval_receipt_sha256"] != approval_raw_sha256
|
|
or payload["legal_source_review_binding_sha256"]
|
|
!= legal_review_binding_sha256
|
|
or payload["environment_id"] != environment_id
|
|
or payload["tenant_id"] != tenant_id
|
|
or payload["idempotency_key"] != online_payload["idempotency_key"]
|
|
or payload["transaction_id"] != online_payload["transaction_id"]
|
|
or payload["online_transaction_receipt_sha256"] != expected_online_sha
|
|
or payload["transaction_mode"] != TRANSACTION_MODE
|
|
or payload["cross_database_atomicity"] is not False
|
|
or payload["publication_item_count"]
|
|
!= EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT
|
|
or payload["runtime_snapshot_dependency_count"]
|
|
!= EXPECTED_RUNTIME_PROMPT_COUNT
|
|
or payload["status"] != "published_and_resolved"
|
|
):
|
|
raise PublicationBatchError("runtime snapshot receipt binding drift")
|
|
if payload["runtime_snapshot_dependencies"] != expected_runtime_snapshot_results(
|
|
manifest
|
|
):
|
|
raise PublicationBatchError("runtime snapshot 14-item evidence drift")
|
|
captured_at = _parse_time(payload["captured_at"], "runtime captured_at")
|
|
committed_at = _parse_time(online_payload["committed_at"], "online committed_at")
|
|
approval_expires_at = _parse_time(
|
|
approval["payload"]["expires_at"], "approval expires_at"
|
|
)
|
|
challenge_expires_at = _parse_time(
|
|
online_payload["authority_challenge"]["payload"]["expires_at"],
|
|
"apply challenge expires_at",
|
|
)
|
|
if (
|
|
captured_at < committed_at
|
|
or captured_at >= approval_expires_at
|
|
or captured_at >= challenge_expires_at
|
|
):
|
|
raise PublicationBatchError("runtime snapshot receipt is outside its authority window")
|
|
actor = _require_keys(
|
|
payload["generated_by"],
|
|
"runtime snapshot generator",
|
|
("type", "component"),
|
|
)
|
|
if actor != {
|
|
"type": "system",
|
|
"component": TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT,
|
|
}:
|
|
raise PublicationBatchError("runtime snapshot receipt generator is untrusted")
|
|
signature = _verify_online_kms_signature(
|
|
online_service_public_key,
|
|
envelope["signature"],
|
|
domain=RUNTIME_SIGNATURE_DOMAIN,
|
|
payload=payload,
|
|
label="runtime snapshot receipt",
|
|
)
|
|
request_id_attestation = _verify_kms_request_id_attestation(
|
|
online_service_public_key,
|
|
envelope["request_id_attestation"],
|
|
receipt_payload=payload,
|
|
primary_signature=signature,
|
|
attestation_domain=RUNTIME_REQUEST_ID_ATTESTATION_DOMAIN,
|
|
label="runtime snapshot receipt",
|
|
)
|
|
expected_id = (
|
|
"rc06-runtime-snapshot-"
|
|
+ _sha256_bytes(
|
|
_canonical(payload)
|
|
+ _canonical(signature)
|
|
+ _canonical(request_id_attestation)
|
|
)[:24]
|
|
)
|
|
if envelope["receipt_id"] != expected_id:
|
|
raise PublicationBatchError(
|
|
"runtime snapshot receipt_id does not match its signed material"
|
|
)
|
|
_assert_no_secret_material(envelope, "runtime snapshot receipt")
|
|
return envelope
|
|
|
|
|
|
def validate_apply_receipt(
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval: Mapping[str, Any],
|
|
approval_keyring: Mapping[str, Any],
|
|
approval_root_public_key: Ed25519PublicKey,
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
online_service_public_key: Any,
|
|
approval_raw_sha256: str,
|
|
receipt: Any,
|
|
*,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> dict[str, Any]:
|
|
validate_approval_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval,
|
|
approval_keyring,
|
|
approval_root_public_key,
|
|
)
|
|
envelope = _require_keys(
|
|
receipt,
|
|
"apply receipt",
|
|
("schema_version", "receipt_id", "payload", "signature"),
|
|
)
|
|
if envelope["schema_version"] != APPLY_SCHEMA_VERSION:
|
|
raise PublicationBatchError("apply receipt schema is unsupported")
|
|
row = _require_keys(
|
|
envelope["payload"],
|
|
"apply receipt payload",
|
|
(
|
|
"schema_version",
|
|
"batch_manifest_raw_sha256",
|
|
"batch_manifest_payload_sha256",
|
|
"approval_receipt_sha256",
|
|
"legal_source_review_binding_sha256",
|
|
"environment_id",
|
|
"tenant_id",
|
|
"idempotency_key",
|
|
"transaction_id",
|
|
"authorized_commit_at",
|
|
"countersigned_at",
|
|
"generated_by",
|
|
"transaction_mode",
|
|
"cross_database_atomicity",
|
|
"item_results",
|
|
"write_count",
|
|
"status",
|
|
"online_transaction_receipt",
|
|
"runtime_snapshot_receipt",
|
|
),
|
|
)
|
|
if (
|
|
row["schema_version"] != APPLY_PAYLOAD_SCHEMA_VERSION
|
|
or row["status"] != "applied_and_verified"
|
|
or row["transaction_mode"] != TRANSACTION_MODE
|
|
or row["cross_database_atomicity"] is not False
|
|
):
|
|
raise PublicationBatchError("apply receipt is not a successful RC-06 receipt")
|
|
if row["batch_manifest_raw_sha256"] != _require_sha(
|
|
manifest_raw_sha256, "apply manifest raw SHA-256"
|
|
):
|
|
raise PublicationBatchError("apply receipt raw manifest binding drift")
|
|
if row["batch_manifest_payload_sha256"] != manifest["manifest_payload_sha256"]:
|
|
raise PublicationBatchError("apply receipt is not bound to this batch")
|
|
if row["approval_receipt_sha256"] != _require_sha(approval_raw_sha256, "approval raw SHA-256"):
|
|
raise PublicationBatchError("apply receipt is not bound to the approval receipt")
|
|
if row["legal_source_review_binding_sha256"] != _require_sha(
|
|
legal_review_binding_sha256, "expected legal review binding SHA-256"
|
|
):
|
|
raise PublicationBatchError("apply receipt is not bound to the legal review")
|
|
if (
|
|
row["environment_id"]
|
|
!= _require_string(environment_id, "expected apply environment_id", max_length=256)
|
|
or row["tenant_id"]
|
|
!= _require_string(tenant_id, "expected apply tenant_id", max_length=64)
|
|
):
|
|
raise PublicationBatchError("apply receipt environment or tenant binding drift")
|
|
expected_idempotency_key = publication_idempotency_key(
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
environment_id,
|
|
tenant_id,
|
|
)
|
|
if (
|
|
row["idempotency_key"] != expected_idempotency_key
|
|
or row["transaction_id"] != publication_transaction_id(expected_idempotency_key)
|
|
):
|
|
raise PublicationBatchError("apply receipt transaction identity drift")
|
|
authorized_commit_at = _parse_time(
|
|
row["authorized_commit_at"], "apply authorized_commit_at"
|
|
)
|
|
countersigned_at = _parse_time(
|
|
row["countersigned_at"], "apply countersigned_at"
|
|
)
|
|
approval_payload = approval["payload"]
|
|
approval_expires_at = _parse_time(
|
|
approval_payload["expires_at"], "approval expires_at"
|
|
)
|
|
legal_expires_at = _parse_time(
|
|
legal_review_expires_at, "legal review expires_at"
|
|
)
|
|
actor = _require_keys(row["generated_by"], "apply generator", ("type", "component"))
|
|
if actor != {"type": "system", "component": "rc06-apply-authority"}:
|
|
raise PublicationBatchError("per-item apply evidence must be authority generated")
|
|
expected_results = _expected_apply_item_results(manifest)
|
|
if row["item_results"] != expected_results:
|
|
raise PublicationBatchError("apply receipt 74-item online evidence drift")
|
|
if row["write_count"] != EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT:
|
|
raise PublicationBatchError("apply receipt write count must be exactly 74")
|
|
if approval_payload["batch_manifest_payload_sha256"] != manifest["manifest_payload_sha256"]:
|
|
raise PublicationBatchError("approval/apply batch identity mismatch")
|
|
online_receipt = validate_online_transaction_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
approval,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
row["online_transaction_receipt"],
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
runtime_receipt = validate_runtime_snapshot_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
approval,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
online_receipt,
|
|
row["runtime_snapshot_receipt"],
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
online_payload = online_receipt["payload"]
|
|
runtime_payload = runtime_receipt["payload"]
|
|
if (
|
|
row["environment_id"] != online_payload["environment_id"]
|
|
or row["tenant_id"] != online_payload["tenant_id"]
|
|
or row["legal_source_review_binding_sha256"]
|
|
!= online_payload["legal_source_review_binding_sha256"]
|
|
or row["idempotency_key"] != online_payload["idempotency_key"]
|
|
or row["transaction_id"] != online_payload["transaction_id"]
|
|
or row["item_results"] != online_payload["item_results"]
|
|
or row["write_count"] != online_payload["write_count"]
|
|
or runtime_payload["online_transaction_receipt_sha256"]
|
|
!= _sha256_json(online_receipt)
|
|
):
|
|
raise PublicationBatchError(
|
|
"apply receipt does not exactly bind the online/runtime evidence pair"
|
|
)
|
|
runtime_captured_at = _parse_time(
|
|
runtime_payload["captured_at"], "runtime captured_at"
|
|
)
|
|
if authorized_commit_at != runtime_captured_at:
|
|
raise PublicationBatchError(
|
|
"apply authorized commit time must equal the signed runtime read-back time"
|
|
)
|
|
if countersigned_at < authorized_commit_at:
|
|
raise PublicationBatchError("apply countersign predates runtime read-back evidence")
|
|
if countersigned_at >= approval_expires_at:
|
|
raise PublicationBatchError("apply countersign is outside the approval window")
|
|
if countersigned_at >= legal_expires_at:
|
|
raise PublicationBatchError("apply countersign is outside the legal review window")
|
|
if not HEX64_RE.fullmatch(TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256):
|
|
raise PublicationBatchError("apply service public key pin is not provisioned")
|
|
service_public_raw = _public_key_raw(apply_service_public_key)
|
|
if _sha256_bytes(service_public_raw) != TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256:
|
|
raise PublicationBatchError("apply service public key does not match the frozen pin")
|
|
if service_public_raw == _public_key_raw(approval_root_public_key):
|
|
raise PublicationBatchError("apply service key must differ from the approval root key")
|
|
service_public_spki = _public_key_spki(apply_service_public_key)
|
|
online_public_spki = _public_key_spki(online_service_public_key)
|
|
approval_root_spki = _public_key_spki(approval_root_public_key)
|
|
if service_public_spki == online_public_spki:
|
|
raise PublicationBatchError(
|
|
"apply service key must differ from the online transaction service key"
|
|
)
|
|
if online_public_spki == approval_root_spki:
|
|
raise PublicationBatchError(
|
|
"online transaction service key must differ from the approval root key"
|
|
)
|
|
for key in approval_keyring["keys"]:
|
|
try:
|
|
approval_public_key = serialization.load_pem_public_key(
|
|
key["public_key_pem"].encode("ascii")
|
|
)
|
|
except (KeyError, AttributeError, UnicodeError, ValueError) as exc:
|
|
raise PublicationBatchError("approval keyring public key is invalid") from exc
|
|
if isinstance(approval_public_key, Ed25519PublicKey) and service_public_spki == _public_key_spki(
|
|
approval_public_key
|
|
):
|
|
raise PublicationBatchError("apply service key must differ from reviewer approval keys")
|
|
if (
|
|
isinstance(approval_public_key, Ed25519PublicKey)
|
|
and online_public_spki == _public_key_spki(approval_public_key)
|
|
):
|
|
raise PublicationBatchError(
|
|
"online transaction service key must differ from reviewer approval keys"
|
|
)
|
|
try:
|
|
signature = base64.b64decode(
|
|
_require_string(envelope["signature"], "apply receipt signature", max_length=1024),
|
|
validate=True,
|
|
)
|
|
apply_service_public_key.verify(
|
|
signature,
|
|
APPLY_SIGNATURE_DOMAIN + _canonical(row),
|
|
)
|
|
except (TypeError, ValueError, InvalidSignature) as exc:
|
|
raise PublicationBatchError("apply service signature verification failed") from exc
|
|
expected_id = f"rc06-apply-receipt-{_sha256_bytes(_canonical(row) + signature)[:24]}"
|
|
if envelope["receipt_id"] != expected_id:
|
|
raise PublicationBatchError("apply receipt_id does not match its signed material")
|
|
_assert_no_secret_material(envelope, "apply receipt")
|
|
return envelope
|
|
|
|
|
|
def _load_manifest(path: Path, expected_sha256: str | None) -> tuple[dict[str, Any], str]:
|
|
payload, raw_sha = secure_read_json(path, "publication batch", expected_sha256)
|
|
return validate_manifest(payload), raw_sha
|
|
|
|
|
|
def prepare(args: argparse.Namespace) -> dict[str, Any]:
|
|
repo = args.repo.resolve(strict=True)
|
|
validate_candidate(repo, args.commit, args.tag)
|
|
source_manifest, source_raw_sha = secure_read_json(
|
|
args.source_manifest, "source manifest", args.source_manifest_sha256
|
|
)
|
|
image_payload, image_raw_sha = secure_read_json(
|
|
args.image_identities, "image identities", args.image_identities_sha256
|
|
)
|
|
rc05_payload, rc05_raw_sha = secure_read_json(
|
|
args.rc05_plan, "RC-05 plan", args.rc05_plan_sha256
|
|
)
|
|
rc05_summary, rc05_summary_raw_sha = secure_read_json(
|
|
args.rc05_summary, "RC-05 terminal summary", args.rc05_summary_sha256
|
|
)
|
|
rc05_ledger_identity, rc05_ledger_raw_sha = secure_read_json(
|
|
args.rc05_ledger_identity,
|
|
"RC-05 ledger identity",
|
|
args.rc05_ledger_identity_sha256,
|
|
)
|
|
rc05_campaign_identity, rc05_campaign_raw_sha = secure_read_json(
|
|
args.rc05_campaign_identity,
|
|
"RC-05 campaign identity",
|
|
args.rc05_campaign_identity_sha256,
|
|
)
|
|
dispositions_payload = None
|
|
if args.golden_dispositions is not None:
|
|
dispositions_payload, _ = secure_read_json(
|
|
args.golden_dispositions,
|
|
"Golden dispositions",
|
|
args.golden_dispositions_sha256,
|
|
)
|
|
elif args.golden_dispositions_sha256 is not None:
|
|
raise PublicationBatchError("Golden disposition SHA supplied without a disposition file")
|
|
|
|
# Determine the complete executable source closure from the frozen Git
|
|
# object, verify it before importing any project module, and verify it a
|
|
# second time after inventory extraction. This prevents a dirty or
|
|
# concurrently swapped worktree module from influencing a candidate that
|
|
# claims the immutable commit/tag identity.
|
|
frozen_source_paths = _inventory_source_paths(repo, args.commit)
|
|
verify_frozen_sources(repo, args.commit, source_manifest, frozen_source_paths)
|
|
# Never execute the mutable checkout after it has merely been checked.
|
|
# Load the inventory from bytes fetched from the immutable Git object and
|
|
# verified against the independently pinned source manifest.
|
|
with tempfile.TemporaryDirectory(prefix="yuqei-rc06-frozen-") as temporary:
|
|
staged_repo = Path(temporary) / "source"
|
|
stage_frozen_sources(
|
|
repo,
|
|
args.commit,
|
|
source_manifest,
|
|
frozen_source_paths,
|
|
staged_repo,
|
|
)
|
|
inventory = load_runtime_inventory(
|
|
staged_repo,
|
|
frozen_source_paths=frozen_source_paths,
|
|
)
|
|
_, source_payload_sha = _source_manifest_index(source_manifest, args.commit)
|
|
image_identity = validate_image_identities(image_payload, args.commit, args.tag)
|
|
expected_rc05_candidate = {
|
|
"schema_version": RC05_CANDIDATE_SCHEMA_VERSION,
|
|
"commit": args.commit,
|
|
"tag": args.tag,
|
|
"source_manifest": {
|
|
"schema_version": "yuqei.release-source-manifest.v1",
|
|
"raw_sha256": source_raw_sha,
|
|
"payload_sha256": source_payload_sha,
|
|
},
|
|
"image_identities": {
|
|
"schema_version": image_identity["source_schema"],
|
|
"raw_sha256": image_raw_sha,
|
|
"identity_set_sha256": image_identity["identity_set_sha256"],
|
|
"images": image_identity["images"],
|
|
},
|
|
}
|
|
rc05_identity = validate_rc05_plan(
|
|
rc05_payload, expected_candidate=expected_rc05_candidate
|
|
)
|
|
verify_rc05_source_pins(repo, args.commit, source_manifest, rc05_payload)
|
|
rc05_terminal_evidence = validate_rc05_terminal_evidence(
|
|
rc05_payload,
|
|
rc05_raw_sha,
|
|
rc05_summary,
|
|
rc05_summary_raw_sha,
|
|
rc05_ledger_identity,
|
|
rc05_ledger_raw_sha,
|
|
rc05_campaign_identity,
|
|
rc05_campaign_raw_sha,
|
|
)
|
|
dispositions = build_dispositions(inventory["golden_cases"], dispositions_payload)
|
|
manifest = assemble_manifest(
|
|
commit=args.commit,
|
|
tag=args.tag,
|
|
source_manifest_sha256=source_raw_sha,
|
|
source_manifest_payload_sha256=source_payload_sha,
|
|
image_identities_sha256=image_raw_sha,
|
|
image_identity=image_identity,
|
|
rc05_plan_sha256=rc05_raw_sha,
|
|
rc05_plan_identity=rc05_identity,
|
|
rc05_terminal_evidence=rc05_terminal_evidence,
|
|
inventory=inventory,
|
|
dispositions=dispositions,
|
|
)
|
|
validate_manifest(manifest)
|
|
_safe_output_path(args.output)
|
|
raw = json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
|
try:
|
|
with args.output.open("x", encoding="utf-8", newline="\n") as handle:
|
|
handle.write(raw)
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
except FileExistsError as exc:
|
|
raise PublicationBatchError(
|
|
"output appeared concurrently; refusing to overwrite immutable evidence"
|
|
) from exc
|
|
return {
|
|
"output": str(args.output),
|
|
"raw_sha256": _sha256_bytes(raw.encode("utf-8")),
|
|
"manifest_payload_sha256": manifest["manifest_payload_sha256"],
|
|
"assets": EXPECTED_ASSET_COUNT,
|
|
"prompt_modules": EXPECTED_MODULE_COUNT,
|
|
"golden_unresolved": manifest["golden_dispositions"]["counts_by_status"]["unresolved"],
|
|
"release_ready": False,
|
|
"blocked_controls": list(BLOCKED_CONTROLS),
|
|
"service_writes": 0,
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
prepare_parser = subparsers.add_parser("prepare", help="write one offline, fail-closed batch manifest")
|
|
prepare_parser.add_argument("--repo", type=Path, default=Path.cwd())
|
|
prepare_parser.add_argument("--commit", required=True)
|
|
prepare_parser.add_argument("--tag", required=True)
|
|
prepare_parser.add_argument("--source-manifest", type=Path, required=True)
|
|
prepare_parser.add_argument("--source-manifest-sha256", required=True)
|
|
prepare_parser.add_argument("--image-identities", type=Path, required=True)
|
|
prepare_parser.add_argument("--image-identities-sha256", required=True)
|
|
prepare_parser.add_argument("--rc05-plan", type=Path, required=True)
|
|
prepare_parser.add_argument("--rc05-plan-sha256", required=True)
|
|
prepare_parser.add_argument("--rc05-summary", type=Path, required=True)
|
|
prepare_parser.add_argument("--rc05-summary-sha256", required=True)
|
|
prepare_parser.add_argument("--rc05-ledger-identity", type=Path, required=True)
|
|
prepare_parser.add_argument("--rc05-ledger-identity-sha256", required=True)
|
|
prepare_parser.add_argument("--rc05-campaign-identity", type=Path, required=True)
|
|
prepare_parser.add_argument("--rc05-campaign-identity-sha256", required=True)
|
|
prepare_parser.add_argument("--golden-dispositions", type=Path)
|
|
prepare_parser.add_argument("--golden-dispositions-sha256")
|
|
prepare_parser.add_argument("--output", type=Path, required=True)
|
|
|
|
validate_parser = subparsers.add_parser("validate", help="validate a batch without writing")
|
|
validate_parser.add_argument("--manifest", type=Path, required=True)
|
|
validate_parser.add_argument("--expected-sha256")
|
|
|
|
approval_parser = subparsers.add_parser("validate-approval", help="validate a separate approval receipt")
|
|
approval_parser.add_argument("--manifest", type=Path, required=True)
|
|
approval_parser.add_argument("--manifest-sha256", required=True)
|
|
approval_parser.add_argument("--approval", type=Path, required=True)
|
|
approval_parser.add_argument("--approval-sha256", required=True)
|
|
approval_parser.add_argument("--approval-keyring", type=Path, required=True)
|
|
approval_parser.add_argument("--approval-keyring-sha256", required=True)
|
|
approval_parser.add_argument("--approval-root-public-key", type=Path, required=True)
|
|
|
|
apply_parser = subparsers.add_parser("validate-apply", help="validate a separate apply receipt")
|
|
apply_parser.add_argument("--manifest", type=Path, required=True)
|
|
apply_parser.add_argument("--manifest-sha256", required=True)
|
|
apply_parser.add_argument("--approval", type=Path, required=True)
|
|
apply_parser.add_argument("--approval-sha256", required=True)
|
|
apply_parser.add_argument("--approval-keyring", type=Path, required=True)
|
|
apply_parser.add_argument("--approval-keyring-sha256", required=True)
|
|
apply_parser.add_argument("--approval-root-public-key", type=Path, required=True)
|
|
apply_parser.add_argument("--apply-service-public-key", type=Path, required=True)
|
|
apply_parser.add_argument("--online-service-public-key", type=Path, required=True)
|
|
apply_parser.add_argument("--legal-review-binding-sha256", required=True)
|
|
apply_parser.add_argument("--legal-review-expires-at", required=True)
|
|
apply_parser.add_argument("--environment-id", required=True)
|
|
apply_parser.add_argument("--tenant-id", required=True)
|
|
apply_parser.add_argument("--apply-receipt", type=Path, required=True)
|
|
apply_parser.add_argument("--apply-receipt-sha256", required=True)
|
|
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
if args.command == "prepare":
|
|
result = prepare(args)
|
|
elif args.command == "validate":
|
|
manifest, raw_sha = _load_manifest(args.manifest, args.expected_sha256)
|
|
result = {
|
|
"structurally_valid": True,
|
|
"raw_sha256": raw_sha,
|
|
"manifest_payload_sha256": manifest["manifest_payload_sha256"],
|
|
"evidence_complete": False,
|
|
"release_ready": False,
|
|
"blocked_controls": list(BLOCKED_CONTROLS),
|
|
"service_writes": 0,
|
|
}
|
|
elif args.command == "validate-approval":
|
|
manifest, manifest_raw_sha = _load_manifest(args.manifest, args.manifest_sha256)
|
|
approval, raw_sha = secure_read_json(args.approval, "approval receipt", args.approval_sha256)
|
|
approval_keyring, _ = secure_read_json(
|
|
args.approval_keyring,
|
|
"approval keyring",
|
|
args.approval_keyring_sha256,
|
|
)
|
|
approval_root_public_key = _read_pinned_raw_public_key(
|
|
args.approval_root_public_key,
|
|
expected_sha256=TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256,
|
|
label="approval root public key",
|
|
)
|
|
validate_approval_receipt(
|
|
manifest,
|
|
manifest_raw_sha,
|
|
approval,
|
|
approval_keyring,
|
|
approval_root_public_key,
|
|
)
|
|
result = {
|
|
"structurally_valid": True,
|
|
"approval_raw_sha256": raw_sha,
|
|
"evidence_complete": False,
|
|
"release_ready": False,
|
|
"blocked_controls": list(BLOCKED_CONTROLS[1:]),
|
|
"service_writes": 0,
|
|
}
|
|
else:
|
|
manifest, manifest_raw_sha = _load_manifest(args.manifest, args.manifest_sha256)
|
|
approval, approval_raw = secure_read_json(
|
|
args.approval, "approval receipt", args.approval_sha256
|
|
)
|
|
approval_keyring, _ = secure_read_json(
|
|
args.approval_keyring,
|
|
"approval keyring",
|
|
args.approval_keyring_sha256,
|
|
)
|
|
approval_root_public_key = _read_pinned_raw_public_key(
|
|
args.approval_root_public_key,
|
|
expected_sha256=TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256,
|
|
label="approval root public key",
|
|
)
|
|
apply_service_public_key = _read_pinned_raw_public_key(
|
|
args.apply_service_public_key,
|
|
expected_sha256=TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256,
|
|
label="apply service public key",
|
|
)
|
|
online_service_public_key = _read_pinned_spki_public_key(
|
|
args.online_service_public_key,
|
|
expected_sha256=TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256,
|
|
label="online publication service public key",
|
|
)
|
|
validate_approval_receipt(
|
|
manifest,
|
|
manifest_raw_sha,
|
|
approval,
|
|
approval_keyring,
|
|
approval_root_public_key,
|
|
)
|
|
apply_receipt, apply_raw = secure_read_json(
|
|
args.apply_receipt, "apply receipt", args.apply_receipt_sha256
|
|
)
|
|
validate_apply_receipt(
|
|
manifest,
|
|
manifest_raw_sha,
|
|
approval,
|
|
approval_keyring,
|
|
approval_root_public_key,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
approval_raw,
|
|
apply_receipt,
|
|
legal_review_binding_sha256=args.legal_review_binding_sha256,
|
|
legal_review_expires_at=args.legal_review_expires_at,
|
|
environment_id=args.environment_id,
|
|
tenant_id=args.tenant_id,
|
|
)
|
|
result = {
|
|
"structurally_valid": True,
|
|
"apply_receipt_raw_sha256": apply_raw,
|
|
"evidence_complete": True,
|
|
"release_ready": False,
|
|
"blocked_controls": [],
|
|
"evidence_scope": "rc06_publication_only",
|
|
"service_writes": 0,
|
|
}
|
|
except PublicationBatchError as exc:
|
|
print(f"publication_batch_error: {exc}", file=sys.stderr)
|
|
return 2
|
|
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|