#!/usr/bin/env python3 """Build source-specific canonical claim context from a Postgres dump without a database write.""" from __future__ import annotations import argparse import codecs import hashlib import importlib.util import json import os import re import shutil import subprocess import tempfile from collections import Counter from decimal import Decimal, InvalidOperation from pathlib import Path from types import ModuleType from typing import Any SCHEMA = "livingip.kbSourceDumpContextReceipt.v1" STATUS_SCHEMA = "livingip.kbSourceDumpContextStatus.v1" DEFAULT_DOCKER_IMAGE = "postgres:16-alpine" DEFAULT_LIMIT = 12 DEFAULT_COPY_ENCODING = "utf-8" MAX_PREVIEW_BYTES = 16_000 TABLES = ("claims", "claim_evidence", "claim_edges") class ContextError(RuntimeError): """Raised when a dump cannot safely produce canonical retrieval context.""" def sha256_bytes(value: bytes) -> str: return hashlib.sha256(value).hexdigest() def _read_file(path: Path, label: str) -> bytes: try: if not path.is_file(): raise ContextError(f"{label} is not a regular file: {path}") return path.read_bytes() except OSError as exc: raise ContextError(f"could not read {label}: {exc}") from exc def _load_kb_tool(repo_root: Path) -> ModuleType: path = repo_root / "hermes-agent" / "leoclean-bin" / "kb_tool.py" spec = importlib.util.spec_from_file_location("teleo_kb_tool_context", path) if spec is None or spec.loader is None: raise ContextError(f"could not load retrieval semantics from {path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def _copy_encoding(value: str) -> str: try: return codecs.lookup(value).name except LookupError as exc: raise ContextError("COPY encoding is not supported") from exc def _decode_copy_field(value: str, *, encoding: str = DEFAULT_COPY_ENCODING) -> str | None: if value == r"\N": return None encoding = _copy_encoding(encoding) result = bytearray() index = 0 escapes = {"b": 8, "f": 12, "n": 10, "r": 13, "t": 9, "v": 11, "\\": 92} def append_text(text: str) -> None: try: result.extend(text.encode(encoding, errors="strict")) except UnicodeEncodeError as exc: raise ContextError(f"COPY field cannot be encoded as {encoding}") from exc while index < len(value): char = value[index] if char != "\\": append_text(char) index += 1 continue if index + 1 >= len(value): raise ContextError("COPY field contains a dangling backslash escape") index += 1 escaped = value[index] if escaped in escapes: result.append(escapes[escaped]) index += 1 continue if escaped == "x": match = re.match(r"[0-9A-Fa-f]{1,2}", value[index + 1 :]) if match is not None: result.append(int(match.group(0), 16)) index += 1 + len(match.group(0)) continue if escaped in "01234567": match = re.match(r"[0-7]{1,3}", value[index:]) if match is None: raise ContextError("COPY field contains a malformed octal byte escape") byte_value = int(match.group(0), 8) if byte_value > 0xFF: raise ContextError("COPY field contains an out-of-range octal byte escape") result.append(byte_value) index += len(match.group(0)) continue append_text(escaped) index += 1 if 0 in result: raise ContextError("COPY field contains a zero byte") try: return result.decode(encoding, errors="strict") except UnicodeDecodeError as exc: raise ContextError(f"COPY byte escapes do not form valid {encoding} text") from exc def parse_copy_table( sql: str, table: str, *, encoding: str = DEFAULT_COPY_ENCODING ) -> list[dict[str, str | None]]: encoding = _copy_encoding(encoding) prefix = f"COPY public.{table} (" lines = iter(line[:-1] if line.endswith("\r") else line for line in sql.split("\n")) for line in lines: if line.startswith(prefix) and line.endswith(") FROM stdin;"): columns = [item.strip() for item in line[len(prefix) : -len(") FROM stdin;")].split(",")] break else: raise ContextError(f"pg_restore output has no COPY block for public.{table}") rows: list[dict[str, str | None]] = [] for line in lines: if line == r"\.": return rows fields = line.split("\t") if len(fields) != len(columns): raise ContextError(f"public.{table} COPY row has {len(fields)} fields; expected {len(columns)}") rows.append( dict(zip(columns, (_decode_copy_field(field, encoding=encoding) for field in fields), strict=True)) ) raise ContextError(f"public.{table} COPY block is unterminated") def restore_table( dump_path: Path, table: str, docker_image: str, *, encoding: str = DEFAULT_COPY_ENCODING ) -> str: if table not in TABLES: raise ContextError(f"unsupported canonical table: {table}") if shutil.which("docker") is None: raise ContextError("docker is not installed or not available on PATH") command = [ "docker", "run", "--rm", "--network=none", "--mount", f"type=bind,src={dump_path.resolve()},dst=/canonical.dump,readonly", docker_image, "pg_restore", "--data-only", "-t", table, "-f", "-", "/canonical.dump", ] encoding = _copy_encoding(encoding) try: completed = subprocess.run( command, capture_output=True, text=True, encoding=encoding, errors="strict", timeout=180, check=False, ) except (OSError, subprocess.TimeoutExpired, UnicodeError) as exc: raise ContextError(f"pg_restore failed ({type(exc).__name__}); diagnostic detail redacted") from exc if completed.returncode != 0: raise ContextError(f"pg_restore returned exit {completed.returncode}; diagnostic detail redacted") return completed.stdout def _decimal(value: str | None) -> Decimal: if value is None: return Decimal(0) try: return Decimal(value) except InvalidOperation as exc: raise ContextError("canonical claim confidence is not numeric") from exc def build_context( *, dump_path: Path, artifact_path: Path, title: str, limit: int, docker_image: str, repo_root: Path, copy_encoding: str = DEFAULT_COPY_ENCODING, ) -> tuple[dict[str, Any], dict[str, Any]]: if limit < 1 or limit > 20: raise ContextError("limit must be between 1 and 20") dump_bytes = _read_file(dump_path, "canonical dump") artifact_bytes = _read_file(artifact_path, "source artifact") try: preview = artifact_bytes[:MAX_PREVIEW_BYTES].decode("utf-8", errors="strict") except UnicodeDecodeError as exc: raise ContextError(f"source artifact preview must be strict UTF-8: {exc}") from exc kb_tool = _load_kb_tool(repo_root) query = " ".join(kb_tool.query_terms(f"{title} {preview}")) terms = kb_tool.query_terms(query) min_score = 2 if len(terms) >= 3 else 1 copy_encoding = _copy_encoding(copy_encoding) tables = { table: parse_copy_table( restore_table(dump_path, table, docker_image, encoding=copy_encoding), table, encoding=copy_encoding, ) for table in TABLES } evidence_counts = Counter(row["claim_id"] for row in tables["claim_evidence"] if row["claim_id"]) edge_counts: Counter[str] = Counter() for row in tables["claim_edges"]: if row["from_claim"]: edge_counts[row["from_claim"]] += 1 if row["to_claim"]: edge_counts[row["to_claim"]] += 1 ranked: list[dict[str, Any]] = [] for row in tables["claims"]: if row["status"] != "open" or not row["id"] or not row["text"]: continue text = row["text"] searchable = f"{text}\n{row['tags'] or ''}".lower() score = sum(term in searchable for term in terms) if score < min_score: continue ranked.append( { "id": row["id"], "text": text, "score": score, "evidence_count": evidence_counts[row["id"]], "edge_count": edge_counts[row["id"]], "_confidence": _decimal(row["confidence"]), } ) ranked.sort( key=lambda row: ( -row["score"], -row["evidence_count"], -row["edge_count"], -row["_confidence"], len(row["text"]), row["text"], row["id"], ) ) candidates = [{key: value for key, value in row.items() if key != "_confidence"} for row in ranked[:limit]] context = {"database_search_query": query, "candidate_claims": candidates} receipt = { "schema": SCHEMA, "status": "prepared", "database_write_performed": False, "model_call_performed": False, "canonical_dump": { "path": str(dump_path.resolve()), "sha256": sha256_bytes(dump_bytes), }, "source": { "path": str(artifact_path.resolve()), "sha256": sha256_bytes(artifact_bytes), }, "retrieval": { "query_sha256": sha256_bytes(query.encode("utf-8")), "term_count": len(terms), "minimum_score": min_score, "canonical_open_claim_count": sum(row["status"] == "open" for row in tables["claims"]), "candidate_count": len(candidates), "candidate_ids": [row["id"] for row in candidates], }, "runtime": { "docker_image": docker_image, "network_disabled": True, "pg_restore_output_retained": False, "copy_encoding": copy_encoding, "copy_decode_errors": "strict", }, } return context, receipt def _stage_private_json(path: Path, value: Any) -> Path: path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(path.parent, 0o700) fd, raw_temp = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent) temp = Path(raw_temp) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(value, handle, indent=2, sort_keys=True, ensure_ascii=True) handle.write("\n") handle.flush() os.fsync(handle.fileno()) return temp except BaseException: if temp.exists(): temp.unlink() raise def _private_json(path: Path, value: Any) -> None: if path.exists(): raise ContextError(f"refusing to overwrite existing output: {path}") temp = _stage_private_json(path, value) try: os.replace(temp, path) os.chmod(path, 0o600) finally: if temp.exists(): temp.unlink() def _publish_private_json_outputs(outputs: tuple[tuple[Path, Any], ...]) -> None: for path, _value in outputs: if path.exists(): raise ContextError(f"refusing to overwrite existing output: {path}") staged: list[tuple[Path, Path]] = [] try: for path, value in outputs: staged.append((path, _stage_private_json(path, value))) for path, temp in staged: os.replace(temp, path) os.chmod(path, 0o600) finally: for _path, temp in staged: if temp.exists(): temp.unlink() def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--dump", required=True, type=Path) parser.add_argument("--artifact", required=True, type=Path) parser.add_argument("--title", required=True) parser.add_argument("--context-output", required=True, type=Path) parser.add_argument("--receipt-output", required=True, type=Path) parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT) parser.add_argument("--docker-image", default=DEFAULT_DOCKER_IMAGE) parser.add_argument("--copy-encoding", default=DEFAULT_COPY_ENCODING) return parser.parse_args(argv) def main(argv: list[str] | None = None) -> int: args = parse_args(argv) repo_root = Path(__file__).resolve().parents[1] context_existed = args.context_output.is_file() receipt_existed = args.receipt_output.is_file() try: if args.context_output.resolve() == args.receipt_output.resolve(): raise ContextError("context and receipt outputs must be different files") context, receipt = build_context( dump_path=args.dump, artifact_path=args.artifact, title=args.title, limit=args.limit, docker_image=args.docker_image, repo_root=repo_root, copy_encoding=args.copy_encoding, ) receipt["outputs"] = { "context": str(args.context_output.resolve()), "receipt": str(args.receipt_output.resolve()), } _publish_private_json_outputs( ((args.context_output, context), (args.receipt_output, receipt)) ) except (ContextError, OSError, ValueError): context_written = not context_existed and args.context_output.is_file() receipt_written = not receipt_existed and args.receipt_output.is_file() print( json.dumps( { "schema": STATUS_SCHEMA, "status": "rejected", "database_write_performed": False, "model_call_performed": False, "private_context_written": context_written, "private_receipt_written": receipt_written, "reason": "context_preparation_failed", }, sort_keys=True, ) ) return 2 print( json.dumps( { "schema": STATUS_SCHEMA, "status": "prepared", "database_write_performed": False, "model_call_performed": False, "private_context_written": True, "private_receipt_written": True, }, indent=2, sort_keys=True, ) ) return 0 if __name__ == "__main__": raise SystemExit(main())