1737 lines
68 KiB
Python
1737 lines
68 KiB
Python
#!/usr/bin/env python3
|
|
"""Deterministically rebuild a disposable Postgres from genesis plus a strict ledger.
|
|
|
|
The v1 replay boundary supports strict ``add_edge``, ``attach_evidence``,
|
|
``approve_claim``, and ``revise_strategy`` receipts. Insert-only actions seed
|
|
their receipt-pinned rows before the guarded transition. ``revise_strategy``
|
|
instead captures the exact prestate identities, executes the exact hash-matched
|
|
guarded SQL, validates the generated delta, and normalizes only that delta to the
|
|
receipt-pinned transaction timestamp, IDs, and rows. Every entry must retain the
|
|
full approved and applied proposal rows plus its immutable approval snapshot.
|
|
Legacy and freeform entries still fail before Docker starts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
try:
|
|
from . import run_local_canonical_postgres_rebuild as canonical_rebuild
|
|
from .verify_postgres_parity_manifest import load_manifest
|
|
except ImportError: # pragma: no cover - direct script execution
|
|
import run_local_canonical_postgres_rebuild as canonical_rebuild
|
|
from verify_postgres_parity_manifest import load_manifest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPTS_DIR = REPO_ROOT / "scripts"
|
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
|
|
import apply_proposal as apply_engine # noqa: E402, I001
|
|
import kb_apply_replay_receipt as replay_receipt # noqa: E402
|
|
|
|
|
|
LEDGER_ARTIFACT = "teleo_genesis_plus_ledger"
|
|
MATERIAL_ARTIFACT = "teleo_strict_proposal_replay_material"
|
|
LEDGER_CONTRACT_VERSION = 1
|
|
MATERIAL_CONTRACT_VERSION = 1
|
|
DEFAULT_REBUILD_IMAGE = (
|
|
"docker.io/library/postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777"
|
|
)
|
|
SUPPORTED_PROPOSAL_TYPES = frozenset({"add_edge", "attach_evidence", "approve_claim", "revise_strategy"})
|
|
CLAIM_CEILING = (
|
|
"exact genesis plus ordered strict proposal replay; supported types are add_edge, "
|
|
"attach_evidence, approve_claim, and revise_strategy; mutating strategy replay "
|
|
"captures prestate identities and normalizes the exact receipt-pinned poststate; "
|
|
"full approved/applied proposal rows and immutable approval snapshots are required"
|
|
)
|
|
NOT_PROVEN = (
|
|
"legacy or freeform proposal replay",
|
|
"source-corpus recompilation from a blank schema",
|
|
"VPS, GCP, Telegram, or any live database mutation",
|
|
)
|
|
|
|
APPLY_ENGINE_PATH = SCRIPTS_DIR / "apply_proposal.py"
|
|
REPLAY_RECEIPT_ENGINE_PATH = SCRIPTS_DIR / "kb_apply_replay_receipt.py"
|
|
GUARD_PREREQUISITES_PATH = SCRIPTS_DIR / "kb_apply_prereqs.sql"
|
|
PARITY_SQL_PATH = REPO_ROOT / "ops" / "postgres_parity_manifest.sql"
|
|
BASE_REBUILD_PATH = REPO_ROOT / "ops" / "run_local_canonical_postgres_rebuild.py"
|
|
GUARD_PREREQUISITES_DESTINATION = "/tmp/kb-apply-prereqs.sql"
|
|
FIXED_ENGINE_PATHS = frozenset(
|
|
{
|
|
APPLY_ENGINE_PATH.resolve(),
|
|
REPLAY_RECEIPT_ENGINE_PATH.resolve(),
|
|
GUARD_PREREQUISITES_PATH.resolve(),
|
|
PARITY_SQL_PATH.resolve(),
|
|
BASE_REBUILD_PATH.resolve(),
|
|
Path(__file__).resolve(),
|
|
}
|
|
)
|
|
|
|
SHA256_RE = re.compile(r"[0-9a-f]{64}\Z")
|
|
IMAGE_DIGEST_RE = re.compile(r"\S+@sha256:[0-9a-f]{64}\Z")
|
|
PROPOSAL_TRANSITION_FIELDS = frozenset(
|
|
{"status", "applied_by_handle", "applied_by_agent_id", "applied_at", "updated_at"}
|
|
)
|
|
PROPOSAL_FIELDS = frozenset(
|
|
{
|
|
"id",
|
|
"proposal_type",
|
|
"status",
|
|
"proposed_by_handle",
|
|
"proposed_by_agent_id",
|
|
"channel",
|
|
"source_ref",
|
|
"rationale",
|
|
"payload",
|
|
"reviewed_by_handle",
|
|
"reviewed_by_agent_id",
|
|
"reviewed_at",
|
|
"review_note",
|
|
"applied_by_handle",
|
|
"applied_by_agent_id",
|
|
"applied_at",
|
|
"created_at",
|
|
"updated_at",
|
|
}
|
|
)
|
|
APPROVAL_FIELDS = frozenset(
|
|
{
|
|
"proposal_id",
|
|
"proposal_type",
|
|
"payload",
|
|
"reviewed_by_handle",
|
|
"reviewed_by_agent_id",
|
|
"reviewed_by_db_role",
|
|
"reviewed_at",
|
|
"review_note",
|
|
}
|
|
)
|
|
CANONICAL_TABLE_ORDER = (
|
|
"claims",
|
|
"sources",
|
|
"reasoning_tools",
|
|
"claim_evidence",
|
|
"claim_edges",
|
|
)
|
|
STRATEGY_REQUIRED_FIELDS = frozenset(
|
|
{
|
|
"id",
|
|
"agent_id",
|
|
"diagnosis",
|
|
"guiding_policy",
|
|
"proximate_objectives",
|
|
"version",
|
|
"active",
|
|
"created_at",
|
|
}
|
|
)
|
|
STRATEGY_NODE_REQUIRED_FIELDS = frozenset(
|
|
{
|
|
"id",
|
|
"agent_id",
|
|
"node_type",
|
|
"title",
|
|
"body",
|
|
"rank",
|
|
"status",
|
|
"horizon",
|
|
"measure",
|
|
"source_ref",
|
|
"metadata",
|
|
"created_at",
|
|
"updated_at",
|
|
}
|
|
)
|
|
|
|
|
|
class ReconstructionError(RuntimeError):
|
|
"""A fail-closed error whose message is safe for the public receipt."""
|
|
|
|
def __init__(self, code: str, safe_message: str) -> None:
|
|
super().__init__(safe_message)
|
|
self.code = code
|
|
self.safe_message = safe_message
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LedgerEntry:
|
|
sequence: int
|
|
material_path: Path
|
|
material_sha256: str
|
|
replay_material_sha256: str
|
|
approved_proposal: dict[str, Any]
|
|
approval_snapshot: dict[str, Any]
|
|
applied_proposal: dict[str, Any]
|
|
receipt: dict[str, Any]
|
|
current_apply_sql: str
|
|
current_apply_sql_sha256: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReconstructionBundle:
|
|
ledger_sha256: str
|
|
ledger_bytes: int
|
|
genesis_dump_sha256: str
|
|
genesis_dump_bytes: int
|
|
genesis_manifest: dict[str, Any]
|
|
genesis_manifest_sha256: str
|
|
genesis_manifest_bytes: int
|
|
final_manifest_path: Path
|
|
final_manifest: dict[str, Any]
|
|
final_manifest_sha256: str
|
|
final_manifest_bytes: int
|
|
engine_hashes: dict[str, str]
|
|
entries: tuple[LedgerEntry, ...]
|
|
protected_paths: frozenset[Path]
|
|
|
|
|
|
def _canonical_json(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
|
|
|
|
|
def _sha256_text(value: str) -> str:
|
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _validate_sha256(value: Any, field: str) -> str:
|
|
if not isinstance(value, str) or not SHA256_RE.fullmatch(value):
|
|
raise ReconstructionError("invalid_hash_pin", f"{field} must be a lowercase SHA-256 digest")
|
|
return value
|
|
|
|
|
|
def _require_exact_keys(value: Any, expected: frozenset[str], field: str) -> dict[str, Any]:
|
|
if not isinstance(value, dict):
|
|
raise ReconstructionError("invalid_contract", f"{field} must be an object")
|
|
observed = frozenset(value)
|
|
if observed != expected:
|
|
raise ReconstructionError(
|
|
"invalid_contract",
|
|
f"{field} fields do not match v1 contract; "
|
|
f"missing_count={len(expected - observed)}, extra_count={len(observed - expected)}",
|
|
)
|
|
return value
|
|
|
|
|
|
def _load_json_object(path: Path, field: str) -> dict[str, Any]:
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
raise ReconstructionError("invalid_json_artifact", f"{field} is not valid UTF-8 JSON") from exc
|
|
if not isinstance(value, dict):
|
|
raise ReconstructionError("invalid_json_artifact", f"{field} must contain one JSON object")
|
|
return value
|
|
|
|
|
|
def _resolve_artifact(ledger_path: Path, declared: Any, field: str) -> Path:
|
|
if not isinstance(declared, str) or not declared.strip():
|
|
raise ReconstructionError("invalid_contract", f"{field} must be a non-empty path string")
|
|
candidate = Path(declared).expanduser()
|
|
if not candidate.is_absolute():
|
|
candidate = ledger_path.parent / candidate
|
|
candidate = candidate.resolve()
|
|
if not candidate.is_file():
|
|
raise ReconstructionError("missing_artifact", f"{field} is not an existing regular file")
|
|
return candidate
|
|
|
|
|
|
def _assert_file_hash(path: Path, expected: Any, field: str) -> str:
|
|
pin = _validate_sha256(expected, field)
|
|
actual = canonical_rebuild.sha256_file(path)
|
|
if actual != pin:
|
|
raise ReconstructionError("hash_pin_mismatch", f"{field} does not match the supplied artifact")
|
|
return actual
|
|
|
|
|
|
def _engine_hashes() -> dict[str, str]:
|
|
paths = {
|
|
"reconstruction_command_sha256": Path(__file__),
|
|
"base_rebuild_engine_sha256": BASE_REBUILD_PATH,
|
|
"apply_engine_sha256": APPLY_ENGINE_PATH,
|
|
"replay_receipt_engine_sha256": REPLAY_RECEIPT_ENGINE_PATH,
|
|
"guard_prerequisites_sha256": GUARD_PREREQUISITES_PATH,
|
|
"parity_sql_sha256": PARITY_SQL_PATH,
|
|
}
|
|
return {name: canonical_rebuild.sha256_file(path) for name, path in paths.items()}
|
|
|
|
|
|
def _validate_proposal_rows(
|
|
approved: dict[str, Any],
|
|
applied: dict[str, Any],
|
|
approval: dict[str, Any],
|
|
receipt_proposal: dict[str, Any],
|
|
) -> None:
|
|
_require_exact_keys(approved, PROPOSAL_FIELDS, "material.approved_proposal")
|
|
_require_exact_keys(applied, PROPOSAL_FIELDS, "material.applied_proposal")
|
|
_require_exact_keys(approval, APPROVAL_FIELDS, "material.approval_snapshot")
|
|
|
|
try:
|
|
proposal_id = str(uuid.UUID(str(applied["id"])))
|
|
except (TypeError, ValueError, AttributeError) as exc:
|
|
raise ReconstructionError("invalid_material", "material proposal id must be a UUID") from exc
|
|
|
|
if approved["id"] != proposal_id or approval["proposal_id"] != proposal_id:
|
|
raise ReconstructionError("material_mismatch", "proposal and approval ids do not match")
|
|
if approved["status"] != "approved" or applied["status"] != "applied":
|
|
raise ReconstructionError(
|
|
"unsupported_proposal_state",
|
|
"material requires one exact approved row and one exact applied row",
|
|
)
|
|
if any(approved[field] is not None for field in ("applied_by_handle", "applied_by_agent_id", "applied_at")):
|
|
raise ReconstructionError(
|
|
"unsupported_proposal_state", "approved proposal material already contains apply metadata"
|
|
)
|
|
if not applied["applied_by_handle"] or not applied["applied_by_agent_id"] or not applied["applied_at"]:
|
|
raise ReconstructionError("invalid_material", "applied proposal material lacks exact apply metadata")
|
|
if not approved["reviewed_by_agent_id"] or not approval["reviewed_by_db_role"]:
|
|
raise ReconstructionError(
|
|
"invalid_material", "strict replay requires a reviewer agent id and reviewer database role"
|
|
)
|
|
|
|
immutable_fields = PROPOSAL_FIELDS - PROPOSAL_TRANSITION_FIELDS
|
|
changed_immutable = sorted(field for field in immutable_fields if approved[field] != applied[field])
|
|
if changed_immutable:
|
|
raise ReconstructionError(
|
|
"material_mismatch",
|
|
f"proposal changed outside the guarded apply transition: {changed_immutable}",
|
|
)
|
|
|
|
approval_projection = {
|
|
"proposal_id": approved["id"],
|
|
"proposal_type": approved["proposal_type"],
|
|
"payload": approved["payload"],
|
|
"reviewed_by_handle": approved["reviewed_by_handle"],
|
|
"reviewed_by_agent_id": approved["reviewed_by_agent_id"],
|
|
"reviewed_at": approved["reviewed_at"],
|
|
"review_note": approved["review_note"],
|
|
}
|
|
for field, expected in approval_projection.items():
|
|
if approval[field] != expected:
|
|
raise ReconstructionError(
|
|
"material_mismatch", f"approval snapshot does not match approved proposal field {field}"
|
|
)
|
|
|
|
envelope_fields = (
|
|
"id",
|
|
"proposal_type",
|
|
"status",
|
|
"payload",
|
|
"reviewed_by_handle",
|
|
"reviewed_by_agent_id",
|
|
"reviewed_at",
|
|
"review_note",
|
|
"applied_by_handle",
|
|
"applied_by_agent_id",
|
|
"applied_at",
|
|
)
|
|
if any(receipt_proposal.get(field) != applied[field] for field in envelope_fields):
|
|
raise ReconstructionError(
|
|
"material_mismatch", "replay receipt proposal envelope does not match applied proposal row"
|
|
)
|
|
|
|
|
|
def _parse_aware_timestamp(value: Any, field: str, *, code: str = "invalid_mutating_receipt") -> datetime:
|
|
if not isinstance(value, str) or not value:
|
|
raise ReconstructionError(code, f"{field} must be an ISO-8601 timestamp")
|
|
try:
|
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise ReconstructionError(code, f"{field} must be an ISO-8601 timestamp") from exc
|
|
if parsed.tzinfo is None:
|
|
raise ReconstructionError(code, f"{field} must include a timezone")
|
|
return parsed
|
|
|
|
|
|
def _validated_uuid_list(value: Any, field: str, *, code: str = "invalid_mutating_state") -> list[str]:
|
|
if not isinstance(value, list):
|
|
raise ReconstructionError(code, f"{field} must be a UUID list")
|
|
normalized: list[str] = []
|
|
for item in value:
|
|
try:
|
|
normalized.append(str(uuid.UUID(str(item))))
|
|
except (TypeError, ValueError, AttributeError) as exc:
|
|
raise ReconstructionError(code, f"{field} must contain only UUIDs") from exc
|
|
if len(normalized) != len(set(normalized)):
|
|
raise ReconstructionError(code, f"{field} contains duplicate UUIDs")
|
|
return normalized
|
|
|
|
|
|
def _validate_revise_strategy_receipt_rows(
|
|
proposal: dict[str, Any], canonical_rows: dict[str, Any]
|
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
payload = proposal.get("payload")
|
|
apply_payload = payload.get("apply_payload") if isinstance(payload, dict) else None
|
|
if not isinstance(apply_payload, dict):
|
|
raise ReconstructionError("invalid_mutating_receipt", "revise_strategy receipt requires a strict apply payload")
|
|
try:
|
|
agent_id = str(uuid.UUID(str(apply_payload.get("agent_id"))))
|
|
except (TypeError, ValueError, AttributeError) as exc:
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy receipt requires one agent UUID"
|
|
) from exc
|
|
|
|
strategies = canonical_rows.get("strategies")
|
|
nodes = canonical_rows.get("strategy_nodes")
|
|
if not isinstance(strategies, list) or len(strategies) != 1 or not isinstance(strategies[0], dict):
|
|
raise ReconstructionError("invalid_mutating_receipt", "revise_strategy receipt must pin one fresh strategy row")
|
|
expected_nodes = apply_payload.get("strategy_nodes")
|
|
if (
|
|
not isinstance(expected_nodes, list)
|
|
or not isinstance(nodes, list)
|
|
or len(nodes) != len(expected_nodes)
|
|
or any(not isinstance(row, dict) for row in nodes)
|
|
):
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy receipt must pin every fresh strategy node row"
|
|
)
|
|
|
|
strategy = strategies[0]
|
|
if frozenset(strategy) != STRATEGY_REQUIRED_FIELDS:
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy receipt strategy row fields are not canonical"
|
|
)
|
|
if any(frozenset(row) != STRATEGY_NODE_REQUIRED_FIELDS for row in nodes):
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy receipt strategy node row fields are not canonical"
|
|
)
|
|
strategy_id = _validated_uuid_list([strategy.get("id")], "receipt strategy id", code="invalid_mutating_receipt")[0]
|
|
node_ids = _validated_uuid_list(
|
|
[row.get("id") for row in nodes],
|
|
"receipt strategy node ids",
|
|
code="invalid_mutating_receipt",
|
|
)
|
|
if strategy.get("agent_id") != agent_id or any(row.get("agent_id") != agent_id for row in nodes):
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy receipt rows do not belong to the payload agent"
|
|
)
|
|
if strategy.get("active") is not True or any(row.get("status") != "active" for row in nodes):
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy receipt must pin the fresh active poststate"
|
|
)
|
|
version = strategy.get("version")
|
|
if isinstance(version, bool) or not isinstance(version, int) or version < 1:
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy receipt strategy version must be positive"
|
|
)
|
|
|
|
transaction_timestamp = strategy.get("created_at")
|
|
transaction_time = _parse_aware_timestamp(transaction_timestamp, "receipt strategy created_at")
|
|
for row in nodes:
|
|
if row.get("created_at") != transaction_timestamp or row.get("updated_at") != transaction_timestamp:
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt",
|
|
"revise_strategy fresh strategy and node timestamps must share one transaction timestamp",
|
|
)
|
|
reviewed_time = _parse_aware_timestamp(proposal.get("reviewed_at"), "receipt proposal reviewed_at")
|
|
applied_time = _parse_aware_timestamp(proposal.get("applied_at"), "receipt proposal applied_at")
|
|
if reviewed_time > applied_time:
|
|
raise ReconstructionError("invalid_mutating_receipt", "revise_strategy proposal approval is after apply time")
|
|
if transaction_time < reviewed_time:
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy transaction timestamp is before proposal approval"
|
|
)
|
|
if transaction_time > applied_time:
|
|
raise ReconstructionError(
|
|
"invalid_mutating_receipt", "revise_strategy transaction timestamp is after proposal apply time"
|
|
)
|
|
|
|
return {**strategy, "id": strategy_id}, [
|
|
{**row, "id": normalized_id} for row, normalized_id in zip(nodes, node_ids, strict=True)
|
|
]
|
|
|
|
|
|
def _validate_entry_material(
|
|
material: dict[str, Any],
|
|
*,
|
|
expected_sequence: int,
|
|
expected_replay_hash: str,
|
|
material_path: Path,
|
|
material_sha256: str,
|
|
) -> LedgerEntry:
|
|
expected_keys = frozenset(
|
|
{
|
|
"artifact",
|
|
"contract_version",
|
|
"sequence",
|
|
"approved_proposal",
|
|
"approval_snapshot",
|
|
"applied_proposal",
|
|
"replay_receipt",
|
|
}
|
|
)
|
|
_require_exact_keys(material, expected_keys, "ledger entry material")
|
|
if material["artifact"] != MATERIAL_ARTIFACT or material["contract_version"] != MATERIAL_CONTRACT_VERSION:
|
|
raise ReconstructionError("unsupported_material_contract", "ledger entry material is not v1")
|
|
if material["sequence"] != expected_sequence:
|
|
raise ReconstructionError("ledger_order_mismatch", "material sequence does not match ledger order")
|
|
|
|
receipt = material["replay_receipt"]
|
|
if not isinstance(receipt, dict):
|
|
raise ReconstructionError("invalid_material", "material.replay_receipt must be an object")
|
|
proposal = receipt.get("proposal")
|
|
if not isinstance(proposal, dict):
|
|
raise ReconstructionError("invalid_material", "replay receipt has no proposal object")
|
|
proposal_type = proposal.get("proposal_type")
|
|
if proposal_type not in SUPPORTED_PROPOSAL_TYPES:
|
|
raise ReconstructionError(
|
|
"unsupported_proposal_type", "ledger entry proposal type is not supported by v1 reconstruction"
|
|
)
|
|
if receipt.get("artifact") != "kb_apply_replay_receipt" or receipt.get("replay_ready") is not True:
|
|
raise ReconstructionError("invalid_replay_receipt", "ledger entry is not a replay-ready apply receipt")
|
|
if receipt.get("receipt_contract_version") != replay_receipt.RECEIPT_CONTRACT_VERSION:
|
|
raise ReconstructionError("unsupported_receipt_contract", "replay receipt contract version is unsupported")
|
|
|
|
apply_metadata = receipt.get("apply_engine")
|
|
canonical_rows = receipt.get("canonical_rows")
|
|
if not isinstance(apply_metadata, dict) or not isinstance(canonical_rows, dict):
|
|
raise ReconstructionError("invalid_replay_receipt", "replay receipt is missing apply or row material")
|
|
if proposal_type == "revise_strategy":
|
|
_validate_revise_strategy_receipt_rows(proposal, canonical_rows)
|
|
try:
|
|
rebuilt = replay_receipt.build_replay_receipt(
|
|
proposal,
|
|
canonical_rows,
|
|
apply_sql_sha256=apply_metadata.get("apply_sql_sha256"),
|
|
apply_sql_source=apply_metadata.get("source"),
|
|
exported_at_utc=receipt.get("exported_at_utc"),
|
|
)
|
|
except (KeyError, TypeError, ValueError) as exc:
|
|
raise ReconstructionError(
|
|
"invalid_replay_receipt", "replay receipt failed strict payload and exact-row validation"
|
|
) from exc
|
|
|
|
if receipt.get("hashes") != rebuilt["hashes"]:
|
|
raise ReconstructionError("replay_receipt_hash_mismatch", "replay receipt hashes are not recoverable")
|
|
if receipt.get("expected_row_counts") != rebuilt["expected_row_counts"]:
|
|
raise ReconstructionError("replay_receipt_count_mismatch", "replay receipt expected counts are invalid")
|
|
if receipt.get("actual_row_counts") != rebuilt["actual_row_counts"]:
|
|
raise ReconstructionError("replay_receipt_count_mismatch", "replay receipt actual counts are invalid")
|
|
actual_replay_hash = rebuilt["hashes"]["replay_material_sha256"]
|
|
if actual_replay_hash != expected_replay_hash:
|
|
raise ReconstructionError(
|
|
"replay_material_hash_mismatch", "ledger replay-material pin does not match the private receipt"
|
|
)
|
|
receipt = {**receipt, "canonical_rows": rebuilt["canonical_rows"]}
|
|
|
|
approved = material["approved_proposal"]
|
|
approval = material["approval_snapshot"]
|
|
applied = material["applied_proposal"]
|
|
_validate_proposal_rows(approved, applied, approval, proposal)
|
|
try:
|
|
current_apply_sql = apply_engine.build_apply_sql(proposal, applied["applied_by_handle"])
|
|
except (KeyError, TypeError, ValueError) as exc:
|
|
raise ReconstructionError(
|
|
"current_apply_engine_rejected_material",
|
|
"current guarded apply engine rejected the strict replay material",
|
|
) from exc
|
|
current_apply_sql_sha256 = _sha256_text(current_apply_sql)
|
|
if proposal_type == "revise_strategy" and (
|
|
apply_metadata.get("source") != "exact_executed_sql"
|
|
or apply_metadata.get("apply_sql_sha256") != current_apply_sql_sha256
|
|
):
|
|
raise ReconstructionError(
|
|
"mutating_apply_engine_mismatch",
|
|
"revise_strategy replay requires the exact executed SQL to match the current guarded apply engine",
|
|
)
|
|
|
|
return LedgerEntry(
|
|
sequence=expected_sequence,
|
|
material_path=material_path,
|
|
material_sha256=material_sha256,
|
|
replay_material_sha256=actual_replay_hash,
|
|
approved_proposal=approved,
|
|
approval_snapshot=approval,
|
|
applied_proposal=applied,
|
|
receipt=receipt,
|
|
current_apply_sql=current_apply_sql,
|
|
current_apply_sql_sha256=current_apply_sql_sha256,
|
|
)
|
|
|
|
|
|
def load_bundle(args: argparse.Namespace) -> ReconstructionBundle:
|
|
canonical_rebuild.validate_custom_dump(args.genesis_dump)
|
|
ledger_sha256 = _assert_file_hash(args.ledger, args.ledger_sha256, "--ledger-sha256")
|
|
ledger = _load_json_object(args.ledger, "ledger")
|
|
top_level_keys = frozenset({"artifact", "contract_version", "engine", "genesis", "entries", "final_parity"})
|
|
_require_exact_keys(ledger, top_level_keys, "ledger")
|
|
if ledger["artifact"] != LEDGER_ARTIFACT or ledger["contract_version"] != LEDGER_CONTRACT_VERSION:
|
|
raise ReconstructionError("unsupported_ledger_contract", "ledger is not the v1 genesis-plus-ledger contract")
|
|
|
|
actual_engine_hashes = _engine_hashes()
|
|
expected_engine = _require_exact_keys(ledger["engine"], frozenset(actual_engine_hashes), "ledger.engine")
|
|
for field, actual_hash in actual_engine_hashes.items():
|
|
if _validate_sha256(expected_engine[field], f"ledger.engine.{field}") != actual_hash:
|
|
raise ReconstructionError("engine_hash_mismatch", f"ledger engine pin does not match current {field}")
|
|
|
|
genesis = _require_exact_keys(
|
|
ledger["genesis"], frozenset({"dump_sha256", "parity_manifest_sha256"}), "ledger.genesis"
|
|
)
|
|
genesis_dump_sha256 = _assert_file_hash(args.genesis_dump, genesis["dump_sha256"], "ledger.genesis.dump_sha256")
|
|
genesis_manifest_sha256 = _assert_file_hash(
|
|
args.genesis_manifest,
|
|
genesis["parity_manifest_sha256"],
|
|
"ledger.genesis.parity_manifest_sha256",
|
|
)
|
|
try:
|
|
genesis_manifest = load_manifest(args.genesis_manifest)
|
|
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
|
|
raise ReconstructionError("invalid_genesis_manifest", "genesis parity manifest is invalid") from exc
|
|
|
|
final_spec = _require_exact_keys(ledger["final_parity"], frozenset({"manifest", "sha256"}), "ledger.final_parity")
|
|
final_manifest_path = _resolve_artifact(args.ledger, final_spec["manifest"], "final parity manifest")
|
|
final_manifest_sha256 = _assert_file_hash(final_manifest_path, final_spec["sha256"], "ledger.final_parity.sha256")
|
|
try:
|
|
final_manifest = load_manifest(final_manifest_path)
|
|
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
|
|
raise ReconstructionError("invalid_final_manifest", "final parity manifest is invalid") from exc
|
|
|
|
raw_entries = ledger["entries"]
|
|
if not isinstance(raw_entries, list):
|
|
raise ReconstructionError("invalid_contract", "ledger.entries must be an ordered list")
|
|
entries: list[LedgerEntry] = []
|
|
protected_paths = {
|
|
args.genesis_dump.resolve(),
|
|
args.genesis_manifest.resolve(),
|
|
args.ledger.resolve(),
|
|
final_manifest_path,
|
|
*FIXED_ENGINE_PATHS,
|
|
}
|
|
seen_proposal_ids: set[str] = set()
|
|
for expected_sequence, raw_entry in enumerate(raw_entries, 1):
|
|
entry_spec = _require_exact_keys(
|
|
raw_entry,
|
|
frozenset({"sequence", "material", "sha256", "replay_material_sha256"}),
|
|
f"ledger.entries[{expected_sequence - 1}]",
|
|
)
|
|
if entry_spec["sequence"] != expected_sequence:
|
|
raise ReconstructionError(
|
|
"ledger_order_mismatch", "ledger entry sequences must be contiguous and start at 1"
|
|
)
|
|
material_path = _resolve_artifact(
|
|
args.ledger, entry_spec["material"], f"ledger entry {expected_sequence} material"
|
|
)
|
|
material_sha256 = _assert_file_hash(
|
|
material_path, entry_spec["sha256"], f"ledger entry {expected_sequence} sha256"
|
|
)
|
|
expected_replay_hash = _validate_sha256(
|
|
entry_spec["replay_material_sha256"],
|
|
f"ledger entry {expected_sequence} replay_material_sha256",
|
|
)
|
|
entry = _validate_entry_material(
|
|
_load_json_object(material_path, f"ledger entry {expected_sequence} material"),
|
|
expected_sequence=expected_sequence,
|
|
expected_replay_hash=expected_replay_hash,
|
|
material_path=material_path,
|
|
material_sha256=material_sha256,
|
|
)
|
|
proposal_id = entry.applied_proposal["id"]
|
|
if proposal_id in seen_proposal_ids:
|
|
raise ReconstructionError("duplicate_proposal", "ledger contains the same proposal more than once")
|
|
seen_proposal_ids.add(proposal_id)
|
|
entries.append(entry)
|
|
protected_paths.add(material_path)
|
|
|
|
if args.output.resolve() in protected_paths:
|
|
raise ReconstructionError("unsafe_output_path", "--output must not overwrite any reconstruction input")
|
|
|
|
return ReconstructionBundle(
|
|
ledger_sha256=ledger_sha256,
|
|
ledger_bytes=args.ledger.stat().st_size,
|
|
genesis_dump_sha256=genesis_dump_sha256,
|
|
genesis_dump_bytes=args.genesis_dump.stat().st_size,
|
|
genesis_manifest=genesis_manifest,
|
|
genesis_manifest_sha256=genesis_manifest_sha256,
|
|
genesis_manifest_bytes=args.genesis_manifest.stat().st_size,
|
|
final_manifest_path=final_manifest_path,
|
|
final_manifest=final_manifest,
|
|
final_manifest_sha256=final_manifest_sha256,
|
|
final_manifest_bytes=final_manifest_path.stat().st_size,
|
|
engine_hashes=actual_engine_hashes,
|
|
entries=tuple(entries),
|
|
protected_paths=frozenset(protected_paths),
|
|
)
|
|
|
|
|
|
def _jsonb_literal(value: Any) -> str:
|
|
return apply_engine.sql_literal(_canonical_json(value)) + "::jsonb"
|
|
|
|
|
|
def build_seed_sql(entry: LedgerEntry) -> str:
|
|
statements = [
|
|
"begin;",
|
|
"set local standard_conforming_strings = on;",
|
|
"insert into kb_stage.kb_proposals",
|
|
"select seeded.* from jsonb_populate_record(",
|
|
f" null::kb_stage.kb_proposals, {_jsonb_literal(entry.approved_proposal)}",
|
|
") seeded",
|
|
"on conflict (id) do nothing;",
|
|
"insert into kb_stage.kb_proposal_approvals",
|
|
"select seeded.* from jsonb_populate_record(",
|
|
f" null::kb_stage.kb_proposal_approvals, {_jsonb_literal(entry.approval_snapshot)}",
|
|
") seeded",
|
|
"on conflict (proposal_id) do nothing;",
|
|
]
|
|
rows = entry.receipt["canonical_rows"]
|
|
for table in CANONICAL_TABLE_ORDER:
|
|
table_rows = rows.get(table)
|
|
if not table_rows:
|
|
continue
|
|
statements.extend(
|
|
[
|
|
f"insert into public.{table}",
|
|
"select seeded.* from jsonb_populate_recordset(",
|
|
f" null::public.{table}, {_jsonb_literal(table_rows)}",
|
|
") seeded",
|
|
"on conflict do nothing;",
|
|
]
|
|
)
|
|
statements.append("commit;")
|
|
return "\n".join(statements) + "\n"
|
|
|
|
|
|
def _uuid_membership_sql(column: str, values: list[str], *, negate: bool = False) -> str:
|
|
if not values:
|
|
return "true" if negate else "false"
|
|
rendered = ", ".join(f"{apply_engine.sql_literal(value)}::uuid" for value in values)
|
|
operator = "not in" if negate else "in"
|
|
return f"{column} {operator} ({rendered})"
|
|
|
|
|
|
def _uuid_array_sql(values: list[str]) -> str:
|
|
if not values:
|
|
return "array[]::uuid[]"
|
|
rendered = ", ".join(f"{apply_engine.sql_literal(value)}::uuid" for value in values)
|
|
return f"array[{rendered}]::uuid[]"
|
|
|
|
|
|
def build_revise_strategy_prestate_sql(entry: LedgerEntry) -> str:
|
|
apply_payload = entry.receipt["proposal"]["payload"]["apply_payload"]
|
|
agent = apply_engine.sql_literal(apply_payload["agent_id"])
|
|
return f"""with strategy_state as (
|
|
select coalesce(jsonb_agg(s.id::text order by s.id::text), '[]'::jsonb) as strategy_ids,
|
|
coalesce(
|
|
jsonb_agg(s.id::text order by s.id::text) filter (where s.active),
|
|
'[]'::jsonb
|
|
) as active_strategy_ids,
|
|
coalesce(max(s.version), 0) as max_strategy_version
|
|
from public.strategies s
|
|
where s.agent_id = {agent}::uuid
|
|
), node_state as (
|
|
select coalesce(jsonb_agg(n.id::text order by n.id::text), '[]'::jsonb) as strategy_node_ids,
|
|
coalesce(
|
|
jsonb_agg(n.id::text order by n.id::text) filter (where n.status <> 'retired'),
|
|
'[]'::jsonb
|
|
) as non_retired_strategy_node_ids
|
|
from public.strategy_nodes n
|
|
where n.agent_id = {agent}::uuid
|
|
)
|
|
select jsonb_build_object(
|
|
'strategy_ids', s.strategy_ids,
|
|
'active_strategy_ids', s.active_strategy_ids,
|
|
'max_strategy_version', s.max_strategy_version,
|
|
'strategy_node_ids', n.strategy_node_ids,
|
|
'non_retired_strategy_node_ids', n.non_retired_strategy_node_ids
|
|
)::text
|
|
from strategy_state s cross join node_state n;
|
|
"""
|
|
|
|
|
|
def _validate_revise_strategy_prestate(value: Any) -> dict[str, Any]:
|
|
expected = frozenset(
|
|
{
|
|
"strategy_ids",
|
|
"active_strategy_ids",
|
|
"max_strategy_version",
|
|
"strategy_node_ids",
|
|
"non_retired_strategy_node_ids",
|
|
}
|
|
)
|
|
state = _require_exact_keys(value, expected, "revise_strategy prestate")
|
|
strategy_ids = _validated_uuid_list(state["strategy_ids"], "prestate strategy ids")
|
|
active_strategy_ids = _validated_uuid_list(state["active_strategy_ids"], "prestate active strategy ids")
|
|
strategy_node_ids = _validated_uuid_list(state["strategy_node_ids"], "prestate strategy node ids")
|
|
non_retired_node_ids = _validated_uuid_list(
|
|
state["non_retired_strategy_node_ids"], "prestate non-retired strategy node ids"
|
|
)
|
|
max_version = state["max_strategy_version"]
|
|
if isinstance(max_version, bool) or not isinstance(max_version, int) or max_version < 0:
|
|
raise ReconstructionError(
|
|
"invalid_mutating_state", "prestate maximum strategy version must be a non-negative integer"
|
|
)
|
|
if len(active_strategy_ids) > 1 or not set(active_strategy_ids).issubset(strategy_ids):
|
|
raise ReconstructionError(
|
|
"invalid_mutating_state", "prestate active strategies violate the one-active invariant"
|
|
)
|
|
if not set(non_retired_node_ids).issubset(strategy_node_ids):
|
|
raise ReconstructionError(
|
|
"invalid_mutating_state", "prestate non-retired strategy nodes are not a subset of all nodes"
|
|
)
|
|
return {
|
|
"strategy_ids": strategy_ids,
|
|
"active_strategy_ids": active_strategy_ids,
|
|
"max_strategy_version": max_version,
|
|
"strategy_node_ids": strategy_node_ids,
|
|
"non_retired_strategy_node_ids": non_retired_node_ids,
|
|
}
|
|
|
|
|
|
def build_revise_strategy_generated_delta_sql(entry: LedgerEntry, prestate: dict[str, Any]) -> str:
|
|
state = _validate_revise_strategy_prestate(prestate)
|
|
apply_payload = entry.receipt["proposal"]["payload"]["apply_payload"]
|
|
agent = apply_engine.sql_literal(apply_payload["agent_id"])
|
|
strategy_filter = _uuid_membership_sql("s.id", state["strategy_ids"], negate=True)
|
|
node_filter = _uuid_membership_sql("n.id", state["strategy_node_ids"], negate=True)
|
|
return f"""select jsonb_build_object(
|
|
'strategies', coalesce((
|
|
select jsonb_agg(to_jsonb(s) order by s.id::text)
|
|
from public.strategies s
|
|
where s.agent_id = {agent}::uuid and {strategy_filter}
|
|
), '[]'::jsonb),
|
|
'strategy_nodes', coalesce((
|
|
select jsonb_agg(to_jsonb(n) order by n.id::text)
|
|
from public.strategy_nodes n
|
|
where n.agent_id = {agent}::uuid and {node_filter}
|
|
), '[]'::jsonb)
|
|
)::text;
|
|
"""
|
|
|
|
|
|
def _validate_revise_strategy_generated_delta(
|
|
entry: LedgerEntry,
|
|
prestate: dict[str, Any],
|
|
generated_rows: Any,
|
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
state = _validate_revise_strategy_prestate(prestate)
|
|
if not isinstance(generated_rows, dict):
|
|
raise ReconstructionError("invalid_mutating_delta", "revise_strategy generated delta must be a row object")
|
|
try:
|
|
replay_receipt.build_replay_receipt(
|
|
entry.receipt["proposal"],
|
|
generated_rows,
|
|
apply_sql_sha256=entry.current_apply_sql_sha256,
|
|
apply_sql_source="exact_executed_sql",
|
|
exported_at_utc=entry.receipt.get("exported_at_utc"),
|
|
)
|
|
except (KeyError, TypeError, ValueError) as exc:
|
|
raise ReconstructionError(
|
|
"invalid_mutating_delta",
|
|
"revise_strategy generated rows do not match the strict payload",
|
|
) from exc
|
|
|
|
strategies = generated_rows.get("strategies")
|
|
nodes = generated_rows.get("strategy_nodes")
|
|
if not isinstance(strategies, list) or len(strategies) != 1 or not isinstance(strategies[0], dict):
|
|
raise ReconstructionError(
|
|
"invalid_mutating_delta", "revise_strategy apply did not generate exactly one strategy"
|
|
)
|
|
if not isinstance(nodes, list) or any(not isinstance(row, dict) for row in nodes):
|
|
raise ReconstructionError("invalid_mutating_delta", "revise_strategy apply generated invalid strategy nodes")
|
|
strategy = strategies[0]
|
|
if strategy.get("version") != state["max_strategy_version"] + 1:
|
|
raise ReconstructionError("invalid_mutating_delta", "revise_strategy generated an unexpected strategy version")
|
|
transaction_timestamp = strategy.get("created_at")
|
|
_parse_aware_timestamp(transaction_timestamp, "generated strategy created_at", code="invalid_mutating_delta")
|
|
if any(
|
|
row.get("created_at") != transaction_timestamp or row.get("updated_at") != transaction_timestamp
|
|
for row in nodes
|
|
):
|
|
raise ReconstructionError(
|
|
"invalid_mutating_delta",
|
|
"revise_strategy generated rows do not share one transaction timestamp",
|
|
)
|
|
_validated_uuid_list([strategy.get("id")], "generated strategy id", code="invalid_mutating_delta")
|
|
_validated_uuid_list(
|
|
[row.get("id") for row in nodes],
|
|
"generated strategy node ids",
|
|
code="invalid_mutating_delta",
|
|
)
|
|
return strategy, nodes
|
|
|
|
|
|
def build_revise_strategy_normalization_sql(
|
|
entry: LedgerEntry,
|
|
prestate: dict[str, Any],
|
|
generated_rows: dict[str, Any],
|
|
) -> str:
|
|
state = _validate_revise_strategy_prestate(prestate)
|
|
generated_strategy, generated_nodes = _validate_revise_strategy_generated_delta(entry, state, generated_rows)
|
|
receipt_strategy, receipt_nodes = _validate_revise_strategy_receipt_rows(
|
|
entry.receipt["proposal"], entry.receipt["canonical_rows"]
|
|
)
|
|
if receipt_strategy["version"] != state["max_strategy_version"] + 1:
|
|
raise ReconstructionError(
|
|
"mutating_receipt_version_mismatch",
|
|
"revise_strategy receipt version does not follow the observed prestate",
|
|
)
|
|
if receipt_strategy["id"] in state["strategy_ids"] or set(row["id"] for row in receipt_nodes).intersection(
|
|
state["strategy_node_ids"]
|
|
):
|
|
raise ReconstructionError(
|
|
"mutating_receipt_id_collision", "revise_strategy receipt IDs collide with the observed prestate"
|
|
)
|
|
|
|
apply_payload = entry.receipt["proposal"]["payload"]["apply_payload"]
|
|
agent = apply_engine.sql_literal(apply_payload["agent_id"])
|
|
generated_strategy_id = str(uuid.UUID(str(generated_strategy["id"])))
|
|
generated_node_ids = [str(uuid.UUID(str(row["id"]))) for row in generated_nodes]
|
|
previous_active = state["active_strategy_ids"]
|
|
changed_node_ids = state["non_retired_strategy_node_ids"]
|
|
transaction_timestamp = apply_engine.sql_literal(receipt_strategy["created_at"])
|
|
generated_node_array = _uuid_array_sql(generated_node_ids)
|
|
previous_active_array = _uuid_array_sql(previous_active)
|
|
changed_node_array = _uuid_array_sql(changed_node_ids)
|
|
|
|
prior_strategy_assertion = ""
|
|
if previous_active:
|
|
prior_strategy_assertion = f"""
|
|
if (select count(*) from public.strategies
|
|
where agent_id = {agent}::uuid and id = any(previous_active_ids) and not active) <> {len(previous_active)} then
|
|
raise exception 'revise_strategy prior active strategy transition mismatch';
|
|
end if;"""
|
|
prior_node_normalization = ""
|
|
if changed_node_ids:
|
|
prior_node_normalization = f"""
|
|
if (select count(*) from public.strategy_nodes
|
|
where agent_id = {agent}::uuid and id = any(changed_node_ids) and status = 'retired') <> {len(changed_node_ids)} then
|
|
raise exception 'revise_strategy prior node transition mismatch';
|
|
end if;
|
|
update public.strategy_nodes
|
|
set status = 'retired', updated_at = {transaction_timestamp}::timestamptz
|
|
where agent_id = {agent}::uuid and id = any(changed_node_ids);
|
|
get diagnostics changed_rows = row_count;
|
|
if changed_rows <> {len(changed_node_ids)} then
|
|
raise exception 'revise_strategy prior node normalization mismatch';
|
|
end if;"""
|
|
|
|
return f"""begin;
|
|
set local standard_conforming_strings = on;
|
|
do $reconstruction$
|
|
declare
|
|
changed_rows integer;
|
|
generated_strategy_id constant uuid := {apply_engine.sql_literal(generated_strategy_id)}::uuid;
|
|
generated_node_ids constant uuid[] := {generated_node_array};
|
|
previous_active_ids constant uuid[] := {previous_active_array};
|
|
changed_node_ids constant uuid[] := {changed_node_array};
|
|
begin{prior_strategy_assertion}
|
|
if exists (
|
|
select 1 from public.strategy_node_anchors
|
|
where from_node_id = any(generated_node_ids) or to_node_id = any(generated_node_ids)
|
|
) then
|
|
raise exception 'revise_strategy generated nodes unexpectedly have anchors';
|
|
end if;
|
|
delete from public.strategy_nodes
|
|
where agent_id = {agent}::uuid and id = any(generated_node_ids);
|
|
get diagnostics changed_rows = row_count;
|
|
if changed_rows <> {len(generated_node_ids)} then
|
|
raise exception 'revise_strategy generated node delta mismatch';
|
|
end if;
|
|
delete from public.strategies
|
|
where agent_id = {agent}::uuid and id = generated_strategy_id;
|
|
get diagnostics changed_rows = row_count;
|
|
if changed_rows <> 1 then
|
|
raise exception 'revise_strategy generated strategy delta mismatch';
|
|
end if;{prior_node_normalization}
|
|
insert into public.strategies
|
|
select seeded.* from jsonb_populate_record(
|
|
null::public.strategies, {_jsonb_literal(receipt_strategy)}
|
|
) seeded;
|
|
insert into public.strategy_nodes
|
|
select seeded.* from jsonb_populate_recordset(
|
|
null::public.strategy_nodes, {_jsonb_literal(receipt_nodes)}
|
|
) seeded;
|
|
if (select count(*) from public.strategies
|
|
where agent_id = {agent}::uuid and active) <> 1 then
|
|
raise exception 'revise_strategy normalized one-active invariant mismatch';
|
|
end if;
|
|
end
|
|
$reconstruction$;
|
|
commit;
|
|
"""
|
|
|
|
|
|
def build_proposal_readback_sql(proposal_id: str) -> str:
|
|
literal = apply_engine.sql_literal(proposal_id)
|
|
return f"""select jsonb_build_object(
|
|
'proposal', (select to_jsonb(p) from kb_stage.kb_proposals p
|
|
where p.id = {literal}::uuid),
|
|
'approval_snapshot', (select to_jsonb(a) from kb_stage.kb_proposal_approvals a
|
|
where a.proposal_id = {literal}::uuid)
|
|
)::text;
|
|
"""
|
|
|
|
|
|
def build_applied_timestamp_normalization_sql(entry: LedgerEntry) -> str:
|
|
applied = entry.applied_proposal
|
|
return f"""do $reconstruction$
|
|
declare
|
|
changed_rows integer;
|
|
begin
|
|
update kb_stage.kb_proposals
|
|
set applied_at = {apply_engine.sql_literal(applied["applied_at"])}::timestamptz,
|
|
updated_at = {apply_engine.sql_literal(applied["updated_at"])}::timestamptz
|
|
where id = {apply_engine.sql_literal(applied["id"])}::uuid
|
|
and status = 'applied'
|
|
and applied_by_handle = {apply_engine.sql_literal(applied["applied_by_handle"])}
|
|
and applied_by_agent_id is not distinct from
|
|
{apply_engine.sql_literal(applied["applied_by_agent_id"])}::uuid;
|
|
get diagnostics changed_rows = row_count;
|
|
if changed_rows <> 1 then
|
|
raise exception 'deterministic proposal timestamp normalization failed';
|
|
end if;
|
|
end
|
|
$reconstruction$;
|
|
"""
|
|
|
|
|
|
def _run_stdin(
|
|
command: list[str],
|
|
*,
|
|
stdin: str,
|
|
timeout: float,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
command,
|
|
input=stdin,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def _diagnostic_fingerprint(completed: subprocess.CompletedProcess[str]) -> str:
|
|
output = (completed.stderr or "") + "\n" + (completed.stdout or "")
|
|
return _sha256_text(output)
|
|
|
|
|
|
def _require_command_success(
|
|
completed: subprocess.CompletedProcess[str],
|
|
*,
|
|
code: str,
|
|
action: str,
|
|
) -> str:
|
|
if completed.returncode != 0:
|
|
fingerprint = _diagnostic_fingerprint(completed)
|
|
raise ReconstructionError(
|
|
code,
|
|
f"{action} failed; private command output withheld; diagnostic_sha256={fingerprint}",
|
|
)
|
|
return completed.stdout
|
|
|
|
|
|
def _psql(
|
|
args: argparse.Namespace,
|
|
container: str,
|
|
sql: str,
|
|
*,
|
|
role: str,
|
|
timeout: float,
|
|
code: str,
|
|
action: str,
|
|
) -> str:
|
|
command = [
|
|
args.docker_bin,
|
|
"exec",
|
|
"-i",
|
|
container,
|
|
"psql",
|
|
"-X",
|
|
"-U",
|
|
role,
|
|
"-h",
|
|
"127.0.0.1",
|
|
"-d",
|
|
args.database,
|
|
"-Atq",
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
]
|
|
try:
|
|
completed = _run_stdin(command, stdin=sql, timeout=timeout)
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
raise ReconstructionError(code, f"{action} did not complete; private details withheld") from exc
|
|
return _require_command_success(completed, code=code, action=action)
|
|
|
|
|
|
def _read_json(
|
|
args: argparse.Namespace,
|
|
container: str,
|
|
sql: str,
|
|
*,
|
|
code: str,
|
|
action: str,
|
|
) -> Any:
|
|
raw = _psql(
|
|
args,
|
|
container,
|
|
sql,
|
|
role="postgres",
|
|
timeout=args.command_timeout,
|
|
code=code,
|
|
action=action,
|
|
).strip()
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise ReconstructionError(code, f"{action} returned an invalid private JSON readback") from exc
|
|
|
|
|
|
def _assert_exact_json(actual: Any, expected: Any, *, code: str, action: str) -> None:
|
|
if _canonical_json(actual) != _canonical_json(expected):
|
|
raise ReconstructionError(code, f"{action} did not match the hash-pinned material")
|
|
|
|
|
|
def _entry_summary(entry: LedgerEntry) -> dict[str, Any]:
|
|
proposal = entry.applied_proposal
|
|
return {
|
|
"sequence": entry.sequence,
|
|
"proposal_id": proposal["id"],
|
|
"proposal_type": proposal["proposal_type"],
|
|
"material_sha256": entry.material_sha256,
|
|
"replay_material_sha256": entry.replay_material_sha256,
|
|
"original_apply_sql_sha256": entry.receipt["apply_engine"]["apply_sql_sha256"],
|
|
"current_apply_sql_sha256": entry.current_apply_sql_sha256,
|
|
"expected_row_counts": entry.receipt["expected_row_counts"],
|
|
"seed_exact": False,
|
|
"proposal_seed_exact": False,
|
|
"canonical_seed_exact": False,
|
|
"guarded_apply_executed": False,
|
|
"mutating_prestate_captured": False,
|
|
"mutating_delta_validated": False,
|
|
"mutating_poststate_normalized": False,
|
|
"proposal_exact": False,
|
|
"canonical_rows_exact": False,
|
|
"status": "pending",
|
|
}
|
|
|
|
|
|
def apply_entry(
|
|
args: argparse.Namespace,
|
|
container: str,
|
|
entry: LedgerEntry,
|
|
summary: dict[str, Any],
|
|
) -> None:
|
|
mutating_prestate: dict[str, Any] | None = None
|
|
try:
|
|
_psql(
|
|
args,
|
|
container,
|
|
build_seed_sql(entry),
|
|
role="postgres",
|
|
timeout=args.apply_timeout,
|
|
code="entry_seed_failed",
|
|
action=f"ledger entry {entry.sequence} exact seed",
|
|
)
|
|
proposal_readback = _read_json(
|
|
args,
|
|
container,
|
|
build_proposal_readback_sql(entry.approved_proposal["id"]),
|
|
code="entry_seed_readback_failed",
|
|
action=f"ledger entry {entry.sequence} proposal seed readback",
|
|
)
|
|
_assert_exact_json(
|
|
proposal_readback,
|
|
{
|
|
"proposal": entry.approved_proposal,
|
|
"approval_snapshot": entry.approval_snapshot,
|
|
},
|
|
code="entry_seed_mismatch",
|
|
action=f"ledger entry {entry.sequence} proposal seed",
|
|
)
|
|
summary["proposal_seed_exact"] = True
|
|
if entry.applied_proposal["proposal_type"] == "revise_strategy":
|
|
mutating_prestate = _validate_revise_strategy_prestate(
|
|
_read_json(
|
|
args,
|
|
container,
|
|
build_revise_strategy_prestate_sql(entry),
|
|
code="mutating_prestate_readback_failed",
|
|
action=f"ledger entry {entry.sequence} revise_strategy prestate readback",
|
|
)
|
|
)
|
|
summary["mutating_prestate_captured"] = True
|
|
else:
|
|
seeded_rows = _read_json(
|
|
args,
|
|
container,
|
|
replay_receipt.build_postflight_sql(entry.receipt["proposal"]),
|
|
code="entry_seed_readback_failed",
|
|
action=f"ledger entry {entry.sequence} canonical seed readback",
|
|
)
|
|
_assert_exact_json(
|
|
seeded_rows,
|
|
entry.receipt["canonical_rows"],
|
|
code="entry_seed_mismatch",
|
|
action=f"ledger entry {entry.sequence} canonical seed",
|
|
)
|
|
summary["canonical_seed_exact"] = True
|
|
summary["seed_exact"] = summary["proposal_seed_exact"] and summary["canonical_seed_exact"]
|
|
|
|
_psql(
|
|
args,
|
|
container,
|
|
entry.current_apply_sql,
|
|
role="kb_apply",
|
|
timeout=args.apply_timeout,
|
|
code="guarded_apply_failed",
|
|
action=f"ledger entry {entry.sequence} guarded apply",
|
|
)
|
|
summary["guarded_apply_executed"] = True
|
|
|
|
if mutating_prestate is not None:
|
|
generated_rows = _read_json(
|
|
args,
|
|
container,
|
|
build_revise_strategy_generated_delta_sql(entry, mutating_prestate),
|
|
code="mutating_delta_readback_failed",
|
|
action=f"ledger entry {entry.sequence} revise_strategy generated delta readback",
|
|
)
|
|
normalization_sql = build_revise_strategy_normalization_sql(entry, mutating_prestate, generated_rows)
|
|
summary["mutating_delta_validated"] = True
|
|
_psql(
|
|
args,
|
|
container,
|
|
normalization_sql,
|
|
role="postgres",
|
|
timeout=args.apply_timeout,
|
|
code="mutating_poststate_normalization_failed",
|
|
action=f"ledger entry {entry.sequence} revise_strategy exact poststate normalization",
|
|
)
|
|
summary["mutating_poststate_normalized"] = True
|
|
|
|
_psql(
|
|
args,
|
|
container,
|
|
build_applied_timestamp_normalization_sql(entry),
|
|
role="postgres",
|
|
timeout=args.command_timeout,
|
|
code="proposal_timestamp_normalization_failed",
|
|
action=f"ledger entry {entry.sequence} deterministic ledger timestamp normalization",
|
|
)
|
|
proposal_readback = _read_json(
|
|
args,
|
|
container,
|
|
build_proposal_readback_sql(entry.applied_proposal["id"]),
|
|
code="entry_postflight_failed",
|
|
action=f"ledger entry {entry.sequence} applied proposal readback",
|
|
)
|
|
_assert_exact_json(
|
|
proposal_readback,
|
|
{
|
|
"proposal": entry.applied_proposal,
|
|
"approval_snapshot": entry.approval_snapshot,
|
|
},
|
|
code="entry_postflight_mismatch",
|
|
action=f"ledger entry {entry.sequence} applied proposal",
|
|
)
|
|
summary["proposal_exact"] = True
|
|
|
|
final_rows = _read_json(
|
|
args,
|
|
container,
|
|
replay_receipt.build_postflight_sql(entry.receipt["proposal"]),
|
|
code="entry_postflight_failed",
|
|
action=f"ledger entry {entry.sequence} exact row readback",
|
|
)
|
|
_assert_exact_json(
|
|
final_rows,
|
|
entry.receipt["canonical_rows"],
|
|
code="entry_postflight_mismatch",
|
|
action=f"ledger entry {entry.sequence} canonical rows",
|
|
)
|
|
summary["canonical_rows_exact"] = True
|
|
summary["status"] = "pass"
|
|
except ReconstructionError:
|
|
summary["status"] = "fail"
|
|
raise
|
|
|
|
|
|
def _sanitize_parity(result: dict[str, Any]) -> dict[str, Any]:
|
|
source = dict(result["source_manifest"])
|
|
source.pop("path", None)
|
|
return {
|
|
"status": result["status"],
|
|
"verifier": result["verifier"],
|
|
"source_manifest": source,
|
|
"target_manifest": result["target_manifest"],
|
|
"problems": result["problems"],
|
|
"details": result["details"],
|
|
}
|
|
|
|
|
|
def _run_parity(
|
|
args: argparse.Namespace,
|
|
container: str,
|
|
manifest_path: Path,
|
|
manifest: dict[str, Any],
|
|
*,
|
|
code: str,
|
|
action: str,
|
|
) -> dict[str, Any]:
|
|
try:
|
|
result = canonical_rebuild.run_parity_check(
|
|
args.docker_bin,
|
|
container,
|
|
args.database,
|
|
source_manifest_path=manifest_path,
|
|
source_manifest=manifest,
|
|
manifest_sql=PARITY_SQL_PATH,
|
|
command_timeout=args.manifest_timeout,
|
|
max_target_query_ms=args.max_target_query_ms,
|
|
max_target_source_ratio=args.max_target_source_ratio,
|
|
)
|
|
except (OSError, ValueError, RuntimeError, subprocess.TimeoutExpired, json.JSONDecodeError) as exc:
|
|
raise ReconstructionError(code, f"{action} could not complete; private details withheld") from exc
|
|
sanitized = _sanitize_parity(result)
|
|
if sanitized["status"] != "pass":
|
|
raise ReconstructionError(code, f"{action} found a manifest mismatch")
|
|
return sanitized
|
|
|
|
|
|
def _initial_receipt(args: argparse.Namespace, container: str) -> dict[str, Any]:
|
|
return {
|
|
"artifact": "local_genesis_plus_ledger_reconstruction",
|
|
"contract_version": 1,
|
|
"generated_at_utc": datetime.now(UTC).isoformat(),
|
|
"status": "running",
|
|
"claim_ceiling": CLAIM_CEILING,
|
|
"not_proven": list(NOT_PROVEN),
|
|
"database": args.database,
|
|
"inputs": {
|
|
"genesis_dump": {"bytes": None, "sha256": None, "pin_verified": False},
|
|
"genesis_manifest": {"bytes": None, "sha256": None, "pin_verified": False},
|
|
"ledger": {"bytes": None, "sha256": None, "pin_verified": False},
|
|
"final_manifest": {"bytes": None, "sha256": None, "pin_verified": False},
|
|
"engine_hashes": {},
|
|
},
|
|
"container": {
|
|
"name": container,
|
|
"image": args.image,
|
|
"network_mode_required": "none",
|
|
"postgres_data_tmpfs_required": (f"rw,nosuid,nodev,noexec,size={args.tmpfs_mb}m"),
|
|
"preexisting_absence_proven": False,
|
|
"started": False,
|
|
"isolation": None,
|
|
},
|
|
"guard_prerequisites": {"status": "pending", "sha256": None},
|
|
"genesis_parity": {"status": "pending"},
|
|
"genesis_key_counts": {},
|
|
"ledger": {"entry_count": None, "entries": []},
|
|
"final_parity": {"status": "pending"},
|
|
"final_key_counts": {},
|
|
"cleanup": {
|
|
"attempted": False,
|
|
"container": container,
|
|
"container_absent": None,
|
|
"status": "pending",
|
|
},
|
|
"timings_seconds": {},
|
|
"safety": {
|
|
"production_database_touched": False,
|
|
"network_access_available_to_container": False,
|
|
"persistent_postgres_storage_used": False,
|
|
"private_replay_material_emitted": False,
|
|
"vps_gcp_telegram_or_live_db_used": False,
|
|
},
|
|
"error": None,
|
|
}
|
|
|
|
|
|
def run_reconstruction(args: argparse.Namespace) -> dict[str, Any]:
|
|
started_at = time.monotonic()
|
|
container = canonical_rebuild.new_container_name(args.container_prefix)
|
|
receipt = _initial_receipt(args, container)
|
|
phase = "input_preflight"
|
|
primary_error: dict[str, Any] | None = None
|
|
|
|
try:
|
|
bundle = load_bundle(args)
|
|
receipt["inputs"] = {
|
|
"genesis_dump": {
|
|
"bytes": bundle.genesis_dump_bytes,
|
|
"sha256": bundle.genesis_dump_sha256,
|
|
"pin_verified": True,
|
|
},
|
|
"genesis_manifest": {
|
|
"bytes": bundle.genesis_manifest_bytes,
|
|
"sha256": bundle.genesis_manifest_sha256,
|
|
"pin_verified": True,
|
|
},
|
|
"ledger": {
|
|
"bytes": bundle.ledger_bytes,
|
|
"sha256": bundle.ledger_sha256,
|
|
"pin_verified": True,
|
|
},
|
|
"final_manifest": {
|
|
"bytes": bundle.final_manifest_bytes,
|
|
"sha256": bundle.final_manifest_sha256,
|
|
"pin_verified": True,
|
|
},
|
|
"engine_hashes": bundle.engine_hashes,
|
|
}
|
|
receipt["ledger"]["entry_count"] = len(bundle.entries)
|
|
receipt["ledger"]["entries"] = [_entry_summary(entry) for entry in bundle.entries]
|
|
|
|
phase = "container_name_preflight"
|
|
absent, absence_error = canonical_rebuild.inspect_container_absence(
|
|
args.docker_bin, container, timeout=args.command_timeout
|
|
)
|
|
if absent is not True:
|
|
raise ReconstructionError(
|
|
"container_name_not_proven_unused",
|
|
"generated disposable container name could not be proven unused",
|
|
) from RuntimeError(absence_error or "container exists")
|
|
receipt["container"]["preexisting_absence_proven"] = True
|
|
|
|
phase = "container_start"
|
|
tmpfs_options = f"{canonical_rebuild.DATA_DIR}:rw,nosuid,nodev,noexec,size={args.tmpfs_mb}m"
|
|
completed = canonical_rebuild._run(
|
|
[
|
|
args.docker_bin,
|
|
"run",
|
|
"--detach",
|
|
"--rm",
|
|
"--network",
|
|
"none",
|
|
"--name",
|
|
container,
|
|
"--label",
|
|
f"{canonical_rebuild.CANARY_LABEL}={container}",
|
|
"--tmpfs",
|
|
tmpfs_options,
|
|
"--env",
|
|
f"POSTGRES_DB={args.database}",
|
|
"--env",
|
|
"POSTGRES_HOST_AUTH_METHOD=trust",
|
|
args.image,
|
|
],
|
|
timeout=args.command_timeout,
|
|
)
|
|
container_id = _require_command_success(
|
|
completed,
|
|
code="container_start_failed",
|
|
action="networkless disposable PostgreSQL start",
|
|
).strip()
|
|
if not container_id:
|
|
raise ReconstructionError("container_start_failed", "container start returned no id")
|
|
receipt["container"]["started"] = True
|
|
receipt["container"]["id_prefix"] = container_id[:12]
|
|
receipt["container"]["isolation"] = canonical_rebuild.inspect_container_isolation(
|
|
args.docker_bin,
|
|
container,
|
|
expected_image=args.image,
|
|
expected_label=container,
|
|
timeout=args.command_timeout,
|
|
)
|
|
|
|
phase = "database_readiness"
|
|
receipt["container"]["named_database_psql_attempts"] = canonical_rebuild.wait_for_named_database(
|
|
args.docker_bin,
|
|
container,
|
|
args.database,
|
|
startup_timeout=args.startup_timeout,
|
|
command_timeout=args.command_timeout,
|
|
poll_interval=args.poll_interval,
|
|
)
|
|
receipt["container"]["named_database_psql_ready"] = True
|
|
|
|
phase = "application_role_bootstrap"
|
|
receipt["application_roles_bootstrapped"] = canonical_rebuild.execute_application_role_bootstrap(
|
|
args.docker_bin,
|
|
container,
|
|
args.database,
|
|
bundle.genesis_manifest,
|
|
timeout=args.command_timeout,
|
|
)
|
|
|
|
phase = "genesis_restore"
|
|
completed = canonical_rebuild._run(
|
|
[
|
|
args.docker_bin,
|
|
"cp",
|
|
str(args.genesis_dump.resolve()),
|
|
f"{container}:{canonical_rebuild.DUMP_DESTINATION}",
|
|
],
|
|
timeout=args.command_timeout,
|
|
)
|
|
_require_command_success(completed, code="genesis_copy_failed", action="genesis dump copy")
|
|
completed = canonical_rebuild._run(
|
|
[
|
|
args.docker_bin,
|
|
"exec",
|
|
container,
|
|
"pg_restore",
|
|
"-U",
|
|
"postgres",
|
|
"-d",
|
|
args.database,
|
|
"--no-owner",
|
|
"--no-privileges",
|
|
"--exit-on-error",
|
|
canonical_rebuild.DUMP_DESTINATION,
|
|
],
|
|
timeout=args.restore_timeout,
|
|
)
|
|
_require_command_success(completed, code="genesis_restore_failed", action="genesis restore")
|
|
|
|
phase = "guard_prerequisites"
|
|
completed = canonical_rebuild._run(
|
|
[
|
|
args.docker_bin,
|
|
"cp",
|
|
str(GUARD_PREREQUISITES_PATH),
|
|
f"{container}:{GUARD_PREREQUISITES_DESTINATION}",
|
|
],
|
|
timeout=args.command_timeout,
|
|
)
|
|
_require_command_success(
|
|
completed,
|
|
code="guard_prerequisites_copy_failed",
|
|
action="guarded apply prerequisites copy",
|
|
)
|
|
completed = canonical_rebuild._run(
|
|
[
|
|
args.docker_bin,
|
|
"exec",
|
|
container,
|
|
"psql",
|
|
"-X",
|
|
"-U",
|
|
"postgres",
|
|
"-d",
|
|
args.database,
|
|
"-Atq",
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
"-f",
|
|
GUARD_PREREQUISITES_DESTINATION,
|
|
],
|
|
timeout=args.apply_timeout,
|
|
)
|
|
_require_command_success(
|
|
completed,
|
|
code="guard_prerequisites_failed",
|
|
action="guarded apply prerequisites",
|
|
)
|
|
receipt["guard_prerequisites"] = {
|
|
"status": "pass",
|
|
"sha256": bundle.engine_hashes["guard_prerequisites_sha256"],
|
|
}
|
|
|
|
phase = "genesis_parity"
|
|
receipt["genesis_parity"] = _run_parity(
|
|
args,
|
|
container,
|
|
args.genesis_manifest,
|
|
bundle.genesis_manifest,
|
|
code="genesis_parity_failed",
|
|
action="genesis parity verification",
|
|
)
|
|
receipt["genesis_key_counts"], _ = canonical_rebuild.collect_key_counts(
|
|
args.docker_bin,
|
|
container,
|
|
args.database,
|
|
timeout=args.command_timeout,
|
|
)
|
|
|
|
phase = "ordered_ledger_replay"
|
|
for entry, summary in zip(bundle.entries, receipt["ledger"]["entries"], strict=True):
|
|
apply_entry(args, container, entry, summary)
|
|
|
|
phase = "final_parity"
|
|
receipt["final_parity"] = _run_parity(
|
|
args,
|
|
container,
|
|
bundle.final_manifest_path,
|
|
bundle.final_manifest,
|
|
code="final_parity_failed",
|
|
action="final parity verification",
|
|
)
|
|
receipt["final_key_counts"], _ = canonical_rebuild.collect_key_counts(
|
|
args.docker_bin,
|
|
container,
|
|
args.database,
|
|
timeout=args.command_timeout,
|
|
)
|
|
receipt["status"] = "pass"
|
|
except ReconstructionError as exc:
|
|
primary_error = {
|
|
"phase": phase,
|
|
"code": exc.code,
|
|
"type": type(exc).__name__,
|
|
"message": exc.safe_message,
|
|
}
|
|
receipt["status"] = "fail"
|
|
receipt["error"] = primary_error
|
|
except (
|
|
OSError,
|
|
ValueError,
|
|
RuntimeError,
|
|
subprocess.TimeoutExpired,
|
|
json.JSONDecodeError,
|
|
KeyboardInterrupt,
|
|
canonical_rebuild.TerminationRequested,
|
|
) as exc:
|
|
primary_error = {
|
|
"phase": phase,
|
|
"code": f"{phase}_failed",
|
|
"type": type(exc).__name__,
|
|
"message": f"{phase} failed; private details withheld",
|
|
}
|
|
receipt["status"] = "fail"
|
|
receipt["error"] = primary_error
|
|
finally:
|
|
cleanup_started = time.monotonic()
|
|
cleanup = canonical_rebuild.cleanup_container(
|
|
args.docker_bin,
|
|
container,
|
|
command_timeout=args.command_timeout,
|
|
poll_interval=args.poll_interval,
|
|
)
|
|
receipt["cleanup"] = cleanup
|
|
receipt["timings_seconds"]["cleanup"] = round(time.monotonic() - cleanup_started, 6)
|
|
receipt["timings_seconds"]["total"] = round(time.monotonic() - started_at, 6)
|
|
if cleanup.get("container_absent") is not True:
|
|
receipt["status"] = "fail"
|
|
cleanup_error = {
|
|
"phase": "cleanup",
|
|
"code": "cleanup_absence_unproven",
|
|
"type": "CleanupProofError",
|
|
"message": "disposable container absence was not independently proven",
|
|
}
|
|
if primary_error is None:
|
|
receipt["error"] = cleanup_error
|
|
else:
|
|
receipt["cleanup_error"] = cleanup_error
|
|
if cleanup.get("interrupted"):
|
|
receipt["status"] = "fail"
|
|
interruption_error = {
|
|
"phase": "cleanup",
|
|
"code": "cleanup_interrupted",
|
|
"type": "TerminationRequested",
|
|
"message": "cleanup was interrupted; private details withheld",
|
|
}
|
|
if primary_error is None:
|
|
receipt["error"] = interruption_error
|
|
else:
|
|
receipt["cleanup_interruption"] = interruption_error
|
|
return receipt
|
|
|
|
|
|
def _declared_artifact_paths(ledger_path: Path) -> set[Path]:
|
|
ledger = _load_json_object(ledger_path, "ledger")
|
|
paths = {ledger_path.resolve()}
|
|
final = ledger.get("final_parity")
|
|
if isinstance(final, dict) and isinstance(final.get("manifest"), str):
|
|
paths.add(_resolve_artifact(ledger_path, final["manifest"], "final parity manifest"))
|
|
entries = ledger.get("entries")
|
|
if isinstance(entries, list):
|
|
for index, entry in enumerate(entries):
|
|
if isinstance(entry, dict) and isinstance(entry.get("material"), str):
|
|
paths.add(_resolve_artifact(ledger_path, entry["material"], f"entry {index + 1} material"))
|
|
return paths
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--genesis-dump", required=True, type=Path)
|
|
parser.add_argument("--genesis-manifest", required=True, type=Path)
|
|
parser.add_argument("--ledger", required=True, type=Path)
|
|
parser.add_argument("--ledger-sha256", required=True)
|
|
parser.add_argument("--database", default="teleo")
|
|
parser.add_argument("--image", default=DEFAULT_REBUILD_IMAGE)
|
|
parser.add_argument("--container-prefix", default="teleo-genesis-ledger-rebuild")
|
|
parser.add_argument("--tmpfs-mb", default=1024, type=int)
|
|
parser.add_argument("--startup-timeout", default=60.0, type=float)
|
|
parser.add_argument("--restore-timeout", default=300.0, type=float)
|
|
parser.add_argument("--apply-timeout", default=180.0, type=float)
|
|
parser.add_argument("--manifest-timeout", default=180.0, type=float)
|
|
parser.add_argument("--command-timeout", default=30.0, type=float)
|
|
parser.add_argument("--poll-interval", default=0.25, type=float)
|
|
parser.add_argument("--max-target-query-ms", default=2000.0, type=float)
|
|
parser.add_argument("--max-target-source-ratio", default=20.0, type=float)
|
|
parser.add_argument("--docker-bin", default="docker")
|
|
parser.add_argument("--output", required=True, type=Path)
|
|
args = parser.parse_args(argv)
|
|
|
|
if not canonical_rebuild.DATABASE_NAME_RE.fullmatch(args.database):
|
|
parser.error("--database must be a safe PostgreSQL identifier up to 63 characters")
|
|
if not canonical_rebuild.CONTAINER_PREFIX_RE.fullmatch(args.container_prefix):
|
|
parser.error("--container-prefix must be 1-40 Docker-safe characters")
|
|
if not IMAGE_DIGEST_RE.fullmatch(args.image):
|
|
parser.error("--image must be pinned by a lowercase sha256 digest")
|
|
if not args.docker_bin or any(character.isspace() for character in args.docker_bin):
|
|
parser.error("--docker-bin must be one executable name or path without whitespace")
|
|
if not 64 <= args.tmpfs_mb <= 65536:
|
|
parser.error("--tmpfs-mb must be between 64 and 65536")
|
|
for value, flag in (
|
|
(args.startup_timeout, "--startup-timeout"),
|
|
(args.restore_timeout, "--restore-timeout"),
|
|
(args.apply_timeout, "--apply-timeout"),
|
|
(args.manifest_timeout, "--manifest-timeout"),
|
|
(args.command_timeout, "--command-timeout"),
|
|
(args.poll_interval, "--poll-interval"),
|
|
(args.max_target_query_ms, "--max-target-query-ms"),
|
|
(args.max_target_source_ratio, "--max-target-source-ratio"),
|
|
):
|
|
if value <= 0:
|
|
parser.error(f"{flag} must be positive")
|
|
for path, flag in (
|
|
(args.genesis_dump, "--genesis-dump"),
|
|
(args.genesis_manifest, "--genesis-manifest"),
|
|
(args.ledger, "--ledger"),
|
|
):
|
|
if not path.is_file():
|
|
parser.error(f"{flag} must be an existing regular file")
|
|
if not SHA256_RE.fullmatch(args.ledger_sha256):
|
|
parser.error("--ledger-sha256 must be a lowercase SHA-256 digest")
|
|
try:
|
|
protected = {
|
|
args.genesis_dump.resolve(),
|
|
args.genesis_manifest.resolve(),
|
|
*_declared_artifact_paths(args.ledger),
|
|
*FIXED_ENGINE_PATHS,
|
|
}
|
|
except ReconstructionError as exc:
|
|
parser.error(exc.safe_message)
|
|
if args.output.resolve() in protected:
|
|
parser.error("--output must not overwrite a reconstruction input")
|
|
return args
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(argv)
|
|
previous_handlers: dict[signal.Signals, Any] = {}
|
|
for watched_signal in (signal.SIGINT, signal.SIGTERM):
|
|
previous_handlers[watched_signal] = signal.getsignal(watched_signal)
|
|
signal.signal(watched_signal, canonical_rebuild._termination_handler)
|
|
try:
|
|
payload = run_reconstruction(args)
|
|
finally:
|
|
for watched_signal, previous in previous_handlers.items():
|
|
signal.signal(watched_signal, previous)
|
|
|
|
try:
|
|
canonical_rebuild.write_receipt(args.output, payload)
|
|
except OSError:
|
|
payload["status"] = "fail"
|
|
payload["receipt_write_error"] = "receipt write failed; private details withheld"
|
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
return 0 if payload["status"] == "pass" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|