2186 lines
85 KiB
Python
2186 lines
85 KiB
Python
#!/usr/bin/env python3
|
|
"""Protocol client for applying one frozen RC-06 batch through a real service.
|
|
|
|
The service contract is deliberately fail-closed. The exact 60 assets and 14
|
|
Prompt modules must be durably committed and read back online before the
|
|
offline apply authority can countersign them. This client never treats an
|
|
unsigned HTTP response as publication evidence.
|
|
|
|
The legal-source request/decision/keyring checked here is an externally signed
|
|
assertion. This program delegates identity, qualification, freshness, status
|
|
URL, rationale and exact source hashes to the canonical scene-review validator;
|
|
it does not perform legal review or infer professional status.
|
|
|
|
This file does not implement the cross-Contract/AI transaction service. RC-06
|
|
remains blocked until a deployed endpoint can truthfully produce the signed
|
|
cross-database saga and online read-back evidence required by this client.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
|
|
from cryptography.exceptions import InvalidSignature
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
REPO_ROOT = SCRIPT_DIR.parents[1]
|
|
BUILDER_PATH = SCRIPT_DIR / "build-rc06-publication-batch.py"
|
|
|
|
|
|
def _load_builder():
|
|
spec = importlib.util.spec_from_file_location("rc06_publication_batch_builder", BUILDER_PATH)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError("cannot load RC-06 publication batch builder")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
batch = _load_builder()
|
|
|
|
SCENE_LEGAL_REVIEW_BUILDER_PATH = SCRIPT_DIR / "build-scene-source-legal-review.py"
|
|
|
|
APPLY_REQUEST_SCHEMA_VERSION = "yuqei.rc06-online-apply-request.v3"
|
|
APPLY_RESPONSE_SCHEMA_VERSION = "yuqei.rc06-online-apply-response.v2"
|
|
TRANSACTION_STATUS_SCHEMA_VERSION = "yuqei.rc06-online-transaction-status.v3"
|
|
ROLLBACK_REQUEST_SCHEMA_VERSION = "yuqei.rc06-online-rollback-request.v2"
|
|
ROLLBACK_RESPONSE_SCHEMA_VERSION = "yuqei.rc06-online-rollback-response.v2"
|
|
FINALIZE_REQUEST_SCHEMA_VERSION = "yuqei.rc06-online-finalize-request.v1"
|
|
FINALIZE_RESPONSE_SCHEMA_VERSION = "yuqei.rc06-online-finalize-response.v1"
|
|
EXECUTION_EVIDENCE_SCHEMA_VERSION = "yuqei.rc06-online-apply-evidence.v2"
|
|
FINAL_EVIDENCE_SCHEMA_VERSION = "yuqei.rc06-finalized-apply-evidence.v1"
|
|
LEDGER_SCHEMA_VERSION = "yuqei.rc06-local-apply-ledger.v2"
|
|
AUTH_TOKEN_ENV = "RC06_APPLY_BEARER_TOKEN"
|
|
MAX_HTTP_BODY_BYTES = 16 * 1024 * 1024
|
|
|
|
class ApplyPublicationError(batch.PublicationBatchError):
|
|
"""The online transaction could not be proven safe and complete."""
|
|
|
|
|
|
def _require_exact(value: Any, label: str, keys: tuple[str, ...]) -> dict[str, Any]:
|
|
if not isinstance(value, dict) or set(value) != set(keys):
|
|
raise ApplyPublicationError(f"{label} fields are invalid")
|
|
return value
|
|
|
|
|
|
def _require_text(value: Any, label: str, *, maximum: int = 512) -> str:
|
|
if not isinstance(value, str) or not value.strip() or value != value.strip() or len(value) > maximum:
|
|
raise ApplyPublicationError(f"{label} is invalid")
|
|
return value
|
|
|
|
|
|
def _sha256(value: bytes) -> str:
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
def _public_raw(key: Ed25519PublicKey) -> bytes:
|
|
return key.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
|
|
|
|
def _load_scene_legal_review_module():
|
|
if not SCENE_LEGAL_REVIEW_BUILDER_PATH.is_file():
|
|
raise ApplyPublicationError(
|
|
"scene legal review validator is absent from the frozen candidate"
|
|
)
|
|
spec = importlib.util.spec_from_file_location(
|
|
"rc06_scene_source_legal_review", SCENE_LEGAL_REVIEW_BUILDER_PATH
|
|
)
|
|
if spec is None or spec.loader is None:
|
|
raise ApplyPublicationError("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(
|
|
manifest: Mapping[str, Any],
|
|
approval: Mapping[str, Any],
|
|
request: Any,
|
|
decision: Any,
|
|
keyring: Any,
|
|
keyring_raw_sha256: str,
|
|
*,
|
|
now: datetime,
|
|
) -> tuple[dict[str, Any], str]:
|
|
"""Delegate legal verification and frozen-keyring trust to the builder."""
|
|
|
|
try:
|
|
return batch.validate_scene_legal_review_evidence(
|
|
REPO_ROOT,
|
|
manifest,
|
|
approval,
|
|
request,
|
|
decision,
|
|
keyring,
|
|
keyring_raw_sha256,
|
|
now=now,
|
|
validator=_load_scene_legal_review_module(),
|
|
)
|
|
except batch.PublicationBatchError as exc:
|
|
raise ApplyPublicationError(f"scene legal review validation failed: {exc}") from exc
|
|
|
|
|
|
def expected_runtime_snapshot_results(manifest: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
return batch.expected_runtime_snapshot_results(manifest)
|
|
|
|
|
|
def validate_runtime_snapshot_receipt(
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval: Mapping[str, Any],
|
|
approval_raw_sha256: str,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
online_service_public_key: Any,
|
|
online_transaction_receipt: Mapping[str, Any],
|
|
receipt: Any,
|
|
*,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return batch.validate_runtime_snapshot_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
approval,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
online_transaction_receipt,
|
|
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,
|
|
)
|
|
except batch.PublicationBatchError as exc:
|
|
raise ApplyPublicationError(str(exc)) from exc
|
|
|
|
|
|
class JsonHttpClient:
|
|
def __init__(
|
|
self,
|
|
base_url: str,
|
|
bearer_token: str,
|
|
*,
|
|
timeout_seconds: float = 30.0,
|
|
allow_http_loopback: bool = False,
|
|
) -> None:
|
|
parsed = urllib.parse.urlsplit(base_url)
|
|
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
raise ApplyPublicationError("publication service URL contains forbidden components")
|
|
loopback = parsed.hostname in {"127.0.0.1", "localhost", "::1"}
|
|
if parsed.scheme != "https" and not (
|
|
allow_http_loopback and parsed.scheme == "http" and loopback
|
|
):
|
|
raise ApplyPublicationError("publication service requires HTTPS")
|
|
if not parsed.netloc or parsed.path not in {"", "/"}:
|
|
raise ApplyPublicationError("publication service base URL is invalid")
|
|
self.base_url = base_url.rstrip("/")
|
|
self.token = _require_text(bearer_token, "publication bearer token", maximum=8192)
|
|
self.timeout_seconds = timeout_seconds
|
|
|
|
def request(
|
|
self,
|
|
method: str,
|
|
path: str,
|
|
*,
|
|
payload: Mapping[str, Any] | None,
|
|
headers: Mapping[str, str],
|
|
) -> Any:
|
|
body = batch._canonical(payload) if payload is not None else None
|
|
request_headers = {
|
|
"Accept": "application/json",
|
|
"Authorization": f"Bearer {self.token}",
|
|
**dict(headers),
|
|
}
|
|
if body is not None:
|
|
request_headers["Content-Type"] = "application/json"
|
|
request = urllib.request.Request(
|
|
self.base_url + path,
|
|
data=body,
|
|
headers=request_headers,
|
|
method=method,
|
|
)
|
|
try:
|
|
response = urllib.request.urlopen(request, timeout=self.timeout_seconds)
|
|
raw = response.read(MAX_HTTP_BODY_BYTES + 1)
|
|
status = response.status
|
|
content_type = response.headers.get_content_type()
|
|
except urllib.error.HTTPError as exc:
|
|
raw = exc.read(MAX_HTTP_BODY_BYTES + 1)
|
|
detail = f"HTTP {exc.code}"
|
|
try:
|
|
parsed = json.loads(raw.decode("utf-8"))
|
|
if isinstance(parsed, dict) and isinstance(parsed.get("error"), str):
|
|
detail += f": {parsed['error'][:512]}"
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
pass
|
|
raise ApplyPublicationError(f"publication service rejected request ({detail})") from exc
|
|
except (OSError, TimeoutError) as exc:
|
|
raise ApplyPublicationError("publication service request failed") from exc
|
|
if status < 200 or status >= 300 or content_type != "application/json":
|
|
raise ApplyPublicationError("publication service returned an invalid HTTP response")
|
|
if len(raw) > MAX_HTTP_BODY_BYTES:
|
|
raise ApplyPublicationError("publication service response is too large")
|
|
try:
|
|
return json.loads(
|
|
raw.decode("utf-8"),
|
|
object_pairs_hook=batch._reject_duplicate_json_pairs,
|
|
parse_constant=batch._reject_json_constant,
|
|
)
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise ApplyPublicationError("publication service response is not valid JSON") from exc
|
|
|
|
|
|
class ApplyLedger:
|
|
"""Small local recovery ledger; it stores hashes/receipts, never credentials."""
|
|
|
|
def __init__(self, path: Path) -> None:
|
|
self.path = path.resolve()
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self.connection = sqlite3.connect(self.path, timeout=30, isolation_level=None)
|
|
self.connection.execute("PRAGMA journal_mode=WAL")
|
|
self.connection.execute("PRAGMA synchronous=FULL")
|
|
legacy_operations_present = self.connection.execute(
|
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='rc06_apply_operations'"
|
|
).fetchone() is not None
|
|
self.connection.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS rc06_apply_ledger_metadata (
|
|
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
|
schema_version TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS rc06_apply_operations (
|
|
idempotency_key TEXT PRIMARY KEY,
|
|
request_sha256 TEXT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
apply_receipt_json TEXT,
|
|
apply_receipt_sha256 TEXT,
|
|
last_error TEXT,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS rc06_apply_events (
|
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
idempotency_key TEXT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
evidence_sha256 TEXT NOT NULL,
|
|
recorded_at TEXT NOT NULL
|
|
);
|
|
"""
|
|
)
|
|
metadata = self.connection.execute(
|
|
"SELECT schema_version FROM rc06_apply_ledger_metadata WHERE singleton = 1"
|
|
).fetchone()
|
|
if metadata is None:
|
|
if legacy_operations_present:
|
|
raise ApplyPublicationError(
|
|
"legacy apply ledger cannot be silently upgraded; use a new ledger path"
|
|
)
|
|
self.connection.execute(
|
|
"INSERT INTO rc06_apply_ledger_metadata(singleton, schema_version) VALUES (1, ?)",
|
|
(LEDGER_SCHEMA_VERSION,),
|
|
)
|
|
elif metadata[0] != LEDGER_SCHEMA_VERSION:
|
|
raise ApplyPublicationError("apply ledger schema is unsupported")
|
|
|
|
def reserve(
|
|
self,
|
|
key: str,
|
|
request_sha256: str,
|
|
now: datetime,
|
|
*,
|
|
initial_dispatch_authorized: bool,
|
|
) -> tuple[str, dict[str, Any] | None]:
|
|
"""Reserve a new request or identify an exact crash-recovery replay.
|
|
|
|
An expired short-lived challenge may recover an already-dispatched exact
|
|
request, but it must never create a new dispatch reservation.
|
|
"""
|
|
|
|
self.connection.execute("BEGIN IMMEDIATE")
|
|
try:
|
|
row = self.connection.execute(
|
|
"SELECT request_sha256, state, apply_receipt_json FROM rc06_apply_operations "
|
|
"WHERE idempotency_key = ?",
|
|
(key,),
|
|
).fetchone()
|
|
if row is not None and row[0] != request_sha256:
|
|
raise ApplyPublicationError("idempotency key is bound to another request")
|
|
if row is not None and row[1] in {
|
|
"online_committed_pending_countersign",
|
|
"succeeded",
|
|
}:
|
|
receipt = json.loads(row[2])
|
|
self.connection.execute("COMMIT")
|
|
return "replay", receipt
|
|
if row is not None and row[1] in {
|
|
"dispatching",
|
|
"verifying",
|
|
"irreversible_failure_no_go",
|
|
}:
|
|
timestamp = now.astimezone(UTC).isoformat(timespec="seconds")
|
|
self._event(
|
|
key,
|
|
f"recovering_{row[1]}",
|
|
request_sha256,
|
|
timestamp,
|
|
)
|
|
self.connection.execute("COMMIT")
|
|
return "recover", None
|
|
if row is not None:
|
|
raise ApplyPublicationError("an apply with this idempotency key needs reconciliation")
|
|
timestamp = now.astimezone(UTC).isoformat(timespec="seconds")
|
|
if not initial_dispatch_authorized:
|
|
self.connection.execute(
|
|
"INSERT INTO rc06_apply_operations "
|
|
"(idempotency_key, request_sha256, state, updated_at) "
|
|
"VALUES (?, ?, 'probing_remote', ?)",
|
|
(key, request_sha256, timestamp),
|
|
)
|
|
self._event(key, "probing_remote", request_sha256, timestamp)
|
|
self.connection.execute("COMMIT")
|
|
return "probe", None
|
|
self.connection.execute(
|
|
"INSERT INTO rc06_apply_operations "
|
|
"(idempotency_key, request_sha256, state, updated_at) VALUES (?, ?, 'dispatching', ?) "
|
|
"ON CONFLICT(idempotency_key) DO UPDATE SET state='dispatching', last_error=NULL, updated_at=excluded.updated_at",
|
|
(key, request_sha256, timestamp),
|
|
)
|
|
self._event(key, "dispatching", request_sha256, timestamp)
|
|
self.connection.execute("COMMIT")
|
|
return "dispatch", None
|
|
except Exception:
|
|
self.connection.execute("ROLLBACK")
|
|
raise
|
|
|
|
def load(self, key: str, request_sha256: str) -> tuple[str, dict[str, Any]]:
|
|
row = self.connection.execute(
|
|
"SELECT request_sha256, state, apply_receipt_json FROM rc06_apply_operations "
|
|
"WHERE idempotency_key = ?",
|
|
(key,),
|
|
).fetchone()
|
|
if row is None or row[0] != request_sha256 or row[2] is None:
|
|
raise ApplyPublicationError("online execution evidence is absent from the ledger")
|
|
try:
|
|
evidence = json.loads(row[2])
|
|
except (TypeError, json.JSONDecodeError) as exc:
|
|
raise ApplyPublicationError("stored online execution evidence is invalid") from exc
|
|
return str(row[1]), evidence
|
|
|
|
def state(
|
|
self,
|
|
key: str,
|
|
state: str,
|
|
evidence: Any,
|
|
now: datetime,
|
|
*,
|
|
receipt: Mapping[str, Any] | None = None,
|
|
error: str | None = None,
|
|
) -> None:
|
|
evidence_sha = batch._sha256_json(evidence)
|
|
receipt_json = (
|
|
batch._canonical(receipt).decode("utf-8") if receipt is not None else None
|
|
)
|
|
receipt_sha = batch._sha256_json(receipt) if receipt is not None else None
|
|
timestamp = now.astimezone(UTC).isoformat(timespec="seconds")
|
|
self.connection.execute("BEGIN IMMEDIATE")
|
|
try:
|
|
self.connection.execute(
|
|
"UPDATE rc06_apply_operations SET state=?, apply_receipt_json=?, "
|
|
"apply_receipt_sha256=?, last_error=?, updated_at=? WHERE idempotency_key=?",
|
|
(state, receipt_json, receipt_sha, error, timestamp, key),
|
|
)
|
|
self._event(key, state, evidence_sha, timestamp)
|
|
self.connection.execute("COMMIT")
|
|
except Exception:
|
|
self.connection.execute("ROLLBACK")
|
|
raise
|
|
|
|
def _event(self, key: str, state: str, evidence_sha: str, timestamp: str) -> None:
|
|
self.connection.execute(
|
|
"INSERT INTO rc06_apply_events "
|
|
"(idempotency_key, state, evidence_sha256, recorded_at) VALUES (?, ?, ?, ?)",
|
|
(key, state, evidence_sha, timestamp),
|
|
)
|
|
|
|
def close(self) -> None:
|
|
self.connection.close()
|
|
|
|
|
|
def _idempotency_key(
|
|
manifest_raw_sha256: str,
|
|
approval_raw_sha256: str,
|
|
legal_review_binding_sha256: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> str:
|
|
return batch.publication_idempotency_key(
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
environment_id,
|
|
tenant_id,
|
|
)
|
|
|
|
|
|
def _transaction_id(idempotency_key: str) -> str:
|
|
return batch.publication_transaction_id(idempotency_key)
|
|
|
|
|
|
def _headers(
|
|
key: str,
|
|
manifest_raw_sha256: str,
|
|
approval_raw_sha256: str,
|
|
legal_review_binding_sha256: str,
|
|
tenant_id: str,
|
|
) -> dict[str, str]:
|
|
return {
|
|
"Idempotency-Key": key,
|
|
"X-RC06-Manifest-SHA256": manifest_raw_sha256,
|
|
"X-RC06-Approval-SHA256": approval_raw_sha256,
|
|
"X-RC06-Legal-Review-SHA256": legal_review_binding_sha256,
|
|
"X-RC06-Tenant-ID": tenant_id,
|
|
}
|
|
|
|
|
|
def _validate_apply_response(
|
|
response: Any,
|
|
*,
|
|
key: str,
|
|
request_sha256: str,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
transaction_id: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> dict[str, Any]:
|
|
row = _require_exact(
|
|
response,
|
|
"online apply response",
|
|
(
|
|
"schema_version",
|
|
"idempotency_key",
|
|
"request_sha256",
|
|
"legal_source_review_binding_sha256",
|
|
"legal_source_review_expires_at",
|
|
"transaction_id",
|
|
"environment_id",
|
|
"tenant_id",
|
|
"transaction_mode",
|
|
"cross_database_atomicity",
|
|
"publication_item_count",
|
|
"runtime_snapshot_dependency_count",
|
|
"online_transaction_receipt",
|
|
"runtime_snapshot_receipt",
|
|
),
|
|
)
|
|
if (
|
|
row["schema_version"] != APPLY_RESPONSE_SCHEMA_VERSION
|
|
or row["idempotency_key"] != key
|
|
or row["request_sha256"] != request_sha256
|
|
or row["legal_source_review_binding_sha256"] != legal_review_binding_sha256
|
|
or row["legal_source_review_expires_at"] != legal_review_expires_at
|
|
or row["transaction_id"] != transaction_id
|
|
or row["environment_id"] != environment_id
|
|
or row["tenant_id"] != tenant_id
|
|
or row["transaction_mode"] != batch.TRANSACTION_MODE
|
|
or row["cross_database_atomicity"] is not False
|
|
or row["publication_item_count"] != 74
|
|
or row["runtime_snapshot_dependency_count"] != 14
|
|
):
|
|
raise ApplyPublicationError("online apply response identity or transaction mode drift")
|
|
return row
|
|
|
|
|
|
_TRANSACTION_STATUS_FIELDS = (
|
|
"schema_version",
|
|
"idempotency_key",
|
|
"request_sha256",
|
|
"transaction_id",
|
|
"state",
|
|
"state_version",
|
|
"no_go",
|
|
"legal_source_review_binding_sha256",
|
|
"environment_id",
|
|
"tenant_id",
|
|
"transaction_mode",
|
|
"cross_database_atomicity",
|
|
"publication_item_results",
|
|
"publication_item_count",
|
|
"runtime_snapshot_dependencies",
|
|
"runtime_snapshot_dependency_count",
|
|
"participant_evidence",
|
|
"online_transaction_receipt",
|
|
"online_transaction_receipt_sha256",
|
|
"runtime_snapshot_receipt",
|
|
"runtime_snapshot_receipt_sha256",
|
|
"apply_receipt",
|
|
"apply_receipt_sha256",
|
|
"finalized_at",
|
|
"rollback_response",
|
|
"rollback_response_sha256",
|
|
"failure_code",
|
|
"failure_at",
|
|
"failure_evidence_sha256",
|
|
"status_updated_at",
|
|
)
|
|
|
|
_TRANSACTION_STATE_VERSION = {
|
|
"prepared": 1,
|
|
"rolled_back": 2,
|
|
# This is the post-Contract-commit, no-rollback boundary. Replaying the
|
|
# exact request may still recover read-back/KMS evidence and advance to 3.
|
|
"irreversible_failure": 2,
|
|
"committed_and_verified": 3,
|
|
"finalized": 4,
|
|
}
|
|
|
|
|
|
def _validate_transaction_status(
|
|
status: Any,
|
|
*,
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval: Mapping[str, Any],
|
|
approval_raw_sha256: str,
|
|
approval_keyring: Mapping[str, Any],
|
|
approval_root_public_key: Ed25519PublicKey,
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
online_service_public_key: Any,
|
|
request_sha256: str,
|
|
key: str,
|
|
transaction_id: str,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
minimum_state_version: int = 1,
|
|
expected_apply_receipt: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Validate the one exact forward GET status contract for every client."""
|
|
|
|
row = _require_exact(status, "online transaction status", _TRANSACTION_STATUS_FIELDS)
|
|
state = row["state"]
|
|
expected_state_version = _TRANSACTION_STATE_VERSION.get(state)
|
|
if (
|
|
row["schema_version"] != TRANSACTION_STATUS_SCHEMA_VERSION
|
|
or row["idempotency_key"] != key
|
|
or row["request_sha256"] != request_sha256
|
|
or row["transaction_id"] != transaction_id
|
|
or expected_state_version is None
|
|
or not isinstance(row["state_version"], int)
|
|
or isinstance(row["state_version"], bool)
|
|
or row["state_version"] != expected_state_version
|
|
or row["state_version"] < minimum_state_version
|
|
or row["no_go"] is not (state != "finalized")
|
|
or row["legal_source_review_binding_sha256"] != legal_review_binding_sha256
|
|
or row["environment_id"] != environment_id
|
|
or row["tenant_id"] != tenant_id
|
|
or row["transaction_mode"] != batch.TRANSACTION_MODE
|
|
or row["cross_database_atomicity"] is not False
|
|
):
|
|
raise ApplyPublicationError("online transaction status identity or state drift")
|
|
batch._require_sha(row["request_sha256"], "online status request SHA-256")
|
|
status_updated_at = batch._parse_time(
|
|
row["status_updated_at"], "online status_updated_at"
|
|
)
|
|
|
|
commit_empty = (
|
|
row["publication_item_results"] == []
|
|
and row["publication_item_count"] == 0
|
|
and row["runtime_snapshot_dependencies"] == []
|
|
and row["runtime_snapshot_dependency_count"] == 0
|
|
and row["participant_evidence"] == []
|
|
and row["online_transaction_receipt"] is None
|
|
and row["online_transaction_receipt_sha256"] is None
|
|
and row["runtime_snapshot_receipt"] is None
|
|
and row["runtime_snapshot_receipt_sha256"] is None
|
|
)
|
|
final_empty = (
|
|
row["apply_receipt"] is None
|
|
and row["apply_receipt_sha256"] is None
|
|
and row["finalized_at"] is None
|
|
)
|
|
rollback_empty = (
|
|
row["rollback_response"] is None
|
|
and row["rollback_response_sha256"] is None
|
|
)
|
|
failure_empty = (
|
|
row["failure_code"] is None
|
|
and row["failure_at"] is None
|
|
and row["failure_evidence_sha256"] is None
|
|
)
|
|
|
|
if state in {"prepared", "irreversible_failure", "rolled_back"} and not commit_empty:
|
|
raise ApplyPublicationError("non-committed online status contains commit evidence")
|
|
if state != "finalized" and not final_empty:
|
|
raise ApplyPublicationError("non-finalized online status contains finalization evidence")
|
|
if state != "rolled_back" and not rollback_empty:
|
|
raise ApplyPublicationError("non-rollback online status contains rollback evidence")
|
|
if state != "irreversible_failure" and not failure_empty:
|
|
raise ApplyPublicationError("non-failure online status contains failure evidence")
|
|
|
|
if state == "prepared":
|
|
if not rollback_empty or not failure_empty:
|
|
raise ApplyPublicationError("prepared online status evidence is invalid")
|
|
return row
|
|
|
|
if state == "rolled_back":
|
|
rollback = _validate_rollback_response(
|
|
row["rollback_response"],
|
|
manifest=manifest,
|
|
key=key,
|
|
transaction_id=transaction_id,
|
|
)
|
|
if row["rollback_response_sha256"] != batch._sha256_json(rollback):
|
|
raise ApplyPublicationError("online rollback status hash drift")
|
|
return row
|
|
|
|
if state == "irreversible_failure":
|
|
_require_text(row["failure_code"], "online failure_code", maximum=128)
|
|
failure_at = batch._parse_time(row["failure_at"], "online failure_at")
|
|
batch._require_sha(
|
|
row["failure_evidence_sha256"], "online failure evidence SHA-256"
|
|
)
|
|
if failure_at > status_updated_at:
|
|
raise ApplyPublicationError("online failure postdates its status update")
|
|
return row
|
|
|
|
expected_publication_results = batch._expected_apply_item_results(manifest)
|
|
expected_runtime_results = expected_runtime_snapshot_results(manifest)
|
|
online_transaction_receipt = row["online_transaction_receipt"]
|
|
runtime_snapshot_receipt = row["runtime_snapshot_receipt"]
|
|
if not isinstance(online_transaction_receipt, dict) or not isinstance(
|
|
runtime_snapshot_receipt, dict
|
|
):
|
|
raise ApplyPublicationError("committed online status omits signed receipt evidence")
|
|
online_receipt = batch.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,
|
|
)
|
|
batch.validate_runtime_snapshot_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
approval,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
online_receipt,
|
|
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,
|
|
)
|
|
if (
|
|
row["publication_item_results"] != expected_publication_results
|
|
or row["publication_item_results"]
|
|
!= online_transaction_receipt["payload"]["item_results"]
|
|
or row["publication_item_count"] != 74
|
|
or row["runtime_snapshot_dependencies"] != expected_runtime_results
|
|
or row["runtime_snapshot_dependencies"]
|
|
!= runtime_snapshot_receipt["payload"]["runtime_snapshot_dependencies"]
|
|
or row["runtime_snapshot_dependency_count"] != 14
|
|
or row["participant_evidence"]
|
|
!= online_transaction_receipt["payload"]["participant_evidence"]
|
|
or row["online_transaction_receipt_sha256"]
|
|
!= batch._sha256_json(online_transaction_receipt)
|
|
or row["runtime_snapshot_receipt_sha256"]
|
|
!= batch._sha256_json(runtime_snapshot_receipt)
|
|
or not rollback_empty
|
|
or not failure_empty
|
|
):
|
|
raise ApplyPublicationError("online read-after-write receipt verification failed")
|
|
latest_commit_evidence_at = max(
|
|
batch._parse_time(
|
|
online_transaction_receipt["payload"]["committed_at"],
|
|
"online receipt committed_at",
|
|
),
|
|
batch._parse_time(
|
|
runtime_snapshot_receipt["payload"]["captured_at"],
|
|
"runtime snapshot captured_at",
|
|
),
|
|
)
|
|
if status_updated_at < latest_commit_evidence_at:
|
|
raise ApplyPublicationError("online status predates its signed commit evidence")
|
|
if state == "committed_and_verified":
|
|
if not final_empty:
|
|
raise ApplyPublicationError("committed status contains finalization evidence")
|
|
return row
|
|
|
|
apply_receipt = row["apply_receipt"]
|
|
if not isinstance(apply_receipt, dict):
|
|
raise ApplyPublicationError("finalized status omits the signed apply receipt")
|
|
batch.validate_apply_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval,
|
|
approval_keyring,
|
|
approval_root_public_key,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
approval_raw_sha256,
|
|
apply_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,
|
|
)
|
|
if (
|
|
apply_receipt["payload"]["online_transaction_receipt"]
|
|
!= online_transaction_receipt
|
|
or apply_receipt["payload"]["runtime_snapshot_receipt"]
|
|
!= runtime_snapshot_receipt
|
|
or row["apply_receipt_sha256"] != batch._sha256_json(apply_receipt)
|
|
or (
|
|
expected_apply_receipt is not None
|
|
and apply_receipt != expected_apply_receipt
|
|
)
|
|
):
|
|
raise ApplyPublicationError("finalized status receipt binding drift")
|
|
finalized_at = batch._parse_time(row["finalized_at"], "online finalized_at")
|
|
countersigned_at = batch._parse_time(
|
|
apply_receipt["payload"]["countersigned_at"], "apply countersigned_at"
|
|
)
|
|
if finalized_at < countersigned_at or status_updated_at < finalized_at:
|
|
raise ApplyPublicationError("online finalized status timestamp drift")
|
|
return row
|
|
|
|
|
|
def _validate_compensation_hashes(
|
|
item: Mapping[str, Any],
|
|
label: str,
|
|
*,
|
|
unchanged_preexisting_sha256: str | None = None,
|
|
) -> tuple[int, int, int]:
|
|
if item["status"] != "verified":
|
|
raise ApplyPublicationError(f"{label} status drift")
|
|
if item["action"] == "restored_previous":
|
|
batch._require_sha(
|
|
item["pre_apply_content_sha256"],
|
|
f"{label} restored content SHA-256",
|
|
)
|
|
if item["post_compensation_content_sha256"] != item["pre_apply_content_sha256"]:
|
|
raise ApplyPublicationError(f"{label} restored content hash drift")
|
|
return 1, 0, 0
|
|
if item["action"] == "removed_new":
|
|
if (
|
|
item["pre_apply_content_sha256"] is not None
|
|
or item["post_compensation_content_sha256"] is not None
|
|
):
|
|
raise ApplyPublicationError(f"{label} removal evidence drift")
|
|
return 0, 1, 0
|
|
if item["action"] == "unchanged_preexisting":
|
|
if unchanged_preexisting_sha256 is None:
|
|
raise ApplyPublicationError(f"{label} cannot be unchanged preexisting")
|
|
expected_sha256 = batch._require_sha(
|
|
unchanged_preexisting_sha256,
|
|
f"{label} expected preexisting content SHA-256",
|
|
)
|
|
if (
|
|
item["pre_apply_content_sha256"] != expected_sha256
|
|
or item["post_compensation_content_sha256"] != expected_sha256
|
|
):
|
|
raise ApplyPublicationError(f"{label} unchanged content hash drift")
|
|
return 0, 0, 1
|
|
raise ApplyPublicationError(f"{label} action is unsupported")
|
|
|
|
|
|
def _validate_rollback_response(
|
|
response: Any,
|
|
*,
|
|
manifest: Mapping[str, Any],
|
|
key: str,
|
|
transaction_id: str,
|
|
) -> dict[str, Any]:
|
|
row = _require_exact(
|
|
response,
|
|
"online rollback response",
|
|
(
|
|
"schema_version",
|
|
"idempotency_key",
|
|
"transaction_id",
|
|
"state",
|
|
"participant_states_before",
|
|
"participant_states_after",
|
|
"compensation_item_results",
|
|
"runtime_snapshot_compensation_results",
|
|
"restored_item_count",
|
|
"removed_item_count",
|
|
"unchanged_item_count",
|
|
"rollback_evidence_sha256",
|
|
),
|
|
)
|
|
if (
|
|
row["schema_version"] != ROLLBACK_RESPONSE_SCHEMA_VERSION
|
|
or row["idempotency_key"] != key
|
|
or row["transaction_id"] != transaction_id
|
|
or row["state"] != "rolled_back"
|
|
or not isinstance(row["restored_item_count"], int)
|
|
or isinstance(row["restored_item_count"], bool)
|
|
or row["restored_item_count"] < 0
|
|
or not isinstance(row["removed_item_count"], int)
|
|
or isinstance(row["removed_item_count"], bool)
|
|
or row["removed_item_count"] < 0
|
|
or not isinstance(row["unchanged_item_count"], int)
|
|
or isinstance(row["unchanged_item_count"], bool)
|
|
or row["unchanged_item_count"] < 0
|
|
):
|
|
raise ApplyPublicationError("online rollback evidence is invalid")
|
|
expected_participants = ["ai-platform", "contract-platform"]
|
|
before = row["participant_states_before"]
|
|
after = row["participant_states_after"]
|
|
if not isinstance(before, list) or not isinstance(after, list):
|
|
raise ApplyPublicationError("online rollback participant evidence is invalid")
|
|
validated_states: dict[str, dict[str, dict[str, Any]]] = {}
|
|
for label, states in (("before", before), ("after", after)):
|
|
if [item.get("participant") for item in states if isinstance(item, dict)] != expected_participants:
|
|
raise ApplyPublicationError(f"online rollback {label} participant identities drift")
|
|
validated_states[label] = {}
|
|
for index, value in enumerate(states):
|
|
state_row = _require_exact(
|
|
value,
|
|
f"online rollback {label} participant {index}",
|
|
(
|
|
"participant",
|
|
"state",
|
|
"write_count",
|
|
"runtime_snapshot_count",
|
|
"durable_state_sha256",
|
|
),
|
|
)
|
|
if (
|
|
not isinstance(state_row["state"], str)
|
|
or not isinstance(state_row["write_count"], int)
|
|
or isinstance(state_row["write_count"], bool)
|
|
or state_row["write_count"] < 0
|
|
or state_row["write_count"] > 74
|
|
or not isinstance(state_row["runtime_snapshot_count"], int)
|
|
or isinstance(state_row["runtime_snapshot_count"], bool)
|
|
or state_row["runtime_snapshot_count"] < 0
|
|
or state_row["runtime_snapshot_count"] > 14
|
|
):
|
|
raise ApplyPublicationError(
|
|
f"online rollback {label} participant state drift"
|
|
)
|
|
supplied_state_sha = batch._require_sha(
|
|
state_row["durable_state_sha256"],
|
|
f"online rollback {label} participant state SHA-256",
|
|
)
|
|
if supplied_state_sha != batch._sha256_json(
|
|
{
|
|
key_name: child
|
|
for key_name, child in state_row.items()
|
|
if key_name != "durable_state_sha256"
|
|
}
|
|
):
|
|
raise ApplyPublicationError(
|
|
f"online rollback {label} participant durable state hash drift"
|
|
)
|
|
validated_states[label][state_row["participant"]] = state_row
|
|
|
|
ai_before = validated_states["before"]["ai-platform"]
|
|
contract_before = validated_states["before"]["contract-platform"]
|
|
ai_was_committed = ai_before["state"] == "committed"
|
|
if ai_was_committed:
|
|
if (
|
|
ai_before["write_count"] != 14
|
|
or ai_before["runtime_snapshot_count"] != 14
|
|
):
|
|
raise ApplyPublicationError(
|
|
"online rollback AI committed state must contain exact 14+14 counts"
|
|
)
|
|
elif (
|
|
ai_before["state"] not in {"not_started", "prepared"}
|
|
or ai_before["write_count"] != 0
|
|
or ai_before["runtime_snapshot_count"] != 0
|
|
):
|
|
raise ApplyPublicationError("online rollback AI before state is not compensatable")
|
|
if (
|
|
contract_before["state"] not in {"not_started", "prepared"}
|
|
or contract_before["write_count"] != 0
|
|
or contract_before["runtime_snapshot_count"] != 0
|
|
):
|
|
raise ApplyPublicationError(
|
|
"online rollback Contract before state crossed the irreversible boundary"
|
|
)
|
|
expected_after = {
|
|
"ai-platform": "compensated_and_verified" if ai_was_committed else "unchanged",
|
|
"contract-platform": "unchanged",
|
|
}
|
|
for participant, expected_state in expected_after.items():
|
|
state_row = validated_states["after"][participant]
|
|
if (
|
|
state_row["state"] != expected_state
|
|
or state_row["write_count"] != 0
|
|
or state_row["runtime_snapshot_count"] != 0
|
|
):
|
|
raise ApplyPublicationError(
|
|
f"online rollback {participant} after state is not exact"
|
|
)
|
|
|
|
item_results = row["compensation_item_results"]
|
|
if not isinstance(item_results, list):
|
|
raise ApplyPublicationError("online rollback item results are invalid")
|
|
expected_identities = sorted(
|
|
(item["kind"], item["item_id"])
|
|
for item in batch._expected_apply_item_results(manifest)
|
|
if item["kind"] == "prompt_module"
|
|
)
|
|
supplied_identities: list[tuple[str, str]] = []
|
|
restored_count = 0
|
|
removed_count = 0
|
|
unchanged_count = 0
|
|
for index, value in enumerate(item_results):
|
|
item = _require_exact(
|
|
value,
|
|
f"online rollback item {index}",
|
|
(
|
|
"kind",
|
|
"item_id",
|
|
"pre_apply_content_sha256",
|
|
"post_compensation_content_sha256",
|
|
"action",
|
|
"status",
|
|
),
|
|
)
|
|
identity = (item["kind"], item["item_id"])
|
|
supplied_identities.append(identity)
|
|
restored, removed, unchanged = _validate_compensation_hashes(
|
|
item, f"online rollback item {index}"
|
|
)
|
|
restored_count += restored
|
|
removed_count += removed
|
|
unchanged_count += unchanged
|
|
required_publication_identities = expected_identities if ai_was_committed else []
|
|
if supplied_identities != required_publication_identities:
|
|
raise ApplyPublicationError(
|
|
"online rollback Prompt compensation identities/order drift"
|
|
)
|
|
|
|
runtime_results = row["runtime_snapshot_compensation_results"]
|
|
if not isinstance(runtime_results, list):
|
|
raise ApplyPublicationError(
|
|
"online rollback runtime snapshot results are invalid"
|
|
)
|
|
runtime_expectations = {
|
|
(item["kind"], item["runtime_key"], item["version"]): item
|
|
for item in expected_runtime_snapshot_results(manifest)
|
|
}
|
|
expected_runtime_identities = sorted(runtime_expectations)
|
|
supplied_runtime_identities: list[tuple[str, str, str]] = []
|
|
for index, value in enumerate(runtime_results):
|
|
item = _require_exact(
|
|
value,
|
|
f"online rollback runtime snapshot item {index}",
|
|
(
|
|
"kind",
|
|
"runtime_key",
|
|
"version",
|
|
"pre_apply_content_sha256",
|
|
"post_compensation_content_sha256",
|
|
"action",
|
|
"status",
|
|
),
|
|
)
|
|
identity = (item["kind"], item["runtime_key"], item["version"])
|
|
supplied_runtime_identities.append(identity)
|
|
expected_runtime = runtime_expectations.get(identity)
|
|
unchanged_sha256 = (
|
|
expected_runtime["expected_content_sha256"]
|
|
if expected_runtime is not None
|
|
and expected_runtime["kind"] == "existing_domain_prompt"
|
|
else None
|
|
)
|
|
restored, removed, unchanged = _validate_compensation_hashes(
|
|
item,
|
|
f"online rollback runtime snapshot item {index}",
|
|
unchanged_preexisting_sha256=unchanged_sha256,
|
|
)
|
|
if unchanged_sha256 is not None and item["action"] != "unchanged_preexisting":
|
|
raise ApplyPublicationError(
|
|
"online rollback preexisting runtime dependency was falsely compensated"
|
|
)
|
|
restored_count += restored
|
|
removed_count += removed
|
|
unchanged_count += unchanged
|
|
required_runtime_identities = expected_runtime_identities if ai_was_committed else []
|
|
if supplied_runtime_identities != required_runtime_identities:
|
|
raise ApplyPublicationError(
|
|
"online rollback runtime snapshot identities/order drift"
|
|
)
|
|
if (
|
|
restored_count != row["restored_item_count"]
|
|
or removed_count != row["removed_item_count"]
|
|
or unchanged_count != row["unchanged_item_count"]
|
|
):
|
|
raise ApplyPublicationError("online rollback item counts drift")
|
|
rollback_sha = batch._require_sha(
|
|
row["rollback_evidence_sha256"], "online rollback evidence SHA-256"
|
|
)
|
|
if rollback_sha != batch._sha256_json(
|
|
{
|
|
key_name: child
|
|
for key_name, child in row.items()
|
|
if key_name != "rollback_evidence_sha256"
|
|
}
|
|
):
|
|
raise ApplyPublicationError("online rollback evidence hash drift")
|
|
return row
|
|
|
|
|
|
def _compensate(
|
|
client: JsonHttpClient,
|
|
*,
|
|
manifest: Mapping[str, Any],
|
|
key: str,
|
|
transaction_id: str,
|
|
headers: Mapping[str, str],
|
|
manifest_raw_sha256: str,
|
|
approval_raw_sha256: str,
|
|
legal_review_binding_sha256: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
) -> dict[str, Any]:
|
|
request = {
|
|
"schema_version": ROLLBACK_REQUEST_SCHEMA_VERSION,
|
|
"idempotency_key": key,
|
|
"transaction_id": transaction_id,
|
|
"batch_manifest_raw_sha256": manifest_raw_sha256,
|
|
"approval_receipt_sha256": approval_raw_sha256,
|
|
"legal_source_review_binding_sha256": legal_review_binding_sha256,
|
|
"environment_id": environment_id,
|
|
"tenant_id": tenant_id,
|
|
"transaction_mode": batch.TRANSACTION_MODE,
|
|
"cross_database_atomicity": False,
|
|
}
|
|
response = client.request(
|
|
"POST",
|
|
f"/internal/rc06/publication-transactions/{transaction_id}/rollback",
|
|
payload=request,
|
|
headers=headers,
|
|
)
|
|
return _validate_rollback_response(
|
|
response,
|
|
manifest=manifest,
|
|
key=key,
|
|
transaction_id=transaction_id,
|
|
)
|
|
|
|
|
|
def _validate_execution_evidence(
|
|
evidence: Any,
|
|
*,
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval: Mapping[str, Any],
|
|
approval_raw_sha256: str,
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
online_service_public_key: Any,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
idempotency_key: str,
|
|
transaction_id: str,
|
|
) -> dict[str, Any]:
|
|
row = _require_exact(
|
|
evidence,
|
|
"online execution evidence",
|
|
(
|
|
"schema_version",
|
|
"idempotency_key",
|
|
"transaction_id",
|
|
"request_sha256",
|
|
"legal_source_review_binding_sha256",
|
|
"legal_source_review_expires_at",
|
|
"environment_id",
|
|
"tenant_id",
|
|
"transaction_mode",
|
|
"cross_database_atomicity",
|
|
"publication_item_count",
|
|
"runtime_snapshot_dependency_count",
|
|
"online_transaction_receipt",
|
|
"runtime_snapshot_receipt",
|
|
),
|
|
)
|
|
if (
|
|
row["schema_version"] != EXECUTION_EVIDENCE_SCHEMA_VERSION
|
|
or row["idempotency_key"] != idempotency_key
|
|
or row["transaction_id"] != transaction_id
|
|
or not batch.HEX64_RE.fullmatch(str(row["request_sha256"]))
|
|
or row["legal_source_review_binding_sha256"]
|
|
!= legal_review_binding_sha256
|
|
or row["legal_source_review_expires_at"] != legal_review_expires_at
|
|
or row["environment_id"] != environment_id
|
|
or row["tenant_id"] != tenant_id
|
|
or row["transaction_mode"] != batch.TRANSACTION_MODE
|
|
or row["cross_database_atomicity"] is not False
|
|
or row["publication_item_count"] != 74
|
|
or row["runtime_snapshot_dependency_count"] != 14
|
|
):
|
|
raise ApplyPublicationError("online execution evidence identity drift")
|
|
online_receipt = batch.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,
|
|
)
|
|
batch.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,
|
|
)
|
|
return row
|
|
|
|
|
|
def _execution_from_transaction_status(
|
|
status: Mapping[str, Any],
|
|
*,
|
|
request_sha256: str,
|
|
legal_review_expires_at: str,
|
|
) -> dict[str, Any]:
|
|
if status["state"] not in {"committed_and_verified", "finalized"}:
|
|
raise ApplyPublicationError("online status does not contain committed execution evidence")
|
|
return {
|
|
"schema_version": EXECUTION_EVIDENCE_SCHEMA_VERSION,
|
|
"idempotency_key": status["idempotency_key"],
|
|
"transaction_id": status["transaction_id"],
|
|
"request_sha256": request_sha256,
|
|
"legal_source_review_binding_sha256": status[
|
|
"legal_source_review_binding_sha256"
|
|
],
|
|
"legal_source_review_expires_at": legal_review_expires_at,
|
|
"environment_id": status["environment_id"],
|
|
"tenant_id": status["tenant_id"],
|
|
"transaction_mode": status["transaction_mode"],
|
|
"cross_database_atomicity": status["cross_database_atomicity"],
|
|
"publication_item_count": status["publication_item_count"],
|
|
"runtime_snapshot_dependency_count": status[
|
|
"runtime_snapshot_dependency_count"
|
|
],
|
|
"online_transaction_receipt": status["online_transaction_receipt"],
|
|
"runtime_snapshot_receipt": status["runtime_snapshot_receipt"],
|
|
}
|
|
|
|
|
|
def _finalize_response_from_transaction_status(
|
|
status: Mapping[str, Any],
|
|
) -> dict[str, Any]:
|
|
if status["state"] != "finalized":
|
|
raise ApplyPublicationError("online status is not finalized")
|
|
return {
|
|
"schema_version": FINALIZE_RESPONSE_SCHEMA_VERSION,
|
|
"idempotency_key": status["idempotency_key"],
|
|
"transaction_id": status["transaction_id"],
|
|
"state": "finalized",
|
|
"no_go": False,
|
|
"apply_receipt_sha256": status["apply_receipt_sha256"],
|
|
"finalized_at": status["finalized_at"],
|
|
}
|
|
|
|
|
|
def _final_evidence_from_transaction_status(
|
|
execution: Mapping[str, Any],
|
|
status: Mapping[str, Any],
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"schema_version": FINAL_EVIDENCE_SCHEMA_VERSION,
|
|
"transaction_mode": batch.TRANSACTION_MODE,
|
|
"cross_database_atomicity": False,
|
|
"publication_item_count": 74,
|
|
"runtime_snapshot_dependency_count": 14,
|
|
"online_execution": execution,
|
|
"apply_receipt": status["apply_receipt"],
|
|
"online_finalize_response": _finalize_response_from_transaction_status(status),
|
|
"online_finalize_status": status,
|
|
}
|
|
|
|
|
|
def execute_online_publication_batch(
|
|
*,
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval: Mapping[str, Any],
|
|
approval_raw_sha256: str,
|
|
approval_keyring: Mapping[str, Any],
|
|
approval_keyring_raw_sha256: str,
|
|
approval_root_public_key: Ed25519PublicKey,
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
online_service_public_key: Any,
|
|
legal_review_request: Mapping[str, Any],
|
|
legal_review_decision: Mapping[str, Any],
|
|
legal_review_keyring: Mapping[str, Any],
|
|
legal_review_keyring_raw_sha256: str,
|
|
authority_challenge: Mapping[str, Any],
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
client: JsonHttpClient,
|
|
ledger: ApplyLedger,
|
|
now: datetime,
|
|
) -> dict[str, Any]:
|
|
"""Execute the online saga and persist evidence pending offline countersign."""
|
|
|
|
if now.tzinfo is None or now.utcoffset() is None:
|
|
raise ApplyPublicationError("online apply time must include a timezone")
|
|
checked_at = now.astimezone(UTC)
|
|
batch.validate_manifest(manifest)
|
|
if manifest["runtime_prompt_dependencies"]["items"] != batch._load_executable_prompt_dependencies(
|
|
REPO_ROOT
|
|
):
|
|
raise ApplyPublicationError(
|
|
"runtime Prompt name/version/content hashes differ from candidate executable source"
|
|
)
|
|
manifest_raw_sha256 = batch._require_sha(manifest_raw_sha256, "manifest raw SHA-256")
|
|
approval_raw_sha256 = batch._require_sha(approval_raw_sha256, "approval raw SHA-256")
|
|
approval_keyring_raw_sha256 = batch._require_sha(
|
|
approval_keyring_raw_sha256,
|
|
"approval keyring raw SHA-256",
|
|
)
|
|
approval_keyring_canonical_sha256 = batch._sha256_json(approval_keyring)
|
|
environment_id = _require_text(environment_id, "environment_id", maximum=256)
|
|
tenant_id = _require_text(tenant_id, "tenant_id", maximum=64)
|
|
batch.validate_approval_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval,
|
|
approval_keyring,
|
|
approval_root_public_key,
|
|
)
|
|
approval_approved_at = batch._parse_time(
|
|
approval["payload"]["approved_at"], "approval approved_at"
|
|
)
|
|
approval_expires_at = batch._parse_time(
|
|
approval["payload"]["expires_at"], "approval expires_at"
|
|
)
|
|
if checked_at < approval_approved_at or checked_at >= approval_expires_at:
|
|
raise ApplyPublicationError("approval is not currently valid for online apply")
|
|
legal_review_summary, legal_review_binding_sha256 = validate_scene_legal_review(
|
|
manifest,
|
|
approval,
|
|
legal_review_request,
|
|
legal_review_decision,
|
|
legal_review_keyring,
|
|
legal_review_keyring_raw_sha256,
|
|
now=now,
|
|
)
|
|
legal_review_expires_at = legal_review_summary["expires_at"]
|
|
keys = {
|
|
batch._public_key_spki(approval_root_public_key),
|
|
batch._public_key_spki(apply_service_public_key),
|
|
batch._public_key_spki(online_service_public_key),
|
|
}
|
|
if len(keys) != 3:
|
|
raise ApplyPublicationError("approval, apply and online service keys must be distinct")
|
|
key = _idempotency_key(
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
environment_id,
|
|
tenant_id,
|
|
)
|
|
transaction_id = _transaction_id(key)
|
|
validated_challenge = batch.validate_apply_challenge(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
apply_service_public_key,
|
|
authority_challenge,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
challenge_issued_at = batch._parse_time(
|
|
validated_challenge["payload"]["issued_at"], "apply challenge issued_at"
|
|
)
|
|
challenge_expires_at = batch._parse_time(
|
|
validated_challenge["payload"]["expires_at"], "apply challenge expires_at"
|
|
)
|
|
challenge_is_current = challenge_issued_at <= checked_at < challenge_expires_at
|
|
request_payload = {
|
|
"schema_version": APPLY_REQUEST_SCHEMA_VERSION,
|
|
"idempotency_key": key,
|
|
"transaction_id": transaction_id,
|
|
"environment_id": environment_id,
|
|
"tenant_id": tenant_id,
|
|
"transaction_mode": batch.TRANSACTION_MODE,
|
|
"cross_database_atomicity": False,
|
|
"batch_manifest_raw_sha256": manifest_raw_sha256,
|
|
"batch_manifest_payload_sha256": manifest["manifest_payload_sha256"],
|
|
"approval_receipt_sha256": approval_raw_sha256,
|
|
"approval_keyring": approval_keyring,
|
|
"approval_keyring_raw_sha256": approval_keyring_raw_sha256,
|
|
"approval_keyring_canonical_sha256": approval_keyring_canonical_sha256,
|
|
"legal_source_review_binding_sha256": legal_review_binding_sha256,
|
|
"legal_source_review_expires_at": legal_review_expires_at,
|
|
"authority_challenge": authority_challenge,
|
|
"manifest": manifest,
|
|
"approval_receipt": approval,
|
|
"legal_source_review_request": legal_review_request,
|
|
"legal_source_review_decision": legal_review_decision,
|
|
"legal_source_review_keyring": legal_review_keyring,
|
|
"legal_source_review_keyring_raw_sha256": legal_review_keyring_raw_sha256,
|
|
"expected_publication_item_results": batch._expected_apply_item_results(manifest),
|
|
"expected_publication_item_count": 74,
|
|
"expected_runtime_snapshot_dependencies": expected_runtime_snapshot_results(manifest),
|
|
"expected_runtime_snapshot_dependency_count": 14,
|
|
}
|
|
request_sha256 = batch._sha256_json(request_payload)
|
|
reservation, replay = ledger.reserve(
|
|
key,
|
|
request_sha256,
|
|
now,
|
|
initial_dispatch_authorized=challenge_is_current,
|
|
)
|
|
if replay is not None:
|
|
if replay.get("schema_version") == FINAL_EVIDENCE_SCHEMA_VERSION:
|
|
replay = replay.get("online_execution")
|
|
return _validate_execution_evidence(
|
|
replay,
|
|
manifest=manifest,
|
|
manifest_raw_sha256=manifest_raw_sha256,
|
|
approval=approval,
|
|
approval_raw_sha256=approval_raw_sha256,
|
|
apply_service_public_key=apply_service_public_key,
|
|
online_service_public_key=online_service_public_key,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
idempotency_key=key,
|
|
transaction_id=transaction_id,
|
|
)
|
|
headers = _headers(
|
|
key,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
tenant_id,
|
|
)
|
|
|
|
def validate_remote_status(
|
|
value: Any,
|
|
*,
|
|
minimum_state_version: int = 1,
|
|
expected_apply_receipt: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
return _validate_transaction_status(
|
|
value,
|
|
manifest=manifest,
|
|
manifest_raw_sha256=manifest_raw_sha256,
|
|
approval=approval,
|
|
approval_raw_sha256=approval_raw_sha256,
|
|
approval_keyring=approval_keyring,
|
|
approval_root_public_key=approval_root_public_key,
|
|
apply_service_public_key=apply_service_public_key,
|
|
online_service_public_key=online_service_public_key,
|
|
request_sha256=request_sha256,
|
|
key=key,
|
|
transaction_id=transaction_id,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
minimum_state_version=minimum_state_version,
|
|
expected_apply_receipt=expected_apply_receipt,
|
|
)
|
|
|
|
def adopt_committed_status(status_row: Mapping[str, Any]) -> dict[str, Any]:
|
|
execution = _execution_from_transaction_status(
|
|
status_row,
|
|
request_sha256=request_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
)
|
|
execution = _validate_execution_evidence(
|
|
execution,
|
|
manifest=manifest,
|
|
manifest_raw_sha256=manifest_raw_sha256,
|
|
approval=approval,
|
|
approval_raw_sha256=approval_raw_sha256,
|
|
apply_service_public_key=apply_service_public_key,
|
|
online_service_public_key=online_service_public_key,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
idempotency_key=key,
|
|
transaction_id=transaction_id,
|
|
)
|
|
if status_row["state"] == "finalized":
|
|
final_evidence = _final_evidence_from_transaction_status(execution, status_row)
|
|
ledger.state(key, "succeeded", status_row, now, receipt=final_evidence)
|
|
else:
|
|
ledger.state(
|
|
key,
|
|
"online_committed_pending_countersign",
|
|
status_row,
|
|
now,
|
|
receipt=execution,
|
|
)
|
|
return execution
|
|
|
|
if reservation in {"probe", "recover"} and not challenge_is_current:
|
|
try:
|
|
reconciliation = client.request(
|
|
"GET",
|
|
f"/internal/rc06/publication-transactions/{transaction_id}/status",
|
|
payload=None,
|
|
headers=headers,
|
|
)
|
|
reconciliation = validate_remote_status(reconciliation)
|
|
except Exception as exc:
|
|
ledger.state(
|
|
key,
|
|
"commit_status_unknown",
|
|
{"reconciliation_error": str(exc)},
|
|
now,
|
|
error="expired challenge recovery could not establish remote commit status",
|
|
)
|
|
raise ApplyPublicationError(
|
|
"apply challenge is not currently valid for an initial dispatch and no existing transaction was proven"
|
|
) from exc
|
|
remote_state = reconciliation["state"]
|
|
if remote_state in {"committed_and_verified", "finalized"}:
|
|
return adopt_committed_status(reconciliation)
|
|
if remote_state == "rolled_back":
|
|
ledger.state(key, "rolled_back", reconciliation, now)
|
|
raise ApplyPublicationError("online apply was already rolled back")
|
|
if remote_state == "prepared":
|
|
ledger.state(
|
|
key,
|
|
"expired_challenge_recovery_no_go",
|
|
reconciliation,
|
|
now,
|
|
error="remote transaction was not terminally committed before challenge expiry",
|
|
)
|
|
raise ApplyPublicationError(
|
|
"expired challenge cannot authorize a new or non-terminal retransmission"
|
|
)
|
|
# irreversible_failure proves that this exact request already crossed
|
|
# the no-rollback boundary. Replaying it may only recover its evidence.
|
|
if remote_state != "irreversible_failure":
|
|
raise ApplyPublicationError("online transaction state is unsupported")
|
|
dispatch_started = False
|
|
response: Any = None
|
|
try:
|
|
dispatch_started = True
|
|
response = client.request(
|
|
"POST",
|
|
"/internal/rc06/publication-transactions/apply",
|
|
payload=request_payload,
|
|
headers={**headers, "X-RC06-Request-SHA256": request_sha256},
|
|
)
|
|
response = _validate_apply_response(
|
|
response,
|
|
key=key,
|
|
request_sha256=request_sha256,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
transaction_id=transaction_id,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
online_receipt = response["online_transaction_receipt"]
|
|
runtime_receipt = response["runtime_snapshot_receipt"]
|
|
batch.validate_online_transaction_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
approval,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
online_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,
|
|
)
|
|
validate_runtime_snapshot_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
legal_review_expires_at,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
online_receipt,
|
|
runtime_receipt,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
online_payload = online_receipt["payload"]
|
|
if online_payload["transaction_id"] != transaction_id:
|
|
raise ApplyPublicationError("signed apply receipt is not bound to the idempotency identity")
|
|
ledger.state(key, "verifying", response, now)
|
|
status = client.request(
|
|
"GET",
|
|
f"/internal/rc06/publication-transactions/{transaction_id}/status",
|
|
payload=None,
|
|
headers=headers,
|
|
)
|
|
status = validate_remote_status(status, minimum_state_version=3)
|
|
if status["state"] not in {"committed_and_verified", "finalized"}:
|
|
raise ApplyPublicationError("online apply did not reach a committed status")
|
|
if (
|
|
status["online_transaction_receipt"] != online_receipt
|
|
or status["runtime_snapshot_receipt"] != runtime_receipt
|
|
):
|
|
raise ApplyPublicationError("online apply response and durable status receipt drift")
|
|
return adopt_committed_status(status)
|
|
except Exception as exc:
|
|
if not isinstance(exc, ApplyPublicationError):
|
|
failure = ApplyPublicationError("unexpected online apply failure")
|
|
failure.__cause__ = exc
|
|
else:
|
|
failure = exc
|
|
if dispatch_started:
|
|
try:
|
|
reconciliation = client.request(
|
|
"GET",
|
|
f"/internal/rc06/publication-transactions/{transaction_id}/status",
|
|
payload=None,
|
|
headers=headers,
|
|
)
|
|
reconciliation = validate_remote_status(reconciliation)
|
|
except Exception as reconciliation_exc:
|
|
ledger.state(
|
|
key,
|
|
"commit_status_unknown",
|
|
{
|
|
"apply_error": str(failure),
|
|
"reconciliation_error": str(reconciliation_exc),
|
|
},
|
|
now,
|
|
error=f"{failure}; commit status is unknown",
|
|
)
|
|
raise ApplyPublicationError(
|
|
"online apply evidence failed and commit status is unknown; no rollback was attempted"
|
|
) from reconciliation_exc
|
|
remote_state = reconciliation["state"]
|
|
if remote_state in {"committed_and_verified", "finalized"}:
|
|
return adopt_committed_status(reconciliation)
|
|
if remote_state == "irreversible_failure":
|
|
ledger.state(
|
|
key,
|
|
"irreversible_failure_no_go",
|
|
reconciliation,
|
|
now,
|
|
error=str(failure),
|
|
)
|
|
raise ApplyPublicationError(
|
|
"online saga crossed the irreversible boundary but evidence recovery is incomplete; rollback is forbidden"
|
|
) from failure
|
|
if remote_state == "rolled_back":
|
|
ledger.state(key, "rolled_back", reconciliation, now, error=str(failure))
|
|
raise failure
|
|
if remote_state != "prepared":
|
|
ledger.state(
|
|
key,
|
|
"commit_status_unknown",
|
|
reconciliation,
|
|
now,
|
|
error=str(failure),
|
|
)
|
|
raise ApplyPublicationError(
|
|
"online saga state is not safely compensatable; no rollback was attempted"
|
|
) from failure
|
|
try:
|
|
rollback = _compensate(
|
|
client,
|
|
manifest=manifest,
|
|
key=key,
|
|
transaction_id=transaction_id,
|
|
headers=headers,
|
|
manifest_raw_sha256=manifest_raw_sha256,
|
|
approval_raw_sha256=approval_raw_sha256,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
ledger.state(key, "rolled_back", rollback, now, error=str(failure))
|
|
except Exception as rollback_exc:
|
|
ledger.state(
|
|
key,
|
|
"compensation_failed",
|
|
{"apply_error": str(failure), "rollback_error": str(rollback_exc)},
|
|
now,
|
|
error=f"{failure}; compensation failed: {rollback_exc}",
|
|
)
|
|
raise ApplyPublicationError(
|
|
"online apply failed and compensation could not be proven"
|
|
) from rollback_exc
|
|
else:
|
|
ledger.state(key, "failed", {"error": str(failure)}, now, error=str(failure))
|
|
raise failure
|
|
|
|
|
|
def finalize_publication_batch(
|
|
*,
|
|
manifest: Mapping[str, Any],
|
|
manifest_raw_sha256: str,
|
|
approval: Mapping[str, Any],
|
|
approval_raw_sha256: str,
|
|
approval_keyring: Mapping[str, Any],
|
|
approval_root_public_key: Ed25519PublicKey,
|
|
apply_service_public_key: Ed25519PublicKey,
|
|
online_service_public_key: Any,
|
|
legal_review_binding_sha256: str,
|
|
legal_review_expires_at: str,
|
|
environment_id: str,
|
|
tenant_id: str,
|
|
online_execution: Mapping[str, Any],
|
|
apply_receipt: Mapping[str, Any],
|
|
client: JsonHttpClient,
|
|
ledger: ApplyLedger,
|
|
now: datetime,
|
|
) -> dict[str, Any]:
|
|
"""Verify the offline countersign and move the local ledger to final success."""
|
|
|
|
key = batch.publication_idempotency_key(
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
environment_id,
|
|
tenant_id,
|
|
)
|
|
transaction_id = batch.publication_transaction_id(key)
|
|
execution = _validate_execution_evidence(
|
|
online_execution,
|
|
manifest=manifest,
|
|
manifest_raw_sha256=manifest_raw_sha256,
|
|
approval=approval,
|
|
approval_raw_sha256=approval_raw_sha256,
|
|
apply_service_public_key=apply_service_public_key,
|
|
online_service_public_key=online_service_public_key,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
idempotency_key=key,
|
|
transaction_id=transaction_id,
|
|
)
|
|
batch.validate_apply_receipt(
|
|
manifest,
|
|
manifest_raw_sha256,
|
|
approval,
|
|
approval_keyring,
|
|
approval_root_public_key,
|
|
apply_service_public_key,
|
|
online_service_public_key,
|
|
approval_raw_sha256,
|
|
apply_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,
|
|
)
|
|
if (
|
|
apply_receipt["payload"]["online_transaction_receipt"]
|
|
!= execution["online_transaction_receipt"]
|
|
or apply_receipt["payload"]["runtime_snapshot_receipt"]
|
|
!= execution["runtime_snapshot_receipt"]
|
|
):
|
|
raise ApplyPublicationError("offline countersign is not bound to this receipt pair")
|
|
apply_receipt_sha256 = batch._sha256_json(apply_receipt)
|
|
state, stored = ledger.load(key, execution["request_sha256"])
|
|
if state not in {
|
|
"online_committed_pending_countersign",
|
|
"finalize_failed_no_go",
|
|
"succeeded",
|
|
}:
|
|
raise ApplyPublicationError("online saga is not eligible for offline finalization")
|
|
|
|
def validate_remote_status(value: Any, *, minimum_state_version: int) -> dict[str, Any]:
|
|
return _validate_transaction_status(
|
|
value,
|
|
manifest=manifest,
|
|
manifest_raw_sha256=manifest_raw_sha256,
|
|
approval=approval,
|
|
approval_raw_sha256=approval_raw_sha256,
|
|
approval_keyring=approval_keyring,
|
|
approval_root_public_key=approval_root_public_key,
|
|
apply_service_public_key=apply_service_public_key,
|
|
online_service_public_key=online_service_public_key,
|
|
request_sha256=execution["request_sha256"],
|
|
key=key,
|
|
transaction_id=transaction_id,
|
|
legal_review_binding_sha256=legal_review_binding_sha256,
|
|
legal_review_expires_at=legal_review_expires_at,
|
|
environment_id=environment_id,
|
|
tenant_id=tenant_id,
|
|
minimum_state_version=minimum_state_version,
|
|
expected_apply_receipt=apply_receipt,
|
|
)
|
|
|
|
if state == "succeeded":
|
|
stored_final = _require_exact(
|
|
stored,
|
|
"stored finalized evidence",
|
|
(
|
|
"schema_version",
|
|
"transaction_mode",
|
|
"cross_database_atomicity",
|
|
"publication_item_count",
|
|
"runtime_snapshot_dependency_count",
|
|
"online_execution",
|
|
"apply_receipt",
|
|
"online_finalize_response",
|
|
"online_finalize_status",
|
|
),
|
|
)
|
|
if (
|
|
stored_final["schema_version"] != FINAL_EVIDENCE_SCHEMA_VERSION
|
|
or stored_final["online_execution"] != execution
|
|
or stored_final["apply_receipt"] != apply_receipt
|
|
):
|
|
raise ApplyPublicationError("stored finalized evidence differs from the exact replay")
|
|
stored_status = validate_remote_status(
|
|
stored_final["online_finalize_status"], minimum_state_version=4
|
|
)
|
|
if stored_final != _final_evidence_from_transaction_status(execution, stored_status):
|
|
raise ApplyPublicationError("stored finalized evidence is internally inconsistent")
|
|
return stored_final
|
|
if stored != execution:
|
|
raise ApplyPublicationError("supplied online evidence differs from the durable ledger")
|
|
|
|
finalize_request = {
|
|
"schema_version": FINALIZE_REQUEST_SCHEMA_VERSION,
|
|
"idempotency_key": key,
|
|
"transaction_id": transaction_id,
|
|
"environment_id": environment_id,
|
|
"tenant_id": tenant_id,
|
|
"batch_manifest_raw_sha256": manifest_raw_sha256,
|
|
"approval_receipt_sha256": approval_raw_sha256,
|
|
"legal_source_review_binding_sha256": legal_review_binding_sha256,
|
|
"online_transaction_receipt_sha256": batch._sha256_json(
|
|
execution["online_transaction_receipt"]
|
|
),
|
|
"runtime_snapshot_receipt_sha256": batch._sha256_json(
|
|
execution["runtime_snapshot_receipt"]
|
|
),
|
|
"apply_receipt_sha256": apply_receipt_sha256,
|
|
"apply_receipt": apply_receipt,
|
|
}
|
|
headers = _headers(
|
|
key,
|
|
manifest_raw_sha256,
|
|
approval_raw_sha256,
|
|
legal_review_binding_sha256,
|
|
tenant_id,
|
|
)
|
|
response_row: dict[str, Any] | None = None
|
|
try:
|
|
response = client.request(
|
|
"POST",
|
|
f"/internal/rc06/publication-transactions/{transaction_id}/finalize",
|
|
payload=finalize_request,
|
|
headers={
|
|
**headers,
|
|
"X-RC06-Request-SHA256": batch._sha256_json(finalize_request),
|
|
},
|
|
)
|
|
response_row = _require_exact(
|
|
response,
|
|
"online finalize response",
|
|
(
|
|
"schema_version",
|
|
"idempotency_key",
|
|
"transaction_id",
|
|
"state",
|
|
"no_go",
|
|
"apply_receipt_sha256",
|
|
"finalized_at",
|
|
),
|
|
)
|
|
if (
|
|
response_row["schema_version"] != FINALIZE_RESPONSE_SCHEMA_VERSION
|
|
or response_row["idempotency_key"] != key
|
|
or response_row["transaction_id"] != transaction_id
|
|
or response_row["state"] != "finalized"
|
|
or response_row["no_go"] is not False
|
|
or response_row["apply_receipt_sha256"] != apply_receipt_sha256
|
|
):
|
|
raise ApplyPublicationError("online finalize response identity drift")
|
|
finalized_at = batch._parse_time(
|
|
response_row["finalized_at"], "online finalized_at"
|
|
)
|
|
if finalized_at < batch._parse_time(
|
|
apply_receipt["payload"]["countersigned_at"], "apply countersigned_at"
|
|
):
|
|
raise ApplyPublicationError("online finalize predates the offline countersign")
|
|
status = client.request(
|
|
"GET",
|
|
f"/internal/rc06/publication-transactions/{transaction_id}/status",
|
|
payload=None,
|
|
headers=headers,
|
|
)
|
|
status_row = validate_remote_status(status, minimum_state_version=4)
|
|
if status_row["state"] != "finalized":
|
|
raise ApplyPublicationError("online finalize did not reach finalized status")
|
|
if response_row != _finalize_response_from_transaction_status(status_row):
|
|
raise ApplyPublicationError("online finalize response and durable status drift")
|
|
except Exception as exc:
|
|
failure = (
|
|
exc
|
|
if isinstance(exc, ApplyPublicationError)
|
|
else ApplyPublicationError("online finalize request failed")
|
|
)
|
|
try:
|
|
reconciliation = client.request(
|
|
"GET",
|
|
f"/internal/rc06/publication-transactions/{transaction_id}/status",
|
|
payload=None,
|
|
headers=headers,
|
|
)
|
|
reconciliation = validate_remote_status(
|
|
reconciliation, minimum_state_version=3
|
|
)
|
|
if reconciliation["state"] == "finalized":
|
|
final_evidence = _final_evidence_from_transaction_status(
|
|
execution, reconciliation
|
|
)
|
|
ledger.state(
|
|
key, "succeeded", reconciliation, now, receipt=final_evidence
|
|
)
|
|
return final_evidence
|
|
if reconciliation["state"] != "committed_and_verified":
|
|
raise ApplyPublicationError(
|
|
"online finalization reconciliation regressed from committed state"
|
|
)
|
|
except Exception as reconciliation_exc:
|
|
ledger.state(
|
|
key,
|
|
"finalize_failed_no_go",
|
|
{
|
|
"finalize_error": str(failure),
|
|
"reconciliation_error": str(reconciliation_exc),
|
|
},
|
|
now,
|
|
receipt=execution,
|
|
error=f"{failure}; finalization status is untrusted",
|
|
)
|
|
raise failure from reconciliation_exc
|
|
ledger.state(
|
|
key,
|
|
"finalize_failed_no_go",
|
|
{"finalize_error": str(failure), "status": reconciliation},
|
|
now,
|
|
receipt=execution,
|
|
error=str(failure),
|
|
)
|
|
raise failure
|
|
final_evidence = _final_evidence_from_transaction_status(execution, status_row)
|
|
ledger.state(key, "succeeded", final_evidence, now, receipt=final_evidence)
|
|
return final_evidence
|
|
|
|
|
|
# Backward-compatible import name with deliberately changed two-stage semantics:
|
|
# it executes online only and never fabricates an offline authority receipt.
|
|
apply_publication_batch = execute_online_publication_batch
|
|
|
|
|
|
def _read_public_key(path: Path, expected_sha256: str, label: str) -> Ed25519PublicKey:
|
|
return batch._read_pinned_raw_public_key(
|
|
path,
|
|
expected_sha256=expected_sha256,
|
|
label=label,
|
|
)
|
|
|
|
|
|
def _read_online_public_key(path: Path) -> Any:
|
|
return batch._read_pinned_spki_public_key(
|
|
path,
|
|
expected_sha256=batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256,
|
|
label="online service public key",
|
|
)
|
|
|
|
|
|
def _outside_repo(path: Path, label: str) -> Path:
|
|
resolved = path.resolve()
|
|
try:
|
|
resolved.relative_to(REPO_ROOT)
|
|
except ValueError:
|
|
return resolved
|
|
raise ApplyPublicationError(f"{label} must be stored outside the source worktree")
|
|
|
|
|
|
def _write_receipt(
|
|
path: Path,
|
|
receipt: Mapping[str, Any],
|
|
*,
|
|
canonical_only: bool = False,
|
|
) -> str:
|
|
raw = batch._canonical(receipt) + (b"" if canonical_only else b"\n")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if path.exists():
|
|
existing = path.read_bytes()
|
|
if existing != raw:
|
|
raise ApplyPublicationError("refusing to overwrite different online apply evidence")
|
|
return _sha256(existing)
|
|
temporary = path.with_name(path.name + f".tmp-{os.getpid()}")
|
|
try:
|
|
with temporary.open("xb") as handle:
|
|
handle.write(raw)
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
os.replace(temporary, path)
|
|
try:
|
|
path.chmod(0o600)
|
|
except OSError:
|
|
pass
|
|
finally:
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
return _sha256(raw)
|
|
|
|
|
|
def _add_common_arguments(parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument("--manifest", type=Path, required=True)
|
|
parser.add_argument("--manifest-sha256", required=True)
|
|
parser.add_argument("--approval", type=Path, required=True)
|
|
parser.add_argument("--approval-sha256", required=True)
|
|
parser.add_argument("--approval-keyring", type=Path, required=True)
|
|
parser.add_argument("--approval-keyring-sha256", required=True)
|
|
parser.add_argument("--approval-root-public-key", type=Path, required=True)
|
|
parser.add_argument("--apply-service-public-key", type=Path, required=True)
|
|
parser.add_argument("--online-service-public-key", type=Path, required=True)
|
|
parser.add_argument("--legal-review-request", type=Path, required=True)
|
|
parser.add_argument("--legal-review-request-sha256", required=True)
|
|
parser.add_argument("--legal-review-decision", type=Path, required=True)
|
|
parser.add_argument("--legal-review-decision-sha256", required=True)
|
|
parser.add_argument("--legal-review-keyring", type=Path, required=True)
|
|
parser.add_argument("--legal-review-keyring-sha256", required=True)
|
|
parser.add_argument("--environment-id", required=True)
|
|
parser.add_argument("--tenant-id", required=True)
|
|
parser.add_argument("--ledger", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
execute = commands.add_parser(
|
|
"execute-online", help="execute and verify the online durable saga"
|
|
)
|
|
_add_common_arguments(execute)
|
|
execute.add_argument("--authority-challenge", type=Path, required=True)
|
|
execute.add_argument("--authority-challenge-sha256", required=True)
|
|
execute.add_argument("--service-url", required=True)
|
|
execute.add_argument("--online-receipt-output", type=Path, required=True)
|
|
execute.add_argument("--runtime-receipt-output", type=Path, required=True)
|
|
finalize = commands.add_parser(
|
|
"finalize", help="verify the offline countersign and finalize local evidence"
|
|
)
|
|
_add_common_arguments(finalize)
|
|
finalize.add_argument("--online-execution", type=Path, required=True)
|
|
finalize.add_argument("--online-execution-sha256", required=True)
|
|
finalize.add_argument("--apply-receipt", type=Path, required=True)
|
|
finalize.add_argument("--apply-receipt-sha256", required=True)
|
|
finalize.add_argument("--service-url", required=True)
|
|
args = parser.parse_args(argv)
|
|
ledger: ApplyLedger | None = None
|
|
try:
|
|
manifest, manifest_raw = batch.secure_read_json(
|
|
args.manifest, "publication manifest", args.manifest_sha256
|
|
)
|
|
approval, approval_raw = batch.secure_read_json(
|
|
args.approval, "approval receipt", args.approval_sha256
|
|
)
|
|
keyring, keyring_raw = batch.secure_read_json(
|
|
args.approval_keyring,
|
|
"approval keyring",
|
|
args.approval_keyring_sha256,
|
|
)
|
|
legal_request, _ = batch.secure_read_json(
|
|
args.legal_review_request,
|
|
"scene legal review request",
|
|
args.legal_review_request_sha256,
|
|
)
|
|
legal_decision, _ = batch.secure_read_json(
|
|
args.legal_review_decision,
|
|
"scene legal review decision",
|
|
args.legal_review_decision_sha256,
|
|
)
|
|
legal_keyring, legal_keyring_raw = batch.secure_read_json(
|
|
args.legal_review_keyring,
|
|
"scene legal review keyring",
|
|
args.legal_review_keyring_sha256,
|
|
)
|
|
root_key = _read_public_key(
|
|
args.approval_root_public_key,
|
|
batch.TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256,
|
|
"approval root public key",
|
|
)
|
|
apply_key = _read_public_key(
|
|
args.apply_service_public_key,
|
|
batch.TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256,
|
|
"apply service public key",
|
|
)
|
|
online_key = _read_online_public_key(args.online_service_public_key)
|
|
ledger_path = _outside_repo(args.ledger, "apply ledger")
|
|
output_path = _outside_repo(args.output, "apply evidence")
|
|
ledger = ApplyLedger(ledger_path)
|
|
current = datetime.now(UTC)
|
|
legal_summary, legal_binding = validate_scene_legal_review(
|
|
manifest,
|
|
approval,
|
|
legal_request,
|
|
legal_decision,
|
|
legal_keyring,
|
|
legal_keyring_raw,
|
|
now=current,
|
|
)
|
|
if args.command == "execute-online":
|
|
token = os.environ.get(AUTH_TOKEN_ENV, "")
|
|
if not token:
|
|
raise ApplyPublicationError(f"{AUTH_TOKEN_ENV} is not set")
|
|
challenge, _ = batch.secure_read_json(
|
|
args.authority_challenge,
|
|
"apply authority challenge",
|
|
args.authority_challenge_sha256,
|
|
)
|
|
evidence = execute_online_publication_batch(
|
|
manifest=manifest,
|
|
manifest_raw_sha256=manifest_raw,
|
|
approval=approval,
|
|
approval_raw_sha256=approval_raw,
|
|
approval_keyring=keyring,
|
|
approval_keyring_raw_sha256=keyring_raw,
|
|
approval_root_public_key=root_key,
|
|
apply_service_public_key=apply_key,
|
|
online_service_public_key=online_key,
|
|
legal_review_request=legal_request,
|
|
legal_review_decision=legal_decision,
|
|
legal_review_keyring=legal_keyring,
|
|
legal_review_keyring_raw_sha256=legal_keyring_raw,
|
|
authority_challenge=challenge,
|
|
environment_id=args.environment_id,
|
|
tenant_id=args.tenant_id,
|
|
client=JsonHttpClient(args.service_url, token),
|
|
ledger=ledger,
|
|
now=current,
|
|
)
|
|
online_output = _outside_repo(
|
|
args.online_receipt_output, "online receipt"
|
|
)
|
|
runtime_output = _outside_repo(
|
|
args.runtime_receipt_output, "runtime receipt"
|
|
)
|
|
_write_receipt(
|
|
online_output,
|
|
evidence["online_transaction_receipt"],
|
|
canonical_only=True,
|
|
)
|
|
_write_receipt(
|
|
runtime_output,
|
|
evidence["runtime_snapshot_receipt"],
|
|
canonical_only=True,
|
|
)
|
|
status = "online_committed_pending_countersign"
|
|
else:
|
|
token = os.environ.get(AUTH_TOKEN_ENV, "")
|
|
if not token:
|
|
raise ApplyPublicationError(f"{AUTH_TOKEN_ENV} is not set")
|
|
online_execution, _ = batch.secure_read_json(
|
|
args.online_execution,
|
|
"online execution evidence",
|
|
args.online_execution_sha256,
|
|
)
|
|
apply_receipt, _ = batch.secure_read_json(
|
|
args.apply_receipt,
|
|
"offline apply receipt",
|
|
args.apply_receipt_sha256,
|
|
)
|
|
evidence = finalize_publication_batch(
|
|
manifest=manifest,
|
|
manifest_raw_sha256=manifest_raw,
|
|
approval=approval,
|
|
approval_raw_sha256=approval_raw,
|
|
approval_keyring=keyring,
|
|
approval_root_public_key=root_key,
|
|
apply_service_public_key=apply_key,
|
|
online_service_public_key=online_key,
|
|
legal_review_binding_sha256=legal_binding,
|
|
legal_review_expires_at=legal_summary["expires_at"],
|
|
environment_id=args.environment_id,
|
|
tenant_id=args.tenant_id,
|
|
online_execution=online_execution,
|
|
apply_receipt=apply_receipt,
|
|
client=JsonHttpClient(args.service_url, token),
|
|
ledger=ledger,
|
|
now=current,
|
|
)
|
|
status = "finalized_with_offline_countersign"
|
|
receipt_sha = _write_receipt(output_path, evidence)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"status": status,
|
|
"apply_evidence": str(output_path),
|
|
"apply_evidence_raw_sha256": receipt_sha,
|
|
"publication_item_count": 74,
|
|
"runtime_snapshot_dependency_count": 14,
|
|
},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0
|
|
except (batch.PublicationBatchError, OSError, sqlite3.Error) as exc:
|
|
print(f"rc06_apply_error: {exc}", file=sys.stderr)
|
|
return 2
|
|
finally:
|
|
if ledger is not None:
|
|
ledger.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|