teleo-infrastructure/scripts/export_kb_transition_replay_material.py

507 lines
19 KiB
Python

#!/usr/bin/env python3
"""Export exact private replay material for one guarded applied proposal.
The exporter is intentionally read-only. It validates an existing worker replay
receipt, reads the approved and applied proposal rows through one gate-owned
function, and atomically publishes deterministic genesis-ledger input.
"""
from __future__ import annotations
import argparse
import json
import os
import stat
import sys
import tempfile
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import apply_proposal as apply_engine # noqa: E402
import kb_apply_replay_receipt as replay_receipt # noqa: E402
MATERIAL_ARTIFACT = "teleo_strict_proposal_replay_material"
MATERIAL_CONTRACT_VERSION = 1
MATERIAL_FIELDS = frozenset(
{
"artifact",
"contract_version",
"sequence",
"approved_proposal",
"approval_snapshot",
"applied_proposal",
"replay_receipt",
}
)
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",
}
)
PROPOSAL_TRANSITION_FIELDS = frozenset(
{"status", "applied_by_handle", "applied_by_agent_id", "applied_at", "updated_at"}
)
PROPOSAL_UUID_FIELDS = frozenset({"id", "proposed_by_agent_id", "reviewed_by_agent_id", "applied_by_agent_id"})
PROPOSAL_TIMESTAMP_FIELDS = frozenset({"reviewed_at", "applied_at", "created_at", "updated_at"})
APPROVAL_UUID_FIELDS = frozenset({"proposal_id", "reviewed_by_agent_id"})
APPROVAL_TIMESTAMP_FIELDS = frozenset({"reviewed_at"})
RECEIPT_PROPOSAL_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",
)
class ExportError(RuntimeError):
"""A fail-closed error that contains no proposal payload material."""
def _canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
def _same_json(left: Any, right: Any) -> bool:
return _canonical_json(left) == _canonical_json(right)
def _reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ExportError("JSON input contains duplicate object fields")
result[key] = value
return result
def _reject_nonfinite_number(value: str) -> None:
raise ExportError(f"JSON input contains invalid numeric constant {value}")
def _parse_json_object(raw: str, *, label: str) -> dict[str, Any]:
try:
value = json.loads(
raw,
object_pairs_hook=_reject_duplicate_keys,
parse_constant=_reject_nonfinite_number,
)
except (json.JSONDecodeError, UnicodeError) as exc:
raise ExportError(f"{label} is not valid UTF-8 JSON") from exc
if not isinstance(value, dict):
raise ExportError(f"{label} must contain one JSON object")
return value
def _absolute_path(value: str | Path) -> Path:
path = Path(value).expanduser()
if not path.is_absolute():
path = Path.cwd() / path
return path
def _load_private_receipt(path: Path) -> dict[str, Any]:
path = _absolute_path(path)
try:
metadata = path.lstat()
except OSError as exc:
raise ExportError("worker replay receipt is unavailable") from exc
if path.is_symlink() or not stat.S_ISREG(metadata.st_mode):
raise ExportError("worker replay receipt must be a regular non-symlink file")
if stat.S_IMODE(metadata.st_mode) != 0o600:
raise ExportError("worker replay receipt permissions must be exactly 0600")
try:
raw = path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as exc:
raise ExportError("worker replay receipt is unreadable") from exc
return _parse_json_object(raw, label="worker replay receipt")
def _require_exact_fields(value: Any, expected: frozenset[str], *, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ExportError(f"{label} must be an object")
observed = frozenset(value)
if observed != expected:
raise ExportError(
f"{label} has invalid fields; missing_count={len(expected - observed)}, "
f"extra_count={len(observed - expected)}"
)
return value
def _canonical_uuid(value: Any, *, label: str, nullable: bool = False) -> str | None:
if value is None and nullable:
return None
try:
return str(uuid.UUID(str(value)))
except (AttributeError, TypeError, ValueError) as exc:
raise ExportError(f"{label} must be a UUID") from exc
def _canonical_timestamp(value: Any, *, label: str, nullable: bool = False) -> str | None:
if value is None and nullable:
return None
try:
return replay_receipt.canonical_utc_timestamp(value, path=label)
except ValueError as exc:
raise ExportError(f"{label} must be a timezone-aware timestamp") from exc
def _normalize_proposal(value: Any, *, label: str) -> dict[str, Any]:
row = dict(_require_exact_fields(value, PROPOSAL_FIELDS, label=label))
for field in PROPOSAL_UUID_FIELDS:
row[field] = _canonical_uuid(
row[field],
label=f"{label}.{field}",
nullable=field != "id",
)
for field in PROPOSAL_TIMESTAMP_FIELDS:
row[field] = _canonical_timestamp(
row[field],
label=f"{label}.{field}",
nullable=field in {"reviewed_at", "applied_at"},
)
if not isinstance(row["proposal_type"], str) or not row["proposal_type"]:
raise ExportError(f"{label}.proposal_type must be a non-empty string")
if not isinstance(row["status"], str) or not row["status"]:
raise ExportError(f"{label}.status must be a non-empty string")
if not isinstance(row["payload"], dict):
raise ExportError(f"{label}.payload must be an object")
for field in (
PROPOSAL_FIELDS
- PROPOSAL_UUID_FIELDS
- PROPOSAL_TIMESTAMP_FIELDS
- {
"payload",
}
):
if field in {"proposal_type", "status"}:
continue
if row[field] is not None and not isinstance(row[field], str):
raise ExportError(f"{label}.{field} must be text or null")
return row
def _normalize_approval(value: Any, *, label: str) -> dict[str, Any]:
row = dict(_require_exact_fields(value, APPROVAL_FIELDS, label=label))
for field in APPROVAL_UUID_FIELDS:
row[field] = _canonical_uuid(row[field], label=f"{label}.{field}")
for field in APPROVAL_TIMESTAMP_FIELDS:
row[field] = _canonical_timestamp(row[field], label=f"{label}.{field}")
if not isinstance(row["payload"], dict):
raise ExportError(f"{label}.payload must be an object")
for field in APPROVAL_FIELDS - APPROVAL_UUID_FIELDS - APPROVAL_TIMESTAMP_FIELDS - {"payload"}:
if not isinstance(row[field], str) or not row[field].strip():
raise ExportError(f"{label}.{field} must be non-empty text")
return row
def _parse_timestamp(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def validate_material(
material: dict[str, Any],
*,
expected_proposal_id: str | None = None,
expected_sequence: int | None = None,
) -> dict[str, Any]:
"""Validate and canonicalize the exact seven-field replay material."""
_require_exact_fields(material, MATERIAL_FIELDS, label="replay material")
if material["artifact"] != MATERIAL_ARTIFACT:
raise ExportError("replay material artifact is unsupported")
if type(material["contract_version"]) is not int or material["contract_version"] != MATERIAL_CONTRACT_VERSION:
raise ExportError("replay material contract version is unsupported")
sequence = material["sequence"]
if type(sequence) is not int or sequence <= 0:
raise ExportError("replay material sequence must be a positive integer")
if expected_sequence is not None and sequence != expected_sequence:
raise ExportError("replay material sequence does not match the requested sequence")
approved = _normalize_proposal(material["approved_proposal"], label="approved_proposal")
approval = _normalize_approval(material["approval_snapshot"], label="approval_snapshot")
applied = _normalize_proposal(material["applied_proposal"], label="applied_proposal")
proposal_id = _canonical_uuid(applied["id"], label="applied_proposal.id")
if expected_proposal_id is not None and proposal_id != _canonical_uuid(
expected_proposal_id, label="expected_proposal_id"
):
raise ExportError("database proposal id does not match the requested proposal")
if approved["id"] != proposal_id or approval["proposal_id"] != proposal_id:
raise ExportError("approved, approval, and applied proposal ids do not match")
if approved["status"] != "approved" or applied["status"] != "applied":
raise ExportError("replay material requires exact approved and applied proposal states")
if any(approved[field] is not None for field in ("applied_by_handle", "applied_by_agent_id", "applied_at")):
raise ExportError("approved proposal snapshot already contains apply metadata")
if not applied["applied_by_handle"] or not applied["applied_by_agent_id"] or not applied["applied_at"]:
raise ExportError("applied proposal row lacks exact apply metadata")
if not approved["reviewed_by_handle"] or not approved["reviewed_by_agent_id"] or not approved["reviewed_at"]:
raise ExportError("approved proposal row lacks exact review metadata")
changed_fields = {field for field in PROPOSAL_FIELDS if not _same_json(approved[field], applied[field])}
if changed_fields != PROPOSAL_TRANSITION_FIELDS:
raise ExportError("proposal rows differ outside or omit the exact guarded apply transition")
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 not _same_json(approval[field], expected):
raise ExportError(f"approval snapshot does not match approved proposal field {field}")
receipt = material["replay_receipt"]
if not isinstance(receipt, dict):
raise ExportError("replay_receipt must be an object")
try:
replay_receipt.validate_replay_receipt(receipt, expected_proposal_id=proposal_id)
except ValueError as exc:
raise ExportError("worker replay receipt failed its exact row-level contract") from exc
receipt_proposal = receipt.get("proposal")
if not isinstance(receipt_proposal, dict):
raise ExportError("worker replay receipt has no proposal envelope")
applied_envelope = {field: applied[field] for field in RECEIPT_PROPOSAL_FIELDS}
if not _same_json(receipt_proposal, applied_envelope):
raise ExportError("worker replay receipt does not match the exact applied proposal row")
created_at = _parse_timestamp(approved["created_at"])
reviewed_at = _parse_timestamp(approved["reviewed_at"])
approved_updated_at = _parse_timestamp(approved["updated_at"])
applied_at = _parse_timestamp(applied["applied_at"])
applied_updated_at = _parse_timestamp(applied["updated_at"])
if not created_at <= reviewed_at <= approved_updated_at < applied_at <= applied_updated_at:
raise ExportError("proposal transition timestamps are not in exact lifecycle order")
return {
"artifact": MATERIAL_ARTIFACT,
"contract_version": MATERIAL_CONTRACT_VERSION,
"sequence": sequence,
"approved_proposal": approved,
"approval_snapshot": approval,
"applied_proposal": applied,
"replay_receipt": receipt,
}
def fetch_transition_rows(args: argparse.Namespace, password: str, proposal_id: str) -> dict[str, Any]:
sql = f"""begin transaction isolation level repeatable read read only;
select kb_stage.export_applied_proposal_replay_rows(
{apply_engine.sql_literal(proposal_id)}::uuid
)::text;
rollback;
"""
raw = apply_engine.run_psql(args, sql, password, redact_output_on_error=True).strip()
if not raw or "\n" in raw:
raise ExportError("applied transition export returned an invalid row count")
return _parse_json_object(raw, label="applied transition export")
def _resolved_output(path: Path) -> Path:
absolute = _absolute_path(path)
if absolute.name in {"", ".", ".."}:
raise ExportError("output path must name a file")
return absolute.parent.resolve(strict=False) / absolute.name
def _paths_alias(left: Path, right: Path) -> bool:
if left.resolve(strict=False) == right.resolve(strict=False):
return True
try:
return left.exists() and right.exists() and os.path.samefile(left, right)
except OSError:
return False
def _assert_output_safe(path: Path, protected_paths: tuple[Path, ...]) -> None:
for protected in protected_paths:
if _paths_alias(path, protected):
raise ExportError("output path aliases a protected input or engine file")
try:
metadata = path.lstat()
except FileNotFoundError:
return
if path.is_symlink() or not stat.S_ISREG(metadata.st_mode):
raise ExportError("output path must be a regular non-symlink file")
if metadata.st_nlink != 1:
raise ExportError("output path has unsafe hard-link aliases")
if stat.S_IMODE(metadata.st_mode) != 0o600:
raise ExportError("existing output permissions must be exactly 0600")
def _sync_directory(path: Path) -> None:
directory_fd = os.open(path, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
def write_private_material(
path: Path,
material: dict[str, Any],
*,
protected_paths: tuple[Path, ...],
) -> tuple[Path, str]:
"""Publish once, or accept an exact 0600 rerun without overwriting drift."""
output = _resolved_output(path)
output.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
_assert_output_safe(output, protected_paths)
payload = (json.dumps(material, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
if output.exists():
if output.read_bytes() != payload:
raise ExportError("existing output contains different replay material")
return output, "unchanged"
fd, temporary_name = tempfile.mkstemp(prefix=f".{output.name}.", dir=str(output.parent))
temporary = Path(temporary_name)
published = False
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
try:
os.link(temporary, output)
published = True
except FileExistsError:
_assert_output_safe(output, protected_paths)
if output.read_bytes() != payload:
raise ExportError("output changed concurrently during replay material export") from None
_sync_directory(output.parent)
finally:
try:
temporary.unlink()
except FileNotFoundError:
pass
_assert_output_safe(output, protected_paths)
if output.read_bytes() != payload:
raise ExportError("published replay material failed exact readback")
return output, "created" if published else "unchanged"
def _positive_sequence(value: str) -> int:
try:
sequence = int(value)
except ValueError as exc:
raise argparse.ArgumentTypeError("sequence must be a positive integer") from exc
if sequence <= 0:
raise argparse.ArgumentTypeError("sequence must be a positive integer")
return sequence
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("proposal_id", help="UUID of the applied proposal")
parser.add_argument("--replay-receipt", required=True, help="private 0600 worker replay receipt")
parser.add_argument("--sequence", required=True, type=_positive_sequence, help="positive ledger sequence")
parser.add_argument("--output", required=True, help="private replay material output path")
parser.add_argument("--secrets-file", default=apply_engine.DEFAULT_SECRETS_FILE)
parser.add_argument("--container", default=apply_engine.DEFAULT_CONTAINER)
parser.add_argument("--db", default=apply_engine.DEFAULT_DB)
parser.add_argument("--host", default=apply_engine.DEFAULT_HOST)
parser.add_argument("--role", default=apply_engine.DEFAULT_ROLE)
return parser.parse_args(argv)
def run(args: argparse.Namespace) -> int:
proposal_id = _canonical_uuid(args.proposal_id, label="proposal_id")
receipt_path = _absolute_path(args.replay_receipt)
receipt = _load_private_receipt(receipt_path)
try:
replay_receipt.validate_replay_receipt(receipt, expected_proposal_id=proposal_id)
except ValueError as exc:
raise ExportError("worker replay receipt failed its exact row-level contract") from exc
password = apply_engine.load_password(args.secrets_file)
rows = fetch_transition_rows(args, password, proposal_id)
material = validate_material(
{
"artifact": MATERIAL_ARTIFACT,
"contract_version": MATERIAL_CONTRACT_VERSION,
"sequence": args.sequence,
"approved_proposal": rows.get("approved_proposal"),
"approval_snapshot": rows.get("approval_snapshot"),
"applied_proposal": rows.get("applied_proposal"),
"replay_receipt": receipt,
},
expected_proposal_id=proposal_id,
expected_sequence=args.sequence,
)
protected_paths = tuple(
_absolute_path(path)
for path in (
receipt_path,
args.secrets_file,
Path(__file__),
Path(apply_engine.__file__),
Path(replay_receipt.__file__),
)
)
_, disposition = write_private_material(
Path(args.output),
material,
protected_paths=protected_paths,
)
print(f"replay material {disposition}: proposal={proposal_id} sequence={args.sequence}")
return 0
def main(argv: list[str] | None = None) -> int:
try:
return run(parse_args(sys.argv[1:] if argv is None else argv))
except ExportError as exc:
print(f"replay material export refused: {exc}", file=sys.stderr)
return 1
except OSError:
print("replay material export refused: private file operation failed", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())