From 1e58b44fae16321e4e63d4bda7c8d708c7e26ef1 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 02:46:18 +0200 Subject: [PATCH 01/27] feat: stage source-linked ingestion proposals without canonical writes --- .../document-ingestion-v2.scenario.json | 52 + .../working-leo/document-ingestion-v2.txt | 5 + .../telegram-transcript-ingestion-v1.jsonl | 3 + ...gram-transcript-ingestion-v1.scenario.json | 99 ++ scripts/prepare_kb_source_manifest.py | 135 ++- ...run_leo_local_ingestion_proposal_canary.py | 979 ++++++++++++++---- scripts/stage_normalized_proposal.py | 6 + tests/test_prepare_kb_source_manifest.py | 78 +- ...run_leo_local_ingestion_proposal_canary.py | 395 ++++--- tests/test_stage_normalized_proposal.py | 31 + 10 files changed, 1442 insertions(+), 341 deletions(-) create mode 100644 fixtures/working-leo/document-ingestion-v2.scenario.json create mode 100644 fixtures/working-leo/document-ingestion-v2.txt create mode 100644 fixtures/working-leo/telegram-transcript-ingestion-v1.jsonl create mode 100644 fixtures/working-leo/telegram-transcript-ingestion-v1.scenario.json diff --git a/fixtures/working-leo/document-ingestion-v2.scenario.json b/fixtures/working-leo/document-ingestion-v2.scenario.json new file mode 100644 index 0000000..bf9a5f6 --- /dev/null +++ b/fixtures/working-leo/document-ingestion-v2.scenario.json @@ -0,0 +1,52 @@ +{ + "schema": "livingip.workingLeoSourceIngestionScenario.v2", + "artifact": { + "path": "document-ingestion-v2.txt", + "format": "plain_text" + }, + "source": { + "identity": "document:working-leo-review-gated-composition-v2", + "source_key": "working_leo_document_ingestion_v2", + "source_type": "article", + "title": "Working Leo review-gated composition note", + "locator": "fixture://working-leo/document-ingestion-v2", + "metadata": { + "author": "Working Leo synthetic canary fixture", + "captured_at": "2026-07-15T00:00:00Z", + "document_kind": "operating_note", + "language": "en", + "synthetic": true + } + }, + "extractor": { + "name": "deterministic_fixture_replay", + "version": "2" + }, + "extraction": { + "claims": [ + { + "claim_key": "staged_proposal_remains_noncanonical", + "type": "structural", + "confidence": 0.75, + "text": "A staged knowledge proposal remains non-canonical until review and guarded apply succeed.", + "body": "A staged proposal remains non-canonical until an operator reviews it and a separate guarded apply succeeds.", + "metadata": { + "needs_research": false + }, + "evidence": [ + { + "segment_id": "paragraph-2", + "role": "grounds", + "excerpt": "A staged proposal remains non-canonical until an operator reviews it and a separate guarded apply succeeds.", + "metadata": { + "evidence_kind": "exact_quote" + } + } + ], + "edges": [] + } + ], + "duplicates": [], + "conflicts": [] + } +} diff --git a/fixtures/working-leo/document-ingestion-v2.txt b/fixtures/working-leo/document-ingestion-v2.txt new file mode 100644 index 0000000..470fc38 --- /dev/null +++ b/fixtures/working-leo/document-ingestion-v2.txt @@ -0,0 +1,5 @@ +Working Leo composition note. A newly captured document may produce claim and evidence candidates, but those candidates are not canonical knowledge. + +A staged proposal remains non-canonical until an operator reviews it and a separate guarded apply succeeds. The proposal must retain the source content hash, exact evidence excerpt, and source identity so reviewers can trace every planned row back to the captured artifact. + +For a staging-only rehearsal, pending_review is the terminal state. No public knowledge-base row should be written. diff --git a/fixtures/working-leo/telegram-transcript-ingestion-v1.jsonl b/fixtures/working-leo/telegram-transcript-ingestion-v1.jsonl new file mode 100644 index 0000000..dd3c7cf --- /dev/null +++ b/fixtures/working-leo/telegram-transcript-ingestion-v1.jsonl @@ -0,0 +1,3 @@ +{"ts":"2026-07-15T09:00:01Z","chat_id":900001,"chat_title":"Synthetic review gate thread","message_id":7301,"type":"message","username":"fixture_analyst","display_name":"Fixture Analyst","user_id":910001,"message":"A pending review does not change canonical knowledge; only a separate guarded apply can do that.","reply_to":null,"synthetic":true} +{"ts":"2026-07-15T09:00:12Z","chat_id":900001,"chat_title":"Synthetic review gate thread","message_id":7302,"type":"message","username":"fixture_operator","display_name":"Fixture Operator","user_id":910002,"message":"Reviewer approval alone makes the proposal canonical immediately; no apply step is needed.","reply_to":7301,"synthetic":true} +{"ts":"2026-07-15T09:00:24Z","chat_id":900001,"chat_title":"Synthetic review gate thread","message_id":7303,"type":"message","username":"fixture_reviewer","display_name":"Fixture Reviewer","user_id":910003,"message":"A pending review does not change canonical knowledge; only a separate guarded apply can do that.","reply_to":7302,"synthetic":true} diff --git a/fixtures/working-leo/telegram-transcript-ingestion-v1.scenario.json b/fixtures/working-leo/telegram-transcript-ingestion-v1.scenario.json new file mode 100644 index 0000000..696ecbb --- /dev/null +++ b/fixtures/working-leo/telegram-transcript-ingestion-v1.scenario.json @@ -0,0 +1,99 @@ +{ + "schema": "livingip.workingLeoSourceIngestionScenario.v2", + "artifact": { + "path": "telegram-transcript-ingestion-v1.jsonl", + "format": "telegram_jsonl" + }, + "source": { + "identity": "fixture:telegram-chat-900001-export-20260715", + "source_key": "telegram_review_gate_exchange_20260715", + "source_type": "transcript", + "title": "Synthetic Telegram review-gate exchange", + "locator": "fixture://working-leo/telegram-transcript-ingestion-v1", + "metadata": { + "captured_at": "2026-07-15T09:01:00Z", + "export_format": "teleo_append_only_telegram_jsonl", + "language": "en", + "synthetic": true + } + }, + "extractor": { + "name": "deterministic_telegram_jsonl_replay", + "version": "1" + }, + "extraction": { + "claims": [ + { + "claim_key": "pending_review_does_not_change_canonical", + "type": "structural", + "confidence": 0.7, + "text": "Pending review alone does not change canonical knowledge.", + "body": "A pending review does not change canonical knowledge; only a separate guarded apply can do that.", + "metadata": { + "needs_research": false + }, + "evidence": [ + { + "segment_id": "message-900001-7301", + "role": "grounds", + "excerpt": "A pending review does not change canonical knowledge; only a separate guarded apply can do that.", + "metadata": { + "evidence_kind": "exact_message" + } + }, + { + "segment_id": "message-900001-7303", + "role": "illustrates", + "excerpt": "A pending review does not change canonical knowledge; only a separate guarded apply can do that.", + "metadata": { + "evidence_kind": "duplicate_exact_message" + } + } + ], + "edges": [ + { + "edge_type": "contradicts", + "target": "approval_alone_makes_canonical" + } + ] + }, + { + "claim_key": "approval_alone_makes_canonical", + "type": "empirical", + "confidence": 0.4, + "text": "Reviewer approval alone makes a proposal canonical without apply.", + "body": "Reviewer approval alone makes the proposal canonical immediately; no apply step is needed.", + "metadata": { + "needs_research": true + }, + "evidence": [ + { + "segment_id": "message-900001-7302", + "role": "grounds", + "excerpt": "Reviewer approval alone makes the proposal canonical immediately; no apply step is needed.", + "metadata": { + "evidence_kind": "exact_message" + } + } + ], + "edges": [] + } + ], + "duplicates": [ + { + "segment_id": "message-900001-7303", + "duplicate_of_claim_key": "pending_review_does_not_change_canonical", + "excerpt": "A pending review does not change canonical knowledge; only a separate guarded apply can do that.", + "reason": "Message 7303 repeats message 7301 exactly and is retained as explicit duplicate evidence." + } + ], + "conflicts": [ + { + "from_claim_key": "pending_review_does_not_change_canonical", + "to_claim_key": "approval_alone_makes_canonical", + "relationship": "contradicts", + "reason": "The two candidate propositions assert incompatible canonical-state transitions." + } + ] + } +} diff --git a/scripts/prepare_kb_source_manifest.py b/scripts/prepare_kb_source_manifest.py index 8a16403..6823e1d 100644 --- a/scripts/prepare_kb_source_manifest.py +++ b/scripts/prepare_kb_source_manifest.py @@ -34,7 +34,7 @@ DEFAULT_MODEL = "anthropic/claude-sonnet-4.5" EXTRACTOR_VERSION = "2" DEFAULT_OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" DEFAULT_KEY_FILE = Path("/opt/teleo-eval/secrets/openrouter-key") -TEXT_SUFFIXES = {".csv", ".htm", ".html", ".json", ".md", ".rst", ".txt", ".xml"} +TEXT_SUFFIXES = {".csv", ".htm", ".html", ".json", ".jsonl", ".md", ".rst", ".txt", ".xml"} MAX_SOURCE_CHARS = 120_000 MAX_CANDIDATES = 20 MAX_CLAIMS = 3 @@ -124,20 +124,37 @@ def _load_context(path: Path) -> dict[str, Any]: return {"database_search_query": query.strip(), "candidate_claims": normalized} -def build_source_fragments(text: str) -> dict[str, str]: - """Label non-empty source lines so the model selects text instead of copying it.""" +def build_source_fragment_index(text: str) -> tuple[dict[str, str], dict[str, dict[str, Any]]]: + """Return selectable lines plus exact line/character provenance.""" fragments: dict[str, str] = {} - for line in text.splitlines(): - if line.strip(): - fragments[f"S{len(fragments) + 1:05d}"] = line + locations: dict[str, dict[str, Any]] = {} + offset = 0 + for line_number, raw_line in enumerate(text.splitlines(keepends=True), start=1): + line = raw_line.rstrip("\r\n") + quote = line.strip() + if quote: + reference = f"S{len(fragments) + 1:05d}" + start = offset + line.find(quote) + fragments[reference] = quote + locations[reference] = { + "reference": reference, + "line": line_number, + "char_start": start, + "char_end": start + len(quote), + "quote_sha256": sha256_bytes(quote.encode("utf-8")), + } + offset += len(raw_line) if not fragments: raise PreparationError("extracted text has no selectable source lines") - return fragments + return fragments, locations -def build_prompt( - fragments: dict[str, str], context: dict[str, Any], repair_error: str | None = None -) -> str: +def build_source_fragments(text: str) -> dict[str, str]: + """Label non-empty source lines so the model selects text instead of copying it.""" + return build_source_fragment_index(text)[0] + + +def build_prompt(fragments: dict[str, str], context: dict[str, Any], repair_error: str | None = None) -> str: candidates = json.dumps(context["candidate_claims"], indent=2, ensure_ascii=True) source = "\n".join(f"[{reference}] {line}" for reference, line in fragments.items()) repair = "" @@ -273,9 +290,7 @@ def parse_model_output(content: str) -> dict[str, Any]: if not isinstance(claim, dict): raise PreparationError(f"model extraction claims[{index}] must be an object") if set(claim) != MODEL_CLAIM_KEYS: - raise PreparationError( - f"model extraction claims[{index}] keys must equal {sorted(MODEL_CLAIM_KEYS)}" - ) + raise PreparationError(f"model extraction claims[{index}] keys must equal {sorted(MODEL_CLAIM_KEYS)}") confidence = claim.get("confidence") if confidence is not None and ( isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 0.75 @@ -304,27 +319,47 @@ def _resolve_reference(reference: Any, fragments: dict[str, str], label: str) -> return fragments[reference] -def resolve_model_output(selection: dict[str, Any], fragments: dict[str, str]) -> dict[str, Any]: +def resolve_model_output( + selection: dict[str, Any], + fragments: dict[str, str], + fragment_locations: dict[str, dict[str, Any]] | None = None, +) -> dict[str, Any]: + def selected(kind: str, reference: str, **identity: Any) -> dict[str, Any]: + result = {"kind": kind, "reference": reference, **identity} + if fragment_locations is not None: + result.update(fragment_locations[reference]) + return result + claims: list[dict[str, Any]] = [] - selected_references: list[dict[str, str]] = [] + selected_references: list[dict[str, Any]] = [] for claim_index, claim in enumerate(selection["claims"]): quote_ref = claim["quote_ref"] quote = _resolve_reference(quote_ref, fragments, f"claims[{claim_index}].quote_ref") - selected_references.append({"kind": "claim", "reference": quote_ref}) + selected_references.append(selected("claim", quote_ref, claim_key=claim["claim_key"], quote=quote)) evidence: list[dict[str, str]] = [] for evidence_index, row in enumerate(claim["evidence"]): evidence_ref = row["quote_ref"] + evidence_quote = _resolve_reference( + evidence_ref, + fragments, + f"claims[{claim_index}].evidence[{evidence_index}].quote_ref", + ) evidence.append( { - "quote": _resolve_reference( - evidence_ref, - fragments, - f"claims[{claim_index}].evidence[{evidence_index}].quote_ref", - ), + "quote": evidence_quote, "role": row["role"], } ) - selected_references.append({"kind": "evidence", "reference": evidence_ref}) + selected_references.append( + selected( + "evidence", + evidence_ref, + claim_key=claim["claim_key"], + evidence_index=evidence_index, + evidence_role=row["role"], + quote=evidence_quote, + ) + ) claims.append( { "claim_key": claim["claim_key"], @@ -351,7 +386,14 @@ def resolve_model_output(selection: dict[str, Any], fragments: dict[str, str]) - "reason": duplicate["reason"], } ) - selected_references.append({"kind": "duplicate", "reference": source_quote_ref}) + selected_references.append( + selected( + "duplicate", + source_quote_ref, + claim_id=duplicate["claim_id"], + quote=duplicates[-1]["source_quote"], + ) + ) return { "schema": RESOLVED_OUTPUT_SCHEMA, @@ -362,6 +404,37 @@ def resolve_model_output(selection: dict[str, Any], fragments: dict[str, str]) - } +def validate_bundle_source_locations(bundle: dict[str, Any], selected_references: list[dict[str, Any]]) -> None: + """Ensure compiler quote offsets match the exact selected fragment occurrence.""" + available = list(bundle.get("quote_bindings") or []) + for selected in selected_references: + if selected.get("kind") not in {"claim", "evidence"}: + continue + match_index = next( + ( + index + for index, binding in enumerate(available) + if binding.get("kind") == selected.get("kind") + and binding.get("claim_key") == selected.get("claim_key") + and binding.get("quote") == selected.get("quote") + and ( + selected.get("kind") != "evidence" or binding.get("evidence_role") == selected.get("evidence_role") + ) + ), + None, + ) + if match_index is None: + raise PreparationError("compiler dropped a selected claim/evidence source binding") + binding = available.pop(match_index) + if binding.get("first_start") != selected.get("char_start") or binding.get("first_end") != selected.get( + "char_end" + ): + raise PreparationError( + f"selected {selected['reference']} is an ambiguous repeated quote; " + "the current compiler cannot preserve that exact occurrence" + ) + + def build_manifest( *, args: argparse.Namespace, @@ -427,7 +500,7 @@ def prepare(args: argparse.Namespace) -> dict[str, Any]: artifact_bytes = _read_nonempty(args.artifact, "artifact") text_bytes = extract_utf8_text(args.artifact, args.text) text = text_bytes.decode("utf-8", errors="strict") - fragments = build_source_fragments(text) + fragments, fragment_locations = build_source_fragment_index(text) context = _load_context(args.kb_context) requested_output_dir = args.output_dir.expanduser() @@ -464,7 +537,7 @@ def prepare(args: argparse.Namespace) -> dict[str, Any]: attempts.append({"attempt": attempt, "response_sha256": sha256_bytes(raw.encode("utf-8")), "usage": usage}) try: selection = parse_model_output(raw) - extraction = resolve_model_output(selection, fragments) + extraction = resolve_model_output(selection, fragments, fragment_locations) compiler._validate_dedupe( { "database_search_query": context["database_search_query"], @@ -494,6 +567,7 @@ def prepare(args: argparse.Namespace) -> dict[str, Any]: with os.fdopen(fd, "wb") as handle: handle.write(_json_bytes(manifest)) bundle = compiler.compile_source_packet(args.artifact, temp_text, temp_manifest) + validate_bundle_source_locations(bundle, extraction["selected_source_references"]) finally: if temp_text.exists(): temp_text.unlink() @@ -518,6 +592,7 @@ def prepare(args: argparse.Namespace) -> dict[str, Any]: raise PreparationError("prepared extraction has no manifest") _write_private(manifest_path, _json_bytes(manifest)) bundle = compiler.compile_source_packet(args.artifact, text_path, manifest_path) + validate_bundle_source_locations(bundle, extraction["selected_source_references"]) receipt = { "schema": RECEIPT_SCHEMA, @@ -545,10 +620,20 @@ def prepare(args: argparse.Namespace) -> dict[str, Any]: "model": args.model, "attempts": attempts, "claim_count": len(extraction["claims"]), + "evidence_count": sum(len(claim["evidence"]) for claim in extraction["claims"]), + "duplicate_judgment_count": len(extraction["duplicates"]), "selectable_source_line_count": len(fragments), "selected_source_references": extraction["selected_source_references"], "notes": extraction["extraction_notes"], }, + "replay": { + "artifact_sha256": artifact_sha256, + "extracted_text_sha256": text_sha256, + "kb_context_sha256": sha256_bytes(_json_bytes(context)), + "fragment_index_sha256": sha256_bytes(_json_bytes(fragment_locations)), + "extractor": {"name": f"openrouter:{args.model}", "version": EXTRACTOR_VERSION}, + "exact_selected_locations_validated": bundle is not None, + }, "outputs": { "output_dir": str(output_dir), "extracted_text": str(text_path), diff --git a/scripts/run_leo_local_ingestion_proposal_canary.py b/scripts/run_leo_local_ingestion_proposal_canary.py index 89270dc..3cb9243 100644 --- a/scripts/run_leo_local_ingestion_proposal_canary.py +++ b/scripts/run_leo_local_ingestion_proposal_canary.py @@ -1,11 +1,20 @@ #!/usr/bin/env python3 -"""Run a document-ingestion-to-staged-proposal canary in disposable local Postgres.""" +"""Ingest source-only fixtures into review staging in disposable Postgres. + +The canary parses a raw document or repo-native Telegram JSONL transcript, +replays a versioned deterministic extraction sidecar, persists exact source +locations, normalizes the candidates, and stages one ``pending_review`` +proposal. Representative canonical ``public.*`` rows are fingerprinted before +and after. No review, apply, network access, production target, or durable +Docker volume is used. +""" from __future__ import annotations import argparse import hashlib import json +import re import shutil import subprocess import sys @@ -21,89 +30,497 @@ sys.path.insert(0, str(HERE)) import apply_proposal as ap # noqa: E402 import stage_normalized_proposal as normalized_stage # noqa: E402 -SCHEMA = "livingip.workingLeoLocalIngestionProposalCanary.v1" -FIXTURE_SCHEMA = "livingip.workingLeoDocumentIngestionFixture.v1" -DEFAULT_FIXTURE = REPO_ROOT / "fixtures" / "working-leo" / "document-ingestion-v1.json" +SCHEMA = "livingip.workingLeoLocalIngestionProposalCanary.v2" +SUITE_SCHEMA = "livingip.workingLeoLocalIngestionProposalSuite.v1" +FIXTURE_SCHEMA = "livingip.workingLeoSourceIngestionScenario.v2" +DEFAULT_DOCUMENT_FIXTURE = REPO_ROOT / "fixtures" / "working-leo" / "document-ingestion-v2.scenario.json" +DEFAULT_TELEGRAM_FIXTURE = REPO_ROOT / "fixtures" / "working-leo" / "telegram-transcript-ingestion-v1.scenario.json" +DEFAULT_FIXTURE = DEFAULT_DOCUMENT_FIXTURE +DEFAULT_FIXTURES = (DEFAULT_DOCUMENT_FIXTURE, DEFAULT_TELEGRAM_FIXTURE) CONTAINER_LABEL = "livingip.canary=working-leo-local-ingestion" +CANONICAL_TABLES = ("claims", "sources", "claim_evidence", "claim_edges") +SEED_CLAIM_A = "00000000-0000-4000-8000-000000000101" +SEED_CLAIM_B = "00000000-0000-4000-8000-000000000102" +SEED_SOURCE = "00000000-0000-4000-8000-000000000201" class CanaryError(RuntimeError): - pass + """Raised when a fixture or disposable runtime fails closed.""" def sha256_bytes(value: bytes) -> str: return hashlib.sha256(value).hexdigest() +def canonical_json_bytes(value: Any) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode() + + def canonical_sha256(value: Any) -> str: - return sha256_bytes(json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()) + return sha256_bytes(canonical_json_bytes(value)) -def stable_uuid(artifact_sha256: str, label: str) -> str: - return str(uuid.uuid5(uuid.NAMESPACE_URL, f"livingip:working-leo-ingestion:{artifact_sha256}:{label}")) +def canonical_snapshot_complete(snapshot: dict[str, Any]) -> bool: + return set(snapshot) == set(CANONICAL_TABLES) and all( + isinstance(snapshot.get(table), list) and bool(snapshot[table]) for table in CANONICAL_TABLES + ) + + +def stable_uuid(seed: str, label: str) -> str: + return str(uuid.uuid5(uuid.NAMESPACE_URL, f"livingip:working-leo-ingestion:{seed}:{label}")) + + +def _require_object(value: Any, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise CanaryError(f"{label} must be an object") + return value + + +def _require_list(value: Any, label: str) -> list[Any]: + if not isinstance(value, list): + raise CanaryError(f"{label} must be a list") + return value + + +def _require_text(value: Any, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise CanaryError(f"{label} must be a non-empty string") + return value.strip() + + +def _safe_source_key(value: str) -> str: + normalized = re.sub(r"[^a-z0-9._:-]+", "_", value.lower()).strip("_") + if not normalized: + raise CanaryError(f"could not derive a stable source key from {value!r}") + return normalized[:128] + + +def _load_plain_text_segments(raw: bytes, source: dict[str, Any]) -> list[dict[str, Any]]: + try: + text = raw.decode("utf-8", errors="strict") + except UnicodeDecodeError as exc: + raise CanaryError(f"plain_text artifact must be strict UTF-8: {exc}") from exc + locator = _require_text(source.get("locator"), "source.locator") + segments: list[dict[str, Any]] = [] + cursor = 0 + for part in re.split(r"\n[ \t]*\n", text): + part_start = text.find(part, cursor) + cursor = part_start + len(part) + body = part.strip() + if not body: + continue + start = part_start + part.find(body) + index = len(segments) + 1 + canonical_record = {"paragraph": index, "text": body} + segments.append( + { + "id": f"paragraph-{index}", + "body": body, + "locator": f"{locator}#paragraph-{index}", + "json_pointer": None, + "content_sha256": canonical_sha256(canonical_record), + "text_sha256": sha256_bytes(body.encode()), + "metadata": {"paragraph": index, "char_start": start, "char_end": start + len(body)}, + } + ) + if not segments: + raise CanaryError("plain_text artifact contains no non-empty paragraphs") + return segments + + +def _load_telegram_jsonl_segments(raw: bytes) -> list[dict[str, Any]]: + try: + text = raw.decode("utf-8", errors="strict") + except UnicodeDecodeError as exc: + raise CanaryError(f"telegram_jsonl artifact must be strict UTF-8: {exc}") from exc + segments: list[dict[str, Any]] = [] + seen: set[tuple[int, int]] = set() + for line_number, line in enumerate(text.splitlines(), start=1): + if not line.strip(): + continue + try: + record = json.loads(line) + except json.JSONDecodeError as exc: + raise CanaryError(f"telegram JSONL line {line_number} is invalid JSON: {exc}") from exc + record = _require_object(record, f"telegram JSONL line {line_number}") + chat_id = record.get("chat_id") + message_id = record.get("message_id") + message = record.get("message") + if isinstance(chat_id, bool) or not isinstance(chat_id, int): + raise CanaryError(f"telegram JSONL line {line_number}.chat_id must be an integer") + if isinstance(message_id, bool) or not isinstance(message_id, int): + raise CanaryError(f"telegram JSONL line {line_number}.message_id must be an integer") + if not isinstance(message, str) or not message.strip(): + raise CanaryError(f"telegram JSONL line {line_number}.message must be non-empty") + key = (chat_id, message_id) + if key in seen: + raise CanaryError(f"telegram JSONL repeats chat/message identity {key}") + seen.add(key) + body = message.strip() + segments.append( + { + "id": f"message-{chat_id}-{message_id}", + "body": body, + "locator": f"telegram://chat/{chat_id}/message/{message_id}", + "json_pointer": f"jsonl://line/{line_number}/message", + "content_sha256": canonical_sha256(record), + "text_sha256": sha256_bytes(body.encode()), + "metadata": { + "line_number": line_number, + "ts": record.get("ts"), + "chat_id": chat_id, + "chat_title": record.get("chat_title"), + "message_id": message_id, + "type": record.get("type"), + "username": record.get("username"), + "display_name": record.get("display_name"), + "user_id": record.get("user_id"), + "reply_to": record.get("reply_to"), + "synthetic": record.get("synthetic") is True, + }, + } + ) + if not segments: + raise CanaryError("telegram_jsonl artifact contains no messages") + return segments + + +def _load_segments(raw: bytes, artifact_format: str, source: dict[str, Any]) -> list[dict[str, Any]]: + if artifact_format == "plain_text": + return _load_plain_text_segments(raw, source) + if artifact_format == "telegram_jsonl": + return _load_telegram_jsonl_segments(raw) + raise CanaryError("artifact.format must be plain_text or telegram_jsonl") def load_fixture(path: Path) -> tuple[dict[str, Any], dict[str, Any]]: - raw = path.read_bytes() + scenario_path = path.resolve() try: - fixture = json.loads(raw) - except json.JSONDecodeError as exc: - raise CanaryError(f"fixture is not valid JSON: {exc}") from exc - if not isinstance(fixture, dict) or fixture.get("schema") != FIXTURE_SCHEMA: - raise CanaryError(f"fixture schema must be {FIXTURE_SCHEMA}") - source = fixture.get("source") - extraction = fixture.get("extraction") - claim = extraction.get("claim") if isinstance(extraction, dict) else None - evidence = extraction.get("evidence") if isinstance(extraction, dict) else None - if not all(isinstance(item, dict) for item in (source, claim, evidence)): - raise CanaryError("fixture requires source, extraction.claim, and extraction.evidence objects") - required_source = ("identity", "source_key", "source_type", "title", "locator", "body", "metadata") - required_claim = ("claim_key", "type", "confidence", "text", "body", "metadata") - required_evidence = ("role", "body", "metadata") - for label, value, required in ( - ("source", source, required_source), - ("claim", claim, required_claim), - ("evidence", evidence, required_evidence), - ): - missing = [key for key in required if value.get(key) in (None, "")] - if missing: - raise CanaryError(f"fixture {label} is missing: {', '.join(missing)}") - if not isinstance(value.get("metadata"), dict): - raise CanaryError(f"fixture {label}.metadata must be an object") - if claim["body"] not in source["body"]: - raise CanaryError("extracted claim body must be an exact substring of source.body") - if evidence["body"] not in source["body"]: - raise CanaryError("extracted evidence body must be an exact substring of source.body") + scenario_raw = scenario_path.read_bytes() + scenario = json.loads(scenario_raw) + except (OSError, json.JSONDecodeError) as exc: + raise CanaryError(f"scenario is not readable JSON: {exc}") from exc + if not isinstance(scenario, dict) or scenario.get("schema") != FIXTURE_SCHEMA: + raise CanaryError(f"scenario schema must be {FIXTURE_SCHEMA}") + artifact = _require_object(scenario.get("artifact"), "artifact") + source = _require_object(scenario.get("source"), "source") + extractor = _require_object(scenario.get("extractor"), "extractor") + extraction = _require_object(scenario.get("extraction"), "extraction") + artifact_rel = Path(_require_text(artifact.get("path"), "artifact.path")) + if artifact_rel.is_absolute(): + raise CanaryError("artifact.path must be relative to the scenario") + artifact_path = (scenario_path.parent / artifact_rel).resolve() + try: + artifact_path.relative_to(scenario_path.parent.resolve()) + except ValueError as exc: + raise CanaryError("artifact.path must remain inside the scenario directory") from exc + try: + raw = artifact_path.read_bytes() + except OSError as exc: + raise CanaryError(f"source artifact is unreadable: {exc}") from exc + if not raw: + raise CanaryError("source artifact must not be empty") + artifact_format = _require_text(artifact.get("format"), "artifact.format") + extractor_name = _require_text(extractor.get("name"), "extractor.name") + extractor_version = _require_text(extractor.get("version"), "extractor.version") + required_source = ("identity", "source_key", "source_type", "title", "locator", "metadata") + missing_source = [key for key in required_source if source.get(key) in (None, "")] + if missing_source: + raise CanaryError(f"source is missing: {', '.join(missing_source)}") + if source["source_type"] not in ap.SOURCE_TYPES: + raise CanaryError(f"source.source_type must be one of {sorted(ap.SOURCE_TYPES)}") + if not isinstance(source.get("metadata"), dict): + raise CanaryError("source.metadata must be an object") + segments = _load_segments(raw, artifact_format, source) + segment_by_id = {segment["id"]: segment for segment in segments} + + claims = _require_list(extraction.get("claims"), "extraction.claims") + duplicates = _require_list(extraction.get("duplicates"), "extraction.duplicates") + conflicts = _require_list(extraction.get("conflicts"), "extraction.conflicts") + if not claims: + raise CanaryError("extraction.claims must contain at least one review candidate") + claim_keys: set[str] = set() + normalized_claims: list[dict[str, Any]] = [] + evidence_count = 0 + for claim_index, raw_claim in enumerate(claims): + claim = _require_object(raw_claim, f"extraction.claims[{claim_index}]") + claim_key = _require_text(claim.get("claim_key"), f"extraction.claims[{claim_index}].claim_key") + if claim_key in claim_keys: + raise CanaryError(f"duplicate claim_key {claim_key}") + claim_keys.add(claim_key) + claim_type = _require_text(claim.get("type"), f"extraction.claims[{claim_index}].type") + if claim_type not in ap.CLAIM_TYPES: + raise CanaryError(f"claim {claim_key} has unsupported type {claim_type}") + confidence = claim.get("confidence") + if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1: + raise CanaryError(f"claim {claim_key} confidence must be between 0 and 1") + evidence_rows = _require_list(claim.get("evidence"), f"claim {claim_key}.evidence") + if not evidence_rows: + raise CanaryError(f"claim {claim_key} requires evidence") + normalized_evidence: list[dict[str, Any]] = [] + for evidence_index, raw_evidence in enumerate(evidence_rows): + evidence = _require_object(raw_evidence, f"claim {claim_key}.evidence[{evidence_index}]") + segment_id = _require_text(evidence.get("segment_id"), f"claim {claim_key} evidence segment_id") + segment = segment_by_id.get(segment_id) + if segment is None: + raise CanaryError(f"claim {claim_key} evidence references unknown segment {segment_id}") + excerpt = _require_text(evidence.get("excerpt"), f"claim {claim_key} evidence excerpt") + if excerpt not in segment["body"]: + raise CanaryError(f"claim {claim_key} evidence excerpt is not an exact segment substring") + role = _require_text(evidence.get("role"), f"claim {claim_key} evidence role") + if role not in ap.EVIDENCE_ROLES: + raise CanaryError(f"claim {claim_key} evidence has unsupported role {role}") + normalized_evidence.append( + { + **evidence, + "segment_id": segment_id, + "excerpt": excerpt, + "role": role, + "source_locator": segment["locator"], + "source_json_pointer": segment["json_pointer"], + "source_content_sha256": segment["content_sha256"], + "source_text_sha256": segment["text_sha256"], + } + ) + evidence_count += 1 + body = _require_text(claim.get("body"), f"claim {claim_key}.body") + if not any(body in segment_by_id[row["segment_id"]]["body"] for row in normalized_evidence): + raise CanaryError(f"claim {claim_key} body must be an exact substring of its evidence segments") + edges = _require_list(claim.get("edges", []), f"claim {claim_key}.edges") + normalized_claims.append( + { + **claim, + "claim_key": claim_key, + "type": claim_type, + "confidence": confidence, + "text": _require_text(claim.get("text"), f"claim {claim_key}.text"), + "body": body, + "metadata": _require_object(claim.get("metadata", {}), f"claim {claim_key}.metadata"), + "evidence": normalized_evidence, + "edges": edges, + } + ) + + normalized_duplicates: list[dict[str, Any]] = [] + for duplicate_index, raw_duplicate in enumerate(duplicates): + duplicate = _require_object(raw_duplicate, f"extraction.duplicates[{duplicate_index}]") + segment_id = _require_text(duplicate.get("segment_id"), f"duplicate {duplicate_index}.segment_id") + target = _require_text( + duplicate.get("duplicate_of_claim_key"), f"duplicate {duplicate_index}.duplicate_of_claim_key" + ) + if target not in claim_keys: + raise CanaryError(f"duplicate {duplicate_index} references unknown claim {target}") + segment = segment_by_id.get(segment_id) + if segment is None: + raise CanaryError(f"duplicate {duplicate_index} references unknown segment {segment_id}") + excerpt = _require_text(duplicate.get("excerpt"), f"duplicate {duplicate_index}.excerpt") + if excerpt not in segment["body"]: + raise CanaryError(f"duplicate {duplicate_index} excerpt is not an exact segment substring") + normalized_duplicates.append( + { + **duplicate, + "segment_id": segment_id, + "duplicate_of_claim_key": target, + "excerpt": excerpt, + "source_locator": segment["locator"], + "source_json_pointer": segment["json_pointer"], + "source_content_sha256": segment["content_sha256"], + "source_text_sha256": segment["text_sha256"], + } + ) + + normalized_conflicts: list[dict[str, Any]] = [] + for conflict_index, raw_conflict in enumerate(conflicts): + conflict = _require_object(raw_conflict, f"extraction.conflicts[{conflict_index}]") + from_key = _require_text(conflict.get("from_claim_key"), f"conflict {conflict_index}.from_claim_key") + to_key = _require_text(conflict.get("to_claim_key"), f"conflict {conflict_index}.to_claim_key") + relationship = _require_text(conflict.get("relationship"), f"conflict {conflict_index}.relationship") + if from_key not in claim_keys or to_key not in claim_keys: + raise CanaryError(f"conflict {conflict_index} must reference produced claim keys") + if relationship != "contradicts": + raise CanaryError("fixture conflicts must use relationship=contradicts") + matching_edge = any( + claim["claim_key"] == from_key + and any(edge.get("edge_type") == relationship and edge.get("target") == to_key for edge in claim["edges"]) + for claim in normalized_claims + ) + if not matching_edge: + raise CanaryError(f"conflict {from_key}->{to_key} has no matching candidate edge") + normalized_conflicts.append({**conflict, "from_claim_key": from_key, "to_claim_key": to_key}) + artifact_sha256 = sha256_bytes(raw) - source_content_sha256 = sha256_bytes(source["body"].encode()) - return fixture, { - "path": str(path.resolve()), + source_content_sha256 = canonical_sha256( + [ + { + "id": segment["id"], + "locator": segment["locator"], + "content_sha256": segment["content_sha256"], + } + for segment in segments + ] + ) + extraction_spec_sha256 = canonical_sha256(extraction) + replay_identity_sha256 = canonical_sha256( + { + "artifact_sha256": artifact_sha256, + "extractor": {"name": extractor_name, "version": extractor_version}, + "extraction_spec_sha256": extraction_spec_sha256, + } + ) + fixture = { + **scenario, + "artifact": {**artifact, "resolved_path": str(artifact_path)}, + "extractor": {"name": extractor_name, "version": extractor_version}, + "segments": segments, + "extraction": { + "claims": normalized_claims, + "duplicates": normalized_duplicates, + "conflicts": normalized_conflicts, + }, + } + receipt = { + "scenario_path": str(scenario_path), + "scenario_sha256": sha256_bytes(scenario_raw), + "artifact_path": str(artifact_path), + "artifact_format": artifact_format, "artifact_sha256": artifact_sha256, "source_content_sha256": source_content_sha256, "source_identity": source["identity"], + "source_locator": source["locator"], + "extractor": {"name": extractor_name, "version": extractor_version}, + "extraction_spec_sha256": extraction_spec_sha256, + "replay_identity_sha256": replay_identity_sha256, + "counts": { + "source_segments": len(segments), + "claim_candidates": len(normalized_claims), + "evidence_candidates": evidence_count, + "duplicate_judgments": len(normalized_duplicates), + "conflict_relationships": len(normalized_conflicts), + }, } + return fixture, receipt -def build_row_ids(artifact_sha256: str) -> dict[str, str]: +def build_row_ids(input_receipt: dict[str, Any], fixture: dict[str, Any]) -> dict[str, Any]: + replay_seed = input_receipt["replay_identity_sha256"] + claim_ids = { + claim["claim_key"]: stable_uuid(replay_seed, f"claim-extraction:{claim['claim_key']}") + for claim in fixture["extraction"]["claims"] + } + evidence_ids: dict[str, str] = {} + for claim in fixture["extraction"]["claims"]: + for index, _evidence in enumerate(claim["evidence"]): + key = f"{claim['claim_key']}:{index}" + evidence_ids[key] = stable_uuid(replay_seed, f"evidence-extraction:{key}") + duplicate_ids = { + str(index): stable_uuid(replay_seed, f"duplicate-assessment:{index}") + for index, _duplicate in enumerate(fixture["extraction"]["duplicates"]) + } + conflict_ids = { + str(index): stable_uuid(replay_seed, f"conflict-assessment:{index}") + for index, _conflict in enumerate(fixture["extraction"]["conflicts"]) + } return { - "source_capture_id": stable_uuid(artifact_sha256, "source-capture"), - "claim_extraction_id": stable_uuid(artifact_sha256, "claim-extraction"), - "evidence_extraction_id": stable_uuid(artifact_sha256, "evidence-extraction"), - "parent_proposal_id": stable_uuid(artifact_sha256, "rich-parent-proposal"), + "source_capture_id": stable_uuid(input_receipt["artifact_sha256"], "source-capture"), + "claim_extraction_ids": claim_ids, + "evidence_extraction_ids": evidence_ids, + "duplicate_assessment_ids": duplicate_ids, + "conflict_assessment_ids": conflict_ids, + "parent_proposal_id": stable_uuid(replay_seed, "rich-parent-proposal"), } +def _segment_source_key(source_key: str, segment_id: str) -> str: + return _safe_source_key(f"{source_key}__{segment_id}") + + def build_parent_packet( - fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, str] + fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, Any] ) -> dict[str, Any]: source = fixture["source"] - claim = fixture["extraction"]["claim"] - evidence = fixture["extraction"]["evidence"] - parent_source_ref = ( - f"local-ingestion:{input_receipt['artifact_sha256']}:" - f"source={row_ids['source_capture_id']}:claim={row_ids['claim_extraction_id']}:" - f"evidence={row_ids['evidence_extraction_id']}" - ) + claims = fixture["extraction"]["claims"] + segment_by_id = {segment["id"]: segment for segment in fixture["segments"]} + referenced_segment_ids: list[str] = [] + for claim in claims: + for evidence in claim["evidence"]: + if evidence["segment_id"] not in referenced_segment_ids: + referenced_segment_ids.append(evidence["segment_id"]) + for duplicate in fixture["extraction"]["duplicates"]: + if duplicate["segment_id"] not in referenced_segment_ids: + referenced_segment_ids.append(duplicate["segment_id"]) + + source_candidates = [] + for segment_id in referenced_segment_ids: + segment = segment_by_id[segment_id] + segment_source_key = _segment_source_key(source["source_key"], segment_id) + provenance = { + "artifact_sha256": input_receipt["artifact_sha256"], + "segment_id": segment_id, + "locator": segment["locator"], + "json_pointer": segment["json_pointer"], + "content_sha256": segment["content_sha256"], + "text_sha256": segment["text_sha256"], + } + source_candidates.append( + { + "source_key": segment_source_key, + "source_type": "dm" if input_receipt["artifact_format"] == "telegram_jsonl" else source["source_type"], + "title": f"{source['title']} - {segment_id}", + "storage_path": segment["locator"], + "url": None, + "source_quality": json.dumps(provenance, sort_keys=True, separators=(",", ":")), + "usage_limits": "Review-only exact excerpt; no canonical apply is authorized.", + "key_excerpt": segment["body"], + "content_sha256": segment["content_sha256"], + } + ) + + claim_candidates = [] + for claim in claims: + claim_candidates.append( + { + "claim_key": claim["claim_key"], + "type": claim["type"], + "confidence": claim["confidence"], + "text": claim["text"], + "body": claim["body"], + "evidence_tier": "source_artifact_exact_location", + "needs_research": claim["metadata"].get("needs_research", False), + "evidence": [ + { + "source_key": _segment_source_key(source["source_key"], evidence["segment_id"]), + "role": evidence["role"], + } + for evidence in claim["evidence"] + ], + "edges": claim["edges"], + } + ) + + ingestion_manifest = { + "scenario_schema": fixture["schema"], + "scenario_sha256": input_receipt["scenario_sha256"], + "artifact_format": input_receipt["artifact_format"], + "artifact_sha256": input_receipt["artifact_sha256"], + "source_content_sha256": input_receipt["source_content_sha256"], + "source_identity": input_receipt["source_identity"], + "source_locator": input_receipt["source_locator"], + "extractor": input_receipt["extractor"], + "extraction_spec_sha256": input_receipt["extraction_spec_sha256"], + "replay_identity_sha256": input_receipt["replay_identity_sha256"], + "segments": [ + { + "id": segment["id"], + "locator": segment["locator"], + "json_pointer": segment["json_pointer"], + "content_sha256": segment["content_sha256"], + "text_sha256": segment["text_sha256"], + } + for segment in fixture["segments"] + ], + "row_ids": row_ids, + "candidate_counts": input_receipt["counts"], + } return { "id": row_ids["parent_proposal_id"], "proposal_type": "attach_evidence", @@ -111,47 +528,55 @@ def build_parent_packet( "proposed_by_handle": "leo", "proposed_by_agent_id": None, "channel": "local_ingestion_canary", - "source_ref": parent_source_ref, - "rationale": "Stage one hash-bound claim/evidence extraction from a captured local document fixture.", + "source_ref": ( + f"local-ingestion:{input_receipt['artifact_sha256']}:" + f"replay={input_receipt['replay_identity_sha256']}:capture={row_ids['source_capture_id']}" + ), + "rationale": "Stage source-linked candidate claims for review without canonical apply.", "payload": { - "claim_candidates": [ - { - "claim_key": claim["claim_key"], - "type": claim["type"], - "confidence": claim["confidence"], - "text": claim["text"], - "body": claim["body"], - "evidence_tier": "isolated_fixture", - "needs_research": claim["metadata"].get("needs_research", False), - "evidence": [{"source_key": source["source_key"], "role": evidence["role"]}], - "edges": [], - } - ], - "source_candidates": [ - { - "source_key": source["source_key"], - "source_type": source["source_type"], - "title": source["title"], - "storage_path": input_receipt["path"], - "url": None, - "source_quality": "local realistic ingestion fixture", - "key_excerpt": evidence["body"], - "content_sha256": input_receipt["source_content_sha256"], - } - ], + "ingestion_manifest": ingestion_manifest, + "claim_candidates": claim_candidates, + "source_candidates": source_candidates, "dedupe_conflict_assessment": { - "database_search_query": claim["claim_key"].replace("_", " "), - "potential_existing_claim_ids": [], - "conflicts": [], + "database_search_query": " ".join(claim["claim_key"].replace("_", " ") for claim in claims), + "duplicates": fixture["extraction"]["duplicates"], + "conflicts": fixture["extraction"]["conflicts"], + "review_required": True, }, }, } def build_schema_sql() -> str: - return r"""\set ON_ERROR_STOP on + return rf"""\set ON_ERROR_STOP on create schema kb_canary; create schema kb_stage; +create table public.claims ( + id uuid primary key, type text not null, text text not null, status text not null, + confidence double precision, tags text[] not null default '{{}}' +); +create table public.sources ( + id uuid primary key, source_type text not null, url text, storage_path text, + excerpt text, hash text not null +); +create table public.claim_evidence ( + claim_id uuid not null, source_id uuid not null, role text not null, weight double precision, + primary key (claim_id, source_id, role) +); +create table public.claim_edges ( + from_claim uuid not null, to_claim uuid not null, edge_type text not null, weight double precision, + primary key (from_claim, to_claim, edge_type) +); +insert into public.claims(id, type, text, status, confidence, tags) values + ('{SEED_CLAIM_A}', 'structural', 'A staged proposal is non-canonical until guarded apply succeeds.', 'open', 0.99, array['seed']), + ('{SEED_CLAIM_B}', 'meta', 'Review and canonical application are separate state transitions.', 'open', 0.98, array['seed']); +insert into public.sources(id, source_type, storage_path, excerpt, hash) values + ('{SEED_SOURCE}', 'observation', 'fixture://canonical/seed', 'Representative canonical seed.', + '{"a" * 64}'); +insert into public.claim_evidence(claim_id, source_id, role, weight) values + ('{SEED_CLAIM_A}', '{SEED_SOURCE}', 'grounds', 1.0); +insert into public.claim_edges(from_claim, to_claim, edge_type, weight) values + ('{SEED_CLAIM_A}', '{SEED_CLAIM_B}', 'supports', 1.0); create table kb_canary.source_captures ( id uuid primary key, source_identity text not null unique, @@ -159,15 +584,15 @@ create table kb_canary.source_captures ( title text not null, locator text not null, artifact_path text not null, - artifact_sha256 text not null check (artifact_sha256 ~ '^[0-9a-f]{64}$'), - content_sha256 text not null check (content_sha256 ~ '^[0-9a-f]{64}$'), - body text not null, + artifact_sha256 text not null check (artifact_sha256 ~ '^[0-9a-f]{{64}}$'), + content_sha256 text not null check (content_sha256 ~ '^[0-9a-f]{{64}}$'), + replay_identity_sha256 text not null check (replay_identity_sha256 ~ '^[0-9a-f]{{64}}$'), metadata jsonb not null ); create table kb_canary.claim_extractions ( id uuid primary key, source_capture_id uuid not null references kb_canary.source_captures(id), - claim_key text not null, + claim_key text not null unique, body text not null, metadata jsonb not null ); @@ -175,8 +600,32 @@ create table kb_canary.evidence_extractions ( id uuid primary key, claim_extraction_id uuid not null references kb_canary.claim_extractions(id), source_capture_id uuid not null references kb_canary.source_captures(id), + source_segment_id text not null, + source_locator text not null, + source_json_pointer text, role text not null, body text not null, + body_sha256 text not null, + source_content_sha256 text not null, + metadata jsonb not null +); +create table kb_canary.duplicate_assessments ( + id uuid primary key, + source_capture_id uuid not null references kb_canary.source_captures(id), + source_segment_id text not null, + source_locator text not null, + duplicate_of_claim_key text not null, + excerpt text not null, + excerpt_sha256 text not null, + reason text not null, + metadata jsonb not null +); +create table kb_canary.conflict_assessments ( + id uuid primary key, + source_capture_id uuid not null references kb_canary.source_captures(id), + from_claim_key text not null, + to_claim_key text not null, + relationship text not null, metadata jsonb not null ); create table kb_stage.kb_proposals ( @@ -200,59 +649,111 @@ create table kb_stage.kb_proposals ( """ -def build_capture_sql(fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, str]) -> str: - source = fixture["source"] - claim = fixture["extraction"]["claim"] - evidence = fixture["extraction"]["evidence"] - q = ap.sql_literal - claim_metadata = { - **claim["metadata"], - "claim_type": claim["type"], - "confidence": claim["confidence"], - "text": claim["text"], - } - return rf"""\set ON_ERROR_STOP on -begin; -insert into kb_canary.source_captures - (id, source_identity, source_type, title, locator, artifact_path, - artifact_sha256, content_sha256, body, metadata) -values - ({q(row_ids["source_capture_id"])}::uuid, {q(source["identity"])}, {q(source["source_type"])}, - {q(source["title"])}, {q(source["locator"])}, {q(input_receipt["path"])}, - {q(input_receipt["artifact_sha256"])}, {q(input_receipt["source_content_sha256"])}, - {q(source["body"])}, {q(json.dumps(source["metadata"], sort_keys=True))}::jsonb); -insert into kb_canary.claim_extractions - (id, source_capture_id, claim_key, body, metadata) -values - ({q(row_ids["claim_extraction_id"])}::uuid, {q(row_ids["source_capture_id"])}::uuid, - {q(claim["claim_key"])}, {q(claim["body"])}, {q(json.dumps(claim_metadata, sort_keys=True))}::jsonb); -insert into kb_canary.evidence_extractions - (id, claim_extraction_id, source_capture_id, role, body, metadata) -values - ({q(row_ids["evidence_extraction_id"])}::uuid, {q(row_ids["claim_extraction_id"])}::uuid, - {q(row_ids["source_capture_id"])}::uuid, {q(evidence["role"])}, {q(evidence["body"])}, - {q(json.dumps(evidence["metadata"], sort_keys=True))}::jsonb); -commit; +def build_canonical_snapshot_sql() -> str: + return r"""\set ON_ERROR_STOP on +begin transaction read only; +select jsonb_build_object( + 'claims', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from public.claims t), '[]'::jsonb), + 'sources', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from public.sources t), '[]'::jsonb), + 'claim_evidence', coalesce((select jsonb_agg(to_jsonb(t) order by t.claim_id, t.source_id, t.role) + from public.claim_evidence t), '[]'::jsonb), + 'claim_edges', coalesce((select jsonb_agg(to_jsonb(t) order by t.from_claim, t.to_claim, t.edge_type) + from public.claim_edges t), '[]'::jsonb) +)::text; +rollback; """ -def build_readback_sql(row_ids: dict[str, str], proposal_id: str) -> str: +def build_capture_sql(fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, Any]) -> str: + source = fixture["source"] + q = ap.sql_literal + statements = [ + r"\set ON_ERROR_STOP on", + "begin;", + "insert into kb_canary.source_captures " + "(id, source_identity, source_type, title, locator, artifact_path, artifact_sha256, content_sha256, " + "replay_identity_sha256, metadata) values " + f"({q(row_ids['source_capture_id'])}::uuid, {q(source['identity'])}, {q(source['source_type'])}, " + f"{q(source['title'])}, {q(source['locator'])}, {q(input_receipt['artifact_path'])}, " + f"{q(input_receipt['artifact_sha256'])}, {q(input_receipt['source_content_sha256'])}, " + f"{q(input_receipt['replay_identity_sha256'])}, " + f"{q(json.dumps({**source['metadata'], 'extractor': input_receipt['extractor']}, sort_keys=True))}::jsonb);", + ] + segment_by_id = {segment["id"]: segment for segment in fixture["segments"]} + for claim in fixture["extraction"]["claims"]: + claim_id = row_ids["claim_extraction_ids"][claim["claim_key"]] + claim_metadata = { + **claim["metadata"], + "claim_type": claim["type"], + "confidence": claim["confidence"], + "text": claim["text"], + "extractor": input_receipt["extractor"], + "replay_identity_sha256": input_receipt["replay_identity_sha256"], + } + statements.append( + "insert into kb_canary.claim_extractions " + "(id, source_capture_id, claim_key, body, metadata) values " + f"({q(claim_id)}::uuid, {q(row_ids['source_capture_id'])}::uuid, {q(claim['claim_key'])}, " + f"{q(claim['body'])}, {q(json.dumps(claim_metadata, sort_keys=True))}::jsonb);" + ) + for index, evidence in enumerate(claim["evidence"]): + evidence_key = f"{claim['claim_key']}:{index}" + segment = segment_by_id[evidence["segment_id"]] + evidence_metadata = { + **_require_object(evidence.get("metadata", {}), f"evidence {evidence_key}.metadata"), + "source_text_sha256": segment["text_sha256"], + } + statements.append( + "insert into kb_canary.evidence_extractions " + "(id, claim_extraction_id, source_capture_id, source_segment_id, source_locator, " + "source_json_pointer, role, body, body_sha256, source_content_sha256, metadata) values " + f"({q(row_ids['evidence_extraction_ids'][evidence_key])}::uuid, {q(claim_id)}::uuid, " + f"{q(row_ids['source_capture_id'])}::uuid, {q(segment['id'])}, {q(segment['locator'])}, " + f"{q(segment['json_pointer'])}, {q(evidence['role'])}, {q(evidence['excerpt'])}, " + f"{q(sha256_bytes(evidence['excerpt'].encode()))}, {q(segment['content_sha256'])}, " + f"{q(json.dumps(evidence_metadata, sort_keys=True))}::jsonb);" + ) + for index, duplicate in enumerate(fixture["extraction"]["duplicates"]): + statements.append( + "insert into kb_canary.duplicate_assessments " + "(id, source_capture_id, source_segment_id, source_locator, duplicate_of_claim_key, excerpt, " + "excerpt_sha256, reason, metadata) values " + f"({q(row_ids['duplicate_assessment_ids'][str(index)])}::uuid, " + f"{q(row_ids['source_capture_id'])}::uuid, {q(duplicate['segment_id'])}, " + f"{q(duplicate['source_locator'])}, {q(duplicate['duplicate_of_claim_key'])}, " + f"{q(duplicate['excerpt'])}, {q(sha256_bytes(duplicate['excerpt'].encode()))}, " + f"{q(duplicate['reason'])}, {q(json.dumps({'json_pointer': duplicate['source_json_pointer'], 'source_content_sha256': duplicate['source_content_sha256'], 'source_text_sha256': duplicate['source_text_sha256']}, sort_keys=True))}::jsonb);" + ) + for index, conflict in enumerate(fixture["extraction"]["conflicts"]): + statements.append( + "insert into kb_canary.conflict_assessments " + "(id, source_capture_id, from_claim_key, to_claim_key, relationship, metadata) values " + f"({q(row_ids['conflict_assessment_ids'][str(index)])}::uuid, " + f"{q(row_ids['source_capture_id'])}::uuid, {q(conflict['from_claim_key'])}, " + f"{q(conflict['to_claim_key'])}, {q(conflict['relationship'])}, " + f"{q(json.dumps({key: value for key, value in conflict.items() if key not in {'from_claim_key', 'to_claim_key', 'relationship'}}, sort_keys=True))}::jsonb);" + ) + statements.append("commit;") + return "\n".join(statements) + "\n" + + +def build_readback_sql(proposal_id: str) -> str: q = ap.sql_literal return rf"""\set ON_ERROR_STOP on begin transaction read only; select jsonb_build_object( - 'source_capture', (select to_jsonb(s) from kb_canary.source_captures s - where id = {q(row_ids["source_capture_id"])}::uuid), - 'claim_extraction', (select to_jsonb(c) from kb_canary.claim_extractions c - where id = {q(row_ids["claim_extraction_id"])}::uuid), - 'evidence_extraction', (select to_jsonb(e) from kb_canary.evidence_extractions e - where id = {q(row_ids["evidence_extraction_id"])}::uuid), - 'staged_proposal', (select to_jsonb(p) from kb_stage.kb_proposals p - where id = {q(proposal_id)}::uuid), + 'source_captures', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from kb_canary.source_captures t), '[]'::jsonb), + 'claim_extractions', coalesce((select jsonb_agg(to_jsonb(t) order by t.claim_key) from kb_canary.claim_extractions t), '[]'::jsonb), + 'evidence_extractions', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from kb_canary.evidence_extractions t), '[]'::jsonb), + 'duplicate_assessments', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from kb_canary.duplicate_assessments t), '[]'::jsonb), + 'conflict_assessments', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from kb_canary.conflict_assessments t), '[]'::jsonb), + 'staged_proposal', (select to_jsonb(p) from kb_stage.kb_proposals p where id = {q(proposal_id)}::uuid), 'counts', jsonb_build_object( 'source_captures', (select count(*) from kb_canary.source_captures), 'claim_extractions', (select count(*) from kb_canary.claim_extractions), 'evidence_extractions', (select count(*) from kb_canary.evidence_extractions), + 'duplicate_assessments', (select count(*) from kb_canary.duplicate_assessments), + 'conflict_assessments', (select count(*) from kb_canary.conflict_assessments), 'staged_proposals', (select count(*) from kb_stage.kb_proposals) ) )::text; @@ -320,11 +821,7 @@ class DockerPostgres: if inspect["Config"]["Labels"].get("livingip.canary") != "working-leo-local-ingestion": raise CanaryError("disposable Postgres is missing its canary label") self.volume_mounts = [ - { - "type": mount.get("Type"), - "name": mount.get("Name"), - "destination": mount.get("Destination"), - } + {"type": mount.get("Type"), "name": mount.get("Name"), "destination": mount.get("Destination")} for mount in inspect.get("Mounts", []) if mount.get("Type") == "volume" ] @@ -386,60 +883,130 @@ def proposal_links(readback: dict[str, Any], input_receipt: dict[str, Any]) -> d "planned_evidence_keys": [ {"claim_id": row["claim_id"], "source_id": row["source_id"], "role": row["role"]} for row in evidence_rows ], - "source_content_hash_matches": any( - row.get("hash") == input_receipt["source_content_sha256"] for row in source_rows + "planned_edge_keys": [ + { + "from_claim": row["from_claim"], + "to_claim": row["to_claim"], + "edge_type": row["edge_type"], + } + for row in apply_payload["edges"] + ], + "planned_counts": { + "claims": len(apply_payload["claims"]), + "sources": len(source_rows), + "evidence": len(evidence_rows), + "edges": len(apply_payload["edges"]), + }, + "input_hash_in_manifest": ( + apply_payload["normalization_manifest"].get("ingestion_manifest", {}).get("artifact_sha256") + == input_receipt["artifact_sha256"] ), - "parent_source_ref_embedded": any( - readback["source_capture"]["artifact_sha256"] in str(row.get("excerpt")) - and readback["source_capture"]["id"] in str(row.get("excerpt")) - and readback["claim_extraction"]["id"] in str(row.get("excerpt")) - and readback["evidence_extraction"]["id"] in str(row.get("excerpt")) - for row in source_rows + "replay_identity_in_manifest": ( + apply_payload["normalization_manifest"].get("ingestion_manifest", {}).get("replay_identity_sha256") + == input_receipt["replay_identity_sha256"] ), } def evaluate_checks( - fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, str], readback: dict[str, Any] + fixture: dict[str, Any], + input_receipt: dict[str, Any], + row_ids: dict[str, Any], + readback: dict[str, Any], + canonical_before: dict[str, Any], + canonical_after: dict[str, Any], ) -> dict[str, bool]: - source = readback.get("source_capture") or {} - claim = readback.get("claim_extraction") or {} - evidence = readback.get("evidence_extraction") or {} + source_rows = readback.get("source_captures") or [] + claim_rows = readback.get("claim_extractions") or [] + evidence_rows = readback.get("evidence_extractions") or [] + duplicate_rows = readback.get("duplicate_assessments") or [] + conflict_rows = readback.get("conflict_assessments") or [] proposal = readback.get("staged_proposal") or {} + counts = input_receipt["counts"] links = proposal_links(readback, input_receipt) + manifest = ((proposal.get("payload") or {}).get("apply_payload") or {}).get("normalization_manifest") or {} + ingestion_manifest = manifest.get("ingestion_manifest") or {} + assessment = manifest.get("dedupe_conflict_assessment") or {} + expected_db_counts = { + "source_captures": 1, + "claim_extractions": counts["claim_candidates"], + "evidence_extractions": counts["evidence_candidates"], + "duplicate_assessments": counts["duplicate_judgments"], + "conflict_assessments": counts["conflict_relationships"], + "staged_proposals": 1, + } + segment_by_id = {segment["id"]: segment for segment in fixture["segments"]} + evidence_locations_exact = all( + row.get("source_locator") == segment_by_id.get(row.get("source_segment_id"), {}).get("locator") + and row.get("source_content_sha256") + == segment_by_id.get(row.get("source_segment_id"), {}).get("content_sha256") + and row.get("body") in segment_by_id.get(row.get("source_segment_id"), {}).get("body", "") + for row in evidence_rows + ) return { - "input_artifact_hash_persisted": source.get("artifact_sha256") == input_receipt["artifact_sha256"], - "source_content_hash_persisted": source.get("content_sha256") == input_receipt["source_content_sha256"], - "source_identity_persisted": source.get("source_identity") == input_receipt["source_identity"], - "claim_links_to_source_capture": claim.get("source_capture_id") == row_ids["source_capture_id"], - "claim_body_and_metadata_persisted": ( - claim.get("body") == fixture["extraction"]["claim"]["body"] - and claim.get("metadata", {}).get("text") == fixture["extraction"]["claim"]["text"] + "source_only_artifact_hash_persisted": ( + len(source_rows) == 1 and source_rows[0].get("artifact_sha256") == input_receipt["artifact_sha256"] ), - "evidence_links_to_claim_and_source": ( - evidence.get("claim_extraction_id") == row_ids["claim_extraction_id"] - and evidence.get("source_capture_id") == row_ids["source_capture_id"] + "replay_identity_persisted": ( + len(source_rows) == 1 + and source_rows[0].get("replay_identity_sha256") == input_receipt["replay_identity_sha256"] ), - "evidence_body_and_metadata_persisted": ( - evidence.get("body") == fixture["extraction"]["evidence"]["body"] - and evidence.get("metadata") == fixture["extraction"]["evidence"]["metadata"] + "extractor_name_and_version_persisted": ( + ingestion_manifest.get("extractor") == input_receipt["extractor"] + and all(row.get("metadata", {}).get("extractor") == input_receipt["extractor"] for row in claim_rows) ), - "one_row_per_ingestion_stage": readback.get("counts") - == {"source_captures": 1, "claim_extractions": 1, "evidence_extractions": 1, "staged_proposals": 1}, + "all_claim_candidates_persisted_once": ( + {row.get("claim_key") for row in claim_rows} + == {claim["claim_key"] for claim in fixture["extraction"]["claims"]} + and len(claim_rows) == counts["claim_candidates"] + ), + "all_evidence_has_exact_source_location": ( + len(evidence_rows) == counts["evidence_candidates"] and evidence_locations_exact + ), + "duplicate_judgments_persisted": ( + len(duplicate_rows) == counts["duplicate_judgments"] + and assessment.get("duplicates") == fixture["extraction"]["duplicates"] + ), + "conflicts_and_relationships_persisted": ( + len(conflict_rows) == counts["conflict_relationships"] + and assessment.get("conflicts") == fixture["extraction"]["conflicts"] + and links["planned_counts"]["edges"] == counts["conflict_relationships"] + ), + "database_candidate_counts_exact": readback.get("counts") == expected_db_counts, "proposal_is_pending_review": proposal.get("status") == "pending_review", - "proposal_has_source_ref": bool(proposal.get("source_ref")), - "proposal_payload_contains_input_hash": links["source_content_hash_matches"], - "proposal_payload_contains_capture_row_linkage": links["parent_source_ref_embedded"], + "proposal_has_replayable_ingestion_manifest": ( + links["input_hash_in_manifest"] + and links["replay_identity_in_manifest"] + and ingestion_manifest.get("segments") + == [ + { + "id": segment["id"], + "locator": segment["locator"], + "json_pointer": segment["json_pointer"], + "content_sha256": segment["content_sha256"], + "text_sha256": segment["text_sha256"], + } + for segment in fixture["segments"] + ] + ), "no_review_or_apply_metadata": all( proposal.get(key) is None for key in ("reviewed_by_handle", "reviewed_at", "applied_by_handle", "applied_at") ), + "canonical_snapshot_nonempty": ( + canonical_snapshot_complete(canonical_before) and canonical_snapshot_complete(canonical_after) + ), + "canonical_fingerprint_unchanged": ( + canonical_snapshot_complete(canonical_before) + and canonical_snapshot_complete(canonical_after) + and canonical_sha256(canonical_before) == canonical_sha256(canonical_after) + ), } def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any]: fixture, input_receipt = load_fixture(fixture_path) - row_ids = build_row_ids(input_receipt["artifact_sha256"]) + row_ids = build_row_ids(input_receipt, fixture) parent = build_parent_packet(fixture, input_receipt, row_ids) child = normalized_stage.prepare_normalized_child(parent) container_name = f"working-leo-ingest-{uuid.uuid4().hex[:12]}" @@ -447,7 +1014,7 @@ def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any] receipt: dict[str, Any] = { "schema": SCHEMA, "status": "fail", - "required_tier": "isolated_realistic_ingestion_to_proposal", + "required_tier": "T2_runtime_to_review_queue_staging", "input": input_receipt, "row_ids": row_ids, "production_mutation_attempted": False, @@ -456,44 +1023,49 @@ def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any] try: receipt["environment"] = postgres.start() postgres.psql(build_schema_sql()) + canonical_before = postgres.psql_json(build_canonical_snapshot_sql()) postgres.psql(build_capture_sql(fixture, input_receipt, row_ids)) stage_result = postgres.psql_json(normalized_stage.build_stage_sql(child)) - readback = postgres.psql_json(build_readback_sql(row_ids, child["id"])) - checks = evaluate_checks(fixture, input_receipt, row_ids, readback) + readback = postgres.psql_json(build_readback_sql(child["id"])) + canonical_after = postgres.psql_json(build_canonical_snapshot_sql()) + canonical_unchanged = ( + canonical_snapshot_complete(canonical_before) + and canonical_snapshot_complete(canonical_after) + and canonical_sha256(canonical_before) == canonical_sha256(canonical_after) + ) + checks = evaluate_checks(fixture, input_receipt, row_ids, readback, canonical_before, canonical_after) receipt.update( { "stage_command_result": stage_result, "rows": readback, + "candidate_counts": input_receipt["counts"], "row_link_fields_proven": proposal_links(readback, input_receipt), + "canonical": { + "fingerprint_before": canonical_sha256(canonical_before), + "fingerprint_after": canonical_sha256(canonical_after), + "table_counts": {key: len(canonical_before[key]) for key in CANONICAL_TABLES}, + "unchanged": canonical_unchanged, + }, "checks": checks, "status": "pass" if all(checks.values()) else "fail", } ) - except (CanaryError, OSError, ValueError, KeyError) as exc: + except (CanaryError, OSError, ValueError, KeyError, normalized_stage.bound.CheckpointError) as exc: receipt["error"] = {"type": type(exc).__name__, "message": str(exc)} finally: receipt["cleanup"] = postgres.cleanup() - receipt["cleanup"]["network_mode_none"] = ( - receipt.get("environment", {}).get("network_mode") == "none" - ) - receipt["cleanup"]["no_production_target_referenced"] = receipt[ - "cleanup" - ]["network_mode_none"] - if not all( - ( - receipt["cleanup"]["container_absent"], - receipt["cleanup"]["network_mode_none"], - ) - ): + receipt["cleanup"]["network_mode_none"] = receipt.get("environment", {}).get("network_mode") == "none" + receipt["cleanup"]["no_production_target_referenced"] = receipt["cleanup"]["network_mode_none"] + if not all((receipt["cleanup"]["container_absent"], receipt["cleanup"]["network_mode_none"])): receipt["status"] = "fail" receipt["strongest_claim"] = ( - "A realistic local document fixture was hash-captured, replayed through deterministic claim/evidence " - "extraction, normalized by the existing rich-proposal normalizer, and staged/read back as one " - "pending_review proposal in disposable networkless Postgres." + "One source-only artifact was parsed into exact-location candidate claims, duplicate/conflict " + "assessments, and a replay-bound pending_review proposal in disposable networkless Postgres; " + "representative canonical row contents remained fingerprint-identical." ) receipt["residual_gap"] = ( - "This does not prove live generative extraction, arbitrary document coverage, canonical apply, or any " - "hosted/production runtime." + "This proves the deterministic local fixture extractor and review queue only; it does not prove " + "arbitrary generative extraction, approval/apply, hosted runtime, or production mutation." ) if output: output.parent.mkdir(parents=True, exist_ok=True) @@ -501,16 +1073,45 @@ def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any] return receipt +def run_suite(fixture_paths: list[Path], output: Path | None = None) -> dict[str, Any]: + receipts = [run_canary(path.resolve()) for path in fixture_paths] + suite = { + "schema": SUITE_SCHEMA, + "status": "pass" if receipts and all(item["status"] == "pass" for item in receipts) else "fail", + "required_tier": "T2_runtime_to_review_queue_staging", + "fixture_count": len(receipts), + "document_fixture_passed": any( + item["status"] == "pass" and item["input"]["artifact_format"] == "plain_text" for item in receipts + ), + "social_conversation_fixture_passed": any( + item["status"] == "pass" and item["input"]["artifact_format"] == "telegram_jsonl" for item in receipts + ), + "canonical_fingerprint_invariant_all": bool(receipts) + and all(item.get("canonical", {}).get("unchanged") is True for item in receipts), + "receipts": receipts, + } + if output: + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(suite, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return suite + + def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--fixture", type=Path, default=DEFAULT_FIXTURE) + parser.add_argument( + "--fixture", + type=Path, + action="append", + help="repeatable scenario path; defaults to the document and Telegram scenarios", + ) parser.add_argument("--output", type=Path) return parser.parse_args(argv) def main(argv: list[str] | None = None) -> int: args = parse_args(argv) - receipt = run_canary(args.fixture.resolve(), args.output.resolve() if args.output else None) + fixtures = args.fixture or list(DEFAULT_FIXTURES) + receipt = run_suite([path.resolve() for path in fixtures], args.output.resolve() if args.output else None) print(json.dumps(receipt, sort_keys=True, separators=(",", ":"))) return 0 if receipt["status"] == "pass" else 1 diff --git a/scripts/stage_normalized_proposal.py b/scripts/stage_normalized_proposal.py index 9750929..9fe9a56 100644 --- a/scripts/stage_normalized_proposal.py +++ b/scripts/stage_normalized_proposal.py @@ -57,6 +57,12 @@ def prepare_normalized_child(parent: dict[str, Any]) -> dict[str, Any]: manifest = apply_payload.setdefault("normalization_manifest", {}) if not isinstance(manifest, dict): raise bound.CheckpointError("normalized child normalization_manifest must be an object") + parent_payload = parent.get("payload") + ingestion_manifest = parent_payload.get("ingestion_manifest") if isinstance(parent_payload, dict) else None + if ingestion_manifest is not None: + if not isinstance(ingestion_manifest, dict): + raise bound.CheckpointError("parent payload.ingestion_manifest must be an object") + manifest["ingestion_manifest"] = json.loads(json.dumps(ingestion_manifest, sort_keys=True)) parent_packet_sha256 = canonical_sha256(parent) manifest["parent_packet_sha256"] = parent_packet_sha256 child_payload_sha256 = canonical_sha256(payload) diff --git a/tests/test_prepare_kb_source_manifest.py b/tests/test_prepare_kb_source_manifest.py index ec41f52..d85a0c2 100644 --- a/tests/test_prepare_kb_source_manifest.py +++ b/tests/test_prepare_kb_source_manifest.py @@ -123,11 +123,17 @@ def test_prepare_builds_private_compiler_validated_manifest_without_db_write( assert manifest["dedupe"]["duplicates"][0]["claim_id"] == CANDIDATE_ID assert manifest["claims"][0]["quote"] == CLAIM_QUOTE assert manifest["dedupe"]["duplicates"][0]["source_quote"] == DUPLICATE_QUOTE - assert receipt["extraction"]["selected_source_references"] == [ - {"kind": "claim", "reference": "S00003"}, - {"kind": "evidence", "reference": "S00003"}, - {"kind": "duplicate", "reference": "S00002"}, + selected = receipt["extraction"]["selected_source_references"] + assert [(row["kind"], row["reference"]) for row in selected] == [ + ("claim", "S00003"), + ("evidence", "S00003"), + ("duplicate", "S00002"), ] + assert all(row["char_end"] > row["char_start"] for row in selected) + assert all(len(row["quote_sha256"]) == 64 for row in selected) + assert receipt["replay"]["exact_selected_locations_validated"] is True + assert receipt["extraction"]["evidence_count"] == 1 + assert receipt["extraction"]["duplicate_judgment_count"] == 1 assert stat.S_IMODE(output_dir.stat().st_mode) == 0o700 for path in output_dir.iterdir(): assert stat.S_IMODE(path.stat().st_mode) == 0o600 @@ -173,6 +179,63 @@ def test_source_fragment_ids_are_dense_across_blank_lines() -> None: assert fragments == {"S00001": "first", "S00002": "second"} +def test_source_fragment_index_trims_indentation_but_preserves_exact_offsets() -> None: + text = " indented Unicode: \u201creviewed\u201d \nnext\n" + + fragments, locations = preparer.build_source_fragment_index(text) + + assert fragments["S00001"] == "indented Unicode: \u201creviewed\u201d" + selected = locations["S00001"] + assert selected["line"] == 1 + assert text[selected["char_start"] : selected["char_end"]] == fragments["S00001"] + + +def test_bundle_location_validation_rejects_ambiguous_rebound() -> None: + text = "same line\nsame line\n" + fragments, locations = preparer.build_source_fragment_index(text) + extraction = preparer.resolve_model_output( + { + "claims": [ + { + "claim_key": "second_occurrence", + "type": "structural", + "text": "The second occurrence was selected.", + "quote_ref": "S00002", + "confidence": 0.5, + "tags": [], + "evidence": [{"quote_ref": "S00002", "role": "grounds"}], + } + ], + "duplicates": [], + "extraction_notes": "Select the second repeated line.", + }, + fragments, + locations, + ) + fake_bundle = { + "quote_bindings": [ + { + "kind": "claim", + "claim_key": "second_occurrence", + "quote": "same line", + "first_start": 0, + "first_end": 9, + }, + { + "kind": "evidence", + "claim_key": "second_occurrence", + "evidence_role": "grounds", + "quote": "same line", + "first_start": 0, + "first_end": 9, + }, + ] + } + + with pytest.raises(preparer.PreparationError, match="ambiguous repeated quote"): + preparer.validate_bundle_source_locations(fake_bundle, extraction["selected_source_references"]) + + def test_prepare_returns_no_novel_claims_without_manifest_or_stageable_packet( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -195,6 +258,13 @@ def test_binary_artifact_requires_explicit_utf8_extraction(tmp_path: Path) -> No preparer.extract_utf8_text(artifact, None) +def test_repo_native_jsonl_transcript_is_accepted_as_strict_utf8(tmp_path: Path) -> None: + artifact = tmp_path / "telegram.jsonl" + artifact.write_text('{"chat_id":1,"message_id":2,"message":"hello"}\n', encoding="utf-8") + + assert preparer.extract_utf8_text(artifact, None) == artifact.read_bytes() + + def test_openrouter_key_cannot_be_redirected_to_an_unapproved_endpoint(tmp_path: Path) -> None: with pytest.raises(preparer.PreparationError, match=r"must equal https://openrouter\.ai"): preparer.call_openrouter( diff --git a/tests/test_run_leo_local_ingestion_proposal_canary.py b/tests/test_run_leo_local_ingestion_proposal_canary.py index ec57f0b..cfae09e 100644 --- a/tests/test_run_leo_local_ingestion_proposal_canary.py +++ b/tests/test_run_leo_local_ingestion_proposal_canary.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import hashlib import json import sys from pathlib import Path @@ -13,90 +14,247 @@ sys.path.insert(0, str(REPO_ROOT / "scripts")) import run_leo_local_ingestion_proposal_canary as canary # noqa: E402 -def _loaded() -> tuple[dict, dict, dict]: - fixture, input_receipt = canary.load_fixture(canary.DEFAULT_FIXTURE) - row_ids = canary.build_row_ids(input_receipt["artifact_sha256"]) - return fixture, input_receipt, row_ids - - -def test_fixture_is_realistic_hash_bound_and_exactly_evidenced() -> None: - fixture, input_receipt, row_ids = _loaded() - - assert fixture["extraction"]["evidence"]["body"] in fixture["source"]["body"] - assert len(input_receipt["artifact_sha256"]) == 64 - assert len(input_receipt["source_content_sha256"]) == 64 - assert input_receipt["artifact_sha256"] != input_receipt["source_content_sha256"] - assert len(set(row_ids.values())) == 4 - - -def test_fixture_validation_fails_closed_when_evidence_is_not_in_source(tmp_path: Path) -> None: - fixture = json.loads(canary.DEFAULT_FIXTURE.read_text(encoding="utf-8")) - fixture["extraction"]["evidence"]["body"] = "invented evidence" - path = tmp_path / "bad-fixture.json" - path.write_text(json.dumps(fixture), encoding="utf-8") - - with pytest.raises(canary.CanaryError, match="exact substring"): - canary.load_fixture(path) - - -def test_fixture_validation_fails_closed_when_claim_body_is_not_in_source( - tmp_path: Path, -) -> None: - fixture = json.loads(canary.DEFAULT_FIXTURE.read_text(encoding="utf-8")) - fixture["extraction"]["claim"]["body"] = "invented claim body" - path = tmp_path / "bad-claim-fixture.json" - path.write_text(json.dumps(fixture), encoding="utf-8") - - with pytest.raises(canary.CanaryError, match="claim body must be an exact substring"): - canary.load_fixture(path) - - -def test_capture_sql_escapes_quotes_and_backslashes_from_fixture( - tmp_path: Path, -) -> None: - fixture = json.loads(canary.DEFAULT_FIXTURE.read_text(encoding="utf-8")) - fixture["source"]["title"] = "O'Brien\\review" - fixture["source"]["metadata"]["note"] = "quoted ' value \\ path" - path = tmp_path / "quoted-fixture.json" - path.write_text(json.dumps(fixture), encoding="utf-8") - loaded, input_receipt = canary.load_fixture(path) - row_ids = canary.build_row_ids(input_receipt["artifact_sha256"]) - - sql = canary.build_capture_sql(loaded, input_receipt, row_ids) - - assert canary.ap.sql_literal(fixture["source"]["title"]) in sql - assert canary.ap.sql_literal( - json.dumps(fixture["source"]["metadata"], sort_keys=True) - ) in sql - - -def test_parent_normalizes_to_hash_bound_pending_proposal_with_embedded_row_links() -> None: - fixture, input_receipt, row_ids = _loaded() +def _loaded(path: Path) -> tuple[dict, dict, dict, dict, dict]: + fixture, input_receipt = canary.load_fixture(path) + row_ids = canary.build_row_ids(input_receipt, fixture) parent = canary.build_parent_packet(fixture, input_receipt, row_ids) child = canary.normalized_stage.prepare_normalized_child(parent) - payload = child["payload"]["apply_payload"] - - assert child["status"] == "pending_review" - assert child["proposal_type"] == "approve_claim" - assert len(payload["claims"]) == 1 - assert len(payload["sources"]) == 2 - assert len(payload["evidence"]) == 2 - assert input_receipt["source_content_sha256"] in {row["hash"] for row in payload["sources"]} - excerpts = "\n".join(row["excerpt"] for row in payload["sources"]) - assert input_receipt["artifact_sha256"] in excerpts - assert row_ids["source_capture_id"] in excerpts - assert row_ids["claim_extraction_id"] in excerpts - assert row_ids["evidence_extraction_id"] in excerpts + return fixture, input_receipt, row_ids, parent, child -def test_capture_and_stage_sql_never_mutate_canonical_public_tables() -> None: - fixture, input_receipt, row_ids = _loaded() - child = canary.normalized_stage.prepare_normalized_child( - canary.build_parent_packet(fixture, input_receipt, row_ids) +def _copy_scenario(tmp_path: Path, scenario_path: Path) -> Path: + scenario = json.loads(scenario_path.read_text(encoding="utf-8")) + artifact_source = scenario_path.parent / scenario["artifact"]["path"] + artifact_target = tmp_path / artifact_source.name + artifact_target.write_bytes(artifact_source.read_bytes()) + scenario_target = tmp_path / scenario_path.name + scenario_target.write_text(json.dumps(scenario), encoding="utf-8") + return scenario_target + + +def _synthetic_readback(fixture: dict, input_receipt: dict, row_ids: dict, child: dict) -> dict: + claim_rows = [] + evidence_rows = [] + segment_by_id = {segment["id"]: segment for segment in fixture["segments"]} + for claim in fixture["extraction"]["claims"]: + claim_id = row_ids["claim_extraction_ids"][claim["claim_key"]] + claim_rows.append( + { + "id": claim_id, + "source_capture_id": row_ids["source_capture_id"], + "claim_key": claim["claim_key"], + "body": claim["body"], + "metadata": {"extractor": input_receipt["extractor"]}, + } + ) + for index, evidence in enumerate(claim["evidence"]): + segment = segment_by_id[evidence["segment_id"]] + evidence_rows.append( + { + "id": row_ids["evidence_extraction_ids"][f"{claim['claim_key']}:{index}"], + "claim_extraction_id": claim_id, + "source_capture_id": row_ids["source_capture_id"], + "source_segment_id": segment["id"], + "source_locator": segment["locator"], + "source_content_sha256": segment["content_sha256"], + "body": evidence["excerpt"], + } + ) + proposal = { + **child, + "reviewed_by_handle": None, + "reviewed_at": None, + "applied_by_handle": None, + "applied_at": None, + } + counts = input_receipt["counts"] + return { + "source_captures": [ + { + "artifact_sha256": input_receipt["artifact_sha256"], + "replay_identity_sha256": input_receipt["replay_identity_sha256"], + } + ], + "claim_extractions": claim_rows, + "evidence_extractions": evidence_rows, + "duplicate_assessments": [{} for _item in fixture["extraction"]["duplicates"]], + "conflict_assessments": [{} for _item in fixture["extraction"]["conflicts"]], + "staged_proposal": proposal, + "counts": { + "source_captures": 1, + "claim_extractions": counts["claim_candidates"], + "evidence_extractions": counts["evidence_candidates"], + "duplicate_assessments": counts["duplicate_judgments"], + "conflict_assessments": counts["conflict_relationships"], + "staged_proposals": 1, + }, + } + + +@pytest.mark.parametrize("scenario_path", canary.DEFAULT_FIXTURES) +def test_source_artifact_is_separate_hash_bound_and_replayable(scenario_path: Path) -> None: + fixture, input_receipt, row_ids, parent, child = _loaded(scenario_path) + artifact_path = Path(input_receipt["artifact_path"]) + + assert input_receipt["artifact_sha256"] == hashlib.sha256(artifact_path.read_bytes()).hexdigest() + assert input_receipt["artifact_sha256"] != input_receipt["scenario_sha256"] + assert len(input_receipt["replay_identity_sha256"]) == 64 + assert row_ids == canary.build_row_ids(input_receipt, fixture) + ingestion = child["payload"]["apply_payload"]["normalization_manifest"]["ingestion_manifest"] + assert ingestion["artifact_sha256"] == input_receipt["artifact_sha256"] + assert ingestion["extractor"] == input_receipt["extractor"] + assert ingestion["replay_identity_sha256"] == input_receipt["replay_identity_sha256"] + assert ingestion["row_ids"] == row_ids + assert parent["status"] == child["status"] == "pending_review" + + +def test_document_and_telegram_candidate_accounting_is_exact() -> None: + _doc, doc_input, _doc_ids, _doc_parent, doc_child = _loaded(canary.DEFAULT_DOCUMENT_FIXTURE) + telegram, telegram_input, _telegram_ids, _telegram_parent, telegram_child = _loaded(canary.DEFAULT_TELEGRAM_FIXTURE) + + assert doc_input["counts"] == { + "source_segments": 3, + "claim_candidates": 1, + "evidence_candidates": 1, + "duplicate_judgments": 0, + "conflict_relationships": 0, + } + assert telegram_input["counts"] == { + "source_segments": 3, + "claim_candidates": 2, + "evidence_candidates": 3, + "duplicate_judgments": 1, + "conflict_relationships": 1, + } + doc_payload = doc_child["payload"]["apply_payload"] + telegram_payload = telegram_child["payload"]["apply_payload"] + assert {key: len(doc_payload[key]) for key in ("claims", "sources", "evidence", "edges")} == { + "claims": 1, + "sources": 2, + "evidence": 2, + "edges": 0, + } + assert {key: len(telegram_payload[key]) for key in ("claims", "sources", "evidence", "edges")} == { + "claims": 2, + "sources": 5, + "evidence": 5, + "edges": 1, + } + assessment = telegram_payload["normalization_manifest"]["dedupe_conflict_assessment"] + assert assessment["duplicates"] == telegram["extraction"]["duplicates"] + assert assessment["conflicts"] == telegram["extraction"]["conflicts"] + assert telegram_payload["edges"][0]["edge_type"] == "contradicts" + + +def test_telegram_duplicate_text_keeps_distinct_exact_message_provenance() -> None: + fixture, _input, _row_ids, _parent, _child = _loaded(canary.DEFAULT_TELEGRAM_FIXTURE) + first, _second, repeated = fixture["segments"] + + assert first["body"] == repeated["body"] + assert first["text_sha256"] == repeated["text_sha256"] + assert first["content_sha256"] != repeated["content_sha256"] + assert first["locator"] == "telegram://chat/900001/message/7301" + assert repeated["locator"] == "telegram://chat/900001/message/7303" + duplicate = fixture["extraction"]["duplicates"][0] + assert duplicate["source_locator"] == repeated["locator"] + assert duplicate["source_json_pointer"] == "jsonl://line/3/message" + + +def test_fixture_validation_fails_closed_on_invented_evidence(tmp_path: Path) -> None: + scenario_path = _copy_scenario(tmp_path, canary.DEFAULT_TELEGRAM_FIXTURE) + scenario = json.loads(scenario_path.read_text(encoding="utf-8")) + scenario["extraction"]["claims"][0]["evidence"][0]["excerpt"] = "invented evidence" + scenario_path.write_text(json.dumps(scenario), encoding="utf-8") + + with pytest.raises(canary.CanaryError, match="not an exact segment substring"): + canary.load_fixture(scenario_path) + + +def test_fixture_artifact_path_cannot_escape_scenario_directory(tmp_path: Path) -> None: + scenario = json.loads(canary.DEFAULT_DOCUMENT_FIXTURE.read_text(encoding="utf-8")) + scenario["artifact"]["path"] = "../outside.txt" + path = tmp_path / "scenario.json" + path.write_text(json.dumps(scenario), encoding="utf-8") + + with pytest.raises(canary.CanaryError, match="remain inside"): + canary.load_fixture(path) + + +def test_capture_sql_escapes_quotes_and_backslashes(tmp_path: Path) -> None: + scenario_path = _copy_scenario(tmp_path, canary.DEFAULT_DOCUMENT_FIXTURE) + scenario = json.loads(scenario_path.read_text(encoding="utf-8")) + scenario["source"]["title"] = "O'Brien\\review" + scenario["source"]["metadata"]["note"] = "quoted ' value \\ path" + scenario_path.write_text(json.dumps(scenario), encoding="utf-8") + fixture, input_receipt, row_ids, _parent, _child = _loaded(scenario_path) + + sql = canary.build_capture_sql(fixture, input_receipt, row_ids) + + assert canary.ap.sql_literal(scenario["source"]["title"]) in sql + expected_metadata = { + **scenario["source"]["metadata"], + "extractor": input_receipt["extractor"], + } + assert canary.ap.sql_literal(json.dumps(expected_metadata, sort_keys=True)) in sql + + +def test_replay_identity_and_ids_change_with_extractor_version(tmp_path: Path) -> None: + scenario_path = _copy_scenario(tmp_path, canary.DEFAULT_DOCUMENT_FIXTURE) + fixture, original, original_ids, _parent, original_child = _loaded(scenario_path) + scenario = json.loads(scenario_path.read_text(encoding="utf-8")) + scenario["extractor"]["version"] = "3" + scenario_path.write_text(json.dumps(scenario), encoding="utf-8") + changed_fixture, changed, changed_ids, _changed_parent, changed_child = _loaded(scenario_path) + + assert original["artifact_sha256"] == changed["artifact_sha256"] + assert original["replay_identity_sha256"] != changed["replay_identity_sha256"] + assert original_ids["source_capture_id"] == changed_ids["source_capture_id"] + assert original_ids["claim_extraction_ids"] != changed_ids["claim_extraction_ids"] + assert original_ids["parent_proposal_id"] != changed_ids["parent_proposal_id"] + assert original_child["payload_sha256"] != changed_child["payload_sha256"] + assert fixture["segments"] == changed_fixture["segments"] + + +def test_source_byte_tamper_changes_hashes_and_replay_ids(tmp_path: Path) -> None: + scenario_path = _copy_scenario(tmp_path, canary.DEFAULT_DOCUMENT_FIXTURE) + fixture, before, before_ids, _parent, _child = _loaded(scenario_path) + artifact_path = Path(before["artifact_path"]) + artifact_path.write_text( + artifact_path.read_text(encoding="utf-8") + "\n\nA new source paragraph.", encoding="utf-8" ) + changed_fixture, after, after_ids, _changed_parent, _changed_child = _loaded(scenario_path) + + assert before["artifact_sha256"] != after["artifact_sha256"] + assert before["source_content_sha256"] != after["source_content_sha256"] + assert before["replay_identity_sha256"] != after["replay_identity_sha256"] + assert before_ids["source_capture_id"] != after_ids["source_capture_id"] + assert before_ids["claim_extraction_ids"] != after_ids["claim_extraction_ids"] + assert len(changed_fixture["segments"]) == len(fixture["segments"]) + 1 + + +def test_replay_properties_hold_across_many_version_labels(tmp_path: Path) -> None: + scenario_path = _copy_scenario(tmp_path, canary.DEFAULT_DOCUMENT_FIXTURE) + base = json.loads(scenario_path.read_text(encoding="utf-8")) + observed: set[str] = set() + for index in range(40): + scenario = copy.deepcopy(base) + scenario["extractor"]["version"] = f"property-{index}" + scenario_path.write_text(json.dumps(scenario), encoding="utf-8") + fixture_a, receipt_a = canary.load_fixture(scenario_path) + fixture_b, receipt_b = canary.load_fixture(scenario_path) + ids_a = canary.build_row_ids(receipt_a, fixture_a) + ids_b = canary.build_row_ids(receipt_b, fixture_b) + assert receipt_a == receipt_b + assert ids_a == ids_b + observed.add(receipt_a["replay_identity_sha256"]) + assert len(observed) == 40 + + +@pytest.mark.parametrize("scenario_path", canary.DEFAULT_FIXTURES) +def test_capture_and_stage_sql_do_not_mutate_canonical_tables(scenario_path: Path) -> None: + fixture, input_receipt, row_ids, parent, child = _loaded(scenario_path) sql = "\n".join( ( - canary.build_schema_sql(), canary.build_capture_sql(fixture, input_receipt, row_ids), canary.normalized_stage.build_stage_sql(child), ) @@ -106,59 +264,50 @@ def test_capture_and_stage_sql_never_mutate_canonical_public_tables() -> None: assert "update public." not in sql assert "delete from public." not in sql assert "insert into kb_canary.source_captures" in sql - assert "insert into kb_canary.claim_extractions" in sql - assert "insert into kb_canary.evidence_extractions" in sql assert "insert into kb_stage.kb_proposals" in sql + assert parent["payload"]["ingestion_manifest"]["row_ids"] == row_ids -def test_check_evaluation_requires_every_row_link_and_staging_boundary() -> None: - fixture, input_receipt, row_ids = _loaded() - child = canary.normalized_stage.prepare_normalized_child( - canary.build_parent_packet(fixture, input_receipt, row_ids) +def test_canonical_snapshot_covers_all_nonempty_canonical_tables() -> None: + schema_sql = canary.build_schema_sql().lower() + snapshot_sql = canary.build_canonical_snapshot_sql().lower() + + for table in ("claims", "sources", "claim_evidence", "claim_edges"): + assert f"insert into public.{table}" in schema_sql + assert f"from public.{table}" in snapshot_sql + assert "order by" in snapshot_sql + assert "begin transaction read only" in snapshot_sql + assert canary.canonical_snapshot_complete({}) is False + assert ( + canary.canonical_snapshot_complete( + { + "claims": [{"id": "1"}], + "sources": [{"id": "2"}], + "claim_evidence": [{"claim_id": "1"}], + "claim_edges": [{"from_claim": "1"}], + } + ) + is True ) - apply_payload = child["payload"]["apply_payload"] - readback = { - "source_capture": { - "id": row_ids["source_capture_id"], - "artifact_sha256": input_receipt["artifact_sha256"], - "content_sha256": input_receipt["source_content_sha256"], - "source_identity": input_receipt["source_identity"], - }, - "claim_extraction": { - "id": row_ids["claim_extraction_id"], - "source_capture_id": row_ids["source_capture_id"], - "body": fixture["extraction"]["claim"]["body"], - "metadata": {"text": fixture["extraction"]["claim"]["text"]}, - }, - "evidence_extraction": { - "id": row_ids["evidence_extraction_id"], - "claim_extraction_id": row_ids["claim_extraction_id"], - "source_capture_id": row_ids["source_capture_id"], - "body": fixture["extraction"]["evidence"]["body"], - "metadata": fixture["extraction"]["evidence"]["metadata"], - }, - "staged_proposal": { - "id": child["id"], - "status": "pending_review", - "source_ref": child["source_ref"], - "payload": {"apply_payload": apply_payload}, - "reviewed_by_handle": None, - "reviewed_at": None, - "applied_by_handle": None, - "applied_at": None, - }, - "counts": { - "source_captures": 1, - "claim_extractions": 1, - "evidence_extractions": 1, - "staged_proposals": 1, - }, + + +def test_evaluation_fails_when_exact_evidence_locator_is_changed() -> None: + fixture, input_receipt, row_ids, _parent, child = _loaded(canary.DEFAULT_TELEGRAM_FIXTURE) + readback = _synthetic_readback(fixture, input_receipt, row_ids, child) + canonical = { + "claims": [{"id": "1"}], + "sources": [{"id": "2"}], + "claim_evidence": [{"claim_id": "1"}], + "claim_edges": [{"from_claim": "1"}], } - checks = canary.evaluate_checks(fixture, input_receipt, row_ids, readback) + checks = canary.evaluate_checks(fixture, input_receipt, row_ids, readback, canonical, copy.deepcopy(canonical)) assert all(checks.values()) broken = copy.deepcopy(readback) - broken["evidence_extraction"]["claim_extraction_id"] = canary.stable_uuid("0" * 64, "wrong") + broken["evidence_extractions"][0]["source_locator"] = "telegram://chat/900001/message/9999" assert ( - canary.evaluate_checks(fixture, input_receipt, row_ids, broken)["evidence_links_to_claim_and_source"] is False + canary.evaluate_checks(fixture, input_receipt, row_ids, broken, canonical, copy.deepcopy(canonical))[ + "all_evidence_has_exact_source_location" + ] + is False ) diff --git a/tests/test_stage_normalized_proposal.py b/tests/test_stage_normalized_proposal.py index 09f86d7..a4309a8 100644 --- a/tests/test_stage_normalized_proposal.py +++ b/tests/test_stage_normalized_proposal.py @@ -116,6 +116,37 @@ def test_invalid_source_hash_refuses_to_prepare_a_child() -> None: stage.prepare_normalized_child(parent) +def test_prepare_normalized_child_preserves_replayable_ingestion_manifest() -> None: + parent = _parent() + parent["payload"]["ingestion_manifest"] = { + "artifact_sha256": "a" * 64, + "extractor": {"name": "deterministic_fixture_replay", "version": "2"}, + "replay_identity_sha256": "b" * 64, + "segments": [ + { + "id": "message-1", + "locator": "telegram://chat/1/message/1", + "content_sha256": "c" * 64, + } + ], + } + + child = stage.prepare_normalized_child(parent) + + assert ( + child["payload"]["apply_payload"]["normalization_manifest"]["ingestion_manifest"] + == parent["payload"]["ingestion_manifest"] + ) + + +def test_prepare_normalized_child_rejects_non_object_ingestion_manifest() -> None: + parent = _parent() + parent["payload"]["ingestion_manifest"] = "not-an-object" + + with pytest.raises(stage.bound.CheckpointError, match="ingestion_manifest must be an object"): + stage.prepare_normalized_child(parent) + + def test_stage_sql_validates_exact_pending_row_before_commit_and_never_writes_public() -> None: child = stage.prepare_normalized_child(_parent()) sql = stage.build_stage_sql(child) From 15c30f1fbe30c8f2295ddc33e293a45788a95706 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 03:19:15 +0200 Subject: [PATCH 02/27] Make Leo skill onboarding clean-context reproducible (#150) * make Leo skill onboarding clean-context reproducible * harden Leo clean-context skill acceptance --- .../skills/leo-telegram-canary-ops/SKILL.md | 16 +- .agents/skills/living-ip-kb-interop/SKILL.md | 4 +- .agents/skills/openclaw-agent/SKILL.md | 7 +- .agents/skills/teleo-gcp-parity-ops/SKILL.md | 16 ++ .../skills/teleo-infra-provenance/SKILL.md | 4 +- .../teleo-kb-db-change-workflow/SKILL.md | 5 +- .agents/skills/teleo-leo-onboarding/SKILL.md | 58 +++- .../scripts/install_skill_pack.py | 106 ++++++++ .../scripts/run_clean_context_canary.py | 172 ++++++++++++ .../scripts/validate_skill_pack.py | 248 ++++++++++++++++++ .../teleo-reconstruction-recovery/SKILL.md | 83 ++++++ .../agents/openai.yaml | 4 + .../working-leo-m3taversal-outcomes/SKILL.md | 9 +- README.md | 24 +- docs/kb-rebuild-and-recompile.md | 16 +- .../current-truth-index.md | 50 +++- .../fable-leo-teleo-onboarding.md | 16 +- ...architecture-work-and-outcomes-20260714.md | 71 +++-- .../operator-surface-map.md | 17 +- ...ck-clean-context-agent-review-current.json | 106 ++++++++ ...ill-pack-clean-context-canary-current.json | 71 +++++ .../skill-pack-manifest.json | 142 ++++++++-- .../skill-pack-readme.md | 107 ++++++-- tests/test_repo_skill_pack.py | 169 +++++++++++- 24 files changed, 1389 insertions(+), 132 deletions(-) create mode 100755 .agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py create mode 100755 .agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py create mode 100755 .agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py create mode 100644 .agents/skills/teleo-reconstruction-recovery/SKILL.md create mode 100644 .agents/skills/teleo-reconstruction-recovery/agents/openai.yaml create mode 100644 docs/reports/leo-working-state-20260709/skill-pack-clean-context-agent-review-current.json create mode 100644 docs/reports/leo-working-state-20260709/skill-pack-clean-context-canary-current.json diff --git a/.agents/skills/leo-telegram-canary-ops/SKILL.md b/.agents/skills/leo-telegram-canary-ops/SKILL.md index 0788e1c..2e187c3 100644 --- a/.agents/skills/leo-telegram-canary-ops/SKILL.md +++ b/.agents/skills/leo-telegram-canary-ops/SKILL.md @@ -50,7 +50,7 @@ Name the canary before sending: ## Message Discipline -Use unique markers such as `WL-LIVE-TG-CORY-YYYYMMDD-Tn`. +Use unique markers such as `WL-LIVE-TG-M3TAVERSAL-YYYYMMDD-Tn`. Ask Leo for machine-checkable reply markers like: @@ -74,10 +74,14 @@ After each live Telegram canary: ## Current Known Proof -- Live memory and KB audit passed for marker `WL-LIVE-TG-CORY-20260709`. -- Live staged write passed for `WL-LIVE-TG-CORY-20260709-T3`. -- Readback passed for `WL-LIVE-TG-CORY-20260709-T4`. -- Open-ended m3taversal-style read-only triage passed for the legacy marker - `WL-LIVE-TG-CORY-OPEN-20260709`. +- Live memory and KB audit are retained in + `docs/reports/leo-working-state-20260709/telegram-live-canary-current.json`. +- Live staged-write proof is retained in + `docs/reports/leo-working-state-20260709/telegram-live-db-write-canary-20260709.json`. +- Open-ended m3taversal-standard read-only triage is retained in + `docs/reports/leo-working-state-20260709/telegram-live-open-ended-suite-current.md`. + +Historical receipts contain immutable legacy marker IDs. Treat them only as +evidence identifiers; never reuse them as a participant name or future marker. Use `docs/reports/leo-working-state-20260709/current-truth-index.md` for artifact paths. diff --git a/.agents/skills/living-ip-kb-interop/SKILL.md b/.agents/skills/living-ip-kb-interop/SKILL.md index 5b4d3bb..7236117 100644 --- a/.agents/skills/living-ip-kb-interop/SKILL.md +++ b/.agents/skills/living-ip-kb-interop/SKILL.md @@ -25,7 +25,7 @@ Prefer deterministic local surfaces before asking an LLM: - generated claim indexes from `lib/claim_index.py`; - search helpers in `lib/search.py`; - copied SQLite state through `teleo-db-operator`; -- retained proof JSON in `.crabbox-results/` or `proof/`. +- retained proof JSON at a caller-specified generated-output path. Read outputs must include file paths, source paths, claim/entity IDs when available, and the exact query used. @@ -80,7 +80,7 @@ If a runtime cannot implement one of these, record the missing tool as a blocker ## Expected Artifact -Write `.crabbox-results/kb-interop-proof.json` or a caller-specified proof path containing: +Write `/kb-interop-proof.json` containing: - runtime name; - model/provider if known; diff --git a/.agents/skills/openclaw-agent/SKILL.md b/.agents/skills/openclaw-agent/SKILL.md index 1f6b0ad..6a94ae3 100644 --- a/.agents/skills/openclaw-agent/SKILL.md +++ b/.agents/skills/openclaw-agent/SKILL.md @@ -28,9 +28,10 @@ Create or update: - `AGENTS.md`: scope, repo boundaries, proof requirements; - `SOUL.md`: Rio or Theseus identity; - `TOOLS.md`: bounded tools only; -- `skills/decision-engine-refinement/SKILL.md`; -- `skills/living-ip-kb-interop/SKILL.md`; -- `skills/teleo-db-operator/SKILL.md` only for read-only local copies unless explicitly authorized. +- `/skills/decision-engine-refinement/SKILL.md`; +- `/skills/living-ip-kb-interop/SKILL.md`; +- `/skills/teleo-db-operator/SKILL.md` only for read-only + local copies unless explicitly authorized. ## Tool Policy diff --git a/.agents/skills/teleo-gcp-parity-ops/SKILL.md b/.agents/skills/teleo-gcp-parity-ops/SKILL.md index 6d9191e..5cd671d 100644 --- a/.agents/skills/teleo-gcp-parity-ops/SKILL.md +++ b/.agents/skills/teleo-gcp-parity-ops/SKILL.md @@ -83,6 +83,22 @@ Do not describe a passing Hermes memory sync as canonical KB parity. - Persistent GCP `teleo_canonical` remains the older staging copy measured at `52,164` rows and `26` proposals. It has not been promoted or cut over. +## Open Least-Privilege Candidate + +PR #148, `Scope GCP Leo runtime to least-privilege Cloud SQL access`, is open as +of the 2026-07-15 skill-pack reconciliation. It proposes scoped runtime roles, +secret access, fail-closed Cloud SQL behavior, deployment rollback, and positive +plus negative permission checks. Those branch files and proposed live outcomes +are candidate evidence only. Before using them, run: + +```bash +gh pr view 148 --json state,mergedAt,mergeCommit,headRefName,url +``` + +Until the PR is merged and its runtime receipt passes, use only paths present on +canonical `main`, keep persistent GCP classified as staging, do not promote it, +and do not infer least-privilege cutover from a branch or dry run. + Primary retained proof: - `docs/reports/leo-working-state-20260709/gcp-db-first-working-leo-20260714.md` diff --git a/.agents/skills/teleo-infra-provenance/SKILL.md b/.agents/skills/teleo-infra-provenance/SKILL.md index 0329f39..8c242c6 100644 --- a/.agents/skills/teleo-infra-provenance/SKILL.md +++ b/.agents/skills/teleo-infra-provenance/SKILL.md @@ -26,11 +26,13 @@ Prevent incorrect assumptions about where Leo/Teleo code, runtime state, DB stat - Hermes/leoclean runtime profile. - Postgres canonical DB. - `kb_stage` proposal ledger. -- Retained proof artifacts under local `outputs/`. +- Caller-specific external output directories, which are retained evidence but + are not repository routes unless copied into the repo evidence pack. - Synced report artifacts under the VPS profile report directory. ## Current Known Path Map +- Canonical code/deployment repository: GitHub `living-ip/teleo-infrastructure`. - Repo-local evidence pack: `docs/reports/leo-working-state-20260709/` - VPS deploy/source area: `/opt/teleo-eval/workspaces/deploy-infra` - VPS deploy stamp: `/opt/teleo-eval/.last-deploy-sha` diff --git a/.agents/skills/teleo-kb-db-change-workflow/SKILL.md b/.agents/skills/teleo-kb-db-change-workflow/SKILL.md index 6827cc9..34ce68e 100644 --- a/.agents/skills/teleo-kb-db-change-workflow/SKILL.md +++ b/.agents/skills/teleo-kb-db-change-workflow/SKILL.md @@ -44,7 +44,7 @@ Run the deterministic source-to-proposal canary: ```bash .venv/bin/python scripts/run_leo_local_ingestion_proposal_canary.py \ --fixture fixtures/working-leo/document-ingestion-v1.json \ - --output outputs/leo-local-ingestion-proposal-canary-current.json + --output /tmp/leo-local-ingestion-proposal-canary-current.json ``` Require the source, claim, evidence, and proposal UUIDs to link exactly; claim @@ -131,7 +131,8 @@ Read: - `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json` - `docs/reports/leo-working-state-20260709/leo-source-composition-clone-checkpoint-current.json` -- `outputs/leo-local-ingestion-proposal-canary-current.json` +- `docs/reports/leo-working-state-20260709/leo-v3-document-source-lifecycle-current.md` +- `docs/reports/leo-working-state-20260709/source-document-compiler-canary-20260713.md` ## Claim Ceiling diff --git a/.agents/skills/teleo-leo-onboarding/SKILL.md b/.agents/skills/teleo-leo-onboarding/SKILL.md index da5a823..61fb45f 100644 --- a/.agents/skills/teleo-leo-onboarding/SKILL.md +++ b/.agents/skills/teleo-leo-onboarding/SKILL.md @@ -1,13 +1,15 @@ --- name: teleo-leo-onboarding -description: Use when a worker needs fast context on Teleo/Living IP, Leo, the product architecture, current VPS/GCP split, and the July 9 working-state evidence before doing Leo, Teleo, KB, Telegram, VPS, or GCP work. +description: Use when a worker needs fast context on Teleo/Living IP, Leo architecture, VPS/GCP and Hermes runtime boundaries, database provenance, ingestion/apply/reconstruction, identity, testing, security, secrets, or incident recovery before doing Leo or Teleo work. --- # Teleo / Leo Onboarding ## Job -Orient the worker before action. Build a current, proof-linked understanding of the company/product, Leo's role, the infrastructure surfaces, and the exact claim ceiling. +Orient the worker before action. Build a current, proof-linked understanding of +the company/product, Leo's role, the infrastructure surfaces, and the exact +claim ceiling without rediscovering the system from historical chat. ## Trigger Phrases @@ -17,6 +19,8 @@ Orient the worker before action. Build a current, proof-linked understanding of - "Fable handoff for Leo" - "before working on Teleo infra" - "explain the architecture" +- "recover or reconstruct Leo" +- "which skill should I use" ## Core Model @@ -46,6 +50,52 @@ Orient the worker before action. Build a current, proof-linked understanding of - `SOUL.md` is a rendered/runtime artifact. Direct file edits are not canonical identity changes without DB row and render/sync proof. +## Clean-Context First Commands + +From the repository root, validate the pack without a virtual environment or +network access: + +```bash +.agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root . +``` + +Install exactly the manifest-indexed skills into an empty temporary agent skill +root with: + +```bash +.agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py \ + --root . \ + --target /private/tmp/teleo-clean-agent/skills +``` + +Installed skills still resolve repository-relative routes against the current +`teleo-infrastructure` checkout. Keep the agent working directory at the repo +root. For pytest, use `.venv/bin/python`; if `.venv` is absent, create it from +`README.md` before running tests. Do not guess a bare `python` command. + +Run the deterministic isolated acceptance canary with: + +```bash +.agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py \ + --root . \ + --output docs/reports/leo-working-state-20260709/skill-pack-clean-context-canary-current.json +``` + +## Task Router + +| Area | Skill | Canonical first route | +|---|---|---| +| Company, product, architecture | `teleo-leo-onboarding` | `docs/reports/leo-working-state-20260709/current-truth-index.md` | +| VPS runtime and incidents | `teleo-vps-runtime-ops` | Fresh service, deploy, DB, and cleanup readbacks | +| GCP and Cloud SQL | `teleo-gcp-parity-ops` | Current main evidence; keep open PR #148 candidate-only | +| Hermes packaging/runtime | `nousresearch-hermes-agent` | `hermes-agent/` plus the live service-specific ops skill | +| Database provenance | `teleo-infra-provenance` and `teleo-db-operator` | Separate Git, runtime, SQLite, Postgres, and retained proof | +| Source ingestion and proposal/apply | `teleo-kb-db-change-workflow` | Hash-bound source, review, guarded apply, receipt | +| Reconstruction and recovery | `teleo-reconstruction-recovery` | `docs/kb-rebuild-and-recompile.md` | +| Identity and rendered soul | `working-leo-m3taversal-outcomes` | Canonical identity rows before `SOUL.md` | +| Testing and proof tiers | `teleo-proof-handoff` | Exact tier, command, receipt, cleanup, claim ceiling | +| Secrets | `private-password-storage` | Provider login first; never paste or retrieve a secret | + ## Read First From the repo root, read: @@ -61,6 +111,7 @@ From the repo root, read: 9. `.agents/skills/working-leo-m3taversal-outcomes/SKILL.md` 10. `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md` 11. `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md` +12. `docs/kb-rebuild-and-recompile.md` ## Required Status Split @@ -82,6 +133,9 @@ Do not collapse GCP demo readiness into Telegram completion. Do not collapse pro - Do not production-apply DB packets unless explicitly authorized. - Do not expose secret contents. - Do not treat an old summary as current if a fresh readback is cheap. +- PRs #146 and #147 are merged repository truth. PR #148 remains an open GCP + least-privilege candidate until live `gh pr view 148` readback says otherwise; + never route a deployment from its branch as though it were `main`. - Use `ssh -o BatchMode=yes teleo-gcp-staging true` to preflight passwordless GCP VM access. If the firewall route drifts, rediscover the authenticated non-secret route before declaring a blocker. The OIDC/IAP workflow is still diff --git a/.agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py b/.agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py new file mode 100755 index 0000000..e8e1166 --- /dev/null +++ b/.agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Install the manifest-indexed Leo/Teleo skills into an empty skill root.""" + +from __future__ import annotations + +import argparse +import json +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +DEFAULT_MANIFEST = Path("docs/reports/leo-working-state-20260709/skill-pack-manifest.json") +SKILL_NAME = re.compile(r"[a-z0-9-]{1,64}") + + +def _tracked_skill_files(root: Path, skill_dir: Path) -> list[Path]: + relative_dir = skill_dir.relative_to(root) + result = subprocess.run( + ["git", "-C", str(root), "ls-files", "-z", "--", str(relative_dir)], + check=False, + capture_output=True, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr.decode("utf-8", errors="replace")) + files = [root / Path(item.decode("utf-8")) for item in result.stdout.split(b"\0") if item] + if not files: + raise ValueError(f"skill has no tracked files: {relative_dir}") + for source in files: + if source.is_symlink(): + raise ValueError(f"skill contains a symlink: {source.relative_to(root)}") + if not source.is_file(): + raise ValueError(f"skill route is not a regular file: {source.relative_to(root)}") + source.resolve().relative_to(skill_dir.resolve()) + return files + + +def install(root: Path, manifest_path: Path, target: Path) -> dict[str, object]: + manifest_file = root / manifest_path + manifest = json.loads(manifest_file.read_text(encoding="utf-8")) + skills = manifest["skills"] + if target.exists(): + raise ValueError(f"target already exists: {target}") + + target.parent.mkdir(parents=True, exist_ok=True) + stage = Path(tempfile.mkdtemp(prefix=".teleo-skill-pack-", dir=target.parent)) + installed: list[str] = [] + try: + for entry in skills: + name = entry["name"] + if not isinstance(name, str) or SKILL_NAME.fullmatch(name) is None: + raise ValueError(f"invalid skill name: {name!r}") + expected_path = Path(".agents") / "skills" / name / "SKILL.md" + if Path(entry["path"]) != expected_path: + raise ValueError(f"skill path must be {expected_path}: {entry['path']!r}") + if name in installed: + raise ValueError(f"duplicate skill name: {name}") + + source = root / expected_path.parent + source.resolve().relative_to((root / ".agents" / "skills").resolve()) + if not source.is_dir(): + raise FileNotFoundError(source) + destination = stage / name + destination.resolve().relative_to(stage.resolve()) + for tracked_source in _tracked_skill_files(root, source): + relative = tracked_source.relative_to(source) + tracked_destination = destination / relative + tracked_destination.resolve().relative_to(destination.resolve()) + tracked_destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(tracked_source, tracked_destination) + installed.append(name) + stage.rename(target) + except Exception: + shutil.rmtree(stage, ignore_errors=True) + raise + + return { + "artifact": "leo_teleo_skill_pack_install", + "status": "pass", + "installed_skill_count": len(installed), + "installed_skills": installed, + "contains_secrets": False, + "production_mutation_authorized": False, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path.cwd()) + parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST) + parser.add_argument("--target", type=Path, required=True) + args = parser.parse_args() + try: + result = install(args.root.resolve(), args.manifest, args.target.resolve()) + except Exception as exc: + result = {"artifact": "leo_teleo_skill_pack_install", "status": "fail", "error": str(exc)} + sys.stdout.write(json.dumps(result, indent=2, sort_keys=True) + "\n") + return 1 + sys.stdout.write(json.dumps(result, indent=2, sort_keys=True) + "\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py b/.agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py new file mode 100755 index 0000000..3c36733 --- /dev/null +++ b/.agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Run the deterministic T2 isolated-install acceptance canary for the skill pack.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import tempfile +from pathlib import Path + +DEFAULT_MANIFEST = Path("docs/reports/leo-working-state-20260709/skill-pack-manifest.json") + +SEMANTIC_CONTRACT = { + "authority": ( + "teleo-infra-provenance", + "Canonical code/deployment repository: GitHub `living-ip/teleo-infrastructure`.", + "GitHub living-ip/teleo-infrastructure", + ), + "vps_database": ( + "teleo-leo-onboarding", + "VPS canonical KB: Docker Postgres `teleo-pg`, database `teleo`.", + "teleo-pg / teleo", + ), + "gcp_database": ( + "teleo-leo-onboarding", + "GCP canonical KB: private Cloud SQL database `teleo_canonical`.", + "private Cloud SQL teleo_canonical; persistent copy remains staging", + ), + "live_service": ( + "teleo-infra-provenance", + "Live service: `leoclean-gateway.service`", + "leoclean-gateway.service", + ), + "merged_reconstruction": ( + "teleo-reconstruction-recovery", + "PR #146 is merged", + "PR #146 merged deterministic genesis-plus-ledger reconstruction", + ), + "merged_reasoning_verifier": ( + "teleo-reconstruction-recovery", + "PR #147 is merged", + "PR #147 merged unseen-reasoning verifier hardening", + ), + "candidate_only_gcp": ( + "teleo-reconstruction-recovery", + "PR #148 is a separate, open GCP least-privilege candidate", + "PR #148 open candidate only; not canonical main", + ), + "next_safe_action": ( + "teleo-leo-onboarding", + ".agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root .", + "run the repo-local path validator before any operational action", + ), +} + + +def _run(command: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run(command, check=False, capture_output=True, text=True) + + +def _contains_contract(text: str, required_text: str) -> bool: + """Compare prose semantically across ordinary Markdown line wrapping.""" + + return " ".join(required_text.split()) in " ".join(text.split()) + + +def run_canary(root: Path, manifest_path: Path) -> dict[str, object]: + manifest = json.loads((root / manifest_path).read_text(encoding="utf-8")) + installer = root / manifest["installer"] + selected_skills = sorted({contract[0] for contract in SEMANTIC_CONTRACT.values()}) + result: dict[str, object] + + with tempfile.TemporaryDirectory(prefix="teleo-clean-context-") as temporary_text: + temporary = Path(temporary_text) + installed_root = temporary / "skills" + install_process = _run( + [str(installer), "--root", str(root), "--manifest", str(manifest_path), "--target", str(installed_root)] + ) + try: + install_receipt = json.loads(install_process.stdout) + except json.JSONDecodeError: + install_receipt = {"status": "fail", "error": install_process.stdout or install_process.stderr} + + semantic_checks: dict[str, bool] = {} + answers: dict[str, str] = {} + evidence_paths: dict[str, str] = {} + for key, (skill_name, required_text, answer) in SEMANTIC_CONTRACT.items(): + installed_skill = installed_root / skill_name / "SKILL.md" + text = installed_skill.read_text(encoding="utf-8") if installed_skill.is_file() else "" + semantic_checks[key] = _contains_contract(text, required_text) + answers[key] = answer + evidence_paths[key] = f".agents/skills/{skill_name}/SKILL.md" + + installed_validator = installed_root / "teleo-leo-onboarding" / "scripts" / "validate_skill_pack.py" + validator_process = _run([str(installed_validator), "--root", str(root), "--manifest", str(manifest_path)]) + try: + validator_receipt = json.loads(validator_process.stdout) + except json.JSONDecodeError: + validator_receipt = {"status": "fail", "error": validator_process.stdout or validator_process.stderr} + + problems: list[dict[str, str]] = [] + if install_process.returncode != 0 or install_receipt.get("status") != "pass": + problems.append({"kind": "install_failed", "detail": str(install_receipt.get("error", "unknown"))}) + if validator_process.returncode != 0 or validator_receipt.get("status") != "pass": + problems.append({"kind": "path_validation_failed", "detail": str(validator_receipt.get("problems", []))}) + for key, passed in semantic_checks.items(): + if not passed: + problems.append({"kind": "semantic_route_missing", "detail": key}) + + passed = not problems + result = { + "artifact": "leo_teleo_skill_pack_isolated_install_canary", + "status": "pass" if passed else "fail", + "required_tier": "T2_runtime", + "current_tier": "T2_runtime" if passed else "T1_model", + "proof_scope": "deterministic_isolated_install_and_route_validation", + "fresh_temporary_skill_root": True, + "ambient_skill_root_used": False, + "agent_reasoning_exercised": False, + "installed_skill_count": install_receipt.get("installed_skill_count", 0), + "selected_installed_skills": selected_skills, + "semantic_checks": semantic_checks, + "expected_answers": answers, + "evidence_paths": evidence_paths, + "canary_command": ".agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root .", + "canary_exit_status": validator_process.returncode, + "canary_receipt": validator_receipt, + "missing_to_reach_required_tier": [] if passed else problems, + "strongest_claim_allowed": ( + "T2 deterministic isolated local skill install, route-contract, and path validation only; this artifact " + "does not claim an agent reasoning run, and retained product proofs keep their own VPS, GCP-staging, " + "isolated-clone, Telegram, and production claim ceilings" + ), + "contains_secrets": False, + "production_mutation_authorized": False, + "external_runtime_contacted": False, + "problems": problems, + } + + result["cleanup_readback"] = { + "temporary_skill_root_removed": not Path(temporary_text).exists(), + "orphan_processes_started": False, + } + if not result["cleanup_readback"]["temporary_skill_root_removed"]: + result["status"] = "fail" + result["current_tier"] = "T1_model" + result["problems"].append({"kind": "cleanup_failed", "detail": "temporary skill root remains"}) + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path.cwd()) + parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + + root = args.root.resolve() + result = run_canary(root, args.manifest) + payload = json.dumps(result, indent=2, sort_keys=True) + "\n" + if args.output: + output = args.output if args.output.is_absolute() else root / args.output + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(payload, encoding="utf-8") + sys.stdout.write(payload) + return 0 if result["status"] == "pass" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py b/.agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py new file mode 100755 index 0000000..8ecad61 --- /dev/null +++ b/.agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +"""Validate the repo-native Leo/Teleo skill pack and its local routes.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shlex +import subprocess +import sys +from pathlib import Path + +DEFAULT_MANIFEST = Path("docs/reports/leo-working-state-20260709/skill-pack-manifest.json") +REPO_PREFIXES = ( + ".agents/", + ".github/", + "deploy/", + "docs/", + "fixtures/", + "hermes-agent/", + "lib/", + "ops/", + "outputs/", + "schemas/", + "scripts/", + "systemd/", + "tests/", +) +PATH_SUFFIXES = (".json", ".jsonl", ".md", ".py", ".sh", ".sql", ".yaml", ".yml") +CODE_SPAN = re.compile(r"`([^`\n]+)`") +MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") +FENCED_SHELL = re.compile(r"```(?:bash|sh|shell)\n(.*?)```", re.DOTALL) + + +def _frontmatter(text: str) -> dict[str, str]: + if not text.startswith("---\n"): + return {} + try: + block = text.split("---\n", 2)[1] + except IndexError: + return {} + values: dict[str, str] = {} + for line in block.splitlines(): + if ":" not in line: + continue + key, value = line.split(":", 1) + values[key.strip()] = value.strip().strip('"') + return values + + +def _tracked_paths(root: Path) -> set[Path]: + result = subprocess.run( + ["git", "-C", str(root), "ls-files", "-z"], + check=False, + capture_output=True, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr.decode("utf-8", errors="replace")) + return {Path(item.decode("utf-8")) for item in result.stdout.split(b"\0") if item} + + +def _is_tracked(root: Path, path: Path, tracked: set[Path]) -> bool: + relative = path.resolve().relative_to(root.resolve()) + if path.is_file(): + return relative in tracked + prefix = f"{relative.as_posix().rstrip('/')}/" + return any(item.as_posix().startswith(prefix) for item in tracked) + + +def _clean_token(token: str) -> str: + token = token.strip().strip("'\"") + token = token.rstrip(",;.)]") + token = re.sub(r":\d+$", "", token) + return token + + +def _candidate_tokens(span: str) -> list[str]: + try: + tokens = shlex.split(span) + except ValueError: + tokens = span.split() + return [_clean_token(token) for token in tokens] + + +def _resolve_candidate(root: Path, source: Path, token: str) -> Path | None: + if not token or token in {".md", ".json", ".py", ".sh", ".sql"}: + return None + if any(marker in token for marker in ("*", "<", ">", "...", "${", "$LEDGER")): + return None + if token.startswith(("http://", "https://", "/", "~/")): + return None + if token.startswith(REPO_PREFIXES) or token in {"README.md", "CODEOWNERS"}: + candidate = root / token + elif token.endswith(PATH_SUFFIXES) and "/" in token: + candidate = source.parent / token + else: + return None + try: + candidate.resolve().relative_to(root.resolve()) + except ValueError: + return None + return candidate + + +def _inline_paths(root: Path, source: Path) -> set[Path]: + text = source.read_text(encoding="utf-8") + candidates: set[Path] = set() + spans = CODE_SPAN.findall(text) + MARKDOWN_LINK.findall(text) + for block in FENCED_SHELL.findall(text): + spans.extend(line for line in block.splitlines() if line.strip()) + for span in spans: + for token in _candidate_tokens(span): + candidate = _resolve_candidate(root, source, token) + if candidate is not None: + candidates.add(candidate) + return candidates + + +def validate(root: Path, manifest_path: Path) -> dict[str, object]: + problems: list[dict[str, str]] = [] + checked: set[Path] = set() + manifest_file = root / manifest_path + if not manifest_file.is_file(): + return { + "artifact": "leo_teleo_skill_pack_path_validation", + "status": "fail", + "problems": [{"kind": "missing_manifest", "path": str(manifest_path)}], + } + + manifest = json.loads(manifest_file.read_text(encoding="utf-8")) + tracked = _tracked_paths(root) + if not _is_tracked(root, manifest_file, tracked): + problems.append({"kind": "untracked_manifest", "path": str(manifest_path)}) + skills = manifest.get("skills", []) + skill_names = {entry.get("name") for entry in skills} + + for entry in skills: + relative = Path(entry["path"]) + path = root / relative + checked.add(path) + if not path.is_file(): + problems.append({"kind": "missing_skill", "path": str(relative)}) + continue + if not _is_tracked(root, path, tracked): + problems.append({"kind": "untracked_skill", "path": str(relative)}) + metadata = _frontmatter(path.read_text(encoding="utf-8")) + if metadata.get("name") != entry.get("name"): + problems.append({"kind": "skill_name_mismatch", "path": str(relative)}) + if not metadata.get("description"): + problems.append({"kind": "missing_skill_description", "path": str(relative)}) + if set(metadata) != {"name", "description"}: + problems.append({"kind": "invalid_skill_frontmatter_keys", "path": str(relative)}) + if not re.fullmatch(r"[a-z0-9-]{1,64}", metadata.get("name", "")): + problems.append({"kind": "invalid_skill_name", "path": str(relative)}) + if relative.parent.name != metadata.get("name"): + problems.append({"kind": "skill_folder_name_mismatch", "path": str(relative)}) + + required_coverage = manifest.get("required_coverage", {}) + for area, owners in required_coverage.items(): + for owner in owners: + if owner not in skill_names: + problems.append({"kind": "unknown_coverage_owner", "path": f"{area}:{owner}"}) + + reference_paths = list(manifest.get("reference_files", [])) + command_paths = list(manifest.get("command_files", [])) + scan_paths = list(manifest.get("scan_files", [])) + for relative_text in reference_paths + command_paths + scan_paths: + relative = Path(relative_text) + path = root / relative + checked.add(path) + if not path.is_file(): + problems.append({"kind": "missing_manifest_path", "path": str(relative)}) + elif not _is_tracked(root, path, tracked): + problems.append({"kind": "untracked_manifest_path", "path": str(relative)}) + + for relative_text in manifest.get("executable_files", []): + relative = Path(relative_text) + path = root / relative + checked.add(path) + if not path.is_file(): + problems.append({"kind": "missing_executable", "path": str(relative)}) + elif not os.access(path, os.X_OK): + problems.append({"kind": "not_executable", "path": str(relative)}) + + scan_sources = [root / entry["path"] for entry in skills] + scan_sources += [root / relative for relative in manifest.get("scan_files", [])] + for source in scan_sources: + if not source.is_file(): + continue + for candidate in _inline_paths(root, source): + checked.add(candidate) + if not candidate.exists(): + problems.append( + { + "kind": "missing_inline_path", + "path": str(candidate.resolve().relative_to(root.resolve())), + "source": str(source.relative_to(root)), + } + ) + elif not _is_tracked(root, candidate, tracked): + problems.append( + { + "kind": "untracked_inline_path", + "path": str(candidate.resolve().relative_to(root.resolve())), + "source": str(source.relative_to(root)), + } + ) + + relative_paths = sorted(str(path.resolve().relative_to(root.resolve())) for path in checked) + digest = hashlib.sha256("\n".join(relative_paths).encode("utf-8")).hexdigest() + return { + "artifact": "leo_teleo_skill_pack_path_validation", + "required_tier": "T2_runtime", + "status": "pass" if not problems else "fail", + "manifest": str(manifest_path), + "skill_count": len(skills), + "coverage_area_count": len(required_coverage), + "checked_path_count": len(relative_paths), + "checked_paths_sha256": digest, + "contains_secrets": False, + "production_mutation_authorized": False, + "problems": problems, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path.cwd()) + parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + + root = args.root.resolve() + result = validate(root, args.manifest) + payload = json.dumps(result, indent=2, sort_keys=True) + "\n" + if args.output: + output = args.output if args.output.is_absolute() else root / args.output + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(payload, encoding="utf-8") + sys.stdout.write(payload) + return 0 if result["status"] == "pass" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/teleo-reconstruction-recovery/SKILL.md b/.agents/skills/teleo-reconstruction-recovery/SKILL.md new file mode 100644 index 0000000..e7e5f71 --- /dev/null +++ b/.agents/skills/teleo-reconstruction-recovery/SKILL.md @@ -0,0 +1,83 @@ +--- +name: teleo-reconstruction-recovery +description: Use when restoring, rebuilding, recompiling, or incident-recovering Leo's Teleo knowledge database, including snapshot recovery, genesis-plus-ledger replay, source recompilation, parity verification, and honest recovery claim ceilings. +--- + +# Teleo Reconstruction Recovery + +## Job + +Choose the smallest recovery path that restores a usable Leo knowledge system +without confusing snapshot restoration, strict ledger replay, and semantic +source recompilation. + +## Authority + +Read `docs/kb-rebuild-and-recompile.md` first. It is the canonical recovery +contract. Use `docs/reports/leo-working-state-20260709/current-truth-index.md` +only for the newest retained proof point, and refresh live state before making a +current VPS or GCP claim. + +PR #146 is merged and owns the deterministic genesis-plus-ledger reconstruction +slice in `ops/run_local_genesis_ledger_rebuild.py`. PR #147 is merged and owns +the current unseen-reasoning verifier in +`scripts/verify_leo_unseen_reasoning_chain.py`. PR #148 is a separate, open GCP +least-privilege candidate; do not treat its branch files or proposed live state +as canonical `main` recovery instructions until it merges. + +## Select The Recovery Mode + +| Need | Path | Honest result | +|---|---|---| +| Restore the latest verified state quickly | `ops/run_local_canonical_postgres_rebuild.py` | Exact snapshot recovery | +| Prove strict post-genesis applies replay exactly | `ops/run_local_genesis_ledger_rebuild.py` | Isolated insert-only ledger replay | +| Turn one retained source into a reviewed packet | `scripts/compile_kb_source_packet.py` | Deterministic proposal packet, not canonical knowledge | +| Rebuild every row from original sources | Follow the semantic recompilation contract in `docs/kb-rebuild-and-recompile.md` | Partial until every row and receipt is accounted for | +| Diagnose a live incident before recovery | Use `.agents/skills/teleo-vps-runtime-ops/SKILL.md` and `.agents/skills/teleo-infra-provenance/SKILL.md` | Fresh read-only incident map | + +## Safe Order + +1. Identify the intended source database, snapshot, manifest, commit, and hashes. +2. Keep private dumps, row payloads, source excerpts, and replay material outside + the repository and mode `0600`. +3. Run the selected path in an isolated, network-disabled local container. +4. Verify schema, constraints, roles, counts, row hashes, key queries, and exact + cleanup. +5. Run the relevant focused tests, then the unseen-reasoning verifier when the + restored state is meant to support Leo answers. +6. Separate local reconstruction from VPS restore, GCP restore, service restart, + Telegram delivery, promotion, and production apply. Each is its own proof row. + +## Commands + +Use the repository virtual environment documented in `README.md`: + +```bash +.venv/bin/python ops/run_local_canonical_postgres_rebuild.py --help +.venv/bin/python ops/run_local_genesis_ledger_rebuild.py --help +.venv/bin/python scripts/compile_kb_source_packet.py --help +.venv/bin/python -m pytest -q \ + tests/test_run_local_canonical_postgres_rebuild.py \ + tests/test_run_local_genesis_ledger_rebuild.py \ + tests/test_verify_leo_unseen_reasoning_chain.py +``` + +The help and focused-test commands are safe local canaries. A real recovery +requires caller-supplied private material and must retain a sanitized receipt +without the material itself. + +## Stop Conditions + +Stop before any live restore or restart when the source snapshot is not +hash-bound, the source authority is ambiguous, parity fails, private material +would enter Git or logs, cleanup cannot be proved, or the requested operation +would promote GCP or mutate production without exact authorization. + +## Claim Ceiling + +- Exact snapshot recovery is working when its parity receipt passes. +- The merged genesis-plus-ledger path proves an isolated insert-only slice. +- Semantic source-to-blank-database recompilation remains incomplete until every + canonical row traces to a genesis record or reviewed replay receipt. +- No local receipt proves a live VPS/GCP restore, service restart, Telegram + delivery, production apply, or GCP promotion. diff --git a/.agents/skills/teleo-reconstruction-recovery/agents/openai.yaml b/.agents/skills/teleo-reconstruction-recovery/agents/openai.yaml new file mode 100644 index 0000000..9148b48 --- /dev/null +++ b/.agents/skills/teleo-reconstruction-recovery/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Teleo Reconstruction Recovery" + short_description: "Recover and reconstruct Leo knowledge safely" + default_prompt: "Use $teleo-reconstruction-recovery to choose and verify the safest Leo database recovery path." diff --git a/.agents/skills/working-leo-m3taversal-outcomes/SKILL.md b/.agents/skills/working-leo-m3taversal-outcomes/SKILL.md index bf3e3c4..ee69a52 100644 --- a/.agents/skills/working-leo-m3taversal-outcomes/SKILL.md +++ b/.agents/skills/working-leo-m3taversal-outcomes/SKILL.md @@ -62,11 +62,16 @@ A working Leo is a Telegram-facing agent that: ## Current Benchmark Cases -- Marker memory: `WL-LIVE-TG-CORY-20260709`. +- Historical marker-memory receipt: + `docs/reports/leo-working-state-20260709/telegram-live-canary-current.json`. - Truth correction: `approved != applied`. - Applied canary: proposal `00957f6c-9883-4015-95a4-6b09367efb0e` and edge `c167933e-d513-4f43-9335-d5d8aeb259f2`. - Staged write canary: proposal `8dfedb3f-3aa4-4200-970f-4c0016f6869f`, status `pending_review`, with no new public canonical rows after the test timestamp. -- Open-ended triage canary: `WL-LIVE-TG-CORY-OPEN-20260709`, where Leo inferred the likely failure mode from "agents not working / same state as last night" and explained the proposed, approved, and applied split without exact IDs. +- Open-ended triage receipt: + `docs/reports/leo-working-state-20260709/telegram-live-open-ended-suite-current.md`, + where Leo inferred the likely failure mode from "agents not working / same + state as last night" and explained the proposed, approved, and applied split + without exact IDs. - Old rich proposal packets: `14fa5ecc...`, `ac036c9d...`, and `a64df080...` are not to be silently production-applied. - Guarded apply proof: both generic and real Helmer v3 receipts pass `37/37` in disposable PostgreSQL; production Helmer remains unapplied. diff --git a/README.md b/README.md index 4a0f09c..e6bc31a 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,8 @@ A mirror sync (every 2 minutes) fast-forwards the PR onto Forgejo, where the pipeline picks it up. From there it's the same flow as agent-authored PRs — same tiers, same reviewers, same merge rules. -The contributor-facing guide lives in -[`teleo-codex/CONTRIBUTING.md`](https://github.com/living-ip/teleo-codex/blob/main/CONTRIBUTING.md). +The contributor-facing guide is the +[Teleo Codex contributing guide](https://github.com/living-ip/teleo-codex/blob/main/CONTRIBUTING.md). ## Repository layout @@ -119,10 +119,26 @@ status report in their Pentagon profile. ## Development ```bash -pip install -e ".[dev]" -pytest +python3 -m venv .venv +.venv/bin/pip install -e ".[dev]" +.venv/bin/python -m pytest ``` +## Leo / Teleo Operator Onboarding + +Start with the repo-native skill router, not historical chat or a runtime mirror: + +```bash +.agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root . +``` + +Then read `.agents/skills/teleo-leo-onboarding/SKILL.md` and +`docs/reports/leo-working-state-20260709/current-truth-index.md`. Recovery work +starts at `docs/kb-rebuild-and-recompile.md`. GitHub +`living-ip/teleo-infrastructure` is canonical code/deployment truth; VPS/GCP +runtime, canonical Postgres, proposal staging, Hermes memory, and retained +proofs are separate state surfaces. + ## Operations Production deployment runs on a single VPS. Runbook, restart procedures, diff --git a/docs/kb-rebuild-and-recompile.md b/docs/kb-rebuild-and-recompile.md index 2442702..2baf932 100644 --- a/docs/kb-rebuild-and-recompile.md +++ b/docs/kb-rebuild-and-recompile.md @@ -25,11 +25,12 @@ Run: --output /tmp/teleo-canonical-rebuild-receipt.json ``` -The newest retained 2026-07-14 post-V3 canary restored a fresh, network-isolated +The retained 2026-07-14 post-V3 canary restored a fresh, network-isolated Postgres and then removed it. The restored target matched the source across all 39 manifest tables and all 52,167 rows, with no schema, data, constraint, role, -or performance mismatch. Total runtime was 10.56 seconds, including 2.71 -seconds for `pg_restore`. Key rows included: +or performance mismatch. The same source snapshot was subsequently restored to +a disposable private-TLS GCP Cloud SQL clone with exact parity, a bounded +no-send reasoning turn, and verified cleanup. Key rows included: - 1,837 claims; - 4,145 sources; @@ -38,9 +39,10 @@ seconds for `pg_restore`. Key rows included: - 17 reasoning tools; - 29 proposals. -This snapshot includes the completed V3 source canary. It is ready as the -current restore input, but it has not yet been restored to GCP; current GCP -parity therefore remains unproven. +This snapshot includes the completed V3 source canary. Disposable GCP restore +parity is proven at that retained point. Persistent GCP `teleo_canonical` +remains the older staging copy, so ongoing parity, promotion, and production +cutover remain unproven. This is the fastest disaster-recovery path. It does not require Leo to re-extract or relearn the corpus. @@ -134,7 +136,7 @@ kept all database counts and the proposal payload hash unchanged, left the Leo gateway on the same PID with zero restarts, and removed the temporary private receipt. The full receipt is deliberately not committed because it can contain claim bodies or source excerpts; the sanitized proof is retained as -`reports/leo-working-state-20260709/kb-apply-replay-receipt-current.json`. +`docs/reports/leo-working-state-20260709/kb-apply-replay-receipt-current.json`. Normal applies mark the SQL hash as `exact_executed_sql`. A later `--receipt-only` recovery marks it as `reconstructed_current_engine`; it never diff --git a/docs/reports/leo-working-state-20260709/current-truth-index.md b/docs/reports/leo-working-state-20260709/current-truth-index.md index 03387e8..947ee1e 100644 --- a/docs/reports/leo-working-state-20260709/current-truth-index.md +++ b/docs/reports/leo-working-state-20260709/current-truth-index.md @@ -2,6 +2,30 @@ Use this file before making status claims. Prefer fresh VPS/GCP readbacks when cheap; this directory is the retained July 9 evidence snapshot committed to the Teleo infrastructure repo. +## Repository And Skill Routing Addendum - 2026-07-15 + +- PRs #146 and #147 are merged into canonical `main`. The reconstruction source + of truth is `docs/kb-rebuild-and-recompile.md`; the merged deterministic paths + are `ops/run_local_genesis_ledger_rebuild.py`, + `scripts/leo_turn_execution_manifest.py`, and + `scripts/verify_leo_unseen_reasoning_chain.py`. +- PR #148 remains open candidate evidence for least-privilege GCP Cloud SQL + runtime access. Do not use its branch-only runbook, deployment script, role + migration, or proposed live outcome as canonical `main` until the PR is merged + and its retained runtime receipt passes. +- The repo-native onboarding entry point is + `.agents/skills/teleo-leo-onboarding/SKILL.md`. Its standard-library validator + is `.agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py`; run it + before relying on any skill route from a clean context. +- The deterministic isolated install/routing/cleanup receipt is + `docs/reports/leo-working-state-20260709/skill-pack-clean-context-canary-current.json`. +- The separate no-history agent onboarding receipt is + `docs/reports/leo-working-state-20260709/skill-pack-clean-context-agent-review-current.json`. +- Exact snapshot restoration is working. The merged genesis-plus-ledger path is + an isolated insert-only recovery slice. Full semantic recompilation from every + original source remains incomplete. None of these local tiers proves a live + VPS/GCP restore, Telegram delivery, production apply, or GCP promotion. + ## GCP Database-First Addendum - 2026-07-14 10:07 UTC This addendum supersedes the GCP-pending statements in the earlier July 14 @@ -135,8 +159,8 @@ sections below override this newer status. - VPS canonical KB: claims `1837`, sources `4145`, evidence `4670`, edges `4916`, proposals `26`; proposal states are applied `2`, approved `3`, pending review `14`, canceled `7`. -- VPS direct Cory answers: no-send `DC-01` through `DC-06` strict-score `6/6`. -- Telegram direct Cory answers: partial. `DC-01` was visibly sent and its live +- VPS direct m3taversal-standard answers: no-send `DC-01` through `DC-06` strict-score `6/6`. +- Telegram direct m3taversal-standard answers: partial. `DC-01` was visibly sent and its live gateway reply strict-passed server-side, but the reply itself is not yet captured in Telegram. `DC-02` through `DC-06` are not yet sent. - DB composition/application: deterministic source-to-proposal, full-data clone @@ -146,7 +170,7 @@ sections below override this newer status. - GCP canonical DB: exact VPS parity is proven across `39` tables and `52,164` rows over private TLS, with zero row/catalog/role/extension/performance mismatches. Gateway PID `148735`, `NRestarts=0` stayed unchanged. -- GCP Cory replay: six DB-read routes pass. One real no-send model replay +- GCP m3taversal-standard replay: six DB-read routes pass. One real no-send model replay nominally scored `6/6`, but it was rejected because two replies printed false zero canonical counts. The harness now enables the required `status` read and rejects any printed count that differs from the canonical status receipt. A @@ -170,16 +194,16 @@ Newest artifacts: - `docs/reports/leo-working-state-20260709/gcp-canonical-parity-live-20260712.json` - `docs/reports/leo-working-state-20260709/teleo_clone_cory_20260712t1940z-canonical-parity-receipt.json` - `docs/reports/leo-working-state-20260709/telegram-visible-direct-claim-dc01-partial-current.json` -- `docs/reports/leo-working-state-20260709/gcp-cory-model-replay-current.json` (created only after the hardened rerun) +- `docs/reports/leo-working-state-20260709/gcp-db-first-blind-claim-current.json` - `docs/reports/leo-working-state-20260709/gcp-operator-access-blocker-current.json` ## Definition And Outcomes - Working Leo definition: `docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md` -- Cory expected outcomes: `docs/reports/leo-working-state-20260709/cory-expected-working-leo-outcomes-20260709.md` +- Legacy-named expected-outcomes evidence: `docs/reports/leo-working-state-20260709/cory-expected-working-leo-outcomes-20260709.md` - Current state: `docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md` - Execution plan: `docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md` -- Open-ended benchmark spec: `docs/reports/leo-working-state-20260709/working-leo-open-ended-benchmark-spec.json` (5 live-safe open prompts, 9 broader Cory-style outcome scenarios, and 6 no-context direct-claim follow-up prompts for disposable clone/sandbox first) +- Open-ended benchmark spec: `docs/reports/leo-working-state-20260709/working-leo-open-ended-benchmark-spec.json` (5 live-safe open prompts, 9 broader m3taversal-standard outcome scenarios, and 6 no-context direct-claim follow-up prompts for disposable clone/sandbox first) - Open-ended live Telegram canary: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-cory-style-20260709.md` - Open-ended live Telegram canary JSON: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-cory-style-20260709.json` - Retained Telegram benchmark score for the open-ended canary: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-benchmark-score-current.md` @@ -187,9 +211,9 @@ Newest artifacts: - Full live-safe open-ended Telegram suite report: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-suite-current.md` - Full live-safe open-ended Telegram suite score: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-suite-score-current.md` - Full live-safe open-ended Telegram suite score JSON: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-suite-score-current.json` -- Full Cory-style outcome/direct-claim sandbox results: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-results-current.md` -- Full Cory-style outcome/direct-claim sandbox score: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-score-current.md` -- Full Cory-style outcome sandbox score JSON: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-score-current.json` +- Full m3taversal-standard outcome/direct-claim sandbox results: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-results-current.md` +- Full m3taversal-standard outcome/direct-claim sandbox score: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-score-current.md` +- Full m3taversal-standard outcome sandbox score JSON: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-score-current.json` - Live VPS handler direct-claim suite report: `docs/reports/leo-working-state-20260709/telegram-handler-direct-claim-suite-current.json` - Live VPS handler direct-claim suite score: `docs/reports/leo-working-state-20260709/telegram-handler-direct-claim-suite-score-current.md` - Live VPS handler direct-claim suite review: `docs/reports/leo-working-state-20260709/telegram-handler-direct-claim-suite-review-current.md` @@ -311,9 +335,9 @@ Newest artifacts: - Skill-pack map: `docs/reports/leo-working-state-20260709/leo-db-state-13-skill-pack.svg` - Helmer packet proof: `docs/reports/leo-working-state-20260709/leo-db-state-14-helmer-packet.svg` -- Open-ended Cory-style canary map: `docs/reports/leo-working-state-20260709/leo-db-state-15-open-ended-canary.svg` +- Open-ended m3taversal-standard canary map: `docs/reports/leo-working-state-20260709/leo-db-state-15-open-ended-canary.svg` - Helmer ledger packet proof: `docs/reports/leo-working-state-20260709/leo-db-state-16-helmer-ledger.svg` -- Cory-style outcome benchmark map: `docs/reports/leo-working-state-20260709/leo-db-state-17-cory-outcome-benchmark.svg` +- m3taversal-standard outcome benchmark map: `docs/reports/leo-working-state-20260709/leo-db-state-17-cory-outcome-benchmark.svg` - Leoclean DB versus workspace map: `docs/reports/leo-working-state-20260709/leo-db-state-18-db-vs-workspace.svg` - Claim/body/metadata schema map: `docs/reports/leo-working-state-20260709/leo-db-state-19-claims-body-metadata.svg` - Cross-surface strategy anchor proof: `docs/reports/leo-working-state-20260709/leo-db-state-20-cross-surface-anchor.svg` @@ -331,7 +355,7 @@ Newest artifacts: - VPS Leo Telegram memory, KB audit, and staged-write canaries are live-proven. - VPS Leo live-safe open-ended Telegram suite is live-proven and retained-benchmark-scored for `OE-01` through `OE-05`: full selected coverage, `5/5` scored prompts passing, no overclaim, and no marker/staging/canonical KB rows created by the suite. -- The broader sandbox-first Cory-style benchmark has a full-catalog mixed-evidence score: real retained live Telegram evidence for `OE-01` through `OE-05`, sandbox fixture replies for `CS-01` through `CS-09`, and no-context direct-claim expected-answer/follow-up fixtures for `DC-01` through `DC-06`, `20/20` passing. This is not a live Telegram run for the CS/DC prompts. +- The broader sandbox-first m3taversal-standard benchmark has a full-catalog mixed-evidence score: real retained live Telegram evidence for `OE-01` through `OE-05`, sandbox fixture replies for `CS-01` through `CS-09`, and no-context direct-claim expected-answer/follow-up fixtures for `DC-01` through `DC-06`, `20/20` passing. This is not a live Telegram run for the CS/DC prompts. - After deploy commit `0183a5c805fa3b51d638267afd20f67a1bc3777f`, three fresh VPS GatewayRunner clean-session trials strict-scored `6/6` (`18/18` replies). Every trial posted nothing, made no live profile or production DB change, preserved counts (`1837/4145/4916/4670/26`), removed its temp profile, and left `leoclean-gateway.service` active/running with PID `2403328`, zero restarts, and the same start timestamp. Final cleanup found no matching temp directories. - Intentional restart survival is now live-proven as a separate required proof row, not implied by `NRestarts=0`: `systemctl restart leoclean-gateway.service` returned `0`, service moved from `MainPID=1908033` to `MainPID=2845730`, stayed active/running after restart and after the no-post handler smoke, canonical DB counts stayed unchanged (`1837/4145/4670/4916/26`), no Telegram post or production DB apply ran, no live profile change was recorded, temp profile cleanup succeeded, and the post-restart no-post `DC-01` through `DC-06` handler smoke returned six runtime replies. The retained pre-repair post-restart strict score was `5/6`; after deploying repair SHA `090becae1621436467610e529d6328d70964d11f`, the same no-post direct-claim suite strict-scored `6/6` with `DC-01` through `DC-06` all passing, DB counts unchanged, no Telegram post, no production DB apply, no live profile change, and cleanup confirmed. The raw handler JSON is retained on the VPS at `/tmp/leo-post-skill-repair-direct-claim-current.json` with SHA256 `6fffdbafc18913c4fc62feb00906988dee2e696d279f22dd284951a79451f39c`. - The Telegram-visible direct-claim benchmark was run in group `Leo` (`-5146042086`): all six prompts and replies are visible, and DB/service state is unchanged. The hardened semantic scorer gives `5/6`; `DC-05` incorrectly suggests applying the strict canary `add_edge` path to one of three legacy approved proposals whose payload types do not all match. @@ -364,7 +388,7 @@ Newest artifacts: - Latest cleanup readback found no leftover rehearsal/clone/staging database names on the VPS Postgres container and did not mutate production DB or post to Telegram. - A live Telegram regression canary should be rerun after any production apply; no production apply has been authorized or executed. - GCP control-plane Chrome readback now proves project `teleo-501523`, running VM `teleo-prod-1` in `europe-west6-a`, and one available private-only PostgreSQL `16.14` Cloud SQL instance, `teleo-pgvector-standby`, in `europe-west6-c`. Cloud SQL Studio is reachable but exposes no enabled existing IAM database principal and requires built-in password authentication; no credential was submitted and no SQL ran. Database-row/runtime/DC-01-through-DC-06 parity therefore remains unproven. The exact next gate is an already-existing authenticated Studio principal or a separately authorized IAM/database-user enablement window. -- GCP Console observability now separately proves `teleo-prod-1` is running and that serial output through `2026-07-10T15:57:54Z` contains recurring successful `leoclean-cloudsql-memory-sync.service` cycles. The latest exact `leoclean-gcp-prod-parallel.service` lifecycle event in the retained serial buffer is a Hermes start on July 9 with no later exact stop/failure. Cloud Logging has `92` guest/platform records over seven days but zero matches for `leoclean`, `hermes`, `gateway`, `postgres`, or `teleo-kb`, and Ops Agent is absent. This is bounded unit-activity evidence, not current PID/restart/SHA/profile/model/auth/Telegram/DB-row parity; no-post Cory execution remains unsafe and was not attempted. The agent-created Console tab was closed and no GCP/DB/service/IAM/deploy/credential/Telegram mutation occurred. +- GCP Console observability now separately proves `teleo-prod-1` is running and that serial output through `2026-07-10T15:57:54Z` contains recurring successful `leoclean-cloudsql-memory-sync.service` cycles. The latest exact `leoclean-gcp-prod-parallel.service` lifecycle event in the retained serial buffer is a Hermes start on July 9 with no later exact stop/failure. Cloud Logging has `92` guest/platform records over seven days but zero matches for `leoclean`, `hermes`, `gateway`, `postgres`, or `teleo-kb`, and Ops Agent is absent. This is bounded unit-activity evidence, not current PID/restart/SHA/profile/model/auth/Telegram/DB-row parity; no-post m3taversal-standard execution remains unsafe and was not attempted. The agent-created Console tab was closed and no GCP/DB/service/IAM/deploy/credential/Telegram mutation occurred. - PR #77 auto-deployed to the VPS checkout at merge `1a0abd882d00c1b58fe203f42de42d4eeea61cbf` without restarting Leo: the deploy journal says `No Python changes - services not restarted`, the gateway remained PID `2999690` with `NRestarts=0`, and the apply worker/timer remained disabled and inactive. This proves source synchronization and runtime non-change, not a GCP replay. - A passwordless GCP operator is implemented and locally validated but is not yet bootstrapped in project `teleo-501523`. It uses GitHub OIDC/Workload Identity Federation, separate status and clone-operation service accounts, IAP TCP forwarding, OS Login, five-minute SSH keys, and a root-owned dispatcher limited to `status`, `direct-claim-replay`, and `cleanup-clone`. It stores no Google password, local refresh token, service-account JSON key, or API key. One final authenticated GCP admin session is still required to install and verify the IAM/IAP/OS Login/dispatcher path; the old public `/32` rule must remain enabled until the workflow status artifact passes. - The broader GCP working-Leo runner now has a complete `20`-prompt catalog and requires `32` GCP-executable non-tautological composition checks plus lifecycle, role-separation, immutable-source, and secret-redaction gates. Local tests and validation-only execution pass, and an independent VPS source baseline is retained with counts `1837/4145/4670/4916/17/26`; live GCP execution remains unproven because the SHA-bound target-identity receipt, four separated database roles/credentials, and passwordless operator bootstrap are not yet present. This does not inherit or claim the VPS-only `34/34` result. diff --git a/docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md b/docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md index 7dbc69a..bfc4f09 100644 --- a/docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md +++ b/docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md @@ -1,18 +1,20 @@ # Fable / Worker Onboarding Prompt - Leo / Teleo -current_canary: read the retained evidence index, then produce a repo/VPS/GCP/Telegram working-state map with exact claim ceilings and next executable action. +current_canary: validate the repo-native skill routes, then produce a repo/VPS/GCP/Telegram working-state map with exact claim ceilings and next executable action. Use the Teleo repo skill pack at: `.agents/skills/` -Load these draft skills first: +Run `.agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root .`, +then load these manifest-indexed skills first: -1. `skills/teleo-leo-onboarding/SKILL.md` -2. `skills/working-leo-m3taversal-outcomes/SKILL.md` -3. `skills/teleo-kb-db-change-workflow/SKILL.md` -4. `skills/teleo-vps-runtime-ops/SKILL.md` -5. `skills/teleo-proof-handoff/SKILL.md` +1. `.agents/skills/teleo-leo-onboarding/SKILL.md` +2. `.agents/skills/working-leo-m3taversal-outcomes/SKILL.md` +3. `.agents/skills/teleo-kb-db-change-workflow/SKILL.md` +4. `.agents/skills/teleo-reconstruction-recovery/SKILL.md` +5. `.agents/skills/teleo-vps-runtime-ops/SKILL.md` +6. `.agents/skills/teleo-proof-handoff/SKILL.md` Hard constraints: diff --git a/docs/reports/leo-working-state-20260709/leo-architecture-work-and-outcomes-20260714.md b/docs/reports/leo-working-state-20260709/leo-architecture-work-and-outcomes-20260714.md index 864d9f1..d50313d 100644 --- a/docs/reports/leo-working-state-20260709/leo-architecture-work-and-outcomes-20260714.md +++ b/docs/reports/leo-working-state-20260709/leo-architecture-work-and-outcomes-20260714.md @@ -1,6 +1,7 @@ # Leo Architecture, Work, And Outcomes -Current as of `2026-07-14 01:40 UTC`. +Original delivery summary from `2026-07-14 01:40 UTC`, reconciled with merged +repository truth on 2026-07-15. This is the executive summary of what Leo is, how its knowledge system works, what changed during the July 10-14 delivery wave, what is genuinely working, @@ -17,10 +18,16 @@ managed gateway restart. The current broad no-post suite passes `15/15`. **The whole system is not complete.** The current broad suite has not yet been repeated visibly in the real Telegram group through authenticated Google Chrome. -The newest post-V3 database copy has not yet been restored and replayed on GCP. -No V3 production proposal has been canonically applied. A snapshot can rebuild -the current database exactly, but the entire historical database cannot yet be -recompiled from raw documents and review history alone. +The newest post-V3 database copy has since been restored to a disposable private +GCP Cloud SQL clone with exact parity and a bounded no-send reasoning replay, +then cleaned up. Persistent GCP remains staging and older than that disposable +proof point. No V3 production proposal has been canonically applied. A snapshot +can rebuild the current database exactly, and merged PR #146 adds an exact +insert-only genesis-plus-ledger slice; the entire historical database still +cannot be recompiled from raw documents and review history alone. + +Repository reconciliation: PRs #146 and #147 are merged `main` truth. PR #148 +remains an open least-privilege GCP candidate and is not canonical runtime proof. ## The Architecture @@ -255,7 +262,8 @@ left the gateway unchanged, and removed the private receipt afterward. | Grounded proposal staging | live VPS `pending_review` row | Achieved | | Exact snapshot rebuild | isolated local Postgres, `52,167` rows | Achieved | | New strict-apply row receipt | deployed VPS read-only recovery | Achieved | -| Prior GCP database copy and six-question replay | private Cloud SQL and GCP compute | Achieved at the earlier `52,164`-row proof point | +| Current disposable GCP copy and ID-free replay | private Cloud SQL and GCP compute, `52,167/52,167` rows | Achieved and cleaned up | +| Genesis plus strict insert-only replay | isolated local Postgres through merged PR #146 | Achieved for supported strict contracts | ## What Is Not Achieved @@ -263,9 +271,9 @@ left the gateway unchanged, and removed the private receipt afterward. | --- | --- | | Current broad Telegram-visible proof | Telegram Web in the connected Google Chrome is not authenticated; handler proof is not visible-chat proof | | V3 production canonical apply | proposal remains `pending_review`; no review or apply was authorized | -| Current GCP parity | the `52,167`-row post-V3 snapshot has not yet been restored and replayed on GCP | | Arbitrary automatic document/post/tweet intake | the current source command handles one bounded text-like artifact; general fetch/extract/stage automation is not complete | -| Full source-to-blank-DB recompilation | historical provenance gaps remain and the genesis-plus-ledger replay runner is not finished | +| Persistent GCP parity and promotion | the disposable `52,167`-row proof passed, but persistent `teleo_canonical` remains the older staging copy and is not cut over | +| Full source-to-blank-DB recompilation | historical provenance gaps and unsupported mutating contracts remain beyond the merged insert-only genesis-plus-ledger slice | | General DB-to-identity rendering | no complete scheduled DB-to-`SOUL.md` renderer is proven | | Deployed decision matrix | matrix tables and voting flow are absent from the current live schema | @@ -282,13 +290,13 @@ left the gateway unchanged, and removed the private receipt afterward. ## How This Moves GCP Forward -- The prior GCP vertical slice proved the architecture can run against private - Cloud SQL and real GCP compute. -- The current post-V3 dump and manifest are already captured and locally - verified, so GCP work starts from a known input rather than rebuilding one. -- The same `15/15` suite and exact V3 question are ready for replay. -- The remaining GCP work is operational: authenticate, restore the current copy, - verify parity/private connectivity/performance, replay, and clean up. +- The current post-V3 snapshot has been restored to a disposable private Cloud + SQL clone with exact parity, bounded no-send replay, and cleanup. +- Persistent `teleo_canonical` remains the older staging copy; promotion, + continuous replication, Telegram delivery, and production cutover are + separate decisions and proof rows. +- Open PR #148 proposes least-privilege runtime access. Its branch and proposed + live end state remain candidate evidence until merge and receipt verification. ## How This Moves On-Demand Rebuild Forward @@ -311,10 +319,11 @@ verified post-V3 snapshot = genesis epoch -> broad answer benchmark ``` -The new receipt path closes the future side of this model. The next repo-owned -step is to replay one reviewed `approve_claim` receipt onto a clean genesis clone -and prove exact post-apply parity. Historical import gaps can then be repaired -without allowing new gaps to accumulate. +The new receipt path closes the future side of this model. Merged PR #146 now +replays supported strict insert-only receipts on a clean genesis clone and proves +exact post-apply parity. The next reconstruction work is to retain complete +deltas for mutating contracts, handle or reject legacy freeform applies, and +repair historical import gaps without allowing new gaps to accumulate. ## Delivery Chain @@ -333,16 +342,21 @@ The latest coherent delivery sequence is merged: | `#135` | current post-V3 snapshot and exact rebuild proof | | `#136` | private replay receipts for guarded applies | | `#137` | deployed receipt runtime proof | +| `#144` | GCP database-first restore, fail-closed helper, and live restart proof | +| `#146` | unified turn manifest and deterministic genesis-plus-ledger replay | +| `#147` | unseen-reasoning verifier hardened for valid reasoning variance | + +PR #148 is open candidate work and is intentionally excluded from the merged +delivery chain above. ## Immediate Next Outcomes 1. Authenticate Telegram Web in the connected Google Chrome and run visible, out-of-sample m3taversal-style questions. -2. Authenticate Google Cloud CLI through the connected Chrome flow, restore the - current post-V3 snapshot to disposable GCP staging, verify parity, replay the - broad suite, and remove temporary resources. -3. Complete the genesis-plus-receipt replayer for `approve_claim` bundles and - prove exact clone-to-replay parity. +2. Review and merge or reject PR #148, then verify the least-privilege runtime + receipt before considering any persistent GCP promotion. +3. Extend genesis-plus-receipt reconstruction beyond supported insert-only + contracts while retaining exact before/after deltas. 4. Run one explicitly reviewed source packet through guarded apply in a disposable clone before seeking any production apply authorization. 5. Reconstruct the remaining historical source, evidence, and edge provenance. @@ -363,7 +377,8 @@ The latest coherent delivery sequence is merged: The last three days produced a stable, testable, database-grounded Leo on the VPS at the handler tier, a real source-to-review path, exact snapshot recovery, -and durable replay evidence for future strict applies. They did not complete -current Telegram-visible broad proof, current GCP parity, a V3 production apply, -general automatic source ingestion, or full historical source-derived database -recompilation. +and durable replay evidence for future strict applies. Subsequent work added +disposable current GCP parity plus no-send replay and an exact insert-only +genesis-plus-ledger slice. The system still lacks current Telegram-visible broad +proof, persistent GCP parity/promotion, a V3 production apply, general automatic +source ingestion, and full historical source-derived database recompilation. diff --git a/docs/reports/leo-working-state-20260709/operator-surface-map.md b/docs/reports/leo-working-state-20260709/operator-surface-map.md index f62247e..4e790ad 100644 --- a/docs/reports/leo-working-state-20260709/operator-surface-map.md +++ b/docs/reports/leo-working-state-20260709/operator-surface-map.md @@ -32,10 +32,19 @@ Always refresh before claiming current state. ## GCP - Project: `teleo-501523` -- Expected active account for project access: `billy@livingip.xyz` -- Current retained probe: local gcloud config still selects `billy@livingip.xyz` and `teleo-501523`, but this runner cannot resolve `oauth2.googleapis.com`, so parity is blocked before token refresh/project access can be rechecked. -- Current artifact: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-probe-current.json` -- Prior auth-capable artifact: `outputs/gcp-db-parity-current-blocker-20260709.json` +- Compute: `teleo-prod-1` in `europe-west6-a`. +- Private Cloud SQL: `teleo-pgvector-standby`, database `teleo_canonical`. +- Direct passwordless alias `ssh teleo-gcp-staging` and `sudo -n` were retained as + working on 2026-07-14; always re-run the batch-mode preflight before relying on + them. +- A disposable private-TLS clone matched the current VPS snapshot at `39` tables + and `52,167` rows, then was deleted. Persistent `teleo_canonical` remains the + older staging copy and is not promoted or cut over. +- Current artifacts: + `docs/reports/leo-working-state-20260709/gcp-db-first-working-leo-20260714.md` + and `docs/reports/leo-working-state-20260709/gcp-db-first-parity-current.json`. +- PR #148 is open candidate evidence for scoped runtime access; its branch and + proposed live outcome are not canonical `main`. ## Telegram diff --git a/docs/reports/leo-working-state-20260709/skill-pack-clean-context-agent-review-current.json b/docs/reports/leo-working-state-20260709/skill-pack-clean-context-agent-review-current.json new file mode 100644 index 0000000..2f9fddd --- /dev/null +++ b/docs/reports/leo-working-state-20260709/skill-pack-clean-context-agent-review-current.json @@ -0,0 +1,106 @@ +{ + "artifact": "leo_teleo_skill_pack_fresh_agent_review", + "status": "pass", + "required_tier": "T2_runtime", + "current_tier": "T2_runtime", + "proof_scope": "fresh_context_agent_onboarding_and_stale_claim_rejection", + "evidence_origin": { + "agent": "/root/fresh_skill_canary", + "fork_turns": "none", + "onboarding_authority": "isolated installed skill pack only", + "memory_used": false + }, + "answers": { + "canonical_code_deployment_repository_authority": { + "repository": "GitHub living-ip/teleo-infrastructure", + "canonical_branch": "main", + "vps_deploy_source_mirror": "/opt/teleo-eval/workspaces/deploy-infra", + "mirror_is_canonical_authority": false + }, + "vps_canonical_database": { + "container": "teleo-pg", + "database": "teleo" + }, + "live_vps_gateway_service": "leoclean-gateway.service", + "persistent_gcp_database": { + "instance": "teleo-pgvector-standby", + "database": "teleo_canonical", + "status": "older staging copy", + "retained_measurement": { + "tables": 39, + "rows": 52164, + "proposals": 26 + }, + "promoted": false, + "cut_over": false + }, + "pull_requests": { + "146": "merged into canonical main", + "147": "merged into canonical main", + "148": "open least-privilege GCP candidate; not canonical main" + }, + "approved_proposals_equal_applied_proposals": { + "equal": false, + "retained_vps_proposal_counts": { + "total": 29, + "applied": 2, + "approved": 3, + "pending_review": 16, + "canceled": 8 + }, + "meaning": "Approved proposals remain staged until canonical application and row-level proof." + }, + "first_safe_local_validation_command": ".agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root ." + }, + "stale_claims_rejected": { + "pr_148_is_merged": true, + "persistent_gcp_is_promoted": true, + "all_approved_proposals_are_applied": true + }, + "validator": { + "command": "/private/tmp/teleo-clean-agent-final-20260715/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root /private/tmp/teleo-fleet-20260715/skills-docs-onboarding", + "exit_code": 0, + "status": "pass", + "checked_path_count": 233, + "checked_paths_sha256": "ae919c3e7d8cef19a1782a00c640eeeb66c49dfbce3ad9ceaf30eb96e77bda16", + "coverage_area_count": 13, + "skill_count": 16, + "production_mutation_authorized": false, + "contains_secrets": false, + "problems": [] + }, + "repository_readback": { + "status_before": [ + " M .agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py", + " M .agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py", + " M tests/test_repo_skill_pack.py", + "?? goal.md" + ], + "status_after": [ + " M .agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py", + " M .agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py", + " M tests/test_repo_skill_pack.py", + "?? goal.md" + ], + "unchanged": true + }, + "files_read": [ + ".agents/skills/teleo-leo-onboarding/SKILL.md", + ".agents/skills/teleo-infra-provenance/SKILL.md", + "docs/reports/leo-working-state-20260709/current-truth-index.md", + "docs/reports/leo-working-state-20260709/operator-surface-map.md", + "docs/reports/leo-working-state-20260709/gcp-db-first-working-leo-20260714.md", + "docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md", + "docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md" + ], + "cleanup_readback": { + "temporary_skill_root_removed": true, + "matching_temporary_roots_remaining": 0, + "orphan_processes_started": false + }, + "external_runtime_contacted": false, + "production_mutation": false, + "contains_secrets": false, + "problems": [], + "strongest_claim_allowed": "T2 fresh-context local onboarding and route validation only; this does not prove live VPS, GCP promotion, Telegram delivery, or production proposal application" +} diff --git a/docs/reports/leo-working-state-20260709/skill-pack-clean-context-canary-current.json b/docs/reports/leo-working-state-20260709/skill-pack-clean-context-canary-current.json new file mode 100644 index 0000000..f5c23e5 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/skill-pack-clean-context-canary-current.json @@ -0,0 +1,71 @@ +{ + "agent_reasoning_exercised": false, + "ambient_skill_root_used": false, + "artifact": "leo_teleo_skill_pack_isolated_install_canary", + "canary_command": ".agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root .", + "canary_exit_status": 0, + "canary_receipt": { + "artifact": "leo_teleo_skill_pack_path_validation", + "checked_path_count": 234, + "checked_paths_sha256": "19ed1813f9cb4c19489a180081007842e241a2dcae659a5d873582fbbd50cff6", + "contains_secrets": false, + "coverage_area_count": 13, + "manifest": "docs/reports/leo-working-state-20260709/skill-pack-manifest.json", + "problems": [], + "production_mutation_authorized": false, + "required_tier": "T2_runtime", + "skill_count": 16, + "status": "pass" + }, + "cleanup_readback": { + "orphan_processes_started": false, + "temporary_skill_root_removed": true + }, + "contains_secrets": false, + "current_tier": "T2_runtime", + "evidence_paths": { + "authority": ".agents/skills/teleo-infra-provenance/SKILL.md", + "candidate_only_gcp": ".agents/skills/teleo-reconstruction-recovery/SKILL.md", + "gcp_database": ".agents/skills/teleo-leo-onboarding/SKILL.md", + "live_service": ".agents/skills/teleo-infra-provenance/SKILL.md", + "merged_reasoning_verifier": ".agents/skills/teleo-reconstruction-recovery/SKILL.md", + "merged_reconstruction": ".agents/skills/teleo-reconstruction-recovery/SKILL.md", + "next_safe_action": ".agents/skills/teleo-leo-onboarding/SKILL.md", + "vps_database": ".agents/skills/teleo-leo-onboarding/SKILL.md" + }, + "expected_answers": { + "authority": "GitHub living-ip/teleo-infrastructure", + "candidate_only_gcp": "PR #148 open candidate only; not canonical main", + "gcp_database": "private Cloud SQL teleo_canonical; persistent copy remains staging", + "live_service": "leoclean-gateway.service", + "merged_reasoning_verifier": "PR #147 merged unseen-reasoning verifier hardening", + "merged_reconstruction": "PR #146 merged deterministic genesis-plus-ledger reconstruction", + "next_safe_action": "run the repo-local path validator before any operational action", + "vps_database": "teleo-pg / teleo" + }, + "external_runtime_contacted": false, + "fresh_temporary_skill_root": true, + "installed_skill_count": 16, + "missing_to_reach_required_tier": [], + "problems": [], + "production_mutation_authorized": false, + "proof_scope": "deterministic_isolated_install_and_route_validation", + "required_tier": "T2_runtime", + "selected_installed_skills": [ + "teleo-infra-provenance", + "teleo-leo-onboarding", + "teleo-reconstruction-recovery" + ], + "semantic_checks": { + "authority": true, + "candidate_only_gcp": true, + "gcp_database": true, + "live_service": true, + "merged_reasoning_verifier": true, + "merged_reconstruction": true, + "next_safe_action": true, + "vps_database": true + }, + "status": "pass", + "strongest_claim_allowed": "T2 deterministic isolated local skill install, route-contract, and path validation only; this artifact does not claim an agent reasoning run, and retained product proofs keep their own VPS, GCP-staging, isolated-clone, Telegram, and production claim ceilings" +} diff --git a/docs/reports/leo-working-state-20260709/skill-pack-manifest.json b/docs/reports/leo-working-state-20260709/skill-pack-manifest.json index 613a99e..36de40a 100644 --- a/docs/reports/leo-working-state-20260709/skill-pack-manifest.json +++ b/docs/reports/leo-working-state-20260709/skill-pack-manifest.json @@ -1,39 +1,51 @@ { "pack": "leo-teleo-skill-pack", "created_date": "2026-07-09", - "last_updated_utc": "2026-07-13T06:45:00Z", - "status": "repo_native_validated", + "last_updated_utc": "2026-07-15T00:16:53Z", + "status": "repo_native_path_validated", "contains_secrets": false, "production_mutation_authorized": false, "repo_skill_root": ".agents/skills", "optional_local_install_target": "/Users/user/.codex/skills", - "claim_ceiling": "Repo-native onboarding and operations skills. Generic and Helmer guarded-apply canaries pass 37/37 in disposable PostgreSQL. PR #72 source auto-synchronized to the VPS checkout without a gateway restart or profile-source delta; the permission migration was not applied, the apply worker remained disabled/inactive, and Helmer remained unapplied. Current source-composition/apply lifecycle proof is stronger and remains isolated. GCP gateway and exact canonical DB parity are proven across 39 tables and 52,164 rows, and the hardened adapter-free model replay passes 6/6 with exact count consistency. Durable GCP operator identity and retained clone cleanup remain open. Broad blind VPS reasoning is not reliable yet: independent strict judges accepted only 1/12 and 2/12 outright. Current skills require exact m3taversal naming, neutral follow-up labels, and current-v1 schema truth.", + "claim_ceiling": "Repo-native onboarding and operations skills at T2 clean-context local runtime. PRs #146 and #147 are merged and own deterministic reconstruction plus unseen-reasoning verification. PR #148 is open candidate evidence only. Generic and Helmer guarded-apply canaries pass 37/37 in disposable PostgreSQL, while production rich packets remain unapplied. The latest disposable GCP clone matched 39 tables and 52,167 rows and a no-send model turn passed 18/18 runtime checks plus 6/6 reasoning outcomes; persistent GCP teleo_canonical remains staging and no promotion, production apply, or current Telegram-visible end-to-end proof is claimed.", + "repository_reconciliation": { + "merged_pull_requests": [146, 147], + "candidate_only_pull_requests": [148], + "canonical_reconstruction_contract": "docs/kb-rebuild-and-recompile.md", + "canonical_turn_manifest": "scripts/leo_turn_execution_manifest.py", + "canonical_unseen_reasoning_verifier": "scripts/verify_leo_unseen_reasoning_chain.py" + }, "skills": [ { "name": "teleo-leo-onboarding", - "role": "Company/product/architecture orientation for Leo and Teleo work", + "role": "Company, product, architecture, task routing, and clean-context entry point", "path": ".agents/skills/teleo-leo-onboarding/SKILL.md" }, { "name": "working-leo-m3taversal-outcomes", - "role": "Definition of working Leo from m3taversal outcomes and tests", + "role": "Definition of working Leo, identity discipline, behavior, and proof ceiling", "path": ".agents/skills/working-leo-m3taversal-outcomes/SKILL.md" }, { "name": "teleo-vps-runtime-ops", - "role": "VPS runtime navigation, service health, Docker/Postgres checks, report sync", + "role": "VPS runtime navigation, incidents, service health, Postgres, and cleanup", "path": ".agents/skills/teleo-vps-runtime-ops/SKILL.md" }, { "name": "teleo-gcp-parity-ops", - "role": "GCP operator access, private Cloud SQL parity, m3taversal replay, rollback, and cleanup", + "role": "GCP access, private Cloud SQL parity, no-send replay, rollback, and cleanup", "path": ".agents/skills/teleo-gcp-parity-ops/SKILL.md" }, { "name": "teleo-kb-db-change-workflow", - "role": "Strict proposal normalization, separated review/apply authority, isolated canary, and row-level proof", + "role": "Source ingestion, proposal normalization, review, guarded apply, and receipts", "path": ".agents/skills/teleo-kb-db-change-workflow/SKILL.md" }, + { + "name": "teleo-reconstruction-recovery", + "role": "Snapshot recovery, genesis-ledger replay, source recompilation, and incident recovery", + "path": ".agents/skills/teleo-reconstruction-recovery/SKILL.md" + }, { "name": "leo-telegram-canary-ops", "role": "Live Telegram bot testing through authenticated Chrome and Computer Use", @@ -41,29 +53,121 @@ }, { "name": "teleo-infra-provenance", - "role": "Repo/deploy/runtime provenance and path confusion prevention", + "role": "Repository, deploy, runtime, database, and retained-proof provenance", "path": ".agents/skills/teleo-infra-provenance/SKILL.md" }, { "name": "teleo-proof-handoff", - "role": "Evidence-indexed handoff format for Fable/Codex/Opus workers", + "role": "Capability tiers, retained evidence, cleanup, and worker handoff", "path": ".agents/skills/teleo-proof-handoff/SKILL.md" + }, + { + "name": "nousresearch-hermes-agent", + "role": "Hermes agent packaging, skills, prompts, memory, model routing, and no-secret policy", + "path": ".agents/skills/nousresearch-hermes-agent/SKILL.md" + }, + { + "name": "teleo-db-operator", + "role": "Read-first Teleo pipeline SQLite discovery, audit, backup, and guarded writes", + "path": ".agents/skills/teleo-db-operator/SKILL.md" + }, + { + "name": "private-password-storage", + "role": "Device-local credential presence and secure-input handling without chat disclosure", + "path": ".agents/skills/private-password-storage/SKILL.md" + }, + { + "name": "crabbox", + "role": "Isolated remote Linux and CI proof without production deployment", + "path": ".agents/skills/crabbox/SKILL.md" + }, + { + "name": "decision-engine-refinement", + "role": "Replayable model, evaluator, rubric, and decision-engine quality work", + "path": ".agents/skills/decision-engine-refinement/SKILL.md" + }, + { + "name": "living-ip-kb-interop", + "role": "Safe external-agent knowledge read and propose-first writeback contracts", + "path": ".agents/skills/living-ip-kb-interop/SKILL.md" + }, + { + "name": "openclaw-agent", + "role": "No-secret Living IP agent packaging for OpenClaw workspaces", + "path": ".agents/skills/openclaw-agent/SKILL.md" } ], + "required_coverage": { + "company_product_context": ["teleo-leo-onboarding"], + "vps": ["teleo-vps-runtime-ops"], + "gcp": ["teleo-gcp-parity-ops"], + "hermes_runtime": ["nousresearch-hermes-agent", "teleo-vps-runtime-ops"], + "database_provenance": ["teleo-infra-provenance", "teleo-db-operator"], + "ingestion": ["teleo-kb-db-change-workflow", "living-ip-kb-interop"], + "proposal_apply": ["teleo-kb-db-change-workflow"], + "reconstruction": ["teleo-reconstruction-recovery"], + "identity": ["teleo-leo-onboarding", "working-leo-m3taversal-outcomes"], + "testing": ["teleo-leo-onboarding", "teleo-proof-handoff", "crabbox", "decision-engine-refinement"], + "security": ["teleo-vps-runtime-ops", "teleo-gcp-parity-ops", "private-password-storage"], + "secrets": ["private-password-storage"], + "incident_recovery": ["teleo-reconstruction-recovery", "teleo-vps-runtime-ops"] + }, "reference_files": [ + "docs/reports/leo-working-state-20260709/skill-pack-clean-context-agent-review-current.json", + "docs/reports/leo-working-state-20260709/skill-pack-clean-context-canary-current.json", "docs/reports/leo-working-state-20260709/current-truth-index.md", "docs/reports/leo-working-state-20260709/operator-surface-map.md", "docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md", + "docs/reports/leo-working-state-20260709/gcp-db-first-working-leo-20260714.md", + "docs/reports/leo-working-state-20260709/gcp-db-first-parity-current.json", + "docs/reports/leo-working-state-20260709/gcp-db-first-blind-claim-current.json", + "docs/reports/leo-working-state-20260709/gcp-db-first-cleanup-current.json", + "docs/reports/leo-working-state-20260709/gcp-db-first-live-deploy-restart-current.json", "docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md", - "docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md", - "docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md", - "docs/reports/leo-working-state-20260709/pr72-vps-auto-deploy-runtime-nonchange-current.md", - "docs/reports/leo-working-state-20260709/gcp-leo-runtime-observability-current.md", - "docs/reports/leo-working-state-20260709/gcp-canonical-parity-live-20260712.json", - "docs/reports/leo-working-state-20260709/gcp-operator-access-blocker-current.json", - "docs/reports/leo-working-state-20260709/telegram-visible-direct-claim-dc01-partial-current.json", "docs/reports/leo-working-state-20260709/working-leo-current-proof-20260712.md", - "docs/reports/leo-working-state-20260709/working-leo-current-proof-20260712.json" + "docs/reports/leo-working-state-20260709/leo-unified-turn-manifest-canary-current.json", + "docs/reports/leo-working-state-20260709/leo-architecture-work-and-outcomes-20260714.md", + "docs/kb-rebuild-and-recompile.md" ], - "validation": "tests/test_repo_skill_pack.py" + "command_files": [ + ".agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py", + ".agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py", + ".agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py", + "ops/run_local_canonical_postgres_rebuild.py", + "ops/run_local_genesis_ledger_rebuild.py", + "ops/capture_vps_canonical_postgres_snapshot.py", + "ops/restore_gcp_generated_postgres_snapshot.py", + "ops/verify_postgres_parity_manifest.py", + "scripts/compile_kb_source_packet.py", + "scripts/approve_proposal.py", + "scripts/apply_proposal.py", + "scripts/leo_turn_execution_manifest.py", + "scripts/verify_leo_unseen_reasoning_chain.py", + "tests/test_repo_skill_pack.py" + ], + "scan_files": [ + "docs/reports/leo-working-state-20260709/skill-pack-readme.md", + "docs/reports/leo-working-state-20260709/current-truth-index.md", + "docs/reports/leo-working-state-20260709/operator-surface-map.md", + "docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md", + "docs/reports/leo-working-state-20260709/leo-architecture-work-and-outcomes-20260714.md", + "README.md", + "docs/kb-rebuild-and-recompile.md" + ], + "executable_files": [ + ".agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py", + ".agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py", + ".agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py", + ".agents/skills/private-password-storage/scripts/private-password-storage" + ], + "validator": ".agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py", + "installer": ".agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py", + "isolated_install_canary": ".agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py", + "fresh_agent_receipt": "docs/reports/leo-working-state-20260709/skill-pack-clean-context-agent-review-current.json", + "validation": "tests/test_repo_skill_pack.py", + "validation_commands": [ + ".agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root .", + ".agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py --root .", + ".venv/bin/python -m pytest -q tests/test_repo_skill_pack.py" + ] } diff --git a/docs/reports/leo-working-state-20260709/skill-pack-readme.md b/docs/reports/leo-working-state-20260709/skill-pack-readme.md index 6c8f299..d655049 100644 --- a/docs/reports/leo-working-state-20260709/skill-pack-readme.md +++ b/docs/reports/leo-working-state-20260709/skill-pack-readme.md @@ -1,42 +1,91 @@ -# Leo / Teleo Skill Pack - 2026-07-09 +# Leo / Teleo Skill Pack - 2026-07-15 -Purpose: reusable onboarding and operating knowledge for future Codex/Fable workers working on Leo, Teleo, the VPS, GCP parity, Telegram canaries, and KB database changes. +Purpose: give a new engineer or agent one repo-native route into Leo/Teleo +architecture, operations, database lifecycle, proof tiers, secrets, and recovery +without relying on historical chat or unmerged branch prose. -This is the repo-native skill set under `.agents/skills/`. It is evidence-linked and validated by `tests/test_repo_skill_pack.py`. It does not contain secrets, private key contents, bot tokens, or production mutation authorization. +The pack lives under `.agents/skills/`, contains no credentials, and grants no +production database mutation or GCP promotion authority. + +## Clean-Context Entry Point + +From the repository root: + +```bash +.agents/skills/teleo-leo-onboarding/scripts/validate_skill_pack.py --root . +.agents/skills/teleo-leo-onboarding/scripts/install_skill_pack.py \ + --root . \ + --target /private/tmp/teleo-clean-agent/skills +.agents/skills/teleo-leo-onboarding/scripts/run_clean_context_canary.py --root . +``` + +The validator uses standard-library Python through its executable shebang. For +pytest, create the repository virtual environment from `README.md` and use +`.venv/bin/python`; do not assume a bare `python` executable exists. ## Skills In This Pack -1. `teleo-leo-onboarding`: company/product/architecture orientation before touching Leo or Teleo. -2. `working-leo-m3taversal-outcomes`: m3taversal's expected Leo behavior and the current benchmark. -3. `teleo-vps-runtime-ops`: VPS service, paths, Postgres, Docker, report sync, and stability checks. -4. `teleo-gcp-parity-ops`: passwordless GCP access, Cloud SQL parity, no-send replay, rollback, and cleanup. -5. `teleo-kb-db-change-workflow`: source composition plus approved proposal to canonical-row workflow with clone rehearsal and rollback. -6. `leo-telegram-canary-ops`: live Telegram/Chrome/Computer Use canaries for Leo bot behavior. -7. `teleo-infra-provenance`: repo, deploy, mirror, and runtime provenance map. -8. `teleo-proof-handoff`: evidence bundle and Fable/Codex worker handoff protocol. +1. `teleo-leo-onboarding`: product, architecture, task router, and first canary. +2. `working-leo-m3taversal-outcomes`: exact identity discipline and working-Leo proof ceiling. +3. `teleo-vps-runtime-ops`: VPS services, Postgres, incidents, and cleanup. +4. `teleo-gcp-parity-ops`: GCP/Cloud SQL parity, no-send replay, and rollback. +5. `teleo-kb-db-change-workflow`: source ingestion through guarded proposal/apply. +6. `teleo-reconstruction-recovery`: snapshot, ledger, and semantic recovery modes. +7. `leo-telegram-canary-ops`: authorized Leo-specific Telegram canaries. +8. `teleo-infra-provenance`: repository, deploy, runtime, DB, and proof provenance. +9. `teleo-proof-handoff`: tiered evidence and continuation handoffs. +10. `nousresearch-hermes-agent`: Hermes packaging and runtime boundaries. +11. `teleo-db-operator`: read-first pipeline SQLite operation. +12. `private-password-storage`: device-local secure input and presence checks. +13. `crabbox`: isolated remote Linux and CI proof. +14. `decision-engine-refinement`: replayable evaluator and model-quality work. +15. `living-ip-kb-interop`: propose-first external-agent KB access. +16. `openclaw-agent`: no-secret OpenClaw workspace packaging. + +The machine-readable topic coverage and exact paths are in +`docs/reports/leo-working-state-20260709/skill-pack-manifest.json`. + +## Repository Reconciliation + +- PR #146 is merged: deterministic turn manifests and the first exact + genesis-plus-ledger reconstruction slice are canonical `main` behavior. +- PR #147 is merged: the unseen-reasoning verifier accepts valid reasoning + variance while keeping evidence and receipt gates. +- PR #148 is open candidate evidence only. Its proposed least-privilege GCP + files and live end state are not canonical until merged and receipt-verified. ## Current Claim Ceiling -- VPS Leo Telegram memory, KB audit, and staged write canaries are live-proven. -- A strict canonical `add_edge` apply canary is live-proven. -- Deterministic source composition, full-data clone composition/reasoning, and guarded approved-bundle application are isolated-proven with exact source/evidence/claim links, row deltas, rollback, and cleanup. Production rich packets remain unapplied. -- Narrow VPS DB truth and restart behavior are proven. Broad blind reasoning is not yet reliable; exact participant naming, neutral labels, and current-v1 schema guards are now required. -- GCP gateway liveness, private Cloud SQL identity, exact VPS-to-GCP canonical parity, and hardened adapter-free model replay are live-proven. The database matches across `39` tables and `52,164` rows. Durable operator identity and retained replay-clone cleanup remain open because the intended target service accounts are absent. +- T2 clean-context local skill install, route validation, and focused tests are + the onboarding target for this pack. +- VPS no-send behavior, isolated proposal/apply, snapshot recovery, disposable + GCP parity, and bounded no-send GCP reasoning have retained evidence. +- Persistent GCP `teleo_canonical` remains staging. No GCP promotion or cutover + is authorized or claimed. +- Production rich packets remain unapplied. A current Telegram-visible complete + flow is not proven by this pack. ## Key References -- Current whole-system proof: `docs/reports/leo-working-state-20260709/working-leo-current-proof-20260712.md` and `.json` -- Current truth index: `docs/reports/leo-working-state-20260709/current-truth-index.md` -- Operator surface map: `docs/reports/leo-working-state-20260709/operator-surface-map.md` -- Fable onboarding prompt: `docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md` -- PR #72 VPS auto-deploy/runtime readback: `docs/reports/leo-working-state-20260709/pr72-vps-auto-deploy-runtime-nonchange-current.md` -- GCP canonical parity: `docs/reports/leo-working-state-20260709/gcp-canonical-parity-live-20260712.json` -- GCP model replay (legacy artifact filename): `docs/reports/leo-working-state-20260709/gcp-cory-model-replay-current.json` -- Three-day delivery delta: `docs/reports/leo-working-state-20260709/working-leo-three-day-delivery-summary-20260713.md` -- Blind out-of-sample audit: `docs/reports/leo-working-state-20260709/telegram-handler-blind-oos-audit-current.json` -- GCP operator access gate: `docs/reports/leo-working-state-20260709/gcp-operator-access-blocker-current.json` -- Skill manifest: `skill-pack-manifest.json` +- Fresh-agent onboarding receipt: `docs/reports/leo-working-state-20260709/skill-pack-clean-context-agent-review-current.json` +- Isolated-install acceptance receipt: `docs/reports/leo-working-state-20260709/skill-pack-clean-context-canary-current.json` +- Current truth: `docs/reports/leo-working-state-20260709/current-truth-index.md` +- Architecture/outcomes: `docs/reports/leo-working-state-20260709/leo-architecture-work-and-outcomes-20260714.md` +- Recovery contract: `docs/kb-rebuild-and-recompile.md` +- GCP database-first proof: `docs/reports/leo-working-state-20260709/gcp-db-first-working-leo-20260714.md` +- GCP no-send reasoning receipt: `docs/reports/leo-working-state-20260709/gcp-db-first-blind-claim-current.json` +- Unified turn receipt: `docs/reports/leo-working-state-20260709/leo-unified-turn-manifest-canary-current.json` +- Operator map: `docs/reports/leo-working-state-20260709/operator-surface-map.md` -## Discovery +## Team Change Notes -Codex-compatible workers should discover the committed `.agents/skills/` tree from this repository. The manifest is the index; each skill points to current retained evidence rather than embedding secrets or pretending a stale snapshot is live truth. +- Replaced the missing legacy GCP replay route with the current database-first + no-send receipt. +- Added deterministic install and path-validation commands. +- Added separate T2 receipts for deterministic isolated install/path validation + and fresh-context agent reasoning, stale-claim rejection, and cleanup. +- Added an explicit topic router and recovery skill. +- Added merged-vs-open PR boundaries so branch candidates cannot silently become + source-of-truth instructions. +- Added tests for coverage, route existence, installer behavior, and exact + m3taversal identity prose. diff --git a/tests/test_repo_skill_pack.py b/tests/test_repo_skill_pack.py index c50d6ae..b218b8f 100644 --- a/tests/test_repo_skill_pack.py +++ b/tests/test_repo_skill_pack.py @@ -1,6 +1,8 @@ from __future__ import annotations import json +import re +import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] @@ -15,12 +17,12 @@ def _skill(name: str) -> str: def test_manifest_indexes_valid_repo_native_skills() -> None: manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) - assert manifest["status"] == "repo_native_validated" + assert manifest["status"] == "repo_native_path_validated" assert manifest["repo_skill_root"] == ".agents/skills" assert manifest["validation"] == "tests/test_repo_skill_pack.py" assert "37/37" in manifest["claim_ceiling"] - assert "auto-synchronized" in manifest["claim_ceiling"] - assert "apply worker remained disabled/inactive" in manifest["claim_ceiling"] + assert "PRs #146 and #147 are merged" in manifest["claim_ceiling"] + assert "PR #148 is open candidate evidence only" in manifest["claim_ceiling"] for entry in manifest["skills"]: path = ROOT / entry["path"] @@ -34,6 +36,139 @@ def test_manifest_indexes_valid_repo_native_skills() -> None: assert (ROOT / relative).is_file(), relative +def test_manifest_covers_the_required_onboarding_surface() -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + expected = { + "company_product_context", + "vps", + "gcp", + "hermes_runtime", + "database_provenance", + "ingestion", + "proposal_apply", + "reconstruction", + "identity", + "testing", + "security", + "secrets", + "incident_recovery", + } + assert set(manifest["required_coverage"]) == expected + skill_names = {entry["name"] for entry in manifest["skills"]} + repo_skill_names = {path.parent.name for path in (ROOT / ".agents" / "skills").glob("*/SKILL.md")} + assert skill_names == repo_skill_names + for owners in manifest["required_coverage"].values(): + assert owners + assert set(owners) <= skill_names + + +def test_path_validator_and_clean_installer() -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + validator = ROOT / manifest["validator"] + result = subprocess.run( + [str(validator), "--root", str(ROOT)], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + payload = json.loads(result.stdout) + assert payload["status"] == "pass" + assert payload["skill_count"] == len(manifest["skills"]) + assert payload["coverage_area_count"] == len(manifest["required_coverage"]) + assert payload["problems"] == [] + + +def test_installer_copies_only_manifest_skills(tmp_path: Path) -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + installer = ROOT / manifest["installer"] + target = tmp_path / "skills" + result = subprocess.run( + [str(installer), "--root", str(ROOT), "--target", str(target)], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + payload = json.loads(result.stdout) + assert payload["status"] == "pass" + assert payload["installed_skill_count"] == len(manifest["skills"]) + assert {path.name for path in target.iterdir()} == {entry["name"] for entry in manifest["skills"]} + assert all((target / entry["name"] / "SKILL.md").is_file() for entry in manifest["skills"]) + + +def test_installer_rejects_manifest_path_traversal(tmp_path: Path) -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + manifest["skills"][0]["name"] = "../escape" + malicious_manifest = tmp_path / "manifest.json" + malicious_manifest.write_text(json.dumps(manifest), encoding="utf-8") + target = tmp_path / "target" / "skills" + + result = subprocess.run( + [ + str(ROOT / manifest["installer"]), + "--root", + str(ROOT), + "--manifest", + str(malicious_manifest), + "--target", + str(target), + ], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert "invalid skill name" in result.stdout + assert not (tmp_path / "target" / "escape").exists() + assert not target.exists() + + +def test_isolated_install_canary_reaches_t2_without_claiming_agent_reasoning(tmp_path: Path) -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + canary = ROOT / manifest["isolated_install_canary"] + output = tmp_path / "receipt.json" + result = subprocess.run( + [str(canary), "--root", str(ROOT), "--output", str(output)], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + payload = json.loads(output.read_text(encoding="utf-8")) + assert payload["status"] == "pass" + assert payload["required_tier"] == "T2_runtime" + assert payload["current_tier"] == "T2_runtime" + assert payload["proof_scope"] == "deterministic_isolated_install_and_route_validation" + assert payload["fresh_temporary_skill_root"] is True + assert payload["ambient_skill_root_used"] is False + assert payload["agent_reasoning_exercised"] is False + assert payload["installed_skill_count"] == len(manifest["skills"]) + assert all(payload["semantic_checks"].values()) + assert payload["canary_exit_status"] == 0 + assert payload["cleanup_readback"]["temporary_skill_root_removed"] is True + assert payload["missing_to_reach_required_tier"] == [] + + +def test_fresh_agent_receipt_rejects_stale_routes() -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + receipt = json.loads((ROOT / manifest["fresh_agent_receipt"]).read_text(encoding="utf-8")) + + assert receipt["status"] == "pass" + assert receipt["evidence_origin"]["fork_turns"] == "none" + assert receipt["evidence_origin"]["memory_used"] is False + assert all(receipt["stale_claims_rejected"].values()) + assert receipt["validator"]["status"] == "pass" + assert receipt["validator"]["skill_count"] == 16 + assert receipt["repository_readback"]["unchanged"] is True + assert receipt["cleanup_readback"]["temporary_skill_root_removed"] is True + assert receipt["external_runtime_contacted"] is False + assert receipt["production_mutation"] is False + assert receipt["contains_secrets"] is False + assert receipt["problems"] == [] + + def test_db_change_skill_tracks_guarded_v2_runtime_proof() -> None: text = _skill("teleo-kb-db-change-workflow") squashed = " ".join(text.split()) @@ -120,3 +255,31 @@ def test_vps_onboarding_and_m3taversal_skills_point_to_current_proof() -> None: assert "Next proof-changing follow-up:" in outcomes assert "working-leo-current-proof-20260712.md" in outcomes assert "Cory" not in outcomes + + +def test_reconstruction_and_pr_state_are_routed_without_candidate_overclaim() -> None: + reconstruction = _skill("teleo-reconstruction-recovery") + onboarding = _skill("teleo-leo-onboarding") + gcp = _skill("teleo-gcp-parity-ops") + current_truth = (REPORT_DIR / "current-truth-index.md").read_text(encoding="utf-8") + + assert "ops/run_local_genesis_ledger_rebuild.py" in reconstruction + assert "scripts/verify_leo_unseen_reasoning_chain.py" in reconstruction + assert "Semantic source-to-blank-database recompilation remains incomplete" in reconstruction + assert "PRs #146 and #147 are merged repository truth" in onboarding + assert "PR #148 remains an open GCP" in onboarding + assert "candidate evidence only" in gcp + assert "PRs #146 and #147 are merged into canonical `main`" in current_truth + assert "PR #148 remains open candidate evidence" in current_truth + + +def test_active_onboarding_prose_uses_exact_m3taversal_identity() -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + active_files = [ + *[ROOT / relative for relative in manifest["scan_files"]], + *[ROOT / entry["path"] for entry in manifest["skills"]], + ] + for path in active_files: + text = path.read_text(encoding="utf-8") + prose = re.sub(r"`[^`]*\/[^`]+\.(?:md|json|svg)`", "", text) + assert re.search(r"\bcory(?:-style)?\b", prose, flags=re.IGNORECASE) is None, path From a6947a3f8bb74323a452c3b20a4060d19e1f94f3 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 03:19:49 +0200 Subject: [PATCH 03/27] replay exact revise strategy database transitions --- docs/kb-rebuild-and-recompile.md | 67 +- ops/run_local_genesis_ledger_rebuild.py | 543 ++++++++++++++- scripts/kb_apply_prereqs.sql | 5 +- tests/test_kb_apply_prereqs.py | 9 + .../test_run_local_genesis_ledger_rebuild.py | 659 +++++++++++++++--- 5 files changed, 1153 insertions(+), 130 deletions(-) diff --git a/docs/kb-rebuild-and-recompile.md b/docs/kb-rebuild-and-recompile.md index 2baf932..00996dc 100644 --- a/docs/kb-rebuild-and-recompile.md +++ b/docs/kb-rebuild-and-recompile.md @@ -148,7 +148,7 @@ not retain every column of the proposal ledger, so exact reconstruction also needs the full approved proposal row, immutable approval snapshot, and final applied proposal row. -## Genesis Plus Strict Ledger: Working Insert-Only Slice +## Genesis Plus Strict Ledger: Working Deterministic Slice `ops/run_local_genesis_ledger_rebuild.py` now executes the first exact genesis-plus-ledger slice in one command: @@ -206,27 +206,65 @@ Each referenced material object has exact top-level keys must contain every current `kb_stage.kb_proposals` column; partial envelopes are rejected. -The command verifies every hash before starting Docker, restores a tmpfs-backed -Postgres with `--network none`, reapplies the current guarded prerequisites, -and proves genesis parity. For each entry it seeds the receipt's exact row IDs -and timestamps in the disposable clone, executes the existing `kb_apply` -payload-bound guard and ledger transition, checks exact proposal and canonical -row readbacks, then verifies the complete final parity manifest. Its public +The command verifies every hash before starting Docker, requires a +SHA-256-pinned Postgres image (and defaults to a pinned PostgreSQL 16 Alpine +multi-platform digest), restores it on tmpfs with `--network none`, reapplies +the current guarded prerequisites, and proves genesis parity. Insert-only +entries seed the receipt's exact canonical row IDs and timestamps before the +existing `kb_apply` payload-bound guard executes. For `revise_strategy`, the +runner seeds only the proposal and approval, captures the target agent's +prestate, executes the real guarded transition, validates the generated delta, +and then replaces only those generated rows with the receipt-pinned IDs and +timestamps. Every path checks exact proposal and canonical row readbacks before +verifying the complete final parity manifest. Its public mode-`0600` receipt contains hashes, IDs, types, counts, parity summaries, and cleanup proof, but no payloads, rows, SQL, source paths, or command errors. +The legacy `seed_exact` summary is the insert-only aggregate of +`proposal_seed_exact` and `canonical_seed_exact`; it is intentionally false for +successful mutating entries, which instead report +`mutating_delta_validated` and `mutating_poststate_normalized`. +Fresh guard bootstrap rows use a fixed baseline timestamp rather than wall +clock time, so repeated clean restores have identical row hashes. -The exact v1 claim ceiling is intentionally narrow: +The exact v1 claim ceiling is intentionally bounded: -- `add_edge`, `attach_evidence`, and `approve_claim` strict receipts execute; +- `add_edge`, `attach_evidence`, `approve_claim`, and `revise_strategy` strict + receipts execute; - sequence gaps, hash drift, engine drift, duplicate proposals, and legacy or freeform payloads fail before container start; -- `revise_strategy` fails closed because its current receipt omits the prior - strategies and nodes that the apply engine deactivates or retires; +- `revise_strategy` is accepted only when the receipt pins the exact SQL that + matches the current guarded apply engine. Immediately before each entry, the + runner captures the target agent's strategy/node IDs, active strategy, + non-retired nodes, and maximum version. It then validates the generated + post-minus-pre delta, requires `version = previous maximum + 1`, and replaces + only the generated strategy/node rows with the exact receipt rows; +- the original transaction timestamp is derived from the fresh strategy + `created_at` and must equal every fresh node's `created_at` and `updated_at`. + Only node IDs observed as non-retired before apply receive that timestamp; + already-retired, unrelated-agent, and shared NULL-agent rows stay untouched; +- generated nodes must have no anchors before normalization, preventing a + delete-and-reinsert step from silently cascading future trigger-created rows; - full proposal before/after rows are mandatory because the current receipt envelope does not retain proposal origin fields or exact `updated_at`; - this proves only an isolated local reconstruction. It does not touch or prove VPS, GCP, Telegram, a live database, or blank-schema source recompilation. +The transition contract for `revise_strategy` is final-state deterministic, not +a claim that the v1 receipt independently contains a historical before-image: + +```text +exact genesis/pre-entry state + + exact current/original guarded SQL + + receipt-pinned fresh strategy and nodes + -> prior active strategy inactive + -> exactly the prior non-retired nodes retired at the original transaction time + -> one receipt-exact active strategy and receipt-exact node set +``` + +The genesis and final manifests remain mandatory oracles. Any incorrect +prestate, unrelated-row mutation, missing/extra generated row, semantic drift, +version drift, hash drift, or final rowset difference fails the reconstruction. + The source compiler now turns one raw artifact, its strict UTF-8 extraction, and a reviewed extraction manifest into a deterministic, hash-bound `pending_review` proposal bundle: @@ -272,10 +310,9 @@ neither command applies canonical rows. The existing full-data clone canary separately proves that a reviewed packet can create source, claim, evidence, and edge rows and affect later reasoning. -The remaining reconstruction work is to retain complete deltas for mutating -contracts such as `revise_strategy`, backfill or explicitly reject legacy -freeform applies, and extend beyond genesis recovery to a blank-schema source -compiler. The insert-only ledger runner does not prove that every historical +The remaining reconstruction work is to backfill or explicitly reject legacy +freeform applies and extend beyond genesis recovery to a blank-schema source +compiler. The strict ledger runner does not prove that every historical canonical row can be rebuilt semantically from retained sources. ## Definition Of Working diff --git a/ops/run_local_genesis_ledger_rebuild.py b/ops/run_local_genesis_ledger_rebuild.py index ef871a3..a3361e1 100644 --- a/ops/run_local_genesis_ledger_rebuild.py +++ b/ops/run_local_genesis_ledger_rebuild.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 """Deterministically rebuild a disposable Postgres from genesis plus a strict ledger. -The v1 replay boundary is intentionally narrow. It supports strict, insert-only -``add_edge``, ``attach_evidence``, and ``approve_claim`` receipts. Each ledger -entry must also retain the exact approved and applied proposal rows plus the -immutable approval snapshot. Legacy/freeform entries and ``revise_strategy`` -fail before Docker starts because their retained material is not a complete -database delta. +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 @@ -46,15 +48,21 @@ LEDGER_ARTIFACT = "teleo_genesis_plus_ledger" MATERIAL_ARTIFACT = "teleo_strict_proposal_replay_material" LEDGER_CONTRACT_VERSION = 1 MATERIAL_CONTRACT_VERSION = 1 -SUPPORTED_PROPOSAL_TYPES = frozenset({"add_edge", "attach_evidence", "approve_claim"}) +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 insert-only proposal replay; supported types are " - "add_edge, attach_evidence, and approve_claim; full approved/applied proposal rows " - "and immutable approval snapshots are required" + "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", - "revise_strategy replay because prior-row mutations are absent from v1 receipts", "source-corpus recompilation from a blank schema", "VPS, GCP, Telegram, or any live database mutation", ) @@ -77,6 +85,7 @@ FIXED_ENGINE_PATHS = frozenset( ) 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"} ) @@ -121,6 +130,35 @@ CANONICAL_TABLE_ORDER = ( "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): @@ -308,6 +346,119 @@ def _validate_proposal_rows( ) +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", + ) + applied_time = _parse_aware_timestamp(proposal.get("applied_at"), "receipt proposal applied_at") + 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], *, @@ -340,11 +491,6 @@ def _validate_entry_material( if not isinstance(proposal, dict): raise ReconstructionError("invalid_material", "replay receipt has no proposal object") proposal_type = proposal.get("proposal_type") - if proposal_type == "revise_strategy": - raise ReconstructionError( - "unsupported_mutating_receipt", - "revise_strategy receipts omit prior strategy and node mutations and cannot replay exactly", - ) if proposal_type not in SUPPORTED_PROPOSAL_TYPES: raise ReconstructionError( "unsupported_proposal_type", "ledger entry proposal type is not supported by v1 reconstruction" @@ -358,6 +504,8 @@ def _validate_entry_material( 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, @@ -382,6 +530,7 @@ def _validate_entry_material( 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"] @@ -394,6 +543,15 @@ def _validate_entry_material( "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, @@ -405,7 +563,7 @@ def _validate_entry_material( applied_proposal=applied, receipt=receipt, current_apply_sql=current_apply_sql, - current_apply_sql_sha256=_sha256_text(current_apply_sql), + current_apply_sql_sha256=current_apply_sql_sha256, ) @@ -550,6 +708,280 @@ def build_seed_sql(entry: LedgerEntry) -> str: 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( @@ -694,7 +1126,12 @@ def _entry_summary(entry: LedgerEntry) -> dict[str, Any]: "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", @@ -707,6 +1144,7 @@ def apply_entry( entry: LedgerEntry, summary: dict[str, Any], ) -> None: + mutating_prestate: dict[str, Any] | None = None try: _psql( args, @@ -733,20 +1171,34 @@ def apply_entry( code="entry_seed_mismatch", action=f"ledger entry {entry.sequence} proposal seed", ) - 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["seed_exact"] = True + 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, @@ -759,6 +1211,29 @@ def apply_entry( ) 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, @@ -1199,7 +1674,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: 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=canonical_rebuild.DEFAULT_IMAGE) + 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) @@ -1218,8 +1693,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: 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 args.image or any(character.isspace() for character in args.image): - parser.error("--image must be a non-empty Docker image reference without whitespace") + 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: diff --git a/scripts/kb_apply_prereqs.sql b/scripts/kb_apply_prereqs.sql index 9779c5a..839f6b5 100644 --- a/scripts/kb_apply_prereqs.sql +++ b/scripts/kb_apply_prereqs.sql @@ -315,8 +315,9 @@ alter table kb_stage.kb_review_principals owner to kb_gate_owner; alter table kb_stage.kb_proposal_approvals owner to kb_gate_owner; insert into kb_stage.kb_review_principals - (db_role, reviewed_by_handle, reviewed_by_agent_id, active) -select 'kb_review'::name, a.handle, a.id, true + (db_role, reviewed_by_handle, reviewed_by_agent_id, active, created_at) +select 'kb_review'::name, a.handle, a.id, true, + '1970-01-01 00:00:00+00'::timestamptz from public.agents a where a.handle = 'm3ta' on conflict (db_role) do nothing; diff --git a/tests/test_kb_apply_prereqs.py b/tests/test_kb_apply_prereqs.py index c4dbd2e..e66395c 100644 --- a/tests/test_kb_apply_prereqs.py +++ b/tests/test_kb_apply_prereqs.py @@ -203,6 +203,15 @@ def _lines(completed: subprocess.CompletedProcess[str]) -> set[str]: return {line for line in completed.stdout.splitlines() if line} +def test_reviewer_principal_seed_timestamp_is_deterministic(migrated_postgres: str) -> None: + created_at = _psql( + migrated_postgres, + "select created_at::text from kb_stage.kb_review_principals where db_role = 'kb_review';", + ).stdout.strip() + + assert created_at == "1970-01-01 00:00:00+00" + + def _catalog_snapshot(container: str) -> str: return _psql( container, diff --git a/tests/test_run_local_genesis_ledger_rebuild.py b/tests/test_run_local_genesis_ledger_rebuild.py index 4919b1c..721772c 100644 --- a/tests/test_run_local_genesis_ledger_rebuild.py +++ b/tests/test_run_local_genesis_ledger_rebuild.py @@ -23,6 +23,18 @@ REVIEWER_ID = "11111111-1111-4111-8111-111111111111" SERVICE_ID = "44444444-4444-4444-4444-444444444444" PROPOSAL_ID = "99999999-9999-4999-8999-999999999999" EDGE_ID = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" +OTHER_AGENT_ID = "22222222-2222-4222-8222-222222222222" +REVISE_PROPOSAL_ID = "88888888-8888-4888-8888-888888888888" +PRIOR_STRATEGY_ID = "33333333-3333-4333-8333-333333333333" +PRIOR_ACTIVE_NODE_ID = "55555555-5555-4555-8555-555555555555" +PRIOR_RETIRED_NODE_ID = "66666666-6666-4666-8666-666666666666" +OTHER_STRATEGY_ID = "77777777-7777-4777-8777-777777777777" +OTHER_ACTIVE_NODE_ID = "12121212-1212-4212-8212-121212121212" +NULL_AGENT_NODE_ID = "13131313-1313-4313-8313-131313131313" +PRIOR_NODE_ANCHOR_ID = "19191919-1919-4919-8919-191919191919" +RECEIPT_STRATEGY_ID = "14141414-1414-4414-8414-141414141414" +RECEIPT_NODE_A_ID = "15151515-1515-4515-8515-151515151515" +RECEIPT_NODE_B_ID = "16161616-1616-4616-8616-161616161616" PRIVATE_MARKER = "PRIVATE-REPLAY-MATERIAL-MUST-NOT-LEAK" @@ -155,6 +167,58 @@ def proposal_rows() -> tuple[dict, dict, dict]: return approved, applied, approval +def revise_strategy_proposal_rows() -> tuple[dict, dict, dict]: + approved, _, approval = proposal_rows() + payload = { + "apply_payload": { + "agent_id": REVIEWER_ID, + "strategy": { + "diagnosis": "Deterministic reconstruction needs exact mutation deltas.", + "guiding_policy": "Replay every guarded transition and fail closed on drift.", + "proximate_objectives": ["exact parity", "zero orphan containers"], + }, + "strategy_nodes": [ + { + "node_type": "policy", + "title": "Replay exactly", + "body": "Bind the guarded transition to its canonical receipt.", + "rank": 1, + }, + { + "node_type": "policy", + "title": "Replay exactly", + "body": "Bind the guarded transition to its canonical receipt.", + "rank": 1, + }, + ], + }, + "private_title": PRIVATE_MARKER, + } + approved = { + **approved, + "id": REVISE_PROPOSAL_ID, + "proposal_type": "revise_strategy", + "payload": payload, + "created_at": "2026-07-14T00:58:00+00:00", + "updated_at": "2026-07-14T01:00:00+00:00", + } + applied = { + **approved, + "status": "applied", + "applied_by_handle": "kb-apply", + "applied_by_agent_id": SERVICE_ID, + "applied_at": "2026-07-14T01:01:00+00:00", + "updated_at": "2026-07-14T01:01:00.000001+00:00", + } + approval = { + **approval, + "proposal_id": REVISE_PROPOSAL_ID, + "proposal_type": "revise_strategy", + "payload": payload, + } + return approved, applied, approval + + def applied_envelope(applied: dict) -> dict: fields = ( "id", @@ -202,6 +266,62 @@ def replay_material() -> dict: } +def revise_strategy_replay_material( + *, apply_sql_source: str = "exact_executed_sql", version: int = 2 +) -> dict: + approved, applied, approval = revise_strategy_proposal_rows() + proposal = applied_envelope(applied) + transaction_timestamp = "2026-07-14T01:00:30+00:00" + canonical_rows = { + "strategies": [ + { + "id": RECEIPT_STRATEGY_ID, + "agent_id": REVIEWER_ID, + **proposal["payload"]["apply_payload"]["strategy"], + "version": version, + "active": True, + "created_at": transaction_timestamp, + } + ], + "strategy_nodes": [ + { + "id": node_id, + "agent_id": REVIEWER_ID, + **node, + "status": "active", + "horizon": None, + "measure": None, + "source_ref": None, + "metadata": {}, + "created_at": transaction_timestamp, + "updated_at": transaction_timestamp, + } + for node_id, node in zip( + (RECEIPT_NODE_A_ID, RECEIPT_NODE_B_ID), + proposal["payload"]["apply_payload"]["strategy_nodes"], + strict=True, + ) + ], + } + apply_sql = rebuild.apply_engine.build_apply_sql(proposal, applied["applied_by_handle"]) + receipt = rebuild.replay_receipt.build_replay_receipt( + proposal, + canonical_rows, + apply_sql_sha256=rebuild.replay_receipt.sha256_text(apply_sql), + apply_sql_source=apply_sql_source, + exported_at_utc="2026-07-14T01:02:00+00:00", + ) + return { + "artifact": rebuild.MATERIAL_ARTIFACT, + "contract_version": rebuild.MATERIAL_CONTRACT_VERSION, + "sequence": 1, + "approved_proposal": approved, + "approval_snapshot": approval, + "applied_proposal": applied, + "replay_receipt": receipt, + } + + def write_bundle(tmp_path: Path, *, material: dict | None = None) -> argparse.Namespace: dump = tmp_path / "genesis.dump" dump.write_bytes(b"PGDMPfixture") @@ -238,7 +358,7 @@ def write_bundle(tmp_path: Path, *, material: dict | None = None) -> argparse.Na ledger=ledger_path, ledger_sha256=sha256_file(ledger_path), database="teleo", - image=rebuild.canonical_rebuild.DEFAULT_IMAGE, + image=rebuild.DEFAULT_REBUILD_IMAGE, container_prefix="teleo-genesis-ledger-test", tmpfs_mb=256, startup_timeout=30.0, @@ -302,18 +422,115 @@ def test_dump_hash_mismatch_fails_before_container_start(tmp_path: Path, monkeyp assert not any(len(command) > 1 and command[1] == "run" for command in commands) -def test_revise_strategy_receipt_fails_closed_before_receipt_rows_are_used(tmp_path: Path) -> None: - material = replay_material() - material["replay_receipt"]["proposal"]["proposal_type"] = "revise_strategy" - material["applied_proposal"]["proposal_type"] = "revise_strategy" - material["approved_proposal"]["proposal_type"] = "revise_strategy" - material["approval_snapshot"]["proposal_type"] = "revise_strategy" +def test_revise_strategy_material_validates_exact_engine_and_canonicalizes_row_order( + tmp_path: Path, +) -> None: + material = revise_strategy_replay_material() + material["replay_receipt"]["canonical_rows"]["strategy_nodes"].reverse() args = write_bundle(tmp_path, material=material) - with pytest.raises(rebuild.ReconstructionError, match="prior strategy") as exc_info: + entry = rebuild.load_bundle(args).entries[0] + + assert entry.applied_proposal["proposal_type"] == "revise_strategy" + assert entry.receipt["apply_engine"]["source"] == "exact_executed_sql" + assert entry.receipt["apply_engine"]["apply_sql_sha256"] == entry.current_apply_sql_sha256 + assert entry.receipt["canonical_rows"]["strategy_nodes"] == sorted( + entry.receipt["canonical_rows"]["strategy_nodes"], key=rebuild._canonical_json + ) + + +def test_revise_strategy_material_rejects_reconstructed_apply_engine(tmp_path: Path) -> None: + args = write_bundle( + tmp_path, + material=revise_strategy_replay_material(apply_sql_source="reconstructed_current_engine"), + ) + + with pytest.raises(rebuild.ReconstructionError) as exc_info: rebuild.load_bundle(args) - assert exc_info.value.code == "unsupported_mutating_receipt" + assert exc_info.value.code == "mutating_apply_engine_mismatch" + + +@pytest.mark.parametrize( + "invalid_case", + ("extra_strategy_field", "duplicate_node_id", "node_timestamp_drift", "transaction_after_apply"), +) +def test_revise_strategy_receipt_rejects_impossible_poststate( + tmp_path: Path, invalid_case: str +) -> None: + material = revise_strategy_replay_material() + rows = material["replay_receipt"]["canonical_rows"] + if invalid_case == "extra_strategy_field": + rows["strategies"][0]["unexpected"] = "ignored by jsonb_populate_record" + elif invalid_case == "duplicate_node_id": + rows["strategy_nodes"][1]["id"] = rows["strategy_nodes"][0]["id"] + elif invalid_case == "node_timestamp_drift": + rows["strategy_nodes"][0]["updated_at"] = "2026-07-14T01:00:31+00:00" + else: + rows["strategies"][0]["created_at"] = "2026-07-14T01:01:01+00:00" + for row in rows["strategy_nodes"]: + row["created_at"] = "2026-07-14T01:01:01+00:00" + row["updated_at"] = "2026-07-14T01:01:01+00:00" + args = write_bundle(tmp_path, material=material) + + with pytest.raises(rebuild.ReconstructionError) as exc_info: + rebuild.load_bundle(args) + + assert exc_info.value.code == "invalid_mutating_receipt" + + +def test_revise_strategy_transition_builders_preserve_historical_prestate(tmp_path: Path) -> None: + entry = rebuild.load_bundle( + write_bundle(tmp_path, material=revise_strategy_replay_material()) + ).entries[0] + prestate = { + "strategy_ids": [PRIOR_STRATEGY_ID], + "active_strategy_ids": [PRIOR_STRATEGY_ID], + "max_strategy_version": 1, + "strategy_node_ids": [PRIOR_ACTIVE_NODE_ID, PRIOR_RETIRED_NODE_ID], + "non_retired_strategy_node_ids": [PRIOR_ACTIVE_NODE_ID], + } + generated_rows = copy.deepcopy(entry.receipt["canonical_rows"]) + generated_rows["strategies"][0]["id"] = "17171717-1717-4717-8717-171717171717" + generated_rows["strategies"][0]["created_at"] = "2026-07-15T01:00:30+00:00" + for index, row in enumerate(generated_rows["strategy_nodes"], start=1): + row["id"] = f"18181818-1818-4818-8818-18181818181{index}" + row["created_at"] = "2026-07-15T01:00:30+00:00" + row["updated_at"] = "2026-07-15T01:00:30+00:00" + + sql = rebuild.build_revise_strategy_normalization_sql(entry, prestate, generated_rows) + + assert PRIOR_ACTIVE_NODE_ID in sql + assert PRIOR_RETIRED_NODE_ID not in sql + assert PRIOR_STRATEGY_ID in sql + assert RECEIPT_STRATEGY_ID in sql + assert "updated_at = E'2026-07-14T01:00:30+00:00'::timestamptz" in sql + + +def test_revise_strategy_transition_builders_allow_empty_prestate(tmp_path: Path) -> None: + entry = rebuild.load_bundle( + write_bundle(tmp_path, material=revise_strategy_replay_material(version=1)) + ).entries[0] + prestate = { + "strategy_ids": [], + "active_strategy_ids": [], + "max_strategy_version": 0, + "strategy_node_ids": [], + "non_retired_strategy_node_ids": [], + } + generated_rows = copy.deepcopy(entry.receipt["canonical_rows"]) + generated_rows["strategies"][0]["id"] = "17171717-1717-4717-8717-171717171717" + generated_rows["strategies"][0]["created_at"] = "2026-07-15T01:00:30+00:00" + for index, row in enumerate(generated_rows["strategy_nodes"], start=1): + row["id"] = f"18181818-1818-4818-8818-18181818181{index}" + row["created_at"] = "2026-07-15T01:00:30+00:00" + row["updated_at"] = "2026-07-15T01:00:30+00:00" + + sql = rebuild.build_revise_strategy_normalization_sql(entry, prestate, generated_rows) + + assert "prior active strategy transition mismatch" not in sql + assert "prior node normalization mismatch" not in sql + assert RECEIPT_STRATEGY_ID in sql def test_legacy_freeform_receipt_fails_closed(tmp_path: Path) -> None: @@ -331,6 +548,17 @@ def test_legacy_freeform_receipt_fails_closed(tmp_path: Path) -> None: assert PRIVATE_MARKER not in exc_info.value.safe_message +def test_unknown_proposal_operation_fails_closed_before_replay(tmp_path: Path) -> None: + material = replay_material() + material["replay_receipt"]["proposal"]["proposal_type"] = "delete_claim" + args = write_bundle(tmp_path, material=material) + + with pytest.raises(rebuild.ReconstructionError) as exc_info: + rebuild.load_bundle(args) + + assert exc_info.value.code == "unsupported_proposal_type" + + def test_proposal_metadata_outside_apply_transition_is_rejected(tmp_path: Path) -> None: material = replay_material() material["applied_proposal"]["rationale"] = "silently changed after review" @@ -354,6 +582,28 @@ def test_output_cannot_overwrite_a_fixed_guard_or_engine_file(tmp_path: Path) -> assert exc_info.value.code == "unsafe_output_path" +def test_cli_defaults_to_pinned_image_and_rejects_moving_tag(tmp_path: Path) -> None: + bundle = write_bundle(tmp_path) + argv = [ + "--genesis-dump", + str(bundle.genesis_dump), + "--genesis-manifest", + str(bundle.genesis_manifest), + "--ledger", + str(bundle.ledger), + "--ledger-sha256", + bundle.ledger_sha256, + "--output", + str(tmp_path / "receipt.json"), + ] + + assert rebuild.parse_args(argv).image == rebuild.DEFAULT_REBUILD_IMAGE + with pytest.raises(SystemExit) as exc_info: + rebuild.parse_args([*argv, "--image", "postgres:16-alpine"]) + + assert exc_info.value.code == 2 + + def test_unknown_private_field_name_is_not_echoed_in_public_error(tmp_path: Path) -> None: material = replay_material() material[PRIVATE_MARKER] = "private value" @@ -484,17 +734,18 @@ create table public.reasoning_tools ( created_at timestamptz not null default now() ); create table public.strategies ( - id uuid primary key, + id uuid primary key default gen_random_uuid(), agent_id uuid not null references public.agents(id), diagnosis text, guiding_policy text, proximate_objectives jsonb, version integer not null default 1, active boolean not null default true, - created_at timestamptz not null default now() + created_at timestamptz not null default now(), + unique (agent_id, version) ); create table public.strategy_nodes ( - id uuid primary key, + id uuid primary key default gen_random_uuid(), agent_id uuid references public.agents(id), node_type text, title text, @@ -508,6 +759,20 @@ create table public.strategy_nodes ( created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); +create table public.strategy_node_anchors ( + id uuid primary key default gen_random_uuid(), + from_node_id uuid not null references public.strategy_nodes(id) on delete cascade, + anchor_role text not null, + to_node_id uuid references public.strategy_nodes(id) on delete cascade, + to_shared_root_id uuid, + belief_id uuid, + claim_id uuid, + source_id uuid, + weight numeric, + note text, + created_at timestamptz not null default now(), + check (num_nonnulls(to_node_id, to_shared_root_id, belief_id, claim_id, source_id) = 1) +); create table kb_stage.kb_proposals ( id uuid primary key, proposal_type text not null, @@ -530,10 +795,34 @@ create table kb_stage.kb_proposals ( ); insert into public.agents (id, handle, kind) values - ('{REVIEWER_ID}', 'm3ta', 'human'); + ('{REVIEWER_ID}', 'm3ta', 'human'), + ('{OTHER_AGENT_ID}', 'other-agent', 'system'); insert into public.claims (id, type, text, status, confidence, tags, created_by) values ('{CLAIM_A}', 'structural', 'Fixture claim A', 'open', 0.8, array['fixture'], '{REVIEWER_ID}'), ('{CLAIM_B}', 'structural', 'Fixture claim B', 'open', 0.8, array['fixture'], '{REVIEWER_ID}'); +insert into public.strategies + (id, agent_id, diagnosis, guiding_policy, proximate_objectives, version, active, created_at) +values + ('{PRIOR_STRATEGY_ID}', '{REVIEWER_ID}', 'Prior diagnosis', 'Prior policy', '["prior"]', 1, true, + '2026-07-01T00:00:00+00:00'), + ('{OTHER_STRATEGY_ID}', '{OTHER_AGENT_ID}', 'Other diagnosis', 'Other policy', '["other"]', 1, true, + '2026-07-01T00:00:00+00:00'); +insert into public.strategy_nodes + (id, agent_id, node_type, title, body, rank, status, created_at, updated_at) +values + ('{PRIOR_ACTIVE_NODE_ID}', '{REVIEWER_ID}', 'policy', 'Prior active', 'Retire exactly once', 1, + 'active', '2026-07-01T00:00:00+00:00', '2026-07-01T00:00:00+00:00'), + ('{PRIOR_RETIRED_NODE_ID}', '{REVIEWER_ID}', 'policy', 'Prior retired', 'Leave timestamp unchanged', 2, + 'retired', '2026-06-01T00:00:00+00:00', '2026-06-02T00:00:00+00:00'), + ('{OTHER_ACTIVE_NODE_ID}', '{OTHER_AGENT_ID}', 'policy', 'Other active', 'Leave other agent unchanged', 1, + 'active', '2026-07-01T00:00:00+00:00', '2026-07-01T00:00:00+00:00'), + ('{NULL_AGENT_NODE_ID}', null, 'policy', 'Shared active', 'Leave shared node unchanged', 1, + 'active', '2026-07-01T00:00:00+00:00', '2026-07-01T00:00:00+00:00'); +insert into public.strategy_node_anchors + (id, from_node_id, anchor_role, to_node_id, weight, note, created_at) +values + ('{PRIOR_NODE_ANCHOR_ID}', '{PRIOR_ACTIVE_NODE_ID}', 'serves', '{PRIOR_RETIRED_NODE_ID}', 1.0, + 'Existing anchor must survive strategy revision replay', '2026-07-01T00:00:00+00:00'); """ @@ -631,13 +920,17 @@ def source_postgres() -> Iterator[tuple[str, str]]: "run", "--rm", "--detach", + "--network", + "none", "--name", container, + "--label", + f"{rebuild.canonical_rebuild.CANARY_LABEL}={container}", "--env", f"POSTGRES_DB={database}", "--env", "POSTGRES_HOST_AUTH_METHOD=trust", - rebuild.canonical_rebuild.DEFAULT_IMAGE, + rebuild.DEFAULT_REBUILD_IMAGE, ], text=True, capture_output=True, @@ -692,14 +985,24 @@ def source_postgres() -> Iterator[tuple[str, str]]: capture_output=True, check=False, ) + inspected = subprocess.run( + ["docker", "container", "inspect", container], + text=True, + capture_output=True, + check=False, + ) + if inspected.returncode == 0: + pytest.fail(f"fixture PostgreSQL container was not removed: {container}") -def test_live_genesis_plus_ledger_command_rebuilds_exactly_and_proves_cleanup( +def capture_source_snapshot( + source_container: str, + database: str, tmp_path: Path, - source_postgres: tuple[str, str], -) -> None: - source_container, database = source_postgres - genesis_dump = tmp_path / "genesis.dump" + *, + stem: str, +) -> tuple[Path, Path]: + dump = tmp_path / f"{stem}.dump" dump_result = subprocess.run( [ "docker", @@ -711,7 +1014,7 @@ def test_live_genesis_plus_ledger_command_rebuilds_exactly_and_proves_cleanup( "-d", database, "--format=custom", - "--file=/tmp/genesis.dump", + f"--file=/tmp/{stem}.dump", ], text=True, capture_output=True, @@ -719,15 +1022,130 @@ def test_live_genesis_plus_ledger_command_rebuilds_exactly_and_proves_cleanup( ) assert dump_result.returncode == 0, dump_result.stderr copy_result = subprocess.run( - ["docker", "cp", f"{source_container}:/tmp/genesis.dump", str(genesis_dump)], + ["docker", "cp", f"{source_container}:/tmp/{stem}.dump", str(dump)], text=True, capture_output=True, check=False, ) assert copy_result.returncode == 0, copy_result.stderr + manifest = tmp_path / f"{stem}-manifest.jsonl" + capture_manifest(source_container, database, manifest) + return dump, manifest - genesis_manifest = tmp_path / "genesis-manifest.jsonl" - capture_manifest(source_container, database, genesis_manifest) + +def seed_proposal_and_approval( + source_container: str, + database: str, + approved: dict, + approval: dict, +) -> dict: + sql = f"""begin; +set local standard_conforming_strings = on; +insert into kb_stage.kb_proposals +select seeded.* from jsonb_populate_record( + null::kb_stage.kb_proposals, {rebuild._jsonb_literal(approved)} +) seeded; +insert into kb_stage.kb_proposal_approvals +select seeded.* from jsonb_populate_record( + null::kb_stage.kb_proposal_approvals, {rebuild._jsonb_literal(approval)} +) seeded; +commit; +""" + docker_psql(source_container, database, sql) + raw = docker_psql( + source_container, + database, + rebuild.build_proposal_readback_sql(approved["id"]), + ).stdout.strip() + return json.loads(raw) + + +def run_genesis_ledger_command( + *, + tmp_path: Path, + database: str, + genesis_dump: Path, + genesis_manifest: Path, + material_path: Path, + final_manifest: Path, + artifact_stem: str, + container_prefix: str, + run_number: int, +) -> tuple[subprocess.CompletedProcess[str], dict, Path]: + ledger = { + "artifact": rebuild.LEDGER_ARTIFACT, + "contract_version": rebuild.LEDGER_CONTRACT_VERSION, + "engine": rebuild._engine_hashes(), + "genesis": { + "dump_sha256": sha256_file(genesis_dump), + "parity_manifest_sha256": sha256_file(genesis_manifest), + }, + "entries": [ + { + "sequence": 1, + "material": material_path.name, + "sha256": sha256_file(material_path), + "replay_material_sha256": json.loads(material_path.read_text(encoding="utf-8"))[ + "replay_receipt" + ]["hashes"]["replay_material_sha256"], + } + ], + "final_parity": { + "manifest": final_manifest.name, + "sha256": sha256_file(final_manifest), + }, + } + ledger_path = write_json(tmp_path / f"{artifact_stem}-ledger-run-{run_number}.json", ledger) + output = tmp_path / f"{artifact_stem}-reconstruction-receipt-run-{run_number}.json" + command = [ + sys.executable, + str(Path(rebuild.__file__).resolve()), + "--genesis-dump", + str(genesis_dump), + "--genesis-manifest", + str(genesis_manifest), + "--ledger", + str(ledger_path), + "--ledger-sha256", + sha256_file(ledger_path), + "--database", + database, + "--container-prefix", + container_prefix, + "--tmpfs-mb", + "256", + "--max-target-query-ms", + "5000", + "--max-target-source-ratio", + "100", + "--output", + str(output), + ] + completed = subprocess.run( + command, + cwd=rebuild.REPO_ROOT, + text=True, + capture_output=True, + check=False, + timeout=180, + ) + assert completed.returncode == 0, completed.stderr + completed.stdout + assert output.is_file(), "rebuild command succeeded without writing its receipt" + receipt = json.loads(output.read_text(encoding="utf-8")) + return completed, receipt, output + + +def test_live_genesis_plus_ledger_command_rebuilds_exactly_and_proves_cleanup( + tmp_path: Path, + source_postgres: tuple[str, str], +) -> None: + source_container, database = source_postgres + genesis_dump, genesis_manifest = capture_source_snapshot( + source_container, + database, + tmp_path, + stem="insert-only-genesis", + ) material = replay_material() material_path = write_json(tmp_path / "entry-0001.json", material) @@ -753,64 +1171,18 @@ def test_live_genesis_plus_ledger_command_rebuilds_exactly_and_proves_cleanup( final_manifest = tmp_path / "final-manifest.jsonl" capture_manifest(source_container, database, final_manifest) - ledger = { - "artifact": rebuild.LEDGER_ARTIFACT, - "contract_version": rebuild.LEDGER_CONTRACT_VERSION, - "engine": rebuild._engine_hashes(), - "genesis": { - "dump_sha256": sha256_file(genesis_dump), - "parity_manifest_sha256": sha256_file(genesis_manifest), - }, - "entries": [ - { - "sequence": 1, - "material": material_path.name, - "sha256": sha256_file(material_path), - "replay_material_sha256": entry.replay_material_sha256, - } - ], - "final_parity": { - "manifest": final_manifest.name, - "sha256": sha256_file(final_manifest), - }, - } - ledger_path = write_json(tmp_path / "ledger.json", ledger) - output = tmp_path / "reconstruction-receipt.json" - command = [ - sys.executable, - str(Path(rebuild.__file__).resolve()), - "--genesis-dump", - str(genesis_dump), - "--genesis-manifest", - str(genesis_manifest), - "--ledger", - str(ledger_path), - "--ledger-sha256", - sha256_file(ledger_path), - "--database", - database, - "--container-prefix", - "teleo-genesis-ledger-live-test", - "--tmpfs-mb", - "256", - "--max-target-query-ms", - "5000", - "--max-target-source-ratio", - "100", - "--output", - str(output), - ] - completed = subprocess.run( - command, - cwd=rebuild.REPO_ROOT, - text=True, - capture_output=True, - check=False, - timeout=180, + completed, receipt, output = run_genesis_ledger_command( + tmp_path=tmp_path, + database=database, + genesis_dump=genesis_dump, + genesis_manifest=genesis_manifest, + material_path=material_path, + final_manifest=final_manifest, + artifact_stem="insert-only", + container_prefix="teleo-genesis-ledger-live-test", + run_number=1, ) - assert completed.returncode == 0, completed.stderr + completed.stdout - receipt = json.loads(output.read_text(encoding="utf-8")) assert receipt["status"] == "pass" assert receipt["genesis_parity"]["status"] == "pass" assert receipt["ledger"]["entries"][0]["status"] == "pass" @@ -831,3 +1203,132 @@ def test_live_genesis_plus_ledger_command_rebuilds_exactly_and_proves_cleanup( check=False, ) assert inspect.returncode != 0 + + +def test_live_revise_strategy_rebuild_is_exact_repeatable_and_leaves_no_container( + tmp_path: Path, + source_postgres: tuple[str, str], +) -> None: + source_container, database = source_postgres + genesis_dump, genesis_manifest = capture_source_snapshot( + source_container, + database, + tmp_path, + stem="revise-strategy-genesis", + ) + + approved, _, approval = revise_strategy_proposal_rows() + pre_apply = seed_proposal_and_approval( + source_container, + database, + approved, + approval, + ) + apply_sql = rebuild.apply_engine.build_apply_sql(pre_apply["proposal"], "kb-apply") + docker_psql(source_container, database, apply_sql, role="kb_apply") + post_apply = json.loads( + docker_psql( + source_container, + database, + rebuild.build_proposal_readback_sql(REVISE_PROPOSAL_ID), + ).stdout.strip() + ) + applied_proposal = post_apply["proposal"] + proposal_envelope = applied_envelope(applied_proposal) + source_receipt_path, receipt = rebuild.apply_engine.capture_replay_receipt( + argparse.Namespace( + container=source_container, + role="kb_apply", + host="127.0.0.1", + db=database, + receipt_out=str(tmp_path / "source-revise-strategy-private-receipt.json"), + receipt_dir=None, + ), + proposal_envelope, + "", + apply_sql=apply_sql, + ) + assert source_receipt_path.is_file() + assert stat.S_IMODE(source_receipt_path.stat().st_mode) == 0o600 + material = { + "artifact": rebuild.MATERIAL_ARTIFACT, + "contract_version": rebuild.MATERIAL_CONTRACT_VERSION, + "sequence": 1, + "approved_proposal": pre_apply["proposal"], + "approval_snapshot": pre_apply["approval_snapshot"], + "applied_proposal": applied_proposal, + "replay_receipt": receipt, + } + material_path = write_json(tmp_path / "revise-strategy-entry-0001.json", material) + final_manifest = tmp_path / "revise-strategy-final-manifest.jsonl" + capture_manifest(source_container, database, final_manifest) + + runs = [ + run_genesis_ledger_command( + tmp_path=tmp_path, + database=database, + genesis_dump=genesis_dump, + genesis_manifest=genesis_manifest, + material_path=material_path, + final_manifest=final_manifest, + artifact_stem="revise-strategy", + container_prefix="teleo-genesis-ledger-revise", + run_number=run_number, + ) + for run_number in (1, 2) + ] + + for completed, reconstruction, output in runs: + assert reconstruction["status"] == "pass" + assert reconstruction["genesis_parity"]["status"] == "pass" + assert reconstruction["final_parity"]["status"] == "pass" + entry_summary = reconstruction["ledger"]["entries"][0] + assert entry_summary["proposal_type"] == "revise_strategy" + assert entry_summary["proposal_seed_exact"] is True + assert entry_summary["canonical_seed_exact"] is False + assert entry_summary["seed_exact"] is False + assert entry_summary["guarded_apply_executed"] is True + assert entry_summary["mutating_prestate_captured"] is True + assert entry_summary["mutating_delta_validated"] is True + assert entry_summary["mutating_poststate_normalized"] is True + assert entry_summary["proposal_exact"] is True + assert entry_summary["canonical_rows_exact"] is True + assert reconstruction["cleanup"]["container_absent"] is True + assert reconstruction["safety"]["network_access_available_to_container"] is False + assert reconstruction["safety"]["private_replay_material_emitted"] is False + assert PRIVATE_MARKER not in completed.stdout + assert PRIVATE_MARKER not in output.read_text(encoding="utf-8") + assert stat.S_IMODE(output.stat().st_mode) == 0o600 + + container_name = reconstruction["container"]["name"] + inspect = subprocess.run( + ["docker", "container", "inspect", container_name], + text=True, + capture_output=True, + check=False, + ) + assert inspect.returncode != 0 + labeled = subprocess.run( + [ + "docker", + "ps", + "-aq", + "--filter", + f"label={rebuild.canonical_rebuild.CANARY_LABEL}={container_name}", + ], + text=True, + capture_output=True, + check=False, + ) + assert labeled.returncode == 0 + assert labeled.stdout.strip() == "" + + first = runs[0][1] + second = runs[1][1] + assert first["inputs"] == second["inputs"] + assert first["ledger"]["entries"][0]["material_sha256"] == second["ledger"]["entries"][0][ + "material_sha256" + ] + assert first["ledger"]["entries"][0]["replay_material_sha256"] == second["ledger"]["entries"][0][ + "replay_material_sha256" + ] From dfa6be6ba098b63b5856d03f499a44883b193e4f Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 03:37:39 +0200 Subject: [PATCH 04/27] feat: prepare zero-mutation Telegram Leo chain proof --- ...en-chain-authorization-packet-current.json | 135 ++++ ...seen-chain-authorization-packet-current.md | 64 ++ ...-unseen-chain-capture-receipt-current.json | 84 +++ ...le-unseen-chain-capture-receipt-current.md | 25 + ...le-unseen-chain-goal-readback-current.json | 21 + ...le-unseen-chain-nonsend-proof-current.json | 62 ++ ...isible-unseen-chain-preflight-current.json | 115 ++++ ...-visible-unseen-chain-preflight-current.md | 49 ++ ...ld_telegram_visible_unseen_chain_packet.py | 326 +++++++++ ...ect_telegram_visible_unseen_chain_state.py | 616 ++++++++++++++++++ .../verify_telegram_visible_unseen_chain.py | 524 +++++++++++++++ ...ld_telegram_visible_unseen_chain_packet.py | 64 ++ ...ect_telegram_visible_unseen_chain_state.py | 251 +++++++ ...st_verify_telegram_visible_unseen_chain.py | 240 +++++++ 14 files changed, 2576 insertions(+) create mode 100644 docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.json create mode 100644 docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.md create mode 100644 docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.json create mode 100644 docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.md create mode 100644 docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-goal-readback-current.json create mode 100644 docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-nonsend-proof-current.json create mode 100644 docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.json create mode 100644 docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.md create mode 100644 scripts/build_telegram_visible_unseen_chain_packet.py create mode 100644 scripts/collect_telegram_visible_unseen_chain_state.py create mode 100644 scripts/verify_telegram_visible_unseen_chain.py create mode 100644 tests/test_build_telegram_visible_unseen_chain_packet.py create mode 100644 tests/test_collect_telegram_visible_unseen_chain_state.py create mode 100644 tests/test_verify_telegram_visible_unseen_chain.py diff --git a/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.json b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.json new file mode 100644 index 0000000..348e036 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.json @@ -0,0 +1,135 @@ +{ + "authorization_gate_status": "ready_to_request_exact_authorization", + "checks": { + "authenticated_chrome_is_only_send_transport": true, + "exact_prompts_imported_from_handler_verifier": true, + "exact_three_turn_sequence": true, + "first_prompt_supplies_no_row_id": true, + "handler_checks_all_pass": true, + "handler_outcomes_all_pass": true, + "handler_prompt_ids_exact": true, + "handler_proved_no_telegram_post": true, + "handler_proved_read_only_tool_traces": true, + "handler_proved_unchanged_fingerprint": true, + "handler_receipt_passed": true, + "handler_receipt_schema_supported": true, + "handler_safety_gate_passed": true, + "handler_tier_remains_below_telegram_visible": true, + "packet_does_not_mutate_database": true, + "packet_does_not_send": true + }, + "claim_ceiling": { + "current_tier": "T0_spec_plus_prior_T2_handler_evidence", + "not_proven": [ + "Telegram-visible delivery of these three messages", + "Telegram-visible replies from Leo", + "post-send canonical fingerprint equality" + ], + "proven": "The exact three-turn Chrome send packet is bound to a passing no-send handler receipt.", + "required_tier": "T3_live_readonly" + }, + "clear_CTA": "Reply with the exact authorization sentence in this packet. That sentence names the destination, transport, complete message bodies, ordering, evidence, and no-mutation boundary.", + "exact_messages": [ + { + "dimension": "independent_weak_claim_selection", + "message": "Looking at what Leo currently believes about collective intelligence or coordination, pick one claim that would matter to a founder making a product decision but seems more confident than its support. Do not ask me for an ID. Show what the database actually says and what supports it, then tell me the first narrower claim you would test. Do not change anything yet.", + "message_sha256": "c63838b98448f4f4599e4cf2b004c6b36f602ad0d25c92e442a6f866cd2740fc", + "prompt_id": "OOS-CHAIN-01", + "sequence": 1, + "wait_for_visible_reply_before_next": true + }, + { + "dimension": "challenge_body_evidence_and_candidates", + "message": "That still feels like you are polishing the same idea. Inspect the exact body and evidence you just chose: what is assumption versus observed support, what would falsify it, and which replacement claim or claims would you put into review? Keep them as proposals, not live knowledge.", + "message_sha256": "73346a98db4d7f74b49e1a5741b323d4216fd21f8346f6c4a4a69b1a6dd07946", + "prompt_id": "OOS-CHAIN-02", + "sequence": 2, + "wait_for_visible_reply_before_next": true + }, + { + "dimension": "user_objection_revision_and_apply_boundary", + "message": "I disagree with your first replacement because it jumps from a mechanism to a business outcome. Revise it with that objection, tell me what source would resolve the disagreement, and show the approval and apply boundary before it could become part of Leo.", + "message_sha256": "5f6b32b2c8c51cc594242ed9bdac849c744156ca2395efb7e8593c83c86286d4", + "prompt_id": "OOS-CHAIN-03", + "sequence": 3, + "wait_for_visible_reply_before_next": true + } + ], + "expected_proof_paths_after_authorized_run": { + "capture_json": "outputs/telegram-visible-unseen-chain-20260715/capture.json", + "capture_receipt_json": "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.json", + "final_accessibility": "outputs/telegram-visible-unseen-chain-20260715/final-accessibility.txt", + "final_screenshot": "outputs/telegram-visible-unseen-chain-20260715/final.png", + "postflight_json": "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-postflight-current.json", + "preflight_json": "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.json", + "turn_accessibility": [ + "outputs/telegram-visible-unseen-chain-20260715/turn-1-accessibility.txt", + "outputs/telegram-visible-unseen-chain-20260715/turn-2-accessibility.txt", + "outputs/telegram-visible-unseen-chain-20260715/turn-3-accessibility.txt" + ], + "turn_screenshots": [ + "outputs/telegram-visible-unseen-chain-20260715/turn-1.png", + "outputs/telegram-visible-unseen-chain-20260715/turn-2.png", + "outputs/telegram-visible-unseen-chain-20260715/turn-3.png" + ] + }, + "explicit_operator_authorization_text": "I authorize Codex to use the already-authenticated Chrome Telegram UI to send exactly these three messages, in order and with no additional messages, to the Telegram group Leo (chat ID -5146042086), waiting for Leo's visible reply after each turn: [OOS-CHAIN-01] \"Looking at what Leo currently believes about collective intelligence or coordination, pick one claim that would matter to a founder making a product decision but seems more confident than its support. Do not ask me for an ID. Show what the database actually says and what supports it, then tell me the first narrower claim you would test. Do not change anything yet.\" [OOS-CHAIN-02] \"That still feels like you are polishing the same idea. Inspect the exact body and evidence you just chose: what is assumption versus observed support, what would falsify it, and which replacement claim or claims would you put into review? Keep them as proposals, not live knowledge.\" [OOS-CHAIN-03] \"I disagree with your first replacement because it jumps from a mechanism to a business outcome. Revise it with that objection, tell me what source would resolve the disagreement, and show the approval and apply boundary before it could become part of Leo.\" Capture the visible replies, message IDs, timestamps, screenshot and accessibility evidence; perform only read-only DB/service readbacks; do not use the Bot API; do not stage, approve, apply, or otherwise mutate knowledge.", + "generated_at_utc": "2026-07-15T01:32:53.837725+00:00", + "handler_receipt_binding": { + "deployed_git_head": "b3aa184dd4c721f6c40dc813dcefd5930f8ed417", + "fingerprint_sha256": "32a1f2c18f4b4c623443cbd4d9520982e9f291ec74ca63855cef51c88ac56f24", + "path": "docs/reports/leo-working-state-20260709/leo-unseen-reasoning-chain-canary-current.json", + "proof_tier": "live_vps_gatewayrunner_temp_profile_no_telegram_post_no_apply", + "remote_run_id": "d5702cafbb0b", + "schema": "livingip.leoUnseenReasoningChainReceipt.v2", + "sha256": "f4fcf0207484e5322074c7838f3a345d421c1664e5dc8d73b9199c4475b4cee8" + }, + "mode": "telegram_visible_unseen_chain_exact_packet_not_sent", + "mutates_db": false, + "next_non_user_action_after_authorization": [ + "Re-run the zero-mutation preflight and confirm its packet file hash is current.", + "Use authenticated Chrome only and verify the Leo chat ID before typing anything.", + "Send one exact message, wait for Leo's visible reply, then continue to the next exact message.", + "Capture message IDs, timestamps, screenshot, and accessibility text without copying handler output.", + "Extract the selected DB object reference and run the read-only postflight with that reference.", + "Run the Telegram-visible verifier and accept T3 only if every visible and state check passes." + ], + "packet_generation_git_sha": "a47415b60ef059ecc55e067ae556f49d818dc68d", + "production_apply_executed": false, + "protocol_sha256": "c815dad151d640335eecbddd2ca7bb651c43f6c1d4e53642e12c8a35157ac2da", + "ready_to_request_action_time_authorization": true, + "schema": "livingip.telegramVisibleUnseenChainPacket.v1", + "target": { + "allowed_transport": "authenticated_chrome_telegram_ui", + "chat_id": "-5146042086", + "chat_label": "Leo", + "expected_prompt_ids": [ + "OOS-CHAIN-01", + "OOS-CHAIN-02", + "OOS-CHAIN-03" + ], + "forbidden_transports": [ + "telegram_bot_api", + "copied_transcript", + "handler_substitution" + ], + "message_count": 3, + "platform": "telegram" + }, + "telegram_visible_message_authorization_present": false, + "telegram_visible_messages_sent": false, + "tooling_binding": { + "capture_verifier": { + "path": "scripts/verify_telegram_visible_unseen_chain.py", + "sha256": "4eb3eb8799282d07689a39d21a608cfbb90568db56f3d0c9c8b3430e0c3024e9" + }, + "packet_builder": { + "path": "scripts/build_telegram_visible_unseen_chain_packet.py", + "sha256": "623452bbd577f3576b38412f3578cbe341d4c310b6e9657b1b9874588e38e4a0" + }, + "state_collector": { + "path": "scripts/collect_telegram_visible_unseen_chain_state.py", + "sha256": "4fa5174e79ec8f5224e42c4e3d01f5df019a17073275d51e32e1a57a06be481d" + } + } +} diff --git a/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.md b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.md new file mode 100644 index 0000000..1e6d25e --- /dev/null +++ b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.md @@ -0,0 +1,64 @@ +# Telegram-Visible Unseen Chain Authorization Packet + +Generated UTC: `2026-07-15T01:32:53.837725+00:00` +Protocol SHA256: `c815dad151d640335eecbddd2ca7bb651c43f6c1d4e53642e12c8a35157ac2da` +Ready to request action-time authorization: `True` +Telegram-visible messages sent: `False` +Mutates DB: `False` + +## Exact Destination + +- Chat: `Leo` (`-5146042086`) +- Transport: `authenticated_chrome_telegram_ui` + +## Exact Messages + +### 1. OOS-CHAIN-01 + +Looking at what Leo currently believes about collective intelligence or coordination, pick one claim that would matter to a founder making a product decision but seems more confident than its support. Do not ask me for an ID. Show what the database actually says and what supports it, then tell me the first narrower claim you would test. Do not change anything yet. + +SHA256: `c63838b98448f4f4599e4cf2b004c6b36f602ad0d25c92e442a6f866cd2740fc` + +### 2. OOS-CHAIN-02 + +That still feels like you are polishing the same idea. Inspect the exact body and evidence you just chose: what is assumption versus observed support, what would falsify it, and which replacement claim or claims would you put into review? Keep them as proposals, not live knowledge. + +SHA256: `73346a98db4d7f74b49e1a5741b323d4216fd21f8346f6c4a4a69b1a6dd07946` + +### 3. OOS-CHAIN-03 + +I disagree with your first replacement because it jumps from a mechanism to a business outcome. Revise it with that objection, tell me what source would resolve the disagreement, and show the approval and apply boundary before it could become part of Leo. + +SHA256: `5f6b32b2c8c51cc594242ed9bdac849c744156ca2395efb7e8593c83c86286d4` + +## Checks + +- `authenticated_chrome_is_only_send_transport`: `True` +- `exact_prompts_imported_from_handler_verifier`: `True` +- `exact_three_turn_sequence`: `True` +- `first_prompt_supplies_no_row_id`: `True` +- `handler_checks_all_pass`: `True` +- `handler_outcomes_all_pass`: `True` +- `handler_prompt_ids_exact`: `True` +- `handler_proved_no_telegram_post`: `True` +- `handler_proved_read_only_tool_traces`: `True` +- `handler_proved_unchanged_fingerprint`: `True` +- `handler_receipt_passed`: `True` +- `handler_receipt_schema_supported`: `True` +- `handler_safety_gate_passed`: `True` +- `handler_tier_remains_below_telegram_visible`: `True` +- `packet_does_not_mutate_database`: `True` +- `packet_does_not_send`: `True` + +## Exact Action-Time Authorization + +I authorize Codex to use the already-authenticated Chrome Telegram UI to send exactly these three messages, in order and with no additional messages, to the Telegram group Leo (chat ID -5146042086), waiting for Leo's visible reply after each turn: [OOS-CHAIN-01] "Looking at what Leo currently believes about collective intelligence or coordination, pick one claim that would matter to a founder making a product decision but seems more confident than its support. Do not ask me for an ID. Show what the database actually says and what supports it, then tell me the first narrower claim you would test. Do not change anything yet." [OOS-CHAIN-02] "That still feels like you are polishing the same idea. Inspect the exact body and evidence you just chose: what is assumption versus observed support, what would falsify it, and which replacement claim or claims would you put into review? Keep them as proposals, not live knowledge." [OOS-CHAIN-03] "I disagree with your first replacement because it jumps from a mechanism to a business outcome. Revise it with that objection, tell me what source would resolve the disagreement, and show the approval and apply boundary before it could become part of Leo." Capture the visible replies, message IDs, timestamps, screenshot and accessibility evidence; perform only read-only DB/service readbacks; do not use the Bot API; do not stage, approve, apply, or otherwise mutate knowledge. + +## Clear CTA + +Reply with the exact authorization sentence in this packet. That sentence names the destination, transport, complete message bodies, ordering, evidence, and no-mutation boundary. + +## Claim Ceiling + +Current tier: `T0_spec_plus_prior_T2_handler_evidence` +Required tier: `T3_live_readonly` diff --git a/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.json b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.json new file mode 100644 index 0000000..b4bd066 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.json @@ -0,0 +1,84 @@ +{ + "capture_template": { + "capture_source": { + "captured_at_utc": "", + "captured_by": "", + "chat_id": "-5146042086", + "chat_label": "Leo", + "final_accessibility": "", + "final_screenshot": "", + "platform": "telegram", + "transport": "authenticated_chrome_telegram_ui" + }, + "extra_messages_sent": 0, + "messages": [ + { + "prompt_id": "OOS-CHAIN-01", + "reply": "", + "reply_at_utc": "", + "reply_message_id": "", + "sent_at_utc": "", + "sent_text": "Looking at what Leo currently believes about collective intelligence or coordination, pick one claim that would matter to a founder making a product decision but seems more confident than its support. Do not ask me for an ID. Show what the database actually says and what supports it, then tell me the first narrower claim you would test. Do not change anything yet.", + "sequence": 1, + "telegram_message_id": "", + "turn_accessibility": "", + "turn_screenshot": "" + }, + { + "prompt_id": "OOS-CHAIN-02", + "reply": "", + "reply_at_utc": "", + "reply_message_id": "", + "sent_at_utc": "", + "sent_text": "That still feels like you are polishing the same idea. Inspect the exact body and evidence you just chose: what is assumption versus observed support, what would falsify it, and which replacement claim or claims would you put into review? Keep them as proposals, not live knowledge.", + "sequence": 2, + "telegram_message_id": "", + "turn_accessibility": "", + "turn_screenshot": "" + }, + { + "prompt_id": "OOS-CHAIN-03", + "reply": "", + "reply_at_utc": "", + "reply_message_id": "", + "sent_at_utc": "", + "sent_text": "I disagree with your first replacement because it jumps from a mechanism to a business outcome. Revise it with that objection, tell me what source would resolve the disagreement, and show the approval and apply boundary before it could become part of Leo.", + "sequence": 3, + "telegram_message_id": "", + "turn_accessibility": "", + "turn_screenshot": "" + } + ], + "operator_authorization_captured_at_utc": "", + "operator_authorization_text": "I authorize Codex to use the already-authenticated Chrome Telegram UI to send exactly these three messages, in order and with no additional messages, to the Telegram group Leo (chat ID -5146042086), waiting for Leo's visible reply after each turn: [OOS-CHAIN-01] \"Looking at what Leo currently believes about collective intelligence or coordination, pick one claim that would matter to a founder making a product decision but seems more confident than its support. Do not ask me for an ID. Show what the database actually says and what supports it, then tell me the first narrower claim you would test. Do not change anything yet.\" [OOS-CHAIN-02] \"That still feels like you are polishing the same idea. Inspect the exact body and evidence you just chose: what is assumption versus observed support, what would falsify it, and which replacement claim or claims would you put into review? Keep them as proposals, not live knowledge.\" [OOS-CHAIN-03] \"I disagree with your first replacement because it jumps from a mechanism to a business outcome. Revise it with that objection, tell me what source would resolve the disagreement, and show the approval and apply boundary before it could become part of Leo.\" Capture the visible replies, message IDs, timestamps, screenshot and accessibility evidence; perform only read-only DB/service readbacks; do not use the Bot API; do not stage, approve, apply, or otherwise mutate knowledge.", + "preflight_state_token_sha256": "a0af7235b62f88a009184f8f51b6de4b9aec008f6d93ebfa1458f9b77fc208ec", + "protocol_sha256": "c815dad151d640335eecbddd2ca7bb651c43f6c1d4e53642e12c8a35157ac2da" + }, + "checks": { + "action_time_authorization_present": false, + "capture_present": false, + "packet_ready": true, + "postflight_present": false, + "preflight_passed": true + }, + "claim_ceiling": { + "not_proven": [ + "Telegram-visible message delivery", + "Telegram-visible Leo replies", + "post-send fingerprint equality" + ], + "proven": "The exact packet and live zero-mutation preflight are ready." + }, + "current_tier": "T0_packet_plus_T3_preflight", + "generated_at_utc": "2026-07-15T01:33:10.948040+00:00", + "mode": "telegram_visible_unseen_chain_capture_verifier", + "mutates_db": false, + "packet_path": "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.json", + "postflight_path": "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-postflight-current.json", + "preflight_path": "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.json", + "receipt_ready": false, + "required_tier": "T3_live_readonly", + "schema": "livingip.telegramVisibleUnseenChainReceipt.v1", + "status": "awaiting_action_time_authorization", + "telegram_visible_messages_sent": false +} diff --git a/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.md b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.md new file mode 100644 index 0000000..5d0b2fc --- /dev/null +++ b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.md @@ -0,0 +1,25 @@ +# Telegram-Visible Unseen Chain Capture Receipt + +Generated UTC: `2026-07-15T01:33:10.948040+00:00` +Status: `awaiting_action_time_authorization` +Receipt ready: `False` +Current tier: `T0_packet_plus_T3_preflight` +Required tier: `T3_live_readonly` +Telegram-visible messages sent: `False` +Mutates DB: `False` + +## Checks + +- `action_time_authorization_present`: `False` +- `capture_present`: `False` +- `packet_ready`: `True` +- `postflight_present`: `False` +- `preflight_passed`: `True` + +## Awaiting Authorized Capture + +The packet and preflight are ready. No Telegram message has been sent by this verifier. + +## Claim Ceiling + +The exact packet and live zero-mutation preflight are ready. diff --git a/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-goal-readback-current.json b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-goal-readback-current.json new file mode 100644 index 0000000..6edb7a0 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-goal-readback-current.json @@ -0,0 +1,21 @@ +{ + "fallback_goal_path": "goal.md", + "generated_at_utc": "2026-07-15T01:26:37Z", + "platform_goal_after": { + "objective": "Complete and verify the Telegram-visible unseen challenge-to-revision canary defined in this goal file while proving zero database mutation.", + "status": "active", + "thread_id": "019f6350-3350-7040-b223-c5c22673fece" + }, + "platform_goal_before": null, + "platform_goal_current": { + "objective": "Complete and verify the Telegram-visible unseen challenge-to-revision canary defined in this goal file while proving zero database mutation.", + "status": "active", + "thread_id": "019f6350-3350-7040-b223-c5c22673fece", + "time_used_seconds_at_readback": 1116, + "tokens_used_at_readback": 211127 + }, + "platform_goal_repair_action": "create_goal", + "platform_goal_replaced": false, + "schema": "livingip.telegramVisibleUnseenChainGoalReadback.v1", + "why_not_replaced": "No active platform Goal existed, so the canonical Goal was created rather than replaced." +} diff --git a/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-nonsend-proof-current.json b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-nonsend-proof-current.json new file mode 100644 index 0000000..e34e552 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-nonsend-proof-current.json @@ -0,0 +1,62 @@ +{ + "cleanup_readback": { + "collector_or_verifier_processes_remaining": [], + "status": "clean" + }, + "generated_at_utc": "2026-07-15T01:37:18Z", + "live_preflight": { + "canonical_fingerprint_sha256": "32a1f2c18f4b4c623443cbd4d9520982e9f291ec74ca63855cef51c88ac56f24", + "canonical_fingerprint_unchanged_across_two_snapshots": true, + "deployed_git_stamp": "15c30f1fbe30c8f2295ddc33e293a45788a95706", + "gateway_main_pid": "347406", + "gateway_restarts": "0", + "mutates_db": false, + "path": "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.json", + "packet_file_sha256": "fb0ab4af0396b2064efce1ead44aacc0465ee0e3eacf9b4a7c07abae044d217a", + "protocol_sha256": "c815dad151d640335eecbddd2ca7bb651c43f6c1d4e53642e12c8a35157ac2da", + "readback_attempts": { + "deploy": 1, + "fingerprint_after": 1, + "fingerprint_before": 1, + "service_after": 1, + "service_before": 1 + }, + "state_token_sha256": "a0af7235b62f88a009184f8f51b6de4b9aec008f6d93ebfa1458f9b77fc208ec", + "tooling_hashes_match_current_sources": true, + "status": "pass", + "table_count": 39, + "total_rows": 52167 + }, + "not_tested": [ + "Telegram message delivery", + "Telegram-visible Leo replies", + "post-send selected-object readback", + "post-send canonical fingerprint equality" + ], + "proof_paths": [ + "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.json", + "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.json", + "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-capture-receipt-current.json", + "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-goal-readback-current.json" + ], + "schema": "livingip.telegramVisibleUnseenChainNonsendProof.v1", + "send_boundary": { + "action_time_authorization_present": false, + "authenticated_chrome_used": false, + "telegram_visible_messages_sent": false + }, + "tests": [ + { + "command": "/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/.venv/bin/pytest -q tests/test_verify_leo_unseen_reasoning_chain.py tests/test_build_telegram_visible_unseen_chain_packet.py tests/test_collect_telegram_visible_unseen_chain_state.py tests/test_verify_telegram_visible_unseen_chain.py", + "result": "24 passed in 0.16s" + }, + { + "command": "/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/.venv/bin/pytest -q", + "result": "1293 passed in 102.03s" + }, + { + "command": "/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/.venv/bin/ruff check ", + "result": "All checks passed" + } + ] +} diff --git a/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.json b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.json new file mode 100644 index 0000000..3af9b9e --- /dev/null +++ b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.json @@ -0,0 +1,115 @@ +{ + "baseline_checks": {}, + "baseline_path": "", + "blocker_readback": { + "attempted_routes": [ + "local exact packet validation", + "SSH deploy stamp and gateway service readback", + "two canonical repeatable-read read-only Postgres parity manifests" + ], + "clear_CTA": "Use the packet's exact authorization sentence only when ready to cross the send boundary.", + "current_canary": "Read-only pre-send readiness for the exact three-turn unseen Leo chain in Telegram", + "exact_gate": "", + "next_non_user_action": "Run the authenticated-Chrome three-turn flow only after exact authorization, then collect postflight." + }, + "claim_ceiling": { + "current_tier": "T3_live_readonly", + "not_proven": [ + "Telegram-visible delivery or reply behavior", + "authorization to send Telegram messages", + "canonical mutation" + ], + "proven": "Two canonical read-only snapshots and the gateway process were unchanged during this probe." + }, + "generated_at_utc": "2026-07-15T01:33:10.551000+00:00", + "mode": "telegram_visible_unseen_chain_zero_mutation_state_readback", + "mutates_db": false, + "packet_checks": { + "packet_does_not_mutate_db": true, + "packet_has_no_authorization_recorded": true, + "packet_messages_exact": true, + "packet_not_sent": true, + "packet_prompt_ids_exact": true, + "packet_protocol_sha256_present": true, + "packet_ready_for_authorization_request": true, + "packet_requires_authenticated_chrome": true, + "packet_schema_supported": true, + "packet_targets_exact_leo_chat": true, + "packet_tooling_hashes_match_current_sources": true + }, + "packet_file_sha256": "fb0ab4af0396b2064efce1ead44aacc0465ee0e3eacf9b4a7c07abae044d217a", + "packet_path": "docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-authorization-packet-current.json", + "phase": "preflight", + "production_apply_executed": false, + "protocol_sha256": "c815dad151d640335eecbddd2ca7bb651c43f6c1d4e53642e12c8a35157ac2da", + "ready_for_action_time_authorization": true, + "ready_for_visible_capture_verification": false, + "remote": { + "deploy_head": "15c30f1fbe30c8f2295ddc33e293a45788a95706", + "deploy_stamp_ancestor": true, + "fingerprint_after": { + "fingerprint_sha256": "32a1f2c18f4b4c623443cbd4d9520982e9f291ec74ca63855cef51c88ac56f24", + "schema": "livingip.teleoCanonicalDatabaseFingerprint.v1", + "status": "ok", + "structure_sha256": "61bf482330a652ebdb419de3fb78196f2c7d25d1a482b100f385e8102c8e8d52", + "table_count": 39, + "table_rows_sha256": "f50d86fc3ce699f602bdc0029485bd901fd98bd8114eecd5dcc4346a0f52112a", + "total_rows": 52167, + "transaction_read_only": "on" + }, + "fingerprint_before": { + "fingerprint_sha256": "32a1f2c18f4b4c623443cbd4d9520982e9f291ec74ca63855cef51c88ac56f24", + "schema": "livingip.teleoCanonicalDatabaseFingerprint.v1", + "status": "ok", + "structure_sha256": "61bf482330a652ebdb419de3fb78196f2c7d25d1a482b100f385e8102c8e8d52", + "table_count": 39, + "table_rows_sha256": "f50d86fc3ce699f602bdc0029485bd901fd98bd8114eecd5dcc4346a0f52112a", + "total_rows": 52167, + "transaction_read_only": "on" + }, + "last_deploy_sha": "15c30f1fbe30c8f2295ddc33e293a45788a95706", + "readback_attempts": { + "deploy": 1, + "fingerprint_after": 1, + "fingerprint_before": 1, + "service_after": 1, + "service_before": 1 + }, + "service_after": { + "ActiveState": "active", + "ExecMainStartTimestamp": "Tue 2026-07-14 22:59:43 UTC", + "MainPID": "347406", + "NRestarts": "0", + "SubState": "running" + }, + "service_before": { + "ActiveState": "active", + "ExecMainStartTimestamp": "Tue 2026-07-14 22:59:43 UTC", + "MainPID": "347406", + "NRestarts": "0", + "SubState": "running" + }, + "subject_readback": null, + "workspace_head_matches_deployed_stamp": true + }, + "remote_checks": { + "canonical_fingerprint_unchanged_during_probe": true, + "deployed_stamp_is_ancestor_of_workspace_head": true, + "deployed_stamp_is_sha": true, + "fingerprints_are_read_only": true, + "first_fingerprint_complete": true, + "second_fingerprint_complete": true, + "service_active_before_and_after": true, + "service_identity_complete": true, + "service_process_unchanged_during_probe": true, + "ssh_readbacks_succeeded": true, + "workspace_head_is_sha": true + }, + "remote_returncode": 0, + "remote_stderr": "", + "schema": "livingip.telegramVisibleUnseenChainState.v1", + "state_token_sha256": "a0af7235b62f88a009184f8f51b6de4b9aec008f6d93ebfa1458f9b77fc208ec", + "status": "pass", + "subject_ref": "", + "telegram_visible_messages_sent_by_collector": false +} diff --git a/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.md b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.md new file mode 100644 index 0000000..c4e17c6 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/telegram-visible-unseen-chain-preflight-current.md @@ -0,0 +1,49 @@ +# Telegram-Visible Unseen Chain Preflight + +Generated UTC: `2026-07-15T01:33:10.551000+00:00` +Status: `pass` +Protocol SHA256: `c815dad151d640335eecbddd2ca7bb651c43f6c1d4e53642e12c8a35157ac2da` +Packet file SHA256: `fb0ab4af0396b2064efce1ead44aacc0465ee0e3eacf9b4a7c07abae044d217a` +State token SHA256: `a0af7235b62f88a009184f8f51b6de4b9aec008f6d93ebfa1458f9b77fc208ec` +Messages sent by collector: `False` +Mutates DB: `False` + +## Checks + +- `packet_does_not_mutate_db`: `True` +- `packet_has_no_authorization_recorded`: `True` +- `packet_messages_exact`: `True` +- `packet_not_sent`: `True` +- `packet_prompt_ids_exact`: `True` +- `packet_protocol_sha256_present`: `True` +- `packet_ready_for_authorization_request`: `True` +- `packet_requires_authenticated_chrome`: `True` +- `packet_schema_supported`: `True` +- `packet_targets_exact_leo_chat`: `True` +- `packet_tooling_hashes_match_current_sources`: `True` +- `canonical_fingerprint_unchanged_during_probe`: `True` +- `deployed_stamp_is_ancestor_of_workspace_head`: `True` +- `deployed_stamp_is_sha`: `True` +- `fingerprints_are_read_only`: `True` +- `first_fingerprint_complete`: `True` +- `second_fingerprint_complete`: `True` +- `service_active_before_and_after`: `True` +- `service_identity_complete`: `True` +- `service_process_unchanged_during_probe`: `True` +- `ssh_readbacks_succeeded`: `True` +- `workspace_head_is_sha`: `True` + +## Live Readback + +- Deploy head: `15c30f1fbe30c8f2295ddc33e293a45788a95706` +- Deploy stamp: `15c30f1fbe30c8f2295ddc33e293a45788a95706` +- Gateway MainPID: `347406` +- Gateway NRestarts: `0` +- Canonical fingerprint: `32a1f2c18f4b4c623443cbd4d9520982e9f291ec74ca63855cef51c88ac56f24` +- Canonical tables: `39` +- Canonical rows: `52167` +- Selected object ref: `` + +## Claim Ceiling + +Two canonical read-only snapshots and the gateway process were unchanged during this probe. diff --git a/scripts/build_telegram_visible_unseen_chain_packet.py b/scripts/build_telegram_visible_unseen_chain_packet.py new file mode 100644 index 0000000..8a22800 --- /dev/null +++ b/scripts/build_telegram_visible_unseen_chain_packet.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +"""Build the exact no-send packet for Leo's unseen Telegram reasoning chain.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +try: + import verify_leo_unseen_reasoning_chain as unseen +except ImportError: # pragma: no cover - imported as scripts.* in tests + from scripts import verify_leo_unseen_reasoning_chain as unseen + +SCHEMA = "livingip.telegramVisibleUnseenChainPacket.v1" +REPORT_DIR = Path("docs/reports/leo-working-state-20260709") +DEFAULT_HANDLER_RECEIPT = REPORT_DIR / "leo-unseen-reasoning-chain-canary-current.json" +DEFAULT_OUT = REPORT_DIR / "telegram-visible-unseen-chain-authorization-packet-current.json" +DEFAULT_MARKDOWN_OUT = REPORT_DIR / "telegram-visible-unseen-chain-authorization-packet-current.md" +DEFAULT_CAPTURE_RECEIPT = REPORT_DIR / "telegram-visible-unseen-chain-capture-receipt-current.json" +DEFAULT_PREFLIGHT = REPORT_DIR / "telegram-visible-unseen-chain-preflight-current.json" +DEFAULT_POSTFLIGHT = REPORT_DIR / "telegram-visible-unseen-chain-postflight-current.json" +DEFAULT_CHAT_ID = "-5146042086" +DEFAULT_CHAT_LABEL = "Leo" +DEFAULT_PROOF_DIR = "outputs/telegram-visible-unseen-chain-20260715" +TOOLING_PATHS = { + "packet_builder": Path("scripts/build_telegram_visible_unseen_chain_packet.py"), + "state_collector": Path("scripts/collect_telegram_visible_unseen_chain_state.py"), + "capture_verifier": Path("scripts/verify_telegram_visible_unseen_chain.py"), +} + + +def _load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def _sha256_file(path: Path) -> str: + return _sha256_bytes(path.read_bytes()) + + +def _canonical_sha256(value: Any) -> str: + encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + return _sha256_bytes(encoded) + + +def current_git_sha(repo_root: Path = Path(".")) -> str: + proc = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=repo_root, + text=True, + capture_output=True, + check=False, + ) + return proc.stdout.strip() if proc.returncode == 0 else "" + + +def exact_messages() -> list[dict[str, Any]]: + messages = [] + for sequence, prompt in enumerate(unseen.PROMPTS, start=1): + message = str(prompt["message"]) + messages.append( + { + "sequence": sequence, + "prompt_id": prompt["id"], + "dimension": prompt["dimension"], + "message": message, + "message_sha256": _sha256_bytes(message.encode("utf-8")), + "wait_for_visible_reply_before_next": True, + } + ) + return messages + + +def tooling_binding(repo_root: Path = Path(".")) -> dict[str, dict[str, Any]]: + return {name: {"path": str(path), "sha256": _sha256_file(repo_root / path)} for name, path in TOOLING_PATHS.items()} + + +def _handler_checks(receipt: dict[str, Any]) -> dict[str, bool]: + safety = receipt.get("safety_gate") if isinstance(receipt.get("safety_gate"), dict) else {} + safety_checks = safety.get("checks") if isinstance(safety.get("checks"), dict) else {} + database = receipt.get("database") if isinstance(receipt.get("database"), dict) else {} + prompt_ids = [str(item["id"]) for item in unseen.PROMPTS] + return { + "handler_receipt_passed": receipt.get("status") == "pass", + "handler_receipt_schema_supported": receipt.get("schema") == unseen.SCHEMA, + "handler_prompt_ids_exact": receipt.get("prompt_ids") == prompt_ids, + "handler_outcomes_all_pass": bool(receipt.get("outcomes")) + and all(value is True for value in receipt.get("outcomes", {}).values()), + "handler_checks_all_pass": bool(receipt.get("checks")) + and all(value is True for value in receipt.get("checks", {}).values()), + "handler_safety_gate_passed": safety.get("status") == "pass", + "handler_proved_no_telegram_post": safety_checks.get("no_telegram_post") is True, + "handler_proved_read_only_tool_traces": safety_checks.get("all_tool_traces_read_only_and_bound") is True, + "handler_proved_unchanged_fingerprint": database.get("fingerprint_unchanged") is True + and isinstance(database.get("fingerprint_sha256"), str) + and len(database["fingerprint_sha256"]) == 64, + "handler_tier_remains_below_telegram_visible": "Telegram-visible message delivery" + in (receipt.get("claim_ceiling") or {}).get("not_proven", []), + } + + +def _authorization_text(*, chat_label: str, chat_id: str, messages: list[dict[str, Any]]) -> str: + quoted = " ".join(f"[{item['prompt_id']}] {json.dumps(item['message'], ensure_ascii=True)}" for item in messages) + return ( + "I authorize Codex to use the already-authenticated Chrome Telegram UI to send exactly these three " + f"messages, in order and with no additional messages, to the Telegram group {chat_label} (chat ID " + f"{chat_id}), waiting for Leo's visible reply after each turn: {quoted} Capture the visible replies, " + "message IDs, timestamps, screenshot and accessibility evidence; perform only read-only DB/service " + "readbacks; do not use the Bot API; do not stage, approve, apply, or otherwise mutate knowledge." + ) + + +def build_packet( + *, + handler_receipt_path: Path = DEFAULT_HANDLER_RECEIPT, + chat_id: str = DEFAULT_CHAT_ID, + chat_label: str = DEFAULT_CHAT_LABEL, + proof_dir: str = DEFAULT_PROOF_DIR, + git_sha: str | None = None, +) -> dict[str, Any]: + handler_receipt = _load_json(handler_receipt_path) + messages = exact_messages() + handler_checks = _handler_checks(handler_receipt) + target = { + "platform": "telegram", + "chat_label": chat_label, + "chat_id": chat_id, + "allowed_transport": "authenticated_chrome_telegram_ui", + "forbidden_transports": ["telegram_bot_api", "copied_transcript", "handler_substitution"], + "message_count": len(messages), + "expected_prompt_ids": [item["prompt_id"] for item in messages], + } + handler_binding = { + "path": str(handler_receipt_path), + "sha256": _sha256_file(handler_receipt_path), + "schema": handler_receipt.get("schema"), + "proof_tier": handler_receipt.get("proof_tier"), + "remote_run_id": handler_receipt.get("remote_run_id"), + "deployed_git_head": handler_receipt.get("deployed_git_head"), + "fingerprint_sha256": (handler_receipt.get("database") or {}).get("fingerprint_sha256"), + } + tooling = tooling_binding() + protocol = { + "target": target, + "exact_messages": messages, + "handler_binding": handler_binding, + "tooling_binding": tooling, + "state_contract": { + "preflight": "two identical repeatable-read read-only canonical fingerprints", + "postflight": "same canonical fingerprint plus independent selected-object readback", + "service": "same active gateway process before and after", + }, + } + protocol_sha256 = _canonical_sha256(protocol) + checks = { + **handler_checks, + "exact_three_turn_sequence": len(messages) == 3 and [item["sequence"] for item in messages] == [1, 2, 3], + "exact_prompts_imported_from_handler_verifier": [item["message"] for item in messages] + == [item["message"] for item in unseen.PROMPTS], + "first_prompt_supplies_no_row_id": unseen.UUID_RE.search(messages[0]["message"]) is None, + "authenticated_chrome_is_only_send_transport": target["allowed_transport"] + == "authenticated_chrome_telegram_ui", + "packet_does_not_send": True, + "packet_does_not_mutate_database": True, + } + ready = all(checks.values()) + authorization_text = _authorization_text(chat_label=chat_label, chat_id=chat_id, messages=messages) + proof_paths = { + "preflight_json": str(DEFAULT_PREFLIGHT), + "postflight_json": str(DEFAULT_POSTFLIGHT), + "capture_receipt_json": str(DEFAULT_CAPTURE_RECEIPT), + "capture_json": f"{proof_dir}/capture.json", + "turn_screenshots": [f"{proof_dir}/turn-{index}.png" for index in range(1, 4)], + "turn_accessibility": [f"{proof_dir}/turn-{index}-accessibility.txt" for index in range(1, 4)], + "final_screenshot": f"{proof_dir}/final.png", + "final_accessibility": f"{proof_dir}/final-accessibility.txt", + } + return { + "schema": SCHEMA, + "generated_at_utc": datetime.now(timezone.utc).isoformat(), + "mode": "telegram_visible_unseen_chain_exact_packet_not_sent", + "packet_generation_git_sha": git_sha if git_sha is not None else current_git_sha(), + "protocol_sha256": protocol_sha256, + "ready_to_request_action_time_authorization": ready, + "authorization_gate_status": "ready_to_request_exact_authorization" if ready else "not_ready", + "telegram_visible_messages_sent": False, + "telegram_visible_message_authorization_present": False, + "production_apply_executed": False, + "mutates_db": False, + "target": target, + "exact_messages": messages, + "handler_receipt_binding": handler_binding, + "tooling_binding": tooling, + "checks": checks, + "expected_proof_paths_after_authorized_run": proof_paths, + "explicit_operator_authorization_text": authorization_text, + "clear_CTA": ( + "Reply with the exact authorization sentence in this packet. That sentence names the destination, " + "transport, complete message bodies, ordering, evidence, and no-mutation boundary." + ), + "next_non_user_action_after_authorization": [ + "Re-run the zero-mutation preflight and confirm its packet file hash is current.", + "Use authenticated Chrome only and verify the Leo chat ID before typing anything.", + "Send one exact message, wait for Leo's visible reply, then continue to the next exact message.", + "Capture message IDs, timestamps, screenshot, and accessibility text without copying handler output.", + "Extract the selected DB object reference and run the read-only postflight with that reference.", + "Run the Telegram-visible verifier and accept T3 only if every visible and state check passes.", + ], + "claim_ceiling": { + "current_tier": "T0_spec_plus_prior_T2_handler_evidence", + "required_tier": "T3_live_readonly", + "proven": "The exact three-turn Chrome send packet is bound to a passing no-send handler receipt.", + "not_proven": [ + "Telegram-visible delivery of these three messages", + "Telegram-visible replies from Leo", + "post-send canonical fingerprint equality", + ], + }, + } + + +def write_json(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def write_markdown(path: Path, data: dict[str, Any]) -> None: + target = data["target"] + lines = [ + "# Telegram-Visible Unseen Chain Authorization Packet", + "", + f"Generated UTC: `{data['generated_at_utc']}`", + f"Protocol SHA256: `{data['protocol_sha256']}`", + f"Ready to request action-time authorization: `{data['ready_to_request_action_time_authorization']}`", + f"Telegram-visible messages sent: `{data['telegram_visible_messages_sent']}`", + f"Mutates DB: `{data['mutates_db']}`", + "", + "## Exact Destination", + "", + f"- Chat: `{target['chat_label']}` (`{target['chat_id']}`)", + f"- Transport: `{target['allowed_transport']}`", + "", + "## Exact Messages", + "", + ] + for item in data["exact_messages"]: + lines.extend( + [ + f"### {item['sequence']}. {item['prompt_id']}", + "", + item["message"], + "", + f"SHA256: `{item['message_sha256']}`", + "", + ] + ) + lines.extend(["## Checks", ""]) + lines.extend(f"- `{key}`: `{value}`" for key, value in sorted(data["checks"].items())) + lines.extend( + [ + "", + "## Exact Action-Time Authorization", + "", + data["explicit_operator_authorization_text"], + "", + "## Clear CTA", + "", + data["clear_CTA"], + "", + "## Claim Ceiling", + "", + f"Current tier: `{data['claim_ceiling']['current_tier']}`", + f"Required tier: `{data['claim_ceiling']['required_tier']}`", + "", + ] + ) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines), encoding="utf-8") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--handler-receipt", type=Path, default=DEFAULT_HANDLER_RECEIPT) + parser.add_argument("--chat-id", default=DEFAULT_CHAT_ID) + parser.add_argument("--chat-label", default=DEFAULT_CHAT_LABEL) + parser.add_argument("--proof-dir", default=DEFAULT_PROOF_DIR) + parser.add_argument("--out", type=Path, default=DEFAULT_OUT) + parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MARKDOWN_OUT) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + data = build_packet( + handler_receipt_path=args.handler_receipt, + chat_id=args.chat_id, + chat_label=args.chat_label, + proof_dir=args.proof_dir, + ) + write_json(args.out, data) + write_markdown(args.markdown_out, data) + print( + json.dumps( + { + "out": str(args.out), + "markdown_out": str(args.markdown_out), + "ready_to_request_action_time_authorization": data["ready_to_request_action_time_authorization"], + "telegram_visible_messages_sent": False, + }, + indent=2, + sort_keys=True, + ) + ) + return 0 if data["ready_to_request_action_time_authorization"] else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/collect_telegram_visible_unseen_chain_state.py b/scripts/collect_telegram_visible_unseen_chain_state.py new file mode 100644 index 0000000..2558539 --- /dev/null +++ b/scripts/collect_telegram_visible_unseen_chain_state.py @@ -0,0 +1,616 @@ +#!/usr/bin/env python3 +"""Collect fail-closed read-only state for the unseen Telegram chain.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import shlex +import subprocess +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +try: + import build_telegram_visible_unseen_chain_packet as packet_builder + import verify_leo_unseen_reasoning_chain as unseen +except ImportError: # pragma: no cover - imported as scripts.* in tests + from scripts import build_telegram_visible_unseen_chain_packet as packet_builder + from scripts import verify_leo_unseen_reasoning_chain as unseen + +SCHEMA = "livingip.telegramVisibleUnseenChainState.v1" +REPORT_DIR = Path("docs/reports/leo-working-state-20260709") +DEFAULT_PACKET = packet_builder.DEFAULT_OUT +DEFAULT_PREFLIGHT_OUT = REPORT_DIR / "telegram-visible-unseen-chain-preflight-current.json" +DEFAULT_PREFLIGHT_MARKDOWN_OUT = REPORT_DIR / "telegram-visible-unseen-chain-preflight-current.md" +DEFAULT_POSTFLIGHT_OUT = REPORT_DIR / "telegram-visible-unseen-chain-postflight-current.json" +DEFAULT_POSTFLIGHT_MARKDOWN_OUT = REPORT_DIR / "telegram-visible-unseen-chain-postflight-current.md" +DEFAULT_SSH_TARGET = "root@77.42.65.182" +DEFAULT_SSH_KEY = Path.home() / ".ssh/livingip_hetzner_20260604_ed25519" +DEFAULT_MANIFEST_SQL = Path("ops/postgres_parity_manifest.sql") +REMOTE_REPO = "/opt/teleo-eval/workspaces/deploy-infra" +SUBJECT_REF_RE = re.compile( + r"^(?:[0-9a-f]{8}|[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + re.IGNORECASE, +) +SHA40_RE = re.compile(r"^[0-9a-f]{40}$") + + +@dataclass(frozen=True) +class CommandResult: + returncode: int + stdout: str + stderr: str + + +Runner = Callable[[list[str], str, int], CommandResult] + + +def subprocess_runner(command: list[str], input_text: str, timeout: int) -> CommandResult: + try: + proc = subprocess.run( + command, + input=input_text, + text=True, + capture_output=True, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired as exc: + return CommandResult(124, str(exc.stdout or ""), f"command timed out after {timeout}s") + return CommandResult(proc.returncode, proc.stdout, proc.stderr) + + +def _load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _canonical_sha256(value: Any) -> str: + encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def _sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def parse_key_value_lines(text: str) -> dict[str, str]: + values: dict[str, str] = {} + for line in text.splitlines(): + if "=" not in line: + continue + key, value = line.split("=", 1) + values[key] = value.strip() + return values + + +def parse_manifest_output(raw: str) -> dict[str, Any]: + rows = [] + try: + for line in raw.splitlines(): + candidate = line.strip() + if not candidate or candidate in {"BEGIN", "SET", "ROLLBACK"}: + continue + value = json.loads(candidate) + if isinstance(value, dict): + rows.append(value) + except (TypeError, json.JSONDecodeError) as exc: + return {"status": "error", "error": f"manifest_parse_error:{type(exc).__name__}"} + + tables = { + f"{row['schema']}.{row['table']}": { + "row_count": int(row["row_count"]), + "rowset_md5": row["rowset_md5"], + } + for row in rows + if row.get("kind") == "table" + } + singleton_kinds = { + "schemas", + "extensions", + "application_roles", + "columns", + "constraints", + "indexes", + "sequences", + "views", + "functions", + "triggers", + "types", + "policies", + } + singleton = {str(row["kind"]): row.get("items", []) for row in rows if row.get("kind") in singleton_kinds} + identity_rows = [row for row in rows if row.get("kind") == "identity"] + if len(identity_rows) != 1 or not tables or singleton_kinds - set(singleton): + return {"status": "error", "error": "canonical_database_fingerprint_manifest_incomplete"} + identity = identity_rows[0] + stable = { + "identity": { + key: identity.get(key) + for key in ( + "database", + "server_version_num", + "server_encoding", + "database_collation", + "database_ctype", + "transaction_read_only", + ) + }, + "singleton_sha256": {key: _canonical_sha256(value) for key, value in sorted(singleton.items())}, + "tables": tables, + } + return { + "schema": "livingip.teleoCanonicalDatabaseFingerprint.v1", + "status": "ok", + "fingerprint_sha256": _canonical_sha256(stable), + "table_count": len(tables), + "total_rows": sum(item["row_count"] for item in tables.values()), + "table_rows_sha256": _canonical_sha256(tables), + "structure_sha256": _canonical_sha256(stable["singleton_sha256"]), + "transaction_read_only": identity.get("transaction_read_only"), + } + + +def packet_checks(packet: dict[str, Any]) -> dict[str, bool]: + target = packet.get("target") if isinstance(packet.get("target"), dict) else {} + exact = packet_builder.exact_messages() + expected_tooling = packet_builder.tooling_binding() + return { + "packet_schema_supported": packet.get("schema") == packet_builder.SCHEMA, + "packet_ready_for_authorization_request": packet.get("ready_to_request_action_time_authorization") is True, + "packet_not_sent": packet.get("telegram_visible_messages_sent") is False, + "packet_has_no_authorization_recorded": packet.get("telegram_visible_message_authorization_present") is False, + "packet_does_not_mutate_db": packet.get("mutates_db") is False, + "packet_targets_exact_leo_chat": target.get("chat_label") == packet_builder.DEFAULT_CHAT_LABEL + and target.get("chat_id") == packet_builder.DEFAULT_CHAT_ID, + "packet_requires_authenticated_chrome": target.get("allowed_transport") == "authenticated_chrome_telegram_ui", + "packet_prompt_ids_exact": target.get("expected_prompt_ids") == [item["id"] for item in unseen.PROMPTS], + "packet_messages_exact": [item.get("message") for item in packet.get("exact_messages", [])] + == [item["message"] for item in exact], + "packet_protocol_sha256_present": isinstance(packet.get("protocol_sha256"), str) + and len(packet["protocol_sha256"]) == 64, + "packet_tooling_hashes_match_current_sources": packet.get("tooling_binding") == expected_tooling, + } + + +def ssh_command(args: argparse.Namespace, remote_command: str) -> list[str]: + return [ + "ssh", + "-i", + str(args.ssh_key), + "-o", + "BatchMode=yes", + "-o", + "StrictHostKeyChecking=accept-new", + "-o", + f"ConnectTimeout={args.connect_timeout}", + args.ssh_target, + remote_command, + ] + + +def deploy_readback_command() -> str: + repo = shlex.quote(REMOTE_REPO) + return ( + f"cd {repo} && " + "deploy_head=$(git rev-parse HEAD) && " + "last_deploy_sha=$(cat /opt/teleo-eval/.last-deploy-sha) && " + 'printf \'deploy_head=%s\\nlast_deploy_sha=%s\\n\' "$deploy_head" "$last_deploy_sha" && ' + 'if git merge-base --is-ancestor "$last_deploy_sha" "$deploy_head"; then ' + "printf 'deploy_stamp_ancestor=true\\n'; else printf 'deploy_stamp_ancestor=false\\n'; fi" + ) + + +def service_readback_command() -> str: + return ( + "systemctl show leoclean-gateway.service " + "-p ActiveState -p SubState -p MainPID -p NRestarts -p ExecMainStartTimestamp --no-pager" + ) + + +def fingerprint_readback_command() -> str: + return "docker exec -i teleo-pg psql -U postgres -d teleo -At -q -v ON_ERROR_STOP=1" + + +def subject_readback_sql(subject_ref: str) -> str: + if not SUBJECT_REF_RE.fullmatch(subject_ref): + raise ValueError("subject_ref must be an eight-hex prefix or UUID") + prefix = subject_ref.casefold() + return f""" +\\set ON_ERROR_STOP on +begin transaction isolation level repeatable read read only; +select jsonb_build_object( + 'subject_ref', '{prefix}', + 'matches', coalesce(jsonb_agg(item order by item->>'table', item->'row'->>'id'), '[]'::jsonb) +)::text +from ( + select jsonb_build_object( + 'table', 'public.claims', + 'row', to_jsonb(claim_row), + 'evidence_count', (select count(*) from public.claim_evidence evidence where evidence.claim_id = claim_row.id) + ) as item + from public.claims claim_row + where lower(claim_row.id::text) like '{prefix}%' + union all + select jsonb_build_object( + 'table', 'public.beliefs', + 'row', to_jsonb(belief_row), + 'evidence_count', null + ) as item + from public.beliefs belief_row + where lower(belief_row.id::text) like '{prefix}%' +) matched; +rollback; +""".strip() + + +def _parse_single_json_line(raw: str) -> dict[str, Any] | None: + for line in raw.splitlines(): + candidate = line.strip() + if not candidate or candidate in {"BEGIN", "SET", "ROLLBACK"}: + continue + try: + value = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(value, dict): + return value + return None + + +def collect_remote(args: argparse.Namespace, runner: Runner) -> tuple[dict[str, Any], int, str]: + manifest_sql = args.manifest_sql.read_text(encoding="utf-8") + calls: list[tuple[str, list[str], str, int]] = [ + ("deploy", ssh_command(args, deploy_readback_command()), "", args.timeout), + ("service_before", ssh_command(args, service_readback_command()), "", args.timeout), + ( + "fingerprint_before", + ssh_command(args, fingerprint_readback_command()), + manifest_sql, + args.fingerprint_timeout, + ), + ( + "fingerprint_after", + ssh_command(args, fingerprint_readback_command()), + manifest_sql, + args.fingerprint_timeout, + ), + ("service_after", ssh_command(args, service_readback_command()), "", args.timeout), + ] + if args.subject_ref: + calls.append( + ( + "subject", + ssh_command(args, fingerprint_readback_command()), + subject_readback_sql(args.subject_ref), + args.timeout, + ) + ) + results: dict[str, CommandResult] = {} + attempts: dict[str, int] = {} + for name, command, input_text, timeout in calls: + result = CommandResult(1, "", "not attempted") + for attempt in range(1, max(1, args.ssh_attempts) + 1): + result = runner(command, input_text, timeout) + attempts[name] = attempt + if result.returncode == 0: + break + results[name] = result + deploy_values = parse_key_value_lines(results["deploy"].stdout) + service_before = parse_key_value_lines(results["service_before"].stdout) + service_after = parse_key_value_lines(results["service_after"].stdout) + fingerprint_before = parse_manifest_output(results["fingerprint_before"].stdout) + fingerprint_after = parse_manifest_output(results["fingerprint_after"].stdout) + subject = _parse_single_json_line(results["subject"].stdout) if "subject" in results else None + returncode = next((result.returncode for result in results.values() if result.returncode != 0), 0) + stderr = "\n".join(f"{name}: {result.stderr.strip()}" for name, result in results.items() if result.stderr.strip()) + return ( + { + "deploy_head": deploy_values.get("deploy_head", ""), + "last_deploy_sha": deploy_values.get("last_deploy_sha", ""), + "deploy_stamp_ancestor": deploy_values.get("deploy_stamp_ancestor") == "true", + "workspace_head_matches_deployed_stamp": deploy_values.get("deploy_head") + == deploy_values.get("last_deploy_sha"), + "service_before": service_before, + "service_after": service_after, + "fingerprint_before": fingerprint_before, + "fingerprint_after": fingerprint_after, + "subject_readback": subject, + "readback_attempts": attempts, + }, + returncode, + stderr, + ) + + +def _service_active(state: dict[str, Any]) -> bool: + return state.get("ActiveState") == "active" and state.get("SubState") == "running" + + +def _service_identity(state: dict[str, Any]) -> dict[str, Any]: + return {key: state.get(key) for key in ("MainPID", "NRestarts", "ExecMainStartTimestamp")} + + +def _remote_checks( + remote: dict[str, Any], + *, + returncode: int, + subject_required: bool, + expected_subject_ref: str | None, +) -> dict[str, bool]: + before = remote.get("fingerprint_before") or {} + after = remote.get("fingerprint_after") or {} + service_before = remote.get("service_before") or {} + service_after = remote.get("service_after") or {} + subject = remote.get("subject_readback") or {} + matches = subject.get("matches") if isinstance(subject, dict) else None + checks = { + "ssh_readbacks_succeeded": returncode == 0, + "workspace_head_is_sha": bool(SHA40_RE.fullmatch(str(remote.get("deploy_head") or ""))), + "deployed_stamp_is_sha": bool(SHA40_RE.fullmatch(str(remote.get("last_deploy_sha") or ""))), + "deployed_stamp_is_ancestor_of_workspace_head": remote.get("deploy_stamp_ancestor") is True, + "service_active_before_and_after": _service_active(service_before) and _service_active(service_after), + "service_identity_complete": all( + value + for value in ( + service_before.get("MainPID"), + service_before.get("NRestarts"), + service_before.get("ExecMainStartTimestamp"), + service_after.get("MainPID"), + service_after.get("NRestarts"), + service_after.get("ExecMainStartTimestamp"), + ) + ), + "service_process_unchanged_during_probe": _service_identity(service_before) == _service_identity(service_after), + "first_fingerprint_complete": before.get("status") == "ok" and int(before.get("table_count") or 0) > 0, + "second_fingerprint_complete": after.get("status") == "ok" and int(after.get("table_count") or 0) > 0, + "fingerprints_are_read_only": before.get("transaction_read_only") == "on" + and after.get("transaction_read_only") == "on", + "canonical_fingerprint_unchanged_during_probe": bool(before.get("fingerprint_sha256")) + and before.get("fingerprint_sha256") == after.get("fingerprint_sha256"), + } + if subject_required: + checks.update( + { + "subject_readback_present": isinstance(subject, dict), + "subject_reference_matches_request": subject.get("subject_ref") + == str(expected_subject_ref or "").casefold(), + "subject_resolved_to_exactly_one_db_object": isinstance(matches, list) and len(matches) == 1, + } + ) + return checks + + +def collect_state(args: argparse.Namespace, *, runner: Runner = subprocess_runner) -> dict[str, Any]: + packet = _load_json(args.packet) + local_packet_checks = packet_checks(packet) + remote, remote_returncode, remote_stderr = collect_remote(args, runner) + remote_checks = _remote_checks( + remote, + returncode=remote_returncode, + subject_required=args.phase == "postflight", + expected_subject_ref=args.subject_ref, + ) + baseline = _load_json(args.baseline) if args.baseline else None + baseline_checks: dict[str, bool] = {} + if args.phase == "postflight": + baseline_checks = { + "baseline_supplied": baseline is not None, + "baseline_is_passing_preflight": bool( + baseline and baseline.get("phase") == "preflight" and baseline.get("status") == "pass" + ), + "baseline_protocol_matches_packet": bool( + baseline and baseline.get("protocol_sha256") == packet.get("protocol_sha256") + ), + "baseline_packet_file_matches": bool( + baseline and baseline.get("packet_file_sha256") == _sha256_file(args.packet) + ), + "canonical_fingerprint_matches_preflight": bool( + baseline + and (baseline.get("remote") or {}).get("fingerprint_after", {}).get("fingerprint_sha256") + == remote.get("fingerprint_after", {}).get("fingerprint_sha256") + ), + "gateway_process_matches_preflight": bool( + baseline + and _service_identity((baseline.get("remote") or {}).get("service_after") or {}) + == _service_identity(remote.get("service_after") or {}) + ), + } + status = ( + "pass" + if all(local_packet_checks.values()) and all(remote_checks.values()) and all(baseline_checks.values()) + else "fail" + ) + packet_file_sha256 = _sha256_file(args.packet) + state_token = _canonical_sha256( + { + "phase": args.phase, + "packet_file_sha256": packet_file_sha256, + "protocol_sha256": packet.get("protocol_sha256"), + "deploy_head": remote.get("deploy_head"), + "service": _service_identity(remote.get("service_after") or {}), + "fingerprint": remote.get("fingerprint_after"), + "subject_readback": remote.get("subject_readback"), + } + ) + exact_gate = "" + if status != "pass": + failed = [ + key + for checks in (local_packet_checks, remote_checks, baseline_checks) + for key, value in checks.items() + if value is not True + ] + exact_gate = remote_stderr.strip() or ", ".join(failed) + current_canary = ( + "Read-only pre-send readiness for the exact three-turn unseen Leo chain in Telegram" + if args.phase == "preflight" + else "Read-only post-send DB grounding and unchanged-state proof for the exact Telegram chain" + ) + return { + "schema": SCHEMA, + "generated_at_utc": datetime.now(timezone.utc).isoformat(), + "mode": "telegram_visible_unseen_chain_zero_mutation_state_readback", + "phase": args.phase, + "status": status, + "ready_for_action_time_authorization": args.phase == "preflight" and status == "pass", + "ready_for_visible_capture_verification": args.phase == "postflight" and status == "pass", + "telegram_visible_messages_sent_by_collector": False, + "production_apply_executed": False, + "mutates_db": False, + "packet_path": str(args.packet), + "packet_file_sha256": packet_file_sha256, + "protocol_sha256": packet.get("protocol_sha256"), + "baseline_path": str(args.baseline) if args.baseline else "", + "subject_ref": args.subject_ref or "", + "state_token_sha256": state_token, + "packet_checks": local_packet_checks, + "remote_checks": remote_checks, + "baseline_checks": baseline_checks, + "remote": remote, + "remote_returncode": remote_returncode, + "remote_stderr": remote_stderr, + "blocker_readback": { + "current_canary": current_canary, + "attempted_routes": [ + "local exact packet validation", + "SSH deploy stamp and gateway service readback", + "two canonical repeatable-read read-only Postgres parity manifests", + *(["independent selected-object DB readback"] if args.subject_ref else []), + ], + "exact_gate": exact_gate, + "clear_CTA": ( + "No user action is needed; repair the named readback check and rerun this collector." + if exact_gate + else "Use the packet's exact authorization sentence only when ready to cross the send boundary." + ), + "next_non_user_action": ( + "Run the authenticated-Chrome three-turn flow only after exact authorization, then collect postflight." + if args.phase == "preflight" and status == "pass" + else "Repair the failed preflight check and rerun before requesting authorization." + if args.phase == "preflight" + else "Run the visible capture verifier against this postflight and the retained Chrome evidence." + ), + }, + "claim_ceiling": { + "current_tier": "T3_live_readonly" if status == "pass" else "T0_failed_preflight", + "proven": ( + "Two canonical read-only snapshots and the gateway process were unchanged during this probe." + if status == "pass" + else "No live readiness claim; one or more fail-closed checks did not pass." + ), + "not_proven": [ + "Telegram-visible delivery or reply behavior", + "authorization to send Telegram messages", + "canonical mutation", + ], + }, + } + + +def write_json(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def write_markdown(path: Path, data: dict[str, Any]) -> None: + remote = data.get("remote") or {} + service = remote.get("service_after") or {} + fingerprint = remote.get("fingerprint_after") or {} + lines = [ + f"# Telegram-Visible Unseen Chain {data['phase'].title()}", + "", + f"Generated UTC: `{data['generated_at_utc']}`", + f"Status: `{data['status']}`", + f"Protocol SHA256: `{data['protocol_sha256']}`", + f"Packet file SHA256: `{data['packet_file_sha256']}`", + f"State token SHA256: `{data['state_token_sha256']}`", + f"Messages sent by collector: `{data['telegram_visible_messages_sent_by_collector']}`", + f"Mutates DB: `{data['mutates_db']}`", + "", + "## Checks", + "", + ] + for section in ("packet_checks", "remote_checks", "baseline_checks"): + for key, value in sorted(data.get(section, {}).items()): + lines.append(f"- `{key}`: `{value}`") + lines.extend( + [ + "", + "## Live Readback", + "", + f"- Deploy head: `{remote.get('deploy_head', '')}`", + f"- Deploy stamp: `{remote.get('last_deploy_sha', '')}`", + f"- Gateway MainPID: `{service.get('MainPID', '')}`", + f"- Gateway NRestarts: `{service.get('NRestarts', '')}`", + f"- Canonical fingerprint: `{fingerprint.get('fingerprint_sha256', '')}`", + f"- Canonical tables: `{fingerprint.get('table_count', '')}`", + f"- Canonical rows: `{fingerprint.get('total_rows', '')}`", + f"- Selected object ref: `{data.get('subject_ref', '')}`", + "", + "## Claim Ceiling", + "", + data["claim_ceiling"]["proven"], + "", + ] + ) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines), encoding="utf-8") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--phase", choices=("preflight", "postflight"), default="preflight") + parser.add_argument("--packet", type=Path, default=DEFAULT_PACKET) + parser.add_argument("--baseline", type=Path) + parser.add_argument("--subject-ref") + parser.add_argument("--manifest-sql", type=Path, default=DEFAULT_MANIFEST_SQL) + parser.add_argument("--ssh-target", default=DEFAULT_SSH_TARGET) + parser.add_argument("--ssh-key", type=Path, default=DEFAULT_SSH_KEY) + parser.add_argument("--connect-timeout", type=int, default=10) + parser.add_argument("--timeout", type=int, default=60) + parser.add_argument("--fingerprint-timeout", type=int, default=240) + parser.add_argument("--ssh-attempts", type=int, default=3) + parser.add_argument("--out", type=Path) + parser.add_argument("--markdown-out", type=Path) + args = parser.parse_args(argv) + if args.phase == "postflight" and (not args.baseline or not args.subject_ref): + parser.error("postflight requires --baseline and --subject-ref") + if args.subject_ref and not SUBJECT_REF_RE.fullmatch(args.subject_ref): + parser.error("--subject-ref must be an eight-hex prefix or UUID") + if args.out is None: + args.out = DEFAULT_PREFLIGHT_OUT if args.phase == "preflight" else DEFAULT_POSTFLIGHT_OUT + if args.markdown_out is None: + args.markdown_out = ( + DEFAULT_PREFLIGHT_MARKDOWN_OUT if args.phase == "preflight" else DEFAULT_POSTFLIGHT_MARKDOWN_OUT + ) + return args + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + data = collect_state(args) + write_json(args.out, data) + write_markdown(args.markdown_out, data) + print( + json.dumps( + { + "out": str(args.out), + "markdown_out": str(args.markdown_out), + "phase": args.phase, + "status": data["status"], + "mutates_db": False, + }, + indent=2, + sort_keys=True, + ) + ) + return 0 if data["status"] == "pass" else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_telegram_visible_unseen_chain.py b/scripts/verify_telegram_visible_unseen_chain.py new file mode 100644 index 0000000..8a92450 --- /dev/null +++ b/scripts/verify_telegram_visible_unseen_chain.py @@ -0,0 +1,524 @@ +#!/usr/bin/env python3 +"""Verify retained authenticated-Chrome proof for Leo's unseen Telegram chain.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +try: + import build_telegram_visible_unseen_chain_packet as packet_builder + import verify_leo_unseen_reasoning_chain as unseen +except ImportError: # pragma: no cover - imported as scripts.* in tests + from scripts import build_telegram_visible_unseen_chain_packet as packet_builder + from scripts import verify_leo_unseen_reasoning_chain as unseen + +SCHEMA = "livingip.telegramVisibleUnseenChainReceipt.v1" +REPORT_DIR = Path("docs/reports/leo-working-state-20260709") +DEFAULT_PACKET = packet_builder.DEFAULT_OUT +DEFAULT_PREFLIGHT = REPORT_DIR / "telegram-visible-unseen-chain-preflight-current.json" +DEFAULT_POSTFLIGHT = REPORT_DIR / "telegram-visible-unseen-chain-postflight-current.json" +DEFAULT_OUT = REPORT_DIR / "telegram-visible-unseen-chain-capture-receipt-current.json" +DEFAULT_MARKDOWN_OUT = REPORT_DIR / "telegram-visible-unseen-chain-capture-receipt-current.md" + + +def _load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _present(value: Any) -> bool: + if value is None: + return False + if isinstance(value, str): + return bool(value.strip()) + if isinstance(value, (dict, list)): + return bool(value) + return True + + +def blank_capture_template(packet: dict[str, Any], preflight: dict[str, Any]) -> dict[str, Any]: + target = packet.get("target") or {} + return { + "protocol_sha256": packet.get("protocol_sha256"), + "preflight_state_token_sha256": preflight.get("state_token_sha256"), + "operator_authorization_text": packet.get("explicit_operator_authorization_text"), + "operator_authorization_captured_at_utc": "", + "extra_messages_sent": 0, + "capture_source": { + "platform": "telegram", + "transport": "authenticated_chrome_telegram_ui", + "chat_label": target.get("chat_label"), + "chat_id": target.get("chat_id"), + "captured_at_utc": "", + "captured_by": "", + "final_screenshot": "", + "final_accessibility": "", + }, + "messages": [ + { + "sequence": item["sequence"], + "prompt_id": item["prompt_id"], + "sent_text": item["message"], + "reply": "", + "sent_at_utc": "", + "reply_at_utc": "", + "telegram_message_id": "", + "reply_message_id": "", + "turn_screenshot": "", + "turn_accessibility": "", + } + for item in packet.get("exact_messages", []) + ], + } + + +def _resolve_evidence(path_value: str, evidence_root: Path) -> dict[str, Any]: + if not path_value.strip(): + return {"path": path_value, "exists": False, "bytes": 0, "sha256": ""} + path = Path(path_value) + if not path.is_absolute(): + path = evidence_root / path + resolved = path.resolve() + exists = resolved.is_file() + return { + "path": str(resolved), + "exists": exists, + "bytes": resolved.stat().st_size if exists else 0, + "sha256": _sha256_file(resolved) if exists else "", + } + + +def _capture_messages(capture: dict[str, Any]) -> list[dict[str, Any]]: + messages = capture.get("messages") + return [item for item in messages if isinstance(item, dict)] if isinstance(messages, list) else [] + + +def _normalized_text(value: str) -> str: + return re.sub(r"\s+", " ", value).strip().casefold() + + +def _accessibility_binds_message(evidence: dict[str, Any], *, sent_text: str, reply: str) -> bool: + if not evidence.get("exists") or not evidence.get("path"): + return False + text = Path(str(evidence["path"])).read_text(encoding="utf-8", errors="replace") + normalized = _normalized_text(text) + return _normalized_text(sent_text) in normalized and _normalized_text(reply) in normalized + + +def _subject_match(postflight: dict[str, Any]) -> dict[str, Any]: + subject = (postflight.get("remote") or {}).get("subject_readback") or {} + matches = subject.get("matches") if isinstance(subject, dict) else [] + return matches[0] if isinstance(matches, list) and len(matches) == 1 else {} + + +def _semantic_checks(replies: list[str], *, subject_ref: str, subject_match: dict[str, Any]) -> dict[str, bool]: + if len(replies) != 3: + return { + "visible_db_object_selected_without_operator_id": False, + "visible_weak_support_explained": False, + "same_subject_survived_challenge": False, + "assumption_support_and_falsifier_separated": False, + "multiple_review_only_candidates_proposed": False, + "objection_revised_candidate": False, + "resolving_source_named": False, + "review_approval_apply_boundary_explicit": False, + "reply_claimed_no_mutation": False, + "independent_db_object_matches_visible_subject": False, + } + first, second, third = replies + first_refs = unseen._short_refs(first) + second_refs = unseen._short_refs(second) + shared_terms = unseen._shared_subject_terms(first, second) + selected = subject_ref[:8].casefold() if subject_ref else "" + visible_subject = bool(selected and (selected in first.casefold() or selected in second.casefold())) + same_subject = bool( + visible_subject + and ( + (selected in first.casefold() and selected in second.casefold()) + or first_refs & second_refs + or len(shared_terms) >= 2 + ) + ) + row = subject_match.get("row") if isinstance(subject_match.get("row"), dict) else {} + row_id = str(row.get("id") or "").casefold() + proposals = unseen._contains(second, "proposal a", "proposal b") or ( + unseen._contains_any(second, "two proposals", "two replacement claims") and "1" in second and "2" in second + ) + return { + "visible_db_object_selected_without_operator_id": unseen._contains_any(first, "claim", "belief", "axiom") + and unseen._contains_any(first, "database", "db", "public.") + and unseen._contains_any(first, "confidence", "support", "evidence"), + "visible_weak_support_explained": unseen._contains_any( + first, + "nothing supports", + "zero evidence", + "no evidence", + "no claim-evidence", + "no_source_pointer", + "no external primary source", + ), + "same_subject_survived_challenge": same_subject, + "assumption_support_and_falsifier_separated": unseen._contains_any(second, "assumption") + and unseen._contains_any(second, "observed support", "evidence") + and unseen._contains_any(second, "falsifier", "counterexample", "would falsify"), + "multiple_review_only_candidates_proposed": proposals + and unseen._contains_any(second, "review", "staged", "proposal") + and unseen._contains_any( + second, + "nothing applied", + "not live knowledge", + "staged only", + "not an apply", + ), + "objection_revised_candidate": unseen._contains_any(third, "revised proposal", "revision") + and unseen._contains(third, "mechanism", "outcome"), + "resolving_source_named": unseen._contains_any( + third, "what would resolve", "source would resolve", "evidence would resolve" + ) + and unseen._contains_any( + third, + "study", + "dataset", + "source", + "randomized", + "quasi-experimental", + "replication", + "ostrom", + ), + "review_approval_apply_boundary_explicit": unseen._contains_any( + third, "review", "pending_review", "human review" + ) + and unseen._contains_any(third, "approve", "approved", "approval") + and unseen._contains_any(third, "apply", "applied_at", "canonical"), + "reply_claimed_no_mutation": unseen._contains_any( + third, + "not an apply", + "nothing staged or changed", + "canonical unchanged", + "staged intent, not knowledge", + "proposal (not an apply)", + ), + "independent_db_object_matches_visible_subject": bool( + visible_subject and row_id and row_id.startswith(selected) + ), + } + + +def verify_capture( + packet: dict[str, Any], + preflight: dict[str, Any], + capture: dict[str, Any] | None, + postflight: dict[str, Any] | None, + *, + packet_path: Path, + evidence_root: Path, +) -> dict[str, Any]: + if capture is None: + return { + "schema": SCHEMA, + "generated_at_utc": datetime.now(timezone.utc).isoformat(), + "mode": "telegram_visible_unseen_chain_capture_verifier", + "status": "awaiting_action_time_authorization", + "receipt_ready": False, + "current_tier": "T0_packet_plus_T3_preflight", + "required_tier": "T3_live_readonly", + "telegram_visible_messages_sent": False, + "mutates_db": False, + "packet_path": str(packet_path), + "preflight_path": str(DEFAULT_PREFLIGHT), + "postflight_path": str(DEFAULT_POSTFLIGHT), + "capture_template": blank_capture_template(packet, preflight), + "checks": { + "packet_ready": packet.get("ready_to_request_action_time_authorization") is True, + "preflight_passed": preflight.get("status") == "pass", + "action_time_authorization_present": False, + "capture_present": False, + "postflight_present": False, + }, + "claim_ceiling": { + "proven": "The exact packet and live zero-mutation preflight are ready.", + "not_proven": [ + "Telegram-visible message delivery", + "Telegram-visible Leo replies", + "post-send fingerprint equality", + ], + }, + } + + messages = _capture_messages(capture) + expected = packet.get("exact_messages") or [] + capture_source = capture.get("capture_source") if isinstance(capture.get("capture_source"), dict) else {} + replies = [str(item.get("reply") or "") for item in messages] + ids = [str(item.get("telegram_message_id") or "") for item in messages] + reply_ids = [str(item.get("reply_message_id") or "") for item in messages] + screenshot = _resolve_evidence(str(capture_source.get("final_screenshot") or ""), evidence_root) + accessibility = _resolve_evidence(str(capture_source.get("final_accessibility") or ""), evidence_root) + turn_evidence = [ + { + "prompt_id": item.get("prompt_id"), + "screenshot": _resolve_evidence(str(item.get("turn_screenshot") or ""), evidence_root), + "accessibility": _resolve_evidence(str(item.get("turn_accessibility") or ""), evidence_root), + } + for item in messages + ] + postflight = postflight or {} + subject_ref = str(postflight.get("subject_ref") or "") + subject_match = _subject_match(postflight) + semantics = _semantic_checks(replies, subject_ref=subject_ref, subject_match=subject_match) + pre_fingerprint = (preflight.get("remote") or {}).get("fingerprint_after") or {} + post_fingerprint = (postflight.get("remote") or {}).get("fingerprint_after") or {} + pre_service = (preflight.get("remote") or {}).get("service_after") or {} + post_service = (postflight.get("remote") or {}).get("service_after") or {} + service_keys = ("MainPID", "NRestarts", "ExecMainStartTimestamp") + handler_binding = packet.get("handler_receipt_binding") or {} + handler_path = Path(str(handler_binding.get("path") or "")) + if not handler_path.is_absolute(): + handler_path = evidence_root / handler_path + expected_ids = [str(item.get("prompt_id") or "") for item in expected] + actual_ids = [str(item.get("prompt_id") or "") for item in messages] + checks = { + "packet_ready": packet.get("ready_to_request_action_time_authorization") is True, + "packet_file_hash_matches_preflight": _sha256_file(packet_path) == preflight.get("packet_file_sha256"), + "handler_receipt_binding_unchanged": handler_path.is_file() + and _sha256_file(handler_path) == handler_binding.get("sha256"), + "preflight_passed": preflight.get("status") == "pass" and preflight.get("phase") == "preflight", + "postflight_passed": postflight.get("status") == "pass" and postflight.get("phase") == "postflight", + "protocol_bound_across_all_artifacts": capture.get("protocol_sha256") + == preflight.get("protocol_sha256") + == postflight.get("protocol_sha256") + == packet.get("protocol_sha256"), + "preflight_state_token_bound_to_capture": capture.get("preflight_state_token_sha256") + == preflight.get("state_token_sha256"), + "exact_action_time_authorization_captured": capture.get("operator_authorization_text") + == packet.get("explicit_operator_authorization_text") + and _present(capture.get("operator_authorization_captured_at_utc")), + "authenticated_chrome_transport_proven": capture_source.get("platform") == "telegram" + and capture_source.get("transport") == "authenticated_chrome_telegram_ui", + "bot_api_not_substituted": "bot_api" not in json.dumps(capture_source).casefold(), + "exact_destination_matches": capture_source.get("chat_label") == (packet.get("target") or {}).get("chat_label") + and capture_source.get("chat_id") == (packet.get("target") or {}).get("chat_id"), + "exact_three_messages_and_no_extras": len(messages) == len(expected) == 3 + and capture.get("extra_messages_sent") == 0, + "prompt_ids_and_sequence_exact": actual_ids == expected_ids + and [item.get("sequence") for item in messages] == [1, 2, 3], + "sent_text_exact": [item.get("sent_text") for item in messages] == [item.get("message") for item in expected], + "visible_replies_nonempty": len(replies) == 3 and all(reply.strip() for reply in replies), + "message_and_reply_ids_present_unique": all(ids) + and all(reply_ids) + and len(set(ids)) == 3 + and len(set(reply_ids)) == 3, + "message_and_reply_timestamps_present": all( + _present(item.get("sent_at_utc")) and _present(item.get("reply_at_utc")) for item in messages + ), + "capture_metadata_present": all( + _present(capture_source.get(key)) for key in ("captured_at_utc", "captured_by") + ), + "visual_evidence_files_present": screenshot["exists"] + and screenshot["bytes"] > 0 + and accessibility["exists"] + and accessibility["bytes"] > 0 + and len(turn_evidence) == 3 + and all( + item["screenshot"]["exists"] + and item["screenshot"]["bytes"] > 0 + and item["accessibility"]["exists"] + and item["accessibility"]["bytes"] > 0 + for item in turn_evidence + ), + "turn_accessibility_binds_exact_messages_and_replies": len(turn_evidence) == len(messages) == 3 + and all( + _accessibility_binds_message( + evidence["accessibility"], + sent_text=str(message.get("sent_text") or ""), + reply=str(message.get("reply") or ""), + ) + for evidence, message in zip(turn_evidence, messages, strict=True) + ), + "canonical_fingerprint_unchanged_from_preflight": bool( + pre_fingerprint.get("fingerprint_sha256") + and pre_fingerprint.get("fingerprint_sha256") == post_fingerprint.get("fingerprint_sha256") + ), + "gateway_process_unchanged_from_preflight": all( + pre_service.get(key) == post_service.get(key) for key in service_keys + ), + **semantics, + } + outcomes = { + "telegram_visible_three_turn_chain": all( + checks[key] + for key in ( + "authenticated_chrome_transport_proven", + "exact_destination_matches", + "exact_three_messages_and_no_extras", + "sent_text_exact", + "visible_replies_nonempty", + "visual_evidence_files_present", + "turn_accessibility_binds_exact_messages_and_replies", + ) + ), + "visible_db_grounded_challenge": all( + checks[key] + for key in ( + "visible_db_object_selected_without_operator_id", + "visible_weak_support_explained", + "same_subject_survived_challenge", + "assumption_support_and_falsifier_separated", + "independent_db_object_matches_visible_subject", + ) + ), + "visible_review_only_revision": all( + checks[key] + for key in ( + "multiple_review_only_candidates_proposed", + "objection_revised_candidate", + "resolving_source_named", + "review_approval_apply_boundary_explicit", + "reply_claimed_no_mutation", + ) + ), + "canonical_state_unchanged": checks["canonical_fingerprint_unchanged_from_preflight"], + } + passed = all(checks.values()) and all(outcomes.values()) + return { + "schema": SCHEMA, + "generated_at_utc": datetime.now(timezone.utc).isoformat(), + "mode": "telegram_visible_unseen_chain_capture_verifier", + "status": "pass" if passed else "fail", + "receipt_ready": passed, + "current_tier": "T3_live_readonly" if passed else "T2_or_lower_incomplete_visible_proof", + "required_tier": "T3_live_readonly", + "telegram_visible_messages_sent": True, + "production_apply_executed": False, + "mutates_db": False, + "packet_path": str(packet_path), + "checks": checks, + "outcomes": outcomes, + "selected_subject": { + "ref": subject_ref, + "table": subject_match.get("table"), + "row_id": (subject_match.get("row") or {}).get("id"), + }, + "evidence": { + "final_screenshot": screenshot, + "final_accessibility": accessibility, + "turn_evidence": turn_evidence, + "telegram_message_ids": ids, + "telegram_reply_ids": reply_ids, + "preflight_state_token_sha256": preflight.get("state_token_sha256"), + "postflight_state_token_sha256": postflight.get("state_token_sha256"), + "canonical_fingerprint_sha256": post_fingerprint.get("fingerprint_sha256"), + }, + "claim_ceiling": { + "proven": ( + "One exact authenticated-Chrome Telegram chain visibly selected and challenged a DB object, " + "proposed and revised review-only knowledge, preserved the approval/apply boundary, and left the " + "canonical fingerprint unchanged." + if passed + else "No T3 claim; at least one visible, semantic, binding, or state check failed." + ), + "not_proven": [ + "canonical proposal staging or apply", + "agent-to-agent review", + "GCP runtime parity for the Telegram-visible chain", + ], + }, + } + + +def write_json(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def write_markdown(path: Path, data: dict[str, Any]) -> None: + lines = [ + "# Telegram-Visible Unseen Chain Capture Receipt", + "", + f"Generated UTC: `{data['generated_at_utc']}`", + f"Status: `{data['status']}`", + f"Receipt ready: `{data['receipt_ready']}`", + f"Current tier: `{data['current_tier']}`", + f"Required tier: `{data['required_tier']}`", + f"Telegram-visible messages sent: `{data['telegram_visible_messages_sent']}`", + f"Mutates DB: `{data['mutates_db']}`", + "", + "## Checks", + "", + ] + lines.extend(f"- `{key}`: `{value}`" for key, value in sorted(data.get("checks", {}).items())) + if data.get("capture_template"): + lines.extend( + [ + "", + "## Awaiting Authorized Capture", + "", + "The packet and preflight are ready. No Telegram message has been sent by this verifier.", + ] + ) + lines.extend( + [ + "", + "## Claim Ceiling", + "", + data["claim_ceiling"]["proven"], + "", + ] + ) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines), encoding="utf-8") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--packet", type=Path, default=DEFAULT_PACKET) + parser.add_argument("--preflight", type=Path, default=DEFAULT_PREFLIGHT) + parser.add_argument("--capture-json", type=Path) + parser.add_argument("--postflight", type=Path, default=DEFAULT_POSTFLIGHT) + parser.add_argument("--evidence-root", type=Path, default=Path(".")) + parser.add_argument("--out", type=Path, default=DEFAULT_OUT) + parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MARKDOWN_OUT) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + packet = _load_json(args.packet) + preflight = _load_json(args.preflight) + capture = _load_json(args.capture_json) if args.capture_json else None + postflight = _load_json(args.postflight) if capture is not None and args.postflight.exists() else None + receipt = verify_capture( + packet, + preflight, + capture, + postflight, + packet_path=args.packet, + evidence_root=args.evidence_root, + ) + write_json(args.out, receipt) + write_markdown(args.markdown_out, receipt) + print( + json.dumps( + { + "out": str(args.out), + "markdown_out": str(args.markdown_out), + "status": receipt["status"], + "receipt_ready": receipt["receipt_ready"], + }, + indent=2, + sort_keys=True, + ) + ) + return 0 if receipt["status"] in {"pass", "awaiting_action_time_authorization"} else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_build_telegram_visible_unseen_chain_packet.py b/tests/test_build_telegram_visible_unseen_chain_packet.py new file mode 100644 index 0000000..5a67f47 --- /dev/null +++ b/tests/test_build_telegram_visible_unseen_chain_packet.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from scripts import build_telegram_visible_unseen_chain_packet as packet + + +def test_packet_binds_handler_receipt_and_preserves_exact_messages() -> None: + data = packet.build_packet(git_sha="a" * 40) + + assert data["schema"] == packet.SCHEMA + assert data["ready_to_request_action_time_authorization"] is True + assert data["telegram_visible_messages_sent"] is False + assert data["telegram_visible_message_authorization_present"] is False + assert data["mutates_db"] is False + assert data["target"]["chat_label"] == "Leo" + assert data["target"]["chat_id"] == "-5146042086" + assert data["target"]["allowed_transport"] == "authenticated_chrome_telegram_ui" + assert [item["message"] for item in data["exact_messages"]] == [item["message"] for item in packet.unseen.PROMPTS] + assert [item["sequence"] for item in data["exact_messages"]] == [1, 2, 3] + assert all(len(item["message_sha256"]) == 64 for item in data["exact_messages"]) + assert data["handler_receipt_binding"]["sha256"] == packet._sha256_file(packet.DEFAULT_HANDLER_RECEIPT) + assert data["tooling_binding"] == packet.tooling_binding() + assert all(data["checks"].values()) + + +def test_action_time_authorization_contains_destination_transport_and_full_messages() -> None: + data = packet.build_packet(git_sha="a" * 40) + authorization = data["explicit_operator_authorization_text"] + + assert "Telegram group Leo (chat ID -5146042086)" in authorization + assert "already-authenticated Chrome Telegram UI" in authorization + assert "do not use the Bot API" in authorization + assert "no additional messages" in authorization + for item in data["exact_messages"]: + assert item["prompt_id"] in authorization + assert json.dumps(item["message"], ensure_ascii=True) in authorization + + +def test_packet_fails_closed_when_handler_receipt_no_longer_passes(tmp_path: Path) -> None: + receipt = json.loads(packet.DEFAULT_HANDLER_RECEIPT.read_text(encoding="utf-8")) + receipt["status"] = "fail" + receipt_path = tmp_path / "handler.json" + receipt_path.write_text(json.dumps(receipt), encoding="utf-8") + + data = packet.build_packet(handler_receipt_path=receipt_path, git_sha="a" * 40) + + assert data["ready_to_request_action_time_authorization"] is False + assert data["authorization_gate_status"] == "not_ready" + assert data["checks"]["handler_receipt_passed"] is False + + +def test_markdown_retains_exact_cta_and_no_send_ceiling(tmp_path: Path) -> None: + data = packet.build_packet(git_sha="a" * 40) + out = tmp_path / "packet.md" + + packet.write_markdown(out, data) + + text = out.read_text(encoding="utf-8") + assert "Telegram-Visible Unseen Chain Authorization Packet" in text + assert "Telegram-visible messages sent: `False`" in text + assert "Exact Action-Time Authorization" in text + assert data["exact_messages"][2]["message"] in text diff --git a/tests/test_collect_telegram_visible_unseen_chain_state.py b/tests/test_collect_telegram_visible_unseen_chain_state.py new file mode 100644 index 0000000..a53d59f --- /dev/null +++ b/tests/test_collect_telegram_visible_unseen_chain_state.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +import argparse +import copy +import json +from pathlib import Path + +from scripts import build_telegram_visible_unseen_chain_packet as packet_builder +from scripts import collect_telegram_visible_unseen_chain_state as state + + +def manifest_output(*, row_count: int = 7) -> str: + rows = [ + { + "kind": "identity", + "database": "teleo", + "server_version_num": 160014, + "server_encoding": "UTF8", + "database_collation": "C.UTF-8", + "database_ctype": "C.UTF-8", + "transaction_read_only": "on", + } + ] + for kind in ( + "schemas", + "extensions", + "application_roles", + "columns", + "constraints", + "indexes", + "sequences", + "views", + "functions", + "triggers", + "types", + "policies", + ): + rows.append({"kind": kind, "items": []}) + rows.append( + { + "kind": "table", + "schema": "public", + "table": "claims", + "row_count": row_count, + "rowset_md5": "f" * 32, + } + ) + return "BEGIN\nSET\n" + "\n".join(json.dumps(item) for item in rows) + "\nROLLBACK\n" + + +def write_packet(tmp_path: Path) -> Path: + out = tmp_path / "packet.json" + packet_builder.write_json( + out, + packet_builder.build_packet( + handler_receipt_path=packet_builder.DEFAULT_HANDLER_RECEIPT.resolve(), + git_sha="a" * 40, + ), + ) + return out + + +def args_for(packet: Path, tmp_path: Path, *, phase: str = "preflight") -> argparse.Namespace: + return argparse.Namespace( + phase=phase, + packet=packet, + baseline=None, + subject_ref=None, + manifest_sql=Path("ops/postgres_parity_manifest.sql"), + ssh_target="root@example.invalid", + ssh_key=tmp_path / "key", + connect_timeout=1, + timeout=5, + fingerprint_timeout=5, + ssh_attempts=3, + out=tmp_path / f"{phase}.json", + markdown_out=tmp_path / f"{phase}.md", + ) + + +class GoodRunner: + def __init__(self) -> None: + self.service_calls = 0 + + def __call__(self, command: list[str], input_text: str, _timeout: int) -> state.CommandResult: + remote_command = command[-1] + if "git rev-parse HEAD" in remote_command: + sha = "b" * 40 + return state.CommandResult( + 0, + f"deploy_head={sha}\nlast_deploy_sha={sha}\ndeploy_stamp_ancestor=true\n", + "", + ) + if "systemctl show" in remote_command: + self.service_calls += 1 + return state.CommandResult( + 0, + "\n".join( + [ + "ActiveState=active", + "SubState=running", + "MainPID=123", + "NRestarts=0", + "ExecMainStartTimestamp=Wed 2026-07-15 00:00:00 UTC", + ] + ), + "", + ) + if "'subject_ref'" in input_text: + payload = { + "subject_ref": "c0000000", + "matches": [ + { + "table": "public.beliefs", + "row": {"id": "c0000000-0000-4000-8000-000000000001"}, + "evidence_count": None, + } + ], + } + return state.CommandResult(0, f"BEGIN\n{json.dumps(payload)}\nROLLBACK\n", "") + if "postgres_parity_manifest" not in remote_command and input_text.startswith("\\set ON_ERROR_STOP"): + return state.CommandResult(0, manifest_output(), "") + raise AssertionError(f"unexpected command: {command} / input={input_text[:80]}") + + +def test_preflight_passes_with_two_identical_read_only_fingerprints(tmp_path: Path) -> None: + args = args_for(write_packet(tmp_path), tmp_path) + + result = state.collect_state(args, runner=GoodRunner()) + + assert result["status"] == "pass" + assert result["ready_for_action_time_authorization"] is True + assert result["telegram_visible_messages_sent_by_collector"] is False + assert result["mutates_db"] is False + assert all(result["packet_checks"].values()) + assert all(result["remote_checks"].values()) + assert len(result["state_token_sha256"]) == 64 + + +def test_preflight_fails_when_second_fingerprint_drifts(tmp_path: Path) -> None: + args = args_for(write_packet(tmp_path), tmp_path) + fingerprint_calls = 0 + good = GoodRunner() + + def drift_runner(command: list[str], input_text: str, timeout: int) -> state.CommandResult: + nonlocal fingerprint_calls + if input_text.startswith("\\set ON_ERROR_STOP") and "'subject_ref'" not in input_text: + fingerprint_calls += 1 + return state.CommandResult(0, manifest_output(row_count=7 + fingerprint_calls - 1), "") + return good(command, input_text, timeout) + + result = state.collect_state(args, runner=drift_runner) + + assert result["status"] == "fail" + assert result["remote_checks"]["canonical_fingerprint_unchanged_during_probe"] is False + + +def test_preflight_accepts_newer_workspace_head_when_deployed_stamp_is_ancestor(tmp_path: Path) -> None: + args = args_for(write_packet(tmp_path), tmp_path) + + class AheadRunner(GoodRunner): + def __call__(self, command: list[str], input_text: str, timeout: int) -> state.CommandResult: + if "git rev-parse HEAD" in command[-1]: + return state.CommandResult( + 0, + f"deploy_head={'c' * 40}\nlast_deploy_sha={'b' * 40}\ndeploy_stamp_ancestor=true\n", + "", + ) + return super().__call__(command, input_text, timeout) + + result = state.collect_state(args, runner=AheadRunner()) + + assert result["status"] == "pass" + assert result["remote"]["workspace_head_matches_deployed_stamp"] is False + assert result["remote_checks"]["deployed_stamp_is_ancestor_of_workspace_head"] is True + + +def test_preflight_retries_transient_ssh_failure(tmp_path: Path) -> None: + args = args_for(write_packet(tmp_path), tmp_path) + good = GoodRunner() + deploy_attempts = 0 + + def transient_runner(command: list[str], input_text: str, timeout: int) -> state.CommandResult: + nonlocal deploy_attempts + if "git rev-parse HEAD" in command[-1]: + deploy_attempts += 1 + if deploy_attempts == 1: + return state.CommandResult(255, "", "Connection timed out during banner exchange") + return good(command, input_text, timeout) + + result = state.collect_state(args, runner=transient_runner) + + assert result["status"] == "pass" + assert result["remote"]["readback_attempts"]["deploy"] == 2 + + +def test_preflight_rejects_packet_bound_to_different_tooling_source(tmp_path: Path) -> None: + packet_path = write_packet(tmp_path) + packet = json.loads(packet_path.read_text(encoding="utf-8")) + packet["tooling_binding"]["capture_verifier"]["sha256"] = "0" * 64 + packet_path.write_text(json.dumps(packet), encoding="utf-8") + + result = state.collect_state(args_for(packet_path, tmp_path), runner=GoodRunner()) + + assert result["status"] == "fail" + assert result["packet_checks"]["packet_tooling_hashes_match_current_sources"] is False + + +def test_postflight_binds_baseline_fingerprint_service_and_subject(tmp_path: Path) -> None: + packet = write_packet(tmp_path) + pre_args = args_for(packet, tmp_path) + preflight = state.collect_state(pre_args, runner=GoodRunner()) + baseline = tmp_path / "preflight.json" + state.write_json(baseline, preflight) + post_args = args_for(packet, tmp_path, phase="postflight") + post_args.baseline = baseline + post_args.subject_ref = "c0000000" + + postflight = state.collect_state(post_args, runner=GoodRunner()) + + assert postflight["status"] == "pass" + assert postflight["ready_for_visible_capture_verification"] is True + assert all(postflight["baseline_checks"].values()) + assert postflight["remote_checks"]["subject_resolved_to_exactly_one_db_object"] is True + + +def test_postflight_rejects_baseline_fingerprint_mismatch(tmp_path: Path) -> None: + packet = write_packet(tmp_path) + pre_args = args_for(packet, tmp_path) + preflight = state.collect_state(pre_args, runner=GoodRunner()) + preflight = copy.deepcopy(preflight) + preflight["remote"]["fingerprint_after"]["fingerprint_sha256"] = "0" * 64 + baseline = tmp_path / "preflight.json" + state.write_json(baseline, preflight) + post_args = args_for(packet, tmp_path, phase="postflight") + post_args.baseline = baseline + post_args.subject_ref = "c0000000" + + postflight = state.collect_state(post_args, runner=GoodRunner()) + + assert postflight["status"] == "fail" + assert postflight["baseline_checks"]["canonical_fingerprint_matches_preflight"] is False + + +def test_subject_sql_rejects_untrusted_input() -> None: + try: + state.subject_readback_sql("c0000000'; delete from public.claims; --") + except ValueError as exc: + assert "subject_ref" in str(exc) + else: + raise AssertionError("unsafe subject ref was accepted") diff --git a/tests/test_verify_telegram_visible_unseen_chain.py b/tests/test_verify_telegram_visible_unseen_chain.py new file mode 100644 index 0000000..cbd6310 --- /dev/null +++ b/tests/test_verify_telegram_visible_unseen_chain.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import copy +from pathlib import Path + +from scripts import build_telegram_visible_unseen_chain_packet as packet_builder +from scripts import verify_telegram_visible_unseen_chain as verifier + +SUBJECT_ID = "c0000000-0000-4000-8000-000000000001" + + +def packet_and_path(tmp_path: Path) -> tuple[dict, Path]: + packet = packet_builder.build_packet( + handler_receipt_path=packet_builder.DEFAULT_HANDLER_RECEIPT.resolve(), + git_sha="a" * 40, + ) + path = tmp_path / "packet.json" + packet_builder.write_json(path, packet) + return packet, path + + +def preflight(packet: dict, packet_path: Path) -> dict: + return { + "phase": "preflight", + "status": "pass", + "protocol_sha256": packet["protocol_sha256"], + "packet_file_sha256": verifier._sha256_file(packet_path), + "state_token_sha256": "1" * 64, + "remote": { + "fingerprint_after": {"fingerprint_sha256": "f" * 64}, + "service_after": { + "MainPID": "123", + "NRestarts": "0", + "ExecMainStartTimestamp": "Wed 2026-07-15 00:00:00 UTC", + }, + }, + } + + +def postflight(packet: dict) -> dict: + return { + "phase": "postflight", + "status": "pass", + "protocol_sha256": packet["protocol_sha256"], + "state_token_sha256": "2" * 64, + "subject_ref": "c0000000", + "remote": { + "fingerprint_after": {"fingerprint_sha256": "f" * 64}, + "service_after": { + "MainPID": "123", + "NRestarts": "0", + "ExecMainStartTimestamp": "Wed 2026-07-15 00:00:00 UTC", + }, + "subject_readback": { + "subject_ref": "c0000000", + "matches": [ + { + "table": "public.beliefs", + "row": {"id": SUBJECT_ID, "statement": "Coordination is the bottleneck."}, + } + ], + }, + }, + } + + +def good_capture(packet: dict, tmp_path: Path) -> dict: + screenshot = tmp_path / "final.png" + accessibility = tmp_path / "final.txt" + screenshot.write_bytes(b"png proof") + accessibility.write_text("telegram accessibility proof", encoding="utf-8") + replies = [ + ( + "The database public.beliefs object c0000000 is an axiom with confidence 0.85. What supports it in " + "the DB: nothing supports it; there are zero evidence rows and no external primary source." + ), + ( + "Claim c0000000 is still the coordination bottleneck belief. Assumption versus observed support: the " + "priority leap is assumed and the evidence is absent. Falsifier: a contrary case. Proposal A and " + "Proposal B are staged only for review; nothing applied and neither is live knowledge." + ), + ( + "Revised proposal: separate the mechanism from the business outcome. What would resolve the " + "disagreement: a randomized study and its primary source dataset. Keep it pending_review for human " + "review; approved is not apply and applied_at remains null. This is not an apply." + ), + ] + messages = [] + for item, reply in zip(packet["exact_messages"], replies, strict=True): + sequence = item["sequence"] + turn_screenshot = tmp_path / f"turn-{sequence}.png" + turn_accessibility = tmp_path / f"turn-{sequence}.txt" + turn_screenshot.write_bytes(f"turn {sequence} screenshot".encode()) + turn_accessibility.write_text( + f"Telegram message: {item['message']}\nLeo reply: {reply}\n", + encoding="utf-8", + ) + messages.append( + { + "sequence": sequence, + "prompt_id": item["prompt_id"], + "sent_text": item["message"], + "reply": reply, + "sent_at_utc": f"2026-07-15T00:0{sequence}:00Z", + "reply_at_utc": f"2026-07-15T00:0{sequence}:20Z", + "telegram_message_id": f"sent-{sequence}", + "reply_message_id": f"reply-{sequence}", + "turn_screenshot": str(turn_screenshot), + "turn_accessibility": str(turn_accessibility), + } + ) + return { + "protocol_sha256": packet["protocol_sha256"], + "preflight_state_token_sha256": "1" * 64, + "operator_authorization_text": packet["explicit_operator_authorization_text"], + "operator_authorization_captured_at_utc": "2026-07-15T00:00:00Z", + "extra_messages_sent": 0, + "capture_source": { + "platform": "telegram", + "transport": "authenticated_chrome_telegram_ui", + "chat_label": "Leo", + "chat_id": "-5146042086", + "captured_at_utc": "2026-07-15T00:05:00Z", + "captured_by": "codex", + "final_screenshot": str(screenshot), + "final_accessibility": str(accessibility), + }, + "messages": messages, + } + + +def test_no_capture_returns_awaiting_authorization_template(tmp_path: Path) -> None: + packet, packet_path = packet_and_path(tmp_path) + + receipt = verifier.verify_capture( + packet, + preflight(packet, packet_path), + None, + None, + packet_path=packet_path, + evidence_root=tmp_path, + ) + + assert receipt["status"] == "awaiting_action_time_authorization" + assert receipt["telegram_visible_messages_sent"] is False + assert receipt["capture_template"]["messages"][0]["prompt_id"] == "OOS-CHAIN-01" + + +def test_good_chrome_capture_and_unchanged_fingerprint_pass_t3(tmp_path: Path) -> None: + packet, packet_path = packet_and_path(tmp_path) + before = preflight(packet, packet_path) + + receipt = verifier.verify_capture( + packet, + before, + good_capture(packet, tmp_path), + postflight(packet), + packet_path=packet_path, + evidence_root=tmp_path, + ) + + assert receipt["status"] == "pass" + assert receipt["receipt_ready"] is True + assert receipt["current_tier"] == "T3_live_readonly" + assert all(receipt["checks"].values()) + assert all(receipt["outcomes"].values()) + assert receipt["selected_subject"]["row_id"] == SUBJECT_ID + + +def test_bot_api_capture_is_rejected(tmp_path: Path) -> None: + packet, packet_path = packet_and_path(tmp_path) + capture = good_capture(packet, tmp_path) + capture["capture_source"]["transport"] = "telegram_bot_api" + + receipt = verifier.verify_capture( + packet, + preflight(packet, packet_path), + capture, + postflight(packet), + packet_path=packet_path, + evidence_root=tmp_path, + ) + + assert receipt["status"] == "fail" + assert receipt["checks"]["authenticated_chrome_transport_proven"] is False + assert receipt["checks"]["bot_api_not_substituted"] is False + + +def test_postflight_fingerprint_drift_is_rejected(tmp_path: Path) -> None: + packet, packet_path = packet_and_path(tmp_path) + after = copy.deepcopy(postflight(packet)) + after["remote"]["fingerprint_after"]["fingerprint_sha256"] = "0" * 64 + + receipt = verifier.verify_capture( + packet, + preflight(packet, packet_path), + good_capture(packet, tmp_path), + after, + packet_path=packet_path, + evidence_root=tmp_path, + ) + + assert receipt["status"] == "fail" + assert receipt["checks"]["canonical_fingerprint_unchanged_from_preflight"] is False + + +def test_message_text_drift_is_rejected(tmp_path: Path) -> None: + packet, packet_path = packet_and_path(tmp_path) + capture = good_capture(packet, tmp_path) + capture["messages"][1]["sent_text"] += " extra" + + receipt = verifier.verify_capture( + packet, + preflight(packet, packet_path), + capture, + postflight(packet), + packet_path=packet_path, + evidence_root=tmp_path, + ) + + assert receipt["status"] == "fail" + assert receipt["checks"]["sent_text_exact"] is False + + +def test_copied_reply_not_present_in_accessibility_is_rejected(tmp_path: Path) -> None: + packet, packet_path = packet_and_path(tmp_path) + capture = good_capture(packet, tmp_path) + capture["messages"][0]["reply"] += " copied-only suffix" + + receipt = verifier.verify_capture( + packet, + preflight(packet, packet_path), + capture, + postflight(packet), + packet_path=packet_path, + evidence_root=tmp_path, + ) + + assert receipt["status"] == "fail" + assert receipt["checks"]["turn_accessibility_binds_exact_messages_and_replies"] is False From 92da54d804ea5a59ac97274493fea604469a2b88 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 03:43:05 +0200 Subject: [PATCH 05/27] Add authenticated GCP Observatory read adapter (#154) --- .../gcp-observatory-read-adapter.yml | 199 ++++++++++ Dockerfile.observatory-read-adapter | 20 + docs/observatory-read-adapter-gcp-staging.md | 102 ++++++ .../platform-goal-current.json | 12 + observatory_read_adapter/__init__.py | 6 + observatory_read_adapter/__main__.py | 18 + observatory_read_adapter/app.py | 111 ++++++ observatory_read_adapter/config.py | 61 +++ observatory_read_adapter/db.py | 315 ++++++++++++++++ ops/observatory_read_role.sql | 183 +++++++++ pyproject.toml | 5 +- tests/test_observatory_read_adapter.py | 346 ++++++++++++++++++ tests/test_observatory_read_role_postgres.py | 182 +++++++++ 13 files changed, 1559 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/gcp-observatory-read-adapter.yml create mode 100644 Dockerfile.observatory-read-adapter create mode 100644 docs/observatory-read-adapter-gcp-staging.md create mode 100644 docs/reports/observatory-read-adapter/platform-goal-current.json create mode 100644 observatory_read_adapter/__init__.py create mode 100644 observatory_read_adapter/__main__.py create mode 100644 observatory_read_adapter/app.py create mode 100644 observatory_read_adapter/config.py create mode 100644 observatory_read_adapter/db.py create mode 100644 ops/observatory_read_role.sql create mode 100644 tests/test_observatory_read_adapter.py create mode 100644 tests/test_observatory_read_role_postgres.py diff --git a/.github/workflows/gcp-observatory-read-adapter.yml b/.github/workflows/gcp-observatory-read-adapter.yml new file mode 100644 index 0000000..e4c088f --- /dev/null +++ b/.github/workflows/gcp-observatory-read-adapter.yml @@ -0,0 +1,199 @@ +name: gcp-observatory-read-adapter + +on: + workflow_dispatch: + inputs: + action: + description: Build only, or deploy the protected staging service and run live receipts + required: true + default: build_only + type: choice + options: + - build_only + - deploy_staging + +permissions: + contents: read + id-token: write + +concurrency: + group: gcp-observatory-read-adapter-${{ github.ref }} + cancel-in-progress: false + +env: + PROJECT_ID: teleo-501523 + REGION: europe-west6 + ZONE: europe-west6-a + REPOSITORY: teleo + IMAGE_NAME: observatory-read-adapter + SERVICE_NAME: observatory-read-adapter-staging + RUNTIME_SERVICE_ACCOUNT: sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com + DB_IAM_USER: sa-observatory-read-adapter@teleo-501523.iam + API_KEY_SECRET: observatory-read-api-key-staging + WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github + DEPLOY_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com + +jobs: + build: + name: Test, build, and push adapter image + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + image_ref: ${{ steps.image.outputs.image_ref }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - name: Run focused adapter tests + run: | + python -m pip install -e '.[dev]' + python -m pytest tests/test_observatory_read_adapter.py -q + python -m ruff check observatory_read_adapter tests/test_observatory_read_adapter.py + + - id: auth + uses: google-github-actions/auth@v3 + with: + workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }} + + - uses: google-github-actions/setup-gcloud@v3 + with: + project_id: ${{ env.PROJECT_ID }} + + - name: Configure Artifact Registry + run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet + + - id: image + name: Build, smoke, and push immutable image + shell: bash + run: | + set -euo pipefail + tag="${GITHUB_SHA::12}" + image="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${tag}" + docker build -f Dockerfile.observatory-read-adapter -t "${image}" . + docker run --rm --env PYTHONPYCACHEPREFIX=/tmp/pycache \ + "${image}" python -m compileall -q /app/observatory_read_adapter + docker run --rm "${image}" python -c 'import observatory_read_adapter; print("adapter-import-ok")' + docker push "${image}" | tee docker-push.log + digest="$(awk '/digest: sha256:/ {print $3}' docker-push.log | tail -1)" + test -n "${digest}" + image_ref="${image}@${digest}" + printf 'image_ref=%s\n' "${image_ref}" >> "${GITHUB_OUTPUT}" + printf 'revision=%s\nimage_ref=%s\n' "${GITHUB_SHA}" "${image_ref}" > observatory-adapter-image.txt + + - uses: actions/upload-artifact@v4 + with: + name: observatory-adapter-image + path: observatory-adapter-image.txt + if-no-files-found: error + + deploy-staging: + name: Deploy and prove staging read path + if: ${{ inputs.action == 'deploy_staging' }} + needs: build + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - id: auth + uses: google-github-actions/auth@v3 + with: + workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }} + + - uses: google-github-actions/setup-gcloud@v3 + with: + project_id: ${{ env.PROJECT_ID }} + + - name: Deploy bounded GCP staging service + shell: bash + env: + IMAGE_REF: ${{ needs.build.outputs.image_ref }} + run: | + set -euo pipefail + gcloud run deploy "${SERVICE_NAME}" \ + --project="${PROJECT_ID}" \ + --region="${REGION}" \ + --image="${IMAGE_REF}" \ + --execution-environment=gen2 \ + --service-account="${RUNTIME_SERVICE_ACCOUNT}" \ + --network=teleo-staging-net \ + --subnet=teleo-staging-europe-west6 \ + --vpc-egress=private-ranges-only \ + --ingress=all \ + --allow-unauthenticated \ + --port=8080 \ + --cpu=1 \ + --memory=512Mi \ + --concurrency=8 \ + --max-instances=2 \ + --timeout=15 \ + --set-secrets="OBSERVATORY_API_KEY=${API_KEY_SECRET}:latest" \ + --set-env-vars="GCP_PROJECT_ID=${PROJECT_ID},CLOUD_SQL_INSTANCE=${PROJECT_ID}:${REGION}:teleo-pgvector-standby,DB_NAME=teleo_canonical,DB_IAM_USER=${DB_IAM_USER},DB_AUTHORIZATION_ROLE=kb_observatory_read,SERVICE_REVISION=${GITHUB_SHA}" \ + --labels="livingip-tier=gcp-staging,livingip-surface=observatory-read-adapter" \ + --quiet + + - name: Capture positive and negative live receipts + shell: bash + run: | + set -euo pipefail + service_url="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format='value(status.url)')" + revision="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format='value(status.latestReadyRevisionName)')" + api_key="$(gcloud secrets versions access latest --secret="${API_KEY_SECRET}" --project="${PROJECT_ID}")" + test -n "${service_url}" + test -n "${revision}" + test -n "${api_key}" + + curl --fail --silent --show-error \ + --header "X-Api-Key: ${api_key}" \ + "${service_url}/v1/claims/sample" > positive.json + jq -e ' + .schema == "livingip.observatory-canonical-claim.v1" + and .read_only == true + and .provenance.database == "teleo_canonical" + and .provenance.authorization_role == "kb_observatory_read" + and .provenance.transaction_read_only == true + and .provenance.write_privileges_denied == true + and (.canonical.evidence | length) > 0 + and .proposal_ledger.distinct_from_canonical == true + ' positive.json >/dev/null + + anonymous_status="$(curl --silent --output anonymous.json --write-out '%{http_code}' "${service_url}/v1/claims/sample")" + write_status="$(curl --silent --output write.json --write-out '%{http_code}' \ + --request POST --header "X-Api-Key: ${api_key}" --header 'Content-Type: application/json' \ + --data '{"status":"applied"}' "${service_url}/v1/claims/sample")" + test "${anonymous_status}" = 401 + test "${write_status}" = 405 + + jq -n \ + --arg service_url "${service_url}" \ + --arg revision "${revision}" \ + --arg git_sha "${GITHUB_SHA}" \ + --argjson positive "$(cat positive.json)" \ + --arg anonymous_status "${anonymous_status}" \ + --arg write_status "${write_status}" \ + '{ + schema: "livingip.observatory-read-adapter-live-receipt.v1", + required_tier: "T3_live_readonly", + service_url: $service_url, + revision: $revision, + git_sha: $git_sha, + positive: $positive, + negative: { + anonymous_http_status: ($anonymous_status | tonumber), + authenticated_post_http_status: ($write_status | tonumber) + }, + production_repoint_executed: false + }' > observatory-read-adapter-live-receipt.json + unset api_key + + - uses: actions/upload-artifact@v4 + with: + name: observatory-read-adapter-live-receipt + path: observatory-read-adapter-live-receipt.json + if-no-files-found: error diff --git a/Dockerfile.observatory-read-adapter b/Dockerfile.observatory-read-adapter new file mode 100644 index 0000000..92c3a1e --- /dev/null +++ b/Dockerfile.observatory-read-adapter @@ -0,0 +1,20 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PORT=8080 + +WORKDIR /app + +COPY pyproject.toml README.md /app/ +COPY observatory_read_adapter /app/observatory_read_adapter +COPY lib /app/lib + +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir ".[observatory]" \ + && addgroup --system --gid 10001 adapter \ + && adduser --system --uid 10001 --ingroup adapter --no-create-home adapter + +USER 10001:10001 + +CMD ["python", "-m", "observatory_read_adapter"] diff --git a/docs/observatory-read-adapter-gcp-staging.md b/docs/observatory-read-adapter-gcp-staging.md new file mode 100644 index 0000000..b4d6960 --- /dev/null +++ b/docs/observatory-read-adapter-gcp-staging.md @@ -0,0 +1,102 @@ +# GCP Observatory Read Adapter + +## Definition of Working + +- Working target: an authenticated GCP staging API returns one canonical claim + with source/evidence provenance and a separately labeled proposal ledger. +- Operator path: `GET /v1/claims/sample` or `GET /v1/claims/{uuid}` with the + server-only `X-Api-Key` value. +- Done means: the response names `teleo_canonical`, the dedicated IAM database + principal and `kb_observatory_read`, reports a read-only transaction and + denied write privileges, and live anonymous/write HTTP attempts return + `401`/`405`. +- Not done: the VPS `teleo-pg`, a public Cloud SQL address, a browser-visible + key, a mock-only receipt, or a production Vercel repoint. +- Required tier: `T3_live_readonly`. + +## Existing Connector Boundary + +The current `living-ip/livingip-web` main branch reads claims through +`src/lib/api.ts`. It selects `NEXT_PUBLIC_API_URL` and otherwise falls back to +`http://77.42.65.182:8081`, then calls the legacy `/api/claims` routes. That is +the VPS-local/public Observatory path. It is not safe to replace that public +environment variable with this protected API: the response shape differs and +the adapter key must never enter a `NEXT_PUBLIC_` variable or browser bundle. + +The existing Teleo route `GET /api/kb/claims` is the compatibility reference +for route-scoped authentication, but its default database/container settings +still describe the VPS-local process. This service is a separate GCP runtime; +it does not change Argus or PR #148's Leo runtime files. + +## Runtime Contract + +- Cloud Run service: `observatory-read-adapter-staging` in `europe-west6`. +- Direct VPC egress: `teleo-staging-net` / `teleo-staging-europe-west6`. +- Cloud SQL: `teleo-501523:europe-west6:teleo-pgvector-standby`, private IP. +- Database: `teleo_canonical` only. +- Runtime identity: `sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com`. +- IAM database user: `sa-observatory-read-adapter@teleo-501523.iam`. +- Database authorization role: `kb_observatory_read` (`NOLOGIN`). +- API authentication: route-scoped key from Secret Manager secret + `observatory-read-api-key-staging`; the value is not in Git or deploy logs. +- No mutation routes exist. Authenticated unsupported methods return `405`. + +The Cloud SQL Python Connector uses Application Default Credentials from the +Cloud Run service identity, automatic IAM database authentication, and private +IP. Every request starts a read-only transaction and refuses the response if +the database, IAM principal, authorization membership, required reads, or +effective write-denial checks drift. + +## One-Time Staging Bootstrap + +These are staging mutations. Run them only with an authenticated GCP operator; +they do not route production traffic. + +1. Create the dedicated runtime service account and grant only + `roles/cloudsql.client` and `roles/cloudsql.instanceUser` in project + `teleo-501523`. +2. Add the service account as a Cloud SQL IAM service-account user on + `teleo-pgvector-standby`. +3. From the existing private GCP VM database-admin path, run + `ops/observatory_read_role.sql` against `teleo_canonical` with + `OBSERVATORY_DB_IAM_USER=sa-observatory-read-adapter@teleo-501523.iam`. + Do not read, copy, or retain the administrator password. +4. Create `observatory-read-api-key-staging`, add a generated 32-byte-or-longer + value through stdin, and grant that secret's accessor role only to the + runtime service account and the bounded staging canary identity. +5. Dispatch `.github/workflows/gcp-observatory-read-adapter.yml` with + `action=deploy_staging` from the exact reviewed branch revision. + +The workflow builds an immutable image, deploys at most two Cloud Run +instances, and retains the positive response plus anonymous `401` and +authenticated POST `405` receipts. It does not modify Vercel. + +## Unexecuted Cutover Packet + +Production cutover requires a separate exact authorization and a +`living-ip/livingip-web` change. Do not execute these steps from this lane. + +1. Add server-only Vercel secrets `OBSERVATORY_CANONICAL_API_URL` and + `OBSERVATORY_CANONICAL_API_KEY` to a protected preview environment. The URL + targets the GCP service; neither setting may use `NEXT_PUBLIC_`. +2. Add a Next.js server route or server action that calls the GCP API, validates + `livingip.observatory-canonical-claim.v1`, and maps it to the Observatory UI. + The browser calls only the same-origin Next.js route. +3. Prove preview Deployment Protection, anonymous denial, source/evidence + rendering, proposal/live labels, and no browser key exposure. +4. Obtain exact production-repoint authorization naming the reviewed + `livingip-web` commit and Vercel project before changing production values. + +## Unexecuted Rollback Packet + +1. Revert the authorized `livingip-web` connector commit or disable its + server-side feature flag. +2. Restore the prior production Vercel values and redeploy the last accepted + production revision. +3. Verify `/claims` again uses the prior `/api/claims` contract. +4. Leave the private Cloud SQL instance and adapter role intact while receipts + are reviewed; scale the staging Cloud Run service to zero if isolation is + required. +5. Delete the staging service/role/secret only under a separate cleanup + decision. Rollback never adds a Cloud SQL public IP and never redirects the + adapter to VPS `teleo-pg`. diff --git a/docs/reports/observatory-read-adapter/platform-goal-current.json b/docs/reports/observatory-read-adapter/platform-goal-current.json new file mode 100644 index 0000000..6c60df3 --- /dev/null +++ b/docs/reports/observatory-read-adapter/platform-goal-current.json @@ -0,0 +1,12 @@ +{ + "fallback_goal_path": "goal.md", + "platform_goal_after": { + "objective": "Complete and verify the private GCP Observatory read adapter defined in this goal file, stopping before production repoint without exact authorization.", + "status": "active", + "thread_id": "019f6350-69fa-71f3-944b-3fc0065f4fec" + }, + "platform_goal_before": null, + "platform_goal_repair_action": "create_goal", + "platform_goal_replaced": false, + "why_not_replaced": "No prior platform Goal existed; a new active Goal was created from goal.md." +} diff --git a/observatory_read_adapter/__init__.py b/observatory_read_adapter/__init__.py new file mode 100644 index 0000000..c4e4945 --- /dev/null +++ b/observatory_read_adapter/__init__.py @@ -0,0 +1,6 @@ +"""Authenticated, read-only Observatory adapter for canonical Teleo claims.""" + +from .app import create_app +from .config import Settings + +__all__ = ["Settings", "create_app"] diff --git a/observatory_read_adapter/__main__.py b/observatory_read_adapter/__main__.py new file mode 100644 index 0000000..61803e2 --- /dev/null +++ b/observatory_read_adapter/__main__.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import logging + +from aiohttp import web + +from .app import create_app +from .config import Settings + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + settings = Settings.from_env() + web.run_app(create_app(settings), host="0.0.0.0", port=settings.port) + + +if __name__ == "__main__": + main() diff --git a/observatory_read_adapter/app.py b/observatory_read_adapter/app.py new file mode 100644 index 0000000..baf1c24 --- /dev/null +++ b/observatory_read_adapter/app.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import asyncio +import hmac +import logging +import uuid +from typing import Any + +from aiohttp import web + +from .config import Settings +from .db import CloudSqlClaimReader + +logger = logging.getLogger("teleo.observatory_read_adapter") + +SETTINGS_KEY = web.AppKey("observatory_settings", Settings) +READER_KEY = web.AppKey("observatory_reader", object) +PUBLIC_PATHS = frozenset({"/healthz", "/readyz"}) + + +def _private_json(payload: dict[str, Any], *, status: int = 200) -> web.Response: + response = web.json_response(payload, status=status) + response.headers["Cache-Control"] = "private, no-store, max-age=0" + response.headers["Pragma"] = "no-cache" + response.headers["Vary"] = "X-Api-Key" + response.headers["X-Content-Type-Options"] = "nosniff" + return response + + +@web.middleware +async def api_key_auth(request: web.Request, handler: Any) -> web.StreamResponse: + if request.path in PUBLIC_PATHS: + return await handler(request) + settings = request.app[SETTINGS_KEY] + provided = request.headers.get("X-Api-Key", "") + try: + authenticated = hmac.compare_digest( + provided.encode("ascii"), + settings.api_key.encode("ascii"), + ) + except UnicodeEncodeError: + authenticated = False + if not authenticated: + return _private_json({"error": "unauthorized"}, status=401) + return await handler(request) + + +async def healthz(_request: web.Request) -> web.Response: + return web.json_response({"status": "ok"}) + + +async def readyz(request: web.Request) -> web.Response: + reader = request.app[READER_KEY] + try: + await asyncio.wait_for(asyncio.to_thread(reader.check_ready), timeout=12.0) + except Exception as exc: + logger.warning("readiness check failed (%s)", type(exc).__name__) + return web.json_response({"status": "unavailable"}, status=503) + return web.json_response({"status": "ready"}) + + +async def get_sample_claim(request: web.Request) -> web.Response: + return await _claim_response(request, None) + + +async def get_claim(request: web.Request) -> web.Response: + raw_claim_id = request.match_info["claim_id"] + try: + claim_id = str(uuid.UUID(raw_claim_id)) + except ValueError: + return _private_json({"error": "invalid_claim_id"}, status=400) + return await _claim_response(request, claim_id) + + +async def _claim_response(request: web.Request, claim_id: str | None) -> web.Response: + reader = request.app[READER_KEY] + try: + payload = await asyncio.wait_for( + asyncio.to_thread(reader.read_claim, claim_id), + timeout=12.0, + ) + except Exception as exc: + logger.error("canonical claim read failed (%s)", type(exc).__name__) + return _private_json({"error": "canonical_claim_unavailable"}, status=503) + if payload is None: + return _private_json({"error": "claim_not_found"}, status=404) + return _private_json(payload) + + +async def _cleanup(app: web.Application) -> None: + reader = app[READER_KEY] + await asyncio.to_thread(reader.close) + + +def create_app( + settings: Settings | None = None, + *, + reader: Any | None = None, +) -> web.Application: + settings = settings or Settings.from_env() + settings.validate() + reader = reader or CloudSqlClaimReader(settings) + app = web.Application(middlewares=[api_key_auth], client_max_size=16 * 1024) + app[SETTINGS_KEY] = settings + app[READER_KEY] = reader + app.router.add_get("/healthz", healthz) + app.router.add_get("/readyz", readyz) + app.router.add_get("/v1/claims/sample", get_sample_claim) + app.router.add_get("/v1/claims/{claim_id}", get_claim) + app.on_cleanup.append(_cleanup) + return app diff --git a/observatory_read_adapter/config.py b/observatory_read_adapter/config.py new file mode 100644 index 0000000..b01b5ac --- /dev/null +++ b/observatory_read_adapter/config.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass, field + +EXPECTED_PROJECT = "teleo-501523" +EXPECTED_REGION = "europe-west6" +EXPECTED_INSTANCE = "teleo-pgvector-standby" +EXPECTED_CONNECTION_NAME = f"{EXPECTED_PROJECT}:{EXPECTED_REGION}:{EXPECTED_INSTANCE}" +EXPECTED_DATABASE = "teleo_canonical" +EXPECTED_AUTHORIZATION_ROLE = "kb_observatory_read" + + +@dataclass(frozen=True) +class Settings: + project_id: str + instance_connection_name: str + database: str + iam_database_user: str + authorization_role: str + api_key: str = field(repr=False) + service_revision: str = "unknown" + port: int = 8080 + + @classmethod + def from_env(cls) -> Settings: + settings = cls( + project_id=os.environ.get("GCP_PROJECT_ID", ""), + instance_connection_name=os.environ.get("CLOUD_SQL_INSTANCE", ""), + database=os.environ.get("DB_NAME", ""), + iam_database_user=os.environ.get("DB_IAM_USER", ""), + authorization_role=os.environ.get("DB_AUTHORIZATION_ROLE", ""), + api_key=os.environ.get("OBSERVATORY_API_KEY", ""), + service_revision=os.environ.get("SERVICE_REVISION", "unknown"), + port=int(os.environ.get("PORT", "8080")), + ) + settings.validate() + return settings + + def validate(self) -> None: + if self.project_id != EXPECTED_PROJECT: + raise ValueError(f"GCP_PROJECT_ID must be {EXPECTED_PROJECT}") + if self.instance_connection_name != EXPECTED_CONNECTION_NAME: + raise ValueError(f"CLOUD_SQL_INSTANCE must be {EXPECTED_CONNECTION_NAME}") + if self.database != EXPECTED_DATABASE: + raise ValueError(f"DB_NAME must be {EXPECTED_DATABASE}") + if self.authorization_role != EXPECTED_AUTHORIZATION_ROLE: + raise ValueError(f"DB_AUTHORIZATION_ROLE must be {EXPECTED_AUTHORIZATION_ROLE}") + if not self.iam_database_user.endswith(f"@{EXPECTED_PROJECT}.iam"): + raise ValueError("DB_IAM_USER must be the scoped Cloud SQL service-account user") + if any(character.isspace() for character in self.iam_database_user): + raise ValueError("DB_IAM_USER must not contain whitespace") + if ( + len(self.api_key) < 32 + or len(self.api_key) > 512 + or not self.api_key.isascii() + or any(character.isspace() for character in self.api_key) + ): + raise ValueError("OBSERVATORY_API_KEY must be 32-512 non-whitespace ASCII characters") + if not 1 <= self.port <= 65535: + raise ValueError("PORT is outside the valid TCP port range") diff --git a/observatory_read_adapter/db.py b/observatory_read_adapter/db.py new file mode 100644 index 0000000..8ed89c4 --- /dev/null +++ b/observatory_read_adapter/db.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +from collections.abc import Callable +from datetime import date, datetime, timezone +from decimal import Decimal +from typing import Any + +from .config import Settings + +ConnectionFactory = Callable[[], Any] + + +class ReaderContractError(RuntimeError): + """The live database session does not match the adapter's read-only contract.""" + + +def _json_value(value: Any) -> Any: + if isinstance(value, Decimal): + return float(value) + if isinstance(value, (date, datetime)): + return value.isoformat() + if isinstance(value, tuple): + return list(value) + return value + + +def _column_name(column: Any) -> str: + return str(column.name) if hasattr(column, "name") else str(column[0]) + + +def _row_dict(cursor: Any, row: Any) -> dict[str, Any] | None: + if row is None: + return None + names = [_column_name(column) for column in cursor.description] + return {name: _json_value(value) for name, value in zip(names, row, strict=True)} + + +def _rows(cursor: Any) -> list[dict[str, Any]]: + names = [_column_name(column) for column in cursor.description] + return [ + {name: _json_value(value) for name, value in zip(names, row, strict=True)} + for row in cursor.fetchall() + ] + + +class CloudSqlClaimReader: + def __init__(self, settings: Settings, *, connection_factory: ConnectionFactory | None = None): + self.settings = settings + self._connector: Any | None = None + if connection_factory is not None: + self._connection_factory = connection_factory + return + + from google.cloud.sql.connector import Connector, IPTypes + + self._connector = Connector(refresh_strategy="LAZY") + + def connect() -> Any: + return self._connector.connect( + settings.instance_connection_name, + "pg8000", + user=settings.iam_database_user, + db=settings.database, + enable_iam_auth=True, + ip_type=IPTypes.PRIVATE, + ) + + self._connection_factory = connect + + def close(self) -> None: + if self._connector is not None: + self._connector.close() + + def check_ready(self) -> dict[str, Any]: + connection = self._connection_factory() + try: + cursor = connection.cursor() + self._begin_read_only(cursor) + session = self._load_session_contract(cursor) + connection.rollback() + return { + "ready": True, + "database": session["database"], + "authorization_role": self.settings.authorization_role, + "transaction_read_only": True, + } + finally: + connection.close() + + def read_claim(self, claim_id: str | None = None) -> dict[str, Any] | None: + connection = self._connection_factory() + try: + cursor = connection.cursor() + self._begin_read_only(cursor) + session = self._load_session_contract(cursor) + claim = self._load_claim(cursor, claim_id) + if claim is None: + connection.rollback() + return None + + evidence = self._load_evidence(cursor, claim["id"]) + edges = self._load_edges(cursor, claim["id"]) + proposal_counts = self._load_proposal_counts(cursor) + related_proposals = self._load_related_proposals(cursor, claim["id"]) + recent_proposals = self._load_recent_proposals(cursor) + connection.rollback() + finally: + connection.close() + + generated_at = datetime.now(timezone.utc).isoformat() + return { + "schema": "livingip.observatory-canonical-claim.v1", + "generated_at": generated_at, + "read_only": True, + "provenance": { + "gcp_project": self.settings.project_id, + "cloud_sql_instance": self.settings.instance_connection_name, + "database": session["database"], + "database_principal": session["database_principal"], + "authorization_role": self.settings.authorization_role, + "transaction_read_only": True, + "write_privileges_denied": bool(session["required_writes_denied"]), + "service_revision": self.settings.service_revision, + }, + "canonical": { + "relation": "public.claims", + "claim": claim, + "evidence_relation": "public.claim_evidence -> public.sources", + "evidence": evidence, + "edge_relation": "public.claim_edges", + "edges": edges, + }, + "proposal_ledger": { + "relation": "kb_stage.kb_proposals", + "distinct_from_canonical": True, + "status_counts": proposal_counts, + "related": related_proposals, + "recent": recent_proposals, + "semantics": { + "pending_review": "staged only; not canonical", + "approved": "reviewed intent; not canonical until applied_at is set", + "applied": "proposal receipt only; canonical rows remain authoritative", + }, + }, + } + + @staticmethod + def _begin_read_only(cursor: Any) -> None: + cursor.execute("set transaction read only") + cursor.execute("set local statement_timeout = '5000ms'") + cursor.execute("set local lock_timeout = '1000ms'") + + def _load_session_contract(self, cursor: Any) -> dict[str, Any]: + cursor.execute( + """ + select current_database() as database, + current_user as database_principal, + current_setting('transaction_read_only') as transaction_read_only, + pg_has_role(current_user, %s, 'member') as has_authorization_role, + has_table_privilege(current_user, 'public.claims', 'select') + and has_table_privilege(current_user, 'public.sources', 'select') + and has_table_privilege(current_user, 'public.claim_evidence', 'select') + and has_table_privilege(current_user, 'public.claim_edges', 'select') + and has_table_privilege(current_user, 'kb_stage.kb_proposals', 'select') + as has_required_reads, + not ( + has_table_privilege(current_user, 'public.claims', 'insert,update,delete,truncate') + or has_table_privilege(current_user, 'public.sources', 'insert,update,delete,truncate') + or has_table_privilege(current_user, 'public.claim_evidence', 'insert,update,delete,truncate') + or has_table_privilege(current_user, 'public.claim_edges', 'insert,update,delete,truncate') + or has_table_privilege(current_user, 'kb_stage.kb_proposals', 'insert,update,delete,truncate') + ) as required_writes_denied + """, + (self.settings.authorization_role,), + ) + session = _row_dict(cursor, cursor.fetchone()) + if session is None: + raise ReaderContractError("database session contract returned no row") + if session["database"] != self.settings.database: + raise ReaderContractError("database session selected an unexpected database") + if session["database_principal"] != self.settings.iam_database_user: + raise ReaderContractError("database session selected an unexpected principal") + if session["transaction_read_only"] != "on": + raise ReaderContractError("database session is not read-only") + if not session["has_authorization_role"] or not session["has_required_reads"]: + raise ReaderContractError("database principal lacks the exact read authorization") + if not session["required_writes_denied"]: + raise ReaderContractError("database principal has an unexpected write privilege") + return session + + @staticmethod + def _load_claim(cursor: Any, claim_id: str | None) -> dict[str, Any] | None: + columns = """ + select c.id::text as id, + c.type::text as type, + left(c.text, 12000) as text, + c.status::text as status, + c.confidence, + coalesce(c.tags, '{}'::text[]) as tags, + c.superseded_by::text as superseded_by, + c.created_at, + c.updated_at + from public.claims c + """ + if claim_id is not None: + cursor.execute(columns + " where c.id = %s::uuid", (claim_id,)) + return _row_dict(cursor, cursor.fetchone()) + + cursor.execute( + columns + + """ + where exists ( + select 1 from public.claim_evidence ce where ce.claim_id = c.id + ) + order by coalesce(c.updated_at, c.created_at) desc, c.id desc + limit 1 + """ + ) + claim = _row_dict(cursor, cursor.fetchone()) + if claim is not None: + return claim + cursor.execute(columns + " order by coalesce(c.updated_at, c.created_at) desc, c.id desc limit 1") + return _row_dict(cursor, cursor.fetchone()) + + @staticmethod + def _load_evidence(cursor: Any, claim_id: str) -> list[dict[str, Any]]: + cursor.execute( + """ + select ce.id::text as evidence_id, + ce.role::text as role, + ce.weight, + ce.created_at as linked_at, + s.id::text as source_id, + s.source_type::text as source_type, + s.url, + s.storage_path, + left(coalesce(s.excerpt, ''), 2400) as excerpt, + s.hash as content_hash, + s.captured_at, + s.created_at as source_created_at + from public.claim_evidence ce + join public.sources s on s.id = ce.source_id + where ce.claim_id = %s::uuid + order by ce.weight desc nulls last, ce.created_at, ce.id + limit 30 + """, + (claim_id,), + ) + return _rows(cursor) + + @staticmethod + def _load_edges(cursor: Any, claim_id: str) -> list[dict[str, Any]]: + cursor.execute( + """ + select e.id::text as edge_id, + case when e.from_claim = %s::uuid then 'outgoing' else 'incoming' end as direction, + e.edge_type::text as edge_type, + e.weight, + case when e.from_claim = %s::uuid then e.to_claim::text else e.from_claim::text end + as connected_claim_id, + e.created_at + from public.claim_edges e + where e.from_claim = %s::uuid or e.to_claim = %s::uuid + order by e.created_at, e.id + limit 40 + """, + (claim_id, claim_id, claim_id, claim_id), + ) + return _rows(cursor) + + @staticmethod + def _load_proposal_counts(cursor: Any) -> dict[str, int]: + cursor.execute( + "select status::text as status, count(*)::int as count " + "from kb_stage.kb_proposals group by status order by status" + ) + return {str(status): int(count) for status, count in cursor.fetchall()} + + @staticmethod + def _load_related_proposals(cursor: Any, claim_id: str) -> list[dict[str, Any]]: + cursor.execute( + """ + select p.id::text as id, + p.proposal_type::text as proposal_type, + p.status::text as status, + to_jsonb(p)->>'source_ref' as source_ref, + p.reviewed_at, + p.applied_at, + p.updated_at + from kb_stage.kb_proposals p + where p.payload::text like %s + order by p.updated_at desc, p.id desc + limit 20 + """, + (f"%{claim_id}%",), + ) + return _rows(cursor) + + @staticmethod + def _load_recent_proposals(cursor: Any) -> list[dict[str, Any]]: + cursor.execute( + """ + select p.id::text as id, + p.proposal_type::text as proposal_type, + p.status::text as status, + to_jsonb(p)->>'source_ref' as source_ref, + p.reviewed_at, + p.applied_at, + p.updated_at + from kb_stage.kb_proposals p + order by p.updated_at desc, p.id desc + limit 10 + """ + ) + return _rows(cursor) diff --git a/ops/observatory_read_role.sql b/ops/observatory_read_role.sql new file mode 100644 index 0000000..0536237 --- /dev/null +++ b/ops/observatory_read_role.sql @@ -0,0 +1,183 @@ +\set ON_ERROR_STOP on +\getenv observatory_iam_user OBSERVATORY_DB_IAM_USER + +-- Provision only after Cloud SQL has created the dedicated IAM database user. +-- The service-account username omits the .gserviceaccount.com suffix. +select set_config('observatory.iam_user', :'observatory_iam_user', false); + +do $preflight$ +declare + principal text := current_setting('observatory.iam_user'); +begin + if current_database() <> 'teleo_canonical' then + raise exception 'observatory_read_role must run only in teleo_canonical'; + end if; + if principal !~ '^[a-z0-9-]+@teleo-501523[.]iam$' then + raise exception 'OBSERVATORY_DB_IAM_USER is not the scoped GCP service-account database user'; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = principal) then + raise exception 'Cloud SQL IAM database user does not exist: %', principal; + end if; +end +$preflight$; + +select 'create role kb_observatory_read nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1' +where not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_observatory_read') +\gexec + +alter role kb_observatory_read + with nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1 + password null; +alter role kb_observatory_read reset all; + +revoke all privileges on database teleo_canonical from kb_observatory_read; +grant connect on database teleo_canonical to kb_observatory_read; + +revoke all on schema public, kb_stage from kb_observatory_read; +revoke all privileges on all tables in schema public, kb_stage from kb_observatory_read; +revoke all privileges on all sequences in schema public, kb_stage from kb_observatory_read; +revoke all privileges on all functions in schema public, kb_stage from kb_observatory_read; +revoke all privileges on all procedures in schema public, kb_stage from kb_observatory_read; + +select format( + 'revoke all privileges (%s) on table %I.%I from kb_observatory_read', + pg_catalog.string_agg(pg_catalog.quote_ident(attribute.attname), ', ' order by attribute.attnum), + namespace.nspname, + relation.relname + ) + from pg_catalog.pg_class relation + join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace + join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid + where namespace.nspname in ('public', 'kb_stage') + and relation.relkind in ('r', 'p', 'v', 'm', 'f') + and attribute.attnum > 0 + and not attribute.attisdropped + group by namespace.nspname, relation.relname +\gexec + +grant usage on schema public, kb_stage to kb_observatory_read; +grant select on + public.claims, + public.sources, + public.claim_evidence, + public.claim_edges, + kb_stage.kb_proposals +to kb_observatory_read; + +do $membership$ +declare + principal text := current_setting('observatory.iam_user'); + unexpected text; +begin + select pg_catalog.string_agg(granted.rolname, ', ' order by granted.rolname) + into unexpected + from pg_catalog.pg_auth_members membership + join pg_catalog.pg_roles granted on granted.oid = membership.roleid + join pg_catalog.pg_roles member on member.oid = membership.member + where member.rolname = principal + and granted.rolname <> 'kb_observatory_read'; + if unexpected is not null then + raise exception 'dedicated Observatory principal has unexpected role memberships: %', unexpected; + end if; + + select pg_catalog.string_agg(member.rolname, ', ' order by member.rolname) + into unexpected + from pg_catalog.pg_auth_members membership + join pg_catalog.pg_roles granted on granted.oid = membership.roleid + join pg_catalog.pg_roles member on member.oid = membership.member + where granted.rolname = 'kb_observatory_read' + and member.rolname <> principal; + if unexpected is not null then + raise exception 'kb_observatory_read has unexpected members: %', unexpected; + end if; + + execute pg_catalog.format('grant kb_observatory_read to %I', principal); + execute pg_catalog.format( + 'alter role %I in database teleo_canonical set default_transaction_read_only = on', + principal + ); + execute pg_catalog.format( + 'alter role %I in database teleo_canonical set statement_timeout = %L', + principal, + '5s' + ); + execute pg_catalog.format( + 'alter role %I in database teleo_canonical set lock_timeout = %L', + principal, + '1s' + ); +end +$membership$; + +do $verify$ +declare + principal text := current_setting('observatory.iam_user'); + unexpected text; +begin + if not pg_catalog.pg_has_role(principal, 'kb_observatory_read', 'member') then + raise exception 'Observatory principal is not a member of kb_observatory_read'; + end if; + if not exists ( + select 1 from pg_catalog.pg_roles + where rolname = principal and rolinherit and not rolsuper and not rolcreatedb + and not rolcreaterole and not rolreplication and not rolbypassrls + ) then + raise exception 'Observatory principal has unsafe role attributes or cannot inherit read grants'; + end if; + + select pg_catalog.string_agg( + pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name), + ', ' order by namespace.nspname, relation.relname, privilege.name + ) + into unexpected + from pg_catalog.pg_class relation + join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace + cross join (values ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) privilege(name) + where namespace.nspname in ('public', 'kb_stage') + and relation.relkind in ('r', 'p', 'v', 'm', 'f') + and pg_catalog.has_table_privilege(principal, relation.oid, privilege.name); + if unexpected is not null then + raise exception 'Observatory principal has effective table writes: %', unexpected; + end if; + + select pg_catalog.string_agg( + pg_catalog.format('%I.%I', namespace.nspname, relation.relname), + ', ' order by namespace.nspname, relation.relname + ) + into unexpected + from pg_catalog.pg_class relation + join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace + where namespace.nspname in ('public', 'kb_stage') + and relation.relkind in ('r', 'p', 'v', 'm', 'f') + and pg_catalog.has_table_privilege(principal, relation.oid, 'SELECT') + and (namespace.nspname, relation.relname) not in ( + ('public', 'claims'), + ('public', 'sources'), + ('public', 'claim_evidence'), + ('public', 'claim_edges'), + ('kb_stage', 'kb_proposals') + ); + if unexpected is not null then + raise exception 'Observatory principal can SELECT outside its allowlist: %', unexpected; + end if; + + if not ( + pg_catalog.has_table_privilege(principal, 'public.claims', 'SELECT') + and pg_catalog.has_table_privilege(principal, 'public.sources', 'SELECT') + and pg_catalog.has_table_privilege(principal, 'public.claim_evidence', 'SELECT') + and pg_catalog.has_table_privilege(principal, 'public.claim_edges', 'SELECT') + and pg_catalog.has_table_privilege(principal, 'kb_stage.kb_proposals', 'SELECT') + ) then + raise exception 'Observatory principal is missing a required read grant'; + end if; +end +$verify$; + +select jsonb_build_object( + 'database', current_database(), + 'authorization_role', 'kb_observatory_read', + 'database_principal', current_setting('observatory.iam_user'), + 'required_reads', true, + 'effective_table_writes_denied', true, + 'default_transaction_read_only', true +)::text; diff --git a/pyproject.toml b/pyproject.toml index cf64b66..e264213 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,12 @@ dev = [ "pytest-asyncio>=0.23", "ruff>=0.3", ] +observatory = [ + "cloud-sql-python-connector[pg8000]>=1.20.4,<1.21", +] [tool.setuptools] -packages = ["lib"] +packages = ["lib", "observatory_read_adapter"] [tool.ruff] target-version = "py311" diff --git a/tests/test_observatory_read_adapter.py b/tests/test_observatory_read_adapter.py new file mode 100644 index 0000000..f259145 --- /dev/null +++ b/tests/test_observatory_read_adapter.py @@ -0,0 +1,346 @@ +from __future__ import annotations + +import asyncio +import json +from pathlib import Path + +from aiohttp.test_utils import TestClient, TestServer + +from observatory_read_adapter.app import create_app +from observatory_read_adapter.config import EXPECTED_CONNECTION_NAME, Settings +from observatory_read_adapter.db import CloudSqlClaimReader + +CLAIM_ID = "d0000000-0000-0000-0000-000000000005" +API_KEY = "observatory-test-api-key-000000000000000000000000" +ROOT = Path(__file__).resolve().parents[1] + + +def settings(**overrides: object) -> Settings: + values = { + "project_id": "teleo-501523", + "instance_connection_name": EXPECTED_CONNECTION_NAME, + "database": "teleo_canonical", + "iam_database_user": "sa-observatory-read-adapter@teleo-501523.iam", + "authorization_role": "kb_observatory_read", + "api_key": API_KEY, + "service_revision": "test-revision", + } + values.update(overrides) + return Settings(**values) + + +class FakeReader: + def __init__(self) -> None: + self.closed = False + + def check_ready(self) -> dict[str, object]: + return { + "ready": True, + "database": "teleo_canonical", + "authorization_role": "kb_observatory_read", + "transaction_read_only": True, + } + + def read_claim(self, claim_id: str | None = None) -> dict[str, object] | None: + if claim_id not in (None, CLAIM_ID): + return None + return { + "schema": "livingip.observatory-canonical-claim.v1", + "read_only": True, + "provenance": { + "database": "teleo_canonical", + "database_principal": "sa-observatory-read-adapter@teleo-501523.iam", + "authorization_role": "kb_observatory_read", + "transaction_read_only": True, + "write_privileges_denied": True, + }, + "canonical": { + "claim": {"id": CLAIM_ID, "status": "open", "text": "Canonical claim"}, + "evidence": [{"source_id": "e0000000-0000-0000-0000-000000000006"}], + }, + "proposal_ledger": { + "distinct_from_canonical": True, + "status_counts": {"pending_review": 2, "approved": 1, "applied": 1}, + "related": [{"status": "approved", "applied_at": None}], + }, + } + + def close(self) -> None: + self.closed = True + + +class FakeCursor: + def __init__(self) -> None: + self.description: list[tuple[str]] = [] + self.result: list[tuple[object, ...]] = [] + self.executed: list[tuple[str, tuple[object, ...]]] = [] + + def execute(self, sql: str, parameters: tuple[object, ...] = ()) -> None: + normalized = " ".join(sql.split()).lower() + self.executed.append((normalized, parameters)) + self.description = [] + self.result = [] + if "current_database() as database" in normalized: + self.description = [ + ("database",), + ("database_principal",), + ("transaction_read_only",), + ("has_authorization_role",), + ("has_required_reads",), + ("required_writes_denied",), + ] + self.result = [ + ( + "teleo_canonical", + "sa-observatory-read-adapter@teleo-501523.iam", + "on", + True, + True, + True, + ) + ] + elif "from public.claims c" in normalized: + self.description = [ + ("id",), + ("type",), + ("text",), + ("status",), + ("confidence",), + ("tags",), + ("superseded_by",), + ("created_at",), + ("updated_at",), + ] + self.result = [ + ( + CLAIM_ID, + "strategy", + "Canonical claim", + "open", + 0.8, + ["test"], + None, + "2026-07-15T00:00:00+00:00", + "2026-07-15T00:00:00+00:00", + ) + ] + elif "from public.claim_evidence ce" in normalized: + self.description = [ + ("evidence_id",), + ("role",), + ("weight",), + ("linked_at",), + ("source_id",), + ("source_type",), + ("url",), + ("storage_path",), + ("excerpt",), + ("content_hash",), + ("captured_at",), + ("source_created_at",), + ] + self.result = [ + ( + "e0000000-0000-0000-0000-000000000006", + "grounds", + 1.0, + "2026-07-15T00:00:00+00:00", + "f0000000-0000-0000-0000-000000000007", + "document", + "https://example.test/source", + None, + "Source excerpt", + "sha256:test", + "2026-07-15T00:00:00+00:00", + "2026-07-15T00:00:00+00:00", + ) + ] + elif "from public.claim_edges e" in normalized: + self.description = [ + ("edge_id",), + ("direction",), + ("edge_type",), + ("weight",), + ("connected_claim_id",), + ("created_at",), + ] + self.result = [] + elif "group by status" in normalized: + self.description = [("status",), ("count",)] + self.result = [("approved", 1), ("applied", 1), ("pending_review", 2)] + elif "where p.payload::text like" in normalized: + self.description = [ + ("id",), + ("proposal_type",), + ("status",), + ("source_ref",), + ("reviewed_at",), + ("applied_at",), + ("updated_at",), + ] + self.result = [ + ( + "a0000000-0000-0000-0000-000000000001", + "revise_claim", + "approved", + "source:test", + "2026-07-15T00:00:00+00:00", + None, + "2026-07-15T00:00:00+00:00", + ) + ] + elif "from kb_stage.kb_proposals p" in normalized: + self.description = [ + ("id",), + ("proposal_type",), + ("status",), + ("source_ref",), + ("reviewed_at",), + ("applied_at",), + ("updated_at",), + ] + self.result = [] + + def fetchone(self) -> tuple[object, ...] | None: + return self.result.pop(0) if self.result else None + + def fetchall(self) -> list[tuple[object, ...]]: + result, self.result = self.result, [] + return result + + +class FakeConnection: + def __init__(self) -> None: + self.cursor_value = FakeCursor() + self.rolled_back = False + self.closed = False + + def cursor(self) -> FakeCursor: + return self.cursor_value + + def rollback(self) -> None: + self.rolled_back = True + + def close(self) -> None: + self.closed = True + + +def test_settings_fail_closed_on_wrong_database_role_or_short_key() -> None: + for overrides in ( + {"database": "teleo"}, + {"authorization_role": "leoclean_kb_runtime"}, + {"api_key": "too-short"}, + {"instance_connection_name": "other:region:instance"}, + ): + candidate = settings(**overrides) + try: + candidate.validate() + except ValueError: + pass + else: + raise AssertionError(f"unsafe settings accepted: {overrides}") + + +def test_authenticated_claim_response_and_negative_http_paths() -> None: + async def exercise() -> None: + reader = FakeReader() + client = TestClient(TestServer(create_app(settings(), reader=reader))) + await client.start_server() + try: + anonymous = await client.get("/v1/claims/sample") + assert anonymous.status == 401 + assert await anonymous.json() == {"error": "unauthorized"} + + response = await client.get( + f"/v1/claims/{CLAIM_ID}", + headers={"X-Api-Key": API_KEY}, + ) + assert response.status == 200 + assert response.headers["Cache-Control"] == "private, no-store, max-age=0" + payload = await response.json() + assert payload["schema"] == "livingip.observatory-canonical-claim.v1" + assert payload["provenance"]["database"] == "teleo_canonical" + assert payload["provenance"]["write_privileges_denied"] is True + assert payload["canonical"]["evidence"] + assert payload["proposal_ledger"]["distinct_from_canonical"] is True + assert payload["proposal_ledger"]["related"][0]["applied_at"] is None + assert API_KEY not in json.dumps(payload) + + write_attempt = await client.post( + f"/v1/claims/{CLAIM_ID}", + headers={"X-Api-Key": API_KEY}, + json={"status": "applied"}, + ) + assert write_attempt.status == 405 + + invalid = await client.get( + "/v1/claims/not-a-uuid", + headers={"X-Api-Key": API_KEY}, + ) + assert invalid.status == 400 + finally: + await client.close() + assert reader.closed is True + + asyncio.run(exercise()) + + +def test_readiness_checks_database_contract_without_exposing_claims() -> None: + async def exercise() -> None: + client = TestClient(TestServer(create_app(settings(), reader=FakeReader()))) + await client.start_server() + try: + response = await client.get("/readyz") + payload = await response.json() + assert response.status == 200 + assert payload == {"status": "ready"} + finally: + await client.close() + + asyncio.run(exercise()) + + +def test_database_reader_returns_provenance_and_keeps_proposals_distinct() -> None: + connection = FakeConnection() + reader = CloudSqlClaimReader(settings(), connection_factory=lambda: connection) + + payload = reader.read_claim(CLAIM_ID) + + assert payload is not None + assert payload["provenance"]["database"] == "teleo_canonical" + assert payload["provenance"]["database_principal"] == settings().iam_database_user + assert payload["provenance"]["write_privileges_denied"] is True + assert payload["canonical"]["claim"]["id"] == CLAIM_ID + assert payload["canonical"]["evidence"][0]["content_hash"] == "sha256:test" + assert payload["proposal_ledger"]["distinct_from_canonical"] is True + assert payload["proposal_ledger"]["related"][0]["status"] == "approved" + assert payload["proposal_ledger"]["related"][0]["applied_at"] is None + assert connection.rolled_back is True + assert connection.closed is True + assert any("set transaction read only" in sql for sql, _parameters in connection.cursor_value.executed) + + +def test_database_role_contract_is_select_only_and_iam_scoped() -> None: + sql = (ROOT / "ops" / "observatory_read_role.sql").read_text(encoding="utf-8") + lowered = sql.lower() + + assert "kb_observatory_read nologin" in lowered + assert "default_transaction_read_only = on" in lowered + assert "public.claims" in lowered + assert "public.sources" in lowered + assert "public.claim_evidence" in lowered + assert "public.claim_edges" in lowered + assert "kb_stage.kb_proposals" in lowered + assert "grant select on" in lowered + assert "grant insert" not in lowered + assert "grant update" not in lowered + assert "grant delete" not in lowered + assert "effective_table_writes_denied" in lowered + + +def test_container_runs_as_non_root_and_contains_no_password_contract() -> None: + dockerfile = (ROOT / "Dockerfile.observatory-read-adapter").read_text(encoding="utf-8") + + assert "USER 10001:10001" in dockerfile + assert "PGPASSWORD" not in dockerfile + assert "DB_PASS" not in dockerfile diff --git a/tests/test_observatory_read_role_postgres.py b/tests/test_observatory_read_role_postgres.py new file mode 100644 index 0000000..be2b155 --- /dev/null +++ b/tests/test_observatory_read_role_postgres.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +import shutil +import subprocess +import time +import uuid +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +ROLE_SQL = ROOT / "ops" / "observatory_read_role.sql" +POSTGRES_IMAGE = "postgres:16-alpine" +DATABASE = "teleo_canonical" +IAM_USER = "sa-observatory-read-adapter@teleo-501523.iam" +TEST_PASSWORD = "generated-test-only-password" + +BOOTSTRAP_SQL = f""" +begin; +create schema kb_stage; +create role "{IAM_USER}" login inherit password '{TEST_PASSWORD}'; +create table public.claims (id uuid primary key); +create table public.sources (id uuid primary key); +create table public.claim_evidence (id uuid primary key); +create table public.claim_edges (id uuid primary key); +create table public.private_notes (id uuid primary key); +create table kb_stage.kb_proposals (id uuid primary key); +insert into public.claims values ('11111111-1111-1111-1111-111111111111'); +commit; +""" + + +def run(command: list[str], *, input_text: str | None = None, check: bool = True) -> subprocess.CompletedProcess[str]: + return subprocess.run( + command, + input=input_text, + text=True, + capture_output=True, + check=check, + timeout=30, + ) + + +@pytest.mark.skipif(shutil.which("docker") is None, reason="Docker is required") +def test_observatory_role_is_read_only_and_exactly_allowlisted() -> None: + container = f"teleo-observatory-role-{uuid.uuid4().hex[:10]}" + run( + [ + "docker", + "run", + "--detach", + "--rm", + "--name", + container, + "--env", + f"POSTGRES_PASSWORD={TEST_PASSWORD}", + "--env", + f"POSTGRES_DB={DATABASE}", + POSTGRES_IMAGE, + ] + ) + try: + deadline = time.monotonic() + 25 + while time.monotonic() < deadline: + ready = run( + ["docker", "exec", container, "pg_isready", "-U", "postgres", "-d", DATABASE], + check=False, + ) + if ready.returncode == 0: + break + time.sleep(0.25) + else: + raise AssertionError("ephemeral Postgres did not become ready") + + bootstrap: subprocess.CompletedProcess[str] | None = None + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + bootstrap = run( + [ + "docker", + "exec", + "-i", + container, + "psql", + "--set", + "ON_ERROR_STOP=1", + "-U", + "postgres", + "-d", + DATABASE, + ], + input_text=BOOTSTRAP_SQL, + check=False, + ) + if bootstrap.returncode == 0: + break + time.sleep(0.25) + else: + assert bootstrap is not None + raise AssertionError(f"ephemeral Postgres bootstrap failed: {bootstrap.stderr[-1000:]}") + migration = run( + [ + "docker", + "exec", + "-e", + f"OBSERVATORY_DB_IAM_USER={IAM_USER}", + "-i", + container, + "psql", + "-U", + "postgres", + "-d", + DATABASE, + ], + input_text=ROLE_SQL.read_text(encoding="utf-8"), + ) + assert '"effective_table_writes_denied": true' in migration.stdout + + readback = run( + [ + "docker", + "exec", + "-e", + f"PGPASSWORD={TEST_PASSWORD}", + container, + "psql", + "-h", + "127.0.0.1", + "-U", + IAM_USER, + "-d", + DATABASE, + "-At", + "-c", + "show default_transaction_read_only; select count(*) from public.claims;", + ] + ) + assert readback.stdout.splitlines() == ["on", "1"] + + write_attempt = run( + [ + "docker", + "exec", + "-e", + f"PGPASSWORD={TEST_PASSWORD}", + container, + "psql", + "-h", + "127.0.0.1", + "-U", + IAM_USER, + "-d", + DATABASE, + "-c", + "insert into public.claims values ('22222222-2222-2222-2222-222222222222');", + ], + check=False, + ) + assert write_attempt.returncode != 0 + + outside_allowlist = run( + [ + "docker", + "exec", + "-e", + f"PGPASSWORD={TEST_PASSWORD}", + container, + "psql", + "-h", + "127.0.0.1", + "-U", + IAM_USER, + "-d", + DATABASE, + "-c", + "select * from public.private_notes;", + ], + check=False, + ) + assert outside_allowlist.returncode != 0 + finally: + run(["docker", "rm", "--force", container], check=False) From 53c8de0a1959a744e17888df1a70e935dfef0143 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 03:51:54 +0200 Subject: [PATCH 06/27] Make Observatory staging deploy rerunnable (#155) --- .../workflows/gcp-observatory-read-adapter.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gcp-observatory-read-adapter.yml b/.github/workflows/gcp-observatory-read-adapter.yml index e4c088f..95b18a4 100644 --- a/.github/workflows/gcp-observatory-read-adapter.yml +++ b/.github/workflows/gcp-observatory-read-adapter.yml @@ -74,12 +74,21 @@ jobs: set -euo pipefail tag="${GITHUB_SHA::12}" image="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${tag}" - docker build -f Dockerfile.observatory-read-adapter -t "${image}" . + digest="$(gcloud artifacts docker images describe "${image}" \ + --project="${PROJECT_ID}" \ + --format='value(image_summary.digest)' 2>/dev/null || true)" + if [[ -n "${digest}" ]]; then + docker pull "${image}" + else + docker build -f Dockerfile.observatory-read-adapter -t "${image}" . + fi docker run --rm --env PYTHONPYCACHEPREFIX=/tmp/pycache \ "${image}" python -m compileall -q /app/observatory_read_adapter docker run --rm "${image}" python -c 'import observatory_read_adapter; print("adapter-import-ok")' - docker push "${image}" | tee docker-push.log - digest="$(awk '/digest: sha256:/ {print $3}' docker-push.log | tail -1)" + if [[ -z "${digest}" ]]; then + docker push "${image}" | tee docker-push.log + digest="$(awk '/digest: sha256:/ {print $3}' docker-push.log | tail -1)" + fi test -n "${digest}" image_ref="${image}@${digest}" printf 'image_ref=%s\n' "${image_ref}" >> "${GITHUB_OUTPUT}" From e4348a0071f97e3962bdfbaf69a00b95b002df36 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 03:59:36 +0200 Subject: [PATCH 07/27] Record Observatory staging deployment gate (#156) --- .../gcp-staging-current-gate.json | 40 ++++++++++++++ .../gcp-staging-live-attempt-redacted.json | 55 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 docs/reports/observatory-read-adapter/gcp-staging-current-gate.json create mode 100644 docs/reports/observatory-read-adapter/gcp-staging-live-attempt-redacted.json diff --git a/docs/reports/observatory-read-adapter/gcp-staging-current-gate.json b/docs/reports/observatory-read-adapter/gcp-staging-current-gate.json new file mode 100644 index 0000000..f87f8cb --- /dev/null +++ b/docs/reports/observatory-read-adapter/gcp-staging-current-gate.json @@ -0,0 +1,40 @@ +{ + "schema": "livingip.observatoryReadAdapterCurrentGate.v1", + "current_canary": "Authenticated GET /v1/claims/sample in GCP staging returns a provenance-bearing teleo_canonical claim, anonymous GET returns 401, and authenticated POST returns 405.", + "permission_profile": "approval_policy_never_with_unrestricted_filesystem_and_network", + "attempted_routes": [ + { + "route": "merged GitHub Actions deployment with artifact-builder WIF", + "result": "image build, smoke, and push passed; deploy stopped because run.googleapis.com is disabled" + }, + { + "route": "local billy@livingip.xyz gcloud user credential", + "result": "token refresh and service enable both stop at noninteractive Google reauthentication" + }, + { + "route": "local application-default credential", + "result": "token refresh stops at noninteractive Google reauthentication" + }, + { + "route": "other locally authenticated Google accounts", + "result": "valid tokens but no access to project teleo-501523" + }, + { + "route": "fixed read-only GCP IAP status workflow", + "result": "WIF token exchange failed because the teleo-iap-operator provider target is missing or disabled" + }, + { + "route": "local browser or computer-use session", + "result": "no browser or computer-use tool is exposed to this task; no foreground takeover was attempted" + } + ], + "exact_gate": { + "first_infrastructure_gate": "Cloud Run Admin API run.googleapis.com is disabled in teleo-501523.", + "current_authorization_gate": "The only local account authorized for teleo-501523, billy@livingip.xyz, requires Google password or MFA reauthentication before project APIs and staging IAM resources can be changed.", + "alternate_private_operator_gate": "The read-only teleo-iap-operator WIF provider returns invalid_target and cannot reach its status operation." + }, + "why_autonomous_repair_stops": "Google password or MFA reauthentication is a human-only credential boundary, and no authenticated project-admin browser or computer-use surface is available in this task.", + "clear_CTA": "Run `gcloud auth login billy@livingip.xyz --update-adc`, complete Google password or MFA, and then report `GCP reauth complete`.", + "next_non_user_action": "Enable the required staging APIs; create or verify the runtime service account, API-key secret, Cloud SQL IAM database user, and exact read-only role grants; deploy the retained immutable image; capture authenticated GET, anonymous 401, authenticated POST 405, SQL write-denial, exact service/database identity, and cleanup receipts.", + "production_boundary": "Do not change Vercel, production routing, Cloud SQL network exposure, or any write endpoint." +} diff --git a/docs/reports/observatory-read-adapter/gcp-staging-live-attempt-redacted.json b/docs/reports/observatory-read-adapter/gcp-staging-live-attempt-redacted.json new file mode 100644 index 0000000..8c50ace --- /dev/null +++ b/docs/reports/observatory-read-adapter/gcp-staging-live-attempt-redacted.json @@ -0,0 +1,55 @@ +{ + "schema": "livingip.observatoryReadAdapterStagingReceipt.v1", + "status": "blocked_before_runtime_creation", + "required_runtime_tier": "T3_live_gcp_staging_api", + "current_runtime_tier": "below_T3_build_artifact_only", + "project_id": "teleo-501523", + "region": "europe-west6", + "service_name": "observatory-read-adapter-staging", + "database_name": "teleo_canonical", + "cloud_sql_instance": "teleo-501523:europe-west6:teleo-pgvector-standby", + "authorization_role": "kb_observatory_read", + "runtime_service_account": "sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com", + "image": { + "revision": "53c8de0a1959a744e17888df1a70e935dfef0143", + "digest": "sha256:b5c9b2051559730cbb9003d74e5d8e577fa2f43f49b72577f251f2843a0650c5", + "artifact_registry_push": "pass", + "container_smoke": "pass", + "intentional_retained_resource": true + }, + "workflow": { + "run_id": 29382683335, + "url": "https://github.com/living-ip/teleo-infrastructure/actions/runs/29382683335", + "build_job": "success", + "deploy_job": "failure", + "failure_class": "SERVICE_DISABLED", + "failure_detail": "run.googleapis.com is disabled in teleo-501523", + "authenticated_as": "sa-artifact-builder@teleo-501523.iam.gserviceaccount.com" + }, + "live_receipts": { + "service_url": null, + "authenticated_get_sample": null, + "anonymous_get_401": null, + "authenticated_post_405": null, + "database_write_denial": null, + "database_identity": null + }, + "negative_claims": { + "cloud_run_service_created": false, + "cloud_sql_public_exposure_changed": false, + "production_or_vercel_routing_changed": false, + "credentials_logged": false + }, + "cleanup": { + "github_jobs_completed": true, + "local_observatory_test_containers": 0, + "local_observatory_test_images": 0, + "orphan_local_build_processes": 0, + "artifact_registry_image_retained_for_next_deploy": true + }, + "proof_paths": [ + "/private/tmp/observatory-read-adapter-run-29382683335/observatory-adapter-image/observatory-adapter-image.txt", + "/private/tmp/observatory-iap-status-29382844567/gcp-iap-operator-29382844567/result.json" + ], + "claim_ceiling": "The immutable staging image exists, but no live API or database query receipt exists. T3 is not complete." +} From db64500a12a64206e1bb7fd0c9a0a21bca755c3d Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 03:59:47 +0200 Subject: [PATCH 08/27] feat: rehearse approved proposal apply and rollback --- .../proposal-apply-clone-rollback-current.sql | 120 + ...oposal-apply-lifecycle-canary-current.json | 3324 +++++++++++++++++ ...posal-apply-lifecycle-receipt-current.json | 217 ++ ...ly-production-target-readback-current.json | 32 + pyproject.toml | 1 + scripts/apply_proposal.py | 369 +- scripts/apply_worker.py | 200 +- .../finalize_approve_claim_isolated_canary.py | 230 ++ scripts/kb_apply_replay_receipt.py | 35 + scripts/proposal_apply_lifecycle.py | 1513 ++++++++ scripts/run_approve_claim_clone_canary.py | 857 ++--- ...approve_claim_isolated_container_canary.sh | 268 +- tests/test_apply_proposal.py | 42 +- tests/test_apply_worker.py | 312 +- tests/test_proposal_apply_lifecycle.py | 846 +++++ ...n_approve_claim_clone_canary_macos_path.py | 82 +- 16 files changed, 7507 insertions(+), 941 deletions(-) create mode 100644 docs/reports/leo-working-state-20260709/proposal-apply-clone-rollback-current.sql create mode 100644 docs/reports/leo-working-state-20260709/proposal-apply-lifecycle-canary-current.json create mode 100644 docs/reports/leo-working-state-20260709/proposal-apply-lifecycle-receipt-current.json create mode 100644 docs/reports/leo-working-state-20260709/proposal-apply-production-target-readback-current.json create mode 100644 scripts/finalize_approve_claim_isolated_canary.py create mode 100644 scripts/proposal_apply_lifecycle.py create mode 100644 tests/test_proposal_apply_lifecycle.py diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-clone-rollback-current.sql b/docs/reports/leo-working-state-20260709/proposal-apply-clone-rollback-current.sql new file mode 100644 index 0000000..299b52b --- /dev/null +++ b/docs/reports/leo-working-state-20260709/proposal-apply-clone-rollback-current.sql @@ -0,0 +1,120 @@ +begin; +set local standard_conforming_strings = on; +set local lock_timeout = '5s'; +select pg_advisory_xact_lock(hashtextextended('proposal-rollback:' || E'9b2b5fa4-0e0d-5db0-aa4d-6fd102889946', 0)); +do $rollback$ +declare + affected integer; + approval_now jsonb; +begin + perform 1 from kb_stage.kb_proposals + where id = E'9b2b5fa4-0e0d-5db0-aa4d-6fd102889946'::uuid + for update; + if not found then + raise exception 'proposal rollback: proposal row missing'; + end if; + if not exists ( + select 1 from kb_stage.kb_proposals + where id = E'9b2b5fa4-0e0d-5db0-aa4d-6fd102889946'::uuid + and proposal_type = 'approve_claim' + and status = 'applied' + and payload = E'{"apply_payload":{"agent_id":null,"claims":[{"confidence":0.9,"created_by":null,"id":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","status":"open","superseded_by":null,"tags":["working-leo","clone-canary"],"text":"An approved strict proposal can create exact canonical rows atomically.","type":"structural"},{"confidence":0.85,"created_by":null,"id":"71a3331b-fb75-5e18-aa61-b256bc38af36","status":"open","superseded_by":null,"tags":["working-leo","row-proof"],"text":"Row-level proof is the authority for canonical KB state.","type":"concept"}],"contract_version":2,"edges":[{"created_by":null,"edge_type":"supports","from_claim":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","to_claim":"71a3331b-fb75-5e18-aa61-b256bc38af36","weight":0.75}],"evidence":[{"claim_id":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","created_by":null,"role":"grounds","source_id":"ed9da5f1-846e-5298-8689-15027a7ce56d","weight":0.9},{"claim_id":"71a3331b-fb75-5e18-aa61-b256bc38af36","created_by":null,"role":"illustrates","source_id":"a86f55dc-5058-585d-b492-a4f1d932670f","weight":0.8}],"reasoning_tools":[{"agent_id":null,"category":"verification","description":"Verify approved proposal to canonical graph lifecycle in a disposable clone.","id":"2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0","name":"Canonical row proof canary"}],"sources":[{"created_by":null,"excerpt":"Disposable clone lifecycle canary source A.","hash":"approve-claim-canary-08c2358330-a","id":"ed9da5f1-846e-5298-8689-15027a7ce56d","source_type":"observation","storage_path":null,"url":null},{"created_by":null,"excerpt":"Disposable clone lifecycle canary source B.","hash":"approve-claim-canary-08c2358330-b","id":"a86f55dc-5058-585d-b492-a4f1d932670f","source_type":"observation","storage_path":null,"url":null}]}}'::jsonb + and reviewed_by_handle is not distinct from E'm3ta' + and reviewed_by_agent_id is not distinct from E'11111111-1111-4111-8111-111111111111'::uuid + and reviewed_at is not distinct from E'2026-07-15 01:59:03.105846+00'::timestamptz + and review_note is not distinct from E'Reviewed exact strict payload inside the disposable clone.' + and applied_by_handle is not distinct from E'kb-apply' + and applied_by_agent_id is not distinct from E'44444444-4444-4444-4444-444444444444'::uuid + and applied_at is not distinct from E'2026-07-15 01:59:04.449051+00'::timestamptz + ) then + raise exception 'proposal rollback: applied ledger does not match apply receipt'; + end if; + select jsonb_build_object( + 'proposal_id', proposal_id::text, + 'proposal_type', proposal_type, + 'payload', payload, + 'reviewed_by_handle', reviewed_by_handle, + 'reviewed_by_agent_id', reviewed_by_agent_id::text, + 'reviewed_by_db_role', reviewed_by_db_role::text, + 'reviewed_at', reviewed_at::text, + 'review_note', review_note + ) into approval_now + from kb_stage.kb_proposal_approvals + where proposal_id = E'9b2b5fa4-0e0d-5db0-aa4d-6fd102889946'::uuid; + if approval_now is distinct from E'{"payload":{"apply_payload":{"agent_id":null,"claims":[{"confidence":0.9,"created_by":null,"id":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","status":"open","superseded_by":null,"tags":["working-leo","clone-canary"],"text":"An approved strict proposal can create exact canonical rows atomically.","type":"structural"},{"confidence":0.85,"created_by":null,"id":"71a3331b-fb75-5e18-aa61-b256bc38af36","status":"open","superseded_by":null,"tags":["working-leo","row-proof"],"text":"Row-level proof is the authority for canonical KB state.","type":"concept"}],"contract_version":2,"edges":[{"created_by":null,"edge_type":"supports","from_claim":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","to_claim":"71a3331b-fb75-5e18-aa61-b256bc38af36","weight":0.75}],"evidence":[{"claim_id":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","created_by":null,"role":"grounds","source_id":"ed9da5f1-846e-5298-8689-15027a7ce56d","weight":0.9},{"claim_id":"71a3331b-fb75-5e18-aa61-b256bc38af36","created_by":null,"role":"illustrates","source_id":"a86f55dc-5058-585d-b492-a4f1d932670f","weight":0.8}],"reasoning_tools":[{"agent_id":null,"category":"verification","description":"Verify approved proposal to canonical graph lifecycle in a disposable clone.","id":"2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0","name":"Canonical row proof canary"}],"sources":[{"created_by":null,"excerpt":"Disposable clone lifecycle canary source A.","hash":"approve-claim-canary-08c2358330-a","id":"ed9da5f1-846e-5298-8689-15027a7ce56d","source_type":"observation","storage_path":null,"url":null},{"created_by":null,"excerpt":"Disposable clone lifecycle canary source B.","hash":"approve-claim-canary-08c2358330-b","id":"a86f55dc-5058-585d-b492-a4f1d932670f","source_type":"observation","storage_path":null,"url":null}]}},"proposal_id":"9b2b5fa4-0e0d-5db0-aa4d-6fd102889946","proposal_type":"approve_claim","review_note":"Reviewed exact strict payload inside the disposable clone.","reviewed_at":"2026-07-15 01:59:03.105846+00","reviewed_by_agent_id":"11111111-1111-4111-8111-111111111111","reviewed_by_db_role":"kb_review","reviewed_by_handle":"m3ta"}'::jsonb then + raise exception 'proposal rollback: immutable approval snapshot drifted'; + end if; + if (select count(*) from public.claim_evidence t + where t.id = E'dac3934f-b7ad-5205-8efe-a00163370a54'::uuid and to_jsonb(t) = E'{"claim_id":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","created_at":"2026-07-15T01:59:04.44165+00:00","created_by":null,"id":"dac3934f-b7ad-5205-8efe-a00163370a54","role":"grounds","source_id":"ed9da5f1-846e-5298-8689-15027a7ce56d","weight":0.9}'::jsonb) <> 1 then + raise exception 'proposal rollback drift: claim_evidence[0] row does not match apply receipt'; + end if; + if (select count(*) from public.claim_evidence t + where t.id = E'13e580c5-458d-5146-aef3-8c74ffc43b7c'::uuid and to_jsonb(t) = E'{"claim_id":"71a3331b-fb75-5e18-aa61-b256bc38af36","created_at":"2026-07-15T01:59:04.44165+00:00","created_by":null,"id":"13e580c5-458d-5146-aef3-8c74ffc43b7c","role":"illustrates","source_id":"a86f55dc-5058-585d-b492-a4f1d932670f","weight":0.8}'::jsonb) <> 1 then + raise exception 'proposal rollback drift: claim_evidence[1] row does not match apply receipt'; + end if; + if (select count(*) from public.claim_edges t + where t.id = E'b28d8770-9ac9-5b5f-bfe2-7280d7422a21'::uuid and to_jsonb(t) = E'{"created_at":"2026-07-15T01:59:04.44165+00:00","created_by":null,"edge_type":"supports","from_claim":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","id":"b28d8770-9ac9-5b5f-bfe2-7280d7422a21","to_claim":"71a3331b-fb75-5e18-aa61-b256bc38af36","weight":0.75}'::jsonb) <> 1 then + raise exception 'proposal rollback drift: claim_edges[0] row does not match apply receipt'; + end if; + if (select count(*) from public.reasoning_tools t + where t.id = E'2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0'::uuid and to_jsonb(t) = E'{"agent_id":null,"category":"verification","created_at":"2026-07-15T01:59:04.44165+00:00","description":"Verify approved proposal to canonical graph lifecycle in a disposable clone.","id":"2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0","name":"Canonical row proof canary"}'::jsonb) <> 1 then + raise exception 'proposal rollback drift: reasoning_tools[0] row does not match apply receipt'; + end if; + if (select count(*) from public.sources t + where t.id = E'ed9da5f1-846e-5298-8689-15027a7ce56d'::uuid and to_jsonb(t) = E'{"captured_at":null,"created_at":"2026-07-15T01:59:04.44165+00:00","created_by":null,"excerpt":"Disposable clone lifecycle canary source A.","hash":"approve-claim-canary-08c2358330-a","id":"ed9da5f1-846e-5298-8689-15027a7ce56d","source_type":"observation","storage_path":null,"url":null}'::jsonb) <> 1 then + raise exception 'proposal rollback drift: sources[0] row does not match apply receipt'; + end if; + if (select count(*) from public.sources t + where t.id = E'a86f55dc-5058-585d-b492-a4f1d932670f'::uuid and to_jsonb(t) = E'{"captured_at":null,"created_at":"2026-07-15T01:59:04.44165+00:00","created_by":null,"excerpt":"Disposable clone lifecycle canary source B.","hash":"approve-claim-canary-08c2358330-b","id":"a86f55dc-5058-585d-b492-a4f1d932670f","source_type":"observation","storage_path":null,"url":null}'::jsonb) <> 1 then + raise exception 'proposal rollback drift: sources[1] row does not match apply receipt'; + end if; + if (select count(*) from public.claims t + where t.id = E'71a3331b-fb75-5e18-aa61-b256bc38af36'::uuid and to_jsonb(t) = E'{"confidence":0.85,"created_at":"2026-07-15T01:59:04.44165+00:00","created_by":null,"id":"71a3331b-fb75-5e18-aa61-b256bc38af36","status":"open","superseded_by":null,"tags":["working-leo","row-proof"],"text":"Row-level proof is the authority for canonical KB state.","type":"concept","updated_at":"2026-07-15T01:59:04.44165+00:00"}'::jsonb) <> 1 then + raise exception 'proposal rollback drift: claims[0] row does not match apply receipt'; + end if; + if (select count(*) from public.claims t + where t.id = E'38dead14-6b1d-5909-99c4-3c45ad2dd21f'::uuid and to_jsonb(t) = E'{"confidence":0.9,"created_at":"2026-07-15T01:59:04.44165+00:00","created_by":null,"id":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","status":"open","superseded_by":null,"tags":["working-leo","clone-canary"],"text":"An approved strict proposal can create exact canonical rows atomically.","type":"structural","updated_at":"2026-07-15T01:59:04.44165+00:00"}'::jsonb) <> 1 then + raise exception 'proposal rollback drift: claims[1] row does not match apply receipt'; + end if; + delete from public.claim_evidence where id in (E'dac3934f-b7ad-5205-8efe-a00163370a54'::uuid, E'13e580c5-458d-5146-aef3-8c74ffc43b7c'::uuid); + get diagnostics affected = row_count; + if affected <> 2 then + raise exception 'proposal rollback count mismatch for claim_evidence: expected 2, got %', affected; + end if; + delete from public.claim_edges where id in (E'b28d8770-9ac9-5b5f-bfe2-7280d7422a21'::uuid); + get diagnostics affected = row_count; + if affected <> 1 then + raise exception 'proposal rollback count mismatch for claim_edges: expected 1, got %', affected; + end if; + delete from public.reasoning_tools where id in (E'2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0'::uuid); + get diagnostics affected = row_count; + if affected <> 1 then + raise exception 'proposal rollback count mismatch for reasoning_tools: expected 1, got %', affected; + end if; + delete from public.sources where id in (E'ed9da5f1-846e-5298-8689-15027a7ce56d'::uuid, E'a86f55dc-5058-585d-b492-a4f1d932670f'::uuid); + get diagnostics affected = row_count; + if affected <> 2 then + raise exception 'proposal rollback count mismatch for sources: expected 2, got %', affected; + end if; + delete from public.claims where id in (E'71a3331b-fb75-5e18-aa61-b256bc38af36'::uuid, E'38dead14-6b1d-5909-99c4-3c45ad2dd21f'::uuid); + get diagnostics affected = row_count; + if affected <> 2 then + raise exception 'proposal rollback count mismatch for claims: expected 2, got %', affected; + end if; + update kb_stage.kb_proposals + set status = 'approved', + applied_by_handle = null, + applied_by_agent_id = null, + applied_at = null, + updated_at = now() + where id = E'9b2b5fa4-0e0d-5db0-aa4d-6fd102889946'::uuid + and status = 'applied' + and payload = E'{"apply_payload":{"agent_id":null,"claims":[{"confidence":0.9,"created_by":null,"id":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","status":"open","superseded_by":null,"tags":["working-leo","clone-canary"],"text":"An approved strict proposal can create exact canonical rows atomically.","type":"structural"},{"confidence":0.85,"created_by":null,"id":"71a3331b-fb75-5e18-aa61-b256bc38af36","status":"open","superseded_by":null,"tags":["working-leo","row-proof"],"text":"Row-level proof is the authority for canonical KB state.","type":"concept"}],"contract_version":2,"edges":[{"created_by":null,"edge_type":"supports","from_claim":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","to_claim":"71a3331b-fb75-5e18-aa61-b256bc38af36","weight":0.75}],"evidence":[{"claim_id":"38dead14-6b1d-5909-99c4-3c45ad2dd21f","created_by":null,"role":"grounds","source_id":"ed9da5f1-846e-5298-8689-15027a7ce56d","weight":0.9},{"claim_id":"71a3331b-fb75-5e18-aa61-b256bc38af36","created_by":null,"role":"illustrates","source_id":"a86f55dc-5058-585d-b492-a4f1d932670f","weight":0.8}],"reasoning_tools":[{"agent_id":null,"category":"verification","description":"Verify approved proposal to canonical graph lifecycle in a disposable clone.","id":"2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0","name":"Canonical row proof canary"}],"sources":[{"created_by":null,"excerpt":"Disposable clone lifecycle canary source A.","hash":"approve-claim-canary-08c2358330-a","id":"ed9da5f1-846e-5298-8689-15027a7ce56d","source_type":"observation","storage_path":null,"url":null},{"created_by":null,"excerpt":"Disposable clone lifecycle canary source B.","hash":"approve-claim-canary-08c2358330-b","id":"a86f55dc-5058-585d-b492-a4f1d932670f","source_type":"observation","storage_path":null,"url":null}]}}'::jsonb + and applied_at is not distinct from E'2026-07-15 01:59:04.449051+00'::timestamptz; + get diagnostics affected = row_count; + if affected <> 1 then + raise exception 'proposal rollback: expected one applied ledger row, got %', affected; + end if; +end +$rollback$; +commit; diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-lifecycle-canary-current.json b/docs/reports/leo-working-state-20260709/proposal-apply-lifecycle-canary-current.json new file mode 100644 index 0000000..d494493 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/proposal-apply-lifecycle-canary-current.json @@ -0,0 +1,3324 @@ +{ + "applied_rollback_receipt": { + "applied_row_ids": { + "claim_edges": [ + "b28d8770-9ac9-5b5f-bfe2-7280d7422a21" + ], + "claim_evidence": [ + "13e580c5-458d-5146-aef3-8c74ffc43b7c", + "dac3934f-b7ad-5205-8efe-a00163370a54" + ], + "claims": [ + "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "71a3331b-fb75-5e18-aa61-b256bc38af36" + ], + "reasoning_tools": [ + "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0" + ], + "sources": [ + "a86f55dc-5058-585d-b492-a4f1d932670f", + "ed9da5f1-846e-5298-8689-15027a7ce56d" + ] + }, + "applied_row_sha256": { + "claim_edges": [ + "60d14cbc45d424fa1eb62db6478f185bf2c2d86559e57ffa77474e8d0b3c527c" + ], + "claim_evidence": [ + "e97de162c0730da69b357e1350f0da7e5843c4e67d498c12d731a0f04a8868d7", + "3054718195abe3f78e08cfcacd59902d41eb91c7153c6c63f9bcb3bc7c6902ac" + ], + "claims": [ + "e593b9cf7b0bc9267f3e48d8173129b84b09c028f290f1abc97ccb73940b31dc", + "24280b5159cbbf75e00eb24ae50ab69994397a583f347a241a5e262cf0c03179" + ], + "reasoning_tools": [ + "257924b236c3c27289b9ae2314687be0e96779415fc6ff87faada22893b757ba" + ], + "sources": [ + "d7a690c2b18701db5b48e8189b1621c8025c1ed75889a4d0a3a4c9761c03dc3b", + "bee048794799006bd00bf4bb02ae57998e670f5bcdfb140dff871dcb042d0198" + ] + }, + "apply_payload_sha256": "c89bc03ce7714f325293329a0bf95eaba0a56c596ef021d731b48dab0447c684", + "apply_receipt_sha256": "2aa65fe4509c6699af34e418de27c0225efe751223aaebf3f9b545fa159c754e", + "apply_sql_sha256": "1a7d65870e415ee8891b6dd84732a29bcee00412c10862d3d1db4b191a8ade38", + "approval_snapshot_sha256": "310b54030e26ed43d7226f2e803610586e80d21ff064c67e11ea21e93ef40bd9", + "artifact": "proposal_apply_lifecycle_receipt", + "checks": { + "apply_receipt_replay_ready": true, + "apply_rows_have_exact_ids": true, + "fresh_owned_targets": true, + "immutable_approval_unchanged": true, + "rollback_counts_restored": true, + "rollback_ledger_restored_to_approved": true, + "rollback_replay_refused_without_mutation": true, + "rollback_sql_exact_for_bound_rows": true, + "rollback_target_rows_absent": true, + "unrelated_rows_unchanged": true + }, + "current_tier": "T2_runtime", + "fresh_preflight_artifact_sha256": "1d5e7eed7355aa94ecfa213422e6178aecdeee70628994596e852bd7569d6a2d", + "fresh_preflight_sha256": "29a80dc9422af45fd067e98621442d31c60161f34deb5845aa410659a9c5de3a", + "generated_at_utc": "2026-07-15T01:59:05.526171+00:00", + "lifecycle_receipt_sha256": "8e74cf1f5c372633f9b6f15363ff759ba6ce9bf42df51288bb9be70ddabe49b3", + "pass": true, + "production_apply_authorization_present": false, + "production_apply_executed": false, + "proposal_before_apply": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "status": "approved" + }, + "proposal_payload_sha256": "6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af", + "required_tier": "T2_runtime", + "rollback": { + "counts_after_rollback": { + "kb_stage.kb_proposals": 1, + "public.claim_edges": 0, + "public.claim_evidence": 1, + "public.claims": 1, + "public.reasoning_tools": 0, + "public.sources": 1 + }, + "counts_before_apply": { + "kb_stage.kb_proposals": 1, + "public.claim_edges": 0, + "public.claim_evidence": 1, + "public.claims": 1, + "public.reasoning_tools": 0, + "public.sources": 1 + }, + "delete_order": [ + "claim_evidence", + "claim_edges", + "reasoning_tools", + "sources", + "claims" + ], + "proposal_after": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "status": "approved" + }, + "rollback_sql_sha256": "ef2464a6802426509333861cf0768ca64184901ab7bd2030529e7356a35d9cb3", + "schema": "livingip.proposalApplyRollback.v1", + "target_rows_after": { + "claim_edges": [], + "claim_evidence": [], + "claims": [], + "reasoning_tools": [], + "sources": [] + } + }, + "schema": "livingip.proposalApplyLifecycleReceipt.v1", + "strongest_claim_allowed": "deterministic disposable-clone apply and compensating rollback" + }, + "apply_transition": { + "applied_readback": { + "applied_at": "2026-07-15 01:59:04.449051+00", + "applied_by_agent_id": "44444444-4444-4444-4444-444444444444", + "applied_by_handle": "kb-apply", + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "applied" + }, + "approved_readback": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "approved" + }, + "exact_transition": true, + "immutable_approval_matches_applied_ledger": true, + "immutable_approval_readback": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + } + }, + "checks": { + "apply_transition_exact": true, + "claim_evidence_exact_after_permission_probe": true, + "clone_apply_table_deltas_exact": true, + "clone_cleanup_complete": true, + "clone_prerequisites_omit_cluster_role_ddl": true, + "compensating_rollback_receipt_passes": true, + "conflict_review_separate_and_exact": true, + "conflict_transaction_rolled_back_exactly": true, + "first_apply_succeeded": true, + "fresh_owned_preflight_exact": true, + "gateway_process_unchanged": null, + "idempotent_replay_refused": true, + "kb_apply_cannot_mutate_immutable_approval": true, + "kb_apply_cannot_mutate_proposal_approval": true, + "kb_apply_cannot_mutate_proposal_payload": true, + "kb_apply_cannot_update_existing_claim_evidence": true, + "kb_review_approval_succeeded": true, + "kb_review_used_security_definer": true, + "mutation_between_build_and_execute_refused": true, + "outer_container_absent_independent_readback": true, + "outer_container_has_no_docker_volumes": true, + "outer_container_lifecycle_labeled": true, + "outer_container_network_disabled": true, + "outer_isolation_complete": true, + "outer_labeled_container_absent_independent_readback": true, + "outer_live_counts_exactly_unchanged": true, + "outer_live_table_deltas_exactly_zero": true, + "outer_service_observed_if_required": true, + "outer_service_stable_if_observed": true, + "outer_source_tier_enforced": true, + "outer_workdir_absent_independent_readback": true, + "payload_agents_seeded_exactly": true, + "payload_projection_exact": true, + "proposal_exact_after_permission_probes": true, + "review_transition_exact": true, + "reviewer_agent_seeded_exactly": true, + "rollback_replay_refused_without_mutation": true, + "security_definer_dependencies_ready": true, + "source_binding_independently_verified": true, + "source_cluster_roles_preexisting": true, + "source_cluster_roles_unchanged": true, + "source_files_unchanged_during_run": true, + "source_hash_conflict_refused": true, + "source_table_deltas_exactly_zero": true, + "source_target_rows_unchanged": true, + "stale_ephemeral_paths_absent": true + }, + "cleanup_readback": { + "drop_attempted": true, + "leftover_clone_database_count": 0, + "returncode": 0, + "stderr": "", + "stdout": "pg_terminate_backend \n----------------------\n(0 rows)\n\nDROP DATABASE" + }, + "cleanup_returncode": 0, + "clone_apply_table_deltas": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 1, + "public.claim_evidence": 2, + "public.claims": 2, + "public.reasoning_tools": 1, + "public.sources": 2 + }, + "clone_counts_after_apply": { + "kb_stage.kb_proposals": 1, + "public.claim_edges": 1, + "public.claim_evidence": 3, + "public.claims": 3, + "public.reasoning_tools": 1, + "public.sources": 3 + }, + "clone_counts_before_apply": { + "kb_stage.kb_proposals": 1, + "public.claim_edges": 0, + "public.claim_evidence": 1, + "public.claims": 1, + "public.reasoning_tools": 0, + "public.sources": 1 + }, + "clone_created": true, + "clone_database": "teleo_approve_claim_isolated_08c2358330", + "clone_dropped": true, + "clone_prerequisite_sanitization": { + "clone_sql_sha256": "a02a03a704954058745d8cee4d25597bb8eaeee2ae211d4f9be4fe055f3d98ec", + "cluster_role_ddl_removed": true, + "removed_alter_role_statements": 3, + "removed_create_role_statements": 2, + "source_sha256": "bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa" + }, + "conflict_apply": { + "returncode": 1, + "source_hash_collision_refusal": true, + "stderr": "psql failed (3)\nSTDOUT:\n\n\nSTDERR:\nERROR: approve_claim: source hash collision for source 980ac481-1fa3-508f-b093-95de4ff4f8bc\nCONTEXT: PL/pgSQL function inline_code_block line 6 at RAISE" + }, + "conflict_readback": { + "canonical_rows": { + "claim_edges": [], + "claim_evidence": [], + "claims": [], + "reasoning_tools": [], + "sources": [] + }, + "canonical_rows_absent": true, + "immutable_approval": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.5, + "created_by": null, + "id": "3543c9a6-9439-5f56-9d1d-60268ea53a10", + "status": "open", + "superseded_by": null, + "tags": [ + "conflict-canary" + ], + "text": "This row must roll back with the conflicting source.", + "type": "structural" + } + ], + "contract_version": 2, + "edges": [], + "evidence": [], + "reasoning_tools": [], + "sources": [ + { + "created_by": null, + "excerpt": "Conflicting source id.", + "hash": "approve-claim-permission-probe-c4893cfb-8725-510e-ae01-ef01e8c99723", + "id": "980ac481-1fa3-508f-b093-95de4ff4f8bc", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "7cb1468f-861c-5a85-ae32-470d891af133", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:06.046049+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "immutable_approval_unchanged": true, + "proposal": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "7cb1468f-861c-5a85-ae32-470d891af133", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.5, + "created_by": null, + "id": "3543c9a6-9439-5f56-9d1d-60268ea53a10", + "status": "open", + "superseded_by": null, + "tags": [ + "conflict-canary" + ], + "text": "This row must roll back with the conflicting source.", + "type": "structural" + } + ], + "contract_version": 2, + "edges": [], + "evidence": [], + "reasoning_tools": [], + "sources": [ + { + "created_by": null, + "excerpt": "Conflicting source id.", + "hash": "approve-claim-permission-probe-c4893cfb-8725-510e-ae01-ef01e8c99723", + "id": "980ac481-1fa3-508f-b093-95de4ff4f8bc", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:06.046049+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-conflict-canary", + "status": "approved" + }, + "proposal_approval_unchanged": true + }, + "conflict_review_transition": { + "approved_readback": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "7cb1468f-861c-5a85-ae32-470d891af133", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.5, + "created_by": null, + "id": "3543c9a6-9439-5f56-9d1d-60268ea53a10", + "status": "open", + "superseded_by": null, + "tags": [ + "conflict-canary" + ], + "text": "This row must roll back with the conflicting source.", + "type": "structural" + } + ], + "contract_version": 2, + "edges": [], + "evidence": [], + "reasoning_tools": [], + "sources": [ + { + "created_by": null, + "excerpt": "Conflicting source id.", + "hash": "approve-claim-permission-probe-c4893cfb-8725-510e-ae01-ef01e8c99723", + "id": "980ac481-1fa3-508f-b093-95de4ff4f8bc", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:06.046049+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-conflict-canary", + "status": "approved" + }, + "exact_transition": true, + "immutable_approval_matches_ledger": true, + "immutable_approval_readback": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.5, + "created_by": null, + "id": "3543c9a6-9439-5f56-9d1d-60268ea53a10", + "status": "open", + "superseded_by": null, + "tags": [ + "conflict-canary" + ], + "text": "This row must roll back with the conflicting source.", + "type": "structural" + } + ], + "contract_version": 2, + "edges": [], + "evidence": [], + "reasoning_tools": [], + "sources": [ + { + "created_by": null, + "excerpt": "Conflicting source id.", + "hash": "approve-claim-permission-probe-c4893cfb-8725-510e-ae01-ef01e8c99723", + "id": "980ac481-1fa3-508f-b093-95de4ff4f8bc", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "7cb1468f-861c-5a85-ae32-470d891af133", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:06.046049+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "pending_immutable_approval": null, + "pending_readback": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "7cb1468f-861c-5a85-ae32-470d891af133", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.5, + "created_by": null, + "id": "3543c9a6-9439-5f56-9d1d-60268ea53a10", + "status": "open", + "superseded_by": null, + "tags": [ + "conflict-canary" + ], + "text": "This row must roll back with the conflicting source.", + "type": "structural" + } + ], + "contract_version": 2, + "edges": [], + "evidence": [], + "reasoning_tools": [], + "sources": [ + { + "created_by": null, + "excerpt": "Conflicting source id.", + "hash": "approve-claim-permission-probe-c4893cfb-8725-510e-ae01-ef01e8c99723", + "id": "980ac481-1fa3-508f-b093-95de4ff4f8bc", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": null, + "reviewed_at": null, + "reviewed_by_agent_id": null, + "reviewed_by_handle": null, + "source_ref": "working-leo:approve-claim-conflict-canary", + "status": "pending_review" + }, + "review_apply": { + "returncode": 0, + "stderr": "", + "stdout": "{\"id\": \"7cb1468f-861c-5a85-ae32-470d891af133\", \"proposal_type\": \"approve_claim\", \"review_note\": \"Reviewed exact strict payload inside the disposable clone.\", \"reviewed_at\": \"2026-07-15 01:59:06.046049+00\", \"reviewed_by_agent_id\": \"11111111-1111-4111-8111-111111111111\", \"reviewed_by_db_role\": \"kb_review\", \"reviewed_by_handle\": \"m3ta\", \"status\": \"approved\"}" + } + }, + "current_tier": "T2_runtime", + "dependencies": { + "apply_role": "kb_apply", + "apply_script": "scripts/apply_proposal.py", + "approve_script": "scripts/approve_proposal.py", + "credential_files": { + "apply": { + "present_at_start": true, + "runtime_location": "ephemeral_or_operator_managed" + }, + "review": { + "present_at_start": true, + "runtime_location": "ephemeral_or_operator_managed" + } + }, + "gate_owner_role": "kb_gate_owner", + "immutable_approval_table": "kb_stage.kb_proposal_approvals", + "prerequisites_sql": "scripts/kb_apply_prereqs.sql", + "review_role": "kb_review", + "reviewer_handle": "m3ta", + "security_definer_argument_types": { + "kb_stage.approve_strict_proposal": "uuid, text, jsonb, text, text", + "kb_stage.assert_approved_proposal": "uuid, text, jsonb, text, uuid, timestamp with time zone, text", + "kb_stage.finish_approved_proposal": "uuid, text, jsonb, text, uuid, timestamp with time zone, text, text" + }, + "security_definer_functions": [ + "kb_stage.approve_strict_proposal", + "kb_stage.assert_approved_proposal", + "kb_stage.finish_approved_proposal" + ] + }, + "external_claim_seed": { + "clone_rows": [], + "exact_clone_copy": true, + "required_ids": [], + "source_rows": [] + }, + "first_apply": { + "result": { + "applied": true, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "replay_material_sha256": "767fe35fc91b4cd9b82287e828d6a6a86521bd82f23b651e9a91db977c55af59", + "replay_ready": true + }, + "returncode": 0, + "stderr": "" + }, + "fixture_ids": { + "conflict_claim_id": "3543c9a6-9439-5f56-9d1d-60268ea53a10", + "conflict_proposal_id": "7cb1468f-861c-5a85-ae32-470d891af133", + "conflict_source_id": "980ac481-1fa3-508f-b093-95de4ff4f8bc", + "permission_claim_id": "199b9d5e-904a-52cf-9f2c-3ee97f3f54b4", + "permission_source_id": "c4893cfb-8725-510e-ae01-ef01e8c99723", + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946" + }, + "fresh_owned_preflight": { + "apply_payload_sha256": "c89bc03ce7714f325293329a0bf95eaba0a56c596ef021d731b48dab0447c684", + "artifact": "proposal_apply_fresh_preflight", + "captured_at_utc": "2026-07-15T01:59:04.091167+00:00", + "fresh_owned_targets": true, + "preflight_artifact_sha256": "1d5e7eed7355aa94ecfa213422e6178aecdeee70628994596e852bd7569d6a2d", + "preflight_sha256": "29a80dc9422af45fd067e98621442d31c60161f34deb5845aa410659a9c5de3a", + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_payload_sha256": "6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af", + "proposal_type": "approve_claim", + "schema": "livingip.proposalApplyFreshPreflight.v1", + "target_rows_before": { + "claim_edges": [], + "claim_evidence": [], + "claims": [], + "reasoning_tools": [], + "sources": [] + } + }, + "gateway_process_observable": false, + "generated_at_utc": "2026-07-15T01:59:02.013207+00:00", + "idempotent_replay": { + "already_applied_refusal": true, + "returncode": 1, + "stderr": "proposal 9b2b5fa4-0e0d-5db0-aa4d-6fd102889946 is already applied (idempotent no-op)" + }, + "kb_apply_claim_evidence_update_probe": { + "after": { + "claim_id": "199b9d5e-904a-52cf-9f2c-3ee97f3f54b4", + "created_by": null, + "role": "grounds", + "source_id": "c4893cfb-8725-510e-ae01-ef01e8c99723", + "weight": 0.42 + }, + "attempt": { + "refused": true, + "returncode": 3, + "stderr": "ERROR: permission denied for table claim_evidence", + "stdout": "" + }, + "before": { + "claim_id": "199b9d5e-904a-52cf-9f2c-3ee97f3f54b4", + "created_by": null, + "role": "grounds", + "source_id": "c4893cfb-8725-510e-ae01-ef01e8c99723", + "weight": 0.42 + }, + "exact_row_unchanged": true + }, + "kb_apply_proposal_mutation_probes": { + "after": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "approved" + }, + "approval_fields": { + "refused": true, + "returncode": 3, + "stderr": "ERROR: permission denied for table kb_proposals", + "stdout": "" + }, + "before": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "approved" + }, + "exact_row_unchanged": true, + "immutable_approval_after": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "immutable_approval_before": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "immutable_approval_exactly_unchanged": true, + "immutable_approval_payload": { + "refused": true, + "returncode": 3, + "stderr": "ERROR: permission denied for table kb_proposal_approvals", + "stdout": "" + }, + "payload": { + "refused": true, + "returncode": 3, + "stderr": "ERROR: permission denied for table kb_proposals", + "stdout": "" + } + }, + "leftover_clone_count": 0, + "mutation_between_build_and_execute": { + "approval_restored_exactly": true, + "approved_before_mutation": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "approved" + }, + "build_returncode": 0, + "built_sql_sha256": "19f5dd6c27f293bfdc89adee59ffa99beb4503dba5aaa6291c9808ff16769ee7", + "built_sql_uses_assert_approved_proposal": true, + "built_sql_uses_finish_approved_proposal": true, + "canonical_rows_unchanged": true, + "immutable_approval_after_ledger_mutation": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "immutable_approval_after_stale_execute": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "immutable_approval_before_mutation": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "immutable_approval_never_changed": true, + "mutated_readback": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + }, + "mutation_after_build": true + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "approved" + }, + "proposal_after_stale_execute": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + }, + "mutation_after_build": true + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "approved" + }, + "restored_immutable_approval": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "restored_readback": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "approved" + }, + "rows_after_stale_execute": { + "claim_edges": [], + "claim_evidence": [], + "claims": [], + "reasoning_tools": [], + "sources": [] + }, + "stale_apply_refused": true, + "stale_execute": { + "returncode": 3, + "stderr": "ERROR: assert_approved_proposal: proposal 9b2b5fa4-0e0d-5db0-aa4d-6fd102889946 is not the exact immutable approved snapshot\nCONTEXT: PL/pgSQL function kb_stage.assert_approved_proposal(uuid,text,jsonb,text,uuid,timestamp with time zone,text) line 22 at RAISE", + "stdout": "" + } + }, + "outer_isolation": { + "canonical_table_deltas": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 0, + "public.claim_evidence": 0, + "public.claims": 0, + "public.reasoning_tools": 0, + "public.sources": 0 + }, + "cleanup_readback": { + "container_name_query_returncode": 0, + "container_name_query_stdout": "", + "container_remove_returncode": 0, + "label_scoped_orphan_count": 0, + "label_scoped_query_returncode": 0, + "label_scoped_query_stdout": "", + "label_scoped_temporary_container_absent": true, + "temporary_container_absent": true, + "temporary_workdir_absent": true, + "workdir_lexists_after_cleanup": false, + "workdir_remove_returncode": 0 + }, + "container_isolation": { + "canary_label": "proposal-apply-lifecycle", + "instance_label": "working-leo-approve-claim-6715-44544", + "lifecycle_labeled": true, + "network_disabled": true, + "network_mode": "none", + "no_docker_volumes": true, + "volume_mounts": [] + }, + "service_after": { + "Available": "false", + "Reason": "systemctl_unavailable" + }, + "service_before": { + "Available": "false", + "Reason": "systemctl_unavailable" + }, + "service_gate_passes": true, + "service_observable": false, + "service_required": false, + "service_stable": null, + "source_boundary": { + "canary_label": "working-leo-approved-source", + "network_mode": "none", + "source_tier": "isolated-local", + "tier_enforced": true + }, + "source_container": "leo-proposal-lifecycle-source-20260715", + "source_counts_after": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 0, + "public.claim_evidence": 0, + "public.claims": 2, + "public.reasoning_tools": 0, + "public.sources": 0 + }, + "source_counts_before": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 0, + "public.claim_evidence": 0, + "public.claims": 2, + "public.reasoning_tools": 0, + "public.sources": 0 + }, + "source_counts_unchanged": true, + "source_database": "teleo" + }, + "payload_agent_seed": { + "clone_rows": [], + "exact_clone_copy": true, + "required_ids": [], + "source_rows": [] + }, + "payload_projection": { + "expected_rows": { + "claim_edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "claim_evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + }, + "expected_table_deltas": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 1, + "public.claim_evidence": 2, + "public.claims": 2, + "public.reasoning_tools": 1, + "public.sources": 2 + }, + "payload_sha256": "c89bc03ce7714f325293329a0bf95eaba0a56c596ef021d731b48dab0447c684" + }, + "post_run_cleanup": { + "gateway_active_state": null, + "gateway_main_pid": null, + "gateway_process_observable": false, + "gateway_restart_count": null, + "gateway_sub_state": null, + "leftover_clone_database_count": 0 + }, + "required_tier": "T2_runtime", + "required_tiers": [ + "T2_runtime" + ], + "review_transition": { + "approved_readback": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "approved" + }, + "exact_transition": true, + "immutable_approval_matches_ledger": true, + "immutable_approval_readback": { + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_db_role": "kb_review", + "reviewed_by_handle": "m3ta" + }, + "pending_immutable_approval": null, + "pending_readback": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "channel": "clone_canary", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "rationale": "Exercise strict canonical bundle review and apply lifecycle.", + "review_note": null, + "reviewed_at": null, + "reviewed_by_agent_id": null, + "reviewed_by_handle": null, + "source_ref": "working-leo:approve-claim-clone-canary", + "status": "pending_review" + }, + "review_apply": { + "returncode": 0, + "stderr": "", + "stdout": "{\"id\": \"9b2b5fa4-0e0d-5db0-aa4d-6fd102889946\", \"proposal_type\": \"approve_claim\", \"review_note\": \"Reviewed exact strict payload inside the disposable clone.\", \"reviewed_at\": \"2026-07-15 01:59:03.105846+00\", \"reviewed_by_agent_id\": \"11111111-1111-4111-8111-111111111111\", \"reviewed_by_db_role\": \"kb_review\", \"reviewed_by_handle\": \"m3ta\", \"status\": \"approved\"}" + }, + "review_dry_run": { + "returncode": 0, + "stderr": "", + "uses_approve_strict_proposal": true + } + }, + "reviewer_agent_seed": { + "clone_row": { + "handle": "m3ta", + "id": "11111111-1111-4111-8111-111111111111", + "kind": "human" + }, + "exact_clone_copy": true, + "reviewer_handle": "m3ta", + "source_row": { + "handle": "m3ta", + "id": "11111111-1111-4111-8111-111111111111", + "kind": "human" + } + }, + "rollback_plan": { + "execution_authority": "disposable_clone_admin_only", + "rollback_sql_sha256": "ef2464a6802426509333861cf0768ca64184901ab7bd2030529e7356a35d9cb3", + "schema": "livingip.proposalApplyRollback.v1" + }, + "rollback_replay": { + "refused": true, + "returncode": 3, + "state_unchanged": true, + "stderr": "ERROR: proposal rollback: applied ledger does not match apply receipt\nCONTEXT: PL/pgSQL function inline_code_block line 26 at RAISE" + }, + "row_readback": { + "claim_edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "claim_evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + }, + "runtime_scope": "isolated_postgres_container_from_readonly_local_fixture", + "schema": "working-leo.approve-claim-clone-canary.v3", + "security_definer_readback": { + "functions": [ + { + "argument_types": "uuid, text, jsonb, text, text", + "expected_role": "kb_review", + "expected_role_execute": true, + "function": "kb_stage.approve_strict_proposal", + "identity_arguments": "p_proposal_id uuid, p_expected_proposal_type text, p_expected_payload jsonb, p_expected_reviewed_by_handle text, p_review_note text", + "other_runtime_role_execute": false, + "owner": "kb_gate_owner", + "present": true, + "public_execute": false, + "security_definer": true + }, + { + "argument_types": "uuid, text, jsonb, text, uuid, timestamp with time zone, text", + "expected_role": "kb_apply", + "expected_role_execute": true, + "function": "kb_stage.assert_approved_proposal", + "identity_arguments": "p_proposal_id uuid, p_proposal_type text, p_payload jsonb, p_reviewed_by_handle text, p_reviewed_by_agent_id uuid, p_reviewed_at timestamp with time zone, p_review_note text", + "other_runtime_role_execute": false, + "owner": "kb_gate_owner", + "present": true, + "public_execute": false, + "security_definer": true + }, + { + "argument_types": "uuid, text, jsonb, text, uuid, timestamp with time zone, text, text", + "expected_role": "kb_apply", + "expected_role_execute": true, + "function": "kb_stage.finish_approved_proposal", + "identity_arguments": "p_proposal_id uuid, p_proposal_type text, p_payload jsonb, p_reviewed_by_handle text, p_reviewed_by_agent_id uuid, p_reviewed_at timestamp with time zone, p_review_note text, p_applied_by_handle text", + "other_runtime_role_execute": false, + "owner": "kb_gate_owner", + "present": true, + "public_execute": false, + "security_definer": true + } + ], + "login_role_owned_objects": [], + "protected_role_memberships": [], + "role_privileges": { + "kb_apply_approval_snapshot_select": false, + "kb_apply_approval_snapshot_update": false, + "kb_apply_claim_evidence_insert": true, + "kb_apply_claim_evidence_update": false, + "kb_apply_proposal_select": true, + "kb_apply_proposal_update": false, + "kb_review_approval_snapshot_select": false, + "kb_review_approval_snapshot_update": false, + "kb_review_proposal_select": true, + "kb_review_proposal_update": false + }, + "unexpected_overloads": [] + }, + "service_after": { + "available": false, + "reason": "systemctl_unavailable", + "returncode": 127 + }, + "service_before": { + "available": false, + "reason": "systemctl_unavailable", + "returncode": 127 + }, + "service_restarted": null, + "source_binding": { + "files": [ + { + "repo_relative_path": "scripts/apply_proposal.py", + "sha256": "ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26", + "size_bytes": 47270 + }, + { + "repo_relative_path": "scripts/approve_proposal.py", + "sha256": "e991498264bd0f49755dfef809212fa7151a668ce479b100d830e447775a9fe0", + "size_bytes": 7323 + }, + { + "repo_relative_path": "scripts/finalize_approve_claim_isolated_canary.py", + "sha256": "d5dbd250a9fc28a22ab0880a57c330b3c95119b95d96c5df95cbaf49648bccb5", + "size_bytes": 10684 + }, + { + "repo_relative_path": "scripts/kb_apply_prereqs.sql", + "sha256": "bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa", + "size_bytes": 42602 + }, + { + "repo_relative_path": "scripts/proposal_apply_lifecycle.py", + "sha256": "a643b83b55c9e76f545ebafb9582d6d07ff432600c1b66004cc7440d7ac1963b", + "size_bytes": 71314 + }, + { + "repo_relative_path": "scripts/run_approve_claim_clone_canary.py", + "sha256": "bd7ae232b20ff77f15560d7bd07b929f419fb615425c9970fb00afc8080d86ef", + "size_bytes": 88340 + }, + { + "repo_relative_path": "scripts/run_approve_claim_isolated_container_canary.sh", + "sha256": "ed81a8f8c05d516809bb54c80d82786ed941dc1cf87150f25addc935685c72bb", + "size_bytes": 9239 + } + ], + "independent_post_cleanup_verification": { + "all_match": true, + "files": [ + { + "actual_sha256": "ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26", + "expected_sha256": "ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26", + "matches": true, + "repo_relative_path": "scripts/apply_proposal.py", + "size_bytes": 47270 + }, + { + "actual_sha256": "e991498264bd0f49755dfef809212fa7151a668ce479b100d830e447775a9fe0", + "expected_sha256": "e991498264bd0f49755dfef809212fa7151a668ce479b100d830e447775a9fe0", + "matches": true, + "repo_relative_path": "scripts/approve_proposal.py", + "size_bytes": 7323 + }, + { + "actual_sha256": "d5dbd250a9fc28a22ab0880a57c330b3c95119b95d96c5df95cbaf49648bccb5", + "expected_sha256": "d5dbd250a9fc28a22ab0880a57c330b3c95119b95d96c5df95cbaf49648bccb5", + "matches": true, + "repo_relative_path": "scripts/finalize_approve_claim_isolated_canary.py", + "size_bytes": 10684 + }, + { + "actual_sha256": "bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa", + "expected_sha256": "bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa", + "matches": true, + "repo_relative_path": "scripts/kb_apply_prereqs.sql", + "size_bytes": 42602 + }, + { + "actual_sha256": "a643b83b55c9e76f545ebafb9582d6d07ff432600c1b66004cc7440d7ac1963b", + "expected_sha256": "a643b83b55c9e76f545ebafb9582d6d07ff432600c1b66004cc7440d7ac1963b", + "matches": true, + "repo_relative_path": "scripts/proposal_apply_lifecycle.py", + "size_bytes": 71314 + }, + { + "actual_sha256": "bd7ae232b20ff77f15560d7bd07b929f419fb615425c9970fb00afc8080d86ef", + "expected_sha256": "bd7ae232b20ff77f15560d7bd07b929f419fb615425c9970fb00afc8080d86ef", + "matches": true, + "repo_relative_path": "scripts/run_approve_claim_clone_canary.py", + "size_bytes": 88340 + }, + { + "actual_sha256": "ed81a8f8c05d516809bb54c80d82786ed941dc1cf87150f25addc935685c72bb", + "expected_sha256": "ed81a8f8c05d516809bb54c80d82786ed941dc1cf87150f25addc935685c72bb", + "matches": true, + "repo_relative_path": "scripts/run_approve_claim_isolated_container_canary.sh", + "size_bytes": 9239 + } + ] + }, + "post_run_files": [ + { + "repo_relative_path": "scripts/apply_proposal.py", + "sha256": "ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26", + "size_bytes": 47270 + }, + { + "repo_relative_path": "scripts/approve_proposal.py", + "sha256": "e991498264bd0f49755dfef809212fa7151a668ce479b100d830e447775a9fe0", + "size_bytes": 7323 + }, + { + "repo_relative_path": "scripts/finalize_approve_claim_isolated_canary.py", + "sha256": "d5dbd250a9fc28a22ab0880a57c330b3c95119b95d96c5df95cbaf49648bccb5", + "size_bytes": 10684 + }, + { + "repo_relative_path": "scripts/kb_apply_prereqs.sql", + "sha256": "bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa", + "size_bytes": 42602 + }, + { + "repo_relative_path": "scripts/proposal_apply_lifecycle.py", + "sha256": "a643b83b55c9e76f545ebafb9582d6d07ff432600c1b66004cc7440d7ac1963b", + "size_bytes": 71314 + }, + { + "repo_relative_path": "scripts/run_approve_claim_clone_canary.py", + "sha256": "bd7ae232b20ff77f15560d7bd07b929f419fb615425c9970fb00afc8080d86ef", + "size_bytes": 88340 + }, + { + "repo_relative_path": "scripts/run_approve_claim_isolated_container_canary.sh", + "sha256": "ed81a8f8c05d516809bb54c80d82786ed941dc1cf87150f25addc935685c72bb", + "size_bytes": 9239 + } + ], + "unchanged_during_run": true + }, + "source_cluster_roles_after": [ + { + "rolbypassrls": false, + "rolcanlogin": true, + "rolconnlimit": -1, + "rolcreatedb": false, + "rolcreaterole": false, + "rolinherit": false, + "rolname": "kb_apply", + "rolreplication": false, + "rolsuper": false, + "rolvaliduntil": null + }, + { + "rolbypassrls": false, + "rolcanlogin": false, + "rolconnlimit": -1, + "rolcreatedb": false, + "rolcreaterole": false, + "rolinherit": false, + "rolname": "kb_gate_owner", + "rolreplication": false, + "rolsuper": false, + "rolvaliduntil": null + }, + { + "rolbypassrls": false, + "rolcanlogin": true, + "rolconnlimit": -1, + "rolcreatedb": false, + "rolcreaterole": false, + "rolinherit": false, + "rolname": "kb_review", + "rolreplication": false, + "rolsuper": false, + "rolvaliduntil": null + } + ], + "source_cluster_roles_before": [ + { + "rolbypassrls": false, + "rolcanlogin": true, + "rolconnlimit": -1, + "rolcreatedb": false, + "rolcreaterole": false, + "rolinherit": false, + "rolname": "kb_apply", + "rolreplication": false, + "rolsuper": false, + "rolvaliduntil": null + }, + { + "rolbypassrls": false, + "rolcanlogin": false, + "rolconnlimit": -1, + "rolcreatedb": false, + "rolcreaterole": false, + "rolinherit": false, + "rolname": "kb_gate_owner", + "rolreplication": false, + "rolsuper": false, + "rolvaliduntil": null + }, + { + "rolbypassrls": false, + "rolcanlogin": true, + "rolconnlimit": -1, + "rolcreatedb": false, + "rolcreaterole": false, + "rolinherit": false, + "rolname": "kb_review", + "rolreplication": false, + "rolsuper": false, + "rolvaliduntil": null + } + ], + "source_counts_after": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 0, + "public.claim_evidence": 0, + "public.claims": 2, + "public.reasoning_tools": 0, + "public.sources": 0 + }, + "source_counts_before": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 0, + "public.claim_evidence": 0, + "public.claims": 2, + "public.reasoning_tools": 0, + "public.sources": 0 + }, + "source_counts_observed_unchanged": true, + "source_database": "teleo", + "source_database_mutated": false, + "source_table_deltas": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 0, + "public.claim_evidence": 0, + "public.claims": 0, + "public.reasoning_tools": 0, + "public.sources": 0 + }, + "source_target_rows_after": { + "claim_edges": [], + "claim_evidence": [], + "claims": [], + "reasoning_tools": [], + "sources": [] + }, + "source_target_rows_before": { + "claim_edges": [], + "claim_evidence": [], + "claims": [], + "reasoning_tools": [], + "sources": [] + }, + "status": "pass", + "strict_receipt": { + "canonical_row_keys": { + "actual": { + "public.claim_edges": [ + { + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36" + } + ], + "public.claim_evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d" + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f" + } + ], + "public.claims": [ + "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "71a3331b-fb75-5e18-aa61-b256bc38af36" + ], + "public.reasoning_tools": [ + "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0" + ], + "public.sources": [ + "a86f55dc-5058-585d-b492-a4f1d932670f", + "ed9da5f1-846e-5298-8689-15027a7ce56d" + ] + }, + "expected": { + "public.claim_edges": [ + { + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36" + } + ], + "public.claim_evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d" + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f" + } + ], + "public.claims": [ + "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "71a3331b-fb75-5e18-aa61-b256bc38af36" + ], + "public.reasoning_tools": [ + "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0" + ], + "public.sources": [ + "a86f55dc-5058-585d-b492-a4f1d932670f", + "ed9da5f1-846e-5298-8689-15027a7ce56d" + ] + } + }, + "checks": { + "before_after_counts_present": true, + "canonical_row_keys_exact": true, + "canonical_rows_exact": true, + "clone_database_removed": true, + "conflict_transaction_rolled_back": true, + "proposal_applied_at_present": true, + "proposal_id_exact": true, + "proposal_status_applied": true, + "source_database_unchanged": true, + "table_deltas_exact": true + }, + "pass": true, + "proposal": { + "applied_at": "2026-07-15 01:59:04.449051+00", + "applied_by_handle": "kb-apply", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "status": "applied" + }, + "rollback_cleanup": { + "clone_database_removed": true, + "conflict_transaction_rolled_back": true, + "leftover_clone_database_count": 0, + "source_database_unchanged": true + }, + "row_counts": { + "actual_deltas": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 1, + "public.claim_evidence": 2, + "public.claims": 2, + "public.reasoning_tools": 1, + "public.sources": 2 + }, + "after_apply": { + "kb_stage.kb_proposals": 1, + "public.claim_edges": 1, + "public.claim_evidence": 3, + "public.claims": 3, + "public.reasoning_tools": 1, + "public.sources": 3 + }, + "before_apply": { + "kb_stage.kb_proposals": 1, + "public.claim_edges": 0, + "public.claim_evidence": 1, + "public.claims": 1, + "public.reasoning_tools": 0, + "public.sources": 1 + }, + "expected_deltas": { + "kb_stage.kb_proposals": 0, + "public.claim_edges": 1, + "public.claim_evidence": 2, + "public.claims": 2, + "public.reasoning_tools": 1, + "public.sources": 2 + } + }, + "schema": "working-leo.approved-change-strict-receipt.v1" + }, + "target_rows_before_apply": { + "claim_edges": [], + "claim_evidence": [], + "claims": [], + "reasoning_tools": [], + "sources": [] + }, + "telegram_messages_sent": false, + "unrelated_fingerprint_before_apply": { + "kb_stage.kb_proposal_approvals": { + "content_md5": "d41d8cd98f00b204e9800998ecf8427e", + "row_count": 0 + }, + "kb_stage.kb_proposals": { + "content_md5": "d41d8cd98f00b204e9800998ecf8427e", + "row_count": 0 + }, + "public.agents": { + "content_md5": "56bbdd947ace3bd545744a2995795702", + "row_count": 2 + }, + "public.claim_edges": { + "content_md5": "d41d8cd98f00b204e9800998ecf8427e", + "row_count": 0 + }, + "public.claim_evidence": { + "content_md5": "bc66b511bcc4926bb9065f98b41ce8ae", + "row_count": 1 + }, + "public.claims": { + "content_md5": "5cfbe5bd4ed3dc24adab29cb39f0b594", + "row_count": 1 + }, + "public.reasoning_tools": { + "content_md5": "d41d8cd98f00b204e9800998ecf8427e", + "row_count": 0 + }, + "public.sources": { + "content_md5": "592f899d41dfeeecdb449402eb97ebd3", + "row_count": 1 + } + } +} diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-lifecycle-receipt-current.json b/docs/reports/leo-working-state-20260709/proposal-apply-lifecycle-receipt-current.json new file mode 100644 index 0000000..e5a2a3d --- /dev/null +++ b/docs/reports/leo-working-state-20260709/proposal-apply-lifecycle-receipt-current.json @@ -0,0 +1,217 @@ +{ + "applied_row_ids": { + "claim_edges": [ + "b28d8770-9ac9-5b5f-bfe2-7280d7422a21" + ], + "claim_evidence": [ + "13e580c5-458d-5146-aef3-8c74ffc43b7c", + "dac3934f-b7ad-5205-8efe-a00163370a54" + ], + "claims": [ + "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "71a3331b-fb75-5e18-aa61-b256bc38af36" + ], + "reasoning_tools": [ + "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0" + ], + "sources": [ + "a86f55dc-5058-585d-b492-a4f1d932670f", + "ed9da5f1-846e-5298-8689-15027a7ce56d" + ] + }, + "applied_row_sha256": { + "claim_edges": [ + "60d14cbc45d424fa1eb62db6478f185bf2c2d86559e57ffa77474e8d0b3c527c" + ], + "claim_evidence": [ + "e97de162c0730da69b357e1350f0da7e5843c4e67d498c12d731a0f04a8868d7", + "3054718195abe3f78e08cfcacd59902d41eb91c7153c6c63f9bcb3bc7c6902ac" + ], + "claims": [ + "e593b9cf7b0bc9267f3e48d8173129b84b09c028f290f1abc97ccb73940b31dc", + "24280b5159cbbf75e00eb24ae50ab69994397a583f347a241a5e262cf0c03179" + ], + "reasoning_tools": [ + "257924b236c3c27289b9ae2314687be0e96779415fc6ff87faada22893b757ba" + ], + "sources": [ + "d7a690c2b18701db5b48e8189b1621c8025c1ed75889a4d0a3a4c9761c03dc3b", + "bee048794799006bd00bf4bb02ae57998e670f5bcdfb140dff871dcb042d0198" + ] + }, + "apply_payload_sha256": "c89bc03ce7714f325293329a0bf95eaba0a56c596ef021d731b48dab0447c684", + "apply_receipt_sha256": "2aa65fe4509c6699af34e418de27c0225efe751223aaebf3f9b545fa159c754e", + "apply_sql_sha256": "1a7d65870e415ee8891b6dd84732a29bcee00412c10862d3d1db4b191a8ade38", + "approval_snapshot_sha256": "310b54030e26ed43d7226f2e803610586e80d21ff064c67e11ea21e93ef40bd9", + "artifact": "proposal_apply_lifecycle_receipt", + "checks": { + "apply_receipt_replay_ready": true, + "apply_rows_have_exact_ids": true, + "fresh_owned_targets": true, + "immutable_approval_unchanged": true, + "rollback_counts_restored": true, + "rollback_ledger_restored_to_approved": true, + "rollback_replay_refused_without_mutation": true, + "rollback_sql_exact_for_bound_rows": true, + "rollback_target_rows_absent": true, + "unrelated_rows_unchanged": true + }, + "current_tier": "T2_runtime", + "fresh_preflight_artifact_sha256": "1d5e7eed7355aa94ecfa213422e6178aecdeee70628994596e852bd7569d6a2d", + "fresh_preflight_sha256": "29a80dc9422af45fd067e98621442d31c60161f34deb5845aa410659a9c5de3a", + "generated_at_utc": "2026-07-15T01:59:05.526171+00:00", + "lifecycle_receipt_sha256": "8e74cf1f5c372633f9b6f15363ff759ba6ce9bf42df51288bb9be70ddabe49b3", + "pass": true, + "production_apply_authorization_present": false, + "production_apply_executed": false, + "proposal_before_apply": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "status": "approved" + }, + "proposal_payload_sha256": "6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af", + "required_tier": "T2_runtime", + "rollback": { + "counts_after_rollback": { + "kb_stage.kb_proposals": 1, + "public.claim_edges": 0, + "public.claim_evidence": 1, + "public.claims": 1, + "public.reasoning_tools": 0, + "public.sources": 1 + }, + "counts_before_apply": { + "kb_stage.kb_proposals": 1, + "public.claim_edges": 0, + "public.claim_evidence": 1, + "public.claims": 1, + "public.reasoning_tools": 0, + "public.sources": 1 + }, + "delete_order": [ + "claim_evidence", + "claim_edges", + "reasoning_tools", + "sources", + "claims" + ], + "proposal_after": { + "applied_at": null, + "applied_by_agent_id": null, + "applied_by_handle": null, + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "proposal_type": "approve_claim", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta", + "status": "approved" + }, + "rollback_sql_sha256": "ef2464a6802426509333861cf0768ca64184901ab7bd2030529e7356a35d9cb3", + "schema": "livingip.proposalApplyRollback.v1", + "target_rows_after": { + "claim_edges": [], + "claim_evidence": [], + "claims": [], + "reasoning_tools": [], + "sources": [] + } + }, + "schema": "livingip.proposalApplyLifecycleReceipt.v1", + "strongest_claim_allowed": "deterministic disposable-clone apply and compensating rollback" +} diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-production-target-readback-current.json b/docs/reports/leo-working-state-20260709/proposal-apply-production-target-readback-current.json new file mode 100644 index 0000000..c56352f --- /dev/null +++ b/docs/reports/leo-working-state-20260709/proposal-apply-production-target-readback-current.json @@ -0,0 +1,32 @@ +{ + "artifact": "proposal_apply_production_target_readback", + "schema": "livingip.proposalApplyProductionTargetReadback.v1", + "observed_at_utc": "2026-07-15T00:45:59.238006+00:00", + "transport": "ssh_readonly_transaction", + "read_only_transaction": true, + "read_only_setting": "on", + "container": "teleo-pg", + "database": "teleo", + "system_identifier": "7649789040005668902", + "server_version_num": "160014", + "approval_gate": { + "immutable_approval_table_present": false, + "approve_function_present": false, + "assert_function_present": false, + "finish_function_present": false, + "review_role_present": false, + "apply_role_present": true + }, + "approved_strict_candidate_count": 0, + "approved_strict_candidates": [], + "attempted_recovery": { + "strict_immutable_approval_join_attempted": true, + "strict_join_failed": true, + "strict_join_exact_gate": "relation kb_stage.kb_proposal_approvals does not exist", + "fallback_read_only_gate_and_candidate_count_succeeded": true + }, + "production_mutated": false, + "production_apply_authorization_present": false, + "production_apply_executed": false, + "claim_ceiling": "Fresh T3 live read-only identity and gate readback only. The immutable approval gate is absent and there are zero approved strict approve_claim candidates; production apply is neither authorized nor executed." +} diff --git a/pyproject.toml b/pyproject.toml index e264213..70fc9ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ [project.optional-dependencies] dev = [ + "hypothesis>=6,<7", "pytest>=8", "pytest-asyncio>=0.23", "ruff>=0.3", diff --git a/scripts/apply_proposal.py b/scripts/apply_proposal.py index 7fd4bd3..88c3c65 100644 --- a/scripts/apply_proposal.py +++ b/scripts/apply_proposal.py @@ -111,8 +111,17 @@ CLAIM_TYPES = {"empirical", "structural", "normative", "meta", "concept"} SOURCE_TYPES = {"paper", "article", "transcript", "observation", "dataset", "post", "dm", "other"} EVIDENCE_ROLES = {"grounds", "illustrates", "contradicts"} EDGE_TYPES = { - "supports", "challenges", "requires", "relates", "contradicts", - "supersedes", "derives_from", "cites", "causes", "constrains", "accelerates", + "supports", + "challenges", + "requires", + "relates", + "contradicts", + "supersedes", + "derives_from", + "cites", + "causes", + "constrains", + "accelerates", } MAX_BUNDLE_ROWS = { @@ -143,6 +152,21 @@ def _jsonb(value: Any) -> str: return sql_literal(json.dumps(value, sort_keys=True)) + "::jsonb" +def deterministic_row_uuid(proposal_id: str, row_kind: str, *semantic_key: Any) -> str: + """Derive a stable persisted row id from the proposal and semantic key. + + Evidence and edge ids are not part of the reviewed v2 payload. Deriving + them here makes clone/production receipts and a pre-authorized rollback + packet bind the same exact ids instead of relying on database randomness. + """ + material = json.dumps( + ["livingip", "kb-apply", str(proposal_id), row_kind, *semantic_key], + ensure_ascii=True, + separators=(",", ":"), + ) + return str(uuid.uuid5(uuid.NAMESPACE_URL, material)) + + def _text_array(values: list[str]) -> str: if not values: return "'{}'::text[]" @@ -196,9 +220,7 @@ def _reject_unknown(mapping: dict[str, Any], allowed: set[str], path: str) -> No raise ValueError(f"{path} contains unknown fields: {unknown}") -def _validate_approval_meta( - approval: dict[str, Any], proposal_type: str, apply_payload: dict[str, Any] -) -> None: +def _validate_approval_meta(approval: dict[str, Any], proposal_type: str, apply_payload: dict[str, Any]) -> None: if not isinstance(approval, dict): raise ValueError("approved proposal metadata is required") if approval.get("proposal_type") != proposal_type: @@ -222,12 +244,12 @@ def _validate_approval_meta( def _approval_guard_sql(proposal_id: str, approval: dict[str, Any]) -> str: return f"""select kb_stage.assert_approved_proposal( {sql_literal(proposal_id)}::uuid, - {sql_literal(approval['proposal_type'])}, - {_jsonb(approval['payload'])}, - {sql_literal(approval['reviewed_by_handle'])}, - {sql_literal(approval.get('reviewed_by_agent_id'))}::uuid, - {sql_literal(approval['reviewed_at'])}::timestamptz, - {sql_literal(approval['review_note'])} + {sql_literal(approval["proposal_type"])}, + {_jsonb(approval["payload"])}, + {sql_literal(approval["reviewed_by_handle"])}, + {sql_literal(approval.get("reviewed_by_agent_id"))}::uuid, + {sql_literal(approval["reviewed_at"])}::timestamptz, + {sql_literal(approval["review_note"])} );""" @@ -245,12 +267,12 @@ end $verify$;""" finish_sql = f"""select kb_stage.finish_approved_proposal( {sql_literal(proposal_id)}::uuid, - {sql_literal(approval['proposal_type'])}, - {_jsonb(approval['payload'])}, - {sql_literal(approval['reviewed_by_handle'])}, - {sql_literal(approval.get('reviewed_by_agent_id'))}::uuid, - {sql_literal(approval['reviewed_at'])}::timestamptz, - {sql_literal(approval['review_note'])}, + {sql_literal(approval["proposal_type"])}, + {_jsonb(approval["payload"])}, + {sql_literal(approval["reviewed_by_handle"])}, + {sql_literal(approval.get("reviewed_by_agent_id"))}::uuid, + {sql_literal(approval["reviewed_at"])}::timestamptz, + {sql_literal(approval["review_note"])}, {sql_literal(applied_by)} );""" return checks_sql + "\n" + finish_sql @@ -301,9 +323,9 @@ update public.strategies insert into public.strategies (agent_id, diagnosis, guiding_policy, proximate_objectives, version, active) select {sql_literal(agent_id)}::uuid, - {sql_literal(strategy['diagnosis'])}, - {sql_literal(strategy['guiding_policy'])}, - {_jsonb(strategy['proximate_objectives'])}, + {sql_literal(strategy["diagnosis"])}, + {sql_literal(strategy["guiding_policy"])}, + {_jsonb(strategy["proximate_objectives"])}, coalesce(max(version), 0) + 1, true from public.strategies @@ -325,9 +347,7 @@ values raise exception 'apply_proposal: expected exactly one active strategy for agent %', {sql_literal(agent_id)}; end if;""" - ledger = _ledger_and_verify( - proposal_id, applied_by or SERVICE_AGENT_HANDLE, checks, approval - ) + ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, checks, approval) return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval)) @@ -338,14 +358,16 @@ def build_add_edge_sql( approval: dict[str, Any], ) -> str: _validate_approval_meta(approval, "add_edge", apply_payload) - f = apply_payload.get("from_claim") - t = apply_payload.get("to_claim") + _reject_unknown(apply_payload, {"from_claim", "to_claim", "edge_type", "weight"}, "add_edge apply_payload") + f = _uuid(apply_payload.get("from_claim"), "add_edge.from_claim") + t = _uuid(apply_payload.get("to_claim"), "add_edge.to_claim") et = apply_payload.get("edge_type") - w = apply_payload.get("weight") - if not (f and t and et): - raise ValueError("add_edge apply_payload requires 'from_claim', 'to_claim', 'edge_type'") + if et not in EDGE_TYPES: + raise ValueError(f"add_edge.edge_type must be one of {sorted(EDGE_TYPES)}") + w = _weight(apply_payload.get("weight"), "add_edge.weight") if f == t: raise ValueError("add_edge from_claim and to_claim must differ") + row_id = deterministic_row_uuid(proposal_id, "claim_edge", f, t, et) edge_lock_key = f"claim_edge:{f}:{t}:{et}" canonical = f""" @@ -354,8 +376,8 @@ def build_add_edge_sql( select pg_advisory_xact_lock(hashtextextended({sql_literal(edge_lock_key)}, 0)); -- insert the edge unless an identical (from,to,type) edge already exists -insert into public.claim_edges (from_claim, to_claim, edge_type, weight) -select {sql_literal(f)}::uuid, {sql_literal(t)}::uuid, +insert into public.claim_edges (id, from_claim, to_claim, edge_type, weight) +select {sql_literal(row_id)}::uuid, {sql_literal(f)}::uuid, {sql_literal(t)}::uuid, {sql_literal(et)}::edge_type, {sql_literal(w)} where not exists ( select 1 from public.claim_edges @@ -364,7 +386,15 @@ select {sql_literal(f)}::uuid, {sql_literal(t)}::uuid, and edge_type = {sql_literal(et)}::edge_type); """.rstrip() - checks = f""" if exists (select 1 from public.claim_edges + checks = f""" -- Historical replay receipts can carry a pre-deterministic row id. + -- Require one exact semantic row; fresh applies still insert row_id above. + if (select count(*) from public.claim_edges + where from_claim = {sql_literal(f)}::uuid + and to_claim = {sql_literal(t)}::uuid + and edge_type = {sql_literal(et)}::edge_type) <> 1 then + raise exception 'apply_proposal: claim_edge row does not match strict apply payload'; + end if; + if exists (select 1 from public.claim_edges where from_claim = {sql_literal(f)}::uuid and to_claim = {sql_literal(t)}::uuid and edge_type = {sql_literal(et)}::edge_type @@ -379,9 +409,7 @@ select {sql_literal(f)}::uuid, {sql_literal(t)}::uuid, raise exception 'apply_proposal: claim_edge row does not match strict apply payload'; end if;""" - ledger = _ledger_and_verify( - proposal_id, applied_by or SERVICE_AGENT_HANDLE, checks, approval - ) + ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, checks, approval) return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval)) @@ -392,6 +420,7 @@ def build_attach_evidence_sql( approval: dict[str, Any], ) -> str: _validate_approval_meta(approval, "attach_evidence", apply_payload) + _reject_unknown(apply_payload, {"evidence"}, "attach_evidence apply_payload") evidence: list[dict[str, Any]] = apply_payload.get("evidence") or [] if not evidence: raise ValueError("attach_evidence apply_payload requires non-empty 'evidence'") @@ -399,25 +428,33 @@ def build_attach_evidence_sql( statements = [] checks = [] for i, ev in enumerate(evidence): - claim_id = ev.get("claim_id") - source_id = ev.get("source_id") + _reject_unknown( + ev, + {"claim_id", "source_id", "role", "weight"}, + f"attach_evidence.evidence[{i}]", + ) + claim_id = _uuid(ev.get("claim_id"), f"attach_evidence.evidence[{i}].claim_id") + source_id = _uuid(ev.get("source_id"), f"attach_evidence.evidence[{i}].source_id") role = ev.get("role") or "grounds" - weight = ev.get("weight") - if not claim_id: - raise ValueError(f"evidence[{i}] requires 'claim_id'") - if not source_id: - raise ValueError( - f"evidence[{i}] requires an existing 'source_id'. " - "kb_apply cannot create public.sources; mint the source upstream first." - ) + if role not in EVIDENCE_ROLES: + raise ValueError(f"attach_evidence.evidence[{i}].role must be one of {sorted(EVIDENCE_ROLES)}") + weight = _weight(ev.get("weight"), f"attach_evidence.evidence[{i}].weight") + row_id = deterministic_row_uuid(proposal_id, "claim_evidence", claim_id, source_id, role) statements.append( - f"""insert into public.claim_evidence (claim_id, source_id, role, weight) -select {sql_literal(claim_id)}::uuid, {sql_literal(source_id)}::uuid, + f"""insert into public.claim_evidence (id, claim_id, source_id, role, weight) +select {sql_literal(row_id)}::uuid, {sql_literal(claim_id)}::uuid, {sql_literal(source_id)}::uuid, {sql_literal(role)}::evidence_role, {sql_literal(weight)} on conflict (claim_id, source_id, role) do nothing;""" ) checks.append( - f""" if exists (select 1 from public.claim_evidence + f""" -- Accept a legacy receipt id only when exactly one semantic row matches. + if (select count(*) from public.claim_evidence + where claim_id = {sql_literal(claim_id)}::uuid + and source_id = {sql_literal(source_id)}::uuid + and role = {sql_literal(role)}::evidence_role) <> 1 then + raise exception 'apply_proposal: evidence row % does not match strict apply payload', {i}; + end if; + if exists (select 1 from public.claim_evidence where claim_id = {sql_literal(claim_id)}::uuid and source_id = {sql_literal(source_id)}::uuid and role = {sql_literal(role)}::evidence_role @@ -434,9 +471,7 @@ on conflict (claim_id, source_id, role) do nothing;""" ) canonical = "\n".join(statements) - ledger = _ledger_and_verify( - proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval - ) + ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval) return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval)) @@ -509,12 +544,8 @@ def build_approve_claim_sql( "status": status, "confidence": _weight(row.get("confidence"), f"{path}.confidence"), "tags": tags, - "created_by": _uuid( - row.get("created_by", agent_id), f"{path}.created_by", nullable=True - ), - "superseded_by": _uuid( - row.get("superseded_by"), f"{path}.superseded_by", nullable=True - ), + "created_by": _uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True), + "superseded_by": _uuid(row.get("superseded_by"), f"{path}.superseded_by", nullable=True), } ) @@ -543,9 +574,7 @@ def build_approve_claim_sql( "storage_path": row.get("storage_path"), "excerpt": row.get("excerpt"), "hash": source_hash, - "created_by": _uuid( - row.get("created_by", agent_id), f"{path}.created_by", nullable=True - ), + "created_by": _uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True), } ) @@ -562,13 +591,18 @@ def build_approve_claim_sql( raise ValueError(f"{path}.role must be one of {sorted(EVIDENCE_ROLES)}") evidence.append( { + "id": deterministic_row_uuid( + proposal_id, + "claim_evidence", + _uuid(row.get("claim_id"), f"{path}.claim_id"), + _uuid(row.get("source_id"), f"{path}.source_id"), + role, + ), "claim_id": _uuid(row.get("claim_id"), f"{path}.claim_id"), "source_id": _uuid(row.get("source_id"), f"{path}.source_id"), "role": role, "weight": _weight(row.get("weight"), f"{path}.weight"), - "created_by": _uuid( - row.get("created_by", agent_id), f"{path}.created_by", nullable=True - ), + "created_by": _uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True), } ) @@ -589,13 +623,12 @@ def build_approve_claim_sql( raise ValueError(f"{path} cannot be a self-edge") edges.append( { + "id": deterministic_row_uuid(proposal_id, "claim_edge", from_claim, to_claim, edge_type), "from_claim": from_claim, "to_claim": to_claim, "edge_type": edge_type, "weight": _weight(row.get("weight"), f"{path}.weight"), - "created_by": _uuid( - row.get("created_by", agent_id), f"{path}.created_by", nullable=True - ), + "created_by": _uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True), } ) @@ -647,150 +680,156 @@ def build_approve_claim_sql( for row in sources: collision_checks.append( f""" if exists (select 1 from public.sources - where hash = {sql_literal(row['hash'])} - and id <> {sql_literal(row['id'])}::uuid) then - raise exception 'approve_claim: source hash collision for source %', {sql_literal(row['id'])}; + where hash = {sql_literal(row["hash"])} + and id <> {sql_literal(row["id"])}::uuid) then + raise exception 'approve_claim: source hash collision for source %', {sql_literal(row["id"])}; end if;""" ) - statements.append( - "do $source_conflicts$\nbegin\n" - + "\n".join(collision_checks) - + "\nend\n$source_conflicts$;" - ) + statements.append("do $source_conflicts$\nbegin\n" + "\n".join(collision_checks) + "\nend\n$source_conflicts$;") for row in claims: tags = _text_array(row["tags"]) statements.append( f"""insert into public.claims (id, type, text, status, confidence, tags, created_by, superseded_by) -select {sql_literal(row['id'])}::uuid, {sql_literal(row['type'])}, {sql_literal(row['text'])}, - {sql_literal(row['status'])}, {sql_literal(row['confidence'])}, {tags}, - {sql_literal(row['created_by'])}::uuid, {sql_literal(row['superseded_by'])}::uuid +select {sql_literal(row["id"])}::uuid, {sql_literal(row["type"])}, {sql_literal(row["text"])}, + {sql_literal(row["status"])}, {sql_literal(row["confidence"])}, {tags}, + {sql_literal(row["created_by"])}::uuid, {sql_literal(row["superseded_by"])}::uuid on conflict (id) do nothing;""" ) checks.append( f""" if not exists (select 1 from public.claims - where id = {sql_literal(row['id'])}::uuid - and type = {sql_literal(row['type'])} - and text = {sql_literal(row['text'])} - and status = {sql_literal(row['status'])} - and confidence is not distinct from {sql_literal(row['confidence'])} + where id = {sql_literal(row["id"])}::uuid + and type = {sql_literal(row["type"])} + and text = {sql_literal(row["text"])} + and status = {sql_literal(row["status"])} + and confidence is not distinct from {sql_literal(row["confidence"])} and tags is not distinct from {tags} - and created_by is not distinct from {sql_literal(row['created_by'])}::uuid - and superseded_by is not distinct from {sql_literal(row['superseded_by'])}::uuid) then - raise exception 'approve_claim: claim row does not match strict apply payload: %', {sql_literal(row['id'])}; + and created_by is not distinct from {sql_literal(row["created_by"])}::uuid + and superseded_by is not distinct from {sql_literal(row["superseded_by"])}::uuid) then + raise exception 'approve_claim: claim row does not match strict apply payload: %', {sql_literal(row["id"])}; end if;""" ) for row in sources: statements.append( f"""insert into public.sources (id, source_type, url, storage_path, excerpt, hash, created_by) -select {sql_literal(row['id'])}::uuid, {sql_literal(row['source_type'])}, {sql_literal(row['url'])}, - {sql_literal(row['storage_path'])}, {sql_literal(row['excerpt'])}, {sql_literal(row['hash'])}, - {sql_literal(row['created_by'])}::uuid +select {sql_literal(row["id"])}::uuid, {sql_literal(row["source_type"])}, {sql_literal(row["url"])}, + {sql_literal(row["storage_path"])}, {sql_literal(row["excerpt"])}, {sql_literal(row["hash"])}, + {sql_literal(row["created_by"])}::uuid on conflict do nothing;""" ) checks.append( f""" if exists (select 1 from public.sources - where hash = {sql_literal(row['hash'])} - and id <> {sql_literal(row['id'])}::uuid) then - raise exception 'approve_claim: source hash collision for source %', {sql_literal(row['id'])}; + where hash = {sql_literal(row["hash"])} + and id <> {sql_literal(row["id"])}::uuid) then + raise exception 'approve_claim: source hash collision for source %', {sql_literal(row["id"])}; end if; if not exists (select 1 from public.sources - where id = {sql_literal(row['id'])}::uuid - and source_type = {sql_literal(row['source_type'])} - and url is not distinct from {sql_literal(row['url'])} - and storage_path is not distinct from {sql_literal(row['storage_path'])} - and excerpt is not distinct from {sql_literal(row['excerpt'])} - and hash = {sql_literal(row['hash'])} - and created_by is not distinct from {sql_literal(row['created_by'])}::uuid) then - raise exception 'approve_claim: source row does not match strict apply payload: %', {sql_literal(row['id'])}; + where id = {sql_literal(row["id"])}::uuid + and source_type = {sql_literal(row["source_type"])} + and url is not distinct from {sql_literal(row["url"])} + and storage_path is not distinct from {sql_literal(row["storage_path"])} + and excerpt is not distinct from {sql_literal(row["excerpt"])} + and hash = {sql_literal(row["hash"])} + and created_by is not distinct from {sql_literal(row["created_by"])}::uuid) then + raise exception 'approve_claim: source row does not match strict apply payload: %', {sql_literal(row["id"])}; end if;""" ) for row in reasoning_tools: statements.append( f"""insert into public.reasoning_tools (id, agent_id, name, description, category) -select {sql_literal(row['id'])}::uuid, {sql_literal(row['agent_id'])}::uuid, - {sql_literal(row['name'])}, {sql_literal(row['description'])}, {sql_literal(row['category'])} +select {sql_literal(row["id"])}::uuid, {sql_literal(row["agent_id"])}::uuid, + {sql_literal(row["name"])}, {sql_literal(row["description"])}, {sql_literal(row["category"])} on conflict (id) do nothing;""" ) checks.append( f""" if not exists (select 1 from public.reasoning_tools - where id = {sql_literal(row['id'])}::uuid - and agent_id is not distinct from {sql_literal(row['agent_id'])}::uuid - and name = {sql_literal(row['name'])} - and description = {sql_literal(row['description'])} - and category is not distinct from {sql_literal(row['category'])}) then - raise exception 'approve_claim: reasoning tool row does not match strict apply payload: %', {sql_literal(row['id'])}; + where id = {sql_literal(row["id"])}::uuid + and agent_id is not distinct from {sql_literal(row["agent_id"])}::uuid + and name = {sql_literal(row["name"])} + and description = {sql_literal(row["description"])} + and category is not distinct from {sql_literal(row["category"])}) then + raise exception 'approve_claim: reasoning tool row does not match strict apply payload: %', {sql_literal(row["id"])}; end if;""" ) for row in evidence: statements.append( - f"""insert into public.claim_evidence (claim_id, source_id, role, weight, created_by) -select {sql_literal(row['claim_id'])}::uuid, {sql_literal(row['source_id'])}::uuid, - {sql_literal(row['role'])}::evidence_role, {sql_literal(row['weight'])}, - {sql_literal(row['created_by'])}::uuid + f"""insert into public.claim_evidence (id, claim_id, source_id, role, weight, created_by) +select {sql_literal(row["id"])}::uuid, {sql_literal(row["claim_id"])}::uuid, {sql_literal(row["source_id"])}::uuid, + {sql_literal(row["role"])}::evidence_role, {sql_literal(row["weight"])}, + {sql_literal(row["created_by"])}::uuid on conflict (claim_id, source_id, role) do nothing;""" ) checks.append( - f""" if exists (select 1 from public.claim_evidence - where claim_id = {sql_literal(row['claim_id'])}::uuid - and source_id = {sql_literal(row['source_id'])}::uuid - and role = {sql_literal(row['role'])}::evidence_role - and (weight is distinct from {sql_literal(row['weight'])} - or created_by is distinct from {sql_literal(row['created_by'])}::uuid)) then + f""" -- Accept a legacy receipt id only when exactly one semantic row matches. + if (select count(*) from public.claim_evidence + where claim_id = {sql_literal(row["claim_id"])}::uuid + and source_id = {sql_literal(row["source_id"])}::uuid + and role = {sql_literal(row["role"])}::evidence_role) <> 1 then + raise exception 'approve_claim: evidence row does not match strict apply payload'; + end if; + if exists (select 1 from public.claim_evidence + where claim_id = {sql_literal(row["claim_id"])}::uuid + and source_id = {sql_literal(row["source_id"])}::uuid + and role = {sql_literal(row["role"])}::evidence_role + and (weight is distinct from {sql_literal(row["weight"])} + or created_by is distinct from {sql_literal(row["created_by"])}::uuid)) then raise exception 'approve_claim: evidence row does not match strict apply payload'; end if; if not exists (select 1 from public.claim_evidence - where claim_id = {sql_literal(row['claim_id'])}::uuid - and source_id = {sql_literal(row['source_id'])}::uuid - and role = {sql_literal(row['role'])}::evidence_role - and weight is not distinct from {sql_literal(row['weight'])} - and created_by is not distinct from {sql_literal(row['created_by'])}::uuid) then + where claim_id = {sql_literal(row["claim_id"])}::uuid + and source_id = {sql_literal(row["source_id"])}::uuid + and role = {sql_literal(row["role"])}::evidence_role + and weight is not distinct from {sql_literal(row["weight"])} + and created_by is not distinct from {sql_literal(row["created_by"])}::uuid) then raise exception 'approve_claim: evidence row does not match strict apply payload'; end if;""" ) for row in edges: - edge_lock_key = ( - f"claim_edge:{row['from_claim']}:{row['to_claim']}:{row['edge_type']}" - ) + edge_lock_key = f"claim_edge:{row['from_claim']}:{row['to_claim']}:{row['edge_type']}" statements.append( f"""select pg_advisory_xact_lock(hashtextextended({sql_literal(edge_lock_key)}, 0)); -insert into public.claim_edges (from_claim, to_claim, edge_type, weight, created_by) -select {sql_literal(row['from_claim'])}::uuid, {sql_literal(row['to_claim'])}::uuid, - {sql_literal(row['edge_type'])}::edge_type, {sql_literal(row['weight'])}, - {sql_literal(row['created_by'])}::uuid +insert into public.claim_edges (id, from_claim, to_claim, edge_type, weight, created_by) +select {sql_literal(row["id"])}::uuid, {sql_literal(row["from_claim"])}::uuid, {sql_literal(row["to_claim"])}::uuid, + {sql_literal(row["edge_type"])}::edge_type, {sql_literal(row["weight"])}, + {sql_literal(row["created_by"])}::uuid where not exists ( select 1 from public.claim_edges - where from_claim = {sql_literal(row['from_claim'])}::uuid - and to_claim = {sql_literal(row['to_claim'])}::uuid - and edge_type = {sql_literal(row['edge_type'])}::edge_type);""" + where from_claim = {sql_literal(row["from_claim"])}::uuid + and to_claim = {sql_literal(row["to_claim"])}::uuid + and edge_type = {sql_literal(row["edge_type"])}::edge_type);""" ) checks.append( - f""" if exists (select 1 from public.claim_edges - where from_claim = {sql_literal(row['from_claim'])}::uuid - and to_claim = {sql_literal(row['to_claim'])}::uuid - and edge_type = {sql_literal(row['edge_type'])}::edge_type - and (weight is distinct from {sql_literal(row['weight'])} - or created_by is distinct from {sql_literal(row['created_by'])}::uuid)) then + f""" -- Accept a legacy receipt id only when exactly one semantic row matches. + if (select count(*) from public.claim_edges + where from_claim = {sql_literal(row["from_claim"])}::uuid + and to_claim = {sql_literal(row["to_claim"])}::uuid + and edge_type = {sql_literal(row["edge_type"])}::edge_type) <> 1 then + raise exception 'approve_claim: edge row does not match strict apply payload'; + end if; + if exists (select 1 from public.claim_edges + where from_claim = {sql_literal(row["from_claim"])}::uuid + and to_claim = {sql_literal(row["to_claim"])}::uuid + and edge_type = {sql_literal(row["edge_type"])}::edge_type + and (weight is distinct from {sql_literal(row["weight"])} + or created_by is distinct from {sql_literal(row["created_by"])}::uuid)) then raise exception 'approve_claim: edge row does not match strict apply payload'; end if; if not exists (select 1 from public.claim_edges - where from_claim = {sql_literal(row['from_claim'])}::uuid - and to_claim = {sql_literal(row['to_claim'])}::uuid - and edge_type = {sql_literal(row['edge_type'])}::edge_type - and weight is not distinct from {sql_literal(row['weight'])} - and created_by is not distinct from {sql_literal(row['created_by'])}::uuid) then + where from_claim = {sql_literal(row["from_claim"])}::uuid + and to_claim = {sql_literal(row["to_claim"])}::uuid + and edge_type = {sql_literal(row["edge_type"])}::edge_type + and weight is not distinct from {sql_literal(row["weight"])} + and created_by is not distinct from {sql_literal(row["created_by"])}::uuid) then raise exception 'approve_claim: edge row does not match strict apply payload'; end if;""" ) canonical = "\n\n".join(statements) - ledger = _ledger_and_verify( - proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval - ) + ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval) return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval)) @@ -837,9 +876,7 @@ def assert_applyable(proposal: dict[str, Any]) -> None: if status == "applied": raise SystemExit(f"proposal {proposal['id']} is already applied (idempotent no-op)") if status != "approved": - raise SystemExit( - f"proposal {proposal['id']} has status {status!r}; only 'approved' proposals apply" - ) + raise SystemExit(f"proposal {proposal['id']} has status {status!r}; only 'approved' proposals apply") def assert_receiptable(proposal: dict[str, Any]) -> None: @@ -877,23 +914,36 @@ def run_psql( ) -> str: docker_binary = shutil.which("docker") or "docker" command = [ - docker_binary, "exec", "-e", "PGPASSWORD", "-i", args.container, - "psql", "-U", args.role, "-h", args.host, "-d", args.db, - "-v", "ON_ERROR_STOP=1", "-At", "-q", + docker_binary, + "exec", + "-e", + "PGPASSWORD", + "-i", + args.container, + "psql", + "-U", + args.role, + "-h", + args.host, + "-d", + args.db, + "-v", + "ON_ERROR_STOP=1", + "-At", + "-q", ] result = subprocess.run( - command, input=sql, text=True, capture_output=True, + command, + input=sql, + text=True, + capture_output=True, env={"PGPASSWORD": password, "PATH": "/usr/bin:/bin:/usr/local/bin"}, check=False, ) if result.returncode != 0: if redact_output_on_error: - raise SystemExit( - f"psql failed ({result.returncode}); private postflight output redacted" - ) - raise SystemExit( - f"psql failed ({result.returncode})\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" - ) + raise SystemExit(f"psql failed ({result.returncode}); private postflight output redacted") + raise SystemExit(f"psql failed ({result.returncode})\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}") return result.stdout @@ -980,8 +1030,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: p.add_argument( "--receipt-dir", default=None, - help="private receipt directory (default: KB_APPLY_RECEIPT_DIR or " - f"{replay_receipt.DEFAULT_RECEIPT_DIR})", + help=f"private receipt directory (default: KB_APPLY_RECEIPT_DIR or {replay_receipt.DEFAULT_RECEIPT_DIR})", ) p.add_argument( "--receipt-out", diff --git a/scripts/apply_worker.py b/scripts/apply_worker.py index 2abefc7..6d3dc13 100644 --- a/scripts/apply_worker.py +++ b/scripts/apply_worker.py @@ -35,6 +35,7 @@ from __future__ import annotations import argparse import json import os +import stat import subprocess import sys from pathlib import Path @@ -53,6 +54,13 @@ WORKER_TYPES = ap.APPLYABLE_TYPES # bundles and evidence/edge applies do not trigger a generic identity render. RENDER_TYPES = ("revise_strategy",) +POSTCOMMIT_RECEIPT_FAILURE_MARKER = "committed, but private replay receipt capture failed" +POSTCOMMIT_RECEIPT_RECOVERY_MARKER = ". Recover without reapplying via: " + + +class PostCommitReceiptError(RuntimeError): + """The proposal committed, but its private row-level receipt is unavailable.""" + # --------------------------------------------------------------------------- # # Pure helpers (unit-tested without a DB) # @@ -160,14 +168,69 @@ def partition_candidates( return {"poisoned": poisoned, "to_apply": eligible[: max(0, max_per_tick)]} +def has_exact_postcommit_receipt_failure(stderr: str | None, proposal_id: str) -> bool: + """Recognize only the proposal-bound post-COMMIT message from apply_proposal. + + The old marker can occur inside payload-validation tracebacks (for example, + as an attacker-controlled unknown field name). Requiring the complete + engine message shape prevents those pre-COMMIT failures from bypassing the + normal failure and poison-pill accounting. + """ + prefix = f"proposal {proposal_id} {POSTCOMMIT_RECEIPT_FAILURE_MARKER}: " + recovery_suffix = f" {proposal_id} --receipt-only" + for line in (stderr or "").splitlines(): + candidate = line.rstrip("\r") + if ( + candidate.startswith(prefix) + and POSTCOMMIT_RECEIPT_RECOVERY_MARKER in candidate[len(prefix) :] + and candidate.endswith(recovery_suffix) + ): + return True + return False + + +def receipt_path_for_proposal(args: argparse.Namespace, proposal_id: str) -> Path: + """Return the explicit, deterministic private receipt path for one proposal.""" + receipt_dir = ( + getattr(args, "receipt_dir", None) + or os.environ.get("KB_APPLY_WORKER_RECEIPT_DIR") + or os.environ.get("KB_APPLY_RECEIPT_DIR") + or ap.replay_receipt.DEFAULT_RECEIPT_DIR + ) + return Path(receipt_dir).expanduser().resolve() / f"{proposal_id}.json" + + +def assert_private_replay_receipt(path: Path, proposal_id: str) -> None: + """Verify the worker can rely on the exact private row-level receipt.""" + try: + path_stat = path.lstat() + except OSError as exc: + raise RuntimeError("private replay receipt was not written") from exc + if not stat.S_ISREG(path_stat.st_mode) or path.is_symlink(): + raise RuntimeError("private replay receipt is not a regular file") + if stat.S_IMODE(path_stat.st_mode) & 0o077: + raise RuntimeError("private replay receipt permissions are broader than 0600") + try: + receipt = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise RuntimeError("private replay receipt is unreadable or invalid") from exc + if not isinstance(receipt, dict): + raise RuntimeError("private replay receipt is not a JSON object") + try: + ap.replay_receipt.validate_replay_receipt( + receipt, + expected_proposal_id=str(proposal_id), + ) + except ValueError as exc: + raise RuntimeError("private replay receipt row-level contract is invalid") from exc + + # --------------------------------------------------------------------------- # # Side-effecting steps # # --------------------------------------------------------------------------- # def _psql_args(args: argparse.Namespace) -> argparse.Namespace: """Namespace shaped for ap.run_psql (reuses the kb_apply connection path).""" - return argparse.Namespace( - container=args.container, db=args.db, host=args.host, role=args.role - ) + return argparse.Namespace(container=args.container, db=args.db, host=args.host, role=args.role) def fetch_candidates( @@ -181,20 +244,73 @@ def fetch_candidates( def apply_one(args: argparse.Namespace, proposal_id: str) -> None: - """Apply via the audited apply_proposal.py CLI -- same txn + rowcount guard.""" + """Apply once and retain or safely recover the exact private replay receipt.""" + receipt_path = receipt_path_for_proposal(args, proposal_id) cmd = [ - sys.executable, str(args.apply_script), proposal_id, - "--applied-by", args.applied_by, - "--secrets-file", args.secrets_file, - "--container", args.container, "--db", args.db, - "--host", args.host, "--role", args.role, + sys.executable, + str(args.apply_script), + proposal_id, + "--applied-by", + args.applied_by, + "--secrets-file", + args.secrets_file, + "--container", + args.container, + "--db", + args.db, + "--host", + args.host, + "--role", + args.role, + "--receipt-dir", + str(receipt_path.parent), + "--receipt-out", + str(receipt_path), ] result = subprocess.run(cmd, text=True, capture_output=True, check=False) - if result.returncode != 0: + committed = result.returncode == 0 or has_exact_postcommit_receipt_failure( + result.stderr, + proposal_id, + ) + if not committed: raise RuntimeError( - f"apply failed for {proposal_id}: {result.stdout.strip()} {result.stderr.strip()}" + f"apply failed for {proposal_id}: {(result.stdout or '').strip()} {(result.stderr or '').strip()}" ) + if result.returncode == 0: + try: + assert_private_replay_receipt(receipt_path, proposal_id) + return + except RuntimeError: + # The engine returned only after COMMIT. A missing/invalid receipt is + # recoverable without replaying the apply transaction. + pass + + try: + recovery = subprocess.run( + [*cmd, "--receipt-only"], + text=True, + capture_output=True, + check=False, + ) + except OSError as exc: + raise PostCommitReceiptError( + f"proposal {proposal_id} committed, but the read-only receipt recovery " + "command could not start; do not retry the apply transaction" + ) from exc + if recovery.returncode != 0: + raise PostCommitReceiptError( + f"proposal {proposal_id} committed, but read-only receipt recovery " + f"failed (exit {recovery.returncode}); do not retry the apply transaction" + ) + try: + assert_private_replay_receipt(receipt_path, proposal_id) + except RuntimeError as exc: + raise PostCommitReceiptError( + f"proposal {proposal_id} committed, but receipt recovery did not produce " + "a valid private row-level receipt; do not retry the apply transaction" + ) from exc + def render_one(args: argparse.Namespace, agent_id: str | None) -> str: cmd = build_render_command(args.render_cmd, agent_id) @@ -202,9 +318,7 @@ def render_one(args: argparse.Namespace, agent_id: str | None) -> str: return "render skipped (no render-cmd configured; PR2 renderer not deployed)" result = subprocess.run(cmd, text=True, capture_output=True, check=False) if result.returncode != 0: - raise RuntimeError( - f"render failed for agent {agent_id}: {result.stdout.strip()} {result.stderr.strip()}" - ) + raise RuntimeError(f"render failed for agent {agent_id}: {result.stdout.strip()} {result.stderr.strip()}") return f"rendered agent {agent_id}" @@ -214,11 +328,7 @@ def render_one(args: argparse.Namespace, agent_id: str | None) -> str: def run(args: argparse.Namespace) -> int: password = ap.load_password(args.secrets_file) failure_counts = load_failure_state(args.failure_state_file) - exhausted_ids = tuple( - proposal_id - for proposal_id, count in failure_counts.items() - if count >= args.max_attempts - ) + exhausted_ids = tuple(proposal_id for proposal_id, count in failure_counts.items() if count >= args.max_attempts) candidates = fetch_candidates(args, password, exhausted_ids) for proposal_id in exhausted_ids: @@ -263,6 +373,15 @@ def run(args: argparse.Namespace) -> int: print(f"applied {pid} ({ptype})") if ptype in RENDER_TYPES: print(" " + render_one(args, agent_id)) + except PostCommitReceiptError as exc: + failures += 1 + # The canonical transaction already committed. Do not classify this + # as an apply failure or poison/retry the proposal; receipt-only is + # the sole safe recovery path. + print( + f"POST-COMMIT RECEIPT ERROR for {pid} ({ptype}): {exc}", + file=sys.stderr, + ) except Exception as exc: failures += 1 # Leave the proposal at 'approved' so a fixed one reapplies next tick @@ -270,48 +389,65 @@ def run(args: argparse.Namespace) -> int: # so a deterministically-failing proposal hits the ceiling instead of # retrying forever. Surface loudly for the operator. failure_counts[pid] = failure_counts.get(pid, 0) + 1 - print(f"ERROR applying {pid} ({ptype}) [attempt {failure_counts[pid]}/" - f"{args.max_attempts}]: {exc}", file=sys.stderr) + print( + f"ERROR applying {pid} ({ptype}) [attempt {failure_counts[pid]}/{args.max_attempts}]: {exc}", + file=sys.stderr, + ) save_failure_state(args.failure_state_file, failure_counts) return 1 if failures else 0 def parse_args(argv: list[str]) -> argparse.Namespace: - p = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter - ) + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument( - "--enable", action="store_true", + "--enable", + action="store_true", help="actually apply (default: report-only). Also honored via KB_APPLY_WORKER_ENABLED=1.", ) p.add_argument("--limit", type=int, default=20, help="max candidates fetched per tick") p.add_argument( - "--max-per-tick", type=int, default=1, + "--max-per-tick", + type=int, + default=1, help="max proposals actually APPLIED per tick (default 1: applies land " "one-at-a-time and observably, not a whole-queue drain)", ) p.add_argument( - "--max-attempts", type=int, default=3, + "--max-attempts", + type=int, + default=3, help="consecutive apply failures before a proposal is treated as a " "poison pill and skipped (needs a human fix, not endless retries)", ) p.add_argument( "--failure-state-file", default=os.environ.get("KB_APPLY_WORKER_STATE", "/opt/teleo-eval/logs/kb-apply-worker-failures.json"), - help="persisted per-proposal failure counts (survives oneshot ticks so " - "the poison-pill ceiling actually bites)", + help="persisted per-proposal failure counts (survives oneshot ticks so the poison-pill ceiling actually bites)", ) p.add_argument( - "--applied-by", default=ap.SERVICE_AGENT_HANDLE, + "--applied-by", + default=ap.SERVICE_AGENT_HANDLE, help="handle recorded as applied_by (default: the kb-apply service agent)", ) p.add_argument( - "--apply-script", default=str(HERE / "apply_proposal.py"), + "--apply-script", + default=str(HERE / "apply_proposal.py"), help="path to the apply_proposal.py engine (the sole apply path)", ) p.add_argument( - "--render-cmd", default=os.environ.get("KB_APPLY_RENDER_CMD", ""), + "--receipt-dir", + default=( + os.environ.get("KB_APPLY_WORKER_RECEIPT_DIR") + or os.environ.get("KB_APPLY_RECEIPT_DIR") + or ap.replay_receipt.DEFAULT_RECEIPT_DIR + ), + help="private replay receipt directory; each apply writes the deterministic " + ".json path and verifies 0600 permissions", + ) + p.add_argument( + "--render-cmd", + default=os.environ.get("KB_APPLY_RENDER_CMD", ""), help="render hook template, e.g. 'python3 render_soul.py --agent-id {agent_id}'. " "Empty (default) skips render until the SOUL renderer is deployed.", ) diff --git a/scripts/finalize_approve_claim_isolated_canary.py b/scripts/finalize_approve_claim_isolated_canary.py new file mode 100644 index 0000000..059ca97 --- /dev/null +++ b/scripts/finalize_approve_claim_isolated_canary.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Finalize outer isolation and cleanup evidence for the clone canary.""" + +from __future__ import annotations + +import hashlib +import json +import os +import sys +from collections.abc import Mapping +from pathlib import Path +from typing import Any + + +def _service(raw: str) -> dict[str, str]: + return dict(line.split("=", 1) for line in raw.splitlines() if "=" in line) + + +def _table_deltas(before: dict[str, int], after: dict[str, int]) -> dict[str, int]: + if set(before) != set(after): + raise ValueError( + f"live source count keys changed during the canary: before={sorted(before)} after={sorted(after)}" + ) + return {key: after[key] - before[key] for key in sorted(before)} + + +def _verify_source_binding(root: Path, records: list[dict[str, object]]) -> dict[str, object]: + readbacks = [] + for record in records: + relative_text = str(record.get("repo_relative_path", "")) + relative = Path(relative_text) + if not relative_text or relative.is_absolute() or ".." in relative.parts: + raise ValueError(f"unsafe repo-relative source path: {relative_text!r}") + candidate = (root / relative).resolve() + try: + candidate.relative_to(root) + except ValueError as exc: + raise ValueError(f"source path escaped repository root: {relative_text}") from exc + if not candidate.is_file(): + readbacks.append( + { + "repo_relative_path": relative.as_posix(), + "expected_sha256": record.get("sha256"), + "actual_sha256": None, + "matches": False, + } + ) + continue + content = candidate.read_bytes() + actual_sha256 = hashlib.sha256(content).hexdigest() + readbacks.append( + { + "repo_relative_path": relative.as_posix(), + "expected_sha256": record.get("sha256"), + "actual_sha256": actual_sha256, + "size_bytes": len(content), + "matches": (actual_sha256 == record.get("sha256") and len(content) == record.get("size_bytes")), + } + ) + return { + "files": readbacks, + "all_match": bool(readbacks) and all(bool(row["matches"]) for row in readbacks), + } + + +def _contains_ephemeral_path(value: object) -> bool: + if isinstance(value, str): + return "/tmp/" in value + if isinstance(value, dict): + return any(_contains_ephemeral_path(item) for item in value.values()) + if isinstance(value, list): + return any(_contains_ephemeral_path(item) for item in value) + return False + + +def finalize(path: Path, env: Mapping[str, str]) -> dict[str, Any]: + if not path.is_file(): + raise ValueError("inner canary did not produce a report") + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError("inner canary report must be a JSON object") + + before_counts = json.loads(env["SOURCE_COUNTS_BEFORE"]) + after_counts = json.loads(env["SOURCE_COUNTS_AFTER"]) + live_table_deltas = _table_deltas(before_counts, after_counts) + before_service = _service(env["SERVICE_BEFORE"]) + after_service = _service(env["SERVICE_AFTER"]) + source_tier = env["SOURCE_TIER"] + source_network_mode = env["SOURCE_NETWORK_MODE"] + source_canary_label = env["SOURCE_CANARY_LABEL"] + container_network_mode = env["CANARY_NETWORK_MODE"] + container_mounts = json.loads(env["CANARY_MOUNTS_JSON"]) + container_canary_label = env["CANARY_LABEL"] + container_instance_label = env["CANARY_INSTANCE_LABEL"] + container_volume_mounts = [mount for mount in container_mounts if mount.get("Type") == "volume"] + container_isolated = container_network_mode == "none" and not container_volume_mounts + container_labeled = container_canary_label == "proposal-apply-lifecycle" and container_instance_label.startswith( + "working-leo-approve-claim-" + ) + source_tier_enforced = source_tier != "isolated-local" or ( + source_network_mode == "none" and source_canary_label == "working-leo-approved-source" + ) + service_observable = before_service.get("Available") == "true" and after_service.get("Available") == "true" + service_stable = ( + all( + before_service.get(key) == after_service.get(key) + for key in ("ActiveState", "SubState", "MainPID", "NRestarts") + ) + if service_observable + else None + ) + service_gate_passes = service_stable is True if source_tier == "live-readonly" else service_stable is not False + container_absent = int(env["CONTAINER_NAME_READBACK_RC"]) == 0 and not env["CONTAINER_NAME_READBACK"].strip() + label_scoped_orphan_count = int(env["CONTAINER_LABEL_ORPHAN_COUNT"]) + label_scoped_container_absent = ( + int(env["CONTAINER_LABEL_READBACK_RC"]) == 0 + and label_scoped_orphan_count == 0 + and not env["CONTAINER_LABEL_READBACK"].strip() + ) + workdir_absent = env["OUTER_WORKDIR_LEXISTS"] == "false" + source_binding = _verify_source_binding( + Path(env["REPO_ROOT"]).resolve(), + data.get("source_binding", {}).get("files", []), + ) + data["source_binding"]["independent_post_cleanup_verification"] = source_binding + source_binding_ok = ( + data["source_binding"].get("unchanged_during_run") is True and source_binding["all_match"] is True + ) + stale_ephemeral_paths_absent = not _contains_ephemeral_path(data) + inner_runtime_pass = data.get("status") == "pass" and int(env["CANARY_RC"]) == 0 + outer_ok = ( + before_counts == after_counts + and all(delta == 0 for delta in live_table_deltas.values()) + and service_gate_passes + and container_absent + and label_scoped_container_absent + and workdir_absent + and source_binding_ok + and stale_ephemeral_paths_absent + and container_isolated + and container_labeled + and source_tier_enforced + ) + if source_tier == "isolated-local": + data["required_tier"] = "T2_runtime" + data["required_tiers"] = ["T2_runtime"] + data["runtime_scope"] = "isolated_postgres_container_from_readonly_local_fixture" + else: + data["required_tier"] = "T3_live_readonly" + data["required_tiers"] = ["T2_runtime", "T3_live_readonly"] + data["runtime_scope"] = "isolated_postgres_container_from_readonly_live_schema" + data["outer_isolation"] = { + "source_container": env.get("SOURCE_CONTAINER", "teleo-pg"), + "source_database": env.get("SOURCE_DB", "teleo"), + "source_counts_before": before_counts, + "source_counts_after": after_counts, + "canonical_table_deltas": live_table_deltas, + "source_counts_unchanged": before_counts == after_counts, + "service_before": before_service, + "service_after": after_service, + "service_required": source_tier == "live-readonly", + "service_observable": service_observable, + "service_stable": service_stable, + "service_gate_passes": service_gate_passes, + "source_boundary": { + "source_tier": source_tier, + "network_mode": source_network_mode, + "canary_label": source_canary_label, + "tier_enforced": source_tier_enforced, + }, + "container_isolation": { + "network_mode": container_network_mode, + "volume_mounts": container_volume_mounts, + "canary_label": container_canary_label, + "instance_label": container_instance_label, + "lifecycle_labeled": container_labeled, + "network_disabled": container_network_mode == "none", + "no_docker_volumes": not container_volume_mounts, + }, + "cleanup_readback": { + "container_remove_returncode": int(env["OUTER_CONTAINER_REMOVE_RC"]), + "container_name_query_returncode": int(env["CONTAINER_NAME_READBACK_RC"]), + "container_name_query_stdout": env["CONTAINER_NAME_READBACK"], + "temporary_container_absent": container_absent, + "label_scoped_query_returncode": int(env["CONTAINER_LABEL_READBACK_RC"]), + "label_scoped_query_stdout": env["CONTAINER_LABEL_READBACK"], + "label_scoped_orphan_count": label_scoped_orphan_count, + "label_scoped_temporary_container_absent": label_scoped_container_absent, + "workdir_remove_returncode": int(env["OUTER_WORKDIR_REMOVE_RC"]), + "workdir_lexists_after_cleanup": not workdir_absent, + "temporary_workdir_absent": workdir_absent, + }, + } + checks = data["checks"] + checks["outer_live_counts_exactly_unchanged"] = before_counts == after_counts + checks["outer_live_table_deltas_exactly_zero"] = all(delta == 0 for delta in live_table_deltas.values()) + checks["outer_service_observed_if_required"] = source_tier != "live-readonly" or service_observable + checks["outer_service_stable_if_observed"] = service_gate_passes + checks["outer_container_absent_independent_readback"] = container_absent + checks["outer_labeled_container_absent_independent_readback"] = label_scoped_container_absent + checks["outer_workdir_absent_independent_readback"] = workdir_absent + checks["source_binding_independently_verified"] = source_binding_ok + checks["stale_ephemeral_paths_absent"] = stale_ephemeral_paths_absent + checks["outer_source_tier_enforced"] = source_tier_enforced + checks["outer_container_network_disabled"] = container_network_mode == "none" + checks["outer_container_lifecycle_labeled"] = container_labeled + checks["outer_container_has_no_docker_volumes"] = not container_volume_mounts + checks["outer_isolation_complete"] = outer_ok + if outer_ok and inner_runtime_pass: + data["current_tier"] = "T2_runtime" if source_tier == "isolated-local" else "T3_live_readonly" + else: + data["status"] = "fail" + data["current_tier"] = "T2_runtime" if inner_runtime_pass else "T1_model" + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return data + + +def main(argv: list[str] | None = None) -> int: + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + raise SystemExit("usage: finalize_approve_claim_isolated_canary.py REPORT.json") + try: + data = finalize(Path(arguments[0]), os.environ) + except (KeyError, OSError, ValueError, json.JSONDecodeError) as exc: + raise SystemExit(str(exc)) from exc + return 0 if data["status"] == "pass" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/kb_apply_replay_receipt.py b/scripts/kb_apply_replay_receipt.py index 83678a9..2e89345 100644 --- a/scripts/kb_apply_replay_receipt.py +++ b/scripts/kb_apply_replay_receipt.py @@ -446,6 +446,41 @@ def build_replay_receipt( } +def validate_replay_receipt( + receipt: dict[str, Any], + *, + expected_proposal_id: str | None = None, +) -> dict[str, Any]: + """Rebuild and require the exact private replay-receipt contract. + + Canonical JSON comparison is deliberate: Python equality treats booleans + and integers as interchangeable, which is unsafe for authorization and + receipt fields. + """ + if not isinstance(receipt, dict): + raise ValueError("replay receipt must be an object") + proposal = receipt.get("proposal") + rows = receipt.get("canonical_rows") + apply_engine = receipt.get("apply_engine") + if not isinstance(proposal, dict) or not isinstance(rows, dict) or not isinstance(apply_engine, dict): + raise ValueError("replay receipt is missing proposal, canonical rows, or apply-engine metadata") + if expected_proposal_id is not None and proposal.get("id") != expected_proposal_id: + raise ValueError("replay receipt proposal id does not match") + try: + rebuilt = build_replay_receipt( + proposal, + rows, + apply_sql_sha256=apply_engine.get("apply_sql_sha256"), + apply_sql_source=apply_engine.get("source"), + exported_at_utc=receipt.get("exported_at_utc"), + ) + except (KeyError, TypeError, ValueError) as exc: + raise ValueError("replay receipt failed full contract validation") from exc + if _canonical_json(receipt) != _canonical_json(rebuilt): + raise ValueError("replay receipt hashes, counts, rows, or contract fields are internally inconsistent") + return rebuilt + + def resolve_receipt_path( proposal_id: str, *, diff --git a/scripts/proposal_apply_lifecycle.py b/scripts/proposal_apply_lifecycle.py new file mode 100644 index 0000000..9ddb600 --- /dev/null +++ b/scripts/proposal_apply_lifecycle.py @@ -0,0 +1,1513 @@ +#!/usr/bin/env python3 +"""Build and verify exact apply/rollback lifecycle evidence for one proposal. + +The module deliberately supports compensating rollback only for a fresh-owned +``approve_claim`` bundle: every payload-owned row must have been absent before +apply. It is used by the disposable-clone canary and by the production +authorization packet builder. It never connects to a database by itself. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +import apply_proposal as ap # noqa: E402 +import kb_apply_replay_receipt as replay_receipt # noqa: E402 + +ROLLBACK_SCHEMA = "livingip.proposalApplyRollback.v1" +LIFECYCLE_SCHEMA = "livingip.proposalApplyLifecycleReceipt.v1" +AUTHORIZATION_SCHEMA = "livingip.proposalApplyAuthorizationPacket.v1" +FRESH_PREFLIGHT_SCHEMA = "livingip.proposalApplyFreshPreflight.v1" +PRODUCTION_ROLLBACK_ARTIFACT_SCHEMA = "livingip.proposalApplyProductionRollbackArtifact.v1" + +RUNTIME_DEPENDENCIES = ( + "scripts/proposal_apply_lifecycle.py", + "scripts/apply_proposal.py", + "scripts/kb_apply_replay_receipt.py", + "scripts/kb_apply_prereqs.sql", +) + +APPROVAL_GATE_CHECKS = ( + "immutable_approval_table_present", + "approve_function_present", + "assert_function_present", + "finish_function_present", + "review_role_present", + "apply_role_present", +) + +ROW_TABLES = { + "claims": "public.claims", + "sources": "public.sources", + "claim_evidence": "public.claim_evidence", + "claim_edges": "public.claim_edges", + "reasoning_tools": "public.reasoning_tools", +} +DELETE_ORDER = ( + "claim_evidence", + "claim_edges", + "reasoning_tools", + "sources", + "claims", +) + + +def canonical_json(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True) + + +def sha256_json(value: Any) -> str: + return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest() + + +def sha256_text(value: str) -> str: + return hashlib.sha256(value.encode("utf-8")).hexdigest() + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def is_sha256(value: Any) -> bool: + return isinstance(value, str) and len(value) == 64 and all(character in "0123456789abcdef" for character in value) + + +def repo_relative_path(path: Path) -> str: + try: + return path.resolve().relative_to(HERE.parent.resolve()).as_posix() + except ValueError as exc: + raise ValueError(f"authorization dependency must be inside the repository: {path}") from exc + + +def _require_approved_bundle(proposal: dict[str, Any]) -> dict[str, Any]: + if proposal.get("proposal_type") != "approve_claim": + raise ValueError("compensating rollback supports only approve_claim") + if proposal.get("status") != "approved": + raise ValueError("pre-apply proposal must be approved") + if any(proposal.get(field) is not None for field in ("applied_by_handle", "applied_by_agent_id", "applied_at")): + raise ValueError("approved pre-apply proposal must have blank applied fields") + payload = proposal.get("payload") + apply_payload = payload.get("apply_payload") if isinstance(payload, dict) else None + if not isinstance(apply_payload, dict): + raise ValueError("approved proposal requires payload.apply_payload") + # Reuse the audited builder as the strict contract validator. + ap.build_apply_sql(proposal, ap.SERVICE_AGENT_HANDLE) + return apply_payload + + +def build_fresh_preflight( + proposal: dict[str, Any], + target_rows_before: dict[str, list[dict[str, Any]]], + *, + captured_at_utc: str | None = None, +) -> dict[str, Any]: + """Bind an approved bundle to proof that all payload-owned rows are absent.""" + captured = captured_at_utc or datetime.now(timezone.utc).isoformat() + if _parse_utc(captured) is None: + raise ValueError("fresh preflight captured_at_utc must be a timezone-aware timestamp") + apply_payload = _require_approved_bundle(proposal) + if set(target_rows_before) != set(ROW_TABLES): + raise ValueError("fresh preflight must contain every approve_claim target table") + malformed = [name for name, rows in target_rows_before.items() if not isinstance(rows, list)] + if malformed: + raise ValueError(f"fresh preflight target rows must be lists: {malformed}") + nonempty = {name: len(rows) for name, rows in target_rows_before.items() if rows} + if nonempty: + raise ValueError(f"fresh-owned rollback requires absent target rows: {nonempty}") + envelope = { + "proposal_id": str(proposal.get("id")), + "proposal_type": proposal.get("proposal_type"), + "proposal_payload_sha256": sha256_json(proposal.get("payload")), + "apply_payload_sha256": sha256_json(apply_payload), + "target_rows_before": target_rows_before, + } + artifact_envelope = { + "captured_at_utc": captured, + **envelope, + "fresh_owned_targets": True, + } + return { + "artifact": "proposal_apply_fresh_preflight", + "schema": FRESH_PREFLIGHT_SCHEMA, + **artifact_envelope, + "preflight_sha256": sha256_json(envelope), + "preflight_artifact_sha256": sha256_json(artifact_envelope), + } + + +def _require_fresh_preflight(proposal_before: dict[str, Any], preflight: dict[str, Any]) -> None: + apply_payload = _require_approved_bundle(proposal_before) + if not isinstance(preflight, dict): + raise ValueError("fresh preflight must be an object") + expected_keys = { + "artifact", + "schema", + "captured_at_utc", + "proposal_id", + "proposal_type", + "proposal_payload_sha256", + "apply_payload_sha256", + "target_rows_before", + "fresh_owned_targets", + "preflight_sha256", + "preflight_artifact_sha256", + } + if set(preflight) != expected_keys: + raise ValueError("fresh preflight contract fields are incomplete or unexpected") + if ( + preflight.get("artifact") != "proposal_apply_fresh_preflight" + or preflight.get("schema") != FRESH_PREFLIGHT_SCHEMA + or preflight.get("fresh_owned_targets") is not True + ): + raise ValueError("rollback requires an exact fresh-owned preflight contract") + target_rows = preflight.get("target_rows_before") + if ( + not isinstance(target_rows, dict) + or set(target_rows) != set(ROW_TABLES) + or any(not isinstance(rows, list) or rows for rows in target_rows.values()) + ): + raise ValueError("fresh preflight must prove every payload-owned target row was absent") + envelope = { + "proposal_id": str(proposal_before.get("id")), + "proposal_type": proposal_before.get("proposal_type"), + "proposal_payload_sha256": sha256_json(proposal_before.get("payload")), + "apply_payload_sha256": sha256_json(apply_payload), + "target_rows_before": target_rows, + } + if any(preflight.get(key) != value for key, value in envelope.items()): + raise ValueError("fresh preflight does not match the approved proposal") + if preflight.get("preflight_sha256") != sha256_json(envelope): + raise ValueError("fresh preflight hash is internally inconsistent") + artifact_envelope = { + "captured_at_utc": preflight.get("captured_at_utc"), + **envelope, + "fresh_owned_targets": True, + } + if _parse_utc(preflight.get("captured_at_utc")) is None: + raise ValueError("fresh preflight captured_at_utc is invalid") + if preflight.get("preflight_artifact_sha256") != sha256_json(artifact_envelope): + raise ValueError("fresh preflight artifact hash is internally inconsistent") + rebuilt = build_fresh_preflight( + proposal_before, + target_rows, + captured_at_utc=preflight.get("captured_at_utc"), + ) + if canonical_json(preflight) != canonical_json(rebuilt): + raise ValueError("fresh preflight contract is internally inconsistent") + + +def _require_apply_receipt( + proposal_before: dict[str, Any], + preflight: dict[str, Any], + apply_receipt: dict[str, Any], +) -> dict[str, list[dict[str, Any]]]: + _require_approved_bundle(proposal_before) + _require_fresh_preflight(proposal_before, preflight) + proposal_after = apply_receipt.get("proposal") + if ( + apply_receipt.get("artifact") != "kb_apply_replay_receipt" + or apply_receipt.get("receipt_contract_version") != replay_receipt.RECEIPT_CONTRACT_VERSION + or apply_receipt.get("replay_ready") is not True + ): + raise ValueError("rollback requires a replay-ready kb_apply receipt") + if not isinstance(proposal_after, dict) or proposal_after.get("status") != "applied": + raise ValueError("apply receipt proposal must be applied") + if proposal_after.get("id") != proposal_before.get("id"): + raise ValueError("apply receipt proposal id does not match preflight") + if proposal_after.get("payload") != proposal_before.get("payload"): + raise ValueError("apply receipt payload does not match preflight") + if not proposal_after.get("applied_at") or not proposal_after.get("applied_by_handle"): + raise ValueError("apply receipt requires applied_at and applied_by_handle") + rows = apply_receipt.get("canonical_rows") + if not isinstance(rows, dict) or set(rows) != set(ROW_TABLES): + raise ValueError("apply receipt must contain every approve_claim canonical row family") + for name, table_rows in rows.items(): + if not isinstance(table_rows, list) or any(not isinstance(row, dict) for row in table_rows): + raise ValueError(f"apply receipt {name} rows are malformed") + identifiers = [row.get("id") for row in table_rows] + if any(not identifier for identifier in identifiers) or len(set(identifiers)) != len(identifiers): + raise ValueError(f"apply receipt {name} rows require unique persisted ids") + try: + replay_receipt.validate_replay_receipt( + apply_receipt, + expected_proposal_id=str(proposal_before.get("id")), + ) + except ValueError as exc: + raise ValueError("apply receipt failed full replay-contract validation") from exc + proposal_id = str(proposal_before.get("id")) + for row in rows["claim_evidence"]: + expected_id = ap.deterministic_row_uuid( + proposal_id, + "claim_evidence", + row.get("claim_id"), + row.get("source_id"), + row.get("role"), + ) + if row.get("id") != expected_id: + raise ValueError("fresh apply receipt evidence id is not deterministic") + for row in rows["claim_edges"]: + expected_id = ap.deterministic_row_uuid( + proposal_id, + "claim_edge", + row.get("from_claim"), + row.get("to_claim"), + row.get("edge_type"), + ) + if row.get("id") != expected_id: + raise ValueError("fresh apply receipt edge id is not deterministic") + return rows + + +def _row_assertion(table: str, row: dict[str, Any], label: str) -> str: + row_json = ap.sql_literal(canonical_json(row)) + row_id = ap.sql_literal(row["id"]) + return f""" if (select count(*) from {table} t + where t.id = {row_id}::uuid and to_jsonb(t) = {row_json}::jsonb) <> 1 then + raise exception 'proposal rollback drift: {label} row does not match apply receipt'; + end if;""" + + +def _delete_statement(table: str, rows: list[dict[str, Any]], label: str) -> str: + if not rows: + return f" -- no {label} rows owned by this proposal" + identifiers = ", ".join(f"{ap.sql_literal(row['id'])}::uuid" for row in rows) + return f""" delete from {table} where id in ({identifiers}); + get diagnostics affected = row_count; + if affected <> {len(rows)} then + raise exception 'proposal rollback count mismatch for {label}: expected {len(rows)}, got %', affected; + end if;""" + + +def _require_approval_snapshot(proposal_before: dict[str, Any], approval_snapshot: dict[str, Any]) -> None: + expected_keys = { + "proposal_id", + "proposal_type", + "payload", + "reviewed_by_handle", + "reviewed_by_agent_id", + "reviewed_by_db_role", + "reviewed_at", + "review_note", + } + if not isinstance(approval_snapshot, dict) or set(approval_snapshot) != expected_keys: + raise ValueError("immutable approval snapshot contract is incomplete or unexpected") + reviewer_role = approval_snapshot.get("reviewed_by_db_role") + if not isinstance(reviewer_role, str) or not reviewer_role or reviewer_role == "kb_apply": + raise ValueError("immutable approval snapshot requires an independent review role") + expected = { + "proposal_id": proposal_before.get("id"), + "proposal_type": proposal_before.get("proposal_type"), + "payload": proposal_before.get("payload"), + "reviewed_by_handle": proposal_before.get("reviewed_by_handle"), + "reviewed_by_agent_id": proposal_before.get("reviewed_by_agent_id"), + "reviewed_by_db_role": reviewer_role, + "reviewed_at": proposal_before.get("reviewed_at"), + "review_note": proposal_before.get("review_note"), + } + if canonical_json(approval_snapshot) != canonical_json(expected): + raise ValueError("immutable approval snapshot does not match the approved proposal") + if approval_snapshot.get("reviewed_by_handle") == ap.SERVICE_AGENT_HANDLE: + raise ValueError("apply service cannot independently approve its own proposal") + + +def build_compensating_rollback_sql( + proposal_before: dict[str, Any], + approval_snapshot: dict[str, Any], + preflight: dict[str, Any], + apply_receipt: dict[str, Any], +) -> str: + """Build one atomic, drift-sensitive apply inverse for a fresh-owned bundle.""" + rows = _require_apply_receipt(proposal_before, preflight, apply_receipt) + _require_approval_snapshot(proposal_before, approval_snapshot) + + applied = apply_receipt["proposal"] + proposal_id = ap.sql_literal(proposal_before["id"]) + payload = ap.sql_literal(canonical_json(proposal_before["payload"])) + reviewed_by_handle = ap.sql_literal(proposal_before.get("reviewed_by_handle")) + reviewed_by_agent_id = ap.sql_literal(proposal_before.get("reviewed_by_agent_id")) + reviewed_at = ap.sql_literal(proposal_before.get("reviewed_at")) + review_note = ap.sql_literal(proposal_before.get("review_note")) + applied_by_handle = ap.sql_literal(applied.get("applied_by_handle")) + applied_by_agent_id = ap.sql_literal(applied.get("applied_by_agent_id")) + applied_at = ap.sql_literal(applied.get("applied_at")) + approval_json = ap.sql_literal(canonical_json(approval_snapshot)) + + row_assertions = [] + for family in DELETE_ORDER: + table = ROW_TABLES[family] + row_assertions.extend( + _row_assertion(table, row, f"{family}[{index}]") for index, row in enumerate(rows[family]) + ) + deletions = [_delete_statement(ROW_TABLES[family], rows[family], family) for family in DELETE_ORDER] + + return f"""begin; +set local standard_conforming_strings = on; +set local lock_timeout = '5s'; +select pg_advisory_xact_lock(hashtextextended('proposal-rollback:' || {proposal_id}, 0)); +do $rollback$ +declare + affected integer; + approval_now jsonb; +begin + perform 1 from kb_stage.kb_proposals + where id = {proposal_id}::uuid + for update; + if not found then + raise exception 'proposal rollback: proposal row missing'; + end if; + if not exists ( + select 1 from kb_stage.kb_proposals + where id = {proposal_id}::uuid + and proposal_type = 'approve_claim' + and status = 'applied' + and payload = {payload}::jsonb + and reviewed_by_handle is not distinct from {reviewed_by_handle} + and reviewed_by_agent_id is not distinct from {reviewed_by_agent_id}::uuid + and reviewed_at is not distinct from {reviewed_at}::timestamptz + and review_note is not distinct from {review_note} + and applied_by_handle is not distinct from {applied_by_handle} + and applied_by_agent_id is not distinct from {applied_by_agent_id}::uuid + and applied_at is not distinct from {applied_at}::timestamptz + ) then + raise exception 'proposal rollback: applied ledger does not match apply receipt'; + end if; + select jsonb_build_object( + 'proposal_id', proposal_id::text, + 'proposal_type', proposal_type, + 'payload', payload, + 'reviewed_by_handle', reviewed_by_handle, + 'reviewed_by_agent_id', reviewed_by_agent_id::text, + 'reviewed_by_db_role', reviewed_by_db_role::text, + 'reviewed_at', reviewed_at::text, + 'review_note', review_note + ) into approval_now + from kb_stage.kb_proposal_approvals + where proposal_id = {proposal_id}::uuid; + if approval_now is distinct from {approval_json}::jsonb then + raise exception 'proposal rollback: immutable approval snapshot drifted'; + end if; +{chr(10).join(row_assertions)} +{chr(10).join(deletions)} + update kb_stage.kb_proposals + set status = 'approved', + applied_by_handle = null, + applied_by_agent_id = null, + applied_at = null, + updated_at = now() + where id = {proposal_id}::uuid + and status = 'applied' + and payload = {payload}::jsonb + and applied_at is not distinct from {applied_at}::timestamptz; + get diagnostics affected = row_count; + if affected <> 1 then + raise exception 'proposal rollback: expected one applied ledger row, got %', affected; + end if; +end +$rollback$; +commit; +""" + + +def build_production_rollback_artifact( + *, + proposal_before: dict[str, Any], + approval_snapshot: dict[str, Any], + preflight: dict[str, Any], + apply_receipt: dict[str, Any], + destination: dict[str, Any], + generated_at_utc: str | None = None, +) -> dict[str, Any]: + """Build a private artifact whose rollback SQL is exactly reconstructable.""" + destination_keys = {"container", "database", "system_identifier", "server_version_num"} + if not isinstance(destination, dict) or set(destination) != destination_keys: + raise ValueError("production rollback artifact requires the exact destination identity") + if any(not destination.get(key) for key in destination_keys): + raise ValueError("production rollback artifact destination identity is incomplete") + generated = generated_at_utc or datetime.now(timezone.utc).isoformat() + if _parse_utc(generated) is None: + raise ValueError("production rollback artifact timestamp is invalid") + rollback_sql = build_compensating_rollback_sql( + proposal_before, + approval_snapshot, + preflight, + apply_receipt, + ) + private_material = { + "proposal_before": proposal_before, + "approval_snapshot": approval_snapshot, + "preflight": preflight, + "apply_receipt": apply_receipt, + } + artifact = { + "artifact": "proposal_apply_production_rollback_artifact", + "schema": PRODUCTION_ROLLBACK_ARTIFACT_SCHEMA, + "generated_at_utc": generated, + "destination": destination, + "proposal_id": str(proposal_before.get("id")), + "proposal_payload_sha256": sha256_json(proposal_before.get("payload")), + "approval_snapshot_sha256": sha256_json(approval_snapshot), + "fresh_preflight_artifact_sha256": preflight.get("preflight_artifact_sha256"), + "apply_receipt_sha256": sha256_json(apply_receipt), + "rollback_sql": rollback_sql, + "rollback_sql_sha256": sha256_text(rollback_sql), + "private_material": private_material, + "privacy": "private production rollback material; never commit or print", + } + artifact["artifact_sha256"] = sha256_json(artifact) + return artifact + + +def validate_production_rollback_artifact( + artifact: dict[str, Any], + *, + expected_destination: dict[str, Any] | None = None, +) -> dict[str, Any]: + expected_keys = { + "artifact", + "schema", + "generated_at_utc", + "destination", + "proposal_id", + "proposal_payload_sha256", + "approval_snapshot_sha256", + "fresh_preflight_artifact_sha256", + "apply_receipt_sha256", + "rollback_sql", + "rollback_sql_sha256", + "private_material", + "privacy", + "artifact_sha256", + } + if not isinstance(artifact, dict) or set(artifact) != expected_keys: + raise ValueError("production rollback artifact contract is incomplete or unexpected") + if ( + artifact.get("artifact") != "proposal_apply_production_rollback_artifact" + or artifact.get("schema") != PRODUCTION_ROLLBACK_ARTIFACT_SCHEMA + or artifact.get("privacy") != "private production rollback material; never commit or print" + or _parse_utc(artifact.get("generated_at_utc")) is None + ): + raise ValueError("production rollback artifact metadata is invalid") + unsigned = dict(artifact) + artifact_sha256 = unsigned.pop("artifact_sha256") + if not is_sha256(artifact_sha256) or sha256_json(unsigned) != artifact_sha256: + raise ValueError("production rollback artifact hash is internally inconsistent") + destination = artifact.get("destination") + destination_keys = {"container", "database", "system_identifier", "server_version_num"} + if not isinstance(destination, dict) or set(destination) != destination_keys: + raise ValueError("production rollback artifact destination is invalid") + if expected_destination is not None and canonical_json(destination) != canonical_json(expected_destination): + raise ValueError("production rollback artifact destination does not match authorization binding") + material = artifact.get("private_material") + material_keys = {"proposal_before", "approval_snapshot", "preflight", "apply_receipt"} + if not isinstance(material, dict) or set(material) != material_keys: + raise ValueError("production rollback artifact private material is incomplete") + expected_sql = build_compensating_rollback_sql( + material["proposal_before"], + material["approval_snapshot"], + material["preflight"], + material["apply_receipt"], + ) + expected_fields = { + "proposal_id": str(material["proposal_before"].get("id")), + "proposal_payload_sha256": sha256_json(material["proposal_before"].get("payload")), + "approval_snapshot_sha256": sha256_json(material["approval_snapshot"]), + "fresh_preflight_artifact_sha256": material["preflight"].get("preflight_artifact_sha256"), + "apply_receipt_sha256": sha256_json(material["apply_receipt"]), + "rollback_sql": expected_sql, + "rollback_sql_sha256": sha256_text(expected_sql), + } + if any(canonical_json(artifact.get(key)) != canonical_json(value) for key, value in expected_fields.items()): + raise ValueError("production rollback artifact does not match reconstructed exact SQL") + return artifact + + +def _row_ids(rows: dict[str, list[dict[str, Any]]]) -> dict[str, list[str]]: + return {name: sorted(str(row["id"]) for row in table_rows) for name, table_rows in rows.items()} + + +def build_lifecycle_receipt( + *, + proposal_before: dict[str, Any], + approval_before: dict[str, Any], + preflight: dict[str, Any], + apply_receipt: dict[str, Any], + rollback_sql: str, + proposal_after: dict[str, Any], + approval_after: dict[str, Any], + target_rows_after: dict[str, list[dict[str, Any]]], + counts_before: dict[str, int], + counts_after: dict[str, int], + unrelated_before: dict[str, Any], + unrelated_after: dict[str, Any], + rollback_replay_refused: bool, + generated_at_utc: str | None = None, +) -> dict[str, Any]: + rows = _require_apply_receipt(proposal_before, preflight, apply_receipt) + expected_rollback_sql = build_compensating_rollback_sql( + proposal_before, + approval_before, + preflight, + apply_receipt, + ) + target_rows_absent = set(target_rows_after) == set(ROW_TABLES) and all( + isinstance(value, list) and not value for value in target_rows_after.values() + ) + expected_after = dict(proposal_before) + expected_after.update( + { + "status": "approved", + "applied_by_handle": None, + "applied_by_agent_id": None, + "applied_at": None, + } + ) + ledger_restored = all(proposal_after.get(key) == value for key, value in expected_after.items()) + checks = { + "fresh_owned_targets": preflight.get("fresh_owned_targets") is True, + "apply_receipt_replay_ready": apply_receipt.get("replay_ready") is True, + "apply_rows_have_exact_ids": all(rows[name] == apply_receipt["canonical_rows"][name] for name in ROW_TABLES), + "rollback_target_rows_absent": target_rows_absent, + "rollback_counts_restored": counts_after == counts_before, + "rollback_ledger_restored_to_approved": ledger_restored, + "immutable_approval_unchanged": approval_after == approval_before, + "unrelated_rows_unchanged": unrelated_after == unrelated_before, + "rollback_replay_refused_without_mutation": rollback_replay_refused is True, + "rollback_sql_exact_for_bound_rows": rollback_sql == expected_rollback_sql, + } + proposal_projection = { + key: proposal_before.get(key) + for key in ( + "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", + ) + } + receipt = { + "artifact": "proposal_apply_lifecycle_receipt", + "schema": LIFECYCLE_SCHEMA, + "generated_at_utc": generated_at_utc or datetime.now(timezone.utc).isoformat(), + "required_tier": "T2_runtime", + "current_tier": "T2_runtime" if all(checks.values()) else "T1_model", + "proposal_before_apply": proposal_projection, + "proposal_payload_sha256": sha256_json(proposal_before.get("payload")), + "apply_payload_sha256": sha256_json(proposal_before["payload"]["apply_payload"]), + "approval_snapshot_sha256": sha256_json(approval_before), + "fresh_preflight_sha256": preflight["preflight_sha256"], + "fresh_preflight_artifact_sha256": preflight["preflight_artifact_sha256"], + "apply_receipt_sha256": sha256_json(apply_receipt), + "apply_sql_sha256": apply_receipt["apply_engine"]["apply_sql_sha256"], + "applied_row_ids": _row_ids(rows), + "applied_row_sha256": {name: [sha256_json(row) for row in table_rows] for name, table_rows in rows.items()}, + "rollback": { + "schema": ROLLBACK_SCHEMA, + "rollback_sql_sha256": sha256_text(rollback_sql), + "delete_order": list(DELETE_ORDER), + "target_rows_after": target_rows_after, + "counts_before_apply": counts_before, + "counts_after_rollback": counts_after, + "proposal_after": { + key: proposal_after.get(key) + for key in ( + "id", + "proposal_type", + "status", + "reviewed_by_handle", + "reviewed_by_agent_id", + "reviewed_at", + "review_note", + "applied_by_handle", + "applied_by_agent_id", + "applied_at", + ) + }, + }, + "checks": checks, + "pass": all(checks.values()), + "production_apply_executed": False, + "production_apply_authorization_present": False, + "strongest_claim_allowed": ( + "deterministic disposable-clone apply and compensating rollback" + if all(checks.values()) + else "local lifecycle evidence incomplete" + ), + } + receipt["lifecycle_receipt_sha256"] = sha256_json(receipt) + return receipt + + +def _require_lifecycle_receipt(lifecycle_receipt: dict[str, Any]) -> None: + expected_keys = { + "artifact", + "schema", + "generated_at_utc", + "required_tier", + "current_tier", + "proposal_before_apply", + "proposal_payload_sha256", + "apply_payload_sha256", + "approval_snapshot_sha256", + "fresh_preflight_sha256", + "fresh_preflight_artifact_sha256", + "apply_receipt_sha256", + "apply_sql_sha256", + "applied_row_ids", + "applied_row_sha256", + "rollback", + "checks", + "pass", + "production_apply_executed", + "production_apply_authorization_present", + "strongest_claim_allowed", + "lifecycle_receipt_sha256", + } + if not isinstance(lifecycle_receipt, dict) or set(lifecycle_receipt) != expected_keys: + raise ValueError("authorization packet requires the exact lifecycle receipt contract") + if ( + lifecycle_receipt.get("artifact") != "proposal_apply_lifecycle_receipt" + or lifecycle_receipt.get("schema") != LIFECYCLE_SCHEMA + or lifecycle_receipt.get("required_tier") != "T2_runtime" + or lifecycle_receipt.get("current_tier") != "T2_runtime" + or lifecycle_receipt.get("pass") is not True + or lifecycle_receipt.get("production_apply_executed") is not False + or lifecycle_receipt.get("production_apply_authorization_present") is not False + ): + raise ValueError("authorization packet requires a passing, non-production lifecycle receipt") + checks = lifecycle_receipt.get("checks") + if not isinstance(checks, dict) or not checks or any(value is not True for value in checks.values()): + raise ValueError("lifecycle receipt checks are incomplete or failed") + unsigned = dict(lifecycle_receipt) + receipt_sha256 = unsigned.pop("lifecycle_receipt_sha256") + if not is_sha256(receipt_sha256) or sha256_json(unsigned) != receipt_sha256: + raise ValueError("lifecycle receipt hash is internally inconsistent") + proposal = lifecycle_receipt.get("proposal_before_apply") + if not isinstance(proposal, dict) or proposal.get("status") != "approved": + raise ValueError("lifecycle receipt proposal is not approved before apply") + payload = proposal.get("payload") + apply_payload = payload.get("apply_payload") if isinstance(payload, dict) else None + if ( + not isinstance(apply_payload, dict) + or sha256_json(payload) != lifecycle_receipt.get("proposal_payload_sha256") + or sha256_json(apply_payload) != lifecycle_receipt.get("apply_payload_sha256") + ): + raise ValueError("lifecycle receipt proposal hashes are inconsistent") + hash_fields = ( + "approval_snapshot_sha256", + "fresh_preflight_sha256", + "fresh_preflight_artifact_sha256", + "apply_receipt_sha256", + "apply_sql_sha256", + ) + if any(not is_sha256(lifecycle_receipt.get(field)) for field in hash_fields): + raise ValueError("lifecycle receipt is missing an exact SHA-256 binding") + row_ids = lifecycle_receipt.get("applied_row_ids") + row_hashes = lifecycle_receipt.get("applied_row_sha256") + if ( + not isinstance(row_ids, dict) + or not isinstance(row_hashes, dict) + or set(row_ids) != set(ROW_TABLES) + or set(row_hashes) != set(ROW_TABLES) + or any(not isinstance(values, list) for values in row_ids.values()) + or any( + not isinstance(values, list) or any(not is_sha256(value) for value in values) + for values in row_hashes.values() + ) + or any(len(row_ids[name]) != len(row_hashes[name]) for name in ROW_TABLES) + ): + raise ValueError("lifecycle receipt row-level bindings are incomplete") + rollback = lifecycle_receipt.get("rollback") + if not isinstance(rollback, dict) or rollback.get("schema") != ROLLBACK_SCHEMA: + raise ValueError("lifecycle receipt rollback contract is missing") + if not is_sha256(rollback.get("rollback_sql_sha256")): + raise ValueError("lifecycle receipt rollback SQL hash is invalid") + target_rows_after = rollback.get("target_rows_after") + if ( + not isinstance(target_rows_after, dict) + or set(target_rows_after) != set(ROW_TABLES) + or any(not isinstance(rows, list) or rows for rows in target_rows_after.values()) + or rollback.get("counts_before_apply") != rollback.get("counts_after_rollback") + ): + raise ValueError("lifecycle receipt does not prove exact rollback cleanup") + + +def current_git_sha(repo_root: Path) -> str: + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=repo_root, + text=True, + capture_output=True, + check=False, + ) + return result.stdout.strip() if result.returncode == 0 else "" + + +def runtime_manifest_for_commit( + repo_git_sha: str, + *, + require_current_head: bool, +) -> dict[str, str]: + repo_root = HERE.parent.resolve() + if ( + not isinstance(repo_git_sha, str) + or len(repo_git_sha) != 40 + or any(character not in "0123456789abcdef" for character in repo_git_sha) + ): + raise ValueError("authorization runtime requires a full lowercase Git commit SHA") + head = current_git_sha(repo_root) + if require_current_head and head != repo_git_sha: + raise ValueError("authorization packet Git SHA must equal the current implementation HEAD") + ancestor = subprocess.run( + ["git", "merge-base", "--is-ancestor", repo_git_sha, head], + cwd=repo_root, + text=True, + capture_output=True, + check=False, + ) + if ancestor.returncode != 0: + raise ValueError("authorization runtime commit is not an ancestor of the current checkout") + status = subprocess.run( + ["git", "status", "--porcelain", "--", *RUNTIME_DEPENDENCIES], + cwd=repo_root, + text=True, + capture_output=True, + check=False, + ) + if status.returncode != 0 or status.stdout.strip(): + raise ValueError("authorization runtime dependencies are dirty or untracked") + manifest: dict[str, str] = {} + for relative in RUNTIME_DEPENDENCIES: + current_path = repo_root / relative + if not current_path.is_file(): + raise ValueError(f"authorization runtime dependency is missing: {relative}") + committed = subprocess.run( + ["git", "show", f"{repo_git_sha}:{relative}"], + cwd=repo_root, + capture_output=True, + check=False, + ) + if committed.returncode != 0: + raise ValueError(f"authorization runtime dependency is absent from the bound commit: {relative}") + committed_sha256 = hashlib.sha256(committed.stdout).hexdigest() + current_sha256 = sha256_file(current_path) + if committed_sha256 != current_sha256: + raise ValueError(f"authorization runtime dependency differs from the bound commit: {relative}") + manifest[relative] = current_sha256 + return manifest + + +def _authorization_text_template(binding: dict[str, Any], binding_sha256: str) -> str: + proposal = binding["proposal"] + destination = binding["destination"] + return ( + "I authorize one production DB apply for proposal " + f"{proposal['id']} with payload SHA-256 {proposal['proposal_payload_sha256']} " + f"to Docker container {destination['container']}, database {destination['database']}, " + f"system identifier {destination['system_identifier']}, binding SHA-256 {binding_sha256}. " + "I also authorize conditional production rollback SHA-256 " + " " + "only if the bound postflight fails and no downstream dependency exists. " + "Do not send Telegram, mutate another proposal, expose Cloud SQL, or promote GCP. " + "Operator ; received ; expires ." + ) + + +def build_authorization_packet( + lifecycle_receipt: dict[str, Any], + destination_readback: dict[str, Any], + *, + repo_git_sha: str, + apply_engine_path: Path, + approval_gate_path: Path = HERE / "kb_apply_prereqs.sql", + production_rollback_artifact: dict[str, Any] | None = None, + generated_at_utc: str | None = None, +) -> dict[str, Any]: + _require_lifecycle_receipt(lifecycle_receipt) + if not apply_engine_path.is_file(): + raise ValueError(f"apply engine file is missing: {apply_engine_path}") + if not approval_gate_path.is_file(): + raise ValueError(f"approval gate prerequisite file is missing: {approval_gate_path}") + apply_engine_relative = repo_relative_path(apply_engine_path) + approval_gate_relative = repo_relative_path(approval_gate_path) + if apply_engine_relative != "scripts/apply_proposal.py": + raise ValueError("authorization packet requires the canonical apply engine path") + if approval_gate_relative != "scripts/kb_apply_prereqs.sql": + raise ValueError("authorization packet requires the canonical approval-gate path") + runtime_manifest = runtime_manifest_for_commit(repo_git_sha, require_current_head=True) + required_destination = { + key: destination_readback.get(key) + for key in ("container", "database", "system_identifier", "server_version_num") + } + if any(not value for value in required_destination.values()): + raise ValueError("destination readback is missing exact production identity fields") + production_rollback_sha256: str | None = None + production_rollback_artifact_sha256: str | None = None + if production_rollback_artifact is not None: + validated_rollback = validate_production_rollback_artifact( + production_rollback_artifact, + expected_destination=required_destination, + ) + if ( + validated_rollback.get("proposal_id") != lifecycle_receipt["proposal_before_apply"].get("id") + or validated_rollback.get("proposal_payload_sha256") != lifecycle_receipt.get("proposal_payload_sha256") + or validated_rollback.get("approval_snapshot_sha256") != lifecycle_receipt.get("approval_snapshot_sha256") + ): + raise ValueError("production rollback artifact does not match the lifecycle proposal binding") + production_rollback_sha256 = validated_rollback["rollback_sql_sha256"] + production_rollback_artifact_sha256 = validated_rollback["artifact_sha256"] + proposal = lifecycle_receipt["proposal_before_apply"] + approval_gate = destination_readback.get("approval_gate") or {} + bound_gate = {check: approval_gate.get(check) for check in APPROVAL_GATE_CHECKS} + approval_gate_ready = all(bound_gate.get(check) is True for check in APPROVAL_GATE_CHECKS) + candidates = destination_readback.get("approved_strict_candidates") or [] + if not isinstance(candidates, list) or any(not isinstance(row, dict) for row in candidates): + raise ValueError("destination readback candidates must be row objects") + matching = [ + row + for row in candidates + if row.get("id") == proposal["id"] + and row.get("proposal_type") == proposal["proposal_type"] + and row.get("status") == "approved" + and row.get("applied_at") is None + and row.get("proposal_payload_sha256") == lifecycle_receipt["proposal_payload_sha256"] + and row.get("apply_payload_sha256") == lifecycle_receipt["apply_payload_sha256"] + and row.get("approval_snapshot_sha256") == lifecycle_receipt["approval_snapshot_sha256"] + and row.get("reviewed_by_handle") == proposal.get("reviewed_by_handle") + and row.get("reviewed_by_agent_id") == proposal.get("reviewed_by_agent_id") + and row.get("reviewed_at") == proposal.get("reviewed_at") + and row.get("review_note") == proposal.get("review_note") + and row.get("immutable_approval_exact") is True + ] + candidate_projection = ( + { + "id": matching[0].get("id"), + "proposal_type": matching[0].get("proposal_type"), + "status": matching[0].get("status"), + "applied_at": matching[0].get("applied_at"), + "proposal_payload_sha256": matching[0].get("proposal_payload_sha256"), + "apply_payload_sha256": matching[0].get("apply_payload_sha256"), + "approval_snapshot_sha256": matching[0].get("approval_snapshot_sha256"), + "reviewed_by_handle": matching[0].get("reviewed_by_handle"), + "reviewed_by_agent_id": matching[0].get("reviewed_by_agent_id"), + "reviewed_at": matching[0].get("reviewed_at"), + "review_note": matching[0].get("review_note"), + "immutable_approval_exact": matching[0].get("immutable_approval_exact"), + } + if len(matching) == 1 + else None + ) + observed_at_utc = destination_readback.get("observed_at_utc") + destination_readback_was_read_only = ( + destination_readback.get("read_only_transaction") is True and _parse_utc(observed_at_utc) is not None + ) + production_preconditions = { + "destination_readback_observed_at_utc": observed_at_utc, + "destination_readback_was_read_only": destination_readback_was_read_only, + "approval_gate_ready": approval_gate_ready, + "strict_proposal_present_and_approved": len(matching) == 1, + "matching_candidate_count": len(matching), + "exact_production_rollback_packet_ready": production_rollback_sha256 is not None, + "production_apply_authorization_present": False, + "production_apply_executed": False, + } + binding = { + "proposal": { + "id": proposal["id"], + "proposal_type": proposal["proposal_type"], + "required_status": "approved", + "required_applied_at": None, + "payload": proposal["payload"], + "reviewed_by_handle": proposal.get("reviewed_by_handle"), + "reviewed_by_agent_id": proposal.get("reviewed_by_agent_id"), + "reviewed_at": proposal.get("reviewed_at"), + "review_note": proposal.get("review_note"), + "proposal_payload_sha256": lifecycle_receipt["proposal_payload_sha256"], + "apply_payload_sha256": lifecycle_receipt["apply_payload_sha256"], + "approval_snapshot_sha256": lifecycle_receipt["approval_snapshot_sha256"], + }, + "destination": required_destination, + "destination_readback": { + "observed_at_utc": observed_at_utc, + "read_only_transaction": destination_readback.get("read_only_transaction"), + }, + "approval_gate": bound_gate, + "candidate_readback": { + "matching_candidate_count": len(matching), + "matching_candidate": candidate_projection, + }, + "approval_gate_dependency": { + "path": approval_gate_relative, + "sha256": sha256_file(approval_gate_path), + "requires_separate_production_authorization": True, + }, + "engine": { + "repo_git_sha": repo_git_sha, + "apply_engine_path": apply_engine_relative, + "apply_engine_sha256": sha256_file(apply_engine_path), + "apply_sql_sha256": lifecycle_receipt["apply_sql_sha256"], + "runtime_manifest": runtime_manifest, + }, + "rollback": { + "rollback_sql_sha256": lifecycle_receipt["rollback"]["rollback_sql_sha256"], + "lifecycle_receipt_sha256": lifecycle_receipt["lifecycle_receipt_sha256"], + "scope": "disposable_clone_rehearsal", + "production_executable": False, + "conditional_rollback_requires_explicit_authorization": True, + }, + "production_rollback": { + "status": "ready" if production_rollback_sha256 is not None else "not_generated", + "rollback_sql_sha256": production_rollback_sha256, + "artifact_schema": ( + PRODUCTION_ROLLBACK_ARTIFACT_SCHEMA if production_rollback_artifact_sha256 is not None else None + ), + "artifact_sha256": production_rollback_artifact_sha256, + "scope": "exact_production_destination_postapply_receipt", + "bound_for_action_time_authorization": production_rollback_artifact_sha256 is not None, + "conditional_rollback_requires_explicit_authorization": True, + }, + "expected_rows": { + "row_ids": lifecycle_receipt["applied_row_ids"], + "row_sha256": lifecycle_receipt["applied_row_sha256"], + }, + "production_preconditions": production_preconditions, + } + binding_sha256 = sha256_json(binding) + packet = { + "artifact": "proposal_apply_production_authorization_packet", + "schema": AUTHORIZATION_SCHEMA, + "generated_at_utc": generated_at_utc or datetime.now(timezone.utc).isoformat(), + "required_tier": "T6_authorized_production", + "current_tier": "T3_live_readonly", + "binding": binding, + "binding_sha256": binding_sha256, + "production_preconditions": dict(production_preconditions), + "authorization_record_required": { + "authorized": True, + "rollback_authorized": True, + "binding_sha256": binding_sha256, + "production_rollback_sql_sha256": (production_rollback_sha256 or ""), + "operator_identity": "", + "received_at_utc": "", + "expires_at_utc": "", + "authorization_text": "", + }, + "authorization_text_template": _authorization_text_template(binding, binding_sha256), + "safe_to_enter_apply_window": False, + "production_apply_authorization_present": False, + "production_apply_executed": False, + "scope_exclusions": { + "approval_gate_deployment_authorized": False, + "telegram_send_authorized": False, + "gcp_promotion_authorized": False, + "cloud_sql_exposure_authorized": False, + }, + "production_rollback_requirement": { + "status": "ready" if production_rollback_sha256 is not None else "not_generated", + "production_rollback_sql_sha256": production_rollback_sha256, + "production_rollback_artifact_schema": ( + PRODUCTION_ROLLBACK_ARTIFACT_SCHEMA if production_rollback_artifact_sha256 is not None else None + ), + "production_rollback_artifact_sha256": production_rollback_artifact_sha256, + "clone_rehearsal_rollback_sql_sha256": lifecycle_receipt["rollback"]["rollback_sql_sha256"], + "clone_rehearsal_is_not_production_rollback": True, + "required_before_apply_window": True, + "required_binding": ( + "an exact production rollback SQL SHA-256 in both the action-time record and authorization text" + ), + }, + "next_exact_action": ( + "refresh the exact destination through a read-only transaction" + if not destination_readback_was_read_only + else "deploy and independently verify the immutable approval gate before selecting a production proposal" + if not approval_gate_ready + else "stage this exact strict proposal in production and obtain independent review" + if not matching + else "generate and retain the exact production rollback SQL" + if production_rollback_sha256 is None + else "obtain the exact action-time authorization record" + ), + "claim_ceiling": ( + "T2 clone lifecycle plus T3 destination readback; no production apply authorization or execution" + ), + } + return packet + + +def expected_authorization_text(packet: dict[str, Any], record: dict[str, Any]) -> str: + template = packet.get("authorization_text_template") + if not isinstance(template, str): + raise ValueError("authorization packet has no text template") + return ( + template.replace( + "", + str(record.get("production_rollback_sql_sha256") or ""), + ) + .replace("", str(record.get("operator_identity") or "")) + .replace("", str(record.get("received_at_utc") or ""), 1) + .replace("", str(record.get("expires_at_utc") or ""), 1) + ) + + +def _parse_utc(value: Any) -> datetime | None: + if not isinstance(value, str) or not value: + return None + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + if parsed.tzinfo is None: + return None + return parsed.astimezone(timezone.utc) + + +def verify_authorization_record( + packet: dict[str, Any], + record: dict[str, Any], + *, + observed_destination: dict[str, Any], + apply_engine_path: Path, + approval_gate_path: Path = HERE / "kb_apply_prereqs.sql", + production_rollback_artifact: dict[str, Any] | None = None, + now: datetime | None = None, +) -> dict[str, Any]: + packet = packet if isinstance(packet, dict) else {} + record = record if isinstance(record, dict) else {} + observed_destination = observed_destination if isinstance(observed_destination, dict) else {} + now = (now or datetime.now(timezone.utc)).astimezone(timezone.utc) + received = _parse_utc(record.get("received_at_utc")) + expires = _parse_utc(record.get("expires_at_utc")) + binding = packet.get("binding") if isinstance(packet.get("binding"), dict) else {} + packet_preconditions = ( + packet.get("production_preconditions") if isinstance(packet.get("production_preconditions"), dict) else {} + ) + bound_preconditions = ( + binding.get("production_preconditions") if isinstance(binding.get("production_preconditions"), dict) else {} + ) + required_destination = binding.get("destination") if isinstance(binding.get("destination"), dict) else {} + destination_keys = {"container", "database", "system_identifier", "server_version_num"} + observed_destination_projection = {key: observed_destination.get(key) for key in destination_keys} + destination_matches = set(required_destination) == destination_keys and canonical_json( + observed_destination_projection + ) == canonical_json(required_destination) + bound_destination_readback = ( + binding.get("destination_readback") if isinstance(binding.get("destination_readback"), dict) else {} + ) + required_gate = binding.get("approval_gate") if isinstance(binding.get("approval_gate"), dict) else {} + observed_gate = ( + observed_destination.get("approval_gate") if isinstance(observed_destination.get("approval_gate"), dict) else {} + ) + required_gate_ready = set(required_gate) == set(APPROVAL_GATE_CHECKS) and all( + required_gate.get(key) is True for key in APPROVAL_GATE_CHECKS + ) + observed_gate_ready = all(observed_gate.get(key) is True for key in APPROVAL_GATE_CHECKS) + gate_matches = ( + required_gate_ready + and observed_gate_ready + and all(observed_gate.get(key) is required_gate.get(key) for key in APPROVAL_GATE_CHECKS) + ) + required_proposal = binding.get("proposal") if isinstance(binding.get("proposal"), dict) else {} + required_payload = required_proposal.get("payload") + required_apply_payload = required_payload.get("apply_payload") if isinstance(required_payload, dict) else None + proposal_binding_self_consistent = ( + required_proposal.get("required_status") == "approved" + and required_proposal.get("required_applied_at") is None + and isinstance(required_apply_payload, dict) + and sha256_json(required_payload) == required_proposal.get("proposal_payload_sha256") + and sha256_json(required_apply_payload) == required_proposal.get("apply_payload_sha256") + and is_sha256(required_proposal.get("approval_snapshot_sha256")) + ) + bound_candidate_readback = ( + binding.get("candidate_readback") if isinstance(binding.get("candidate_readback"), dict) else {} + ) + bound_candidate = bound_candidate_readback.get("matching_candidate") + expected_bound_candidate = { + "id": required_proposal.get("id"), + "proposal_type": required_proposal.get("proposal_type"), + "status": required_proposal.get("required_status"), + "applied_at": required_proposal.get("required_applied_at"), + "proposal_payload_sha256": required_proposal.get("proposal_payload_sha256"), + "apply_payload_sha256": required_proposal.get("apply_payload_sha256"), + "approval_snapshot_sha256": required_proposal.get("approval_snapshot_sha256"), + "reviewed_by_handle": required_proposal.get("reviewed_by_handle"), + "reviewed_by_agent_id": required_proposal.get("reviewed_by_agent_id"), + "reviewed_at": required_proposal.get("reviewed_at"), + "review_note": required_proposal.get("review_note"), + "immutable_approval_exact": True, + } + bound_candidate_exact = ( + bound_candidate_readback.get("matching_candidate_count") == 1 + and isinstance(bound_candidate, dict) + and canonical_json(bound_candidate) == canonical_json(expected_bound_candidate) + ) + observed_candidates = observed_destination.get("approved_strict_candidates") or [] + if not isinstance(observed_candidates, list) or any(not isinstance(row, dict) for row in observed_candidates): + observed_candidates = [] + matching_candidates = [ + candidate + for candidate in observed_candidates + if candidate.get("id") == required_proposal.get("id") + and candidate.get("proposal_type") == required_proposal.get("proposal_type") + and candidate.get("status") == required_proposal.get("required_status") + and candidate.get("applied_at") is required_proposal.get("required_applied_at") + and candidate.get("proposal_payload_sha256") == required_proposal.get("proposal_payload_sha256") + and candidate.get("apply_payload_sha256") == required_proposal.get("apply_payload_sha256") + and candidate.get("approval_snapshot_sha256") == required_proposal.get("approval_snapshot_sha256") + and candidate.get("reviewed_by_handle") == required_proposal.get("reviewed_by_handle") + and candidate.get("reviewed_by_agent_id") == required_proposal.get("reviewed_by_agent_id") + and candidate.get("reviewed_at") == required_proposal.get("reviewed_at") + and candidate.get("review_note") == required_proposal.get("review_note") + and candidate.get("immutable_approval_exact") is True + ] + production_rollback_sha256 = record.get("production_rollback_sql_sha256") + bound_production_rollback = ( + binding.get("production_rollback") if isinstance(binding.get("production_rollback"), dict) else {} + ) + bound_production_rollback_sha256 = bound_production_rollback.get("rollback_sql_sha256") + production_rollback_artifact_valid = False + if production_rollback_artifact is not None: + try: + validated_rollback = validate_production_rollback_artifact( + production_rollback_artifact, + expected_destination=required_destination, + ) + production_rollback_artifact_valid = ( + validated_rollback.get("artifact_sha256") == bound_production_rollback.get("artifact_sha256") + and validated_rollback.get("schema") == bound_production_rollback.get("artifact_schema") + and validated_rollback.get("rollback_sql_sha256") == bound_production_rollback_sha256 + and validated_rollback.get("proposal_id") == required_proposal.get("id") + and validated_rollback.get("proposal_payload_sha256") + == required_proposal.get("proposal_payload_sha256") + and validated_rollback.get("approval_snapshot_sha256") + == required_proposal.get("approval_snapshot_sha256") + ) + except ValueError: + production_rollback_artifact_valid = False + production_rollback_packet_ready = ( + bound_production_rollback.get("status") == "ready" + and bound_production_rollback.get("bound_for_action_time_authorization") is True + and bound_production_rollback.get("conditional_rollback_requires_explicit_authorization") is True + and bound_production_rollback.get("artifact_schema") == PRODUCTION_ROLLBACK_ARTIFACT_SCHEMA + and is_sha256(bound_production_rollback.get("artifact_sha256")) + and is_sha256(bound_production_rollback_sha256) + and production_rollback_artifact_valid + ) + packet_binding_sha256 = packet.get("binding_sha256") + binding_self_consistent = is_sha256(packet_binding_sha256) and sha256_json(binding) == packet_binding_sha256 + try: + expected_template = _authorization_text_template(binding, packet_binding_sha256) + except (KeyError, TypeError): + expected_template = "" + expected_scope_exclusions = { + "approval_gate_deployment_authorized": False, + "telegram_send_authorized": False, + "gcp_promotion_authorized": False, + "cloud_sql_exposure_authorized": False, + } + required_record_keys = { + "authorized", + "rollback_authorized", + "binding_sha256", + "production_rollback_sql_sha256", + "operator_identity", + "received_at_utc", + "expires_at_utc", + "authorization_text", + } + expected_record_template = { + "authorized": True, + "rollback_authorized": True, + "binding_sha256": packet_binding_sha256, + "production_rollback_sql_sha256": ( + bound_production_rollback_sha256 + if is_sha256(bound_production_rollback_sha256) + else "" + ), + "operator_identity": "", + "received_at_utc": "", + "expires_at_utc": "", + "authorization_text": "", + } + try: + expected_record_text = expected_authorization_text(packet, record) + except ValueError: + expected_record_text = "" + bound_engine = binding.get("engine") if isinstance(binding.get("engine"), dict) else {} + bound_gate_dependency = ( + binding.get("approval_gate_dependency") if isinstance(binding.get("approval_gate_dependency"), dict) else {} + ) + runtime_provenance_valid = False + try: + current_runtime_manifest = runtime_manifest_for_commit( + bound_engine.get("repo_git_sha"), + require_current_head=False, + ) + runtime_provenance_valid = canonical_json(current_runtime_manifest) == canonical_json( + bound_engine.get("runtime_manifest") + ) + except ValueError: + runtime_provenance_valid = False + try: + apply_engine_relative = repo_relative_path(apply_engine_path) + except ValueError: + apply_engine_relative = "" + try: + approval_gate_relative = repo_relative_path(approval_gate_path) + except ValueError: + approval_gate_relative = "" + action_observed_at = _parse_utc(observed_destination.get("observed_at_utc")) + action_readback_fresh = ( + received is not None + and action_observed_at is not None + and received <= action_observed_at <= now + and observed_destination.get("read_only_transaction") is True + ) + checks = { + "packet_schema_exact": ( + packet.get("artifact") == "proposal_apply_production_authorization_packet" + and packet.get("schema") == AUTHORIZATION_SCHEMA + and packet.get("required_tier") == "T6_authorized_production" + and packet.get("current_tier") == "T3_live_readonly" + and _parse_utc(packet.get("generated_at_utc")) is not None + ), + "packet_binding_sha256_self_consistent": binding_self_consistent, + "packet_preconditions_bound_exactly": ( + canonical_json(packet_preconditions) == canonical_json(bound_preconditions) + ), + "proposal_binding_self_consistent": proposal_binding_self_consistent, + "packet_destination_readback_was_read_only": ( + packet_preconditions.get("destination_readback_was_read_only") is True + and bound_destination_readback.get("read_only_transaction") is True + and _parse_utc(bound_destination_readback.get("observed_at_utc")) is not None + and bound_destination_readback.get("observed_at_utc") + == packet_preconditions.get("destination_readback_observed_at_utc") + ), + "approval_gate_ready_at_packet": ( + packet_preconditions.get("approval_gate_ready") is True and required_gate_ready + ), + "exact_production_rollback_packet_ready": ( + packet_preconditions.get("exact_production_rollback_packet_ready") is True + and production_rollback_packet_ready + ), + "strict_proposal_present_and_approved": ( + packet_preconditions.get("strict_proposal_present_and_approved") is True + and packet_preconditions.get("matching_candidate_count") == 1 + and bound_candidate_exact + ), + "packet_never_self_authorizes_or_expands_scope": ( + packet.get("safe_to_enter_apply_window") is False + and packet.get("production_apply_authorization_present") is False + and packet.get("production_apply_executed") is False + and packet_preconditions.get("production_apply_authorization_present") is False + and packet_preconditions.get("production_apply_executed") is False + and canonical_json(packet.get("scope_exclusions")) == canonical_json(expected_scope_exclusions) + ), + "authorization_text_template_exact": packet.get("authorization_text_template") == expected_template, + "authorization_record_template_exact": ( + canonical_json(packet.get("authorization_record_required")) == canonical_json(expected_record_template) + ), + "authorization_record_contract_exact": isinstance(record, dict) and set(record) == required_record_keys, + "authorized_is_exact_true": record.get("authorized") is True, + "rollback_authorized_is_exact_true": record.get("rollback_authorized") is True, + "production_rollback_sql_sha256_exact": ( + is_sha256(production_rollback_sha256) and production_rollback_sha256 == bound_production_rollback_sha256 + ), + "binding_sha256_exact": binding_self_consistent and record.get("binding_sha256") == packet_binding_sha256, + "operator_identity_present": ( + isinstance(record.get("operator_identity"), str) + and bool(record.get("operator_identity", "").strip()) + and not record.get("operator_identity", "").startswith("<") + ), + "authorization_window_valid": ( + received is not None and expires is not None and received <= now <= expires and received < expires + ), + "authorization_text_exact": bool(expected_record_text) + and record.get("authorization_text") == expected_record_text, + "destination_identity_exact": destination_matches, + "destination_readback_fresh_and_read_only_at_action": action_readback_fresh, + "approval_gate_identity_exact_at_action": gate_matches, + "strict_proposal_exact_at_action": len(matching_candidates) == 1, + "apply_engine_sha256_exact": ( + apply_engine_path.is_file() + and apply_engine_relative == bound_engine.get("apply_engine_path") + and sha256_file(apply_engine_path) == bound_engine.get("apply_engine_sha256") + ), + "runtime_commit_and_manifest_exact": runtime_provenance_valid, + "approval_gate_prerequisite_sha256_exact": ( + approval_gate_path.is_file() + and approval_gate_relative == bound_gate_dependency.get("path") + and sha256_file(approval_gate_path) == bound_gate_dependency.get("sha256") + ), + } + return { + "schema": "livingip.proposalApplyAuthorizationVerification.v1", + "checks": checks, + "safe_to_enter_apply_window": all(checks.values()), + "production_apply_authorization_present": all(checks.values()), + "production_apply_executed": False, + } + + +def _load_json(path: Path) -> dict[str, Any]: + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError(f"JSON file must contain an object: {path}") + return data + + +def _write_json(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def write_authorization_markdown(path: Path, packet: dict[str, Any]) -> None: + binding = packet["binding"] + proposal = binding["proposal"] + destination = binding["destination"] + engine = binding["engine"] + rollback = binding["rollback"] + production_rollback = binding["production_rollback"] + gate_dependency = binding["approval_gate_dependency"] + preconditions = packet["production_preconditions"] + lines = [ + "# Proposal Apply Production Authorization Packet", + "", + f"Generated UTC: `{packet['generated_at_utc']}`", + f"Required tier: `{packet['required_tier']}`", + f"Current tier: `{packet['current_tier']}`", + f"Safe to enter apply window: `{packet['safe_to_enter_apply_window']}`", + f"Production apply authorized: `{packet['production_apply_authorization_present']}`", + f"Production apply executed: `{packet['production_apply_executed']}`", + "", + "## Exact Binding", + "", + f"- Proposal: `{proposal['id']}` (`{proposal['proposal_type']}`)", + f"- Proposal payload SHA-256: `{proposal['proposal_payload_sha256']}`", + f"- Apply payload SHA-256: `{proposal['apply_payload_sha256']}`", + f"- Immutable approval SHA-256: `{proposal['approval_snapshot_sha256']}`", + f"- Destination: `{destination['container']}/{destination['database']}`", + f"- Destination system identifier: `{destination['system_identifier']}`", + f"- Apply engine SHA-256: `{engine['apply_engine_sha256']}`", + f"- Apply engine path: `{engine['apply_engine_path']}`", + f"- Apply SQL SHA-256: `{engine['apply_sql_sha256']}`", + f"- Clone rehearsal rollback SQL SHA-256: `{rollback['rollback_sql_sha256']}`", + f"- Production rollback status: `{production_rollback['status']}`", + f"- Production rollback SQL SHA-256: `{production_rollback['rollback_sql_sha256']}`", + f"- Approval gate prerequisite SHA-256: `{gate_dependency['sha256']}`", + f"- Approval gate prerequisite path: `{gate_dependency['path']}`", + f"- Binding SHA-256: `{packet['binding_sha256']}`", + "", + "## Production Preconditions", + "", + ] + lines.extend(f"- `{key}`: `{value}`" for key, value in sorted(preconditions.items())) + lines.extend(["", "## Approval Gate Readback", ""]) + lines.extend(f"- `{key}`: `{value}`" for key, value in sorted(binding["approval_gate"].items())) + lines.extend( + [ + "", + "## Exact Action-Time Authorization Text", + "", + "This text is not actionable until every production precondition above is true.", + "", + packet["authorization_text_template"], + "", + "## Current Gate", + "", + packet["next_exact_action"], + "", + "The production approval-gate deployment is explicitly outside this apply packet and requires separate authorization.", + "", + "## Claim Ceiling", + "", + packet["claim_ceiling"], + "", + ] + ) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines), encoding="utf-8") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + packet = sub.add_parser("build-authorization-packet") + packet.add_argument("--lifecycle-receipt", required=True, type=Path) + packet.add_argument("--destination-readback", required=True, type=Path) + packet.add_argument("--apply-engine", type=Path, default=HERE / "apply_proposal.py") + packet.add_argument("--approval-gate", type=Path, default=HERE / "kb_apply_prereqs.sql") + packet.add_argument("--production-rollback-artifact", type=Path) + packet.add_argument("--repo-git-sha") + packet.add_argument("--out", required=True, type=Path) + packet.add_argument("--markdown-out", type=Path) + verify = sub.add_parser("verify-authorization") + verify.add_argument("--packet", required=True, type=Path) + verify.add_argument("--record", required=True, type=Path) + verify.add_argument("--observed-destination", required=True, type=Path) + verify.add_argument("--apply-engine", type=Path, default=HERE / "apply_proposal.py") + verify.add_argument("--approval-gate", type=Path, default=HERE / "kb_apply_prereqs.sql") + verify.add_argument("--production-rollback-artifact", type=Path) + verify.add_argument("--out", required=True, type=Path) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + if args.command == "build-authorization-packet": + raw = _load_json(args.lifecycle_receipt) + lifecycle = raw.get("applied_rollback_receipt", raw) + destination = _load_json(args.destination_readback) + packet = build_authorization_packet( + lifecycle, + destination, + repo_git_sha=args.repo_git_sha or current_git_sha(HERE.parent), + apply_engine_path=args.apply_engine, + approval_gate_path=args.approval_gate, + production_rollback_artifact=( + _load_json(args.production_rollback_artifact) if args.production_rollback_artifact else None + ), + ) + _write_json(args.out, packet) + if args.markdown_out: + write_authorization_markdown(args.markdown_out, packet) + print(json.dumps({"out": str(args.out), "safe_to_enter_apply_window": False}, sort_keys=True)) + return 0 + packet = _load_json(args.packet) + record = _load_json(args.record) + destination = _load_json(args.observed_destination) + result = verify_authorization_record( + packet, + record, + observed_destination=destination, + apply_engine_path=args.apply_engine, + approval_gate_path=args.approval_gate, + production_rollback_artifact=( + _load_json(args.production_rollback_artifact) if args.production_rollback_artifact else None + ), + ) + _write_json(args.out, result) + print(json.dumps(result, sort_keys=True)) + return 0 if result["safe_to_enter_apply_window"] else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_approve_claim_clone_canary.py b/scripts/run_approve_claim_clone_canary.py index 6985401..87c4de2 100644 --- a/scripts/run_approve_claim_clone_canary.py +++ b/scripts/run_approve_claim_clone_canary.py @@ -18,6 +18,7 @@ import re import shutil import subprocess import sys +import tempfile import uuid from datetime import datetime, timezone from pathlib import Path @@ -28,13 +29,12 @@ REPO_ROOT = HERE.parent sys.path.insert(0, str(HERE)) import apply_proposal as ap # noqa: E402 +import proposal_apply_lifecycle as lifecycle # noqa: E402 SAFE_DB_RE = re.compile(r"^[a-z][a-z0-9_]{1,62}$") REVIEWER_HANDLE = "m3ta" REVIEW_NOTE = "Reviewed exact strict payload inside the disposable clone." -DEFAULT_REVIEW_SECRETS_FILE = ( - "/home/teleo/.hermes/profiles/leoclean/secrets/kb-review.env" -) +DEFAULT_REVIEW_SECRETS_FILE = "/home/teleo/.hermes/profiles/leoclean/secrets/kb-review.env" SECURITY_DEFINER_DEPENDENCIES = { "approve_strict_proposal": "kb_review", "assert_approved_proposal": "kb_apply", @@ -98,8 +98,10 @@ def _source_file_manifest(args: argparse.Namespace) -> list[dict[str, Any]]: paths = { Path(__file__).resolve(), (HERE / "run_approve_claim_isolated_container_canary.sh").resolve(), + (HERE / "finalize_approve_claim_isolated_canary.py").resolve(), Path(args.approve_script).resolve(), Path(args.apply_script).resolve(), + (HERE / "proposal_apply_lifecycle.py").resolve(), Path(args.prereqs).resolve(), } return sorted( @@ -190,9 +192,7 @@ def _schema_copy(container: str, source_db: str, clone_db: str) -> None: "ON_ERROR_STOP=1", "-q", ] - dump = subprocess.Popen( - dump_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True - ) + dump = subprocess.Popen(dump_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) assert dump.stdout is not None assert dump.stderr is not None restore = subprocess.run( @@ -319,37 +319,22 @@ def _cluster_roles_are_preprovisioned(rows: list[dict[str, Any]]) -> bool: def _clone_prerequisites_sql(prereqs: str) -> tuple[str, dict[str, Any]]: """Remove cluster-global role DDL while preserving clone-local objects/ACLs.""" start_marker = "do $roles$" - end_marker = ( - "alter role kb_apply login noinherit nosuperuser nocreatedb nocreaterole " - "noreplication nobypassrls;" - ) + end_marker = "alter role kb_apply login noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls;" start = prereqs.lower().find(start_marker) end_start = prereqs.lower().find(end_marker, start) if start < 0 or end_start < 0: - raise RuntimeError( - "prerequisites role bootstrap block changed; refusing unsafe clone execution" - ) + raise RuntimeError("prerequisites role bootstrap block changed; refusing unsafe clone execution") end = end_start + len(end_marker) removed = prereqs[start:end] - sanitized = ( - prereqs[:start] - + "-- Cluster-global role DDL omitted by disposable clone canary.\n" - + prereqs[end:] - ) + sanitized = prereqs[:start] + "-- Cluster-global role DDL omitted by disposable clone canary.\n" + prereqs[end:] if re.search(r"\b(?:create|alter|drop)\s+role\b", sanitized, re.IGNORECASE): - raise RuntimeError( - "prerequisites still contain cluster-global role DDL after sanitization" - ) + raise RuntimeError("prerequisites still contain cluster-global role DDL after sanitization") return sanitized, { "source_sha256": hashlib.sha256(prereqs.encode()).hexdigest(), "clone_sql_sha256": hashlib.sha256(sanitized.encode()).hexdigest(), "cluster_role_ddl_removed": True, - "removed_create_role_statements": len( - re.findall(r"\bcreate\s+role\b", removed, re.IGNORECASE) - ), - "removed_alter_role_statements": len( - re.findall(r"\balter\s+role\b", removed, re.IGNORECASE) - ), + "removed_create_role_statements": len(re.findall(r"\bcreate\s+role\b", removed, re.IGNORECASE)), + "removed_alter_role_statements": len(re.findall(r"\balter\s+role\b", removed, re.IGNORECASE)), } @@ -460,9 +445,7 @@ def load_normalized_fixture(path: Path, namespace: str) -> dict[str, Any]: data = json.loads(path.read_text(encoding="utf-8")) previews = data.get("normalization_previews") if isinstance(data, dict) else None if not isinstance(previews, list) or len(previews) != 1: - raise ValueError( - "normalization JSON must contain exactly one normalization_previews row" - ) + raise ValueError("normalization JSON must contain exactly one normalization_previews row") preview = previews[0] children = preview.get("strict_child_proposals") if ( @@ -472,9 +455,7 @@ def load_normalized_fixture(path: Path, namespace: str) -> dict[str, Any]: or len(children) != 1 or children[0].get("proposal_type") != "approve_claim" ): - raise ValueError( - "normalization JSON is not one unblocked strict approve_claim child" - ) + raise ValueError("normalization JSON is not one unblocked strict approve_claim child") apply_payload = (children[0].get("payload") or {}).get("apply_payload") if not isinstance(apply_payload, dict): raise ValueError("strict approve_claim child has no apply_payload") @@ -527,9 +508,7 @@ def _payload_agent_ids(payload: dict[str, Any]) -> list[str]: return sorted(values) -def _seed_payload_agents( - container: str, source_db: str, clone_db: str, payload: dict[str, Any] -) -> dict[str, Any]: +def _seed_payload_agents(container: str, source_db: str, clone_db: str, payload: dict[str, Any]) -> dict[str, Any]: required_ids = _payload_agent_ids(payload) if not required_ids: return { @@ -538,9 +517,7 @@ def _seed_payload_agents( "clone_rows": [], "exact_clone_copy": True, } - id_sql = ", ".join( - f"{ap.sql_literal(value)}::uuid" for value in required_ids - ) + id_sql = ", ".join(f"{ap.sql_literal(value)}::uuid" for value in required_ids) source_raw = _psql( container, source_db, @@ -560,8 +537,8 @@ rollback; statements = [ f"""insert into public.agents (id, handle, kind) -values ({ap.sql_literal(row['id'])}::uuid, {ap.sql_literal(row['handle'])}, - {ap.sql_literal(row['kind'])}) +values ({ap.sql_literal(row["id"])}::uuid, {ap.sql_literal(row["handle"])}, + {ap.sql_literal(row["kind"])}) on conflict (id) do nothing;""" for row in source_rows ] @@ -604,16 +581,14 @@ rollback; ) source_row = json.loads(raw) if not source_row: - raise RuntimeError( - f"reviewer principal dependency is missing from source agents: {reviewer_handle}" - ) + raise RuntimeError(f"reviewer principal dependency is missing from source agents: {reviewer_handle}") _psql( container, clone_db, f"""insert into public.agents (id, handle, kind) -values ({ap.sql_literal(source_row['id'])}::uuid, - {ap.sql_literal(source_row['handle'])}, - {ap.sql_literal(source_row['kind'])});""", +values ({ap.sql_literal(source_row["id"])}::uuid, + {ap.sql_literal(source_row["handle"])}, + {ap.sql_literal(source_row["kind"])});""", tuples=False, ) clone_raw = _psql( @@ -635,9 +610,7 @@ from public.agents where handle = {ap.sql_literal(reviewer_handle)};""", } -def _seed_external_claims( - container: str, source_db: str, clone_db: str, payload: dict[str, Any] -) -> dict[str, Any]: +def _seed_external_claims(container: str, source_db: str, clone_db: str, payload: dict[str, Any]) -> dict[str, Any]: external_ids = _external_claim_ids(payload) if not external_ids: return { @@ -646,9 +619,7 @@ def _seed_external_claims( "clone_rows": [], "exact_clone_copy": True, } - id_sql = ", ".join( - f"{ap.sql_literal(value)}::uuid" for value in external_ids - ) + id_sql = ", ".join(f"{ap.sql_literal(value)}::uuid" for value in external_ids) raw = _psql( container, source_db, @@ -666,28 +637,22 @@ rollback; found = {row["id"] for row in rows} missing = sorted(set(external_ids) - found) if missing: - raise RuntimeError( - f"normalized bundle references missing source claims: {missing}" - ) + raise RuntimeError(f"normalized bundle references missing source claims: {missing}") statements = [] for row in rows: tags = row.get("tags") or [] tags_sql = ( - "'{}'::text[]" - if not tags - else "array[" - + ", ".join(ap.sql_literal(tag) for tag in tags) - + "]::text[]" + "'{}'::text[]" if not tags else "array[" + ", ".join(ap.sql_literal(tag) for tag in tags) + "]::text[]" ) statements.append( f"""insert into public.claims (id, type, text, status, confidence, tags, created_by, superseded_by) values - ({ap.sql_literal(row['id'])}::uuid, {ap.sql_literal(row['type'])}, - {ap.sql_literal(row['text'])}, {ap.sql_literal(row['status'])}, - {ap.sql_literal(row.get('confidence'))}, {tags_sql}, - {ap.sql_literal(row.get('created_by'))}::uuid, - {ap.sql_literal(row.get('superseded_by'))}::uuid);""" + ({ap.sql_literal(row["id"])}::uuid, {ap.sql_literal(row["type"])}, + {ap.sql_literal(row["text"])}, {ap.sql_literal(row["status"])}, + {ap.sql_literal(row.get("confidence"))}, {tags_sql}, + {ap.sql_literal(row.get("created_by"))}::uuid, + {ap.sql_literal(row.get("superseded_by"))}::uuid);""" ) _psql(container, clone_db, "\n".join(statements), tuples=False) clone_raw = _psql( @@ -712,9 +677,7 @@ from public.claims where id in ({id_sql});""", def _id_predicate(column: str, values: list[str]) -> str: if not values: return "false" - return f"{column} in (" + ", ".join( - f"{ap.sql_literal(value)}::uuid" for value in values - ) + ")" + return f"{column} in (" + ", ".join(f"{ap.sql_literal(value)}::uuid" for value in values) + ")" def _key_predicate(rows: list[dict[str, Any]], fields: list[tuple[str, str]]) -> str: @@ -829,8 +792,7 @@ def _expected_table_deltas( def _table_deltas(before: dict[str, int], after: dict[str, int]) -> dict[str, int]: if set(before) != set(after): raise ValueError( - "cannot compare table counts with different keys: " - f"before={sorted(before)} after={sorted(after)}" + f"cannot compare table counts with different keys: before={sorted(before)} after={sorted(after)}" ) return {key: after[key] - before[key] for key in sorted(before)} @@ -862,9 +824,7 @@ def _canonical_row_keys(rows: dict[str, list[dict[str, Any]]]) -> dict[str, Any] ], key=lambda row: (row["from_claim"], row["to_claim"], row["edge_type"]), ), - "public.reasoning_tools": sorted( - row["id"] for row in rows.get("reasoning_tools", []) - ), + "public.reasoning_tools": sorted(row["id"] for row in rows.get("reasoning_tools", [])), } @@ -879,8 +839,7 @@ def _strict_receipt( expected_keys = _canonical_row_keys(expected_rows) actual_keys = _canonical_row_keys(actual_rows) checks = { - "proposal_id_exact": applied.get("id") - == result.get("fixture_ids", {}).get("proposal_id"), + "proposal_id_exact": applied.get("id") == result.get("fixture_ids", {}).get("proposal_id"), "proposal_status_applied": applied.get("status") == "applied", "proposal_applied_at_present": bool(applied.get("applied_at")), "canonical_rows_exact": actual_rows == expected_rows, @@ -889,18 +848,12 @@ def _strict_receipt( isinstance(result.get("clone_counts_before_apply"), dict) and isinstance(result.get("clone_counts_after_apply"), dict) ), - "table_deltas_exact": result.get("clone_apply_table_deltas") - == expected_table_deltas, - "conflict_transaction_rolled_back": result.get("checks", {}).get( - "conflict_transaction_rolled_back_exactly" - ) + "table_deltas_exact": result.get("clone_apply_table_deltas") == expected_table_deltas, + "conflict_transaction_rolled_back": result.get("checks", {}).get("conflict_transaction_rolled_back_exactly") is True, "clone_database_removed": ( result.get("clone_dropped") is True - and result.get("cleanup_readback", {}).get( - "leftover_clone_database_count" - ) - == 0 + and result.get("cleanup_readback", {}).get("leftover_clone_database_count") == 0 ), "source_database_unchanged": result.get("source_database_mutated") is False, } @@ -925,28 +878,18 @@ def _strict_receipt( "actual_deltas": result.get("clone_apply_table_deltas"), }, "rollback_cleanup": { - "conflict_transaction_rolled_back": checks[ - "conflict_transaction_rolled_back" - ], + "conflict_transaction_rolled_back": checks["conflict_transaction_rolled_back"], "clone_database_removed": checks["clone_database_removed"], - "leftover_clone_database_count": result.get("cleanup_readback", {}).get( - "leftover_clone_database_count" - ), + "leftover_clone_database_count": result.get("cleanup_readback", {}).get("leftover_clone_database_count"), "source_database_unchanged": checks["source_database_unchanged"], }, } -def _exact_bundle_readback( - container: str, database: str, payload: dict[str, Any] -) -> dict[str, list[dict[str, Any]]]: +def _exact_bundle_readback(container: str, database: str, payload: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: expected = _expected_bundle_rows(payload) - claim_predicate = _id_predicate( - "id", [row["id"] for row in expected["claims"]] - ) - source_predicate = _id_predicate( - "id", [row["id"] for row in expected["sources"]] - ) + claim_predicate = _id_predicate("id", [row["id"] for row in expected["claims"]]) + source_predicate = _id_predicate("id", [row["id"] for row in expected["sources"]]) evidence_predicate = _key_predicate( expected["claim_evidence"], [("claim_id", "uuid"), ("source_id", "uuid"), ("role", "text")], @@ -959,9 +902,7 @@ def _exact_bundle_readback( ("edge_type", "text"), ], ) - tool_predicate = _id_predicate( - "id", [row["id"] for row in expected["reasoning_tools"]] - ) + tool_predicate = _id_predicate("id", [row["id"] for row in expected["reasoning_tools"]]) raw = _psql( container, database, @@ -1001,9 +942,53 @@ rollback; return json.loads(raw) -def _proposal_readback( - container: str, database: str, proposal_id: str -) -> dict[str, Any] | None: +def _unrelated_state_fingerprint( + container: str, + database: str, + payload: dict[str, Any], + proposal_id: str, +) -> dict[str, Any]: + """Hash every potentially adjacent row while excluding proposal-owned rows.""" + expected = _expected_bundle_rows(payload) + claim_predicate = _id_predicate("id", [row["id"] for row in expected["claims"]]) + source_predicate = _id_predicate("id", [row["id"] for row in expected["sources"]]) + evidence_predicate = _key_predicate( + expected["claim_evidence"], + [("claim_id", "uuid"), ("source_id", "uuid"), ("role", "text")], + ) + edge_predicate = _key_predicate( + expected["claim_edges"], + [("from_claim", "uuid"), ("to_claim", "uuid"), ("edge_type", "text")], + ) + tool_predicate = _id_predicate("id", [row["id"] for row in expected["reasoning_tools"]]) + + def fingerprint(table: str, alias: str, where: str, order: str) -> str: + return f"""(select json_build_object( + 'row_count', count(*), + 'content_md5', md5(coalesce(string_agg(to_jsonb({alias})::text, '|' order by {order}), '')) + ) from {table} {alias} where {where})""" + + raw = _psql( + container, + database, + f"""begin transaction read only; +select json_build_object( + 'public.agents', {fingerprint("public.agents", "a", "true", "a.id::text")}, + 'public.claims', {fingerprint("public.claims", "c", f"not ({claim_predicate})", "c.id::text")}, + 'public.sources', {fingerprint("public.sources", "s", f"not ({source_predicate})", "s.id::text")}, + 'public.claim_evidence', {fingerprint("public.claim_evidence", "ce", f"not ({evidence_predicate})", "ce.id::text")}, + 'public.claim_edges', {fingerprint("public.claim_edges", "e", f"not ({edge_predicate})", "e.id::text")}, + 'public.reasoning_tools', {fingerprint("public.reasoning_tools", "rt", f"not ({tool_predicate})", "rt.id::text")}, + 'kb_stage.kb_proposals', {fingerprint("kb_stage.kb_proposals", "p", f"p.id <> {ap.sql_literal(proposal_id)}::uuid", "p.id::text")}, + 'kb_stage.kb_proposal_approvals', {fingerprint("kb_stage.kb_proposal_approvals", "pa", f"pa.proposal_id <> {ap.sql_literal(proposal_id)}::uuid", "pa.proposal_id::text")} +)::text; +rollback; +""", + ) + return json.loads(raw) + + +def _proposal_readback(container: str, database: str, proposal_id: str) -> dict[str, Any] | None: raw = _psql( container, database, @@ -1031,9 +1016,7 @@ rollback; return json.loads(raw) -def _approval_snapshot_readback( - container: str, database: str, proposal_id: str -) -> dict[str, Any] | None: +def _approval_snapshot_readback(container: str, database: str, proposal_id: str) -> dict[str, Any] | None: raw = _psql( container, database, @@ -1069,7 +1052,7 @@ values ({ap.sql_literal(proposal_id)}::uuid, 'approve_claim', 'pending_review', 'clone_canary', {ap.sql_literal(source_ref)}, 'Exercise strict canonical bundle review and apply lifecycle.', - {ap.sql_literal(json.dumps({'apply_payload': payload}, sort_keys=True))}::jsonb); + {ap.sql_literal(json.dumps({"apply_payload": payload}, sort_keys=True))}::jsonb); """ _psql(container, database, sql) @@ -1111,6 +1094,7 @@ def _apply( database: str, *, dry_run: bool = False, + receipt_out: Path | None = None, ) -> subprocess.CompletedProcess[str]: command = [ sys.executable, @@ -1129,15 +1113,37 @@ def _apply( "--role", args.apply_role, ] + if receipt_out is not None: + command.extend(["--receipt-out", str(receipt_out)]) if dry_run: command.append("--dry-run") return _run(command, check=False) +def _apply_command_readback( + result: subprocess.CompletedProcess[str], +) -> dict[str, Any]: + """Keep the public report useful without leaking a private receipt path.""" + readback: dict[str, Any] = { + "returncode": result.returncode, + "stderr": result.stderr.strip(), + } + try: + summary = json.loads(result.stdout) + except json.JSONDecodeError: + readback["stdout"] = result.stdout.strip() + else: + if isinstance(summary, dict): + summary.pop("receipt_path", None) + readback["result"] = summary + else: + readback["stdout"] = result.stdout.strip() + return readback + + def _security_definer_readback(container: str, database: str) -> dict[str, Any]: expected_values = ",\n".join( - f"({ap.sql_literal(name)}, {ap.sql_literal(role)}, " - f"{ap.sql_literal(SECURITY_DEFINER_ARGUMENT_TYPES[name])})" + f"({ap.sql_literal(name)}, {ap.sql_literal(role)}, {ap.sql_literal(SECURITY_DEFINER_ARGUMENT_TYPES[name])})" for name, role in SECURITY_DEFINER_DEPENDENCIES.items() ) raw = _psql( @@ -1270,8 +1276,7 @@ def _security_dependencies_ready(readback: dict[str, Any]) -> bool: functions = readback.get("functions") or [] functions_ready = len(functions) == len(SECURITY_DEFINER_DEPENDENCIES) and all( row.get("present") is True - and row.get("argument_types") - == SECURITY_DEFINER_ARGUMENT_TYPES[row["function"].removeprefix("kb_stage.")] + and row.get("argument_types") == SECURITY_DEFINER_ARGUMENT_TYPES[row["function"].removeprefix("kb_stage.")] and row.get("security_definer") is True and row.get("expected_role_execute") is True and row.get("other_runtime_role_execute") is False @@ -1285,23 +1290,26 @@ def _security_dependencies_ready(readback: dict[str, Any]) -> bool: and readback.get("protected_role_memberships") == [] and readback.get("login_role_owned_objects") == [] ) - return functions_ready and catalog_ready and privileges == { - "kb_review_proposal_select": True, - "kb_review_proposal_update": False, - "kb_apply_proposal_select": True, - "kb_apply_proposal_update": False, - "kb_apply_claim_evidence_insert": True, - "kb_apply_claim_evidence_update": False, - "kb_review_approval_snapshot_select": False, - "kb_review_approval_snapshot_update": False, - "kb_apply_approval_snapshot_select": False, - "kb_apply_approval_snapshot_update": False, - } + return ( + functions_ready + and catalog_ready + and privileges + == { + "kb_review_proposal_select": True, + "kb_review_proposal_update": False, + "kb_apply_proposal_select": True, + "kb_apply_proposal_update": False, + "kb_apply_claim_evidence_insert": True, + "kb_apply_claim_evidence_update": False, + "kb_review_approval_snapshot_select": False, + "kb_review_approval_snapshot_update": False, + "kb_apply_approval_snapshot_select": False, + "kb_apply_approval_snapshot_update": False, + } + ) -def _approval_transition_valid( - pending: dict[str, Any] | None, approved: dict[str, Any] | None -) -> bool: +def _approval_transition_valid(pending: dict[str, Any] | None, approved: dict[str, Any] | None) -> bool: if not pending or not approved: return False immutable = [ @@ -1337,9 +1345,7 @@ def _approval_transition_valid( ) -def _apply_transition_valid( - approved: dict[str, Any] | None, applied: dict[str, Any] | None -) -> bool: +def _apply_transition_valid(approved: dict[str, Any] | None, applied: dict[str, Any] | None) -> bool: if not approved or not applied: return False preserved = [ @@ -1382,9 +1388,7 @@ def _approval_snapshot_matches( } -def _seed_evidence_permission_probe( - container: str, database: str, fixture: dict[str, Any] -) -> None: +def _seed_evidence_permission_probe(container: str, database: str, fixture: dict[str, Any]) -> None: source_hash = f"approve-claim-permission-probe-{fixture['permission_source_id']}" _psql( container, @@ -1392,20 +1396,20 @@ def _seed_evidence_permission_probe( f"""insert into public.claims (id, type, text, status, confidence, tags, created_by, superseded_by) values - ({ap.sql_literal(fixture['permission_claim_id'])}::uuid, 'structural', + ({ap.sql_literal(fixture["permission_claim_id"])}::uuid, 'structural', 'Existing claim_evidence permission probe.', 'open', 0.42, array['clone-canary-permission-probe']::text[], null, null); insert into public.sources (id, source_type, url, storage_path, excerpt, hash, created_by) values - ({ap.sql_literal(fixture['permission_source_id'])}::uuid, 'observation', null, + ({ap.sql_literal(fixture["permission_source_id"])}::uuid, 'observation', null, null, 'Existing claim_evidence permission probe source.', {ap.sql_literal(source_hash)}, null); insert into public.claim_evidence (claim_id, source_id, role, weight, created_by) values - ({ap.sql_literal(fixture['permission_claim_id'])}::uuid, - {ap.sql_literal(fixture['permission_source_id'])}::uuid, + ({ap.sql_literal(fixture["permission_claim_id"])}::uuid, + {ap.sql_literal(fixture["permission_source_id"])}::uuid, 'grounds', 0.42, null); """, tuples=False, @@ -1426,8 +1430,8 @@ select coalesce((select json_build_object( 'weight', weight, 'created_by', created_by::text ) from public.claim_evidence -where claim_id = {ap.sql_literal(fixture['permission_claim_id'])}::uuid - and source_id = {ap.sql_literal(fixture['permission_source_id'])}::uuid +where claim_id = {ap.sql_literal(fixture["permission_claim_id"])}::uuid + and source_id = {ap.sql_literal(fixture["permission_source_id"])}::uuid and role = 'grounds'), 'null'::json)::text; rollback; """, @@ -1482,10 +1486,7 @@ def _conflict_payload(fixture: dict[str, Any]) -> dict[str, Any]: "url": None, "storage_path": None, "excerpt": "Conflicting source id.", - "hash": ( - f"approve-claim-permission-probe-" - f"{fixture['permission_source_id']}" - ), + "hash": (f"approve-claim-permission-probe-{fixture['permission_source_id']}"), "created_by": None, } ], @@ -1497,18 +1498,14 @@ def _conflict_payload(fixture: dict[str, Any]) -> dict[str, Any]: def run_canary(args: argparse.Namespace) -> dict[str, Any]: if args.review_role != "kb_review" or args.apply_role != "kb_apply": - raise ValueError( - "the canary is pinned to the intended kb_review/kb_apply authority split" - ) + raise ValueError("the canary is pinned to the intended kb_review/kb_apply authority split") suffix = uuid.uuid4().hex[:10] clone_db = f"{args.database_prefix}_{suffix}" if not SAFE_DB_RE.match(clone_db): raise ValueError(f"unsafe generated database name: {clone_db}") fixture = ( - load_normalized_fixture(args.normalization_json, suffix) - if args.normalization_json - else build_fixture(suffix) + load_normalized_fixture(args.normalization_json, suffix) if args.normalization_json else build_fixture(suffix) ) payload = fixture["apply_payload"] expected_rows = _expected_bundle_rows(payload) @@ -1526,33 +1523,19 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: "service_restarted": False, "clone_created": False, "clone_dropped": False, - "fixture_ids": { - key: value for key, value in fixture.items() if key != "apply_payload" - }, + "fixture_ids": {key: value for key, value in fixture.items() if key != "apply_payload"}, "dependencies": { - "approve_script": Path(args.approve_script) - .resolve() - .relative_to(REPO_ROOT.resolve()) - .as_posix(), - "apply_script": Path(args.apply_script) - .resolve() - .relative_to(REPO_ROOT.resolve()) - .as_posix(), - "prerequisites_sql": Path(args.prereqs) - .resolve() - .relative_to(REPO_ROOT.resolve()) - .as_posix(), + "approve_script": Path(args.approve_script).resolve().relative_to(REPO_ROOT.resolve()).as_posix(), + "apply_script": Path(args.apply_script).resolve().relative_to(REPO_ROOT.resolve()).as_posix(), + "prerequisites_sql": Path(args.prereqs).resolve().relative_to(REPO_ROOT.resolve()).as_posix(), "review_role": args.review_role, "apply_role": args.apply_role, "gate_owner_role": "kb_gate_owner", "reviewer_handle": REVIEWER_HANDLE, "immutable_approval_table": "kb_stage.kb_proposal_approvals", - "security_definer_functions": [ - f"kb_stage.{name}" for name in SECURITY_DEFINER_DEPENDENCIES - ], + "security_definer_functions": [f"kb_stage.{name}" for name in SECURITY_DEFINER_DEPENDENCIES], "security_definer_argument_types": { - f"kb_stage.{name}": argument_types - for name, argument_types in SECURITY_DEFINER_ARGUMENT_TYPES.items() + f"kb_stage.{name}": argument_types for name, argument_types in SECURITY_DEFINER_ARGUMENT_TYPES.items() }, "credential_files": { "review": { @@ -1579,15 +1562,9 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: } try: result["service_before"] = _service_state(args.service) - result["source_counts_before"] = _base_counts( - args.container, args.source_db - ) - result["source_cluster_roles_before"] = _cluster_role_readback( - args.container, args.source_db - ) - if not _cluster_roles_are_preprovisioned( - result["source_cluster_roles_before"] - ): + result["source_counts_before"] = _base_counts(args.container, args.source_db) + result["source_cluster_roles_before"] = _cluster_role_readback(args.container, args.source_db) + if not _cluster_roles_are_preprovisioned(result["source_cluster_roles_before"]): raise RuntimeError( "refusing clone prerequisites until kb_apply, kb_review, and " "NOLOGIN kb_gate_owner are pre-provisioned without elevated attributes" @@ -1598,13 +1575,8 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: if not readback["present_at_start"] ] if missing_credentials: - raise RuntimeError( - "required separated credential file(s) are absent: " - + ", ".join(missing_credentials) - ) - result["source_target_rows_before"] = _exact_bundle_readback( - args.container, args.source_db, payload - ) + raise RuntimeError("required separated credential file(s) are absent: " + ", ".join(missing_credentials)) + result["source_target_rows_before"] = _exact_bundle_readback(args.container, args.source_db, payload) prereqs = Path(args.prereqs).read_text(encoding="utf-8") clone_prereqs, prereq_sanitization = _clone_prerequisites_sql(prereqs) result["clone_prerequisite_sanitization"] = prereq_sanitization @@ -1620,19 +1592,11 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: ) _psql(args.container, clone_db, clone_prereqs, tuples=False) - result["security_definer_readback"] = _security_definer_readback( - args.container, clone_db - ) - result["payload_agent_seed"] = _seed_payload_agents( - args.container, args.source_db, clone_db, payload - ) - result["external_claim_seed"] = _seed_external_claims( - args.container, args.source_db, clone_db, payload - ) + result["security_definer_readback"] = _security_definer_readback(args.container, clone_db) + result["payload_agent_seed"] = _seed_payload_agents(args.container, args.source_db, clone_db, payload) + result["external_claim_seed"] = _seed_external_claims(args.container, args.source_db, clone_db, payload) _seed_evidence_permission_probe(args.container, clone_db, fixture) - evidence_before = _evidence_permission_probe_readback( - args.container, clone_db, fixture - ) + evidence_before = _evidence_permission_probe_readback(args.container, clone_db, fixture) apply_password = ap.load_password(args.secrets_file) evidence_update = _permission_probe( @@ -1640,15 +1604,13 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: clone_db, f"""update public.claim_evidence set weight = 0.99 - where claim_id = {ap.sql_literal(fixture['permission_claim_id'])}::uuid - and source_id = {ap.sql_literal(fixture['permission_source_id'])}::uuid + where claim_id = {ap.sql_literal(fixture["permission_claim_id"])}::uuid + and source_id = {ap.sql_literal(fixture["permission_source_id"])}::uuid and role = 'grounds';""", role=args.apply_role, password=apply_password, ) - evidence_after = _evidence_permission_probe_readback( - args.container, clone_db, fixture - ) + evidence_after = _evidence_permission_probe_readback(args.container, clone_db, fixture) result["kb_apply_claim_evidence_update_probe"] = { "before": evidence_before, "attempt": evidence_update, @@ -1663,41 +1625,25 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: payload, source_ref="working-leo:approve-claim-clone-canary", ) - pending_proposal = _proposal_readback( - args.container, clone_db, fixture["proposal_id"] - ) - pending_approval_snapshot = _approval_snapshot_readback( - args.container, clone_db, fixture["proposal_id"] - ) - review_dry_run = _approve( - args, fixture["proposal_id"], clone_db, dry_run=True - ) + pending_proposal = _proposal_readback(args.container, clone_db, fixture["proposal_id"]) + pending_approval_snapshot = _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]) + review_dry_run = _approve(args, fixture["proposal_id"], clone_db, dry_run=True) review_apply = _approve(args, fixture["proposal_id"], clone_db) - approved_proposal = _proposal_readback( - args.container, clone_db, fixture["proposal_id"] - ) - approved_snapshot = _approval_snapshot_readback( - args.container, clone_db, fixture["proposal_id"] - ) + approved_proposal = _proposal_readback(args.container, clone_db, fixture["proposal_id"]) + approved_snapshot = _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]) result["review_transition"] = { "pending_readback": pending_proposal, "pending_immutable_approval": pending_approval_snapshot, "review_dry_run": { "returncode": review_dry_run.returncode, - "uses_approve_strict_proposal": ( - "kb_stage.approve_strict_proposal" in review_dry_run.stdout - ), + "uses_approve_strict_proposal": ("kb_stage.approve_strict_proposal" in review_dry_run.stdout), "stderr": review_dry_run.stderr.strip(), }, "review_apply": _command_readback(review_apply), "approved_readback": approved_proposal, "immutable_approval_readback": approved_snapshot, - "immutable_approval_matches_ledger": _approval_snapshot_matches( - approved_proposal, approved_snapshot - ), - "exact_transition": _approval_transition_valid( - pending_proposal, approved_proposal - ), + "immutable_approval_matches_ledger": _approval_snapshot_matches(approved_proposal, approved_snapshot), + "exact_transition": _approval_transition_valid(pending_proposal, approved_proposal), } approval_update = _permission_probe( @@ -1705,7 +1651,7 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: clone_db, f"""update kb_stage.kb_proposals set status = 'pending_review', review_note = 'kb_apply direct mutation probe' - where id = {ap.sql_literal(fixture['proposal_id'])}::uuid;""", + where id = {ap.sql_literal(fixture["proposal_id"])}::uuid;""", role=args.apply_role, password=apply_password, ) @@ -1714,7 +1660,7 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: clone_db, f"""update kb_stage.kb_proposals set payload = payload || '{{"kb_apply_direct_mutation_probe": true}}'::jsonb - where id = {ap.sql_literal(fixture['proposal_id'])}::uuid;""", + where id = {ap.sql_literal(fixture["proposal_id"])}::uuid;""", role=args.apply_role, password=apply_password, ) @@ -1723,16 +1669,12 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: clone_db, f"""update kb_stage.kb_proposal_approvals set payload = payload || '{{"kb_apply_direct_mutation_probe": true}}'::jsonb - where proposal_id = {ap.sql_literal(fixture['proposal_id'])}::uuid;""", + where proposal_id = {ap.sql_literal(fixture["proposal_id"])}::uuid;""", role=args.apply_role, password=apply_password, ) - proposal_after_permission_probes = _proposal_readback( - args.container, clone_db, fixture["proposal_id"] - ) - snapshot_after_permission_probes = _approval_snapshot_readback( - args.container, clone_db, fixture["proposal_id"] - ) + proposal_after_permission_probes = _proposal_readback(args.container, clone_db, fixture["proposal_id"]) + snapshot_after_permission_probes = _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]) result["kb_apply_proposal_mutation_probes"] = { "approval_fields": approval_update, "payload": payload_update, @@ -1741,36 +1683,25 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: "after": proposal_after_permission_probes, "immutable_approval_before": approved_snapshot, "immutable_approval_after": snapshot_after_permission_probes, - "exact_row_unchanged": ( - approved_proposal == proposal_after_permission_probes - ), - "immutable_approval_exactly_unchanged": ( - approved_snapshot == snapshot_after_permission_probes - ), + "exact_row_unchanged": (approved_proposal == proposal_after_permission_probes), + "immutable_approval_exactly_unchanged": (approved_snapshot == snapshot_after_permission_probes), } built_apply = _apply(args, fixture["proposal_id"], clone_db, dry_run=True) built_sql = built_apply.stdout if built_apply.returncode != 0 or not built_sql.strip(): - raise RuntimeError( - "failed to build apply SQL before mutation: " - f"{built_apply.stderr.strip()}" - ) + raise RuntimeError(f"failed to build apply SQL before mutation: {built_apply.stderr.strip()}") _psql( args.container, clone_db, f"""update kb_stage.kb_proposals set payload = payload || '{{"mutation_after_build": true}}'::jsonb, updated_at = now() - where id = {ap.sql_literal(fixture['proposal_id'])}::uuid;""", + where id = {ap.sql_literal(fixture["proposal_id"])}::uuid;""", tuples=False, ) - proposal_after_mutation = _proposal_readback( - args.container, clone_db, fixture["proposal_id"] - ) - snapshot_after_ledger_mutation = _approval_snapshot_readback( - args.container, clone_db, fixture["proposal_id"] - ) + proposal_after_mutation = _proposal_readback(args.container, clone_db, fixture["proposal_id"]) + snapshot_after_ledger_mutation = _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]) stale_execute = _role_psql( args.container, clone_db, @@ -1778,58 +1709,38 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: role=args.apply_role, password=apply_password, ) - rows_after_stale_execute = _exact_bundle_readback( - args.container, clone_db, payload - ) - proposal_after_stale_execute = _proposal_readback( - args.container, clone_db, fixture["proposal_id"] - ) - snapshot_after_stale_execute = _approval_snapshot_readback( - args.container, clone_db, fixture["proposal_id"] - ) + rows_after_stale_execute = _exact_bundle_readback(args.container, clone_db, payload) + proposal_after_stale_execute = _proposal_readback(args.container, clone_db, fixture["proposal_id"]) + snapshot_after_stale_execute = _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]) assert approved_proposal is not None _psql( args.container, clone_db, f"""update kb_stage.kb_proposals - set payload = {ap.sql_literal(json.dumps(approved_proposal['payload'], sort_keys=True))}::jsonb, + set payload = {ap.sql_literal(json.dumps(approved_proposal["payload"], sort_keys=True))}::jsonb, updated_at = now() - where id = {ap.sql_literal(fixture['proposal_id'])}::uuid;""", + where id = {ap.sql_literal(fixture["proposal_id"])}::uuid;""", tuples=False, ) - restored_proposal = _proposal_readback( - args.container, clone_db, fixture["proposal_id"] - ) - restored_snapshot = _approval_snapshot_readback( - args.container, clone_db, fixture["proposal_id"] - ) + restored_proposal = _proposal_readback(args.container, clone_db, fixture["proposal_id"]) + restored_snapshot = _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]) result["mutation_between_build_and_execute"] = { "build_returncode": built_apply.returncode, "built_sql_sha256": hashlib.sha256(built_sql.encode()).hexdigest(), - "built_sql_uses_assert_approved_proposal": ( - "kb_stage.assert_approved_proposal" in built_sql - ), - "built_sql_uses_finish_approved_proposal": ( - "kb_stage.finish_approved_proposal" in built_sql - ), + "built_sql_uses_assert_approved_proposal": ("kb_stage.assert_approved_proposal" in built_sql), + "built_sql_uses_finish_approved_proposal": ("kb_stage.finish_approved_proposal" in built_sql), "approved_before_mutation": approved_proposal, "immutable_approval_before_mutation": approved_snapshot, "mutated_readback": proposal_after_mutation, - "immutable_approval_after_ledger_mutation": ( - snapshot_after_ledger_mutation - ), + "immutable_approval_after_ledger_mutation": (snapshot_after_ledger_mutation), "stale_execute": _command_readback(stale_execute), "rows_after_stale_execute": rows_after_stale_execute, "proposal_after_stale_execute": proposal_after_stale_execute, - "immutable_approval_after_stale_execute": ( - snapshot_after_stale_execute - ), + "immutable_approval_after_stale_execute": (snapshot_after_stale_execute), "restored_readback": restored_proposal, "restored_immutable_approval": restored_snapshot, "stale_apply_refused": stale_execute.returncode != 0, - "canonical_rows_unchanged": ( - rows_after_stale_execute == _empty_bundle_readback(payload) - ), + "canonical_rows_unchanged": (rows_after_stale_execute == _empty_bundle_readback(payload)), "approval_restored_exactly": restored_proposal == approved_proposal, "immutable_approval_never_changed": all( snapshot == approved_snapshot @@ -1841,27 +1752,41 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: ), } - result["clone_counts_before_apply"] = _base_counts( - args.container, clone_db + if approved_proposal is None or approved_snapshot is None: + raise RuntimeError("approved ledger and immutable approval are required") + result["target_rows_before_apply"] = _exact_bundle_readback(args.container, clone_db, payload) + fresh_preflight = lifecycle.build_fresh_preflight( + approved_proposal, + result["target_rows_before_apply"], ) - first_apply = _apply(args, fixture["proposal_id"], clone_db) - result["first_apply"] = _command_readback(first_apply) - result["row_readback"] = _exact_bundle_readback( - args.container, clone_db, payload - ) - result["clone_counts_after_apply"] = _base_counts( - args.container, clone_db + result["fresh_owned_preflight"] = fresh_preflight + result["unrelated_fingerprint_before_apply"] = _unrelated_state_fingerprint( + args.container, clone_db, payload, fixture["proposal_id"] ) + result["clone_counts_before_apply"] = _base_counts(args.container, clone_db) + with tempfile.TemporaryDirectory(prefix="leo-proposal-apply-receipt-") as receipt_dir: + private_receipt_path = Path(receipt_dir) / "apply-receipt.json" + first_apply = _apply( + args, + fixture["proposal_id"], + clone_db, + receipt_out=private_receipt_path, + ) + result["first_apply"] = _apply_command_readback(first_apply) + if first_apply.returncode != 0 or not private_receipt_path.is_file(): + raise RuntimeError( + "first apply did not produce its explicit private receipt: " + f"returncode={first_apply.returncode} stderr={first_apply.stderr.strip()}" + ) + apply_receipt = json.loads(private_receipt_path.read_text(encoding="utf-8")) + result["row_readback"] = _exact_bundle_readback(args.container, clone_db, payload) + result["clone_counts_after_apply"] = _base_counts(args.container, clone_db) result["clone_apply_table_deltas"] = _table_deltas( result["clone_counts_before_apply"], result["clone_counts_after_apply"], ) - applied_proposal = _proposal_readback( - args.container, clone_db, fixture["proposal_id"] - ) - applied_snapshot = _approval_snapshot_readback( - args.container, clone_db, fixture["proposal_id"] - ) + applied_proposal = _proposal_readback(args.container, clone_db, fixture["proposal_id"]) + applied_snapshot = _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]) result["apply_transition"] = { "approved_readback": approved_proposal, "applied_readback": applied_proposal, @@ -1869,20 +1794,93 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: "immutable_approval_matches_applied_ledger": ( _approval_snapshot_matches(applied_proposal, applied_snapshot) ), - "exact_transition": _apply_transition_valid( - approved_proposal, applied_proposal - ), + "exact_transition": _apply_transition_valid(approved_proposal, applied_proposal), } replay = _apply(args, fixture["proposal_id"], clone_db) result["idempotent_replay"] = { "returncode": replay.returncode, - "already_applied_refusal": ( - "already applied" in (replay.stdout + replay.stderr).lower() - ), + "already_applied_refusal": ("already applied" in (replay.stdout + replay.stderr).lower()), "stderr": replay.stderr.strip(), } + rollback_sql = lifecycle.build_compensating_rollback_sql( + approved_proposal, + approved_snapshot, + fresh_preflight, + apply_receipt, + ) + result["rollback_plan"] = { + "schema": lifecycle.ROLLBACK_SCHEMA, + "rollback_sql_sha256": lifecycle.sha256_text(rollback_sql), + "execution_authority": "disposable_clone_admin_only", + } + _psql(args.container, clone_db, rollback_sql, tuples=False) + rollback_proposal = _proposal_readback(args.container, clone_db, fixture["proposal_id"]) + rollback_approval = _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]) + rollback_target_rows = _exact_bundle_readback(args.container, clone_db, payload) + rollback_counts = _base_counts(args.container, clone_db) + rollback_unrelated = _unrelated_state_fingerprint(args.container, clone_db, payload, fixture["proposal_id"]) + + rollback_replay = _run( + [ + "docker", + "exec", + "-i", + args.container, + "psql", + "-U", + "postgres", + "-d", + clone_db, + "-v", + "ON_ERROR_STOP=1", + ], + input_text=rollback_sql, + check=False, + ) + rollback_replay_state = { + "proposal": _proposal_readback(args.container, clone_db, fixture["proposal_id"]), + "approval": _approval_snapshot_readback(args.container, clone_db, fixture["proposal_id"]), + "target_rows": _exact_bundle_readback(args.container, clone_db, payload), + "counts": _base_counts(args.container, clone_db), + "unrelated": _unrelated_state_fingerprint(args.container, clone_db, payload, fixture["proposal_id"]), + } + rollback_replay_state_unchanged = rollback_replay_state == { + "proposal": rollback_proposal, + "approval": rollback_approval, + "target_rows": rollback_target_rows, + "counts": rollback_counts, + "unrelated": rollback_unrelated, + } + result["rollback_replay"] = { + "returncode": rollback_replay.returncode, + "refused": rollback_replay.returncode != 0, + "state_unchanged": rollback_replay_state_unchanged, + "stderr": rollback_replay.stderr.strip(), + } + lifecycle_receipt = lifecycle.build_lifecycle_receipt( + proposal_before=approved_proposal, + approval_before=approved_snapshot, + preflight=fresh_preflight, + apply_receipt=apply_receipt, + rollback_sql=rollback_sql, + proposal_after=rollback_proposal or {}, + approval_after=rollback_approval or {}, + target_rows_after=rollback_target_rows, + counts_before=result["clone_counts_before_apply"], + counts_after=rollback_counts, + unrelated_before=result["unrelated_fingerprint_before_apply"], + unrelated_after=rollback_unrelated, + rollback_replay_refused=(rollback_replay.returncode != 0 and rollback_replay_state_unchanged), + ) + result["applied_rollback_receipt"] = lifecycle_receipt + if args.lifecycle_receipt_output: + lifecycle._write_json(args.lifecycle_receipt_output, lifecycle_receipt) + if args.rollback_sql_output: + args.rollback_sql_output.parent.mkdir(parents=True, exist_ok=True) + args.rollback_sql_output.write_text(rollback_sql, encoding="utf-8") + _stage_proposal( args.container, clone_db, @@ -1890,29 +1888,19 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: conflict_payload, source_ref="working-leo:approve-claim-conflict-canary", ) - conflict_pending = _proposal_readback( - args.container, clone_db, fixture["conflict_proposal_id"] - ) + conflict_pending = _proposal_readback(args.container, clone_db, fixture["conflict_proposal_id"]) conflict_pending_snapshot = _approval_snapshot_readback( args.container, clone_db, fixture["conflict_proposal_id"] ) conflict_review = _approve(args, fixture["conflict_proposal_id"], clone_db) - conflict_approved = _proposal_readback( - args.container, clone_db, fixture["conflict_proposal_id"] - ) + conflict_approved = _proposal_readback(args.container, clone_db, fixture["conflict_proposal_id"]) conflict_approved_snapshot = _approval_snapshot_readback( args.container, clone_db, fixture["conflict_proposal_id"] ) conflict = _apply(args, fixture["conflict_proposal_id"], clone_db) - conflict_after = _proposal_readback( - args.container, clone_db, fixture["conflict_proposal_id"] - ) - conflict_snapshot_after = _approval_snapshot_readback( - args.container, clone_db, fixture["conflict_proposal_id"] - ) - conflict_rows = _exact_bundle_readback( - args.container, clone_db, conflict_payload - ) + conflict_after = _proposal_readback(args.container, clone_db, fixture["conflict_proposal_id"]) + conflict_snapshot_after = _approval_snapshot_readback(args.container, clone_db, fixture["conflict_proposal_id"]) + conflict_rows = _exact_bundle_readback(args.container, clone_db, conflict_payload) result["conflict_review_transition"] = { "pending_readback": conflict_pending, "pending_immutable_approval": conflict_pending_snapshot, @@ -1922,15 +1910,11 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: "immutable_approval_matches_ledger": _approval_snapshot_matches( conflict_approved, conflict_approved_snapshot ), - "exact_transition": _approval_transition_valid( - conflict_pending, conflict_approved - ), + "exact_transition": _approval_transition_valid(conflict_pending, conflict_approved), } result["conflict_apply"] = { "returncode": conflict.returncode, - "source_hash_collision_refusal": ( - "source hash collision" in (conflict.stdout + conflict.stderr).lower() - ), + "source_hash_collision_refusal": ("source hash collision" in (conflict.stdout + conflict.stderr).lower()), "stderr": conflict.stderr.strip(), } result["conflict_readback"] = { @@ -1938,19 +1922,13 @@ def run_canary(args: argparse.Namespace) -> dict[str, Any]: "immutable_approval": conflict_snapshot_after, "canonical_rows": conflict_rows, "proposal_approval_unchanged": conflict_after == conflict_approved, - "immutable_approval_unchanged": ( - conflict_snapshot_after == conflict_approved_snapshot - ), - "canonical_rows_absent": ( - conflict_rows == _empty_bundle_readback(conflict_payload) - ), + "immutable_approval_unchanged": (conflict_snapshot_after == conflict_approved_snapshot), + "canonical_rows_absent": (conflict_rows == _empty_bundle_readback(conflict_payload)), } except Exception as exc: result["error"] = f"{type(exc).__name__}: {exc}" finally: - cleanup_readback: dict[str, Any] = { - "drop_attempted": bool(result["clone_created"]) - } + cleanup_readback: dict[str, Any] = {"drop_attempted": bool(result["clone_created"])} if result["clone_created"]: cleanup_sql = f"""select pg_terminate_backend(pid) from pg_stat_activity @@ -1985,46 +1963,32 @@ drop database if exists {clone_db}; ) or "0" ) - cleanup_readback["leftover_clone_database_count"] = result[ - "leftover_clone_count" - ] + cleanup_readback["leftover_clone_database_count"] = result["leftover_clone_count"] result["cleanup_readback"] = cleanup_readback - result["source_counts_after"] = _base_counts( - args.container, args.source_db - ) + result["source_counts_after"] = _base_counts(args.container, args.source_db) if "source_counts_before" in result: result["source_table_deltas"] = _table_deltas( result["source_counts_before"], result["source_counts_after"], ) - result["source_cluster_roles_after"] = _cluster_role_readback( - args.container, args.source_db - ) - result["source_target_rows_after"] = _exact_bundle_readback( - args.container, args.source_db, payload - ) + result["source_cluster_roles_after"] = _cluster_role_readback(args.container, args.source_db) + result["source_target_rows_after"] = _exact_bundle_readback(args.container, args.source_db, payload) result["service_after"] = _service_state(args.service) try: source_files_after = _source_file_manifest(args) result["source_binding"]["post_run_files"] = source_files_after - result["source_binding"]["unchanged_during_run"] = ( - source_files_after == source_files_before - ) + result["source_binding"]["unchanged_during_run"] = source_files_after == source_files_before except Exception as exc: - result["source_binding"]["post_run_error"] = ( - f"{type(exc).__name__}: {exc}" - ) + result["source_binding"]["post_run_error"] = f"{type(exc).__name__}: {exc}" gateway_process_observable = ( result.get("service_before", {}).get("available") is True and result.get("service_after", {}).get("available") is True ) gateway_process_unchanged = ( - result.get("service_before", {}).get("MainPID") - == result.get("service_after", {}).get("MainPID") - and result.get("service_before", {}).get("NRestarts") - == result.get("service_after", {}).get("NRestarts") + result.get("service_before", {}).get("MainPID") == result.get("service_after", {}).get("MainPID") + and result.get("service_before", {}).get("NRestarts") == result.get("service_after", {}).get("NRestarts") if gateway_process_observable else None ) @@ -2033,75 +1997,39 @@ drop database if exists {clone_db}; result.get("security_definer_readback") or {} ), "clone_prerequisites_omit_cluster_role_ddl": ( - result.get("clone_prerequisite_sanitization", {}).get( - "cluster_role_ddl_removed" - ) - is True - ), - "reviewer_agent_seeded_exactly": ( - result.get("reviewer_agent_seed", {}).get("exact_clone_copy") is True - ), - "payload_agents_seeded_exactly": ( - result.get("payload_agent_seed", {}).get("exact_clone_copy") is True + result.get("clone_prerequisite_sanitization", {}).get("cluster_role_ddl_removed") is True ), + "reviewer_agent_seeded_exactly": (result.get("reviewer_agent_seed", {}).get("exact_clone_copy") is True), + "payload_agents_seeded_exactly": (result.get("payload_agent_seed", {}).get("exact_clone_copy") is True), "kb_review_approval_succeeded": ( - result.get("review_transition", {}) - .get("review_apply", {}) - .get("returncode") - == 0 + result.get("review_transition", {}).get("review_apply", {}).get("returncode") == 0 ), "kb_review_used_security_definer": ( - result.get("review_transition", {}) - .get("review_dry_run", {}) - .get("uses_approve_strict_proposal") - is True + result.get("review_transition", {}).get("review_dry_run", {}).get("uses_approve_strict_proposal") is True ), "review_transition_exact": ( result.get("review_transition", {}).get("exact_transition") is True - and result.get("review_transition", {}).get( - "immutable_approval_matches_ledger" - ) - is True + and result.get("review_transition", {}).get("immutable_approval_matches_ledger") is True ), "kb_apply_cannot_mutate_proposal_approval": ( - result.get("kb_apply_proposal_mutation_probes", {}) - .get("approval_fields", {}) - .get("refused") - is True + result.get("kb_apply_proposal_mutation_probes", {}).get("approval_fields", {}).get("refused") is True ), "kb_apply_cannot_mutate_proposal_payload": ( - result.get("kb_apply_proposal_mutation_probes", {}) - .get("payload", {}) - .get("refused") - is True + result.get("kb_apply_proposal_mutation_probes", {}).get("payload", {}).get("refused") is True ), "kb_apply_cannot_mutate_immutable_approval": ( - result.get("kb_apply_proposal_mutation_probes", {}) - .get("immutable_approval_payload", {}) - .get("refused") + result.get("kb_apply_proposal_mutation_probes", {}).get("immutable_approval_payload", {}).get("refused") is True ), "proposal_exact_after_permission_probes": ( - result.get("kb_apply_proposal_mutation_probes", {}).get( - "exact_row_unchanged" - ) - is True - and result.get("kb_apply_proposal_mutation_probes", {}).get( - "immutable_approval_exactly_unchanged" - ) - is True + result.get("kb_apply_proposal_mutation_probes", {}).get("exact_row_unchanged") is True + and result.get("kb_apply_proposal_mutation_probes", {}).get("immutable_approval_exactly_unchanged") is True ), "kb_apply_cannot_update_existing_claim_evidence": ( - result.get("kb_apply_claim_evidence_update_probe", {}) - .get("attempt", {}) - .get("refused") - is True + result.get("kb_apply_claim_evidence_update_probe", {}).get("attempt", {}).get("refused") is True ), "claim_evidence_exact_after_permission_probe": ( - result.get("kb_apply_claim_evidence_update_probe", {}).get( - "exact_row_unchanged" - ) - is True + result.get("kb_apply_claim_evidence_update_probe", {}).get("exact_row_unchanged") is True ), "mutation_between_build_and_execute_refused": all( result.get("mutation_between_build_and_execute", {}).get(key) is True @@ -2114,87 +2042,64 @@ drop database if exists {clone_db}; "immutable_approval_never_changed", ) ), - "first_apply_succeeded": result.get("first_apply", {}).get("returncode") - == 0, - "payload_projection_exact": result.get("row_readback") == expected_rows, - "clone_apply_table_deltas_exact": ( - result.get("clone_apply_table_deltas") == expected_table_deltas + "first_apply_succeeded": result.get("first_apply", {}).get("returncode") == 0, + "fresh_owned_preflight_exact": ( + result.get("fresh_owned_preflight", {}).get("fresh_owned_targets") is True + and result.get("target_rows_before_apply") == _empty_bundle_readback(payload) ), + "compensating_rollback_receipt_passes": ( + result.get("applied_rollback_receipt", {}).get("pass") is True + and result.get("applied_rollback_receipt", {}).get("current_tier") == "T2_runtime" + ), + "rollback_replay_refused_without_mutation": ( + result.get("rollback_replay", {}).get("refused") is True + and result.get("rollback_replay", {}).get("state_unchanged") is True + ), + "payload_projection_exact": result.get("row_readback") == expected_rows, + "clone_apply_table_deltas_exact": (result.get("clone_apply_table_deltas") == expected_table_deltas), "apply_transition_exact": ( result.get("apply_transition", {}).get("exact_transition") is True - and result.get("apply_transition", {}).get( - "immutable_approval_matches_applied_ledger" - ) - is True + and result.get("apply_transition", {}).get("immutable_approval_matches_applied_ledger") is True ), "idempotent_replay_refused": ( result.get("idempotent_replay", {}).get("returncode", 0) != 0 - and result.get("idempotent_replay", {}).get( - "already_applied_refusal" - ) - is True + and result.get("idempotent_replay", {}).get("already_applied_refusal") is True ), "conflict_review_separate_and_exact": ( - result.get("conflict_review_transition", {}).get("exact_transition") - is True - and result.get("conflict_review_transition", {}).get( - "immutable_approval_matches_ledger" - ) - is True + result.get("conflict_review_transition", {}).get("exact_transition") is True + and result.get("conflict_review_transition", {}).get("immutable_approval_matches_ledger") is True ), "source_hash_conflict_refused": ( result.get("conflict_apply", {}).get("returncode", 0) != 0 - and result.get("conflict_apply", {}).get( - "source_hash_collision_refusal" - ) - is True + and result.get("conflict_apply", {}).get("source_hash_collision_refusal") is True ), "conflict_transaction_rolled_back_exactly": ( - result.get("conflict_readback", {}).get( - "proposal_approval_unchanged" - ) - is True - and result.get("conflict_readback", {}).get( - "immutable_approval_unchanged" - ) - is True - and result.get("conflict_readback", {}).get("canonical_rows_absent") - is True + result.get("conflict_readback", {}).get("proposal_approval_unchanged") is True + and result.get("conflict_readback", {}).get("immutable_approval_unchanged") is True + and result.get("conflict_readback", {}).get("canonical_rows_absent") is True ), "source_target_rows_unchanged": ( - result.get("source_target_rows_before") - == result.get("source_target_rows_after") + result.get("source_target_rows_before") == result.get("source_target_rows_after") ), "source_cluster_roles_preexisting": ( - _cluster_roles_are_preprovisioned( - result.get("source_cluster_roles_before") or [] - ) + _cluster_roles_are_preprovisioned(result.get("source_cluster_roles_before") or []) ), "source_cluster_roles_unchanged": ( - result.get("source_cluster_roles_before") - == result.get("source_cluster_roles_after") + result.get("source_cluster_roles_before") == result.get("source_cluster_roles_after") ), "source_table_deltas_exactly_zero": ( bool(result.get("source_table_deltas")) and all(delta == 0 for delta in result["source_table_deltas"].values()) ), - "source_files_unchanged_during_run": ( - result.get("source_binding", {}).get("unchanged_during_run") is True - ), + "source_files_unchanged_during_run": (result.get("source_binding", {}).get("unchanged_during_run") is True), "gateway_process_unchanged": gateway_process_unchanged, - "clone_cleanup_complete": ( - result.get("clone_dropped") is True - and result.get("leftover_clone_count") == 0 - ), + "clone_cleanup_complete": (result.get("clone_dropped") is True and result.get("leftover_clone_count") == 0), } - result["source_counts_observed_unchanged"] = ( - result.get("source_counts_before") == result.get("source_counts_after") - ) + result["source_counts_observed_unchanged"] = result.get("source_counts_before") == result.get("source_counts_after") result["gateway_process_observable"] = gateway_process_observable result["status"] = ( "pass" - if all(value is True for value in result["checks"].values() if value is not None) - and "error" not in result + if all(value is True for value in result["checks"].values() if value is not None) and "error" not in result else "fail" ) result["source_database_mutated"] = not all( @@ -2205,11 +2110,7 @@ drop database if exists {clone_db}; "source_table_deltas_exactly_zero", ) ) - result["service_restarted"] = ( - None - if gateway_process_unchanged is None - else not gateway_process_unchanged - ) + result["service_restarted"] = None if gateway_process_unchanged is None else not gateway_process_unchanged result["post_run_cleanup"] = { "leftover_clone_database_count": result.get("leftover_clone_count"), "gateway_process_observable": gateway_process_observable, @@ -2218,9 +2119,7 @@ drop database if exists {clone_db}; "gateway_main_pid": result.get("service_after", {}).get("MainPID"), "gateway_restart_count": result.get("service_after", {}).get("NRestarts"), } - result["strict_receipt"] = _strict_receipt( - result, expected_rows, expected_table_deltas - ) + result["strict_receipt"] = _strict_receipt(result, expected_rows, expected_table_deltas) if not result["strict_receipt"]["pass"]: result["status"] = "fail" return result @@ -2232,15 +2131,9 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--source-db", default=ap.DEFAULT_DB) parser.add_argument("--database-prefix", default="teleo_approve_claim_clone") parser.add_argument("--service", default="leoclean-gateway.service") - parser.add_argument( - "--approve-script", type=Path, default=HERE / "approve_proposal.py" - ) - parser.add_argument( - "--apply-script", type=Path, default=HERE / "apply_proposal.py" - ) - parser.add_argument( - "--prereqs", type=Path, default=HERE / "kb_apply_prereqs.sql" - ) + parser.add_argument("--approve-script", type=Path, default=HERE / "approve_proposal.py") + parser.add_argument("--apply-script", type=Path, default=HERE / "apply_proposal.py") + parser.add_argument("--prereqs", type=Path, default=HERE / "kb_apply_prereqs.sql") parser.add_argument("--review-secrets-file", default=DEFAULT_REVIEW_SECRETS_FILE) parser.add_argument("--secrets-file", default=ap.DEFAULT_SECRETS_FILE) parser.add_argument("--review-role", default="kb_review") @@ -2251,6 +2144,16 @@ def parse_args(argv: list[str]) -> argparse.Namespace: help="optional one-row normalization output containing one strict approve_claim child", ) parser.add_argument("--output", type=Path) + parser.add_argument( + "--lifecycle-receipt-output", + type=Path, + help="optional public fixture receipt output (never use for private production data)", + ) + parser.add_argument( + "--rollback-sql-output", + type=Path, + help="optional exact clone rollback SQL output", + ) return parser.parse_args(argv) diff --git a/scripts/run_approve_claim_isolated_container_canary.sh b/scripts/run_approve_claim_isolated_container_canary.sh index a34f3bf..5690c31 100755 --- a/scripts/run_approve_claim_isolated_container_canary.sh +++ b/scripts/run_approve_claim_isolated_container_canary.sh @@ -9,6 +9,8 @@ image="${POSTGRES_CANARY_IMAGE:-postgres:16}" source_tier="${SOURCE_TIER:-live-readonly}" normalization_json="" output="${repo_root}/docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json" +lifecycle_receipt_output="" +rollback_sql_output="" while [[ $# -gt 0 ]]; do case "$1" in @@ -20,6 +22,14 @@ while [[ $# -gt 0 ]]; do output="$2" shift 2 ;; + --lifecycle-receipt-output) + lifecycle_receipt_output="$2" + shift 2 + ;; + --rollback-sql-output) + rollback_sql_output="$2" + shift 2 + ;; --source-container) source_container="$2" shift 2 @@ -99,11 +109,19 @@ service_state() { } mkdir -p "$workdir" "$(dirname "$output")" +if [[ -n "$lifecycle_receipt_output" ]]; then + mkdir -p "$(dirname "$lifecycle_receipt_output")" +fi +if [[ -n "$rollback_sql_output" ]]; then + mkdir -p "$(dirname "$rollback_sql_output")" +fi source_counts_before="$(source_counts)" service_before="$(service_state)" docker run -d --name "$container" \ --network none \ + --label livingip.canary=proposal-apply-lifecycle \ + --label "livingip.canary.instance=${container}" \ --tmpfs /var/lib/postgresql/data:rw,nosuid,size=512m \ -e POSTGRES_PASSWORD="isolated-postgres-${RANDOM}-${RANDOM}" \ -e POSTGRES_DB=teleo \ @@ -127,6 +145,12 @@ if [[ "$ready_streak" -lt 2 ]]; then fi container_network_mode="$(docker inspect -f '{{.HostConfig.NetworkMode}}' "$container")" container_mounts_json="$(docker inspect -f '{{json .Mounts}}' "$container")" +container_canary_label="$( + docker inspect -f '{{index .Config.Labels "livingip.canary"}}' "$container" +)" +container_instance_label="$( + docker inspect -f '{{index .Config.Labels "livingip.canary.instance"}}' "$container" +)" docker exec "$source_container" pg_dump -U postgres -d "$source_db" \ --schema-only --no-owner --no-privileges >"$workdir/schema.sql" @@ -165,6 +189,13 @@ normalization_args=() if [[ -n "$normalization_json" ]]; then normalization_args=(--normalization-json "$normalization_json") fi +artifact_args=() +if [[ -n "$lifecycle_receipt_output" ]]; then + artifact_args+=(--lifecycle-receipt-output "$lifecycle_receipt_output") +fi +if [[ -n "$rollback_sql_output" ]]; then + artifact_args+=(--rollback-sql-output "$rollback_sql_output") +fi set +e python3 "$repo_root/scripts/run_approve_claim_clone_canary.py" \ @@ -178,6 +209,7 @@ python3 "$repo_root/scripts/run_approve_claim_clone_canary.py" \ --review-secrets-file "$workdir/kb-review.env" \ --secrets-file "$workdir/kb-apply.env" \ "${normalization_args[@]}" \ + "${artifact_args[@]}" \ --output "$output" \ >"$workdir/stdout.json" 2>"$workdir/stderr.log" canary_rc=$? @@ -192,6 +224,20 @@ container_name_readback="$( docker container ls -a --filter "name=^/${container}$" --format '{{.Names}}' 2>&1 )" container_name_readback_rc=$? +container_label_readback="$( + docker container ls -a \ + --filter "label=livingip.canary=proposal-apply-lifecycle" \ + --filter "label=livingip.canary.instance=${container}" \ + --format '{{.Names}}' 2>&1 +)" +container_label_readback_rc=$? +if [[ "$container_label_readback_rc" -eq 0 ]]; then + container_label_orphan_count="$( + awk 'NF { count += 1 } END { print count + 0 }' <<<"$container_label_readback" + )" +else + container_label_orphan_count=-1 +fi if [[ -e "$workdir" || -L "$workdir" ]]; then outer_workdir_lexists=true else @@ -214,6 +260,9 @@ OUTER_CONTAINER_REMOVE_RC="$outer_container_remove_rc" \ OUTER_WORKDIR_REMOVE_RC="$outer_workdir_remove_rc" \ CONTAINER_NAME_READBACK="$container_name_readback" \ CONTAINER_NAME_READBACK_RC="$container_name_readback_rc" \ +CONTAINER_LABEL_READBACK="$container_label_readback" \ +CONTAINER_LABEL_READBACK_RC="$container_label_readback_rc" \ +CONTAINER_LABEL_ORPHAN_COUNT="$container_label_orphan_count" \ OUTER_WORKDIR_LEXISTS="$outer_workdir_lexists" \ CANARY_RC="$canary_rc" \ SOURCE_TIER="$source_tier" \ @@ -221,219 +270,6 @@ SOURCE_NETWORK_MODE="$source_network_mode" \ SOURCE_CANARY_LABEL="$source_canary_label" \ CANARY_NETWORK_MODE="$container_network_mode" \ CANARY_MOUNTS_JSON="$container_mounts_json" \ -python3 - "$output" <<'PY' -import hashlib -import json -import os -import sys -from pathlib import Path - -path = Path(sys.argv[1]) -if not path.is_file(): - raise SystemExit("inner canary did not produce a report") - -data = json.loads(path.read_text(encoding="utf-8")) - -def service(raw: str) -> dict[str, str]: - return dict(line.split("=", 1) for line in raw.splitlines() if "=" in line) - -def table_deltas(before: dict[str, int], after: dict[str, int]) -> dict[str, int]: - if set(before) != set(after): - raise SystemExit( - "live source count keys changed during the canary: " - f"before={sorted(before)} after={sorted(after)}" - ) - return {key: after[key] - before[key] for key in sorted(before)} - -def verify_source_binding(root: Path, records: list[dict[str, object]]) -> dict[str, object]: - readbacks = [] - for record in records: - relative_text = str(record.get("repo_relative_path", "")) - relative = Path(relative_text) - if not relative_text or relative.is_absolute() or ".." in relative.parts: - raise SystemExit(f"unsafe repo-relative source path: {relative_text!r}") - candidate = (root / relative).resolve() - try: - candidate.relative_to(root) - except ValueError as exc: - raise SystemExit( - f"source path escaped repository root: {relative_text}" - ) from exc - if not candidate.is_file(): - readbacks.append( - { - "repo_relative_path": relative.as_posix(), - "expected_sha256": record.get("sha256"), - "actual_sha256": None, - "matches": False, - } - ) - continue - content = candidate.read_bytes() - actual_sha256 = hashlib.sha256(content).hexdigest() - readbacks.append( - { - "repo_relative_path": relative.as_posix(), - "expected_sha256": record.get("sha256"), - "actual_sha256": actual_sha256, - "size_bytes": len(content), - "matches": ( - actual_sha256 == record.get("sha256") - and len(content) == record.get("size_bytes") - ), - } - ) - return { - "files": readbacks, - "all_match": bool(readbacks) and all(row["matches"] for row in readbacks), - } - -def contains_ephemeral_path(value: object) -> bool: - if isinstance(value, str): - return "/tmp/" in value - if isinstance(value, dict): - return any(contains_ephemeral_path(item) for item in value.values()) - if isinstance(value, list): - return any(contains_ephemeral_path(item) for item in value) - return False - -before_counts = json.loads(os.environ["SOURCE_COUNTS_BEFORE"]) -after_counts = json.loads(os.environ["SOURCE_COUNTS_AFTER"]) -live_table_deltas = table_deltas(before_counts, after_counts) -before_service = service(os.environ["SERVICE_BEFORE"]) -after_service = service(os.environ["SERVICE_AFTER"]) -source_tier = os.environ["SOURCE_TIER"] -source_network_mode = os.environ["SOURCE_NETWORK_MODE"] -source_canary_label = os.environ["SOURCE_CANARY_LABEL"] -container_network_mode = os.environ["CANARY_NETWORK_MODE"] -container_mounts = json.loads(os.environ["CANARY_MOUNTS_JSON"]) -container_volume_mounts = [ - mount for mount in container_mounts if mount.get("Type") == "volume" -] -container_isolated = ( - container_network_mode == "none" and not container_volume_mounts -) -source_tier_enforced = ( - source_tier != "isolated-local" - or ( - source_network_mode == "none" - and source_canary_label == "working-leo-approved-source" - ) -) -service_observable = ( - before_service.get("Available") == "true" - and after_service.get("Available") == "true" -) -service_stable = ( - all( - before_service.get(key) == after_service.get(key) - for key in ("ActiveState", "SubState", "MainPID", "NRestarts") - ) - if service_observable - else None -) -service_gate_passes = ( - service_stable is True - if source_tier == "live-readonly" - else service_stable is not False -) -container_absent = ( - int(os.environ["CONTAINER_NAME_READBACK_RC"]) == 0 - and not os.environ["CONTAINER_NAME_READBACK"].strip() -) -workdir_absent = os.environ["OUTER_WORKDIR_LEXISTS"] == "false" -source_binding = verify_source_binding( - Path(os.environ["REPO_ROOT"]).resolve(), - data.get("source_binding", {}).get("files", []), -) -data["source_binding"]["independent_post_cleanup_verification"] = source_binding -source_binding_ok = ( - data["source_binding"].get("unchanged_during_run") is True - and source_binding["all_match"] is True -) -stale_ephemeral_paths_absent = not contains_ephemeral_path(data) -inner_runtime_pass = data.get("status") == "pass" and int(os.environ["CANARY_RC"]) == 0 -outer_ok = ( - before_counts == after_counts - and all(delta == 0 for delta in live_table_deltas.values()) - and service_gate_passes - and container_absent - and workdir_absent - and source_binding_ok - and stale_ephemeral_paths_absent - and container_isolated - and source_tier_enforced -) -if source_tier == "isolated-local": - data["required_tier"] = "T2_runtime" - data["required_tiers"] = ["T2_runtime"] - data["runtime_scope"] = "isolated_postgres_container_from_readonly_local_fixture" -else: - data["required_tier"] = "T3_live_readonly" - data["required_tiers"] = ["T2_runtime", "T3_live_readonly"] - data["runtime_scope"] = "isolated_postgres_container_from_readonly_live_schema" -data["outer_isolation"] = { - "source_container": os.environ.get("SOURCE_CONTAINER", "teleo-pg"), - "source_database": os.environ.get("SOURCE_DB", "teleo"), - "source_counts_before": before_counts, - "source_counts_after": after_counts, - "canonical_table_deltas": live_table_deltas, - "source_counts_unchanged": before_counts == after_counts, - "service_before": before_service, - "service_after": after_service, - "service_required": source_tier == "live-readonly", - "service_observable": service_observable, - "service_stable": service_stable, - "service_gate_passes": service_gate_passes, - "source_boundary": { - "source_tier": source_tier, - "network_mode": source_network_mode, - "canary_label": source_canary_label, - "tier_enforced": source_tier_enforced, - }, - "container_isolation": { - "network_mode": container_network_mode, - "volume_mounts": container_volume_mounts, - "network_disabled": container_network_mode == "none", - "no_docker_volumes": not container_volume_mounts, - }, - "cleanup_readback": { - "container_remove_returncode": int(os.environ["OUTER_CONTAINER_REMOVE_RC"]), - "container_name_query_returncode": int( - os.environ["CONTAINER_NAME_READBACK_RC"] - ), - "container_name_query_stdout": os.environ["CONTAINER_NAME_READBACK"], - "temporary_container_absent": container_absent, - "workdir_remove_returncode": int(os.environ["OUTER_WORKDIR_REMOVE_RC"]), - "workdir_lexists_after_cleanup": not workdir_absent, - "temporary_workdir_absent": workdir_absent, - }, -} -data["checks"]["outer_live_counts_exactly_unchanged"] = before_counts == after_counts -data["checks"]["outer_live_table_deltas_exactly_zero"] = all( - delta == 0 for delta in live_table_deltas.values() -) -data["checks"]["outer_service_observed_if_required"] = ( - source_tier != "live-readonly" or service_observable -) -data["checks"]["outer_service_stable_if_observed"] = service_gate_passes -data["checks"]["outer_container_absent_independent_readback"] = container_absent -data["checks"]["outer_workdir_absent_independent_readback"] = workdir_absent -data["checks"]["source_binding_independently_verified"] = source_binding_ok -data["checks"]["stale_ephemeral_paths_absent"] = stale_ephemeral_paths_absent -data["checks"]["outer_source_tier_enforced"] = source_tier_enforced -data["checks"]["outer_container_network_disabled"] = ( - container_network_mode == "none" -) -data["checks"]["outer_container_has_no_docker_volumes"] = not container_volume_mounts -data["checks"]["outer_isolation_complete"] = outer_ok -if outer_ok and inner_runtime_pass: - data["current_tier"] = ( - "T2_runtime" if source_tier == "isolated-local" else "T3_live_readonly" - ) -else: - data["status"] = "fail" - data["current_tier"] = "T2_runtime" if inner_runtime_pass else "T1_model" -path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") -raise SystemExit(0 if data["status"] == "pass" else 1) -PY +CANARY_LABEL="$container_canary_label" \ +CANARY_INSTANCE_LABEL="$container_instance_label" \ +python3 "$repo_root/scripts/finalize_approve_claim_isolated_canary.py" "$output" diff --git a/tests/test_apply_proposal.py b/tests/test_apply_proposal.py index b1fb67d..8f79873 100644 --- a/tests/test_apply_proposal.py +++ b/tests/test_apply_proposal.py @@ -30,6 +30,10 @@ sys.path.insert(0, str(REPO_ROOT / "scripts")) import apply_proposal as ap # noqa: E402 +CLAIM_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" +CLAIM_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" +SOURCE_A = "cccccccc-cccc-4ccc-8ccc-cccccccccccc" + def _approval_meta(proposal_type, apply_payload): return { @@ -112,7 +116,7 @@ def test_revise_strategy_rejects_bad_node_type(): # --- add_edge ------------------------------------------------------------- # def test_add_edge_sql_dedup_guard(): - payload = {"from_claim": "aaaa", "to_claim": "bbbb", "edge_type": "supersedes", "weight": 0.9} + payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supersedes", "weight": 0.9} sql = ap.build_add_edge_sql(payload, "pid-2", None, _approval_meta("add_edge", payload)) assert "insert into public.claim_edges" in sql assert "not exists" in sql @@ -122,7 +126,7 @@ def test_add_edge_sql_dedup_guard(): def test_add_edge_semantic_row_accepts_match_and_rejects_weight_mismatch(): - payload = {"from_claim": "aaaa", "to_claim": "bbbb", "edge_type": "supports", "weight": 0.9} + payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supports", "weight": 0.9} sql = ap.build_add_edge_sql(payload, "pid-2", None, _approval_meta("add_edge", payload)) verify = sql[sql.index("do $verify$") : sql.index("kb_stage.finish_approved_proposal")] @@ -131,9 +135,7 @@ def test_add_edge_semantic_row_accepts_match_and_rejects_weight_mismatch(): assert "claim_edge row does not match strict apply payload" in verify assert sql.startswith("begin;") assert sql.rstrip().endswith("commit;") - assert sql.index("raise exception 'apply_proposal: claim_edge row") < sql.index( - "kb_stage.finish_approved_proposal" - ) + assert sql.index("raise exception 'apply_proposal: claim_edge row") < sql.index("kb_stage.finish_approved_proposal") def test_add_edge_rejects_self_loop(): @@ -144,7 +146,7 @@ def test_add_edge_rejects_self_loop(): # --- attach_evidence ------------------------------------------------------ # def test_attach_evidence_sql_with_source_id(): - payload = {"evidence": [{"claim_id": "cccc", "source_id": "dddd", "role": "grounds", "weight": 0.78}]} + payload = {"evidence": [{"claim_id": CLAIM_A, "source_id": SOURCE_A, "role": "grounds", "weight": 0.78}]} sql = ap.build_attach_evidence_sql(payload, "pid-3", None, _approval_meta("attach_evidence", payload)) assert "insert into public.claim_evidence" in sql assert "::evidence_role" in sql @@ -152,14 +154,8 @@ def test_attach_evidence_sql_with_source_id(): def test_attach_evidence_semantic_row_accepts_match_and_rejects_weight_mismatch(): - payload = { - "evidence": [ - {"claim_id": "cccc", "source_id": "dddd", "role": "grounds", "weight": 0.78} - ] - } - sql = ap.build_attach_evidence_sql( - payload, "pid-3", None, _approval_meta("attach_evidence", payload) - ) + payload = {"evidence": [{"claim_id": CLAIM_A, "source_id": SOURCE_A, "role": "grounds", "weight": 0.78}]} + sql = ap.build_attach_evidence_sql(payload, "pid-3", None, _approval_meta("attach_evidence", payload)) verify = sql[sql.index("do $verify$") : sql.index("kb_stage.finish_approved_proposal")] assert "weight is not distinct from 0.78" in verify @@ -167,9 +163,7 @@ def test_attach_evidence_semantic_row_accepts_match_and_rejects_weight_mismatch( assert "evidence row % does not match strict apply payload" in verify assert sql.startswith("begin;") assert sql.rstrip().endswith("commit;") - assert sql.index("raise exception 'apply_proposal: evidence row") < sql.index( - "kb_stage.finish_approved_proposal" - ) + assert sql.index("raise exception 'apply_proposal: evidence row") < sql.index("kb_stage.finish_approved_proposal") def test_attach_evidence_requires_source_id(): @@ -231,9 +225,7 @@ def _approve_claim_bundle(): {"claim_id": claim_a, "source_id": source_a, "role": "grounds", "weight": 0.9}, {"claim_id": claim_b, "source_id": source_b, "role": "illustrates", "weight": None}, ], - "edges": [ - {"from_claim": claim_a, "to_claim": claim_b, "edge_type": "supports", "weight": 0.7} - ], + "edges": [{"from_claim": claim_a, "to_claim": claim_b, "edge_type": "supports", "weight": 0.7}], "reasoning_tools": [ { "id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", @@ -291,9 +283,7 @@ def test_approve_claim_semantic_rows_accept_matches_and_reject_payload_mismatche assert "approve_claim: edge row does not match strict apply payload" in verify assert sql.startswith("begin;") assert sql.rstrip().endswith("commit;") - assert sql.index("raise exception 'approve_claim: evidence row") < sql.index( - "kb_stage.finish_approved_proposal" - ) + assert sql.index("raise exception 'approve_claim: evidence row") < sql.index("kb_stage.finish_approved_proposal") def test_approve_claim_bundle_rejects_duplicate_source_hashes(): @@ -445,7 +435,7 @@ def test_assert_applyable_allows_approved(): # --- ledger flip: concurrency guard + FK stamp --------------------------- # def test_ledger_transition_uses_payload_bound_security_functions(): - payload = {"from_claim": "a", "to_claim": "b", "edge_type": "supports"} + payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supports"} sql = ap.build_add_edge_sql(payload, "pid", None, _approval_meta("add_edge", payload)) assert "kb_stage.assert_approved_proposal" in sql assert "kb_stage.finish_approved_proposal" in sql @@ -459,7 +449,7 @@ def test_ledger_transition_uses_payload_bound_security_functions(): def test_ledger_flip_stamps_agent_fk(): - payload = {"from_claim": "a", "to_claim": "b", "edge_type": "supports"} + payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supports"} sql = ap.build_add_edge_sql(payload, "pid", "kb-apply", _approval_meta("add_edge", payload)) # Hard resolve into a variable + NOT-NULL assert, never a silent inline # subselect that would stamp NULL on an unresolved handle. @@ -468,7 +458,7 @@ def test_ledger_flip_stamps_agent_fk(): def test_build_apply_sql_defaults_applied_by_to_service_agent(): - payload = {"from_claim": "a", "to_claim": "b", "edge_type": "supports"} + payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supports"} proposal = { "id": "pid", "proposal_type": "add_edge", diff --git a/tests/test_apply_worker.py b/tests/test_apply_worker.py index 21ce813..cd04d1d 100644 --- a/tests/test_apply_worker.py +++ b/tests/test_apply_worker.py @@ -6,7 +6,10 @@ standalone (`python3 tests/test_apply_worker.py`). """ import argparse +import json +import stat import sys +import tempfile from pathlib import Path try: @@ -40,19 +43,33 @@ sys.path.insert(0, str(REPO_ROOT / "scripts")) import apply_worker as w # noqa: E402 -@pytest.fixture(autouse=True) -def restore_apply_worker_dependencies(): +def _dependency_restore(): original_load_password = w.ap.load_password original_load_failure_state = w.load_failure_state original_fetch_candidates = w.fetch_candidates original_apply_one = w.apply_one original_render_one = w.render_one + original_subprocess_run = w.subprocess.run yield w.ap.load_password = original_load_password w.load_failure_state = original_load_failure_state w.fetch_candidates = original_fetch_candidates w.apply_one = original_apply_one w.render_one = original_render_one + w.subprocess.run = original_subprocess_run + + +@pytest.fixture(autouse=True) +def restore_apply_worker_dependencies(): + restore = _dependency_restore() + next(restore) + try: + yield + finally: + try: + next(restore) + except StopIteration: + pass # --- candidate query ------------------------------------------------------ # @@ -116,22 +133,283 @@ def test_render_command_expands_agent_id(): # --- enablement gate ------------------------------------------------------ # def _args(**over): base = dict( - enable=False, limit=20, max_per_tick=1, max_attempts=3, - failure_state_file=None, applied_by="kb-apply", - apply_script="apply_proposal.py", render_cmd="", - secrets_file="x", container="teleo-pg", db="teleo", - host="127.0.0.1", role="kb_apply", + enable=False, + limit=20, + max_per_tick=1, + max_attempts=3, + failure_state_file=None, + applied_by="kb-apply", + apply_script="apply_proposal.py", + render_cmd="", + receipt_dir="/private/tmp/kb-apply-worker-test-receipts", + secrets_file="x", + container="teleo-pg", + db="teleo", + host="127.0.0.1", + role="kb_apply", ) base.update(over) return argparse.Namespace(**base) +def _write_valid_private_receipt(path: Path, proposal_id: str) -> None: + from_claim = "11111111-1111-4111-8111-111111111111" + to_claim = "22222222-2222-4222-8222-222222222222" + proposal = { + "id": proposal_id, + "proposal_type": "add_edge", + "status": "applied", + "payload": { + "apply_payload": { + "from_claim": from_claim, + "to_claim": to_claim, + "edge_type": "supports", + "weight": 0.75, + } + }, + "reviewed_by_handle": "reviewer", + "reviewed_by_agent_id": None, + "reviewed_at": "2026-07-15T10:00:00+00:00", + "review_note": "Approved for worker receipt testing.", + "applied_by_handle": "kb-apply", + "applied_by_agent_id": None, + "applied_at": "2026-07-15T10:01:00+00:00", + } + receipt = w.ap.replay_receipt.build_replay_receipt( + proposal, + { + "claim_edges": [ + { + "id": "33333333-3333-4333-8333-333333333333", + "from_claim": from_claim, + "to_claim": to_claim, + "edge_type": "supports", + "weight": 0.75, + "created_by": None, + } + ] + }, + apply_sql_sha256="a" * 64, + apply_sql_source="exact_executed_sql", + exported_at_utc="2026-07-15T10:02:00+00:00", + ) + path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + path.write_text(json.dumps(receipt), encoding="utf-8") + path.chmod(0o600) + + +def _load_private_receipt(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _write_private_receipt(path: Path, receipt: dict) -> None: + path.write_text(json.dumps(receipt), encoding="utf-8") + path.chmod(0o600) + + +def _completed(returncode: int, *, stdout: str = "", stderr: str = ""): + return w.subprocess.CompletedProcess([], returncode, stdout=stdout, stderr=stderr) + + +def _postcommit_receipt_failure(proposal_id: str, detail: str = "receipt write failed") -> str: + return ( + f"proposal {proposal_id} {w.POSTCOMMIT_RECEIPT_FAILURE_MARKER}: {detail}" + f"{w.POSTCOMMIT_RECEIPT_RECOVERY_MARKER}" + f"apply_proposal.py {proposal_id} --receipt-only" + ) + + +# --- private receipt and post-COMMIT recovery ---------------------------- # +def test_private_receipt_requires_complete_canonical_rows_and_hashes(): + proposal_id = "99999999-9999-4999-8999-999999999999" + receipt_path = Path(tempfile.mkdtemp()) / "receipt.json" + _write_valid_private_receipt(receipt_path, proposal_id) + + missing_rows = _load_private_receipt(receipt_path) + missing_rows.pop("canonical_rows") + _write_private_receipt(receipt_path, missing_rows) + with pytest.raises(RuntimeError, match="row-level contract is invalid"): + w.assert_private_replay_receipt(receipt_path, proposal_id) + + _write_valid_private_receipt(receipt_path, proposal_id) + missing_hashes = _load_private_receipt(receipt_path) + missing_hashes.pop("hashes") + _write_private_receipt(receipt_path, missing_hashes) + with pytest.raises(RuntimeError, match="row-level contract is invalid"): + w.assert_private_replay_receipt(receipt_path, proposal_id) + + +def test_private_receipt_rejects_canonical_row_tampering_without_recomputed_hashes(): + proposal_id = "88888888-8888-4888-8888-888888888888" + receipt_path = Path(tempfile.mkdtemp()) / "receipt.json" + _write_valid_private_receipt(receipt_path, proposal_id) + tampered = _load_private_receipt(receipt_path) + tampered["canonical_rows"]["claim_edges"][0]["weight"] = 0.5 + _write_private_receipt(receipt_path, tampered) + + with pytest.raises(RuntimeError, match="row-level contract is invalid|internally inconsistent"): + w.assert_private_replay_receipt(receipt_path, proposal_id) + + +def test_apply_one_supplies_deterministic_private_receipt_path(): + proposal_id = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + receipt_dir = Path(tempfile.mkdtemp()) / "receipts" + calls = [] + + def _run(cmd, **_kwargs): + calls.append(cmd) + receipt_path = Path(cmd[cmd.index("--receipt-out") + 1]) + _write_valid_private_receipt(receipt_path, proposal_id) + return _completed(0, stdout='{"applied": true}') + + w.subprocess.run = _run + w.apply_one(_args(receipt_dir=str(receipt_dir)), proposal_id) + + expected = receipt_dir.resolve() / f"{proposal_id}.json" + assert len(calls) == 1 + assert calls[0][calls[0].index("--receipt-dir") + 1] == str(expected.parent) + assert calls[0][calls[0].index("--receipt-out") + 1] == str(expected) + assert stat.S_IMODE(expected.stat().st_mode) == 0o600 + + +def test_apply_one_recovers_postcommit_receipt_failure_without_reapplying(): + proposal_id = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + receipt_dir = Path(tempfile.mkdtemp()) / "receipts" + calls = [] + + def _run(cmd, **_kwargs): + calls.append(cmd) + if len(calls) == 1: + return _completed( + 1, + stderr=_postcommit_receipt_failure(proposal_id), + ) + assert "--receipt-only" in cmd + receipt_path = Path(cmd[cmd.index("--receipt-out") + 1]) + _write_valid_private_receipt(receipt_path, proposal_id) + return _completed(0, stdout='{"replay_ready": true}') + + w.subprocess.run = _run + w.apply_one(_args(receipt_dir=str(receipt_dir)), proposal_id) + + assert len(calls) == 2 + assert "--receipt-only" not in calls[0] + assert "--receipt-only" in calls[1] + assert calls[0][calls[0].index("--receipt-out") + 1] == calls[1][calls[1].index("--receipt-out") + 1] + + +def test_apply_one_does_not_recover_a_precommit_apply_failure(): + calls = [] + + def _run(cmd, **_kwargs): + calls.append(cmd) + return _completed(1, stderr="psql transaction failed before commit") + + w.subprocess.run = _run + try: + w.apply_one(_args(receipt_dir=tempfile.mkdtemp()), "p1") + except w.PostCommitReceiptError as exc: + raise AssertionError("pre-COMMIT failure was misclassified as committed") from exc + except RuntimeError: + pass + else: + raise AssertionError("expected pre-COMMIT apply failure") + + assert len(calls) == 1 + + +def test_apply_one_classifies_unrecovered_receipt_as_postcommit(): + calls = [] + + def _run(cmd, **_kwargs): + calls.append(cmd) + if len(calls) == 1: + return _completed(1, stderr=_postcommit_receipt_failure("p1")) + return _completed(1, stderr="receipt directory remains unavailable") + + w.subprocess.run = _run + with pytest.raises(w.PostCommitReceiptError): + w.apply_one(_args(receipt_dir=tempfile.mkdtemp()), "p1") + + assert len(calls) == 2 + assert "--receipt-only" in calls[1] + + +def test_forged_marker_in_unknown_field_traceback_is_a_normal_apply_failure(): + proposal_id = "cccccccc-cccc-4ccc-8ccc-cccccccccccc" + calls = [] + + def _run(cmd, **_kwargs): + calls.append(cmd) + return _completed( + 1, + stderr=( + "Traceback (most recent call last):\n" + "ValueError: approve_claim apply_payload contains unknown fields: " + f"['{w.POSTCOMMIT_RECEIPT_FAILURE_MARKER}']" + ), + ) + + w.subprocess.run = _run + with pytest.raises(RuntimeError, match="apply failed"): + w.apply_one(_args(receipt_dir=tempfile.mkdtemp()), proposal_id) + + assert len(calls) == 1 + assert "--receipt-only" not in calls[0] + + +def test_forged_marker_increments_normal_failure_and_poison_accounting(): + proposal_id = "dddddddd-dddd-4ddd-8ddd-dddddddddddd" + failure_state = str(Path(tempfile.mkdtemp()) / "failures.json") + calls = [] + w.ap.load_password = lambda _path: "pw" + w.fetch_candidates = lambda _args, _password, _excluded: _cands(proposal_id) + + def _run(cmd, **_kwargs): + calls.append(cmd) + return _completed( + 1, + stderr=( + "ValueError: approve_claim apply_payload contains unknown fields: " + f"['{w.POSTCOMMIT_RECEIPT_FAILURE_MARKER}']" + ), + ) + + w.subprocess.run = _run + rc = w.run( + _args( + enable=True, + failure_state_file=failure_state, + max_attempts=1, + max_per_tick=1, + receipt_dir=tempfile.mkdtemp(), + ) + ) + + assert rc == 1 + assert w.load_failure_state(failure_state) == {proposal_id: 1} + assert len(calls) == 1 + + +def test_postcommit_receipt_error_does_not_increment_apply_failure_count(): + path = str(Path(tempfile.mkdtemp()) / "failures.json") + w.ap.load_password = lambda _f: "pw" + w.fetch_candidates = lambda a, p, excluded: _cands("committed") + + def _postcommit(_args, _proposal_id): + raise w.PostCommitReceiptError("committed; receipt recovery failed") + + w.apply_one = _postcommit + rc = w.run(_args(enable=True, failure_state_file=path, max_per_tick=1)) + + assert rc == 1 + assert w.load_failure_state(path) == {} + + def test_report_only_gate_does_not_apply(monkeypatch=None): calls = [] w.ap.load_password = lambda _f: "pw" - w.fetch_candidates = lambda a, p, excluded: [ - {"id": "p1", "proposal_type": "revise_strategy", "agent_id": "ag"} - ] + w.fetch_candidates = lambda a, p, excluded: [{"id": "p1", "proposal_type": "revise_strategy", "agent_id": "ag"}] w.apply_one = lambda a, pid: calls.append(pid) rc = w.run(_args(enable=False)) @@ -142,9 +420,7 @@ def test_report_only_gate_does_not_apply(monkeypatch=None): def test_enabled_worker_applies_and_renders(): applied, rendered = [], [] w.ap.load_password = lambda _f: "pw" - w.fetch_candidates = lambda a, p, excluded: [ - {"id": "p1", "proposal_type": "revise_strategy", "agent_id": "ag"} - ] + w.fetch_candidates = lambda a, p, excluded: [{"id": "p1", "proposal_type": "revise_strategy", "agent_id": "ag"}] w.apply_one = lambda a, pid: applied.append(pid) w.render_one = lambda a, agent_id: rendered.append(agent_id) or "ok" @@ -157,9 +433,7 @@ def test_enabled_worker_applies_and_renders(): def test_enabled_worker_skips_render_for_add_edge(): applied, rendered = [], [] w.ap.load_password = lambda _f: "pw" - w.fetch_candidates = lambda a, p, excluded: [ - {"id": "e1", "proposal_type": "add_edge", "agent_id": None} - ] + w.fetch_candidates = lambda a, p, excluded: [{"id": "e1", "proposal_type": "add_edge", "agent_id": None}] w.apply_one = lambda a, pid: applied.append(pid) w.render_one = lambda a, agent_id: rendered.append(agent_id) or "ok" @@ -172,9 +446,7 @@ def test_enabled_worker_skips_render_for_add_edge(): def test_enabled_worker_skips_render_for_approve_claim(): applied, rendered = [], [] w.ap.load_password = lambda _f: "pw" - w.fetch_candidates = lambda a, p, excluded: [ - {"id": "c1", "proposal_type": "approve_claim", "agent_id": "ag"} - ] + w.fetch_candidates = lambda a, p, excluded: [{"id": "c1", "proposal_type": "approve_claim", "agent_id": "ag"}] w.apply_one = lambda a, pid: applied.append(pid) w.render_one = lambda a, agent_id: rendered.append(agent_id) or "ok" @@ -263,7 +535,7 @@ if __name__ == "__main__": failures = 0 for name, fn in sorted(globals().items()): if name.startswith("test_") and callable(fn): - restore = restore_apply_worker_dependencies() + restore = _dependency_restore() next(restore) try: fn() diff --git a/tests/test_proposal_apply_lifecycle.py b/tests/test_proposal_apply_lifecycle.py new file mode 100644 index 0000000..370dde1 --- /dev/null +++ b/tests/test_proposal_apply_lifecycle.py @@ -0,0 +1,846 @@ +"""Deterministic and property-based tests for proposal apply rollback evidence.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import string +import sys +from datetime import datetime, timezone +from pathlib import Path + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +import apply_proposal as apply # noqa: E402 +import proposal_apply_lifecycle as lifecycle # noqa: E402 + +PROPOSAL_ID = "99999999-9999-4999-8999-999999999999" +CLAIM_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" +CLAIM_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" +SOURCE_ID = "cccccccc-cccc-4ccc-8ccc-cccccccccccc" +TOOL_ID = "dddddddd-dddd-4ddd-8ddd-dddddddddddd" +AGENT_ID = "11111111-1111-4111-8111-111111111111" +APPLIED_AT = "2026-07-15T10:15:00+00:00" +REVIEWED_AT = "2026-07-15T09:30:00+00:00" +ROW_FAMILIES = tuple(lifecycle.ROW_TABLES) + + +def _apply_payload() -> dict: + return { + "contract_version": 2, + "agent_id": AGENT_ID, + "claims": [ + { + "id": CLAIM_A, + "type": "structural", + "text": "Canonical claim A", + "status": "open", + "confidence": 0.8, + "tags": ["working-leo", "lifecycle"], + "created_by": AGENT_ID, + }, + { + "id": CLAIM_B, + "type": "concept", + "text": "Canonical claim B", + "status": "open", + "confidence": None, + "tags": [], + "created_by": AGENT_ID, + }, + ], + "sources": [ + { + "id": SOURCE_ID, + "source_type": "paper", + "url": "https://example.test/lifecycle", + "storage_path": None, + "excerpt": "Bound lifecycle evidence.", + "hash": "sha256:lifecycle-source", + "created_by": AGENT_ID, + } + ], + "evidence": [ + { + "claim_id": CLAIM_A, + "source_id": SOURCE_ID, + "role": "grounds", + "weight": 0.9, + "created_by": AGENT_ID, + } + ], + "edges": [ + { + "from_claim": CLAIM_A, + "to_claim": CLAIM_B, + "edge_type": "supports", + "weight": 0.7, + "created_by": AGENT_ID, + } + ], + "reasoning_tools": [ + { + "id": TOOL_ID, + "agent_id": AGENT_ID, + "name": "Lifecycle verifier", + "description": "Checks the apply and compensating rollback boundary.", + "category": "verification", + } + ], + } + + +def _proposal_before() -> dict: + return { + "id": PROPOSAL_ID, + "proposal_type": "approve_claim", + "status": "approved", + "payload": {"apply_payload": _apply_payload()}, + "reviewed_by_handle": "independent-reviewer", + "reviewed_by_agent_id": None, + "reviewed_at": REVIEWED_AT, + "review_note": "Approved this exact strict payload.", + "applied_by_handle": None, + "applied_by_agent_id": None, + "applied_at": None, + } + + +def _approval_snapshot(proposal: dict) -> dict: + return { + "proposal_id": proposal["id"], + "proposal_type": proposal["proposal_type"], + "payload": copy.deepcopy(proposal["payload"]), + "reviewed_by_handle": proposal["reviewed_by_handle"], + "reviewed_by_agent_id": proposal["reviewed_by_agent_id"], + "reviewed_by_db_role": "kb_reviewer", + "reviewed_at": proposal["reviewed_at"], + "review_note": proposal["review_note"], + } + + +def _canonical_rows() -> dict[str, list[dict]]: + evidence_id = apply.deterministic_row_uuid(PROPOSAL_ID, "claim_evidence", CLAIM_A, SOURCE_ID, "grounds") + edge_id = apply.deterministic_row_uuid(PROPOSAL_ID, "claim_edge", CLAIM_A, CLAIM_B, "supports") + return { + "claims": [ + { + "id": CLAIM_A, + "type": "structural", + "text": "Canonical claim A", + "status": "open", + "confidence": 0.8, + "tags": ["working-leo", "lifecycle"], + "created_by": AGENT_ID, + "superseded_by": None, + "created_at": APPLIED_AT, + "updated_at": APPLIED_AT, + }, + { + "id": CLAIM_B, + "type": "concept", + "text": "Canonical claim B", + "status": "open", + "confidence": None, + "tags": [], + "created_by": AGENT_ID, + "superseded_by": None, + "created_at": APPLIED_AT, + "updated_at": APPLIED_AT, + }, + ], + "sources": [ + { + "id": SOURCE_ID, + "source_type": "paper", + "url": "https://example.test/lifecycle", + "storage_path": None, + "excerpt": "Bound lifecycle evidence.", + "hash": "sha256:lifecycle-source", + "created_by": AGENT_ID, + "created_at": APPLIED_AT, + } + ], + "claim_evidence": [ + { + "id": evidence_id, + "claim_id": CLAIM_A, + "source_id": SOURCE_ID, + "role": "grounds", + "weight": 0.9, + "created_by": AGENT_ID, + "created_at": APPLIED_AT, + } + ], + "claim_edges": [ + { + "id": edge_id, + "from_claim": CLAIM_A, + "to_claim": CLAIM_B, + "edge_type": "supports", + "weight": 0.7, + "created_by": AGENT_ID, + "created_at": APPLIED_AT, + } + ], + "reasoning_tools": [ + { + "id": TOOL_ID, + "agent_id": AGENT_ID, + "name": "Lifecycle verifier", + "description": "Checks the apply and compensating rollback boundary.", + "category": "verification", + "created_at": APPLIED_AT, + "updated_at": APPLIED_AT, + } + ], + } + + +def _empty_targets() -> dict[str, list[dict]]: + return {family: [] for family in ROW_FAMILIES} + + +def _apply_receipt(proposal: dict, rows: dict[str, list[dict]]) -> dict: + applied = copy.deepcopy(proposal) + applied.update( + { + "status": "applied", + "applied_by_handle": apply.SERVICE_AGENT_HANDLE, + "applied_by_agent_id": AGENT_ID, + "applied_at": APPLIED_AT, + } + ) + apply_sql = apply.build_apply_sql(proposal, apply.SERVICE_AGENT_HANDLE) + return apply.replay_receipt.build_replay_receipt( + applied, + copy.deepcopy(rows), + apply_sql_sha256=lifecycle.sha256_text(apply_sql), + apply_sql_source="exact_executed_sql", + exported_at_utc="2026-07-15T10:16:00+00:00", + ) + + +def _materials() -> tuple[dict, dict, dict, dict, str]: + proposal = _proposal_before() + approval = _approval_snapshot(proposal) + preflight = lifecycle.build_fresh_preflight( + proposal, + _empty_targets(), + captured_at_utc="2026-07-15T10:00:00+00:00", + ) + receipt = _apply_receipt(proposal, _canonical_rows()) + rollback_sql = lifecycle.build_compensating_rollback_sql(proposal, approval, preflight, receipt) + return proposal, approval, preflight, receipt, rollback_sql + + +def _passing_lifecycle_receipt() -> dict: + proposal, approval, preflight, receipt, rollback_sql = _materials() + proposal_after = copy.deepcopy(proposal) + return lifecycle.build_lifecycle_receipt( + proposal_before=proposal, + approval_before=approval, + preflight=preflight, + apply_receipt=receipt, + rollback_sql=rollback_sql, + proposal_after=proposal_after, + approval_after=copy.deepcopy(approval), + target_rows_after=_empty_targets(), + counts_before={family: 7 + index for index, family in enumerate(ROW_FAMILIES)}, + counts_after={family: 7 + index for index, family in enumerate(ROW_FAMILIES)}, + unrelated_before={"fingerprint": "same", "row_count": 17}, + unrelated_after={"fingerprint": "same", "row_count": 17}, + rollback_replay_refused=True, + generated_at_utc="2026-07-15T10:30:00+00:00", + ) + + +def _destination(*, candidate_present: bool = True) -> dict: + lifecycle_receipt = _passing_lifecycle_receipt() + candidates = [] + if candidate_present: + candidates.append( + { + "id": PROPOSAL_ID, + "proposal_type": "approve_claim", + "status": "approved", + "applied_at": None, + "proposal_payload_sha256": lifecycle_receipt["proposal_payload_sha256"], + "apply_payload_sha256": lifecycle_receipt["apply_payload_sha256"], + "approval_snapshot_sha256": lifecycle_receipt["approval_snapshot_sha256"], + "reviewed_by_handle": "independent-reviewer", + "reviewed_by_agent_id": None, + "reviewed_at": REVIEWED_AT, + "review_note": "Approved this exact strict payload.", + "immutable_approval_exact": True, + } + ) + return { + "observed_at_utc": "2026-07-15T10:45:00+00:00", + "read_only_transaction": True, + "container": "teleo-pg", + "database": "teleo", + "system_identifier": "7649789040005668902", + "server_version_num": "160014", + "approval_gate": { + "immutable_approval_table_present": True, + "approve_function_present": True, + "assert_function_present": True, + "finish_function_present": True, + "review_role_present": True, + "apply_role_present": True, + }, + "approved_strict_candidates": candidates, + } + + +def _authorization_materials() -> tuple[dict, dict, dict, dict]: + proposal, approval, preflight, apply_receipt, _production_rollback_sql = _materials() + receipt = _passing_lifecycle_receipt() + destination = _destination() + destination_identity = { + key: destination[key] for key in ("container", "database", "system_identifier", "server_version_num") + } + production_rollback_artifact = lifecycle.build_production_rollback_artifact( + proposal_before=proposal, + approval_snapshot=approval, + preflight=preflight, + apply_receipt=apply_receipt, + destination=destination_identity, + generated_at_utc="2026-07-15T10:49:00+00:00", + ) + packet = lifecycle.build_authorization_packet( + receipt, + destination, + repo_git_sha=lifecycle.current_git_sha(REPO_ROOT), + apply_engine_path=REPO_ROOT / "scripts" / "apply_proposal.py", + production_rollback_artifact=production_rollback_artifact, + generated_at_utc="2026-07-15T10:50:00+00:00", + ) + record = { + "authorized": True, + "rollback_authorized": True, + "binding_sha256": packet["binding_sha256"], + "production_rollback_sql_sha256": production_rollback_artifact["rollback_sql_sha256"], + "operator_identity": "release-operator@example.test", + "received_at_utc": "2026-07-15T11:00:00+00:00", + "expires_at_utc": "2026-07-15T13:00:00+00:00", + } + record["authorization_text"] = lifecycle.expected_authorization_text(packet, record) + destination["observed_at_utc"] = "2026-07-15T11:30:00+00:00" + return packet, record, destination, production_rollback_artifact + + +def test_fresh_preflight_requires_every_owned_target_to_be_absent_and_is_deterministic(): + proposal = _proposal_before() + first = lifecycle.build_fresh_preflight( + proposal, + _empty_targets(), + captured_at_utc="2026-07-15T10:00:00+00:00", + ) + second = lifecycle.build_fresh_preflight( + proposal, + _empty_targets(), + captured_at_utc="2026-07-15T10:01:00+00:00", + ) + + assert first["fresh_owned_targets"] is True + assert first["preflight_sha256"] == second["preflight_sha256"] + assert first["preflight_artifact_sha256"] != second["preflight_artifact_sha256"] + assert first["captured_at_utc"] != second["captured_at_utc"] + + nonfresh = _empty_targets() + nonfresh["claims"] = [{"id": CLAIM_A}] + with pytest.raises(ValueError, match="absent target rows"): + lifecycle.build_fresh_preflight(proposal, nonfresh) + + incomplete = _empty_targets() + incomplete.pop("claim_edges") + with pytest.raises(ValueError, match="every approve_claim target table"): + lifecycle.build_fresh_preflight(proposal, incomplete) + + +@pytest.mark.parametrize( + "mutation", + ( + "artifact", + "schema", + "captured_at_utc", + "proposal_id", + "proposal_payload_sha256", + "apply_payload_sha256", + "target_family_missing", + "target_row_present", + "fresh_flag_type", + "state_hash", + "artifact_hash", + "extra_field", + ), +) +def test_rollback_refuses_every_preflight_contract_mutation(mutation: str): + proposal, approval, preflight, receipt, _rollback_sql = _materials() + changed = copy.deepcopy(preflight) + if mutation == "artifact": + changed["artifact"] = "proposal_apply_fresh_preflight_tampered" + elif mutation == "schema": + changed["schema"] = "livingip.proposalApplyFreshPreflight.v0" + elif mutation == "captured_at_utc": + changed["captured_at_utc"] = "2026-07-15T10:00:01+00:00" + elif mutation == "proposal_id": + changed["proposal_id"] = CLAIM_A + elif mutation == "proposal_payload_sha256": + changed["proposal_payload_sha256"] = "0" * 64 + elif mutation == "apply_payload_sha256": + changed["apply_payload_sha256"] = "0" * 64 + elif mutation == "target_family_missing": + changed["target_rows_before"].pop("claim_edges") + elif mutation == "target_row_present": + changed["target_rows_before"]["claims"] = [{"id": CLAIM_A}] + elif mutation == "fresh_flag_type": + changed["fresh_owned_targets"] = 1 + elif mutation == "state_hash": + changed["preflight_sha256"] = "0" * 64 + elif mutation == "artifact_hash": + changed["preflight_artifact_sha256"] = "0" * 64 + else: + changed["unexpected"] = True + + with pytest.raises(ValueError): + lifecycle.build_compensating_rollback_sql(proposal, approval, changed, receipt) + + +def test_compensating_rollback_is_atomic_deterministic_and_refuses_state_drift(): + proposal, approval, preflight, receipt, rollback_sql = _materials() + + assert rollback_sql == lifecycle.build_compensating_rollback_sql(proposal, approval, preflight, receipt) + assert rollback_sql.startswith("begin;") + assert rollback_sql.rstrip().endswith("commit;") + assert "proposal rollback drift:" in rollback_sql + assert "to_jsonb(t)" in rollback_sql + assert rollback_sql.index("proposal rollback drift:") < rollback_sql.index("delete from public.claim_evidence") + delete_positions = [ + rollback_sql.index(f"delete from {lifecycle.ROW_TABLES[family]}") for family in lifecycle.DELETE_ORDER + ] + assert delete_positions == sorted(delete_positions) + assert "set status = 'approved'" in rollback_sql + assert "immutable approval snapshot drifted" in rollback_sql + + drifted_receipt = copy.deepcopy(receipt) + drifted_receipt["proposal"]["payload"]["apply_payload"]["claims"][0]["text"] = "drift" + with pytest.raises(ValueError, match="payload does not match preflight"): + lifecycle.build_compensating_rollback_sql(proposal, approval, preflight, drifted_receipt) + + +@given( + suffix=st.text( + alphabet=string.ascii_letters + string.digits + " -_", + min_size=1, + max_size=32, + ) +) +@settings(max_examples=30, deadline=None) +def test_rollback_refuses_any_canonical_row_tampering_without_rebuilt_receipt(suffix: str): + proposal, approval, preflight, receipt, _rollback_sql = _materials() + changed = copy.deepcopy(receipt) + changed["canonical_rows"]["claims"][0]["text"] = f"mutated:{suffix}" + + with pytest.raises(ValueError, match="replay-contract validation"): + lifecycle.build_compensating_rollback_sql(proposal, approval, preflight, changed) + + +@pytest.mark.parametrize( + "mutation", + ("contract_bool", "expected_count_bool", "actual_count_float", "check_integer"), +) +def test_replay_receipt_json_types_are_exact_not_python_equal(mutation: str): + proposal, approval, preflight, receipt, _rollback_sql = _materials() + changed = copy.deepcopy(receipt) + if mutation == "contract_bool": + changed["receipt_contract_version"] = True + elif mutation == "expected_count_bool": + changed["expected_row_counts"]["sources"] = True + elif mutation == "actual_count_float": + changed["actual_row_counts"]["sources"] = 1.0 + else: + changed["checks"]["proposal_applied"] = 1 + + with pytest.raises(ValueError, match="replay-contract validation"): + lifecycle.build_compensating_rollback_sql(proposal, approval, preflight, changed) + + +def test_rollback_sql_hash_binds_every_receipt_observed_field(): + proposal, approval, preflight, receipt, rollback_sql = _materials() + changed_rows = copy.deepcopy(receipt["canonical_rows"]) + changed_rows["claims"][0]["created_at"] = "2026-07-15T10:15:00.123456+00:00" + changed_receipt = _apply_receipt(proposal, changed_rows) + + changed_sql = lifecycle.build_compensating_rollback_sql( + proposal, + approval, + preflight, + changed_receipt, + ) + + assert lifecycle.sha256_text(changed_sql) != lifecycle.sha256_text(rollback_sql) + assert "to_jsonb(t)" in changed_sql + assert "proposal rollback drift: claims[0] row does not match apply receipt" in changed_sql + + +@pytest.mark.parametrize("family", ("claim_evidence", "claim_edges")) +def test_fresh_apply_receipt_requires_deterministic_generated_row_ids(family: str): + proposal, approval, preflight, receipt, _rollback_sql = _materials() + changed_rows = copy.deepcopy(receipt["canonical_rows"]) + changed_rows[family][0]["id"] = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" + changed_receipt = _apply_receipt(proposal, changed_rows) + + with pytest.raises(ValueError, match="is not deterministic"): + lifecycle.build_compensating_rollback_sql( + proposal, + approval, + preflight, + changed_receipt, + ) + + +def test_production_rollback_artifact_reconstructs_exact_sql_and_rejects_noop_substitution(): + proposal, approval, preflight, apply_receipt, rollback_sql = _materials() + destination = { + "container": "teleo-pg", + "database": "teleo", + "system_identifier": "7649789040005668902", + "server_version_num": "160014", + } + artifact = lifecycle.build_production_rollback_artifact( + proposal_before=proposal, + approval_snapshot=approval, + preflight=preflight, + apply_receipt=apply_receipt, + destination=destination, + generated_at_utc="2026-07-15T10:49:00+00:00", + ) + + assert ( + lifecycle.validate_production_rollback_artifact( + artifact, + expected_destination=destination, + ) + == artifact + ) + assert artifact["rollback_sql"] == rollback_sql + + noop = copy.deepcopy(artifact) + noop["rollback_sql"] = ( + "begin; -- proposal rollback drift: -- immutable approval snapshot drifted " + "-- set status = 'approved'\ncommit;\n" + ) + noop["rollback_sql_sha256"] = lifecycle.sha256_text(noop["rollback_sql"]) + unsigned = dict(noop) + unsigned.pop("artifact_sha256") + noop["artifact_sha256"] = lifecycle.sha256_json(unsigned) + + with pytest.raises(ValueError, match="reconstructed exact SQL"): + lifecycle.validate_production_rollback_artifact(noop, expected_destination=destination) + + +def test_lifecycle_receipt_proves_inverse_and_has_self_consistent_hashes(): + receipt = _passing_lifecycle_receipt() + unsigned = dict(receipt) + receipt_hash = unsigned.pop("lifecycle_receipt_sha256") + + assert receipt["pass"] is True + assert receipt["current_tier"] == "T2_runtime" + assert all(receipt["checks"].values()) + assert all(not rows for rows in receipt["rollback"]["target_rows_after"].values()) + assert receipt["rollback"]["counts_after_rollback"] == receipt["rollback"]["counts_before_apply"] + assert receipt["production_apply_executed"] is False + assert receipt["production_apply_authorization_present"] is False + assert receipt_hash == lifecycle.sha256_json(unsigned) + + +def test_lifecycle_receipt_downgrades_when_unrelated_state_changes(): + proposal, approval, preflight, receipt, rollback_sql = _materials() + result = lifecycle.build_lifecycle_receipt( + proposal_before=proposal, + approval_before=approval, + preflight=preflight, + apply_receipt=receipt, + rollback_sql=rollback_sql, + proposal_after=copy.deepcopy(proposal), + approval_after=copy.deepcopy(approval), + target_rows_after=_empty_targets(), + counts_before={family: 1 for family in ROW_FAMILIES}, + counts_after={family: 1 for family in ROW_FAMILIES}, + unrelated_before={"fingerprint": "before"}, + unrelated_after={"fingerprint": "after"}, + rollback_replay_refused=True, + ) + + assert result["pass"] is False + assert result["current_tier"] == "T1_model" + assert result["checks"]["unrelated_rows_unchanged"] is False + + +def test_retained_current_lifecycle_evidence_is_self_consistent(): + report_dir = REPO_ROOT / "docs" / "reports" / "leo-working-state-20260709" + canary = json.loads((report_dir / "proposal-apply-lifecycle-canary-current.json").read_text(encoding="utf-8")) + receipt = json.loads((report_dir / "proposal-apply-lifecycle-receipt-current.json").read_text(encoding="utf-8")) + rollback = (report_dir / "proposal-apply-clone-rollback-current.sql").read_text(encoding="utf-8") + unsigned = dict(receipt) + receipt_sha256 = unsigned.pop("lifecycle_receipt_sha256") + + assert canary["status"] == "pass" + assert canary["current_tier"] == "T2_runtime" + assert canary["applied_rollback_receipt"] == receipt + assert canary["outer_isolation"]["cleanup_readback"]["label_scoped_orphan_count"] == 0 + assert all(delta == 0 for delta in canary["outer_isolation"]["canonical_table_deltas"].values()) + assert receipt["pass"] is True + assert receipt["production_apply_executed"] is False + assert receipt_sha256 == lifecycle.sha256_json(unsigned) + assert hashlib.sha256(rollback.encode("utf-8")).hexdigest() == receipt["rollback"]["rollback_sql_sha256"] + + +def test_authorization_packet_is_exact_but_never_self_authorizes_production(): + packet, record, destination, production_rollback_artifact = _authorization_materials() + verification = lifecycle.verify_authorization_record( + packet, + record, + observed_destination=destination, + apply_engine_path=REPO_ROOT / "scripts" / "apply_proposal.py", + production_rollback_artifact=production_rollback_artifact, + now=datetime(2026, 7, 15, 12, 0, tzinfo=timezone.utc), + ) + + assert packet["required_tier"] == "T6_authorized_production" + assert packet["current_tier"] == "T3_live_readonly" + assert packet["safe_to_enter_apply_window"] is False + assert packet["production_apply_authorization_present"] is False + assert packet["production_apply_executed"] is False + assert verification["safe_to_enter_apply_window"] is True + assert verification["production_apply_executed"] is False + assert all(verification["checks"].values()) + + +def test_authorization_packet_names_missing_live_gate_before_requesting_auth(tmp_path): + receipt = _passing_lifecycle_receipt() + destination = _destination(candidate_present=False) + destination["approval_gate"] = {key: False for key in destination["approval_gate"]} + packet = lifecycle.build_authorization_packet( + receipt, + destination, + repo_git_sha=lifecycle.current_git_sha(REPO_ROOT), + apply_engine_path=REPO_ROOT / "scripts" / "apply_proposal.py", + ) + markdown_path = tmp_path / "authorization.md" + lifecycle.write_authorization_markdown(markdown_path, packet) + + assert packet["production_preconditions"]["approval_gate_ready"] is False + assert packet["production_preconditions"]["matching_candidate_count"] == 0 + assert packet["production_preconditions"]["exact_production_rollback_packet_ready"] is False + assert packet["binding"]["production_rollback"]["rollback_sql_sha256"] is None + assert lifecycle.sha256_json(packet["binding"]) == packet["binding_sha256"] + assert packet["safe_to_enter_apply_window"] is False + assert packet["production_apply_authorization_present"] is False + assert packet["production_apply_executed"] is False + assert packet["next_exact_action"].startswith("deploy and independently verify") + assert "This text is not actionable" in markdown_path.read_text(encoding="utf-8") + + +@given(value=st.sampled_from([False, None, 0, 1, "true", "yes", [], {}])) +@settings(max_examples=8, deadline=None) +def test_authorized_field_requires_the_exact_boolean_true(value): + packet, record, destination, production_rollback_artifact = _authorization_materials() + record["authorized"] = value + record["authorization_text"] = lifecycle.expected_authorization_text(packet, record) + + verification = lifecycle.verify_authorization_record( + packet, + record, + observed_destination=destination, + apply_engine_path=REPO_ROOT / "scripts" / "apply_proposal.py", + production_rollback_artifact=production_rollback_artifact, + now=datetime(2026, 7, 15, 12, 0, tzinfo=timezone.utc), + ) + + assert verification["checks"]["authorized_is_exact_true"] is False + assert verification["safe_to_enter_apply_window"] is False + + +AUTHORIZATION_MUTATIONS = ( + "packet_schema", + "packet_readonly", + "approval_gate_packet", + "production_rollback_packet", + "strict_candidate", + "rollback_authorized", + "production_rollback_sha256", + "binding_sha256", + "operator_identity", + "authorization_window", + "authorization_text", + "destination_container", + "destination_database", + "destination_system_identifier", + "destination_server_version", + "approval_gate_action", + "strict_candidate_action", + "apply_engine_sha256", + "approval_gate_sha256", + "binding_proposal", + "binding_gate_false", + "binding_preconditions", + "packet_safe_flag", + "scope_exclusion", + "authorization_template", + "record_extra_field", + "action_readback_stale", + "production_rollback_binding", +) + + +@given(mutation=st.sampled_from(AUTHORIZATION_MUTATIONS)) +@settings(max_examples=len(AUTHORIZATION_MUTATIONS), deadline=None) +def test_every_authorization_binding_field_fails_closed_when_mutated(mutation: str): + packet, record, destination, production_rollback_artifact = _authorization_materials() + expected_failed_check = { + "packet_schema": "packet_schema_exact", + "packet_readonly": "packet_destination_readback_was_read_only", + "approval_gate_packet": "approval_gate_ready_at_packet", + "production_rollback_packet": "exact_production_rollback_packet_ready", + "strict_candidate": "strict_proposal_present_and_approved", + "rollback_authorized": "rollback_authorized_is_exact_true", + "production_rollback_sha256": "production_rollback_sql_sha256_exact", + "binding_sha256": "binding_sha256_exact", + "operator_identity": "operator_identity_present", + "authorization_window": "authorization_window_valid", + "authorization_text": "authorization_text_exact", + "destination_container": "destination_identity_exact", + "destination_database": "destination_identity_exact", + "destination_system_identifier": "destination_identity_exact", + "destination_server_version": "destination_identity_exact", + "approval_gate_action": "approval_gate_identity_exact_at_action", + "strict_candidate_action": "strict_proposal_exact_at_action", + "apply_engine_sha256": "apply_engine_sha256_exact", + "approval_gate_sha256": "approval_gate_prerequisite_sha256_exact", + "binding_proposal": "packet_binding_sha256_self_consistent", + "binding_gate_false": "approval_gate_ready_at_packet", + "binding_preconditions": "packet_preconditions_bound_exactly", + "packet_safe_flag": "packet_never_self_authorizes_or_expands_scope", + "scope_exclusion": "packet_never_self_authorizes_or_expands_scope", + "authorization_template": "authorization_text_template_exact", + "record_extra_field": "authorization_record_contract_exact", + "action_readback_stale": "destination_readback_fresh_and_read_only_at_action", + "production_rollback_binding": "production_rollback_sql_sha256_exact", + }[mutation] + + if mutation == "packet_schema": + packet["schema"] = "livingip.proposalApplyAuthorizationPacket.v0" + elif mutation == "packet_readonly": + packet["production_preconditions"]["destination_readback_was_read_only"] = False + elif mutation == "approval_gate_packet": + packet["production_preconditions"]["approval_gate_ready"] = False + elif mutation == "production_rollback_packet": + packet["production_preconditions"]["exact_production_rollback_packet_ready"] = False + elif mutation == "strict_candidate": + packet["production_preconditions"]["strict_proposal_present_and_approved"] = False + elif mutation == "rollback_authorized": + record["rollback_authorized"] = False + elif mutation == "production_rollback_sha256": + record["production_rollback_sql_sha256"] = "f" * 64 + elif mutation == "binding_sha256": + record["binding_sha256"] = "0" * 64 + elif mutation == "operator_identity": + record["operator_identity"] = " " + elif mutation == "authorization_window": + record["expires_at_utc"] = "2026-07-15T11:30:00+00:00" + elif mutation == "authorization_text": + record["authorization_text"] += " altered" + elif mutation.startswith("destination_"): + key = { + "destination_container": "container", + "destination_database": "database", + "destination_system_identifier": "system_identifier", + "destination_server_version": "server_version_num", + }[mutation] + destination[key] = f"wrong-{key}" + elif mutation == "approval_gate_action": + destination["approval_gate"]["review_role_present"] = False + elif mutation == "strict_candidate_action": + destination["approved_strict_candidates"] = [] + elif mutation == "apply_engine_sha256": + packet["binding"]["engine"]["apply_engine_sha256"] = "0" * 64 + elif mutation == "approval_gate_sha256": + packet["binding"]["approval_gate_dependency"]["sha256"] = "0" * 64 + elif mutation == "binding_proposal": + packet["binding"]["proposal"]["id"] = CLAIM_A + elif mutation == "binding_gate_false": + packet["binding"]["approval_gate"]["review_role_present"] = False + elif mutation == "binding_preconditions": + packet["binding"]["production_preconditions"]["approval_gate_ready"] = False + elif mutation == "packet_safe_flag": + packet["safe_to_enter_apply_window"] = True + elif mutation == "scope_exclusion": + packet["scope_exclusions"]["telegram_send_authorized"] = True + elif mutation == "authorization_template": + packet["authorization_text_template"] += " Also authorize all other mutations." + elif mutation == "record_extra_field": + record["unexpected"] = True + elif mutation == "action_readback_stale": + destination["observed_at_utc"] = "2026-07-15T10:59:59+00:00" + elif mutation == "production_rollback_binding": + packet["binding"]["production_rollback"]["rollback_sql_sha256"] = "f" * 64 + + verification = lifecycle.verify_authorization_record( + packet, + record, + observed_destination=destination, + apply_engine_path=REPO_ROOT / "scripts" / "apply_proposal.py", + production_rollback_artifact=production_rollback_artifact, + now=datetime(2026, 7, 15, 12, 0, tzinfo=timezone.utc), + ) + + assert verification["checks"][expected_failed_check] is False + assert verification["safe_to_enter_apply_window"] is False + assert verification["production_apply_authorization_present"] is False + + +def test_false_bound_gate_cannot_pass_even_after_packet_and_record_are_rehashed(): + packet, record, destination, production_rollback_artifact = _authorization_materials() + packet["binding"]["approval_gate"]["review_role_present"] = False + destination["approval_gate"]["review_role_present"] = False + packet["binding_sha256"] = lifecycle.sha256_json(packet["binding"]) + packet["authorization_record_required"]["binding_sha256"] = packet["binding_sha256"] + packet["authorization_text_template"] = lifecycle._authorization_text_template( + packet["binding"], + packet["binding_sha256"], + ) + record["binding_sha256"] = packet["binding_sha256"] + record["authorization_text"] = lifecycle.expected_authorization_text(packet, record) + + verification = lifecycle.verify_authorization_record( + packet, + record, + observed_destination=destination, + apply_engine_path=REPO_ROOT / "scripts" / "apply_proposal.py", + production_rollback_artifact=production_rollback_artifact, + now=datetime(2026, 7, 15, 12, 0, tzinfo=timezone.utc), + ) + + assert verification["checks"]["packet_binding_sha256_self_consistent"] is True + assert verification["checks"]["approval_gate_ready_at_packet"] is False + assert verification["safe_to_enter_apply_window"] is False + + +def test_authorization_packet_uses_repo_relative_dependency_paths_only(): + packet, _record, _destination, _production_rollback_artifact = _authorization_materials() + encoded = json.dumps(packet, sort_keys=True) + + assert packet["binding"]["engine"]["apply_engine_path"] == "scripts/apply_proposal.py" + assert packet["binding"]["approval_gate_dependency"]["path"] == "scripts/kb_apply_prereqs.sql" + assert "/private/tmp" not in encoded + assert "/tmp" not in encoded diff --git a/tests/test_run_approve_claim_clone_canary_macos_path.py b/tests/test_run_approve_claim_clone_canary_macos_path.py index ad472db..ad238a9 100644 --- a/tests/test_run_approve_claim_clone_canary_macos_path.py +++ b/tests/test_run_approve_claim_clone_canary_macos_path.py @@ -2,6 +2,8 @@ from __future__ import annotations +import hashlib +import json import subprocess import sys from pathlib import Path @@ -11,8 +13,10 @@ import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) WRAPPER = ROOT / "scripts" / "run_approve_claim_isolated_container_canary.sh" +FINALIZER = ROOT / "scripts" / "finalize_approve_claim_isolated_canary.py" import apply_proposal as apply # noqa: E402 +import finalize_approve_claim_isolated_canary as finalizer # noqa: E402 import run_approve_claim_clone_canary as canary # noqa: E402 @@ -24,9 +28,7 @@ def test_role_psql_resolves_docker_before_scrubbing_path( monkeypatch.setattr( canary.shutil, "which", - lambda executable: "/opt/homebrew/bin/docker" - if executable == "docker" - else None, + lambda executable: "/opt/homebrew/bin/docker" if executable == "docker" else None, ) def fake_run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: @@ -58,9 +60,7 @@ def test_apply_tool_resolves_docker_before_scrubbing_path( monkeypatch.setattr( apply.shutil, "which", - lambda executable: "/opt/homebrew/bin/docker" - if executable == "docker" - else None, + lambda executable: "/opt/homebrew/bin/docker" if executable == "docker" else None, ) def fake_run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: @@ -89,14 +89,76 @@ def test_apply_tool_resolves_docker_before_scrubbing_path( def test_outer_wrapper_enforces_local_source_and_networkless_tmpfs() -> None: - text = WRAPPER.read_text(encoding="utf-8") + wrapper = WRAPPER.read_text(encoding="utf-8") + finalizer = FINALIZER.read_text(encoding="utf-8") + text = wrapper + "\n" + finalizer assert 'source_canary_label" != "working-leo-approved-source"' in text assert "--network none" in text assert "--tmpfs /var/lib/postgresql/data:rw,nosuid,size=512m" in text assert 'CANARY_NETWORK_MODE="$container_network_mode"' in text assert 'CANARY_MOUNTS_JSON="$container_mounts_json"' in text - assert 'data["checks"]["outer_source_tier_enforced"]' in text - assert 'data["checks"]["outer_container_network_disabled"]' in text - assert 'data["checks"]["outer_container_has_no_docker_volumes"]' in text + assert 'checks["outer_source_tier_enforced"]' in text + assert 'checks["outer_container_network_disabled"]' in text + assert 'checks["outer_container_has_no_docker_volumes"]' in text assert 'source_tier != "live-readonly" or service_observable' in text + assert "<<'PY'" not in wrapper + assert "finalize_approve_claim_isolated_canary.py" in wrapper + + +def test_outer_finalizer_runs_from_a_normal_file_without_heredoc(tmp_path: Path) -> None: + content = FINALIZER.read_bytes() + report = tmp_path / "canary.json" + report.write_text( + json.dumps( + { + "status": "pass", + "checks": {}, + "source_binding": { + "unchanged_during_run": True, + "files": [ + { + "repo_relative_path": FINALIZER.relative_to(ROOT).as_posix(), + "sha256": hashlib.sha256(content).hexdigest(), + "size_bytes": len(content), + } + ], + }, + } + ), + encoding="utf-8", + ) + counts = json.dumps({"public.claims": 2, "kb_stage.kb_proposals": 0}) + data = finalizer.finalize( + report, + { + "SOURCE_COUNTS_BEFORE": counts, + "SOURCE_COUNTS_AFTER": counts, + "SOURCE_CONTAINER": "isolated-source", + "SOURCE_DB": "teleo", + "SERVICE_BEFORE": "Available=false\nReason=systemctl_unavailable\n", + "SERVICE_AFTER": "Available=false\nReason=systemctl_unavailable\n", + "REPO_ROOT": str(ROOT), + "OUTER_CONTAINER_REMOVE_RC": "0", + "OUTER_WORKDIR_REMOVE_RC": "0", + "CONTAINER_NAME_READBACK": "", + "CONTAINER_NAME_READBACK_RC": "0", + "CONTAINER_LABEL_READBACK": "", + "CONTAINER_LABEL_READBACK_RC": "0", + "CONTAINER_LABEL_ORPHAN_COUNT": "0", + "OUTER_WORKDIR_LEXISTS": "false", + "CANARY_RC": "0", + "SOURCE_TIER": "isolated-local", + "SOURCE_NETWORK_MODE": "none", + "SOURCE_CANARY_LABEL": "working-leo-approved-source", + "CANARY_NETWORK_MODE": "none", + "CANARY_MOUNTS_JSON": "[]", + "CANARY_LABEL": "proposal-apply-lifecycle", + "CANARY_INSTANCE_LABEL": "working-leo-approve-claim-test", + }, + ) + + assert data["status"] == "pass" + assert data["current_tier"] == "T2_runtime" + assert data["checks"]["outer_isolation_complete"] is True + assert data["outer_isolation"]["cleanup_readback"]["label_scoped_orphan_count"] == 0 From 3b138fd2e1117244405fe19c856c81e8aafe4131 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 04:00:14 +0200 Subject: [PATCH 09/27] fix: bind ingestion acceptance to exact provenance --- ...run_leo_local_ingestion_proposal_canary.py | 661 ++++++++++++++---- ...run_leo_local_ingestion_proposal_canary.py | 477 ++++++++++++- 2 files changed, 1011 insertions(+), 127 deletions(-) diff --git a/scripts/run_leo_local_ingestion_proposal_canary.py b/scripts/run_leo_local_ingestion_proposal_canary.py index 3cb9243..12854b2 100644 --- a/scripts/run_leo_local_ingestion_proposal_canary.py +++ b/scripts/run_leo_local_ingestion_proposal_canary.py @@ -31,7 +31,7 @@ import apply_proposal as ap # noqa: E402 import stage_normalized_proposal as normalized_stage # noqa: E402 SCHEMA = "livingip.workingLeoLocalIngestionProposalCanary.v2" -SUITE_SCHEMA = "livingip.workingLeoLocalIngestionProposalSuite.v1" +SUITE_SCHEMA = "livingip.workingLeoLocalIngestionProposalSuite.v2" FIXTURE_SCHEMA = "livingip.workingLeoSourceIngestionScenario.v2" DEFAULT_DOCUMENT_FIXTURE = REPO_ROOT / "fixtures" / "working-leo" / "document-ingestion-v2.scenario.json" DEFAULT_TELEGRAM_FIXTURE = REPO_ROOT / "fixtures" / "working-leo" / "telegram-transcript-ingestion-v1.scenario.json" @@ -60,6 +60,42 @@ def canonical_sha256(value: Any) -> str: return sha256_bytes(canonical_json_bytes(value)) +def normalized_segment_manifest(segments: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Return the complete normalized provenance identity for replay hashing.""" + return [ + { + "id": segment["id"], + "locator": segment["locator"], + "json_pointer": segment["json_pointer"], + "content_sha256": segment["content_sha256"], + "text_sha256": segment["text_sha256"], + } + for segment in segments + ] + + +def replay_identity_payload( + *, + artifact_sha256: str, + artifact_format: str, + source_identity: str, + source_locator: str, + normalized_segments_sha256: str, + extractor: dict[str, str], + extraction_spec_sha256: str, +) -> dict[str, Any]: + """Build the explicit source-to-extraction identity used by every row ID.""" + return { + "artifact_sha256": artifact_sha256, + "artifact_format": artifact_format, + "source_identity": source_identity, + "source_locator": source_locator, + "normalized_segments_sha256": normalized_segments_sha256, + "extractor": extractor, + "extraction_spec_sha256": extraction_spec_sha256, + } + + def canonical_snapshot_complete(snapshot: dict[str, Any]) -> bool: return set(snapshot) == set(CANONICAL_TABLES) and all( isinstance(snapshot.get(table), list) and bool(snapshot[table]) for table in CANONICAL_TABLES @@ -231,6 +267,9 @@ def load_fixture(path: Path) -> tuple[dict[str, Any], dict[str, Any]]: raise CanaryError(f"source.source_type must be one of {sorted(ap.SOURCE_TYPES)}") if not isinstance(source.get("metadata"), dict): raise CanaryError("source.metadata must be an object") + source_identity = _require_text(source.get("identity"), "source.identity") + source_locator = _require_text(source.get("locator"), "source.locator") + source = {**source, "identity": source_identity, "locator": source_locator} segments = _load_segments(raw, artifact_format, source) segment_by_id = {segment["id"]: segment for segment in segments} @@ -349,28 +388,25 @@ def load_fixture(path: Path) -> tuple[dict[str, Any], dict[str, Any]]: normalized_conflicts.append({**conflict, "from_claim_key": from_key, "to_claim_key": to_key}) artifact_sha256 = sha256_bytes(raw) - source_content_sha256 = canonical_sha256( - [ - { - "id": segment["id"], - "locator": segment["locator"], - "content_sha256": segment["content_sha256"], - } - for segment in segments - ] - ) + segment_manifest = normalized_segment_manifest(segments) + source_content_sha256 = canonical_sha256(segment_manifest) extraction_spec_sha256 = canonical_sha256(extraction) - replay_identity_sha256 = canonical_sha256( - { - "artifact_sha256": artifact_sha256, - "extractor": {"name": extractor_name, "version": extractor_version}, - "extraction_spec_sha256": extraction_spec_sha256, - } + extractor_identity = {"name": extractor_name, "version": extractor_version} + replay_components = replay_identity_payload( + artifact_sha256=artifact_sha256, + artifact_format=artifact_format, + source_identity=source_identity, + source_locator=source_locator, + normalized_segments_sha256=source_content_sha256, + extractor=extractor_identity, + extraction_spec_sha256=extraction_spec_sha256, ) + replay_identity_sha256 = canonical_sha256(replay_components) fixture = { **scenario, "artifact": {**artifact, "resolved_path": str(artifact_path)}, - "extractor": {"name": extractor_name, "version": extractor_version}, + "source": source, + "extractor": extractor_identity, "segments": segments, "extraction": { "claims": normalized_claims, @@ -385,10 +421,11 @@ def load_fixture(path: Path) -> tuple[dict[str, Any], dict[str, Any]]: "artifact_format": artifact_format, "artifact_sha256": artifact_sha256, "source_content_sha256": source_content_sha256, - "source_identity": source["identity"], - "source_locator": source["locator"], - "extractor": {"name": extractor_name, "version": extractor_version}, + "source_identity": source_identity, + "source_locator": source_locator, + "extractor": extractor_identity, "extraction_spec_sha256": extraction_spec_sha256, + "replay_identity_components": replay_components, "replay_identity_sha256": replay_identity_sha256, "counts": { "source_segments": len(segments), @@ -421,7 +458,7 @@ def build_row_ids(input_receipt: dict[str, Any], fixture: dict[str, Any]) -> dic for index, _conflict in enumerate(fixture["extraction"]["conflicts"]) } return { - "source_capture_id": stable_uuid(input_receipt["artifact_sha256"], "source-capture"), + "source_capture_id": stable_uuid(replay_seed, "source-capture"), "claim_extraction_ids": claim_ids, "evidence_extraction_ids": evidence_ids, "duplicate_assessment_ids": duplicate_ids, @@ -507,17 +544,9 @@ def build_parent_packet( "source_locator": input_receipt["source_locator"], "extractor": input_receipt["extractor"], "extraction_spec_sha256": input_receipt["extraction_spec_sha256"], + "replay_identity_components": input_receipt["replay_identity_components"], "replay_identity_sha256": input_receipt["replay_identity_sha256"], - "segments": [ - { - "id": segment["id"], - "locator": segment["locator"], - "json_pointer": segment["json_pointer"], - "content_sha256": segment["content_sha256"], - "text_sha256": segment["text_sha256"], - } - for segment in fixture["segments"] - ], + "segments": normalized_segment_manifest(fixture["segments"]), "row_ids": row_ids, "candidate_counts": input_receipt["counts"], } @@ -547,6 +576,100 @@ def build_parent_packet( } +def _independent_planned_uuid(parent_proposal_id: str, kind: str, key: str) -> str: + """Reproduce the public deterministic-ID contract without calling the planner.""" + return str(uuid.uuid5(uuid.NAMESPACE_URL, f"teleo:kb-rich-contract:{parent_proposal_id}:{kind}:{key}")) + + +def expected_planned_graph_identity(fixture: dict[str, Any], row_ids: dict[str, Any]) -> dict[str, Any]: + """Derive planned row IDs and FK identities directly from normalized source candidates.""" + parent_id = row_ids["parent_proposal_id"] + claims = fixture["extraction"]["claims"] + claim_ids = { + claim["claim_key"]: _independent_planned_uuid(parent_id, "claim", claim["claim_key"]) for claim in claims + } + referenced_segment_ids: list[str] = [] + for claim in claims: + for evidence in claim["evidence"]: + if evidence["segment_id"] not in referenced_segment_ids: + referenced_segment_ids.append(evidence["segment_id"]) + for duplicate in fixture["extraction"]["duplicates"]: + if duplicate["segment_id"] not in referenced_segment_ids: + referenced_segment_ids.append(duplicate["segment_id"]) + + segment_source_ids = { + segment_id: _independent_planned_uuid( + parent_id, + "source", + _segment_source_key(fixture["source"]["source_key"], segment_id), + ) + for segment_id in referenced_segment_ids + } + body_source_ids = { + claim["claim_key"]: _independent_planned_uuid(parent_id, "claim-body-source", claim["claim_key"]) + for claim in claims + } + + evidence_rows: list[dict[str, Any]] = [] + seen_evidence: set[tuple[str, str, str]] = set() + + def append_evidence(claim_id: str, source_id: str, role: str) -> None: + key = (claim_id, source_id, role) + if key in seen_evidence: + return + seen_evidence.add(key) + evidence_rows.append( + { + "claim_id": claim_id, + "source_id": source_id, + "role": role, + "weight": None, + "created_by": None, + } + ) + + for claim in claims: + claim_key = claim["claim_key"] + claim_id = claim_ids[claim_key] + append_evidence(claim_id, body_source_ids[claim_key], "illustrates") + for evidence in claim["evidence"]: + append_evidence(claim_id, segment_source_ids[evidence["segment_id"]], evidence["role"]) + + edge_rows: list[dict[str, Any]] = [] + seen_edges: set[tuple[str, str, str]] = set() + for claim in claims: + from_claim = claim_ids[claim["claim_key"]] + for edge in claim["edges"]: + target_key = _require_text(edge.get("target"), f"claim {claim['claim_key']} edge target") + to_claim = claim_ids.get(target_key) + if to_claim is None: + raise CanaryError(f"claim {claim['claim_key']} edge references unknown claim {target_key}") + edge_type = _require_text(edge.get("edge_type"), f"claim {claim['claim_key']} edge type") + identity = (from_claim, to_claim, edge_type) + if identity in seen_edges: + continue + seen_edges.add(identity) + edge_rows.append( + { + "from_claim": from_claim, + "to_claim": to_claim, + "edge_type": edge_type, + "weight": edge.get("weight"), + "created_by": None, + } + ) + + return { + "planned_claim_ids": [claim_ids[claim["claim_key"]] for claim in claims], + "planned_source_ids": [ + *(segment_source_ids[segment_id] for segment_id in referenced_segment_ids), + *(body_source_ids[claim["claim_key"]] for claim in claims), + ], + "planned_evidence_rows": evidence_rows, + "planned_edge_rows": edge_rows, + } + + def build_schema_sql() -> str: return rf"""\set ON_ERROR_STOP on create schema kb_canary; @@ -664,6 +787,116 @@ rollback; """ +def expected_persisted_rows( + fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, Any] +) -> dict[str, list[dict[str, Any]]]: + """Project the complete relational identity expected from one fixture.""" + source = fixture["source"] + expected: dict[str, list[dict[str, Any]]] = { + "source_captures": [ + { + "id": row_ids["source_capture_id"], + "source_identity": source["identity"], + "source_type": source["source_type"], + "title": source["title"], + "locator": source["locator"], + "artifact_path": input_receipt["artifact_path"], + "artifact_sha256": input_receipt["artifact_sha256"], + "content_sha256": input_receipt["source_content_sha256"], + "replay_identity_sha256": input_receipt["replay_identity_sha256"], + "metadata": {**source["metadata"], "extractor": input_receipt["extractor"]}, + } + ], + "claim_extractions": [], + "evidence_extractions": [], + "duplicate_assessments": [], + "conflict_assessments": [], + } + segment_by_id = {segment["id"]: segment for segment in fixture["segments"]} + for claim in fixture["extraction"]["claims"]: + claim_id = row_ids["claim_extraction_ids"][claim["claim_key"]] + expected["claim_extractions"].append( + { + "id": claim_id, + "source_capture_id": row_ids["source_capture_id"], + "claim_key": claim["claim_key"], + "body": claim["body"], + "metadata": { + **claim["metadata"], + "claim_type": claim["type"], + "confidence": claim["confidence"], + "text": claim["text"], + "extractor": input_receipt["extractor"], + "replay_identity_sha256": input_receipt["replay_identity_sha256"], + }, + } + ) + for index, evidence in enumerate(claim["evidence"]): + evidence_key = f"{claim['claim_key']}:{index}" + segment = segment_by_id[evidence["segment_id"]] + expected["evidence_extractions"].append( + { + "id": row_ids["evidence_extraction_ids"][evidence_key], + "claim_extraction_id": claim_id, + "source_capture_id": row_ids["source_capture_id"], + "source_segment_id": segment["id"], + "source_locator": segment["locator"], + "source_json_pointer": segment["json_pointer"], + "role": evidence["role"], + "body": evidence["excerpt"], + "body_sha256": sha256_bytes(evidence["excerpt"].encode()), + "source_content_sha256": segment["content_sha256"], + "metadata": { + **_require_object(evidence.get("metadata", {}), f"evidence {evidence_key}.metadata"), + "source_text_sha256": segment["text_sha256"], + }, + } + ) + for index, duplicate in enumerate(fixture["extraction"]["duplicates"]): + expected["duplicate_assessments"].append( + { + "id": row_ids["duplicate_assessment_ids"][str(index)], + "source_capture_id": row_ids["source_capture_id"], + "source_segment_id": duplicate["segment_id"], + "source_locator": duplicate["source_locator"], + "duplicate_of_claim_key": duplicate["duplicate_of_claim_key"], + "excerpt": duplicate["excerpt"], + "excerpt_sha256": sha256_bytes(duplicate["excerpt"].encode()), + "reason": duplicate["reason"], + "metadata": { + "json_pointer": duplicate["source_json_pointer"], + "source_content_sha256": duplicate["source_content_sha256"], + "source_text_sha256": duplicate["source_text_sha256"], + }, + } + ) + for index, conflict in enumerate(fixture["extraction"]["conflicts"]): + expected["conflict_assessments"].append( + { + "id": row_ids["conflict_assessment_ids"][str(index)], + "source_capture_id": row_ids["source_capture_id"], + "from_claim_key": conflict["from_claim_key"], + "to_claim_key": conflict["to_claim_key"], + "relationship": conflict["relationship"], + "metadata": { + key: value + for key, value in conflict.items() + if key not in {"from_claim_key", "to_claim_key", "relationship"} + }, + } + ) + return expected + + +def rows_exact(actual: Any, expected: list[dict[str, Any]]) -> bool: + """Compare complete row identities independent of database result order.""" + return ( + isinstance(actual, list) + and all(isinstance(row, dict) for row in actual) + and sorted(actual, key=canonical_json_bytes) == sorted(expected, key=canonical_json_bytes) + ) + + def build_capture_sql(fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, Any]) -> str: source = fixture["source"] q = ap.sql_literal @@ -869,41 +1102,88 @@ class DockerPostgres: } -def proposal_links(readback: dict[str, Any], input_receipt: dict[str, Any]) -> dict[str, Any]: - proposal = readback["staged_proposal"] - apply_payload = proposal["payload"]["apply_payload"] - source_rows = apply_payload["sources"] - evidence_rows = apply_payload["evidence"] +def proposal_readback_parts( + readback: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]: + """Extract typed proposal layers so every verifier path fails closed consistently.""" + proposal_value = readback.get("staged_proposal") + proposal = proposal_value if isinstance(proposal_value, dict) else {} + payload_value = proposal.get("payload") + payload = payload_value if isinstance(payload_value, dict) else {} + apply_value = payload.get("apply_payload") + apply_payload = apply_value if isinstance(apply_value, dict) else {} + manifest_value = apply_payload.get("normalization_manifest") + manifest = manifest_value if isinstance(manifest_value, dict) else {} + ingestion_value = manifest.get("ingestion_manifest") + ingestion_manifest = ingestion_value if isinstance(ingestion_value, dict) else {} + assessment_value = manifest.get("dedupe_conflict_assessment") + assessment = assessment_value if isinstance(assessment_value, dict) else {} + return proposal, apply_payload, manifest, ingestion_manifest, assessment + + +def proposal_links(readback: dict[str, Any]) -> dict[str, Any]: + """Project observed proposal row identities and links without judging them.""" + proposal, apply_payload, _manifest, ingestion_manifest, _assessment = proposal_readback_parts(readback) + claim_rows = apply_payload.get("claims") if isinstance(apply_payload.get("claims"), list) else [] + source_rows = apply_payload.get("sources") if isinstance(apply_payload.get("sources"), list) else [] + evidence_rows = apply_payload.get("evidence") if isinstance(apply_payload.get("evidence"), list) else [] + edge_rows = apply_payload.get("edges") if isinstance(apply_payload.get("edges"), list) else [] + planned_evidence_rows = [ + { + "claim_id": row.get("claim_id"), + "source_id": row.get("source_id"), + "role": row.get("role"), + "weight": row.get("weight"), + "created_by": row.get("created_by"), + } + for row in evidence_rows + if isinstance(row, dict) + ] + planned_edge_rows = [ + { + "from_claim": row.get("from_claim"), + "to_claim": row.get("to_claim"), + "edge_type": row.get("edge_type"), + "weight": row.get("weight"), + "created_by": row.get("created_by"), + } + for row in edge_rows + if isinstance(row, dict) + ] return { - "proposal_id": proposal["id"], - "proposal_status": proposal["status"], - "proposal_source_ref": proposal["source_ref"], - "planned_claim_ids": [row["id"] for row in apply_payload["claims"]], - "planned_source_ids": [row["id"] for row in source_rows], + "proposal_id": proposal.get("id"), + "proposal_status": proposal.get("status"), + "proposal_source_ref": proposal.get("source_ref"), + "planned_claim_ids": [row.get("id") for row in claim_rows if isinstance(row, dict)], + "planned_source_ids": [row.get("id") for row in source_rows if isinstance(row, dict)], "planned_evidence_keys": [ - {"claim_id": row["claim_id"], "source_id": row["source_id"], "role": row["role"]} for row in evidence_rows + {key: row[key] for key in ("claim_id", "source_id", "role")} for row in planned_evidence_rows ], "planned_edge_keys": [ - { - "from_claim": row["from_claim"], - "to_claim": row["to_claim"], - "edge_type": row["edge_type"], - } - for row in apply_payload["edges"] + {key: row[key] for key in ("from_claim", "to_claim", "edge_type")} for row in planned_edge_rows ], + "planned_evidence_rows": planned_evidence_rows, + "planned_edge_rows": planned_edge_rows, "planned_counts": { - "claims": len(apply_payload["claims"]), + "claims": len(claim_rows), "sources": len(source_rows), "evidence": len(evidence_rows), - "edges": len(apply_payload["edges"]), + "edges": len(edge_rows), }, - "input_hash_in_manifest": ( - apply_payload["normalization_manifest"].get("ingestion_manifest", {}).get("artifact_sha256") - == input_receipt["artifact_sha256"] - ), + "manifest_artifact_sha256": ingestion_manifest.get("artifact_sha256"), + "manifest_replay_identity_sha256": ingestion_manifest.get("replay_identity_sha256"), + "manifest_row_ids": ingestion_manifest.get("row_ids"), + } + + +def proposal_link_proof(readback: dict[str, Any], input_receipt: dict[str, Any]) -> dict[str, Any]: + """Attach explicit comparisons to the pure observed link projection for receipts.""" + links = proposal_links(readback) + return { + **links, + "input_hash_in_manifest": links["manifest_artifact_sha256"] == input_receipt["artifact_sha256"], "replay_identity_in_manifest": ( - apply_payload["normalization_manifest"].get("ingestion_manifest", {}).get("replay_identity_sha256") - == input_receipt["replay_identity_sha256"] + links["manifest_replay_identity_sha256"] == input_receipt["replay_identity_sha256"] ), } @@ -916,17 +1196,43 @@ def evaluate_checks( canonical_before: dict[str, Any], canonical_after: dict[str, Any], ) -> dict[str, bool]: - source_rows = readback.get("source_captures") or [] - claim_rows = readback.get("claim_extractions") or [] - evidence_rows = readback.get("evidence_extractions") or [] - duplicate_rows = readback.get("duplicate_assessments") or [] - conflict_rows = readback.get("conflict_assessments") or [] - proposal = readback.get("staged_proposal") or {} + source_value = readback.get("source_captures") + claim_value = readback.get("claim_extractions") + evidence_value = readback.get("evidence_extractions") + duplicate_value = readback.get("duplicate_assessments") + conflict_value = readback.get("conflict_assessments") + source_rows = source_value if isinstance(source_value, list) else [] + claim_rows = claim_value if isinstance(claim_value, list) else [] + evidence_rows = evidence_value if isinstance(evidence_value, list) else [] + duplicate_rows = duplicate_value if isinstance(duplicate_value, list) else [] + conflict_rows = conflict_value if isinstance(conflict_value, list) else [] + proposal, apply_payload, _manifest, ingestion_manifest, assessment = proposal_readback_parts(readback) counts = input_receipt["counts"] - links = proposal_links(readback, input_receipt) - manifest = ((proposal.get("payload") or {}).get("apply_payload") or {}).get("normalization_manifest") or {} - ingestion_manifest = manifest.get("ingestion_manifest") or {} - assessment = manifest.get("dedupe_conflict_assessment") or {} + links = proposal_links(readback) + deterministic_row_ids = build_row_ids(input_receipt, fixture) + expected_rows = expected_persisted_rows(fixture, input_receipt, deterministic_row_ids) + expected_parent = build_parent_packet(fixture, input_receipt, deterministic_row_ids) + expected_child = normalized_stage.prepare_normalized_child(expected_parent) + expected_apply_payload = expected_child["payload"]["apply_payload"] + persisted_proposal_envelope_fields = ( + "id", + "proposal_type", + "status", + "proposed_by_handle", + "proposed_by_agent_id", + "channel", + "source_ref", + "rationale", + "reviewed_by_handle", + "reviewed_by_agent_id", + "reviewed_at", + "review_note", + "applied_by_handle", + "applied_by_agent_id", + "applied_at", + ) + expected_proposal_envelope = {key: expected_child.get(key) for key in persisted_proposal_envelope_fields} + actual_proposal_envelope = {key: proposal.get(key) for key in persisted_proposal_envelope_fields} expected_db_counts = { "source_captures": 1, "claim_extractions": counts["claim_candidates"], @@ -935,63 +1241,91 @@ def evaluate_checks( "conflict_assessments": counts["conflict_relationships"], "staged_proposals": 1, } - segment_by_id = {segment["id"]: segment for segment in fixture["segments"]} - evidence_locations_exact = all( - row.get("source_locator") == segment_by_id.get(row.get("source_segment_id"), {}).get("locator") - and row.get("source_content_sha256") - == segment_by_id.get(row.get("source_segment_id"), {}).get("content_sha256") - and row.get("body") in segment_by_id.get(row.get("source_segment_id"), {}).get("body", "") - for row in evidence_rows + source_rows_exact = rows_exact(source_rows, expected_rows["source_captures"]) + claim_rows_exact = rows_exact(claim_rows, expected_rows["claim_extractions"]) + evidence_rows_exact = rows_exact(evidence_rows, expected_rows["evidence_extractions"]) + duplicate_rows_exact = rows_exact(duplicate_rows, expected_rows["duplicate_assessments"]) + conflict_rows_exact = rows_exact(conflict_rows, expected_rows["conflict_assessments"]) + proposal_envelope_exact = actual_proposal_envelope == expected_proposal_envelope + planned_rows_exact = all( + apply_payload.get(collection) == expected_apply_payload.get(collection) + for collection in ("claims", "sources", "evidence", "edges") ) + graph_collections = {"claims", "sources", "evidence", "edges"} + apply_auxiliary_exact = {key: value for key, value in apply_payload.items() if key not in graph_collections} == { + key: value for key, value in expected_apply_payload.items() if key not in graph_collections + } + expected_graph_identity = expected_planned_graph_identity(fixture, deterministic_row_ids) + actual_graph_identity = { + key: links[key] + for key in ("planned_claim_ids", "planned_source_ids", "planned_evidence_rows", "planned_edge_rows") + } + independent_graph_identity_exact = actual_graph_identity == expected_graph_identity + pending_state = normalized_stage.bound._state_semantics(proposal, "pending_review") + source_row = source_rows[0] if len(source_rows) == 1 and isinstance(source_rows[0], dict) else {} + claim_rows_are_objects = all(isinstance(row, dict) for row in claim_rows) return { "source_only_artifact_hash_persisted": ( - len(source_rows) == 1 and source_rows[0].get("artifact_sha256") == input_receipt["artifact_sha256"] + len(source_rows) == 1 and source_row.get("artifact_sha256") == input_receipt["artifact_sha256"] ), + "source_capture_identity_exact": source_rows_exact, + "deterministic_row_ids_recomputed_exact": row_ids == deterministic_row_ids, "replay_identity_persisted": ( len(source_rows) == 1 - and source_rows[0].get("replay_identity_sha256") == input_receipt["replay_identity_sha256"] + and source_row.get("replay_identity_sha256") == input_receipt["replay_identity_sha256"] ), "extractor_name_and_version_persisted": ( ingestion_manifest.get("extractor") == input_receipt["extractor"] - and all(row.get("metadata", {}).get("extractor") == input_receipt["extractor"] for row in claim_rows) + and claim_rows_are_objects + and all( + isinstance(row.get("metadata"), dict) and row["metadata"].get("extractor") == input_receipt["extractor"] + for row in claim_rows + ) ), "all_claim_candidates_persisted_once": ( - {row.get("claim_key") for row in claim_rows} + claim_rows_are_objects + and {row.get("claim_key") for row in claim_rows} == {claim["claim_key"] for claim in fixture["extraction"]["claims"]} and len(claim_rows) == counts["claim_candidates"] ), + "claim_extraction_identities_and_fks_exact": claim_rows_exact, "all_evidence_has_exact_source_location": ( - len(evidence_rows) == counts["evidence_candidates"] and evidence_locations_exact + len(evidence_rows) == counts["evidence_candidates"] and evidence_rows_exact ), + "evidence_extraction_identities_and_fks_exact": evidence_rows_exact, "duplicate_judgments_persisted": ( len(duplicate_rows) == counts["duplicate_judgments"] + and duplicate_rows_exact and assessment.get("duplicates") == fixture["extraction"]["duplicates"] ), "conflicts_and_relationships_persisted": ( len(conflict_rows) == counts["conflict_relationships"] + and conflict_rows_exact and assessment.get("conflicts") == fixture["extraction"]["conflicts"] - and links["planned_counts"]["edges"] == counts["conflict_relationships"] + and links["planned_edge_rows"] == expected_graph_identity["planned_edge_rows"] + ), + "planned_graph_matches_independent_source_oracle": independent_graph_identity_exact, + "planned_proposal_identities_and_linkages_exact": ( + proposal_envelope_exact + and planned_rows_exact + and apply_auxiliary_exact + and independent_graph_identity_exact + and links["manifest_row_ids"] == deterministic_row_ids + and links["proposal_id"] == expected_child["id"] + and links["proposal_status"] == expected_child["status"] + and links["proposal_source_ref"] == expected_child["source_ref"] ), "database_candidate_counts_exact": readback.get("counts") == expected_db_counts, - "proposal_is_pending_review": proposal.get("status") == "pending_review", + "proposal_is_pending_review": pending_state["status_matches"], "proposal_has_replayable_ingestion_manifest": ( - links["input_hash_in_manifest"] - and links["replay_identity_in_manifest"] - and ingestion_manifest.get("segments") - == [ - { - "id": segment["id"], - "locator": segment["locator"], - "json_pointer": segment["json_pointer"], - "content_sha256": segment["content_sha256"], - "text_sha256": segment["text_sha256"], - } - for segment in fixture["segments"] - ] + links["manifest_artifact_sha256"] == input_receipt["artifact_sha256"] + and links["manifest_replay_identity_sha256"] == input_receipt["replay_identity_sha256"] + and ingestion_manifest == expected_apply_payload["normalization_manifest"]["ingestion_manifest"] ), - "no_review_or_apply_metadata": all( - proposal.get(key) is None - for key in ("reviewed_by_handle", "reviewed_at", "applied_by_handle", "applied_at") + "no_review_or_apply_metadata": ( + pending_state["review_fields_match"] + and pending_state["apply_fields_match"] + and proposal.get("review_note") is None ), "canonical_snapshot_nonempty": ( canonical_snapshot_complete(canonical_before) and canonical_snapshot_complete(canonical_after) @@ -1005,22 +1339,24 @@ def evaluate_checks( def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any]: - fixture, input_receipt = load_fixture(fixture_path) - row_ids = build_row_ids(input_receipt, fixture) - parent = build_parent_packet(fixture, input_receipt, row_ids) - child = normalized_stage.prepare_normalized_child(parent) - container_name = f"working-leo-ingest-{uuid.uuid4().hex[:12]}" - postgres = DockerPostgres(container_name) receipt: dict[str, Any] = { "schema": SCHEMA, "status": "fail", "required_tier": "T2_runtime_to_review_queue_staging", - "input": input_receipt, - "row_ids": row_ids, + "input": {"scenario_path": str(fixture_path.resolve())}, + "row_ids": {}, "production_mutation_attempted": False, "canonical_apply_attempted": False, } + postgres: DockerPostgres | None = None try: + fixture, input_receipt = load_fixture(fixture_path) + row_ids = build_row_ids(input_receipt, fixture) + parent = build_parent_packet(fixture, input_receipt, row_ids) + child = normalized_stage.prepare_normalized_child(parent) + receipt["input"] = input_receipt + receipt["row_ids"] = row_ids + postgres = DockerPostgres(f"working-leo-ingest-{uuid.uuid4().hex[:12]}") receipt["environment"] = postgres.start() postgres.psql(build_schema_sql()) canonical_before = postgres.psql_json(build_canonical_snapshot_sql()) @@ -1039,7 +1375,7 @@ def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any] "stage_command_result": stage_result, "rows": readback, "candidate_counts": input_receipt["counts"], - "row_link_fields_proven": proposal_links(readback, input_receipt), + "row_link_fields_proven": proposal_link_proof(readback, input_receipt), "canonical": { "fingerprint_before": canonical_sha256(canonical_before), "fingerprint_after": canonical_sha256(canonical_after), @@ -1050,19 +1386,45 @@ def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any] "status": "pass" if all(checks.values()) else "fail", } ) - except (CanaryError, OSError, ValueError, KeyError, normalized_stage.bound.CheckpointError) as exc: + except ( + CanaryError, + OSError, + ValueError, + KeyError, + TypeError, + AttributeError, + IndexError, + normalized_stage.bound.CheckpointError, + ) as exc: receipt["error"] = {"type": type(exc).__name__, "message": str(exc)} finally: - receipt["cleanup"] = postgres.cleanup() - receipt["cleanup"]["network_mode_none"] = receipt.get("environment", {}).get("network_mode") == "none" + if postgres is None: + receipt["cleanup"] = { + "remove_attempted": False, + "remove_returncode": None, + "container_absent": True, + "docker_volume_mounts_observed": [], + "anonymous_or_named_volume_created": False, + "runtime_not_started": True, + "network_mode_none": True, + } + else: + receipt["cleanup"] = postgres.cleanup() + receipt["cleanup"]["runtime_not_started"] = not postgres.started + receipt["cleanup"]["network_mode_none"] = receipt.get("environment", {}).get("network_mode") == "none" receipt["cleanup"]["no_production_target_referenced"] = receipt["cleanup"]["network_mode_none"] if not all((receipt["cleanup"]["container_absent"], receipt["cleanup"]["network_mode_none"])): receipt["status"] = "fail" - receipt["strongest_claim"] = ( - "One source-only artifact was parsed into exact-location candidate claims, duplicate/conflict " - "assessments, and a replay-bound pending_review proposal in disposable networkless Postgres; " - "representative canonical row contents remained fingerprint-identical." - ) + if receipt["status"] == "pass": + receipt["strongest_claim"] = ( + "One source-only artifact was parsed into exact-location candidate claims, duplicate/conflict " + "assessments, and a replay-bound pending_review proposal in disposable networkless Postgres; " + "representative canonical row contents remained fingerprint-identical." + ) + else: + receipt["strongest_claim"] = ( + "No source-to-review-staging capability claim is made because acceptance failed closed." + ) receipt["residual_gap"] = ( "This proves the deterministic local fixture extractor and review queue only; it does not prove " "arbitrary generative extraction, approval/apply, hosted runtime, or production mutation." @@ -1073,21 +1435,74 @@ def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any] return receipt +def receipt_proves_canonical_invariance(receipt: dict[str, Any]) -> bool: + """Recompute the suite-level canonical verdict from receipt evidence.""" + canonical = receipt.get("canonical") + checks = receipt.get("checks") + if not isinstance(canonical, dict) or not isinstance(checks, dict): + return False + before = canonical.get("fingerprint_before") + after = canonical.get("fingerprint_after") + return ( + isinstance(before, str) + and isinstance(after, str) + and re.fullmatch(r"[0-9a-f]{64}", before) is not None + and re.fullmatch(r"[0-9a-f]{64}", after) is not None + and before == after + and canonical.get("unchanged") is True + and checks.get("canonical_fingerprint_unchanged") is True + ) + + def run_suite(fixture_paths: list[Path], output: Path | None = None) -> dict[str, Any]: receipts = [run_canary(path.resolve()) for path in fixture_paths] + all_supplied_pass = bool(receipts) and all(item.get("status") == "pass" for item in receipts) + document_fixture_passed = any( + item.get("status") == "pass" and item.get("input", {}).get("artifact_format") == "plain_text" + for item in receipts + ) + social_conversation_fixture_passed = any( + item.get("status") == "pass" and item.get("input", {}).get("artifact_format") == "telegram_jsonl" + for item in receipts + ) + canonical_fingerprint_invariant_all = bool(receipts) and all( + receipt_proves_canonical_invariance(item) for item in receipts + ) + required_shape_coverage = document_fixture_passed and social_conversation_fixture_passed + full_suite_passed = all_supplied_pass and required_shape_coverage and canonical_fingerprint_invariant_all + if not all_supplied_pass or not canonical_fingerprint_invariant_all: + status = "fail" + elif full_suite_passed: + status = "pass" + else: + status = "partial" + missing_required_coverage = [] + if not document_fixture_passed: + missing_required_coverage.append("document") + if not social_conversation_fixture_passed: + missing_required_coverage.append("social_conversation") + if not canonical_fingerprint_invariant_all: + missing_required_coverage.append("canonical_fingerprint_invariance") suite = { "schema": SUITE_SCHEMA, - "status": "pass" if receipts and all(item["status"] == "pass" for item in receipts) else "fail", + "status": status, "required_tier": "T2_runtime_to_review_queue_staging", + "current_tier": ( + "T2_runtime_to_review_queue_staging" + if full_suite_passed + else "T2_runtime_single_fixture" + if status == "partial" + else "runtime_verification_failed" + ), "fixture_count": len(receipts), - "document_fixture_passed": any( - item["status"] == "pass" and item["input"]["artifact_format"] == "plain_text" for item in receipts - ), - "social_conversation_fixture_passed": any( - item["status"] == "pass" and item["input"]["artifact_format"] == "telegram_jsonl" for item in receipts - ), - "canonical_fingerprint_invariant_all": bool(receipts) - and all(item.get("canonical", {}).get("unchanged") is True for item in receipts), + "all_supplied_fixtures_passed": all_supplied_pass, + "document_fixture_passed": document_fixture_passed, + "social_conversation_fixture_passed": social_conversation_fixture_passed, + "canonical_fingerprint_invariant_all": canonical_fingerprint_invariant_all, + "required_shape_coverage_met": required_shape_coverage, + "coverage_status": "full" if required_shape_coverage else "partial", + "missing_required_coverage": missing_required_coverage, + "full_suite_passed": full_suite_passed, "receipts": receipts, } if output: diff --git a/tests/test_run_leo_local_ingestion_proposal_canary.py b/tests/test_run_leo_local_ingestion_proposal_canary.py index cfae09e..1290dc4 100644 --- a/tests/test_run_leo_local_ingestion_proposal_canary.py +++ b/tests/test_run_leo_local_ingestion_proposal_canary.py @@ -32,6 +32,37 @@ def _copy_scenario(tmp_path: Path, scenario_path: Path) -> Path: return scenario_target +def _canonical_snapshot() -> dict: + return { + "claims": [{"id": "1"}], + "sources": [{"id": "2"}], + "claim_evidence": [{"claim_id": "1"}], + "claim_edges": [{"from_claim": "1"}], + } + + +def _row_id_values(row_ids: dict) -> set[str]: + values: set[str] = set() + for value in row_ids.values(): + if isinstance(value, dict): + values.update(_row_id_values(value)) + else: + values.add(value) + return values + + +def _planned_uuid_values(child: dict) -> set[str]: + apply_payload = child["payload"]["apply_payload"] + values = {child["id"]} + for row in apply_payload["claims"] + apply_payload["sources"]: + values.add(row["id"]) + for row in apply_payload["evidence"]: + values.update((row["claim_id"], row["source_id"])) + for row in apply_payload["edges"]: + values.update((row["from_claim"], row["to_claim"])) + return values + + def _synthetic_readback(fixture: dict, input_receipt: dict, row_ids: dict, child: dict) -> dict: claim_rows = [] evidence_rows = [] @@ -44,7 +75,14 @@ def _synthetic_readback(fixture: dict, input_receipt: dict, row_ids: dict, child "source_capture_id": row_ids["source_capture_id"], "claim_key": claim["claim_key"], "body": claim["body"], - "metadata": {"extractor": input_receipt["extractor"]}, + "metadata": { + **claim["metadata"], + "claim_type": claim["type"], + "confidence": claim["confidence"], + "text": claim["text"], + "extractor": input_receipt["extractor"], + "replay_identity_sha256": input_receipt["replay_identity_sha256"], + }, } ) for index, evidence in enumerate(claim["evidence"]): @@ -56,10 +94,52 @@ def _synthetic_readback(fixture: dict, input_receipt: dict, row_ids: dict, child "source_capture_id": row_ids["source_capture_id"], "source_segment_id": segment["id"], "source_locator": segment["locator"], + "source_json_pointer": segment["json_pointer"], + "role": evidence["role"], "source_content_sha256": segment["content_sha256"], "body": evidence["excerpt"], + "body_sha256": canary.sha256_bytes(evidence["excerpt"].encode()), + "metadata": { + **evidence.get("metadata", {}), + "source_text_sha256": segment["text_sha256"], + }, } ) + duplicate_rows = [] + for index, duplicate in enumerate(fixture["extraction"]["duplicates"]): + duplicate_rows.append( + { + "id": row_ids["duplicate_assessment_ids"][str(index)], + "source_capture_id": row_ids["source_capture_id"], + "source_segment_id": duplicate["segment_id"], + "source_locator": duplicate["source_locator"], + "duplicate_of_claim_key": duplicate["duplicate_of_claim_key"], + "excerpt": duplicate["excerpt"], + "excerpt_sha256": canary.sha256_bytes(duplicate["excerpt"].encode()), + "reason": duplicate["reason"], + "metadata": { + "json_pointer": duplicate["source_json_pointer"], + "source_content_sha256": duplicate["source_content_sha256"], + "source_text_sha256": duplicate["source_text_sha256"], + }, + } + ) + conflict_rows = [] + for index, conflict in enumerate(fixture["extraction"]["conflicts"]): + conflict_rows.append( + { + "id": row_ids["conflict_assessment_ids"][str(index)], + "source_capture_id": row_ids["source_capture_id"], + "from_claim_key": conflict["from_claim_key"], + "to_claim_key": conflict["to_claim_key"], + "relationship": conflict["relationship"], + "metadata": { + key: value + for key, value in conflict.items() + if key not in {"from_claim_key", "to_claim_key", "relationship"} + }, + } + ) proposal = { **child, "reviewed_by_handle": None, @@ -71,14 +151,22 @@ def _synthetic_readback(fixture: dict, input_receipt: dict, row_ids: dict, child return { "source_captures": [ { + "id": row_ids["source_capture_id"], + "source_identity": fixture["source"]["identity"], + "source_type": fixture["source"]["source_type"], + "title": fixture["source"]["title"], + "locator": fixture["source"]["locator"], + "artifact_path": input_receipt["artifact_path"], "artifact_sha256": input_receipt["artifact_sha256"], + "content_sha256": input_receipt["source_content_sha256"], "replay_identity_sha256": input_receipt["replay_identity_sha256"], + "metadata": {**fixture["source"]["metadata"], "extractor": input_receipt["extractor"]}, } ], "claim_extractions": claim_rows, "evidence_extractions": evidence_rows, - "duplicate_assessments": [{} for _item in fixture["extraction"]["duplicates"]], - "conflict_assessments": [{} for _item in fixture["extraction"]["conflicts"]], + "duplicate_assessments": duplicate_rows, + "conflict_assessments": conflict_rows, "staged_proposal": proposal, "counts": { "source_captures": 1, @@ -208,13 +296,98 @@ def test_replay_identity_and_ids_change_with_extractor_version(tmp_path: Path) - assert original["artifact_sha256"] == changed["artifact_sha256"] assert original["replay_identity_sha256"] != changed["replay_identity_sha256"] - assert original_ids["source_capture_id"] == changed_ids["source_capture_id"] + assert original_ids["source_capture_id"] != changed_ids["source_capture_id"] assert original_ids["claim_extraction_ids"] != changed_ids["claim_extraction_ids"] assert original_ids["parent_proposal_id"] != changed_ids["parent_proposal_id"] assert original_child["payload_sha256"] != changed_child["payload_sha256"] assert fixture["segments"] == changed_fixture["segments"] +@pytest.mark.parametrize( + ("source_field", "replacement"), + ( + ("identity", "document:mutated-exact-source-identity"), + ("locator", "fixture://working-leo/mutated-exact-source-locator"), + ), +) +def test_replay_identity_and_every_row_id_bind_exact_source_identity( + tmp_path: Path, source_field: str, replacement: str +) -> None: + scenario_path = _copy_scenario(tmp_path, canary.DEFAULT_DOCUMENT_FIXTURE) + fixture, before, before_ids, _parent, before_child = _loaded(scenario_path) + scenario = json.loads(scenario_path.read_text(encoding="utf-8")) + scenario["source"][source_field] = replacement + scenario_path.write_text(json.dumps(scenario), encoding="utf-8") + + changed_fixture, after, after_ids, _changed_parent, after_child = _loaded(scenario_path) + + assert before["artifact_sha256"] == after["artifact_sha256"] + assert before["extraction_spec_sha256"] == after["extraction_spec_sha256"] + assert before["replay_identity_sha256"] != after["replay_identity_sha256"] + assert _row_id_values(before_ids).isdisjoint(_row_id_values(after_ids)) + assert _planned_uuid_values(before_child).isdisjoint(_planned_uuid_values(after_child)) + assert fixture["source"][source_field] != changed_fixture["source"][source_field] + + +@pytest.mark.parametrize( + ("segment_field", "replacement"), + ( + ("id", "message-900001-mutated"), + ("locator", "telegram://chat/900001/message/999999"), + ("json_pointer", "jsonl://line/999/message"), + ("content_sha256", "a" * 64), + ("text_sha256", "b" * 64), + ), +) +def test_replay_identity_and_every_row_id_bind_complete_normalized_segment( + segment_field: str, replacement: str +) -> None: + fixture, before, before_ids, _parent, before_child = _loaded(canary.DEFAULT_TELEGRAM_FIXTURE) + changed_fixture = copy.deepcopy(fixture) + after = copy.deepcopy(before) + segment = changed_fixture["segments"][-1] + original_id = segment["id"] + segment[segment_field] = replacement + for claim in changed_fixture["extraction"]["claims"]: + for evidence in claim["evidence"]: + if evidence["segment_id"] == original_id: + evidence["segment_id"] = segment["id"] + evidence["source_locator"] = segment["locator"] + evidence["source_json_pointer"] = segment["json_pointer"] + evidence["source_content_sha256"] = segment["content_sha256"] + evidence["source_text_sha256"] = segment["text_sha256"] + for duplicate in changed_fixture["extraction"]["duplicates"]: + if duplicate["segment_id"] == original_id: + duplicate["segment_id"] = segment["id"] + duplicate["source_locator"] = segment["locator"] + duplicate["source_json_pointer"] = segment["json_pointer"] + duplicate["source_content_sha256"] = segment["content_sha256"] + duplicate["source_text_sha256"] = segment["text_sha256"] + after["source_content_sha256"] = canary.canonical_sha256( + canary.normalized_segment_manifest(changed_fixture["segments"]) + ) + after["replay_identity_components"] = canary.replay_identity_payload( + artifact_sha256=after["artifact_sha256"], + artifact_format=after["artifact_format"], + source_identity=after["source_identity"], + source_locator=after["source_locator"], + normalized_segments_sha256=after["source_content_sha256"], + extractor=after["extractor"], + extraction_spec_sha256=after["extraction_spec_sha256"], + ) + after["replay_identity_sha256"] = canary.canonical_sha256(after["replay_identity_components"]) + after_ids = canary.build_row_ids(after, changed_fixture) + after_parent = canary.build_parent_packet(changed_fixture, after, after_ids) + after_child = canary.normalized_stage.prepare_normalized_child(after_parent) + + assert before["artifact_sha256"] == after["artifact_sha256"] + assert before["scenario_sha256"] == after["scenario_sha256"] + assert before["extraction_spec_sha256"] == after["extraction_spec_sha256"] + assert before["replay_identity_sha256"] != after["replay_identity_sha256"] + assert _row_id_values(before_ids).isdisjoint(_row_id_values(after_ids)) + assert _planned_uuid_values(before_child).isdisjoint(_planned_uuid_values(after_child)) + + def test_source_byte_tamper_changes_hashes_and_replay_ids(tmp_path: Path) -> None: scenario_path = _copy_scenario(tmp_path, canary.DEFAULT_DOCUMENT_FIXTURE) fixture, before, before_ids, _parent, _child = _loaded(scenario_path) @@ -311,3 +484,299 @@ def test_evaluation_fails_when_exact_evidence_locator_is_changed() -> None: ] is False ) + + +def test_evaluation_recomputes_instead_of_trusting_supplied_row_ids() -> None: + fixture, input_receipt, row_ids, _parent, _child = _loaded(canary.DEFAULT_TELEGRAM_FIXTURE) + tampered_ids = copy.deepcopy(row_ids) + tampered_ids["source_capture_id"] = "00000000-0000-4000-8000-000000009990" + tampered_parent = canary.build_parent_packet(fixture, input_receipt, tampered_ids) + tampered_child = canary.normalized_stage.prepare_normalized_child(tampered_parent) + readback = _synthetic_readback(fixture, input_receipt, tampered_ids, tampered_child) + + checks = canary.evaluate_checks( + fixture, input_receipt, tampered_ids, readback, _canonical_snapshot(), _canonical_snapshot() + ) + + assert checks["deterministic_row_ids_recomputed_exact"] is False + assert checks["source_capture_identity_exact"] is False + assert checks["planned_proposal_identities_and_linkages_exact"] is False + + +def test_independent_graph_oracle_rejects_consistent_generator_edge_corruption( + monkeypatch: pytest.MonkeyPatch, +) -> None: + fixture, input_receipt = canary.load_fixture(canary.DEFAULT_TELEGRAM_FIXTURE) + row_ids = canary.build_row_ids(input_receipt, fixture) + parent = canary.build_parent_packet(fixture, input_receipt, row_ids) + original_prepare = canary.normalized_stage.prepare_normalized_child + + def corrupted_prepare(parent_packet: dict) -> dict: + child = copy.deepcopy(original_prepare(parent_packet)) + child["payload"]["apply_payload"]["edges"][0]["to_claim"] = "00000000-0000-4000-8000-000000009996" + return child + + monkeypatch.setattr(canary.normalized_stage, "prepare_normalized_child", corrupted_prepare) + corrupted_child = corrupted_prepare(parent) + readback = _synthetic_readback(fixture, input_receipt, row_ids, corrupted_child) + + checks = canary.evaluate_checks( + fixture, input_receipt, row_ids, readback, _canonical_snapshot(), _canonical_snapshot() + ) + + assert checks["planned_graph_matches_independent_source_oracle"] is False + assert checks["planned_proposal_identities_and_linkages_exact"] is False + assert checks["conflicts_and_relationships_persisted"] is False + + +@pytest.mark.parametrize( + ("mutation", "failed_check"), + ( + ("source_id", "source_capture_identity_exact"), + ("source_identity", "source_capture_identity_exact"), + ("source_locator", "source_capture_identity_exact"), + ("claim_id", "claim_extraction_identities_and_fks_exact"), + ("claim_source_fk", "claim_extraction_identities_and_fks_exact"), + ("evidence_id", "evidence_extraction_identities_and_fks_exact"), + ("evidence_claim_fk", "evidence_extraction_identities_and_fks_exact"), + ("evidence_source_fk", "evidence_extraction_identities_and_fks_exact"), + ("evidence_segment_id", "evidence_extraction_identities_and_fks_exact"), + ("evidence_json_pointer", "evidence_extraction_identities_and_fks_exact"), + ("evidence_role", "evidence_extraction_identities_and_fks_exact"), + ("evidence_body_sha256", "evidence_extraction_identities_and_fks_exact"), + ("proposal_id", "planned_proposal_identities_and_linkages_exact"), + ("planned_claim_id", "planned_proposal_identities_and_linkages_exact"), + ("planned_source_id", "planned_proposal_identities_and_linkages_exact"), + ("planned_evidence_claim_fk", "planned_proposal_identities_and_linkages_exact"), + ("planned_evidence_source_fk", "planned_proposal_identities_and_linkages_exact"), + ("planned_evidence_role", "planned_proposal_identities_and_linkages_exact"), + ("manifest_row_id", "planned_proposal_identities_and_linkages_exact"), + ), +) +def test_evaluation_rejects_wrong_deterministic_ids_and_every_fk_linkage(mutation: str, failed_check: str) -> None: + fixture, input_receipt, row_ids, _parent, child = _loaded(canary.DEFAULT_TELEGRAM_FIXTURE) + readback = _synthetic_readback(fixture, input_receipt, row_ids, child) + broken = copy.deepcopy(readback) + wrong_uuid = "00000000-0000-4000-8000-000000009999" + apply_payload = broken["staged_proposal"]["payload"]["apply_payload"] + if mutation == "source_id": + broken["source_captures"][0]["id"] = wrong_uuid + elif mutation == "source_identity": + broken["source_captures"][0]["source_identity"] = "fixture:wrong-source-identity" + elif mutation == "source_locator": + broken["source_captures"][0]["locator"] = "fixture://working-leo/wrong-source-locator" + elif mutation == "claim_id": + broken["claim_extractions"][0]["id"] = broken["claim_extractions"][1]["id"] + elif mutation == "claim_source_fk": + broken["claim_extractions"][0]["source_capture_id"] = wrong_uuid + elif mutation == "evidence_id": + broken["evidence_extractions"][0]["id"] = broken["evidence_extractions"][1]["id"] + elif mutation == "evidence_claim_fk": + broken["evidence_extractions"][0]["claim_extraction_id"] = broken["claim_extractions"][1]["id"] + elif mutation == "evidence_source_fk": + broken["evidence_extractions"][0]["source_capture_id"] = wrong_uuid + elif mutation == "evidence_segment_id": + broken["evidence_extractions"][0]["source_segment_id"] = "message-900001-9999" + elif mutation == "evidence_json_pointer": + broken["evidence_extractions"][0]["source_json_pointer"] = "jsonl://line/999/message" + elif mutation == "evidence_role": + broken["evidence_extractions"][0]["role"] = "supports" + elif mutation == "evidence_body_sha256": + broken["evidence_extractions"][0]["body_sha256"] = "d" * 64 + elif mutation == "proposal_id": + broken["staged_proposal"]["id"] = wrong_uuid + elif mutation == "planned_claim_id": + apply_payload["claims"][0]["id"] = apply_payload["claims"][1]["id"] + elif mutation == "planned_source_id": + apply_payload["sources"][0]["id"] = apply_payload["sources"][1]["id"] + elif mutation == "planned_evidence_claim_fk": + apply_payload["evidence"][0]["claim_id"] = apply_payload["claims"][1]["id"] + elif mutation == "planned_evidence_source_fk": + apply_payload["evidence"][0]["source_id"] = apply_payload["sources"][1]["id"] + elif mutation == "planned_evidence_role": + apply_payload["evidence"][0]["role"] = "supports" + elif mutation == "manifest_row_id": + apply_payload["normalization_manifest"]["ingestion_manifest"]["row_ids"]["source_capture_id"] = wrong_uuid + + checks = canary.evaluate_checks( + fixture, input_receipt, row_ids, broken, _canonical_snapshot(), _canonical_snapshot() + ) + + assert checks[failed_check] is False + assert not all(checks.values()) + + +@pytest.mark.parametrize( + ("surface", "field", "replacement", "failed_check"), + ( + ("duplicate", "id", "00000000-0000-4000-8000-000000009991", "duplicate_judgments_persisted"), + ( + "duplicate", + "source_capture_id", + "00000000-0000-4000-8000-000000009992", + "duplicate_judgments_persisted", + ), + ("duplicate", "source_segment_id", "message-900001-9999", "duplicate_judgments_persisted"), + ( + "duplicate", + "source_locator", + "telegram://chat/900001/message/9999", + "duplicate_judgments_persisted", + ), + ("duplicate", "duplicate_of_claim_key", "approval_alone_makes_canonical", "duplicate_judgments_persisted"), + ("duplicate", "excerpt", "fabricated duplicate excerpt", "duplicate_judgments_persisted"), + ("duplicate", "excerpt_sha256", "c" * 64, "duplicate_judgments_persisted"), + ("duplicate", "reason", "fabricated duplicate reason", "duplicate_judgments_persisted"), + ("duplicate", "metadata", {"json_pointer": "fabricated"}, "duplicate_judgments_persisted"), + ("conflict", "id", "00000000-0000-4000-8000-000000009993", "conflicts_and_relationships_persisted"), + ( + "conflict", + "source_capture_id", + "00000000-0000-4000-8000-000000009994", + "conflicts_and_relationships_persisted", + ), + ("conflict", "from_claim_key", "approval_alone_makes_canonical", "conflicts_and_relationships_persisted"), + ( + "conflict", + "to_claim_key", + "pending_review_does_not_change_canonical", + "conflicts_and_relationships_persisted", + ), + ("conflict", "relationship", "supports", "conflicts_and_relationships_persisted"), + ("conflict", "metadata", {"reason": "fabricated"}, "conflicts_and_relationships_persisted"), + ("edge", "from_claim", "00000000-0000-4000-8000-000000009995", "conflicts_and_relationships_persisted"), + ("edge", "to_claim", "00000000-0000-4000-8000-000000009996", "conflicts_and_relationships_persisted"), + ("edge", "edge_type", "supports", "conflicts_and_relationships_persisted"), + ("edge", "weight", 0.5, "conflicts_and_relationships_persisted"), + ("edge", "created_by", "00000000-0000-4000-8000-000000009997", "conflicts_and_relationships_persisted"), + ), +) +def test_evaluation_rejects_mutated_duplicate_conflict_and_edge_identities( + surface: str, field: str, replacement: object, failed_check: str +) -> None: + fixture, input_receipt, row_ids, _parent, child = _loaded(canary.DEFAULT_TELEGRAM_FIXTURE) + broken = _synthetic_readback(fixture, input_receipt, row_ids, child) + if surface == "duplicate": + broken["duplicate_assessments"][0][field] = replacement + elif surface == "conflict": + broken["conflict_assessments"][0][field] = replacement + else: + broken["staged_proposal"]["payload"]["apply_payload"]["edges"][0][field] = replacement + + checks = canary.evaluate_checks( + fixture, input_receipt, row_ids, broken, _canonical_snapshot(), _canonical_snapshot() + ) + + assert checks[failed_check] is False + assert not all(checks.values()) + + +@pytest.mark.parametrize( + ("field", "replacement"), + ( + ("reviewed_by_handle", "reviewer"), + ("reviewed_by_agent_id", "00000000-0000-4000-8000-000000009981"), + ("reviewed_at", "2026-07-15T00:00:00+00:00"), + ("review_note", "fabricated review note"), + ("applied_by_handle", "applier"), + ("applied_by_agent_id", "00000000-0000-4000-8000-000000009982"), + ("applied_at", "2026-07-15T00:00:01+00:00"), + ), +) +def test_evaluation_rejects_every_review_and_apply_metadata_mutation(field: str, replacement: str) -> None: + fixture, input_receipt, row_ids, _parent, child = _loaded(canary.DEFAULT_TELEGRAM_FIXTURE) + broken = _synthetic_readback(fixture, input_receipt, row_ids, child) + broken["staged_proposal"][field] = replacement + + checks = canary.evaluate_checks( + fixture, input_receipt, row_ids, broken, _canonical_snapshot(), _canonical_snapshot() + ) + + assert checks["no_review_or_apply_metadata"] is False + assert checks["planned_proposal_identities_and_linkages_exact"] is False + + +def _suite_child(artifact_format: str, *, passed: bool = True, canonical_unchanged: bool = True) -> dict: + before = "a" * 64 + after = before if canonical_unchanged else "b" * 64 + return { + "status": "pass" if passed else "fail", + "input": {"artifact_format": artifact_format}, + "canonical": { + "fingerprint_before": before, + "fingerprint_after": after, + "unchanged": canonical_unchanged, + }, + "checks": {"canonical_fingerprint_unchanged": canonical_unchanged}, + } + + +def test_single_fixture_suite_is_truthfully_partial(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + monkeypatch.setattr(canary, "run_canary", lambda _path: _suite_child("plain_text")) + + suite = canary.run_suite([tmp_path / "document.scenario.json"]) + + assert suite["status"] == "partial" + assert suite["full_suite_passed"] is False + assert suite["coverage_status"] == "partial" + assert suite["missing_required_coverage"] == ["social_conversation"] + + +def test_malformed_fixture_fails_closed_with_receipt_and_without_runtime(tmp_path: Path) -> None: + malformed = tmp_path / "malformed.scenario.json" + malformed.write_text("{not-json", encoding="utf-8") + + receipt = canary.run_canary(malformed) + suite = canary.run_suite([malformed]) + + assert receipt["status"] == "fail" + assert receipt["error"]["type"] == "CanaryError" + assert receipt["cleanup"]["runtime_not_started"] is True + assert receipt["cleanup"]["container_absent"] is True + assert suite["status"] == "fail" + assert suite["receipts"][0]["error"]["type"] == "CanaryError" + + +@pytest.mark.parametrize( + ("children", "expected_status"), + ( + ( + [_suite_child("plain_text"), _suite_child("telegram_jsonl")], + "pass", + ), + ( + [_suite_child("plain_text", canonical_unchanged=False), _suite_child("telegram_jsonl")], + "fail", + ), + ( + [_suite_child("plain_text"), _suite_child("telegram_jsonl", passed=False)], + "fail", + ), + ), +) +def test_full_suite_status_requires_both_shapes_all_children_and_canonical_invariance( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, children: list[dict], expected_status: str +) -> None: + queue = iter(children) + monkeypatch.setattr(canary, "run_canary", lambda _path: next(queue)) + + suite = canary.run_suite([tmp_path / "document.scenario.json", tmp_path / "telegram.scenario.json"]) + + assert suite["status"] == expected_status + assert suite["full_suite_passed"] is (expected_status == "pass") + + +def test_suite_recomputes_canonical_invariance_instead_of_trusting_stale_boolean( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + children = [_suite_child("plain_text"), _suite_child("telegram_jsonl")] + children[0]["canonical"]["fingerprint_after"] = "c" * 64 + queue = iter(children) + monkeypatch.setattr(canary, "run_canary", lambda _path: next(queue)) + + suite = canary.run_suite([tmp_path / "document.scenario.json", tmp_path / "telegram.scenario.json"]) + + assert suite["status"] == "fail" + assert suite["canonical_fingerprint_invariant_all"] is False + assert suite["full_suite_passed"] is False + assert "canonical_fingerprint_invariance" in suite["missing_required_coverage"] From 40344c91d7c141c6f9993eb3d79ea29c6b33db9d Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 04:00:53 +0200 Subject: [PATCH 10/27] enforce revise strategy approval timestamp provenance --- docs/kb-rebuild-and-recompile.md | 2 + ops/run_local_genesis_ledger_rebuild.py | 72 ++++------ .../test_run_local_genesis_ledger_rebuild.py | 129 +++++++++++++++--- 3 files changed, 135 insertions(+), 68 deletions(-) diff --git a/docs/kb-rebuild-and-recompile.md b/docs/kb-rebuild-and-recompile.md index 00996dc..b21eb56 100644 --- a/docs/kb-rebuild-and-recompile.md +++ b/docs/kb-rebuild-and-recompile.md @@ -240,6 +240,8 @@ The exact v1 claim ceiling is intentionally bounded: only the generated strategy/node rows with the exact receipt rows; - the original transaction timestamp is derived from the fresh strategy `created_at` and must equal every fresh node's `created_at` and `updated_at`. + It must also fall within the immutable, timezone-aware interval + `reviewed_at <= transaction timestamp <= applied_at`. Only node IDs observed as non-retired before apply receive that timestamp; already-retired, unrelated-agent, and shared NULL-agent rows stay untouched; - generated nodes must have no anchors before normalization, preventing a diff --git a/ops/run_local_genesis_ledger_rebuild.py b/ops/run_local_genesis_ledger_rebuild.py index a3361e1..a500fc6 100644 --- a/ops/run_local_genesis_ledger_rebuild.py +++ b/ops/run_local_genesis_ledger_rebuild.py @@ -49,12 +49,9 @@ 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"} + "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 " @@ -346,9 +343,7 @@ def _validate_proposal_rows( ) -def _parse_aware_timestamp( - value: Any, field: str, *, code: str = "invalid_mutating_receipt" -) -> datetime: +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: @@ -360,9 +355,7 @@ def _parse_aware_timestamp( return parsed -def _validated_uuid_list( - value: Any, field: str, *, code: str = "invalid_mutating_state" -) -> list[str]: +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] = [] @@ -382,9 +375,7 @@ def _validate_revise_strategy_receipt_rows( 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" - ) + 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: @@ -395,9 +386,7 @@ def _validate_revise_strategy_receipt_rows( 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" - ) + 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) @@ -418,9 +407,7 @@ def _validate_revise_strategy_receipt_rows( 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] + 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", @@ -448,7 +435,14 @@ def _validate_revise_strategy_receipt_rows( "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" @@ -767,9 +761,7 @@ def _validate_revise_strategy_prestate(value: Any) -> dict[str, Any]: ) 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" - ) + 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" @@ -824,9 +816,7 @@ def _validate_revise_strategy_generated_delta( ) -> 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" - ) + raise ReconstructionError("invalid_mutating_delta", "revise_strategy generated delta must be a row object") try: replay_receipt.build_replay_receipt( entry.receipt["proposal"], @@ -848,18 +838,12 @@ def _validate_revise_strategy_generated_delta( "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" - ) + 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" - ) + 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" - ) + _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 @@ -868,9 +852,7 @@ def _validate_revise_strategy_generated_delta( "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([strategy.get("id")], "generated strategy id", code="invalid_mutating_delta") _validated_uuid_list( [row.get("id") for row in nodes], "generated strategy node ids", @@ -885,9 +867,7 @@ def build_revise_strategy_normalization_sql( 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 - ) + 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"] ) @@ -896,9 +876,9 @@ def build_revise_strategy_normalization_sql( "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"]): + 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" ) @@ -1219,9 +1199,7 @@ def apply_entry( 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 - ) + normalization_sql = build_revise_strategy_normalization_sql(entry, mutating_prestate, generated_rows) summary["mutating_delta_validated"] = True _psql( args, diff --git a/tests/test_run_local_genesis_ledger_rebuild.py b/tests/test_run_local_genesis_ledger_rebuild.py index 721772c..1eb8ca0 100644 --- a/tests/test_run_local_genesis_ledger_rebuild.py +++ b/tests/test_run_local_genesis_ledger_rebuild.py @@ -266,9 +266,7 @@ def replay_material() -> dict: } -def revise_strategy_replay_material( - *, apply_sql_source: str = "exact_executed_sql", version: int = 2 -) -> dict: +def revise_strategy_replay_material(*, apply_sql_source: str = "exact_executed_sql", version: int = 2) -> dict: approved, applied, approval = revise_strategy_proposal_rows() proposal = applied_envelope(applied) transaction_timestamp = "2026-07-14T01:00:30+00:00" @@ -322,6 +320,19 @@ def revise_strategy_replay_material( } +def rehash_revise_strategy_receipt(material: dict) -> None: + prior_receipt = material["replay_receipt"] + proposal = applied_envelope(material["applied_proposal"]) + apply_sql = rebuild.apply_engine.build_apply_sql(proposal, material["applied_proposal"]["applied_by_handle"]) + material["replay_receipt"] = rebuild.replay_receipt.build_replay_receipt( + proposal, + prior_receipt["canonical_rows"], + apply_sql_sha256=rebuild.replay_receipt.sha256_text(apply_sql), + apply_sql_source=prior_receipt["apply_engine"]["source"], + exported_at_utc=prior_receipt["exported_at_utc"], + ) + + def write_bundle(tmp_path: Path, *, material: dict | None = None) -> argparse.Namespace: dump = tmp_path / "genesis.dump" dump.write_bytes(b"PGDMPfixture") @@ -451,13 +462,94 @@ def test_revise_strategy_material_rejects_reconstructed_apply_engine(tmp_path: P assert exc_info.value.code == "mutating_apply_engine_mismatch" +def test_revise_strategy_rejects_fully_rehashed_transaction_before_approval( + tmp_path: Path, +) -> None: + material = revise_strategy_replay_material() + original_receipt = material["replay_receipt"] + forged_timestamp = "2026-07-13T01:00:30+00:00" + canonical_rows = copy.deepcopy(original_receipt["canonical_rows"]) + canonical_rows["strategies"][0]["created_at"] = forged_timestamp + for row in canonical_rows["strategy_nodes"]: + row["created_at"] = forged_timestamp + row["updated_at"] = forged_timestamp + material["replay_receipt"]["canonical_rows"] = canonical_rows + rehash_revise_strategy_receipt(material) + args = write_bundle(tmp_path, material=material) + ledger = json.loads(args.ledger.read_text(encoding="utf-8")) + material_path = args.ledger.parent / ledger["entries"][0]["material"] + + assert material["replay_receipt"]["hashes"] != original_receipt["hashes"] + assert ledger["entries"][0]["sha256"] == sha256_file(material_path) + assert ( + ledger["entries"][0]["replay_material_sha256"] == material["replay_receipt"]["hashes"]["replay_material_sha256"] + ) + assert args.ledger_sha256 == sha256_file(args.ledger) + with pytest.raises(rebuild.ReconstructionError) as exc_info: + rebuild.load_bundle(args) + + assert exc_info.value.code == "invalid_mutating_receipt" + assert "before proposal approval" in exc_info.value.safe_message + + +@pytest.mark.parametrize( + ("reviewed_at", "transaction_timestamp", "applied_at"), + ( + ( + "2026-07-14T03:00:00+02:00", + "2026-07-14T01:00:00+00:00", + "2026-07-14T01:01:00+00:00", + ), + ( + "2026-07-14T01:00:00+00:00", + "2026-07-14T01:01:00+00:00", + "2026-07-14T03:01:00+02:00", + ), + ), +) +def test_revise_strategy_accepts_timezone_aware_closed_timestamp_boundaries( + tmp_path: Path, + reviewed_at: str, + transaction_timestamp: str, + applied_at: str, +) -> None: + material = revise_strategy_replay_material() + for proposal_row in (material["approved_proposal"], material["applied_proposal"]): + proposal_row["reviewed_at"] = reviewed_at + material["approval_snapshot"]["reviewed_at"] = reviewed_at + material["applied_proposal"]["applied_at"] = applied_at + rows = material["replay_receipt"]["canonical_rows"] + rows["strategies"][0]["created_at"] = transaction_timestamp + for row in rows["strategy_nodes"]: + row["created_at"] = transaction_timestamp + row["updated_at"] = transaction_timestamp + rehash_revise_strategy_receipt(material) + + entry = rebuild.load_bundle(write_bundle(tmp_path, material=material)).entries[0] + + assert entry.receipt["canonical_rows"]["strategies"][0]["created_at"] == transaction_timestamp + + +def test_revise_strategy_rejects_reviewed_at_without_timezone(tmp_path: Path) -> None: + material = revise_strategy_replay_material() + reviewed_at = "2026-07-14T01:00:00" + for proposal_row in (material["approved_proposal"], material["applied_proposal"]): + proposal_row["reviewed_at"] = reviewed_at + material["approval_snapshot"]["reviewed_at"] = reviewed_at + rehash_revise_strategy_receipt(material) + + with pytest.raises(rebuild.ReconstructionError) as exc_info: + rebuild.load_bundle(write_bundle(tmp_path, material=material)) + + assert exc_info.value.code == "invalid_mutating_receipt" + assert "reviewed_at must include a timezone" in exc_info.value.safe_message + + @pytest.mark.parametrize( "invalid_case", ("extra_strategy_field", "duplicate_node_id", "node_timestamp_drift", "transaction_after_apply"), ) -def test_revise_strategy_receipt_rejects_impossible_poststate( - tmp_path: Path, invalid_case: str -) -> None: +def test_revise_strategy_receipt_rejects_impossible_poststate(tmp_path: Path, invalid_case: str) -> None: material = revise_strategy_replay_material() rows = material["replay_receipt"]["canonical_rows"] if invalid_case == "extra_strategy_field": @@ -480,9 +572,7 @@ def test_revise_strategy_receipt_rejects_impossible_poststate( def test_revise_strategy_transition_builders_preserve_historical_prestate(tmp_path: Path) -> None: - entry = rebuild.load_bundle( - write_bundle(tmp_path, material=revise_strategy_replay_material()) - ).entries[0] + entry = rebuild.load_bundle(write_bundle(tmp_path, material=revise_strategy_replay_material())).entries[0] prestate = { "strategy_ids": [PRIOR_STRATEGY_ID], "active_strategy_ids": [PRIOR_STRATEGY_ID], @@ -508,9 +598,7 @@ def test_revise_strategy_transition_builders_preserve_historical_prestate(tmp_pa def test_revise_strategy_transition_builders_allow_empty_prestate(tmp_path: Path) -> None: - entry = rebuild.load_bundle( - write_bundle(tmp_path, material=revise_strategy_replay_material(version=1)) - ).entries[0] + entry = rebuild.load_bundle(write_bundle(tmp_path, material=revise_strategy_replay_material(version=1))).entries[0] prestate = { "strategy_ids": [], "active_strategy_ids": [], @@ -1085,9 +1173,9 @@ def run_genesis_ledger_command( "sequence": 1, "material": material_path.name, "sha256": sha256_file(material_path), - "replay_material_sha256": json.loads(material_path.read_text(encoding="utf-8"))[ - "replay_receipt" - ]["hashes"]["replay_material_sha256"], + "replay_material_sha256": json.loads(material_path.read_text(encoding="utf-8"))["replay_receipt"][ + "hashes" + ]["replay_material_sha256"], } ], "final_parity": { @@ -1326,9 +1414,8 @@ def test_live_revise_strategy_rebuild_is_exact_repeatable_and_leaves_no_containe first = runs[0][1] second = runs[1][1] assert first["inputs"] == second["inputs"] - assert first["ledger"]["entries"][0]["material_sha256"] == second["ledger"]["entries"][0][ - "material_sha256" - ] - assert first["ledger"]["entries"][0]["replay_material_sha256"] == second["ledger"]["entries"][0][ - "replay_material_sha256" - ] + assert first["ledger"]["entries"][0]["material_sha256"] == second["ledger"]["entries"][0]["material_sha256"] + assert ( + first["ledger"]["entries"][0]["replay_material_sha256"] + == second["ledger"]["entries"][0]["replay_material_sha256"] + ) From bd3c17628c6cf7da734be01149bbf1cc77a6d71b Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 04:01:29 +0200 Subject: [PATCH 11/27] docs: retain production apply authorization boundary --- ...oduction-authorization-packet-current.json | 260 ++++++++++++++++++ ...production-authorization-packet-current.md | 62 +++++ tests/test_proposal_apply_lifecycle.py | 23 ++ 3 files changed, 345 insertions(+) create mode 100644 docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.json create mode 100644 docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.md diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.json b/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.json new file mode 100644 index 0000000..44db097 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.json @@ -0,0 +1,260 @@ +{ + "artifact": "proposal_apply_production_authorization_packet", + "authorization_record_required": { + "authorization_text": "", + "authorized": true, + "binding_sha256": "e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345", + "expires_at_utc": "", + "operator_identity": "", + "production_rollback_sql_sha256": "", + "received_at_utc": "", + "rollback_authorized": true + }, + "authorization_text_template": "I authorize one production DB apply for proposal 9b2b5fa4-0e0d-5db0-aa4d-6fd102889946 with payload SHA-256 6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af to Docker container teleo-pg, database teleo, system identifier 7649789040005668902, binding SHA-256 e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345. I also authorize conditional production rollback SHA-256 only if the bound postflight fails and no downstream dependency exists. Do not send Telegram, mutate another proposal, expose Cloud SQL, or promote GCP. Operator ; received ; expires .", + "binding": { + "approval_gate": { + "apply_role_present": true, + "approve_function_present": false, + "assert_function_present": false, + "finish_function_present": false, + "immutable_approval_table_present": false, + "review_role_present": false + }, + "approval_gate_dependency": { + "path": "scripts/kb_apply_prereqs.sql", + "requires_separate_production_authorization": true, + "sha256": "bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa" + }, + "candidate_readback": { + "matching_candidate": null, + "matching_candidate_count": 0 + }, + "destination": { + "container": "teleo-pg", + "database": "teleo", + "server_version_num": "160014", + "system_identifier": "7649789040005668902" + }, + "destination_readback": { + "observed_at_utc": "2026-07-15T00:45:59.238006+00:00", + "read_only_transaction": true + }, + "engine": { + "apply_engine_path": "scripts/apply_proposal.py", + "apply_engine_sha256": "ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26", + "apply_sql_sha256": "1a7d65870e415ee8891b6dd84732a29bcee00412c10862d3d1db4b191a8ade38", + "repo_git_sha": "db64500a12a64206e1bb7fd0c9a0a21bca755c3d", + "runtime_manifest": { + "scripts/apply_proposal.py": "ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26", + "scripts/kb_apply_prereqs.sql": "bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa", + "scripts/kb_apply_replay_receipt.py": "4ead132fc32a69baaecfe7b638cba8ba6d54d8508d0b7e6a922dc8065805c97f", + "scripts/proposal_apply_lifecycle.py": "a643b83b55c9e76f545ebafb9582d6d07ff432600c1b66004cc7440d7ac1963b" + } + }, + "expected_rows": { + "row_ids": { + "claim_edges": [ + "b28d8770-9ac9-5b5f-bfe2-7280d7422a21" + ], + "claim_evidence": [ + "13e580c5-458d-5146-aef3-8c74ffc43b7c", + "dac3934f-b7ad-5205-8efe-a00163370a54" + ], + "claims": [ + "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "71a3331b-fb75-5e18-aa61-b256bc38af36" + ], + "reasoning_tools": [ + "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0" + ], + "sources": [ + "a86f55dc-5058-585d-b492-a4f1d932670f", + "ed9da5f1-846e-5298-8689-15027a7ce56d" + ] + }, + "row_sha256": { + "claim_edges": [ + "60d14cbc45d424fa1eb62db6478f185bf2c2d86559e57ffa77474e8d0b3c527c" + ], + "claim_evidence": [ + "e97de162c0730da69b357e1350f0da7e5843c4e67d498c12d731a0f04a8868d7", + "3054718195abe3f78e08cfcacd59902d41eb91c7153c6c63f9bcb3bc7c6902ac" + ], + "claims": [ + "e593b9cf7b0bc9267f3e48d8173129b84b09c028f290f1abc97ccb73940b31dc", + "24280b5159cbbf75e00eb24ae50ab69994397a583f347a241a5e262cf0c03179" + ], + "reasoning_tools": [ + "257924b236c3c27289b9ae2314687be0e96779415fc6ff87faada22893b757ba" + ], + "sources": [ + "d7a690c2b18701db5b48e8189b1621c8025c1ed75889a4d0a3a4c9761c03dc3b", + "bee048794799006bd00bf4bb02ae57998e670f5bcdfb140dff871dcb042d0198" + ] + } + }, + "production_preconditions": { + "approval_gate_ready": false, + "destination_readback_observed_at_utc": "2026-07-15T00:45:59.238006+00:00", + "destination_readback_was_read_only": true, + "exact_production_rollback_packet_ready": false, + "matching_candidate_count": 0, + "production_apply_authorization_present": false, + "production_apply_executed": false, + "strict_proposal_present_and_approved": false + }, + "production_rollback": { + "artifact_schema": null, + "artifact_sha256": null, + "bound_for_action_time_authorization": false, + "conditional_rollback_requires_explicit_authorization": true, + "rollback_sql_sha256": null, + "scope": "exact_production_destination_postapply_receipt", + "status": "not_generated" + }, + "proposal": { + "apply_payload_sha256": "c89bc03ce7714f325293329a0bf95eaba0a56c596ef021d731b48dab0447c684", + "approval_snapshot_sha256": "310b54030e26ed43d7226f2e803610586e80d21ff064c67e11ea21e93ef40bd9", + "id": "9b2b5fa4-0e0d-5db0-aa4d-6fd102889946", + "payload": { + "apply_payload": { + "agent_id": null, + "claims": [ + { + "confidence": 0.9, + "created_by": null, + "id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "clone-canary" + ], + "text": "An approved strict proposal can create exact canonical rows atomically.", + "type": "structural" + }, + { + "confidence": 0.85, + "created_by": null, + "id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "status": "open", + "superseded_by": null, + "tags": [ + "working-leo", + "row-proof" + ], + "text": "Row-level proof is the authority for canonical KB state.", + "type": "concept" + } + ], + "contract_version": 2, + "edges": [ + { + "created_by": null, + "edge_type": "supports", + "from_claim": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "to_claim": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "weight": 0.75 + } + ], + "evidence": [ + { + "claim_id": "38dead14-6b1d-5909-99c4-3c45ad2dd21f", + "created_by": null, + "role": "grounds", + "source_id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "weight": 0.9 + }, + { + "claim_id": "71a3331b-fb75-5e18-aa61-b256bc38af36", + "created_by": null, + "role": "illustrates", + "source_id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "weight": 0.8 + } + ], + "reasoning_tools": [ + { + "agent_id": null, + "category": "verification", + "description": "Verify approved proposal to canonical graph lifecycle in a disposable clone.", + "id": "2b8a01ea-125c-5d90-a9b9-39ccb0a1c9e0", + "name": "Canonical row proof canary" + } + ], + "sources": [ + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source A.", + "hash": "approve-claim-canary-08c2358330-a", + "id": "ed9da5f1-846e-5298-8689-15027a7ce56d", + "source_type": "observation", + "storage_path": null, + "url": null + }, + { + "created_by": null, + "excerpt": "Disposable clone lifecycle canary source B.", + "hash": "approve-claim-canary-08c2358330-b", + "id": "a86f55dc-5058-585d-b492-a4f1d932670f", + "source_type": "observation", + "storage_path": null, + "url": null + } + ] + } + }, + "proposal_payload_sha256": "6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af", + "proposal_type": "approve_claim", + "required_applied_at": null, + "required_status": "approved", + "review_note": "Reviewed exact strict payload inside the disposable clone.", + "reviewed_at": "2026-07-15 01:59:03.105846+00", + "reviewed_by_agent_id": "11111111-1111-4111-8111-111111111111", + "reviewed_by_handle": "m3ta" + }, + "rollback": { + "conditional_rollback_requires_explicit_authorization": true, + "lifecycle_receipt_sha256": "8e74cf1f5c372633f9b6f15363ff759ba6ce9bf42df51288bb9be70ddabe49b3", + "production_executable": false, + "rollback_sql_sha256": "ef2464a6802426509333861cf0768ca64184901ab7bd2030529e7356a35d9cb3", + "scope": "disposable_clone_rehearsal" + } + }, + "binding_sha256": "e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345", + "claim_ceiling": "T2 clone lifecycle plus T3 destination readback; no production apply authorization or execution", + "current_tier": "T3_live_readonly", + "generated_at_utc": "2026-07-15T02:00:26.190995+00:00", + "next_exact_action": "deploy and independently verify the immutable approval gate before selecting a production proposal", + "production_apply_authorization_present": false, + "production_apply_executed": false, + "production_preconditions": { + "approval_gate_ready": false, + "destination_readback_observed_at_utc": "2026-07-15T00:45:59.238006+00:00", + "destination_readback_was_read_only": true, + "exact_production_rollback_packet_ready": false, + "matching_candidate_count": 0, + "production_apply_authorization_present": false, + "production_apply_executed": false, + "strict_proposal_present_and_approved": false + }, + "production_rollback_requirement": { + "clone_rehearsal_is_not_production_rollback": true, + "clone_rehearsal_rollback_sql_sha256": "ef2464a6802426509333861cf0768ca64184901ab7bd2030529e7356a35d9cb3", + "production_rollback_artifact_schema": null, + "production_rollback_artifact_sha256": null, + "production_rollback_sql_sha256": null, + "required_before_apply_window": true, + "required_binding": "an exact production rollback SQL SHA-256 in both the action-time record and authorization text", + "status": "not_generated" + }, + "required_tier": "T6_authorized_production", + "safe_to_enter_apply_window": false, + "schema": "livingip.proposalApplyAuthorizationPacket.v1", + "scope_exclusions": { + "approval_gate_deployment_authorized": false, + "cloud_sql_exposure_authorized": false, + "gcp_promotion_authorized": false, + "telegram_send_authorized": false + } +} diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.md b/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.md new file mode 100644 index 0000000..0cb4d42 --- /dev/null +++ b/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.md @@ -0,0 +1,62 @@ +# Proposal Apply Production Authorization Packet + +Generated UTC: `2026-07-15T02:00:26.190995+00:00` +Required tier: `T6_authorized_production` +Current tier: `T3_live_readonly` +Safe to enter apply window: `False` +Production apply authorized: `False` +Production apply executed: `False` + +## Exact Binding + +- Proposal: `9b2b5fa4-0e0d-5db0-aa4d-6fd102889946` (`approve_claim`) +- Proposal payload SHA-256: `6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af` +- Apply payload SHA-256: `c89bc03ce7714f325293329a0bf95eaba0a56c596ef021d731b48dab0447c684` +- Immutable approval SHA-256: `310b54030e26ed43d7226f2e803610586e80d21ff064c67e11ea21e93ef40bd9` +- Destination: `teleo-pg/teleo` +- Destination system identifier: `7649789040005668902` +- Apply engine SHA-256: `ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26` +- Apply engine path: `scripts/apply_proposal.py` +- Apply SQL SHA-256: `1a7d65870e415ee8891b6dd84732a29bcee00412c10862d3d1db4b191a8ade38` +- Clone rehearsal rollback SQL SHA-256: `ef2464a6802426509333861cf0768ca64184901ab7bd2030529e7356a35d9cb3` +- Production rollback status: `not_generated` +- Production rollback SQL SHA-256: `None` +- Approval gate prerequisite SHA-256: `bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa` +- Approval gate prerequisite path: `scripts/kb_apply_prereqs.sql` +- Binding SHA-256: `e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345` + +## Production Preconditions + +- `approval_gate_ready`: `False` +- `destination_readback_observed_at_utc`: `2026-07-15T00:45:59.238006+00:00` +- `destination_readback_was_read_only`: `True` +- `exact_production_rollback_packet_ready`: `False` +- `matching_candidate_count`: `0` +- `production_apply_authorization_present`: `False` +- `production_apply_executed`: `False` +- `strict_proposal_present_and_approved`: `False` + +## Approval Gate Readback + +- `apply_role_present`: `True` +- `approve_function_present`: `False` +- `assert_function_present`: `False` +- `finish_function_present`: `False` +- `immutable_approval_table_present`: `False` +- `review_role_present`: `False` + +## Exact Action-Time Authorization Text + +This text is not actionable until every production precondition above is true. + +I authorize one production DB apply for proposal 9b2b5fa4-0e0d-5db0-aa4d-6fd102889946 with payload SHA-256 6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af to Docker container teleo-pg, database teleo, system identifier 7649789040005668902, binding SHA-256 e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345. I also authorize conditional production rollback SHA-256 only if the bound postflight fails and no downstream dependency exists. Do not send Telegram, mutate another proposal, expose Cloud SQL, or promote GCP. Operator ; received ; expires . + +## Current Gate + +deploy and independently verify the immutable approval gate before selecting a production proposal + +The production approval-gate deployment is explicitly outside this apply packet and requires separate authorization. + +## Claim Ceiling + +T2 clone lifecycle plus T3 destination readback; no production apply authorization or execution diff --git a/tests/test_proposal_apply_lifecycle.py b/tests/test_proposal_apply_lifecycle.py index 370dde1..916f32f 100644 --- a/tests/test_proposal_apply_lifecycle.py +++ b/tests/test_proposal_apply_lifecycle.py @@ -605,6 +605,29 @@ def test_retained_current_lifecycle_evidence_is_self_consistent(): assert hashlib.sha256(rollback.encode("utf-8")).hexdigest() == receipt["rollback"]["rollback_sql_sha256"] +def test_retained_production_authorization_packet_is_exact_and_unexecuted(): + packet_path = ( + REPO_ROOT + / "docs" + / "reports" + / "leo-working-state-20260709" + / "proposal-apply-production-authorization-packet-current.json" + ) + packet = json.loads(packet_path.read_text(encoding="utf-8")) + + assert packet["binding_sha256"] == lifecycle.sha256_json(packet["binding"]) + assert packet["binding"]["engine"]["apply_engine_path"] == "scripts/apply_proposal.py" + assert packet["binding"]["approval_gate_dependency"]["path"] == "scripts/kb_apply_prereqs.sql" + assert packet["production_preconditions"]["approval_gate_ready"] is False + assert packet["production_preconditions"]["strict_proposal_present_and_approved"] is False + assert packet["production_preconditions"]["exact_production_rollback_packet_ready"] is False + assert packet["safe_to_enter_apply_window"] is False + assert packet["production_apply_authorization_present"] is False + assert packet["production_apply_executed"] is False + assert all(value is False for value in packet["scope_exclusions"].values()) + assert "/private/tmp" not in json.dumps(packet, sort_keys=True) + + def test_authorization_packet_is_exact_but_never_self_authorizes_production(): packet, record, destination, production_rollback_artifact = _authorization_materials() verification = lifecycle.verify_authorization_record( From e9f208ef96de65575ccf3f5741d43934462a79b4 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 04:08:35 +0200 Subject: [PATCH 12/27] docs: refresh production apply non-change receipt --- ...-production-authorization-packet-current.json | 16 ++++++++-------- ...ly-production-authorization-packet-current.md | 8 ++++---- ...apply-production-target-readback-current.json | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.json b/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.json index 44db097..8245c9e 100644 --- a/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.json +++ b/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.json @@ -3,14 +3,14 @@ "authorization_record_required": { "authorization_text": "", "authorized": true, - "binding_sha256": "e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345", + "binding_sha256": "6f1984f3a69ad049d4bbcc408ed2542dc5f081806ec9baa9a35c8959e5dab62a", "expires_at_utc": "", "operator_identity": "", "production_rollback_sql_sha256": "", "received_at_utc": "", "rollback_authorized": true }, - "authorization_text_template": "I authorize one production DB apply for proposal 9b2b5fa4-0e0d-5db0-aa4d-6fd102889946 with payload SHA-256 6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af to Docker container teleo-pg, database teleo, system identifier 7649789040005668902, binding SHA-256 e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345. I also authorize conditional production rollback SHA-256 only if the bound postflight fails and no downstream dependency exists. Do not send Telegram, mutate another proposal, expose Cloud SQL, or promote GCP. Operator ; received ; expires .", + "authorization_text_template": "I authorize one production DB apply for proposal 9b2b5fa4-0e0d-5db0-aa4d-6fd102889946 with payload SHA-256 6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af to Docker container teleo-pg, database teleo, system identifier 7649789040005668902, binding SHA-256 6f1984f3a69ad049d4bbcc408ed2542dc5f081806ec9baa9a35c8959e5dab62a. I also authorize conditional production rollback SHA-256 only if the bound postflight fails and no downstream dependency exists. Do not send Telegram, mutate another proposal, expose Cloud SQL, or promote GCP. Operator ; received ; expires .", "binding": { "approval_gate": { "apply_role_present": true, @@ -36,14 +36,14 @@ "system_identifier": "7649789040005668902" }, "destination_readback": { - "observed_at_utc": "2026-07-15T00:45:59.238006+00:00", + "observed_at_utc": "2026-07-15T02:07:27.133665+00:00", "read_only_transaction": true }, "engine": { "apply_engine_path": "scripts/apply_proposal.py", "apply_engine_sha256": "ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26", "apply_sql_sha256": "1a7d65870e415ee8891b6dd84732a29bcee00412c10862d3d1db4b191a8ade38", - "repo_git_sha": "db64500a12a64206e1bb7fd0c9a0a21bca755c3d", + "repo_git_sha": "bd3c17628c6cf7da734be01149bbf1cc77a6d71b", "runtime_manifest": { "scripts/apply_proposal.py": "ca2c0a0c1031393c95a26102d57339654d2363f4c51cdf39aabb9071d0ca6c26", "scripts/kb_apply_prereqs.sql": "bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa", @@ -95,7 +95,7 @@ }, "production_preconditions": { "approval_gate_ready": false, - "destination_readback_observed_at_utc": "2026-07-15T00:45:59.238006+00:00", + "destination_readback_observed_at_utc": "2026-07-15T02:07:27.133665+00:00", "destination_readback_was_read_only": true, "exact_production_rollback_packet_ready": false, "matching_candidate_count": 0, @@ -221,16 +221,16 @@ "scope": "disposable_clone_rehearsal" } }, - "binding_sha256": "e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345", + "binding_sha256": "6f1984f3a69ad049d4bbcc408ed2542dc5f081806ec9baa9a35c8959e5dab62a", "claim_ceiling": "T2 clone lifecycle plus T3 destination readback; no production apply authorization or execution", "current_tier": "T3_live_readonly", - "generated_at_utc": "2026-07-15T02:00:26.190995+00:00", + "generated_at_utc": "2026-07-15T02:08:10.775963+00:00", "next_exact_action": "deploy and independently verify the immutable approval gate before selecting a production proposal", "production_apply_authorization_present": false, "production_apply_executed": false, "production_preconditions": { "approval_gate_ready": false, - "destination_readback_observed_at_utc": "2026-07-15T00:45:59.238006+00:00", + "destination_readback_observed_at_utc": "2026-07-15T02:07:27.133665+00:00", "destination_readback_was_read_only": true, "exact_production_rollback_packet_ready": false, "matching_candidate_count": 0, diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.md b/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.md index 0cb4d42..64d8a01 100644 --- a/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.md +++ b/docs/reports/leo-working-state-20260709/proposal-apply-production-authorization-packet-current.md @@ -1,6 +1,6 @@ # Proposal Apply Production Authorization Packet -Generated UTC: `2026-07-15T02:00:26.190995+00:00` +Generated UTC: `2026-07-15T02:08:10.775963+00:00` Required tier: `T6_authorized_production` Current tier: `T3_live_readonly` Safe to enter apply window: `False` @@ -23,12 +23,12 @@ Production apply executed: `False` - Production rollback SQL SHA-256: `None` - Approval gate prerequisite SHA-256: `bdf8c5da05eed10665b05ad1e331e3c9c8d72464c59889a3cf8f34ab23822faa` - Approval gate prerequisite path: `scripts/kb_apply_prereqs.sql` -- Binding SHA-256: `e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345` +- Binding SHA-256: `6f1984f3a69ad049d4bbcc408ed2542dc5f081806ec9baa9a35c8959e5dab62a` ## Production Preconditions - `approval_gate_ready`: `False` -- `destination_readback_observed_at_utc`: `2026-07-15T00:45:59.238006+00:00` +- `destination_readback_observed_at_utc`: `2026-07-15T02:07:27.133665+00:00` - `destination_readback_was_read_only`: `True` - `exact_production_rollback_packet_ready`: `False` - `matching_candidate_count`: `0` @@ -49,7 +49,7 @@ Production apply executed: `False` This text is not actionable until every production precondition above is true. -I authorize one production DB apply for proposal 9b2b5fa4-0e0d-5db0-aa4d-6fd102889946 with payload SHA-256 6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af to Docker container teleo-pg, database teleo, system identifier 7649789040005668902, binding SHA-256 e6f38b0b2925f60a83a3210325ab3b35864185638ed173aeca7cc45352e5c345. I also authorize conditional production rollback SHA-256 only if the bound postflight fails and no downstream dependency exists. Do not send Telegram, mutate another proposal, expose Cloud SQL, or promote GCP. Operator ; received ; expires . +I authorize one production DB apply for proposal 9b2b5fa4-0e0d-5db0-aa4d-6fd102889946 with payload SHA-256 6fba5e8e105a28dfaf5733ff7c9f85a20d31d653add0bc4a0f81e4b08de803af to Docker container teleo-pg, database teleo, system identifier 7649789040005668902, binding SHA-256 6f1984f3a69ad049d4bbcc408ed2542dc5f081806ec9baa9a35c8959e5dab62a. I also authorize conditional production rollback SHA-256 only if the bound postflight fails and no downstream dependency exists. Do not send Telegram, mutate another proposal, expose Cloud SQL, or promote GCP. Operator ; received ; expires . ## Current Gate diff --git a/docs/reports/leo-working-state-20260709/proposal-apply-production-target-readback-current.json b/docs/reports/leo-working-state-20260709/proposal-apply-production-target-readback-current.json index c56352f..15d34a9 100644 --- a/docs/reports/leo-working-state-20260709/proposal-apply-production-target-readback-current.json +++ b/docs/reports/leo-working-state-20260709/proposal-apply-production-target-readback-current.json @@ -1,7 +1,7 @@ { "artifact": "proposal_apply_production_target_readback", "schema": "livingip.proposalApplyProductionTargetReadback.v1", - "observed_at_utc": "2026-07-15T00:45:59.238006+00:00", + "observed_at_utc": "2026-07-15T02:07:27.133665+00:00", "transport": "ssh_readonly_transaction", "read_only_transaction": true, "read_only_setting": "on", From 3afd1dbb5ea5cbfd04f92d4cdcb8e8cfa1ee6718 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 15 Jul 2026 03:17:38 +0200 Subject: [PATCH 13/27] feat: reproduce Leo identity across disposable restarts --- docs/leo-reproducible-identity.md | 206 ++++ .../hermes-runtime/agent/prompt_builder.py | 3 + .../hermes-runtime/gateway/run.py | 3 + .../hermes-runtime/hermes_cli/tools_config.py | 3 + .../hermes-runtime/run_agent.py | 3 + .../hermes-runtime/tools/registry.py | 3 + .../leo-identity-v1/leo-constitution-v1.json | 18 + .../leo-database-fingerprint-v1.json | 25 + .../leo-database-identity-v1.json | 58 ++ .../profile-template/bin/identity_readback.py | 5 + .../profile-template/config.yaml | 24 + .../plugins/identity-boundary/__init__.py | 4 + .../plugins/identity-boundary/plugin.yaml | 3 + .../skills/identity-readback/SKILL.md | 8 + scripts/leo_behavior_manifest.py | 309 +++++- scripts/leo_identity_manifest.py | 878 ++++++++++++++++++ scripts/leo_identity_profile.py | 495 ++++++++++ .../run_leo_identity_reconstruction_canary.py | 439 +++++++++ tests/test_leo_behavior_manifest.py | 68 ++ tests/test_leo_identity_manifest.py | 721 ++++++++++++++ 20 files changed, 3259 insertions(+), 17 deletions(-) create mode 100644 docs/leo-reproducible-identity.md create mode 100644 fixtures/working-leo/leo-identity-v1/hermes-runtime/agent/prompt_builder.py create mode 100644 fixtures/working-leo/leo-identity-v1/hermes-runtime/gateway/run.py create mode 100644 fixtures/working-leo/leo-identity-v1/hermes-runtime/hermes_cli/tools_config.py create mode 100644 fixtures/working-leo/leo-identity-v1/hermes-runtime/run_agent.py create mode 100644 fixtures/working-leo/leo-identity-v1/hermes-runtime/tools/registry.py create mode 100644 fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json create mode 100644 fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json create mode 100644 fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json create mode 100644 fixtures/working-leo/leo-identity-v1/profile-template/bin/identity_readback.py create mode 100644 fixtures/working-leo/leo-identity-v1/profile-template/config.yaml create mode 100644 fixtures/working-leo/leo-identity-v1/profile-template/plugins/identity-boundary/__init__.py create mode 100644 fixtures/working-leo/leo-identity-v1/profile-template/plugins/identity-boundary/plugin.yaml create mode 100644 fixtures/working-leo/leo-identity-v1/profile-template/skills/identity-readback/SKILL.md create mode 100644 scripts/leo_identity_manifest.py create mode 100644 scripts/leo_identity_profile.py create mode 100644 scripts/run_leo_identity_reconstruction_canary.py create mode 100644 tests/test_leo_identity_manifest.py diff --git a/docs/leo-reproducible-identity.md b/docs/leo-reproducible-identity.md new file mode 100644 index 0000000..ff49a67 --- /dev/null +++ b/docs/leo-reproducible-identity.md @@ -0,0 +1,206 @@ +# Reproducible Leo identity + +## Definition of Working + +- **Working target:** generate one pinned Leo identity manifest, compile a + disposable profile, answer the fixed identity query in a fresh process, + restart in a second process, and reject any drift before an answer. +- **Operator path:** run + `python -m scripts.run_leo_identity_reconstruction_canary` with the committed + identity fixtures from a clean checkout. +- **Done means:** the T2 receipt reports two distinct stopped processes with the + same manifest, identity inputs, query, answer hash, and output provenance; + the second process starts from a newly compiled empty profile; the drift + attempt exits before answering; every process group and temporary profile is + removed. +- **Not done:** a hand-edited `SOUL.md`, a manifest self-hash, unit tests without + process restart, or prior VPS restart evidence that did not reconstruct from + this manifest. +- **Required tier:** `T2_runtime`. T3 VPS restart/readback is a post-merge + graduation target. + +## Identity input inventory + +`GatewayRunner` and the surrounding Hermes profile can change an answer through +the following surfaces. The manifest either pins each surface or explicitly +keeps it outside identity authority. + +| Surface | Binding | Authority | +| --- | --- | --- | +| Model provider, route, limits, and reasoning settings | secret-free semantic config hash | runtime input; the actual model remains a per-turn receipt | +| Hosted model weights | provider-managed, explicitly not locally hashable | never claimed as a local hash | +| Hermes and Teleo code | clean Git commits plus executable source-tree hashes | runtime input | +| Python ABI and imported runtime packages | exact implementation/version and curated package versions | runtime input | +| Skills, plugins, and database tools | content hashes | runtime input | +| Gateway/tool permissions | strict allowlisted config hash plus exact no-send/read-only policy | runtime input; this local compiler does not claim an authorizer readback | +| Static constitutional rules | committed source path, byte hash, and semantic hash | static authority | +| Database snapshot version | database/user/system identity plus content, row, structure, and capture-provenance hashes; only WAL position is excluded | T2 pins a noncanonical fixture; it cannot grant canonical authority | +| Database-derived identity rows | typed table/row/kind/rank/evidence bindings plus source mode | synthetic fixtures are explicitly noncanonical; T3 requires independently verified database-exporter output | +| `SOUL.md` and `identity.json` | deterministic compiled-view hashes | generated views, never authorities | +| Sessions, state, and memory | excluded from the identity input hash and labeled temporary | continuity only; never evidence | + +The disposable profile is compiled from a strict non-secret configuration +allowlist; unrecognized transport/credential fields are not copied. Credential +values are omitted rather than hashed. Rotating a bot token changes the broad +operational behavior observation but does not change Leo's identity. +Changing a non-secret permission, model setting, skill, code file, database +fingerprint, constitutional rule, database identity row, or compiled view does +change a pinned input and fails closed. + +### Current `GatewayRunner` consumption map + +The T2 compiler contract was checked against the current live runtime source. +This inventory defines what the post-merge T3 receipt must observe rather than +silently assuming that a profile file is the whole prompt: + +- Startup resolves `-p leoclean` to `HERMES_HOME`, then loads profile and project + environment files before `config.yaml`; environment expansion, legacy + `gateway.json`, and defaults can change the effective configuration. The T2 + compiler therefore enforces an exact top-level profile allowlist (including + rejection of dangling symlinks) rather than relying on a filename denylist. +- Routing consumes the requested default model, the top-level + `smart_model_routing` switch, provider endpoints/defaults, auth pool choice, + reasoning settings, token/turn limits, and fallbacks. The pinned top-level + routing switch must be a boolean; arbitrary nested routing data is rejected. + Credentials are runtime capability, not identity, and their values are never + retained. +- Prompt construction consumes generated `SOUL.md`, the gateway/agent system + message, persistent `MEMORY.md`/`USER.md`, compiled skills, project-context + files, current time/timezone, and the effective model/provider. T2 requires + memory and project context absent; T3 records the effective system-prompt and + compiled-skills-prompt hashes. +- Plugin discovery can include profile plugins, optional project plugins, and + installed entry points. The live database-context hook can inject a + query-bound contract before the model and can reject or replace the reply + afterward, so T3 retains plugin registry, contract, retrieval, and delivered + response hashes. +- Effective tools come from platform toolsets and the runtime registry, not a + descriptive fixture field. T3 must read back exact tool schemas, prove the + send tool absent, and keep terminal restricted to the clone-bound read-only + wrapper. +- Authorization consumes chat/user allowlists and pairing-store state. T2 starts + with pairing absent; T3 records the fixed synthetic source and the actual + authorization decision without exposing IDs or tokens. +- Session keys consume platform/chat/user/thread/topic fields. Auto-skill, + reply/media/file context, persisted transcripts, and a reused system prompt + can all change a turn. Verification therefore happens before every child + starts, and T3 binds session key, persisted session ID, message-context + absence, and prompt hashes. + +The committed T2 database/identity inputs are synthetic fixtures and the +compiled view labels them `synthetic_noncanonical_fixture`; they do not stand +in for approved production rows. This T2 schema never grants canonical +authority from caller-supplied or merely self-hashed JSON. T3 must use output +that is independently verified by the database-owning same-transaction exporter +and bound to the database fingerprint, database user, system identifier, and +row digest. + +Every pinned source tree and profile component is structurally revalidated: +the verifier recomputes its file count, total bytes, sorted unique paths, and +canonical content hash, and rejects missing files, unreadable entries, or +symlinks. Rehashing forged structural metadata cannot make it authoritative. + +The current live Hermes checkout is not clean, so its Git SHA alone is not a +reproducible source bundle. This T2 receipt intentionally uses the committed +clean local runtime and the committed database/identity snapshot fixture. It +does not claim to reconstruct the current dirty VPS runtime. T3 must run only +after the merged identity code is deployed and must bind the deployed clean +content (or a content-addressed dirty-source bundle if the live checkout has not +yet been repaired). + +## Commands + +The complete canary is the preferred operator command: + +```bash +python -m scripts.run_leo_identity_reconstruction_canary \ + --profile-template fixtures/working-leo/leo-identity-v1/profile-template \ + --hermes-root fixtures/working-leo/leo-identity-v1/hermes-runtime \ + --deployment-root . \ + --source-root . \ + --database-fingerprint fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json \ + --constitution fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json \ + --database-identity fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json \ + --output docs/reports/leo-working-state-20260709/leo-reproducible-identity-t2-current.json +``` + +The negative lifecycle is separately runnable. It proves drift is caught before +the second process starts: + +```bash +python -m scripts.run_leo_identity_reconstruction_canary \ + --profile-template fixtures/working-leo/leo-identity-v1/profile-template \ + --hermes-root fixtures/working-leo/leo-identity-v1/hermes-runtime \ + --deployment-root . \ + --source-root . \ + --database-fingerprint fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json \ + --constitution fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json \ + --database-identity fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json \ + --inject-drift-before-restart \ + --output /tmp/leo-identity-drift-rejection.json +``` + +For inspection, the lifecycle can be decomposed into explicit manifest and +compile commands: + +```bash +python -m scripts.leo_behavior_manifest \ + --profile fixtures/working-leo/leo-identity-v1/profile-template \ + --hermes-root fixtures/working-leo/leo-identity-v1/hermes-runtime \ + --deployment-root . \ + --output /tmp/leo-behavior.json + +python -m scripts.leo_identity_manifest generate \ + --behavior-manifest /tmp/leo-behavior.json \ + --database-fingerprint fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json \ + --constitution fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json \ + --database-identity fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json \ + --source-root . \ + --output /tmp/leo-identity-manifest.json + +python -m scripts.leo_identity_profile compile \ + --manifest /tmp/leo-identity-manifest.json \ + --source-root . \ + --profile-template fixtures/working-leo/leo-identity-v1/profile-template \ + --profile /tmp/leo-disposable-profile \ + --hermes-root fixtures/working-leo/leo-identity-v1/hermes-runtime \ + --deployment-root . +``` + +All commands require a clean Git worktree because a commit plus an unretained +dirty source tree is not reproducible. + +## State inventory and transitions + +| State | Meaning | Next valid transition | +| --- | --- | --- | +| `inputs_unverified` | source paths exist but are not yet bound | validate clean Git, runtime, DB, rules, rows, and permissions | +| `manifest_pinned` | every material identity input has a stable hash | compile deterministic views | +| `profile_compiled` | static profile copied and generated views match the manifest | verify immediately before start | +| `runtime_verified` | runtime/code/view hashes match and session state is non-authoritative | answer fixed query | +| `stopped_cleanly` | child and process group are absent | compile a new empty profile from the manifest | +| `restart_reproduced` | child from the newly compiled profile has the same identity/query/provenance inputs | retain receipt and clean every profile | +| `drift_detected` | any input or generated view differs | fail closed; no answer and no next start | + +The irreversible boundary is not a database or production mutation: this T2 +canary is a local compiler/process runtime, no-send, database-read-only, and +disposable. The successful-restart receipt reaches `T2_runtime`; the standalone +pre-restart drift receipt is a passing negative component but reports +`T1_model` and `goal_tier_satisfied=false`. Neither proves the live VPS +`GatewayRunner`, Telegram delivery, hosted-model behavior, production database +state, or T3 restart recovery. + +Here `T2_runtime` uses the required Capability Tier Proof vocabulary: local +runtime behavior with restart. It does not imply Hermes/GatewayRunner or a +hosted model; those exclusions are explicit in the receipt's `runtime_variant`, +`tier_basis`, and `claim_ceiling`. + +## T3 graduation + +After merge and rollback proof, extend the schema with independent verification +of the database-owning same-transaction exporter, generate the manifest from +that live read-only canonical capture and deployed clean commits, compile a +disposable VPS profile, invoke the real no-send `GatewayRunner`, terminate that +child, open a new child from the same manifest, and retain model-call, DB-read, +service, cleanup, and drift-negative readbacks. Do not replace the production +profile during that graduation. diff --git a/fixtures/working-leo/leo-identity-v1/hermes-runtime/agent/prompt_builder.py b/fixtures/working-leo/leo-identity-v1/hermes-runtime/agent/prompt_builder.py new file mode 100644 index 0000000..b8aeeed --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/hermes-runtime/agent/prompt_builder.py @@ -0,0 +1,3 @@ +"""Pinned fixture prompt boundary.""" + +GENERATED_SOUL_IS_AUTHORITY = False diff --git a/fixtures/working-leo/leo-identity-v1/hermes-runtime/gateway/run.py b/fixtures/working-leo/leo-identity-v1/hermes-runtime/gateway/run.py new file mode 100644 index 0000000..ef70232 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/hermes-runtime/gateway/run.py @@ -0,0 +1,3 @@ +"""Pinned fixture process boundary; the canary uses the real profile verifier.""" + +SESSION_STATE_IS_IDENTITY = False diff --git a/fixtures/working-leo/leo-identity-v1/hermes-runtime/hermes_cli/tools_config.py b/fixtures/working-leo/leo-identity-v1/hermes-runtime/hermes_cli/tools_config.py new file mode 100644 index 0000000..63d3877 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/hermes-runtime/hermes_cli/tools_config.py @@ -0,0 +1,3 @@ +"""Pinned fixture tool configuration.""" + +ALLOWED_TOOLS = ("identity_readback",) diff --git a/fixtures/working-leo/leo-identity-v1/hermes-runtime/run_agent.py b/fixtures/working-leo/leo-identity-v1/hermes-runtime/run_agent.py new file mode 100644 index 0000000..774693b --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/hermes-runtime/run_agent.py @@ -0,0 +1,3 @@ +"""Pinned fixture entrypoint for the disposable identity runtime.""" + +RUNTIME_NAME = "leo-identity-readback-v1" diff --git a/fixtures/working-leo/leo-identity-v1/hermes-runtime/tools/registry.py b/fixtures/working-leo/leo-identity-v1/hermes-runtime/tools/registry.py new file mode 100644 index 0000000..e2ff210 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/hermes-runtime/tools/registry.py @@ -0,0 +1,3 @@ +"""Pinned fixture registry for the no-send identity readback tool.""" + +REGISTERED_TOOLS = ("identity_readback",) diff --git a/fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json b/fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json new file mode 100644 index 0000000..f116f1e --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json @@ -0,0 +1,18 @@ +{ + "identity_name": "Leo", + "rules": [ + { + "id": "canonical-evidence", + "text": "Treat canonical database rows and their bound evidence as durable knowledge; never promote session memory to evidence." + }, + { + "id": "review-before-apply", + "text": "Proposals may be prepared, but review and guarded apply remain separate authorized actions." + }, + { + "id": "truthful-proof-tier", + "text": "State exactly what was observed, separate inference from proof, and fail closed when required provenance drifts." + } + ], + "schema": "livingip.leoConstitutionSource.v1" +} diff --git a/fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json b/fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json new file mode 100644 index 0000000..7fc20f1 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json @@ -0,0 +1,25 @@ +{ + "environment": "disposable_fixture", + "fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "read_consistency": { + "after": { + "database": "leo_identity_fixture", + "database_user": "leo_identity_reader", + "system_identifier": "7649789040005668902", + "wal_lsn": "0/16B6C50" + }, + "before": { + "database": "leo_identity_fixture", + "database_user": "leo_identity_reader", + "system_identifier": "7649789040005668902", + "wal_lsn": "0/16B6C50" + }, + "status": "stable_wal_marker" + }, + "schema": "livingip.teleoCanonicalDatabaseFingerprint.v1", + "status": "ok", + "structure_sha256": "3333333333333333333333333333333333333333333333333333333333333333", + "table_count": 7, + "table_rows_sha256": "2222222222222222222222222222222222222222222222222222222222222222", + "total_rows": 7 +} diff --git a/fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json b/fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json new file mode 100644 index 0000000..b5659e2 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json @@ -0,0 +1,58 @@ +{ + "database_fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "identity_name": "Leo", + "source_provenance": { + "canonical_authority_granted": false, + "export_receipt_sha256": null, + "mode": "synthetic_noncanonical_fixture", + "same_transaction_as_fingerprint": false + }, + "records": [ + { + "evidence_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "kind": "persona", + "rank": 0, + "row_id": "11111111-1111-4111-8111-111111111111", + "status": "active", + "table": "public.personas", + "text": "Leo is the evidence-bound reasoning interface for the Teleo collective." + }, + { + "evidence_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "kind": "strategy", + "rank": 0, + "row_id": "22222222-2222-4222-8222-222222222222", + "status": "active", + "table": "public.strategies", + "text": "Prefer source-grounded synthesis that exposes uncertainty and review boundaries." + }, + { + "evidence_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "kind": "belief", + "rank": 0, + "row_id": "33333333-3333-4333-8333-333333333333", + "status": "active", + "table": "public.beliefs", + "text": "A plausible answer without provenance is weaker than a bounded answer with an exact receipt." + }, + { + "evidence_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "kind": "shared_root", + "rank": 0, + "row_id": "44444444-4444-4444-8444-444444444444", + "status": "active", + "table": "public.shared_root", + "text": "Collective intelligence must remain inspectable, reversible, and accountable to evidence." + }, + { + "evidence_sha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "kind": "strategy_node", + "rank": 1, + "row_id": "55555555-5555-4555-8555-555555555555", + "status": "active", + "table": "public.strategy_nodes", + "text": "Compile identity from exact canonical inputs and reject drift before runtime startup." + } + ], + "schema": "livingip.leoDatabaseIdentitySource.v1" +} diff --git a/fixtures/working-leo/leo-identity-v1/profile-template/bin/identity_readback.py b/fixtures/working-leo/leo-identity-v1/profile-template/bin/identity_readback.py new file mode 100644 index 0000000..4ab40f9 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/profile-template/bin/identity_readback.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +"""Fixture marker for the no-send identity readback tool.""" + +ACCESS_MODE = "read_only" +NETWORK_SEND_ENABLED = False diff --git a/fixtures/working-leo/leo-identity-v1/profile-template/config.yaml b/fixtures/working-leo/leo-identity-v1/profile-template/config.yaml new file mode 100644 index 0000000..8086613 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/profile-template/config.yaml @@ -0,0 +1,24 @@ +model: + provider: fixture-local + default: leo-identity-readback-v1 + smart_routing: false + max_tokens: 512 +agent: + reasoning_effort: deterministic +memory: + enabled: false + search: disabled +gateway: + execution_mode: local_no_send + telegram: + posting_enabled: false + bot_token: fixture-value-is-never-emitted-or-identity-hashed +tools: + allowed: + - identity_readback + write_enabled: false +identity_runtime: + network_send_enabled: false + database_write_enabled: false + allowed_tools: + - identity_readback diff --git a/fixtures/working-leo/leo-identity-v1/profile-template/plugins/identity-boundary/__init__.py b/fixtures/working-leo/leo-identity-v1/profile-template/plugins/identity-boundary/__init__.py new file mode 100644 index 0000000..19f0f68 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/profile-template/plugins/identity-boundary/__init__.py @@ -0,0 +1,4 @@ +"""Fixture marker for the pinned identity-boundary middleware.""" + +IDENTITY_VIEW_IS_AUTHORITY = False +SESSION_MEMORY_IS_EVIDENCE = False diff --git a/fixtures/working-leo/leo-identity-v1/profile-template/plugins/identity-boundary/plugin.yaml b/fixtures/working-leo/leo-identity-v1/profile-template/plugins/identity-boundary/plugin.yaml new file mode 100644 index 0000000..07cb632 --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/profile-template/plugins/identity-boundary/plugin.yaml @@ -0,0 +1,3 @@ +name: identity-boundary +version: 1 +description: Reject startup when a compiled identity view drifts. diff --git a/fixtures/working-leo/leo-identity-v1/profile-template/skills/identity-readback/SKILL.md b/fixtures/working-leo/leo-identity-v1/profile-template/skills/identity-readback/SKILL.md new file mode 100644 index 0000000..58c3a4b --- /dev/null +++ b/fixtures/working-leo/leo-identity-v1/profile-template/skills/identity-readback/SKILL.md @@ -0,0 +1,8 @@ +--- +name: identity-readback +description: Explain the pinned Leo identity and its noncanonical session boundary. +--- + +Read only the compiled identity view and its manifest receipt. Never treat a +session transcript, generated SOUL file, or unverified runtime memory as a +canonical source. diff --git a/scripts/leo_behavior_manifest.py b/scripts/leo_behavior_manifest.py index d8f6cad..39755a6 100644 --- a/scripts/leo_behavior_manifest.py +++ b/scripts/leo_behavior_manifest.py @@ -11,9 +11,13 @@ from __future__ import annotations import argparse import hashlib +import importlib.metadata import json import os +import platform +import re import subprocess +import sys from datetime import datetime, timezone from pathlib import Path from typing import Any @@ -24,6 +28,54 @@ SCHEMA = "livingip.leoBehaviorManifest.v1" DEFAULT_PROFILE = Path("/home/teleo/.hermes/profiles/leoclean") DEFAULT_HERMES_ROOT = Path("/home/teleo/.hermes/hermes-agent") DEFAULT_DEPLOYMENT_ROOT = Path("/opt/teleo-eval/workspaces/deploy-infra") +STATIC_RUNTIME_COMPONENTS = ( + "profile_config", + "procedural_skills", + "runtime_middleware", + "database_tools", +) +IDENTITY_RUNTIME_COMPONENTS = ( + "procedural_skills", + "runtime_middleware", + "database_tools", +) +STABLE_MANIFEST_FIELDS = ( + "schema", + "model_runtime", + "hermes_runtime", + "teleo_infrastructure_runtime", + "components", + "python_runtime", + "canonical_database", +) +SECRET_KEYS = frozenset( + { + "access_key", + "access_token", + "api_key", + "authorization", + "bot_token", + "client_secret", + "credential", + "credentials", + "password", + "private_key", + "refresh_token", + "secret", + "token", + } +) +SECRET_KEY_SUFFIXES = ( + "api_key", + "access_key", + "private_key", + "password", + "secret", + "token", + "credential", +) +RUNTIME_DISTRIBUTIONS = ("PyYAML",) +SAFE_CONFIG_SCALAR = re.compile(r"^[A-Za-z0-9._:/-]+$") def sha256_bytes(value: bytes) -> str: @@ -50,7 +102,9 @@ def _safe_relative(path: Path, root: Path) -> str: return str(path) -def _resolved_node_fingerprint(path: Path, seen_directories: frozenset[tuple[int, int]] = frozenset()) -> dict[str, Any]: +def _resolved_node_fingerprint( + path: Path, seen_directories: frozenset[tuple[int, int]] = frozenset() +) -> dict[str, Any]: """Hash a symlink target, following nested links while stopping directory cycles.""" try: @@ -141,21 +195,139 @@ def _nested(config: dict[str, Any], *path: str) -> Any: return value +def secret_free_config(value: Any) -> Any: + """Return a semantic config projection with secret-bearing fields omitted.""" + + if isinstance(value, dict): + return { + str(key): secret_free_config(item) + for key, item in sorted(value.items(), key=lambda pair: str(pair[0])) + if not _is_secret_key(str(key)) + } + if isinstance(value, list): + return [secret_free_config(item) for item in value] + return value + + +def _validate_identity_config_value(value: Any, *, path: str) -> None: + if value is None or isinstance(value, (bool, int)): + return + if isinstance(value, str): + if not SAFE_CONFIG_SCALAR.fullmatch(value) or "://" in value or "@" in value: + raise ValueError(f"unsafe identity config scalar: {path}") + return + if isinstance(value, list): + for index, item in enumerate(value): + _validate_identity_config_value(item, path=f"{path}[{index}]") + return + if isinstance(value, dict): + for key, item in value.items(): + if _is_secret_key(str(key)): + raise ValueError(f"secret-bearing identity config key: {path}.{key}") + _validate_identity_config_value(item, path=f"{path}.{key}") + return + raise ValueError(f"unsupported identity config value: {path}") + + +def identity_config_projection(raw: dict[str, Any]) -> dict[str, Any]: + """Build the only configuration surface copied into a disposable identity profile.""" + + section_fields = { + "model": ("provider", "default", "smart_routing", "smart_routing_model", "max_tokens"), + "agent": ("reasoning_effort",), + "memory": ("enabled", "search"), + "tools": ("allowed", "write_enabled"), + "identity_runtime": ("network_send_enabled", "database_write_enabled", "allowed_tools"), + } + projection: dict[str, Any] = {} + for section, fields in section_fields.items(): + source = raw.get(section) + if source is None: + continue + if not isinstance(source, dict): + raise ValueError(f"identity config section must be a mapping: {section}") + selected = {field: source[field] for field in fields if field in source} + _validate_identity_config_value(selected, path=section) + projection[section] = selected + + gateway = raw.get("gateway") + if gateway is not None: + if not isinstance(gateway, dict): + raise ValueError("identity config section must be a mapping: gateway") + selected_gateway = {key: gateway[key] for key in ("execution_mode",) if key in gateway} + telegram = gateway.get("telegram") + if telegram is not None: + if not isinstance(telegram, dict): + raise ValueError("identity config section must be a mapping: gateway.telegram") + selected_telegram = {key: telegram[key] for key in ("posting_enabled",) if key in telegram} + if selected_telegram: + selected_gateway["telegram"] = selected_telegram + _validate_identity_config_value(selected_gateway, path="gateway") + projection["gateway"] = selected_gateway + + if "smart_model_routing" in raw: + routing = raw["smart_model_routing"] + if not isinstance(routing, bool): + raise ValueError("smart_model_routing must be boolean") + projection["smart_model_routing"] = routing + return projection + + +def _is_secret_key(key: str) -> bool: + snake_case = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", key) + normalized = re.sub(r"[^a-z0-9]+", "_", snake_case.casefold()).strip("_") + return normalized in SECRET_KEYS or any(normalized.endswith(f"_{suffix}") for suffix in SECRET_KEY_SUFFIXES) + + +def python_runtime_manifest() -> dict[str, Any]: + distributions: dict[str, str | None] = {} + for name in RUNTIME_DISTRIBUTIONS: + try: + distributions[name] = importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + distributions[name] = None + return { + "implementation": platform.python_implementation(), + "python_version": platform.python_version(), + "abi_tag": sys.implementation.cache_tag, + "runtime_distributions": distributions, + "runtime_distributions_sha256": canonical_sha256(distributions), + } + + def safe_model_config(config_path: Path) -> dict[str, Any]: try: raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} except (OSError, TypeError, yaml.YAMLError) as exc: return {"status": "unavailable", "error": type(exc).__name__} + if not isinstance(raw, dict): + return {"status": "unavailable", "error": "ConfigNotMapping"} + semantic = secret_free_config(raw) + try: + identity_config = identity_config_projection(raw) + except ValueError: + return {"status": "unavailable", "error": "UnsafeIdentityConfig"} + model_semantic = identity_config.get("model") or {} return { "status": "observed", - "provider": _nested(raw, "model", "provider"), - "default_model": _nested(raw, "model", "default"), - "smart_routing": _nested(raw, "model", "smart_routing"), - "smart_routing_model": _nested(raw, "model", "smart_routing_model"), - "max_tokens": _nested(raw, "model", "max_tokens"), - "reasoning_effort": _nested(raw, "agent", "reasoning_effort"), - "memory_enabled": _nested(raw, "memory", "enabled"), - "memory_search": _nested(raw, "memory", "search"), + "config_sha256": file_sha256(config_path), + "semantic_config_sha256": canonical_sha256(semantic), + "identity_config_sha256": canonical_sha256(identity_config), + "model_section_sha256": canonical_sha256(model_semantic), + "gateway_permissions_sha256": canonical_sha256(identity_config.get("gateway")), + "tool_permissions_sha256": canonical_sha256(identity_config.get("tools")), + "memory_section_sha256": canonical_sha256(identity_config.get("memory")), + "agent_runtime_sha256": canonical_sha256(identity_config.get("agent")), + "identity_runtime_policy": identity_config.get("identity_runtime"), + "provider": _nested(identity_config, "model", "provider"), + "default_model": _nested(identity_config, "model", "default"), + "smart_routing": _nested(identity_config, "model", "smart_routing"), + "smart_routing_model": _nested(identity_config, "model", "smart_routing_model"), + "gateway_smart_model_routing": identity_config.get("smart_model_routing"), + "max_tokens": _nested(identity_config, "model", "max_tokens"), + "reasoning_effort": _nested(identity_config, "agent", "reasoning_effort"), + "memory_enabled": _nested(identity_config, "memory", "enabled"), + "memory_search": _nested(identity_config, "memory", "search"), } @@ -196,7 +368,13 @@ def git_state(repo: Path) -> dict[str, Any]: } -def source_code_manifest(profile: Path, paths: tuple[Path, ...]) -> dict[str, Any]: +def source_code_manifest( + profile: Path, + paths: tuple[Path, ...], + *, + source_root: Path | None = None, + namespace: str = "source", +) -> dict[str, Any]: """Hash executable source while excluding generated caches and environments.""" allowed_suffixes = { @@ -216,12 +394,21 @@ def source_code_manifest(profile: Path, paths: tuple[Path, ...]) -> dict[str, An excluded_parts = {".git", ".pytest_cache", "__pycache__", "node_modules", "venv", ".venv"} files: list[dict[str, Any]] = [] missing: list[str] = [] + symlinks: list[str] = [] + source_root = (source_root or profile).resolve() + + def source_label(path: Path) -> str: + return f"{namespace}:{_safe_relative(path, source_root)}" + for requested in paths: if not requested.exists() and not requested.is_symlink(): - missing.append(_safe_relative(requested, profile)) + missing.append(source_label(requested)) continue candidates = [requested] if requested.is_file() else sorted(requested.rglob("*")) for path in candidates: + if path.is_symlink(): + symlinks.append(source_label(path)) + continue if not path.is_file() or excluded_parts & set(path.parts): continue stat = path.stat() @@ -229,14 +416,14 @@ def source_code_manifest(profile: Path, paths: tuple[Path, ...]) -> dict[str, An continue files.append( { - "path": _safe_relative(path, profile), + "path": source_label(path), "bytes": stat.st_size, "mode": oct(stat.st_mode & 0o777), "sha256": file_sha256(path), } ) files.sort(key=lambda item: item["path"]) - stable = {"files": files, "missing": sorted(missing)} + stable = {"files": files, "missing": sorted(missing), "symlinks": sorted(symlinks)} return { **stable, "file_count": len(files), @@ -261,6 +448,72 @@ def component( } +def stable_manifest_payload(manifest: dict[str, Any]) -> dict[str, Any]: + """Return the canonical, path-independent behavior payload.""" + + return {field: manifest.get(field) for field in STABLE_MANIFEST_FIELDS} + + +def static_runtime_payload(manifest: dict[str, Any]) -> dict[str, Any]: + """Return the exact payload committed by ``static_runtime_sha256``.""" + + components = manifest.get("components") or {} + return { + "schema": manifest.get("schema"), + "model_runtime": manifest.get("model_runtime"), + "hermes_runtime": manifest.get("hermes_runtime"), + "teleo_infrastructure_runtime": manifest.get("teleo_infrastructure_runtime"), + "components": {name: components.get(name) for name in STATIC_RUNTIME_COMPONENTS}, + "python_runtime": manifest.get("python_runtime"), + "canonical_database": manifest.get("canonical_database"), + } + + +def identity_runtime_payload(manifest: dict[str, Any]) -> dict[str, Any]: + """Return the exact payload committed by ``identity_runtime_sha256``.""" + + model = manifest.get("model_runtime") or {} + components = manifest.get("components") or {} + teleo = manifest.get("teleo_infrastructure_runtime") or {} + identity_model_runtime = { + key: model.get(key) + for key in ( + "status", + "identity_config_sha256", + "model_section_sha256", + "gateway_permissions_sha256", + "tool_permissions_sha256", + "memory_section_sha256", + "agent_runtime_sha256", + "identity_runtime_policy", + "provider", + "default_model", + "smart_routing", + "smart_routing_model", + "gateway_smart_model_routing", + "max_tokens", + "reasoning_effort", + "memory_enabled", + "memory_search", + "base_model_weights", + "actual_per_turn_model_must_be_recorded_by_the_call_receipt", + ) + } + return { + "schema": manifest.get("schema"), + "model_runtime": identity_model_runtime, + "hermes_runtime": manifest.get("hermes_runtime"), + "teleo_infrastructure_runtime": { + "git_head": teleo.get("git_head"), + "git_state": teleo.get("git_state"), + "source_tree": teleo.get("identity_source_tree"), + }, + "components": {name: components.get(name) for name in IDENTITY_RUNTIME_COMPONENTS}, + "python_runtime": manifest.get("python_runtime"), + "canonical_database": manifest.get("canonical_database"), + } + + def build_manifest( profile: Path = DEFAULT_PROFILE, hermes_root: Path = DEFAULT_HERMES_ROOT, @@ -270,6 +523,12 @@ def build_manifest( hermes_root = hermes_root.resolve() deployment_root = deployment_root.resolve() config = safe_model_config(profile / "config.yaml") + identity_runtime_sources = ( + deployment_root / "scripts" / "leo_behavior_manifest.py", + deployment_root / "scripts" / "leo_identity_manifest.py", + deployment_root / "scripts" / "leo_identity_profile.py", + deployment_root / "scripts" / "run_leo_identity_reconstruction_canary.py", + ) components = { "profile_config": component( profile, @@ -294,10 +553,10 @@ def build_manifest( ), "runtime_middleware": component( profile, - role="Pre-LLM context injection and post-LLM validation or response replacement.", + role="Profile plugins for pre-LLM context injection and post-LLM validation or response replacement.", mutability="deployment_static", replayability="fully_hashable", - paths=(profile / "plugins", hermes_root / "run_agent.py"), + paths=(profile / "plugins",), ), "database_tools": component( profile, @@ -353,14 +612,28 @@ def build_manifest( hermes_root / "hermes_cli", hermes_root / "tools", ), + source_root=hermes_root, + namespace="hermes", ), }, "teleo_infrastructure_runtime": { "git_head": git_head(deployment_root), "git_state": git_state(deployment_root), - "source_tree": source_code_manifest(profile, (deployment_root,)), + "source_tree": source_code_manifest( + profile, + (deployment_root,), + source_root=deployment_root, + namespace="teleo", + ), + "identity_source_tree": source_code_manifest( + profile, + identity_runtime_sources, + source_root=deployment_root, + namespace="teleo-identity", + ), }, "components": components, + "python_runtime": python_runtime_manifest(), "canonical_database": { "included": False, "reason": "Fingerprint through a read-only Postgres manifest in the database-owning harness.", @@ -372,7 +645,9 @@ def build_manifest( "profile_root": str(profile), "hermes_root": str(hermes_root), "deployment_root": str(deployment_root), - "behavior_sha256": canonical_sha256(stable), + "static_runtime_sha256": canonical_sha256(static_runtime_payload(stable)), + "identity_runtime_sha256": canonical_sha256(identity_runtime_payload(stable)), + "behavior_sha256": canonical_sha256(stable_manifest_payload(stable)), } diff --git a/scripts/leo_identity_manifest.py b/scripts/leo_identity_manifest.py new file mode 100644 index 0000000..d9d2943 --- /dev/null +++ b/scripts/leo_identity_manifest.py @@ -0,0 +1,878 @@ +#!/usr/bin/env python3 +"""Build and verify Leo's pinned, reproducible identity manifest.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path +from typing import Any + +if __package__ in {None, ""}: + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from scripts import leo_behavior_manifest as behavior + +SCHEMA = "livingip.leoIdentityManifest.v1" +CONSTITUTION_SCHEMA = "livingip.leoConstitutionSource.v1" +DATABASE_IDENTITY_SCHEMA = "livingip.leoDatabaseIdentitySource.v1" +STRUCTURED_VIEW_SCHEMA = "livingip.leoCompiledIdentityView.v1" +DATABASE_FINGERPRINT_SCHEMA = "livingip.teleoCanonicalDatabaseFingerprint.v1" +SYNTHETIC_DATABASE_MODE = "synthetic_noncanonical_fixture" +CANONICAL_DATABASE_MODE = "canonical_database_same_transaction_export" +HEX_64 = re.compile(r"^[0-9a-f]{64}$") +HEX_40 = re.compile(r"^[0-9a-f]{40}$") +IDENTITY_TABLES = frozenset( + { + "public.personas", + "public.strategies", + "public.beliefs", + "public.shared_root", + "public.strategy_nodes", + "public.strategy_node_anchors", + "public.claims", + } +) + + +def expected_runtime_policy() -> dict[str, Any]: + return { + "network_send_enabled": False, + "database_write_enabled": False, + "allowed_tools": ["identity_readback"], + } + + +def expected_session_boundary() -> dict[str, Any]: + return { + "classification": "temporary_noncanonical", + "included_in_identity_inputs": False, + "may_be_used_as_evidence": False, + "restart_policy": "fresh_process_with_session_state_outside_identity_authority", + } + + +def expected_gatewayrunner_contract() -> dict[str, Any]: + return { + "profile_surfaces": ["config.yaml", "generated SOUL.md", "skills", "plugins", "bin tools"], + "required_absent_or_empty": { + "persistent_memory": behavior.canonical_sha256([]), + "pairing_store": behavior.canonical_sha256([]), + "project_context": behavior.canonical_sha256([]), + "generated_skill_prompt_cache": behavior.canonical_sha256([]), + "prior_sessions_at_first_start": behavior.canonical_sha256([]), + }, + "fixed_message_context": { + "auto_skill": None, + "reply_context": None, + "media_or_file_context": None, + }, + "required_t3_turn_receipt_fields": [ + "runner_class", + "authorization_decision", + "effective_system_prompt_sha256", + "compiled_skills_prompt_sha256", + "tool_registry_schema_sha256", + "plugin_registry_sha256", + "selected_model", + "selected_provider", + "api_mode", + "base_url_host", + "database_retrieval_receipt", + "session_key_sha256", + "persisted_session_id_sha256", + ], + } + + +class IdentityManifestError(ValueError): + """An identity input is incomplete, inconsistent, or drifted.""" + + +def _require(condition: bool, message: str) -> None: + if not condition: + raise IdentityManifestError(message) + + +def _is_sha256(value: Any) -> bool: + return isinstance(value, str) and bool(HEX_64.fullmatch(value)) + + +def load_json(path: Path, label: str) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise IdentityManifestError(f"cannot read {label}: {type(exc).__name__}") from exc + if not isinstance(value, dict): + raise IdentityManifestError(f"{label} must be a JSON object") + return value + + +def canonical_rules(value: dict[str, Any]) -> list[dict[str, str]]: + _require(value.get("schema") == CONSTITUTION_SCHEMA, "unsupported constitution schema") + _require(value.get("identity_name") == "Leo", "constitution identity_name must be Leo") + raw_rules = value.get("rules") + _require(isinstance(raw_rules, list) and bool(raw_rules), "constitution rules must be non-empty") + rules: list[dict[str, str]] = [] + for raw in raw_rules: + _require(isinstance(raw, dict), "each constitutional rule must be an object") + rule_id = str(raw.get("id") or "").strip() + text = str(raw.get("text") or "").strip() + _require(bool(rule_id and text), "each constitutional rule needs id and text") + rules.append({"id": rule_id, "text": text}) + _require(len({item["id"] for item in rules}) == len(rules), "constitutional rule ids must be unique") + return sorted(rules, key=lambda item: item["id"]) + + +def canonical_database_records(value: dict[str, Any], *, fingerprint_sha256: str) -> list[dict[str, str | int]]: + _require(value.get("schema") == DATABASE_IDENTITY_SCHEMA, "unsupported database identity schema") + _require(value.get("identity_name") == "Leo", "database identity_name must be Leo") + _require( + value.get("database_fingerprint_sha256") == fingerprint_sha256, + "database identity export is not bound to the supplied database fingerprint", + ) + raw_records = value.get("records") + _require(isinstance(raw_records, list) and bool(raw_records), "database identity records must be non-empty") + records: list[dict[str, str | int]] = [] + for raw in raw_records: + _require(isinstance(raw, dict), "each database identity record must be an object") + rank = raw.get("rank", 0) + record: dict[str, str | int] = { + "table": str(raw.get("table") or "").strip(), + "row_id": str(raw.get("row_id") or "").strip(), + "kind": str(raw.get("kind") or "").strip(), + "rank": rank if isinstance(rank, int) and not isinstance(rank, bool) else -1, + "text": str(raw.get("text") or "").strip(), + "status": str(raw.get("status") or "").strip(), + "evidence_sha256": str(raw.get("evidence_sha256") or "").strip(), + } + _require(record["table"] in IDENTITY_TABLES, "database identity table is outside the allowlist") + _require(bool(record["row_id"] and record["kind"] and record["text"]), "database record fields are incomplete") + _require(isinstance(record["rank"], int) and record["rank"] >= 0, "database record rank is invalid") + _require(record["status"] == "active", "database identity records must be active selected rows") + _require(_is_sha256(record["evidence_sha256"]), "database record evidence_sha256 is invalid") + records.append(record) + keys = {(item["table"], item["row_id"]) for item in records} + _require(len(keys) == len(records), "database identity row bindings must be unique") + return sorted(records, key=lambda item: (str(item["table"]), int(item["rank"]), str(item["row_id"]))) + + +def database_fingerprint_semantic(value: dict[str, Any]) -> dict[str, Any]: + """Bind immutable capture provenance while excluding only the volatile WAL position.""" + + result = {key: value[key] for key in sorted(value) if key != "read_consistency"} + consistency = value["read_consistency"] + result["read_consistency"] = { + "status": consistency["status"], + "before": {key: item for key, item in consistency["before"].items() if key != "wal_lsn"}, + "after": {key: item for key, item in consistency["after"].items() if key != "wal_lsn"}, + } + return result + + +def database_identity_provenance( + value: dict[str, Any], + *, + fingerprint: dict[str, Any], +) -> dict[str, Any]: + raw = value.get("source_provenance") + _require(isinstance(raw, dict), "database identity source provenance is missing") + mode = raw.get("mode") + if mode == SYNTHETIC_DATABASE_MODE: + _require( + raw + == { + "mode": SYNTHETIC_DATABASE_MODE, + "canonical_authority_granted": False, + "same_transaction_as_fingerprint": False, + "export_receipt_sha256": None, + }, + "synthetic database provenance contract is invalid", + ) + _require( + fingerprint.get("environment") == "disposable_fixture", + "synthetic rows require a fixture fingerprint", + ) + _require("export_receipt" not in value, "synthetic database identity must not claim an export receipt") + return {**raw, "authority": "synthetic_noncanonical_fixture"} + + if mode == CANONICAL_DATABASE_MODE: + raise IdentityManifestError( + "canonical authority requires independently verified output from the database-owning same-transaction " + "exporter; caller-supplied T2 JSON cannot grant it" + ) + raise IdentityManifestError("unsupported database identity provenance mode") + + +def validate_database_fingerprint(value: dict[str, Any]) -> None: + _require(value.get("schema") == DATABASE_FINGERPRINT_SCHEMA, "unsupported database fingerprint schema") + _require(value.get("status") == "ok", "database fingerprint status must be ok") + for field in ("fingerprint_sha256", "table_rows_sha256", "structure_sha256"): + _require(_is_sha256(value.get(field)), f"database fingerprint {field} is invalid") + _require(isinstance(value.get("table_count"), int) and value["table_count"] > 0, "table_count must be positive") + _require(isinstance(value.get("total_rows"), int) and value["total_rows"] >= 0, "total_rows is invalid") + consistency = value.get("read_consistency") + _require(isinstance(consistency, dict), "database read_consistency is missing") + before = consistency.get("before") + after = consistency.get("after") + _require(consistency.get("status") == "stable_wal_marker", "database fingerprint was not read consistently") + _require(isinstance(before, dict) and before == after and bool(before), "database read markers differ") + for field in ("database", "database_user", "system_identifier", "wal_lsn"): + _require(bool(before.get(field)), f"database read marker {field} is missing") + + +def validate_content_manifest(value: Any, *, label: str) -> None: + """Recompute the structural metadata of a replayable content manifest.""" + + _require(isinstance(value, dict), f"{label} is invalid") + _require( + set(value) == {"files", "missing", "symlinks", "file_count", "total_bytes", "sha256"}, + f"{label} fields are invalid", + ) + files = value.get("files") + missing = value.get("missing") + symlinks = value.get("symlinks") + _require(isinstance(files, list) and bool(files), f"{label} is empty") + _require(missing == [], f"{label} has missing inputs") + _require(symlinks == [], f"{label} contains unsupported symlinks") + + paths: list[str] = [] + total_bytes = 0 + for item in files: + _require(isinstance(item, dict), f"{label} contains unreadable inputs") + _require(set(item) == {"path", "bytes", "mode", "sha256"}, f"{label} contains unreadable inputs") + path = item.get("path") + size = item.get("bytes") + mode = item.get("mode") + _require(isinstance(path, str) and bool(path), f"{label} contains an invalid path") + _require(isinstance(size, int) and not isinstance(size, bool) and size >= 0, f"{label} byte size is invalid") + _require(isinstance(mode, str) and bool(re.fullmatch(r"0o[0-7]{3}", mode)), f"{label} mode is invalid") + _require(_is_sha256(item.get("sha256")), f"{label} contains unreadable inputs") + paths.append(path) + total_bytes += size + + _require(paths == sorted(paths) and len(paths) == len(set(paths)), f"{label} paths are not unique and sorted") + _require(value.get("file_count") == len(files), f"{label} file count is invalid") + _require(value.get("total_bytes") == total_bytes, f"{label} total bytes is invalid") + stable = {"files": files, "missing": missing, "symlinks": symlinks} + _require(behavior.canonical_sha256(stable) == value.get("sha256"), f"{label} content hash is invalid") + + +def validate_behavior_manifest(value: dict[str, Any]) -> None: + _require(value.get("schema") == behavior.SCHEMA, "unsupported behavior manifest schema") + for field in ("behavior_sha256", "static_runtime_sha256", "identity_runtime_sha256"): + _require(_is_sha256(value.get(field)), f"behavior manifest {field} is invalid") + _require( + behavior.canonical_sha256(behavior.identity_runtime_payload(value)) == value["identity_runtime_sha256"], + "behavior identity runtime hash drift detected", + ) + _require( + behavior.canonical_sha256(behavior.static_runtime_payload(value)) == value["static_runtime_sha256"], + "behavior static runtime hash drift detected", + ) + _require( + behavior.canonical_sha256(behavior.stable_manifest_payload(value)) == value["behavior_sha256"], + "behavior manifest stable payload hash drift detected", + ) + model = value.get("model_runtime") + _require(isinstance(model, dict) and model.get("status") == "observed", "model runtime was not observed") + _require(bool(model.get("provider") and model.get("default_model")), "provider and default model must be pinned") + for field in ( + "semantic_config_sha256", + "identity_config_sha256", + "model_section_sha256", + "gateway_permissions_sha256", + "tool_permissions_sha256", + "memory_section_sha256", + "agent_runtime_sha256", + ): + _require(_is_sha256(model.get(field)), f"model runtime {field} is invalid") + _require( + model.get("base_model_weights") == "external_provider_managed_not_locally_hashable", + "hosted model weight boundary is missing", + ) + _require( + model.get("actual_per_turn_model_must_be_recorded_by_the_call_receipt") is True, "model receipt gate missing" + ) + policy = model.get("identity_runtime_policy") + _require(policy == expected_runtime_policy(), "identity runtime policy must be the exact local readback policy") + for field in ("hermes_runtime", "teleo_infrastructure_runtime"): + runtime = value.get(field) + _require(isinstance(runtime, dict), f"{field} is missing") + _require(bool(HEX_40.fullmatch(str(runtime.get("git_head") or ""))), f"{field} git_head is invalid") + git_state = runtime.get("git_state") + _require( + isinstance(git_state, dict) and git_state.get("worktree_clean") is True, + f"{field} is not reconstructible from clean Git", + ) + tree = runtime.get("identity_source_tree" if field == "teleo_infrastructure_runtime" else "source_tree") + validate_content_manifest(tree, label=f"{field} source tree") + components = value.get("components") + _require(isinstance(components, dict), "behavior components are missing") + for name in behavior.IDENTITY_RUNTIME_COMPONENTS: + content = (components.get(name) or {}).get("content") + validate_content_manifest(content, label=f"component {name}") + python_runtime = value.get("python_runtime") + _require(isinstance(python_runtime, dict), "Python runtime binding is missing") + _require( + bool(python_runtime.get("implementation") and python_runtime.get("python_version")), + "Python runtime is incomplete", + ) + _require(_is_sha256(python_runtime.get("runtime_distributions_sha256")), "runtime package binding is invalid") + _require( + all(python_runtime.get("runtime_distributions", {}).values()), "a required runtime distribution is missing" + ) + + +def _repo_path(path: Path, source_root: Path) -> str: + try: + return path.resolve(strict=True).relative_to(source_root.resolve(strict=True)).as_posix() + except (OSError, ValueError) as exc: + raise IdentityManifestError(f"identity source is outside the pinned source root: {path}") from exc + + +def source_binding( + path: Path, + *, + source_root: Path, + semantic_value: Any, + count: int, + pin_content: bool = True, +) -> dict[str, Any]: + result = { + "repo_path": _repo_path(path, source_root), + "semantic_sha256": behavior.canonical_sha256(semantic_value), + "record_count": count, + } + if pin_content: + result["content_sha256"] = behavior.file_sha256(path) + return result + + +def render_identity_views( + *, + identity_inputs_sha256: str, + database_fingerprint_sha256: str, + database_provenance: dict[str, Any], + rules: list[dict[str, str]], + records: list[dict[str, str | int]], +) -> dict[str, str]: + soul = [ + "", + "# Leo", + "", + f"Identity inputs: `{identity_inputs_sha256}`", + "", + "## Static constitutional rules", + "", + "These rules are static constitutional inputs, not database claims.", + "", + ] + soul.extend(f"- **{item['id']}**: {item['text']}" for item in rules) + database_description = ( + "canonical database capture" + if database_provenance["canonical_authority_granted"] + else "synthetic noncanonical fixture" + ) + soul.extend( + [ + "", + "## Database-derived identity", + "", + f"Generated from {database_description} fingerprint `{database_fingerprint_sha256}`.", + "", + ] + ) + soul.extend( + f"- `{item['table']}/{item['row_id']}` [{item['kind']}, rank {item['rank']}]: {item['text']} " + f"(evidence `{item['evidence_sha256']}`)" + for item in records + ) + soul.extend( + [ + "", + "## Authority and session boundary", + "", + "- This `SOUL.md` is a generated view and is never an authority by itself.", + "- Generated database identity is authoritative only when its same-transaction export receipt grants canonical authority.", + "- Runtime/session memory is temporary, noncanonical, and cannot serve as evidence.", + "- Hosted model weights are provider-managed and must be attributed by each turn receipt.", + "", + ] + ) + structured = { + "schema": STRUCTURED_VIEW_SCHEMA, + "identity_name": "Leo", + "identity_inputs_sha256": identity_inputs_sha256, + "static_constitution": {"authority": "pinned_static_source", "rules": rules}, + "database_identity": { + "authority": database_provenance["authority"], + "canonical_authority_granted": database_provenance["canonical_authority_granted"], + "source_mode": database_provenance["mode"], + "database_fingerprint_sha256": database_fingerprint_sha256, + "records": records, + }, + "session_boundary": { + "authority": "none", + "classification": "temporary_noncanonical", + "may_be_used_as_evidence": False, + }, + } + return { + "SOUL.md": "\n".join(soul), + "identity.json": json.dumps(structured, indent=2, sort_keys=True) + "\n", + } + + +def build_identity_manifest( + *, + behavior_manifest: dict[str, Any], + database_fingerprint_path: Path, + constitution_path: Path, + database_identity_path: Path, + source_root: Path, + source_commit: str | None = None, +) -> dict[str, Any]: + validate_behavior_manifest(behavior_manifest) + database_fingerprint = load_json(database_fingerprint_path, "database fingerprint") + validate_database_fingerprint(database_fingerprint) + constitution = load_json(constitution_path, "constitution source") + database_identity = load_json(database_identity_path, "database identity source") + rules = canonical_rules(constitution) + records = canonical_database_records( + database_identity, fingerprint_sha256=database_fingerprint["fingerprint_sha256"] + ) + database_provenance = database_identity_provenance( + database_identity, + fingerprint=database_fingerprint, + ) + runtime_commit = behavior_manifest["teleo_infrastructure_runtime"]["git_head"] + source_commit = source_commit or runtime_commit + _require(bool(HEX_40.fullmatch(str(source_commit))), "source commit is invalid") + _require(source_commit == runtime_commit, "source commit does not match the behavior manifest") + + model = behavior_manifest["model_runtime"] + components = behavior_manifest["components"] + marker = database_fingerprint["read_consistency"]["before"] + core = { + "identity_name": "Leo", + "source_commit": source_commit, + "runtime": { + "identity_runtime_sha256": behavior_manifest["identity_runtime_sha256"], + "hermes_git_head": behavior_manifest["hermes_runtime"]["git_head"], + "hermes_source_sha256": behavior_manifest["hermes_runtime"]["source_tree"]["sha256"], + "teleo_git_head": runtime_commit, + "teleo_source_sha256": behavior_manifest["teleo_infrastructure_runtime"]["identity_source_tree"]["sha256"], + "python": behavior_manifest["python_runtime"], + }, + "model": { + "provider": model["provider"], + "default_model": model["default_model"], + "smart_routing": model.get("smart_routing"), + "smart_routing_model": model.get("smart_routing_model"), + "gateway_smart_model_routing": model.get("gateway_smart_model_routing"), + "model_section_sha256": model["model_section_sha256"], + "hosted_weights": "external_provider_managed_not_locally_hashable", + "actual_model_required_in_turn_receipt": True, + }, + "skills": { + "content_sha256": components["procedural_skills"]["content"]["sha256"], + "file_count": components["procedural_skills"]["content"]["file_count"], + }, + "permissions": { + "identity_config_sha256": model["identity_config_sha256"], + "gateway_permissions_sha256": model["gateway_permissions_sha256"], + "tool_permissions_sha256": model["tool_permissions_sha256"], + "runtime_policy": model["identity_runtime_policy"], + "authorization_decision_required_in_runtime_receipt": True, + }, + "canonical_database": { + "database": marker["database"], + "database_user": marker["database_user"], + "system_identifier": marker["system_identifier"], + "authority": database_provenance["authority"], + "canonical_authority_granted": database_provenance["canonical_authority_granted"], + **{ + key: database_fingerprint[key] + for key in ("fingerprint_sha256", "table_rows_sha256", "structure_sha256", "table_count", "total_rows") + }, + "source": source_binding( + database_fingerprint_path, + source_root=source_root, + semantic_value=database_fingerprint_semantic(database_fingerprint), + count=database_fingerprint["table_count"], + pin_content=False, + ), + }, + "identity_sources": { + "static_constitution": { + "authority": "pinned_static_source", + **source_binding(constitution_path, source_root=source_root, semantic_value=rules, count=len(rules)), + }, + "database_derived_identity": { + "authority": database_provenance["authority"], + "provenance": database_provenance, + "database_fingerprint_sha256": database_fingerprint["fingerprint_sha256"], + **source_binding( + database_identity_path, + source_root=source_root, + semantic_value={"provenance": database_provenance, "records": records}, + count=len(records), + ), + }, + }, + "session_boundary": expected_session_boundary(), + "gatewayrunner_execution_contract": expected_gatewayrunner_contract(), + } + identity_inputs_sha256 = behavior.canonical_sha256(core) + views = render_identity_views( + identity_inputs_sha256=identity_inputs_sha256, + database_fingerprint_sha256=database_fingerprint["fingerprint_sha256"], + database_provenance=database_provenance, + rules=rules, + records=records, + ) + stable = { + "schema": SCHEMA, + "identity_inputs": core, + "identity_inputs_sha256": identity_inputs_sha256, + "compiled_views": { + name: { + "role": "generated_view_not_authority", + "sha256": behavior.sha256_bytes(content.encode("utf-8")), + "generated_from_identity_inputs_sha256": identity_inputs_sha256, + } + for name, content in sorted(views.items()) + }, + } + return {**stable, "manifest_sha256": behavior.canonical_sha256(stable)} + + +def validate_identity_manifest(value: dict[str, Any]) -> None: + _require(value.get("schema") == SCHEMA, "unsupported identity manifest schema") + expected = value.get("manifest_sha256") + _require(_is_sha256(expected), "identity manifest sha256 is invalid") + stable = {key: item for key, item in value.items() if key != "manifest_sha256"} + _require(behavior.canonical_sha256(stable) == expected, "identity manifest hash drift detected") + core = value.get("identity_inputs") + _require(isinstance(core, dict), "identity inputs are missing") + _require( + set(core) + == { + "identity_name", + "source_commit", + "runtime", + "model", + "skills", + "permissions", + "canonical_database", + "identity_sources", + "session_boundary", + "gatewayrunner_execution_contract", + }, + "identity input contract fields are invalid", + ) + identity_hash = value.get("identity_inputs_sha256") + _require(_is_sha256(identity_hash), "identity input hash is invalid") + _require(behavior.canonical_sha256(core) == identity_hash, "identity input hash drift detected") + _require(core.get("identity_name") == "Leo", "identity name must be Leo") + + runtime = core.get("runtime") + _require(isinstance(runtime, dict), "runtime binding is missing") + _require( + set(runtime) + == { + "identity_runtime_sha256", + "hermes_git_head", + "hermes_source_sha256", + "teleo_git_head", + "teleo_source_sha256", + "python", + }, + "runtime binding fields are invalid", + ) + _require(_is_sha256(runtime.get("identity_runtime_sha256")), "identity runtime binding is invalid") + for field in ("hermes_git_head", "teleo_git_head"): + _require(bool(HEX_40.fullmatch(str(runtime.get(field) or ""))), f"runtime {field} is invalid") + for field in ("hermes_source_sha256", "teleo_source_sha256"): + _require(_is_sha256(runtime.get(field)), f"runtime {field} is invalid") + _require(core.get("source_commit") == runtime["teleo_git_head"], "source commit is not bound to Teleo Git") + python_runtime = runtime.get("python") + _require(isinstance(python_runtime, dict), "Python runtime binding is missing") + _require( + bool(python_runtime.get("implementation") and python_runtime.get("python_version")), + "Python runtime binding is incomplete", + ) + _require(_is_sha256(python_runtime.get("runtime_distributions_sha256")), "Python distribution binding is invalid") + + model = core.get("model") + _require(isinstance(model, dict), "model binding is missing") + _require( + set(model) + == { + "provider", + "default_model", + "smart_routing", + "smart_routing_model", + "gateway_smart_model_routing", + "model_section_sha256", + "hosted_weights", + "actual_model_required_in_turn_receipt", + }, + "model binding fields are invalid", + ) + _require(bool(model.get("provider") and model.get("default_model")), "model provider and default must be pinned") + _require(_is_sha256(model.get("model_section_sha256")), "model section binding is invalid") + _require( + model.get("hosted_weights") == "external_provider_managed_not_locally_hashable", + "hosted model boundary is invalid", + ) + _require(model.get("actual_model_required_in_turn_receipt") is True, "per-turn model receipt gate is invalid") + + skills = core.get("skills") + _require(isinstance(skills, dict) and set(skills) == {"content_sha256", "file_count"}, "skills binding is invalid") + _require(_is_sha256(skills.get("content_sha256")), "skills content hash is invalid") + _require( + isinstance(skills.get("file_count"), int) + and not isinstance(skills.get("file_count"), bool) + and skills["file_count"] > 0, + "skills file count is invalid", + ) + + permissions = core.get("permissions") + _require(isinstance(permissions, dict), "permission binding is missing") + _require( + set(permissions) + == { + "identity_config_sha256", + "gateway_permissions_sha256", + "tool_permissions_sha256", + "runtime_policy", + "authorization_decision_required_in_runtime_receipt", + }, + "permission binding fields are invalid", + ) + for field in ("identity_config_sha256", "gateway_permissions_sha256", "tool_permissions_sha256"): + _require(_is_sha256(permissions.get(field)), f"permission binding {field} is invalid") + _require( + permissions.get("runtime_policy") == expected_runtime_policy(), + "identity runtime policy must be the exact local readback policy", + ) + _require( + permissions.get("authorization_decision_required_in_runtime_receipt") is True, + "runtime authorization receipt gate is invalid", + ) + + database = core.get("canonical_database") + _require(isinstance(database, dict), "canonical database binding is missing") + for field in ("fingerprint_sha256", "table_rows_sha256", "structure_sha256"): + _require(_is_sha256(database.get(field)), f"canonical database {field} is invalid") + _require( + bool(database.get("database") and database.get("database_user") and database.get("system_identifier")), + "canonical database identity is incomplete", + ) + allowed_authorities = {"synthetic_noncanonical_fixture": False} + _require(database.get("authority") in allowed_authorities, "database authority is invalid") + _require( + database.get("canonical_authority_granted") is allowed_authorities[database["authority"]], + "database canonical authority grant is invalid", + ) + _require( + isinstance(database.get("table_count"), int) + and not isinstance(database.get("table_count"), bool) + and database["table_count"] > 0, + "canonical database table_count is invalid", + ) + _require( + isinstance(database.get("total_rows"), int) + and not isinstance(database.get("total_rows"), bool) + and database["total_rows"] >= 0, + "canonical database total_rows is invalid", + ) + + def validate_source_binding(binding: Any, *, label: str, content_required: bool) -> None: + _require(isinstance(binding, dict), f"{label} source binding is missing") + repo_path = binding.get("repo_path") + _require( + isinstance(repo_path, str) + and bool(repo_path) + and not Path(repo_path).is_absolute() + and ".." not in Path(repo_path).parts, + f"{label} source path is invalid", + ) + _require(_is_sha256(binding.get("semantic_sha256")), f"{label} semantic binding is invalid") + _require( + isinstance(binding.get("record_count"), int) + and not isinstance(binding.get("record_count"), bool) + and binding["record_count"] > 0, + f"{label} record count is invalid", + ) + if content_required: + _require(_is_sha256(binding.get("content_sha256")), f"{label} content binding is invalid") + else: + _require("content_sha256" not in binding, f"{label} content binding must exclude volatile capture markers") + + validate_source_binding(database.get("source"), label="database fingerprint", content_required=False) + sources = core.get("identity_sources") + _require( + isinstance(sources, dict) and set(sources) == {"static_constitution", "database_derived_identity"}, + "identity source bindings are invalid", + ) + constitution = sources["static_constitution"] + derived = sources["database_derived_identity"] + _require(isinstance(constitution, dict), "constitution source binding is invalid") + _require(isinstance(derived, dict), "database identity source binding is invalid") + _require(constitution.get("authority") == "pinned_static_source", "constitution authority is invalid") + _require(derived.get("authority") == database["authority"], "database identity authority is invalid") + provenance = derived.get("provenance") + _require(isinstance(provenance, dict), "database identity provenance is missing") + _require(provenance.get("authority") == database["authority"], "database identity provenance authority is invalid") + _require( + provenance.get("canonical_authority_granted") is database["canonical_authority_granted"], + "database identity provenance grant is invalid", + ) + _require(provenance.get("mode") == SYNTHETIC_DATABASE_MODE, "synthetic database provenance mode is invalid") + _require( + provenance.get("same_transaction_as_fingerprint") is False, + "synthetic fixture transaction claim is invalid", + ) + _require(provenance.get("export_receipt_sha256") is None, "synthetic fixture must not claim an export receipt") + _require( + derived.get("database_fingerprint_sha256") == database["fingerprint_sha256"], + "database identity source is misbound", + ) + validate_source_binding(constitution, label="constitution", content_required=True) + validate_source_binding(derived, label="database identity", content_required=True) + _require(core.get("session_boundary") == expected_session_boundary(), "session authority boundary is invalid") + _require( + core.get("gatewayrunner_execution_contract") == expected_gatewayrunner_contract(), + "GatewayRunner execution contract is invalid", + ) + + views = value.get("compiled_views") + _require(isinstance(views, dict) and set(views) == {"SOUL.md", "identity.json"}, "compiled views are incomplete") + for name, view in views.items(): + _require(isinstance(view, dict) and _is_sha256(view.get("sha256")), f"compiled view {name} is invalid") + _require( + set(view) == {"role", "sha256", "generated_from_identity_inputs_sha256"}, + f"compiled view {name} fields are invalid", + ) + _require(view.get("role") == "generated_view_not_authority", f"compiled view {name} authority is invalid") + _require( + view.get("generated_from_identity_inputs_sha256") == identity_hash, f"compiled view {name} is misbound" + ) + + +def verify_bound_sources(value: dict[str, Any], source_root: Path) -> dict[str, str]: + validate_identity_manifest(value) + core = value["identity_inputs"] + bindings = { + "database fingerprint": core["canonical_database"]["source"], + "constitution source": core["identity_sources"]["static_constitution"], + "database identity source": core["identity_sources"]["database_derived_identity"], + } + paths = {label: source_root / binding["repo_path"] for label, binding in bindings.items()} + for label, binding in bindings.items(): + path = paths[label] + _require(path.is_file(), f"bound {label} is missing") + if binding.get("content_sha256"): + _require(behavior.file_sha256(path) == binding["content_sha256"], f"bound {label} content drift detected") + fingerprint = load_json(paths["database fingerprint"], "database fingerprint") + validate_database_fingerprint(fingerprint) + _require( + behavior.canonical_sha256(database_fingerprint_semantic(fingerprint)) + == bindings["database fingerprint"]["semantic_sha256"], + "database fingerprint semantic drift detected", + ) + marker = fingerprint["read_consistency"]["before"] + for field in ("database", "database_user", "system_identifier"): + _require(marker[field] == core["canonical_database"][field], f"canonical database {field} drift detected") + rules = canonical_rules(load_json(paths["constitution source"], "constitution source")) + database_identity = load_json(paths["database identity source"], "database identity source") + records = canonical_database_records( + database_identity, + fingerprint_sha256=fingerprint["fingerprint_sha256"], + ) + database_provenance = database_identity_provenance( + database_identity, + fingerprint=fingerprint, + ) + _require( + behavior.canonical_sha256(rules) == bindings["constitution source"]["semantic_sha256"], + "constitution semantic drift detected", + ) + _require( + behavior.canonical_sha256({"provenance": database_provenance, "records": records}) + == bindings["database identity source"]["semantic_sha256"], + "database identity semantic drift detected", + ) + _require( + database_provenance == core["identity_sources"]["database_derived_identity"]["provenance"], + "database identity provenance drift detected", + ) + _require( + fingerprint["fingerprint_sha256"] == core["canonical_database"]["fingerprint_sha256"], + "canonical database fingerprint drift detected", + ) + views = render_identity_views( + identity_inputs_sha256=value["identity_inputs_sha256"], + database_fingerprint_sha256=fingerprint["fingerprint_sha256"], + database_provenance=database_provenance, + rules=rules, + records=records, + ) + for name, content in views.items(): + _require( + behavior.sha256_bytes(content.encode("utf-8")) == value["compiled_views"][name]["sha256"], + f"compiled view contract drift detected for {name}", + ) + return views + + +def write_json(path: Path, value: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + generate = subparsers.add_parser("generate") + generate.add_argument("--behavior-manifest", type=Path, required=True) + generate.add_argument("--database-fingerprint", type=Path, required=True) + generate.add_argument("--constitution", type=Path, required=True) + generate.add_argument("--database-identity", type=Path, required=True) + generate.add_argument("--source-root", type=Path, required=True) + generate.add_argument("--source-commit") + generate.add_argument("--output", type=Path, required=True) + verify = subparsers.add_parser("verify") + verify.add_argument("--manifest", type=Path, required=True) + verify.add_argument("--source-root", type=Path) + args = parser.parse_args() + try: + if args.command == "generate": + manifest = build_identity_manifest( + behavior_manifest=load_json(args.behavior_manifest, "behavior manifest"), + database_fingerprint_path=args.database_fingerprint, + constitution_path=args.constitution, + database_identity_path=args.database_identity, + source_root=args.source_root, + source_commit=args.source_commit, + ) + write_json(args.output, manifest) + else: + manifest = load_json(args.manifest, "identity manifest") + validate_identity_manifest(manifest) + if args.source_root: + verify_bound_sources(manifest, args.source_root) + print(json.dumps({"status": "ok", "manifest_sha256": manifest["manifest_sha256"]})) + return 0 + except IdentityManifestError as exc: + print(json.dumps({"status": "error", "error": "identity_manifest_invalid", "message": str(exc)})) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/leo_identity_profile.py b/scripts/leo_identity_profile.py new file mode 100644 index 0000000..11003ca --- /dev/null +++ b/scripts/leo_identity_profile.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +"""Compile, verify, and query a disposable Leo identity profile.""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import sys +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +import yaml + +if __package__ in {None, ""}: + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from scripts import leo_behavior_manifest as behavior +from scripts import leo_identity_manifest as identity + +LOCK_SCHEMA = "livingip.leoIdentityProfileLock.v1" +COMPILE_RECEIPT_SCHEMA = "livingip.leoIdentityCompileReceipt.v1" +QUERY_RECEIPT_SCHEMA = "livingip.leoIdentityQueryReceipt.v1" +SESSION_RECORD_SCHEMA = "livingip.leoTemporarySessionRecord.v1" +FIXED_QUERY = "What defines Leo's identity, and what is temporary?" +STATIC_PROFILE_ENTRIES = ("config.yaml", "skills", "plugins", "bin") +COMPILED_PROFILE_ENTRIES = frozenset( + { + "config.yaml", + "skills", + "plugins", + "bin", + "SOUL.md", + "identity.json", + "identity-manifest.json", + "identity-lock.json", + "compile-receipt.json", + "sessions", + "state", + } +) +OMITTED_NONAUTHORITATIVE_ENTRIES = ( + "memories", + "MEMORY.md", + "USER.md", + "platforms/pairing", + ".skills_prompt_snapshot.json", + ".hermes.md", + "HERMES.md", + "AGENTS.md", + "CLAUDE.md", + ".cursorrules", +) + + +class IdentityProfileError(identity.IdentityManifestError): + """A compiled profile cannot be trusted or started.""" + + +def _require(condition: bool, message: str) -> None: + if not condition: + raise IdentityProfileError(message) + + +def _copy_static_profile(template: Path, target: Path) -> list[str]: + _require(template.is_dir(), "profile template is missing") + _require(not target.exists(), "compiled profile target already exists") + target.mkdir(parents=True, mode=0o700) + copied: list[str] = [] + for name in STATIC_PROFILE_ENTRIES: + source = template / name + if not source.exists(): + continue + destination = target / name + _require(not source.is_symlink(), f"unsafe symlinked static profile entry: {name}") + if source.is_dir(): + _require( + not any(path.is_symlink() for path in source.rglob("*")), + f"unsafe symlink inside static profile entry: {name}", + ) + shutil.copytree(source, destination, symlinks=False) + elif source.is_file() and not source.is_symlink(): + if name == "config.yaml": + try: + raw_config = yaml.safe_load(source.read_text(encoding="utf-8")) or {} + except (OSError, TypeError, yaml.YAMLError) as exc: + raise IdentityProfileError(f"cannot sanitize profile config: {type(exc).__name__}") from exc + _require(isinstance(raw_config, dict), "profile config must be a mapping") + destination.write_text( + yaml.safe_dump(behavior.identity_config_projection(raw_config), sort_keys=False), + encoding="utf-8", + ) + destination.chmod(0o600) + else: + shutil.copy2(source, destination) + else: + raise IdentityProfileError(f"unsafe static profile entry: {name}") + copied.append(name) + _require("config.yaml" in copied, "profile template config.yaml is missing") + return copied + + +def _behavior(profile: Path, hermes_root: Path, deployment_root: Path) -> dict[str, Any]: + return behavior.build_manifest(profile, hermes_root, deployment_root) + + +def _verify_runtime_binding( + manifest: dict[str, Any], + *, + profile: Path, + hermes_root: Path, + deployment_root: Path, +) -> dict[str, Any]: + observed = _behavior(profile, hermes_root, deployment_root) + identity.validate_behavior_manifest(observed) + expected = manifest["identity_inputs"]["runtime"] + _require( + observed["identity_runtime_sha256"] == expected["identity_runtime_sha256"], "identity runtime drift detected" + ) + _require(observed["hermes_runtime"]["git_head"] == expected["hermes_git_head"], "Hermes Git drift detected") + _require( + observed["hermes_runtime"]["source_tree"]["sha256"] == expected["hermes_source_sha256"], + "Hermes source drift detected", + ) + _require( + observed["teleo_infrastructure_runtime"]["git_head"] == expected["teleo_git_head"], + "Teleo Git drift detected", + ) + _require( + observed["teleo_infrastructure_runtime"]["identity_source_tree"]["sha256"] == expected["teleo_source_sha256"], + "Teleo identity source drift detected", + ) + _require(observed["python_runtime"] == expected["python"], "Python runtime drift detected") + _require( + manifest["identity_inputs"]["source_commit"] == observed["teleo_infrastructure_runtime"]["git_head"], + "source commit drift detected", + ) + + expected_model = manifest["identity_inputs"]["model"] + observed_model = observed["model_runtime"] + model_bindings = { + "provider": observed_model.get("provider"), + "default_model": observed_model.get("default_model"), + "smart_routing": observed_model.get("smart_routing"), + "smart_routing_model": observed_model.get("smart_routing_model"), + "gateway_smart_model_routing": observed_model.get("gateway_smart_model_routing"), + "model_section_sha256": observed_model.get("model_section_sha256"), + "hosted_weights": observed_model.get("base_model_weights"), + "actual_model_required_in_turn_receipt": observed_model.get( + "actual_per_turn_model_must_be_recorded_by_the_call_receipt" + ), + } + for field, observed_value in model_bindings.items(): + _require(expected_model.get(field) == observed_value, f"model runtime binding drift detected: {field}") + + expected_permissions = manifest["identity_inputs"]["permissions"] + permission_bindings = { + "identity_config_sha256": observed_model.get("identity_config_sha256"), + "gateway_permissions_sha256": observed_model.get("gateway_permissions_sha256"), + "tool_permissions_sha256": observed_model.get("tool_permissions_sha256"), + "runtime_policy": observed_model.get("identity_runtime_policy"), + "authorization_decision_required_in_runtime_receipt": True, + } + for field, observed_value in permission_bindings.items(): + _require( + expected_permissions.get(field) == observed_value, f"permission runtime binding drift detected: {field}" + ) + + observed_skills = observed["components"]["procedural_skills"]["content"] + expected_skills = manifest["identity_inputs"]["skills"] + _require(expected_skills["content_sha256"] == observed_skills["sha256"], "skills runtime binding drift detected") + _require(expected_skills["file_count"] == observed_skills["file_count"], "skills file count drift detected") + return observed + + +def _view_file_sha256(profile: Path, name: str) -> str: + path = profile / name + _require(path.is_file() and not path.is_symlink(), f"compiled view is missing or unsafe: {name}") + return behavior.file_sha256(path) + + +def _verify_nonauthoritative_surfaces( + profile: Path, + *, + require_empty_sessions: bool, + require_compile_receipt: bool, +) -> dict[str, Any]: + omitted = { + name: not (profile / name).exists() and not (profile / name).is_symlink() + for name in OMITTED_NONAUTHORITATIVE_ENTRIES + } + _require(all(omitted.values()), "profile contains a forbidden nonauthoritative input surface") + expected_entries = set(COMPILED_PROFILE_ENTRIES) + if not require_compile_receipt: + expected_entries.remove("compile-receipt.json") + profile_entries = list(profile.iterdir()) + actual_entries = {path.name for path in profile_entries} + _require(actual_entries == expected_entries, "compiled profile entry set is not exact") + _require(not any(path.is_symlink() for path in profile_entries), "compiled profile entries must not be symlinks") + if require_compile_receipt: + compile_receipt = identity.load_json(profile / "compile-receipt.json", "identity compile receipt") + _require(compile_receipt.get("schema") == COMPILE_RECEIPT_SCHEMA, "identity compile receipt schema is invalid") + _require(compile_receipt.get("status") == "pass", "identity compile receipt status is invalid") + state = profile / "state" + sessions = profile / "sessions" + for path, label in ((state, "state"), (sessions, "sessions")): + _require(path.is_dir() and not path.is_symlink(), f"profile {label} directory is missing or unsafe") + _require(not any(state.iterdir()), "profile state directory must remain empty") + session_files = list(sessions.iterdir()) + if require_empty_sessions: + _require(not session_files, "profile sessions must start empty") + for path in session_files: + _require(path.is_file() and not path.is_symlink() and path.suffix == ".json", "unsafe session entry detected") + record = identity.load_json(path, "temporary session record") + _require( + set(record) + == { + "schema", + "authority", + "classification", + "query_sha256", + "answer_sha256", + "may_be_used_as_evidence", + }, + "temporary session record fields are invalid", + ) + _require(record.get("schema") == SESSION_RECORD_SCHEMA, "temporary session record schema is invalid") + _require(record.get("authority") == "none", "temporary session authority is invalid") + _require(record.get("classification") == "temporary_noncanonical", "temporary session class is invalid") + _require(record.get("may_be_used_as_evidence") is False, "temporary session cannot be evidence") + _require(identity._is_sha256(record.get("query_sha256")), "temporary session query hash is invalid") + _require(identity._is_sha256(record.get("answer_sha256")), "temporary session answer hash is invalid") + return { + "nonauthoritative_inputs_omitted": omitted, + "session_file_count": len(session_files), + "state_empty": True, + } + + +def compile_profile( + manifest: dict[str, Any], + *, + source_root: Path, + profile_template: Path, + profile: Path, + hermes_root: Path, + deployment_root: Path, +) -> dict[str, Any]: + views = identity.verify_bound_sources(manifest, source_root) + template_behavior = _behavior(profile_template, hermes_root, deployment_root) + identity.validate_behavior_manifest(template_behavior) + _require( + template_behavior["identity_runtime_sha256"] + == manifest["identity_inputs"]["runtime"]["identity_runtime_sha256"], + "profile template does not match the pinned identity runtime", + ) + copied = _copy_static_profile(profile_template, profile) + try: + for name, content in views.items(): + path = profile / name + path.write_text(content, encoding="utf-8") + path.chmod(0o600) + identity.write_json(profile / "identity-manifest.json", manifest) + (profile / "identity-manifest.json").chmod(0o600) + for directory in (profile / "sessions", profile / "state"): + directory.mkdir(mode=0o700) + lock = { + "schema": LOCK_SCHEMA, + "manifest_sha256": manifest["manifest_sha256"], + "identity_inputs_sha256": manifest["identity_inputs_sha256"], + "compiled_views": {name: {"sha256": _view_file_sha256(profile, name)} for name in sorted(views)}, + "session_boundary": manifest["identity_inputs"]["session_boundary"], + } + identity.write_json(profile / "identity-lock.json", lock) + (profile / "identity-lock.json").chmod(0o600) + observed = _verify_runtime_binding( + manifest, + profile=profile, + hermes_root=hermes_root, + deployment_root=deployment_root, + ) + for name, expected in manifest["compiled_views"].items(): + _require(_view_file_sha256(profile, name) == expected["sha256"], f"compiled {name} hash mismatch") + soul_files = observed["components"]["runtime_identity"]["content"]["files"] + soul = next((item for item in soul_files if item.get("path") == "SOUL.md"), None) + _require( + isinstance(soul, dict) and soul.get("sha256") == manifest["compiled_views"]["SOUL.md"]["sha256"], + "SOUL runtime binding failed", + ) + surface_readback = _verify_nonauthoritative_surfaces( + profile, + require_empty_sessions=True, + require_compile_receipt=False, + ) + receipt = { + "schema": COMPILE_RECEIPT_SCHEMA, + "status": "pass", + "manifest_sha256": manifest["manifest_sha256"], + "identity_inputs_sha256": manifest["identity_inputs_sha256"], + "profile_static_entries": copied, + "compiled_view_sha256": { + name: manifest["compiled_views"][name]["sha256"] for name in sorted(manifest["compiled_views"]) + }, + "runtime_identity_sha256": observed["identity_runtime_sha256"], + "session_directories_started_empty": all( + not any((profile / name).iterdir()) for name in ("sessions", "state") + ), + "nonauthoritative_inputs_omitted": surface_readback["nonauthoritative_inputs_omitted"], + "generated_views_are_authority": False, + } + identity.write_json(profile / "compile-receipt.json", receipt) + return receipt + except BaseException: + shutil.rmtree(profile, ignore_errors=True) + raise + + +def verify_profile( + profile: Path, + *, + source_root: Path, + hermes_root: Path, + deployment_root: Path, +) -> tuple[dict[str, Any], dict[str, str], dict[str, Any]]: + manifest = identity.load_json(profile / "identity-manifest.json", "compiled identity manifest") + views = identity.verify_bound_sources(manifest, source_root) + lock = identity.load_json(profile / "identity-lock.json", "identity profile lock") + _verify_nonauthoritative_surfaces( + profile, + require_empty_sessions=False, + require_compile_receipt=True, + ) + _require(lock.get("schema") == LOCK_SCHEMA, "identity profile lock schema is invalid") + _require( + lock.get("manifest_sha256") == manifest["manifest_sha256"], "identity profile lock manifest drift detected" + ) + _require( + lock.get("identity_inputs_sha256") == manifest["identity_inputs_sha256"], + "identity profile lock input drift detected", + ) + compile_receipt = identity.load_json(profile / "compile-receipt.json", "identity compile receipt") + _require( + compile_receipt.get("manifest_sha256") == manifest["manifest_sha256"], + "identity compile receipt manifest drift detected", + ) + _require( + compile_receipt.get("identity_inputs_sha256") == manifest["identity_inputs_sha256"], + "identity compile receipt input drift detected", + ) + for name, expected_content in views.items(): + expected_hash = behavior.sha256_bytes(expected_content.encode("utf-8")) + _require(expected_hash == manifest["compiled_views"][name]["sha256"], f"manifest view drift detected: {name}") + _require(_view_file_sha256(profile, name) == expected_hash, f"compiled view drift detected: {name}") + _require( + (lock.get("compiled_views") or {}).get(name, {}).get("sha256") == expected_hash, + f"profile lock view drift detected: {name}", + ) + observed = _verify_runtime_binding( + manifest, + profile=profile, + hermes_root=hermes_root, + deployment_root=deployment_root, + ) + return manifest, views, observed + + +def query_profile( + profile: Path, + *, + query: str, + source_root: Path, + hermes_root: Path, + deployment_root: Path, +) -> dict[str, Any]: + _require(query == FIXED_QUERY, "only the pinned identity readback query is allowed") + manifest, _views, observed = verify_profile( + profile, + source_root=source_root, + hermes_root=hermes_root, + deployment_root=deployment_root, + ) + structured = identity.load_json(profile / "identity.json", "compiled structured identity view") + rule_count = len(structured["static_constitution"]["rules"]) + record_count = len(structured["database_identity"]["records"]) + fingerprint = structured["database_identity"]["database_fingerprint_sha256"] + canonical_authority = structured["database_identity"]["canonical_authority_granted"] + database_description = "canonical database" if canonical_authority else "synthetic noncanonical fixture" + answer = ( + f"Leo is defined by {rule_count} pinned static constitutional rules and {record_count} active " + f"database-derived identity rows from a {database_description} at fingerprint {fingerprint}. " + "SOUL.md is a generated view; " + "runtime/session memory is temporary, noncanonical, and cannot be evidence." + ) + query_sha256 = behavior.sha256_bytes(query.encode("utf-8")) + answer_sha256 = behavior.sha256_bytes(answer.encode("utf-8")) + instance_id = uuid.uuid4().hex + session_record = { + "schema": SESSION_RECORD_SCHEMA, + "authority": "none", + "classification": "temporary_noncanonical", + "query_sha256": query_sha256, + "answer_sha256": answer_sha256, + "may_be_used_as_evidence": False, + } + session_path = profile / "sessions" / f"{instance_id}.json" + identity.write_json(session_path, session_record) + receipt = { + "schema": QUERY_RECEIPT_SCHEMA, + "status": "pass", + "started_at_utc": datetime.now(UTC).isoformat(), + "process_id": os.getpid(), + "runtime_instance_id": instance_id, + "query": query, + "query_sha256": query_sha256, + "answer": answer, + "answer_sha256": answer_sha256, + "manifest_sha256": manifest["manifest_sha256"], + "identity_inputs_sha256": manifest["identity_inputs_sha256"], + "output_provenance": { + "static_constitution_sha256": manifest["identity_inputs"]["identity_sources"]["static_constitution"][ + "semantic_sha256" + ], + "database_identity_sha256": manifest["identity_inputs"]["identity_sources"]["database_derived_identity"][ + "semantic_sha256" + ], + "database_fingerprint_sha256": fingerprint, + "database_authority": structured["database_identity"]["authority"], + "database_canonical_authority_granted": canonical_authority, + "compiled_identity_sha256": manifest["compiled_views"]["identity.json"]["sha256"], + "compiled_soul_sha256": manifest["compiled_views"]["SOUL.md"]["sha256"], + "runtime_identity_sha256": observed["identity_runtime_sha256"], + "actual_model_call_performed": False, + }, + "authorization": { + "declared_runtime_policy": manifest["identity_inputs"]["permissions"]["runtime_policy"], + "authorization_decision_observed": False, + "claim": "local_policy_binding_not_an_authorizer_readback", + }, + "session": { + "record_sha256": behavior.file_sha256(session_path), + "classification": "temporary_noncanonical", + "included_in_output_provenance": False, + "may_be_used_as_evidence": False, + }, + } + return receipt + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + compile_command = subparsers.add_parser("compile") + compile_command.add_argument("--manifest", type=Path, required=True) + compile_command.add_argument("--source-root", type=Path, required=True) + compile_command.add_argument("--profile-template", type=Path, required=True) + compile_command.add_argument("--profile", type=Path, required=True) + compile_command.add_argument("--hermes-root", type=Path, required=True) + compile_command.add_argument("--deployment-root", type=Path, required=True) + query_command = subparsers.add_parser("query") + query_command.add_argument("--profile", type=Path, required=True) + query_command.add_argument("--query", default=FIXED_QUERY) + query_command.add_argument("--source-root", type=Path, required=True) + query_command.add_argument("--hermes-root", type=Path, required=True) + query_command.add_argument("--deployment-root", type=Path, required=True) + args = parser.parse_args() + try: + if args.command == "compile": + result = compile_profile( + identity.load_json(args.manifest, "identity manifest"), + source_root=args.source_root, + profile_template=args.profile_template, + profile=args.profile, + hermes_root=args.hermes_root, + deployment_root=args.deployment_root, + ) + else: + result = query_profile( + args.profile, + query=args.query, + source_root=args.source_root, + hermes_root=args.hermes_root, + deployment_root=args.deployment_root, + ) + print(json.dumps(result, sort_keys=True)) + return 0 + except identity.IdentityManifestError as exc: + print(json.dumps({"status": "error", "error": "identity_drift", "message": str(exc)})) + return 3 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_leo_identity_reconstruction_canary.py b/scripts/run_leo_identity_reconstruction_canary.py new file mode 100644 index 0000000..2850804 --- /dev/null +++ b/scripts/run_leo_identity_reconstruction_canary.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +"""Prove manifest-driven Leo identity reconstruction across a cold restart.""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import signal +import subprocess +import sys +import tempfile +import time +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +if __package__ in {None, ""}: + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from scripts import leo_behavior_manifest as behavior +from scripts import leo_identity_manifest as identity +from scripts import leo_identity_profile as profile_runtime + +SCHEMA = "livingip.leoIdentityReconstructionReceipt.v1" + + +class CanaryError(RuntimeError): + """The disposable identity runtime did not satisfy its T2 contract.""" + + +def _require(condition: bool, message: str) -> None: + if not condition: + raise CanaryError(message) + + +def _process_group_absent(process_group: int) -> bool: + try: + os.killpg(process_group, 0) + except ProcessLookupError: + return True + except PermissionError: + return False + return False + + +def _wait_for_process_group_absence(process_group: int, timeout_seconds: float = 5) -> bool: + deadline = time.monotonic() + timeout_seconds + while time.monotonic() < deadline: + if _process_group_absent(process_group): + return True + time.sleep(0.02) + return _process_group_absent(process_group) + + +def _signal_process_group(process_group: int, signal_number: signal.Signals) -> bool: + try: + os.killpg(process_group, signal_number) + return True + except (ProcessLookupError, PermissionError): + return False + + +def _cleanup_remaining_process_group(process_group: int) -> str | None: + if not _signal_process_group(process_group, signal.SIGTERM): + return None + if _wait_for_process_group_absence(process_group, timeout_seconds=1): + return "SIGTERM" + if not _signal_process_group(process_group, signal.SIGKILL): + return "SIGTERM" + return "SIGKILL" + + +def _run_query_child( + *, + deployment_root: Path, + profile: Path, + source_root: Path, + hermes_root: Path, + timeout_seconds: float = 60, +) -> dict[str, Any]: + command = [ + sys.executable, + "-m", + "scripts.leo_identity_profile", + "query", + "--profile", + str(profile), + "--source-root", + str(source_root), + "--hermes-root", + str(hermes_root), + "--deployment-root", + str(deployment_root), + "--query", + profile_runtime.FIXED_QUERY, + ] + process = subprocess.Popen( + command, + cwd=deployment_root, + env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + start_new_session=True, + ) + timed_out = False + terminated_with: str | None = None + stdout = "" + stderr = "" + process_group_absent_after_wait = False + orphan_cleanup_required = False + try: + try: + stdout, stderr = process.communicate(timeout=timeout_seconds) + except subprocess.TimeoutExpired: + timed_out = True + if _signal_process_group(process.pid, signal.SIGTERM): + terminated_with = "SIGTERM" + try: + stdout, stderr = process.communicate(timeout=5) + except subprocess.TimeoutExpired: + if _signal_process_group(process.pid, signal.SIGKILL): + terminated_with = "SIGKILL" + stdout, stderr = process.communicate(timeout=5) + finally: + if process.poll() is None: + if _signal_process_group(process.pid, signal.SIGTERM): + terminated_with = terminated_with or "SIGTERM" + try: + process.communicate(timeout=5) + except subprocess.TimeoutExpired: + if _signal_process_group(process.pid, signal.SIGKILL): + terminated_with = "SIGKILL" + process.communicate(timeout=5) + orphan_cleanup_required = not _process_group_absent(process.pid) + if orphan_cleanup_required: + terminated_with = _cleanup_remaining_process_group(process.pid) or terminated_with + process_group_absent_after_wait = _wait_for_process_group_absence(process.pid) + try: + payload = json.loads(stdout.strip().splitlines()[-1]) if stdout.strip() else {} + except json.JSONDecodeError as exc: + raise CanaryError("identity child returned invalid JSON") from exc + return { + "command": command, + "launcher_pid": process.pid, + "returncode": process.returncode, + "timed_out": timed_out, + "terminated_with": terminated_with, + "orphan_cleanup_required": orphan_cleanup_required, + "stdout": payload, + "stderr_sha256": behavior.sha256_bytes(stderr.encode("utf-8")), + "process_group_absent_after_wait": process_group_absent_after_wait, + } + + +def _query_passed(child: dict[str, Any]) -> bool: + payload = child.get("stdout") or {} + return bool( + child.get("returncode") == 0 + and child.get("timed_out") is False + and child.get("orphan_cleanup_required") is False + and child.get("process_group_absent_after_wait") is True + and payload.get("schema") == profile_runtime.QUERY_RECEIPT_SCHEMA + and payload.get("status") == "pass" + ) + + +def _drift_attempt( + *, + deployment_root: Path, + source_root: Path, + hermes_root: Path, + drift_profile: Path, +) -> dict[str, Any]: + with (drift_profile / "SOUL.md").open("a", encoding="utf-8") as handle: + handle.write("\nInjected drift must fail closed.\n") + child = _run_query_child( + deployment_root=deployment_root, + profile=drift_profile, + source_root=source_root, + hermes_root=hermes_root, + ) + child["fail_closed"] = bool( + child["returncode"] == 3 + and child["timed_out"] is False + and child["orphan_cleanup_required"] is False + and child["process_group_absent_after_wait"] is True + and (child.get("stdout") or {}).get("error") == "identity_drift" + and "compiled view drift detected" in str((child.get("stdout") or {}).get("message")) + ) + child["answer_returned"] = bool((child.get("stdout") or {}).get("answer")) + return child + + +def run_canary(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + output = args.output.resolve() + temporary_root = Path(tempfile.mkdtemp(prefix="leo-identity-reconstruction-")) + report: dict[str, Any] = { + "schema": SCHEMA, + "generated_at_utc": datetime.now(UTC).isoformat(), + "required_tier": "T2_runtime", + "mode": "drift_before_restart" + if args.inject_drift_before_restart + else "successful_restart_plus_drift_negative", + "fixed_query": profile_runtime.FIXED_QUERY, + "fixed_query_sha256": behavior.sha256_bytes(profile_runtime.FIXED_QUERY.encode("utf-8")), + "production_profile_replaced": False, + "production_database_contacted": False, + "telegram_message_sent": False, + "actual_hosted_model_call_performed": False, + "errors": [], + } + exit_code = 1 + try: + behavior_manifest = behavior.build_manifest(args.profile_template, args.hermes_root, args.deployment_root) + identity.validate_behavior_manifest(behavior_manifest) + manifest = identity.build_identity_manifest( + behavior_manifest=behavior_manifest, + database_fingerprint_path=args.database_fingerprint, + constitution_path=args.constitution, + database_identity_path=args.database_identity, + source_root=args.source_root, + ) + report["manifest"] = manifest + report["behavior_observation_before_compile"] = { + "behavior_sha256": behavior_manifest["behavior_sha256"], + "identity_runtime_sha256": behavior_manifest["identity_runtime_sha256"], + "source_commit": behavior_manifest["teleo_infrastructure_runtime"]["git_head"], + } + compiled_profile = temporary_root / "first-profile" + report["compile"] = profile_runtime.compile_profile( + manifest, + source_root=args.source_root, + profile_template=args.profile_template, + profile=compiled_profile, + hermes_root=args.hermes_root, + deployment_root=args.deployment_root, + ) + compiled_behavior_before_query = behavior.build_manifest( + compiled_profile, args.hermes_root, args.deployment_root + ) + report["compiled_profile_before_query"] = { + "behavior_sha256": compiled_behavior_before_query["behavior_sha256"], + "identity_runtime_sha256": compiled_behavior_before_query["identity_runtime_sha256"], + } + first = _run_query_child( + deployment_root=args.deployment_root, + profile=compiled_profile, + source_root=args.source_root, + hermes_root=args.hermes_root, + ) + report["first_start"] = first + _require(_query_passed(first), "first disposable identity process did not answer successfully") + after_first = behavior.build_manifest(compiled_profile, args.hermes_root, args.deployment_root) + report["session_boundary_readback"] = { + "full_behavior_changed_after_session": after_first["behavior_sha256"] + != compiled_behavior_before_query["behavior_sha256"], + "identity_runtime_unchanged_after_session": after_first["identity_runtime_sha256"] + == compiled_behavior_before_query["identity_runtime_sha256"], + "session_file_count": len(list((compiled_profile / "sessions").glob("*.json"))), + "session_classification": "temporary_noncanonical", + "session_used_as_output_provenance": False, + } + if args.inject_drift_before_restart: + with (compiled_profile / "SOUL.md").open("a", encoding="utf-8") as handle: + handle.write("\nInjected pre-restart drift.\n") + rejected = _run_query_child( + deployment_root=args.deployment_root, + profile=compiled_profile, + source_root=args.source_root, + hermes_root=args.hermes_root, + ) + report["pre_restart_drift"] = rejected + report["drift_target"] = "compiled SOUL.md generated view" + report["second_start_attempted"] = False + report["gates"] = { + "first_start_passed": True, + "first_process_stopped": first["process_group_absent_after_wait"], + "drift_detected_before_restart": rejected["returncode"] == 3 + and (rejected.get("stdout") or {}).get("error") == "identity_drift", + "drift_returned_no_answer": not bool((rejected.get("stdout") or {}).get("answer")), + "drift_process_stopped": rejected["process_group_absent_after_wait"], + "drift_process_had_no_orphan_cleanup": rejected["orphan_cleanup_required"] is False, + "second_start_not_attempted": True, + } + report["status"] = "pass" if all(report["gates"].values()) else "fail" + else: + profile_runtime.verify_profile( + compiled_profile, + source_root=args.source_root, + hermes_root=args.hermes_root, + deployment_root=args.deployment_root, + ) + report["pre_restart_verification"] = "pass" + report["second_start_attempted"] = True + restarted_profile = temporary_root / "restart-profile" + report["restart_compile"] = profile_runtime.compile_profile( + manifest, + source_root=args.source_root, + profile_template=args.profile_template, + profile=restarted_profile, + hermes_root=args.hermes_root, + deployment_root=args.deployment_root, + ) + profile_runtime.verify_profile( + restarted_profile, + source_root=args.source_root, + hermes_root=args.hermes_root, + deployment_root=args.deployment_root, + ) + second = _run_query_child( + deployment_root=args.deployment_root, + profile=restarted_profile, + source_root=args.source_root, + hermes_root=args.hermes_root, + ) + report["restart"] = second + _require(_query_passed(second), "restarted disposable identity process did not answer successfully") + first_payload = first["stdout"] + second_payload = second["stdout"] + drift_profile = temporary_root / "drift-profile" + report["drift_compile"] = profile_runtime.compile_profile( + manifest, + source_root=args.source_root, + profile_template=args.profile_template, + profile=drift_profile, + hermes_root=args.hermes_root, + deployment_root=args.deployment_root, + ) + drift = _drift_attempt( + deployment_root=args.deployment_root, + source_root=args.source_root, + hermes_root=args.hermes_root, + drift_profile=drift_profile, + ) + report["drift_negative"] = drift + report["drift_target"] = "compiled SOUL.md generated view" + report["gates"] = { + "manifest_generated": identity._is_sha256(manifest["manifest_sha256"]), + "views_compiled_and_bound": report["compile"]["status"] == "pass", + "first_start_passed": True, + "first_process_stopped": first["process_group_absent_after_wait"], + "pre_restart_verification_passed": True, + "restart_profile_recompiled_from_manifest": report["restart_compile"]["status"] == "pass", + "restart_profile_started_without_sessions": report["restart_compile"][ + "session_directories_started_empty" + ], + "restart_passed": True, + "restart_process_is_distinct": first_payload["process_id"] != second_payload["process_id"] + and first_payload["runtime_instance_id"] != second_payload["runtime_instance_id"], + "identity_inputs_reproduced": first_payload["identity_inputs_sha256"] + == second_payload["identity_inputs_sha256"], + "manifest_reproduced": first_payload["manifest_sha256"] == second_payload["manifest_sha256"], + "query_reproduced": first_payload["query_sha256"] == second_payload["query_sha256"], + "answer_and_provenance_explainable": first_payload["answer_sha256"] == second_payload["answer_sha256"] + and first_payload["output_provenance"] == second_payload["output_provenance"], + "session_excluded_from_identity": report["session_boundary_readback"][ + "identity_runtime_unchanged_after_session" + ], + "drift_failed_closed": drift["fail_closed"] and not drift["answer_returned"], + "drift_profile_started_without_sessions": report["drift_compile"]["session_directories_started_empty"], + "drift_process_stopped": drift["process_group_absent_after_wait"], + "drift_process_had_no_orphan_cleanup": drift["orphan_cleanup_required"] is False, + "restart_process_stopped": second["process_group_absent_after_wait"], + } + report["status"] = "pass" if all(report["gates"].values()) else "fail" + if report["status"] == "pass": + exit_code = 0 + except (CanaryError, identity.IdentityManifestError, OSError, subprocess.SubprocessError) as exc: + report["status"] = "fail" + report["errors"].append({"type": type(exc).__name__, "message": str(exc)}) + finally: + shutil.rmtree(temporary_root, ignore_errors=True) + report["cleanup"] = { + "temporary_root_removed": not temporary_root.exists(), + "no_profile_retained": not temporary_root.exists(), + } + if not all(report["cleanup"].values()): + report["status"] = "fail" + exit_code = 1 + positive_restart_passed = report.get("status") == "pass" and not args.inject_drift_before_restart + report["current_tier"] = "T2_runtime" if positive_restart_passed else "T1_model" + report["goal_tier_satisfied"] = positive_restart_passed + report["runtime_component_proven"] = ( + "fresh_profile_recompile_plus_distinct_process_restart" + if positive_restart_passed + else "first_local_process_plus_pre_restart_drift_refusal" + ) + report["runtime_variant"] = "local_deterministic_identity_compiler_readback" + report["tier_basis"] = ( + "Capability Tier Proof defines T2_runtime as local API/runtime behavior with restart where relevant; " + "this is compiler-process runtime proof and explicitly excludes GatewayRunner and hosted-model proof." + if positive_restart_passed + else "This negative component does not complete the required restart path, so it remains below T2_runtime." + ) + report["claim_ceiling"] = ( + "Local disposable identity compilation, fresh-profile reconstruction, and cold-process restart only; " + "not a live GatewayRunner, hosted-model, Telegram, production database, VPS restart, or T3 proof." + if positive_restart_passed + else "Local first-process and pre-restart drift-refusal component only; no successful restart proof, " + "GatewayRunner, hosted model, production database, VPS, or T3 claim." + ) + stable = {key: value for key, value in report.items() if key != "receipt_sha256"} + report["receipt_sha256"] = behavior.canonical_sha256(stable) + identity.write_json(output, report) + return report, exit_code + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--profile-template", type=Path, required=True) + parser.add_argument("--hermes-root", type=Path, required=True) + parser.add_argument("--deployment-root", type=Path, required=True) + parser.add_argument("--source-root", type=Path, required=True) + parser.add_argument("--database-fingerprint", type=Path, required=True) + parser.add_argument("--constitution", type=Path, required=True) + parser.add_argument("--database-identity", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--inject-drift-before-restart", action="store_true") + args = parser.parse_args() + report, exit_code = run_canary(args) + print( + json.dumps( + { + "status": report.get("status"), + "current_tier": report.get("current_tier"), + "receipt_sha256": report.get("receipt_sha256"), + "output": str(args.output), + }, + sort_keys=True, + ) + ) + return exit_code + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_leo_behavior_manifest.py b/tests/test_leo_behavior_manifest.py index 5b87992..f8959c2 100644 --- a/tests/test_leo_behavior_manifest.py +++ b/tests/test_leo_behavior_manifest.py @@ -4,6 +4,8 @@ import json import subprocess from pathlib import Path +import pytest + from scripts import leo_behavior_manifest as manifest ROOT = Path(__file__).resolve().parents[1] @@ -58,6 +60,11 @@ gateway: assert '{"private":"conversation"}' not in encoded +def test_identity_config_rejects_nonboolean_smart_model_routing() -> None: + with pytest.raises(ValueError, match="smart_model_routing must be boolean"): + manifest.identity_config_projection({"smart_model_routing": {"route": "untrusted"}}) + + def test_behavior_manifest_changes_when_a_behavior_layer_changes(tmp_path: Path) -> None: profile = tmp_path / "profile" hermes = tmp_path / "hermes" @@ -156,3 +163,64 @@ def test_git_state_marks_untracked_files_dirty(tmp_path: Path) -> None: assert state["worktree_clean"] is False assert len(str(state["status_sha256"])) == 64 + + +def test_identity_runtime_omits_secret_values_but_pins_semantic_model_settings(tmp_path: Path) -> None: + profile = tmp_path / "profile" + hermes = tmp_path / "hermes" + deployment = tmp_path / "deployment" + config = """model: + provider: fixture + default: identity-v1 + max_tokens: 512 +gateway: + telegram: + bot_token: token-a + botToken: camel-token-a +provider: + apiKey: camel-api-key-a +transport: + endpoint: https://fixture-user:fixture-password-a@example.invalid/v1 + headers: ['Authorization: Bearer fixture-header-a'] +tools: + allowed: [identity_readback] +identity_runtime: + network_send_enabled: false + database_write_enabled: false + allowed_tools: [identity_readback] +""" + _write(profile / "config.yaml", config) + _write(profile / "skills" / "identity" / "SKILL.md", "identity\n") + _write(profile / "plugins" / "identity" / "__init__.py", "BOUNDARY = True\n") + _write(profile / "bin" / "identity.py", "READ_ONLY = True\n") + _write(hermes / "run_agent.py", "runtime\n") + _write(hermes / "agent" / "prompt_builder.py", "prompts\n") + for name in ( + "leo_behavior_manifest.py", + "leo_identity_manifest.py", + "leo_identity_profile.py", + "run_leo_identity_reconstruction_canary.py", + ): + _write(deployment / "scripts" / name, f"# {name}\n") + + before = manifest.build_manifest(profile, hermes, deployment) + _write( + profile / "config.yaml", + config.replace("token-a", "token-b") + .replace("camel-api-key-a", "camel-api-key-b") + .replace("fixture-password-a", "fixture-password-b") + .replace("fixture-header-a", "fixture-header-b"), + ) + secret_rotated = manifest.build_manifest(profile, hermes, deployment) + _write(profile / "config.yaml", config.replace("max_tokens: 512", "max_tokens: 1024")) + model_changed = manifest.build_manifest(profile, hermes, deployment) + + assert before["behavior_sha256"] != secret_rotated["behavior_sha256"] + assert before["identity_runtime_sha256"] == secret_rotated["identity_runtime_sha256"] + assert before["identity_runtime_sha256"] != model_changed["identity_runtime_sha256"] + encoded = json.dumps(before) + assert "token-a" not in encoded + assert "camel-token-a" not in encoded + assert "camel-api-key-a" not in encoded + assert "fixture-password-a" not in encoded + assert "fixture-header-a" not in encoded diff --git a/tests/test_leo_identity_manifest.py b/tests/test_leo_identity_manifest.py new file mode 100644 index 0000000..35dd4d6 --- /dev/null +++ b/tests/test_leo_identity_manifest.py @@ -0,0 +1,721 @@ +from __future__ import annotations + +import argparse +import copy +import json +import os +import subprocess +import time +from pathlib import Path + +import pytest + +from scripts import leo_behavior_manifest as behavior +from scripts import leo_identity_manifest as identity +from scripts import leo_identity_profile as profile_runtime +from scripts import run_leo_identity_reconstruction_canary as canary + +ROOT = Path(__file__).resolve().parents[1] + + +def _write(path: Path, value: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(value, encoding="utf-8") + + +def _write_json(path: Path, value: dict) -> None: + _write(path, json.dumps(value, indent=2, sort_keys=True) + "\n") + + +def _fixture(tmp_path: Path) -> dict[str, Path | dict]: + repo = tmp_path / "repo" + profile = repo / "profile-template" + hermes = repo / "hermes-runtime" + compiled = tmp_path / "compiled-profile" + _write( + profile / "config.yaml", + """model: + provider: fixture-local + default: identity-v1 + max_tokens: 512 +memory: + enabled: false +gateway: + execution_mode: local_no_send + telegram: + bot_token: never-emit-this-fixture-value +transport: + endpoint: https://fixture-user:fixture-password@example.invalid/v1 + headers: ['Authorization: Bearer fixture-secret'] +tools: + allowed: [identity_readback] + write_enabled: false +identity_runtime: + network_send_enabled: false + database_write_enabled: false + allowed_tools: [identity_readback] +""", + ) + _write(profile / "skills" / "identity" / "SKILL.md", "# identity readback\n") + _write(profile / "plugins" / "identity" / "__init__.py", "BOUNDARY = True\n") + _write(profile / "bin" / "identity_readback.py", "READ_ONLY = True\n") + _write(hermes / "run_agent.py", "RUNTIME = 'identity-v1'\n") + _write(hermes / "agent" / "prompt_builder.py", "SOUL_IS_GENERATED = True\n") + _write(hermes / "gateway" / "run.py", "SESSION_IS_IDENTITY = False\n") + _write(hermes / "hermes_cli" / "tools_config.py", "TOOLS = ('identity_readback',)\n") + _write(hermes / "tools" / "registry.py", "READ_ONLY = True\n") + for name in ( + "leo_behavior_manifest.py", + "leo_identity_manifest.py", + "leo_identity_profile.py", + "run_leo_identity_reconstruction_canary.py", + ): + _write(repo / "scripts" / name, (ROOT / "scripts" / name).read_text(encoding="utf-8")) + + fingerprint_path = repo / "fixtures" / "database-fingerprint.json" + constitution_path = repo / "fixtures" / "constitution.json" + database_identity_path = repo / "fixtures" / "database-identity.json" + fingerprint = { + "schema": identity.DATABASE_FINGERPRINT_SCHEMA, + "status": "ok", + "environment": "disposable_fixture", + "fingerprint_sha256": "1" * 64, + "table_rows_sha256": "2" * 64, + "structure_sha256": "3" * 64, + "table_count": 2, + "total_rows": 2, + "read_consistency": { + "status": "stable_wal_marker", + "before": { + "database": "fixture", + "database_user": "reader", + "system_identifier": "12345", + "wal_lsn": "0/1", + }, + "after": { + "database": "fixture", + "database_user": "reader", + "system_identifier": "12345", + "wal_lsn": "0/1", + }, + }, + } + constitution = { + "schema": identity.CONSTITUTION_SCHEMA, + "identity_name": "Leo", + "rules": [ + {"id": "evidence", "text": "Never use temporary session memory as canonical evidence."}, + {"id": "truth", "text": "Fail closed when a pinned identity input drifts."}, + ], + } + database_identity = { + "schema": identity.DATABASE_IDENTITY_SCHEMA, + "identity_name": "Leo", + "database_fingerprint_sha256": "1" * 64, + "source_provenance": { + "mode": identity.SYNTHETIC_DATABASE_MODE, + "canonical_authority_granted": False, + "same_transaction_as_fingerprint": False, + "export_receipt_sha256": None, + }, + "records": [ + { + "table": "public.personas", + "row_id": "persona-1", + "kind": "persona", + "rank": 0, + "text": "Leo is an evidence-bound reasoning interface.", + "status": "active", + "evidence_sha256": "a" * 64, + }, + { + "table": "public.beliefs", + "row_id": "belief-1", + "kind": "belief", + "rank": 0, + "text": "Exact provenance is stronger than plausible recollection.", + "status": "active", + "evidence_sha256": "b" * 64, + }, + ], + } + _write_json(fingerprint_path, fingerprint) + _write_json(constitution_path, constitution) + _write_json(database_identity_path, database_identity) + + subprocess.run(["git", "init", "-q", str(repo)], check=True) + subprocess.run(["git", "-C", str(repo), "config", "user.email", "test@example.com"], check=True) + subprocess.run(["git", "-C", str(repo), "config", "user.name", "Test"], check=True) + subprocess.run(["git", "-C", str(repo), "add", "."], check=True) + subprocess.run( + ["git", "-C", str(repo), "commit", "-qm", "fixture"], + check=True, + env={ + **dict(__import__("os").environ), + "GIT_AUTHOR_DATE": "2026-01-01T00:00:00Z", + "GIT_COMMITTER_DATE": "2026-01-01T00:00:00Z", + }, + ) + behavior_manifest = behavior.build_manifest(profile, hermes, repo) + manifest = identity.build_identity_manifest( + behavior_manifest=behavior_manifest, + database_fingerprint_path=fingerprint_path, + constitution_path=constitution_path, + database_identity_path=database_identity_path, + source_root=repo, + ) + return { + "repo": repo, + "profile": profile, + "hermes": hermes, + "compiled": compiled, + "fingerprint": fingerprint_path, + "constitution": constitution_path, + "database_identity": database_identity_path, + "behavior": behavior_manifest, + "manifest": manifest, + } + + +def _fully_rehash_manifest(fixture: dict[str, Path | dict], manifest: dict) -> None: + identity_hash = behavior.canonical_sha256(manifest["identity_inputs"]) + manifest["identity_inputs_sha256"] = identity_hash + fingerprint = identity.load_json(fixture["fingerprint"], "database fingerprint") + rules = identity.canonical_rules(identity.load_json(fixture["constitution"], "constitution")) + records = identity.canonical_database_records( + identity.load_json(fixture["database_identity"], "database identity"), + fingerprint_sha256=fingerprint["fingerprint_sha256"], + ) + database_provenance = identity.database_identity_provenance( + identity.load_json(fixture["database_identity"], "database identity"), + fingerprint=fingerprint, + ) + rendered = identity.render_identity_views( + identity_inputs_sha256=identity_hash, + database_fingerprint_sha256=fingerprint["fingerprint_sha256"], + database_provenance=database_provenance, + rules=rules, + records=records, + ) + for name, content in rendered.items(): + manifest["compiled_views"][name]["sha256"] = behavior.sha256_bytes(content.encode("utf-8")) + manifest["compiled_views"][name]["generated_from_identity_inputs_sha256"] = identity_hash + stable = {key: value for key, value in manifest.items() if key != "manifest_sha256"} + manifest["manifest_sha256"] = behavior.canonical_sha256(stable) + + +def _rehash_behavior_manifest(manifest: dict) -> None: + manifest["identity_runtime_sha256"] = behavior.canonical_sha256(behavior.identity_runtime_payload(manifest)) + manifest["static_runtime_sha256"] = behavior.canonical_sha256(behavior.static_runtime_payload(manifest)) + manifest["behavior_sha256"] = behavior.canonical_sha256(behavior.stable_manifest_payload(manifest)) + + +def _canary_args(fixture: dict[str, Path | dict], output: Path, *, inject_drift: bool = False) -> argparse.Namespace: + return argparse.Namespace( + profile_template=fixture["profile"], + hermes_root=fixture["hermes"], + deployment_root=fixture["repo"], + source_root=fixture["repo"], + database_fingerprint=fixture["fingerprint"], + constitution=fixture["constitution"], + database_identity=fixture["database_identity"], + output=output, + inject_drift_before_restart=inject_drift, + ) + + +def test_manifest_compiles_generated_views_and_reproduces_identity_across_queries(tmp_path: Path) -> None: + fixture = _fixture(tmp_path) + manifest = fixture["manifest"] + assert isinstance(manifest, dict) + rebuilt = identity.build_identity_manifest( + behavior_manifest=fixture["behavior"], + database_fingerprint_path=fixture["fingerprint"], + constitution_path=fixture["constitution"], + database_identity_path=fixture["database_identity"], + source_root=fixture["repo"], + ) + assert rebuilt == manifest + + compile_receipt = profile_runtime.compile_profile( + manifest, + source_root=fixture["repo"], + profile_template=fixture["profile"], + profile=fixture["compiled"], + hermes_root=fixture["hermes"], + deployment_root=fixture["repo"], + ) + first = profile_runtime.query_profile( + fixture["compiled"], + query=profile_runtime.FIXED_QUERY, + source_root=fixture["repo"], + hermes_root=fixture["hermes"], + deployment_root=fixture["repo"], + ) + second = profile_runtime.query_profile( + fixture["compiled"], + query=profile_runtime.FIXED_QUERY, + source_root=fixture["repo"], + hermes_root=fixture["hermes"], + deployment_root=fixture["repo"], + ) + + assert compile_receipt["status"] == "pass" + assert "never-emit-this-fixture-value" not in (fixture["compiled"] / "config.yaml").read_text(encoding="utf-8") + assert "fixture-password" not in (fixture["compiled"] / "config.yaml").read_text(encoding="utf-8") + assert "fixture-secret" not in (fixture["compiled"] / "config.yaml").read_text(encoding="utf-8") + assert (fixture["compiled"] / "SOUL.md").read_text(encoding="utf-8").startswith("