diff --git a/docs/kb-rebuild-and-recompile.md b/docs/kb-rebuild-and-recompile.md index 2baf932..b21eb56 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,67 @@ 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`. + 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 + 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 +312,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/docs/leo-reproducible-identity.md b/docs/leo-reproducible-identity.md new file mode 100644 index 0000000..74b31f4 --- /dev/null +++ b/docs/leo-reproducible-identity.md @@ -0,0 +1,212 @@ +# 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. + +Child processes execute the real interpreter and temporary-profile paths only +in process-local memory. Retained receipts persist a repo-relative +`logical_command`, the stable `` and `` placeholders, +and an explicit `profile_role`; they never serialize the checkout, interpreter, +or temporary directory path. + +## 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/docs/reports/leo-working-state-20260709/leo-reproducible-identity-drift-negative-current.json b/docs/reports/leo-working-state-20260709/leo-reproducible-identity-drift-negative-current.json new file mode 100644 index 0000000..f1c74aa --- /dev/null +++ b/docs/reports/leo-working-state-20260709/leo-reproducible-identity-drift-negative-current.json @@ -0,0 +1,332 @@ +{ + "actual_hosted_model_call_performed": false, + "behavior_observation_before_compile": { + "behavior_sha256": "693a5d542e3d817c1c77c8aa3180f14d5f98ec1e71f5e4313685b4da70b489aa", + "identity_runtime_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "source_commit": "b238fd6fc2636ed3582e14900774014c52732698" + }, + "claim_ceiling": "Local first-process and pre-restart drift-refusal component only; no successful restart proof, GatewayRunner, hosted model, production database, VPS, or T3 claim.", + "cleanup": { + "no_profile_retained": true, + "temporary_root_removed": true + }, + "compile": { + "compiled_view_sha256": { + "SOUL.md": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a", + "identity.json": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6" + }, + "generated_views_are_authority": false, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "nonauthoritative_inputs_omitted": { + ".cursorrules": true, + ".hermes.md": true, + ".skills_prompt_snapshot.json": true, + "AGENTS.md": true, + "CLAUDE.md": true, + "HERMES.md": true, + "MEMORY.md": true, + "USER.md": true, + "memories": true, + "platforms/pairing": true + }, + "profile_static_entries": [ + "config.yaml", + "skills", + "plugins", + "bin" + ], + "runtime_identity_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "schema": "livingip.leoIdentityCompileReceipt.v1", + "session_directories_started_empty": true, + "status": "pass" + }, + "compiled_profile_before_query": { + "behavior_sha256": "a62b26015a7c9506f0296404be150608a816a2d545f471f7960bd27324d6849c", + "identity_runtime_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159" + }, + "current_tier": "T1_model", + "drift_target": "compiled SOUL.md generated view", + "errors": [], + "first_start": { + "actual_argv_persisted": false, + "command_serialization": "logical_redacted_v1", + "launcher_pid": 80040, + "logical_command": [ + "", + "-m", + "scripts.leo_identity_profile", + "query", + "--profile", + "", + "--source-root", + ".", + "--hermes-root", + "fixtures/working-leo/leo-identity-v1/hermes-runtime", + "--deployment-root", + ".", + "--query", + "What defines Leo's identity, and what is temporary?" + ], + "orphan_cleanup_required": false, + "process_group_absent_after_wait": true, + "profile_role": "first_start", + "returncode": 0, + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout": { + "answer": "Leo is defined by 3 pinned static constitutional rules and 5 active database-derived identity rows from a synthetic noncanonical fixture at fingerprint 1111111111111111111111111111111111111111111111111111111111111111. SOUL.md is a generated view; runtime/session memory is temporary, noncanonical, and cannot be evidence.", + "answer_sha256": "e0a21e1fc5357a2f358b4e96c52e971f3addee2468e44748d898adfd62dafa25", + "authorization": { + "authorization_decision_observed": false, + "claim": "local_policy_binding_not_an_authorizer_readback", + "declared_runtime_policy": { + "allowed_tools": [ + "identity_readback" + ], + "database_write_enabled": false, + "network_send_enabled": false + } + }, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "output_provenance": { + "actual_model_call_performed": false, + "compiled_identity_sha256": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6", + "compiled_soul_sha256": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a", + "database_authority": "synthetic_noncanonical_fixture", + "database_canonical_authority_granted": false, + "database_fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "database_identity_sha256": "70aaa7287cc41b52a3c902cf2e1b90fa4d84a5ee0a61e10c642f556fcc9304c3", + "runtime_identity_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "static_constitution_sha256": "b576d5cee08530a95e0b196a3296f4743b70d94bcadffaa36544dd6bf1387f8a" + }, + "process_id": 80040, + "query": "What defines Leo's identity, and what is temporary?", + "query_sha256": "13704c2c9ae999e3f9b3f1c9c5572d062cd7fe49903c2317fa85aaa39490af66", + "runtime_instance_id": "493f274a5f0340a2aa675f13b8e5b4af", + "schema": "livingip.leoIdentityQueryReceipt.v1", + "session": { + "classification": "temporary_noncanonical", + "included_in_output_provenance": false, + "may_be_used_as_evidence": false, + "record_sha256": "56b6a2ea25657a87dc90dc19f49bece0ac62241f5814732ed80ef4f34d426d84" + }, + "started_at_utc": "2026-07-15T02:46:34.316819+00:00", + "status": "pass" + }, + "terminated_with": null, + "timed_out": false + }, + "fixed_query": "What defines Leo's identity, and what is temporary?", + "fixed_query_sha256": "13704c2c9ae999e3f9b3f1c9c5572d062cd7fe49903c2317fa85aaa39490af66", + "gates": { + "drift_detected_before_restart": true, + "drift_process_had_no_orphan_cleanup": true, + "drift_process_stopped": true, + "drift_returned_no_answer": true, + "first_process_stopped": true, + "first_start_passed": true, + "second_start_not_attempted": true + }, + "generated_at_utc": "2026-07-15T02:46:31.831631+00:00", + "goal_tier_satisfied": false, + "manifest": { + "compiled_views": { + "SOUL.md": { + "generated_from_identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "role": "generated_view_not_authority", + "sha256": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a" + }, + "identity.json": { + "generated_from_identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "role": "generated_view_not_authority", + "sha256": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6" + } + }, + "identity_inputs": { + "canonical_database": { + "authority": "synthetic_noncanonical_fixture", + "canonical_authority_granted": false, + "database": "leo_identity_fixture", + "database_user": "leo_identity_reader", + "fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "source": { + "record_count": 7, + "repo_path": "fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json", + "semantic_sha256": "c5bbf346613270a5e3b17f604f69306ed42f545d4f227d784ec0dcb3d675dcb2" + }, + "structure_sha256": "3333333333333333333333333333333333333333333333333333333333333333", + "system_identifier": "7649789040005668902", + "table_count": 7, + "table_rows_sha256": "2222222222222222222222222222222222222222222222222222222222222222", + "total_rows": 7 + }, + "gatewayrunner_execution_contract": { + "fixed_message_context": { + "auto_skill": null, + "media_or_file_context": null, + "reply_context": null + }, + "profile_surfaces": [ + "config.yaml", + "generated SOUL.md", + "skills", + "plugins", + "bin tools" + ], + "required_absent_or_empty": { + "generated_skill_prompt_cache": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "pairing_store": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "persistent_memory": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "prior_sessions_at_first_start": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "project_context": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945" + }, + "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" + ] + }, + "identity_name": "Leo", + "identity_sources": { + "database_derived_identity": { + "authority": "synthetic_noncanonical_fixture", + "content_sha256": "9de2dfa37529b23ba277348f3d910e967551e490a9b4d2be637bf45bb2aa7dde", + "database_fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "provenance": { + "authority": "synthetic_noncanonical_fixture", + "canonical_authority_granted": false, + "export_receipt_sha256": null, + "mode": "synthetic_noncanonical_fixture", + "same_transaction_as_fingerprint": false + }, + "record_count": 5, + "repo_path": "fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json", + "semantic_sha256": "70aaa7287cc41b52a3c902cf2e1b90fa4d84a5ee0a61e10c642f556fcc9304c3" + }, + "static_constitution": { + "authority": "pinned_static_source", + "content_sha256": "b5477e778a2be0abba783390dac2e04ed199b8ce5679fc324270f808d46543f1", + "record_count": 3, + "repo_path": "fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json", + "semantic_sha256": "b576d5cee08530a95e0b196a3296f4743b70d94bcadffaa36544dd6bf1387f8a" + } + }, + "model": { + "actual_model_required_in_turn_receipt": true, + "default_model": "leo-identity-readback-v1", + "gateway_smart_model_routing": null, + "hosted_weights": "external_provider_managed_not_locally_hashable", + "model_section_sha256": "90b3943ace9ff83b1711002fda8dc9736448e8d3c1c58a17ac90608f4ce58756", + "provider": "fixture-local", + "smart_routing": false, + "smart_routing_model": null + }, + "permissions": { + "authorization_decision_required_in_runtime_receipt": true, + "gateway_permissions_sha256": "eac17ada1d02b5413183587d12fc265538479de00709ed8c275e9045cd720290", + "identity_config_sha256": "e6df9b65bab148ea8502555bbc260df66b4785e7dd527cfd15d2f0a48c2e8b3e", + "runtime_policy": { + "allowed_tools": [ + "identity_readback" + ], + "database_write_enabled": false, + "network_send_enabled": false + }, + "tool_permissions_sha256": "9bb2cec2f65d43efb597a72bdced44076c25aac292f2fccbc4c448b6e7fd3e16" + }, + "runtime": { + "hermes_git_head": "b238fd6fc2636ed3582e14900774014c52732698", + "hermes_source_sha256": "dcf8109051e0b69cafe2e8dd328ff501211b62ea19725416c3781bbdd594d028", + "identity_runtime_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "python": { + "abi_tag": "cpython-311", + "implementation": "CPython", + "python_version": "3.11.15", + "runtime_distributions": { + "PyYAML": "6.0.3" + }, + "runtime_distributions_sha256": "57a91bb880a01800903c1604b6eec1419514864ebd6a0644121920da15a8f165" + }, + "teleo_git_head": "b238fd6fc2636ed3582e14900774014c52732698", + "teleo_source_sha256": "2bd2e659eafb0ac0823f7f51c4296b2eb500b6f74469b3ac5f5287a5d4810aea" + }, + "session_boundary": { + "classification": "temporary_noncanonical", + "included_in_identity_inputs": false, + "may_be_used_as_evidence": false, + "restart_policy": "fresh_process_with_session_state_outside_identity_authority" + }, + "skills": { + "content_sha256": "d3f449ddcd7c88238dc88c6233cf2130875f51bd2e0911b2ce477b7c602ae90f", + "file_count": 1 + }, + "source_commit": "b238fd6fc2636ed3582e14900774014c52732698" + }, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "schema": "livingip.leoIdentityManifest.v1" + }, + "mode": "drift_before_restart", + "pre_restart_drift": { + "actual_argv_persisted": false, + "command_serialization": "logical_redacted_v1", + "launcher_pid": 80246, + "logical_command": [ + "", + "-m", + "scripts.leo_identity_profile", + "query", + "--profile", + "", + "--source-root", + ".", + "--hermes-root", + "fixtures/working-leo/leo-identity-v1/hermes-runtime", + "--deployment-root", + ".", + "--query", + "What defines Leo's identity, and what is temporary?" + ], + "orphan_cleanup_required": false, + "process_group_absent_after_wait": true, + "profile_role": "pre_restart_drift", + "returncode": 3, + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout": { + "error": "identity_drift", + "message": "compiled view drift detected: SOUL.md", + "status": "error" + }, + "terminated_with": null, + "timed_out": false + }, + "production_database_contacted": false, + "production_profile_replaced": false, + "receipt_sha256": "50278cc740d0427316f73a78abad840b3e3b128a516f464001d42e632e19c2a8", + "required_tier": "T2_runtime", + "runtime_component_proven": "first_local_process_plus_pre_restart_drift_refusal", + "runtime_variant": "local_deterministic_identity_compiler_readback", + "schema": "livingip.leoIdentityReconstructionReceipt.v1", + "second_start_attempted": false, + "session_boundary_readback": { + "full_behavior_changed_after_session": true, + "identity_runtime_unchanged_after_session": true, + "session_classification": "temporary_noncanonical", + "session_file_count": 1, + "session_used_as_output_provenance": false + }, + "status": "pass", + "telegram_message_sent": false, + "tier_basis": "This negative component does not complete the required restart path, so it remains below T2_runtime." +} diff --git a/docs/reports/leo-working-state-20260709/leo-reproducible-identity-t2-current.json b/docs/reports/leo-working-state-20260709/leo-reproducible-identity-t2-current.json new file mode 100644 index 0000000..af00b9b --- /dev/null +++ b/docs/reports/leo-working-state-20260709/leo-reproducible-identity-t2-current.json @@ -0,0 +1,478 @@ +{ + "actual_hosted_model_call_performed": false, + "behavior_observation_before_compile": { + "behavior_sha256": "693a5d542e3d817c1c77c8aa3180f14d5f98ec1e71f5e4313685b4da70b489aa", + "identity_runtime_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "source_commit": "b238fd6fc2636ed3582e14900774014c52732698" + }, + "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.", + "cleanup": { + "no_profile_retained": true, + "temporary_root_removed": true + }, + "compile": { + "compiled_view_sha256": { + "SOUL.md": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a", + "identity.json": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6" + }, + "generated_views_are_authority": false, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "nonauthoritative_inputs_omitted": { + ".cursorrules": true, + ".hermes.md": true, + ".skills_prompt_snapshot.json": true, + "AGENTS.md": true, + "CLAUDE.md": true, + "HERMES.md": true, + "MEMORY.md": true, + "USER.md": true, + "memories": true, + "platforms/pairing": true + }, + "profile_static_entries": [ + "config.yaml", + "skills", + "plugins", + "bin" + ], + "runtime_identity_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "schema": "livingip.leoIdentityCompileReceipt.v1", + "session_directories_started_empty": true, + "status": "pass" + }, + "compiled_profile_before_query": { + "behavior_sha256": "a62b26015a7c9506f0296404be150608a816a2d545f471f7960bd27324d6849c", + "identity_runtime_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159" + }, + "current_tier": "T2_runtime", + "drift_compile": { + "compiled_view_sha256": { + "SOUL.md": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a", + "identity.json": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6" + }, + "generated_views_are_authority": false, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "nonauthoritative_inputs_omitted": { + ".cursorrules": true, + ".hermes.md": true, + ".skills_prompt_snapshot.json": true, + "AGENTS.md": true, + "CLAUDE.md": true, + "HERMES.md": true, + "MEMORY.md": true, + "USER.md": true, + "memories": true, + "platforms/pairing": true + }, + "profile_static_entries": [ + "config.yaml", + "skills", + "plugins", + "bin" + ], + "runtime_identity_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "schema": "livingip.leoIdentityCompileReceipt.v1", + "session_directories_started_empty": true, + "status": "pass" + }, + "drift_negative": { + "actual_argv_persisted": false, + "answer_returned": false, + "command_serialization": "logical_redacted_v1", + "fail_closed": true, + "launcher_pid": 80851, + "logical_command": [ + "", + "-m", + "scripts.leo_identity_profile", + "query", + "--profile", + "", + "--source-root", + ".", + "--hermes-root", + "fixtures/working-leo/leo-identity-v1/hermes-runtime", + "--deployment-root", + ".", + "--query", + "What defines Leo's identity, and what is temporary?" + ], + "orphan_cleanup_required": false, + "process_group_absent_after_wait": true, + "profile_role": "drift_negative", + "returncode": 3, + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout": { + "error": "identity_drift", + "message": "compiled view drift detected: SOUL.md", + "status": "error" + }, + "terminated_with": null, + "timed_out": false + }, + "drift_target": "compiled SOUL.md generated view", + "errors": [], + "first_start": { + "actual_argv_persisted": false, + "command_serialization": "logical_redacted_v1", + "launcher_pid": 80054, + "logical_command": [ + "", + "-m", + "scripts.leo_identity_profile", + "query", + "--profile", + "", + "--source-root", + ".", + "--hermes-root", + "fixtures/working-leo/leo-identity-v1/hermes-runtime", + "--deployment-root", + ".", + "--query", + "What defines Leo's identity, and what is temporary?" + ], + "orphan_cleanup_required": false, + "process_group_absent_after_wait": true, + "profile_role": "first_start", + "returncode": 0, + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout": { + "answer": "Leo is defined by 3 pinned static constitutional rules and 5 active database-derived identity rows from a synthetic noncanonical fixture at fingerprint 1111111111111111111111111111111111111111111111111111111111111111. SOUL.md is a generated view; runtime/session memory is temporary, noncanonical, and cannot be evidence.", + "answer_sha256": "e0a21e1fc5357a2f358b4e96c52e971f3addee2468e44748d898adfd62dafa25", + "authorization": { + "authorization_decision_observed": false, + "claim": "local_policy_binding_not_an_authorizer_readback", + "declared_runtime_policy": { + "allowed_tools": [ + "identity_readback" + ], + "database_write_enabled": false, + "network_send_enabled": false + } + }, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "output_provenance": { + "actual_model_call_performed": false, + "compiled_identity_sha256": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6", + "compiled_soul_sha256": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a", + "database_authority": "synthetic_noncanonical_fixture", + "database_canonical_authority_granted": false, + "database_fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "database_identity_sha256": "70aaa7287cc41b52a3c902cf2e1b90fa4d84a5ee0a61e10c642f556fcc9304c3", + "runtime_identity_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "static_constitution_sha256": "b576d5cee08530a95e0b196a3296f4743b70d94bcadffaa36544dd6bf1387f8a" + }, + "process_id": 80054, + "query": "What defines Leo's identity, and what is temporary?", + "query_sha256": "13704c2c9ae999e3f9b3f1c9c5572d062cd7fe49903c2317fa85aaa39490af66", + "runtime_instance_id": "4bbcaeeeb5654b69adabed33b91cf3e4", + "schema": "livingip.leoIdentityQueryReceipt.v1", + "session": { + "classification": "temporary_noncanonical", + "included_in_output_provenance": false, + "may_be_used_as_evidence": false, + "record_sha256": "56b6a2ea25657a87dc90dc19f49bece0ac62241f5814732ed80ef4f34d426d84" + }, + "started_at_utc": "2026-07-15T02:46:34.391880+00:00", + "status": "pass" + }, + "terminated_with": null, + "timed_out": false + }, + "fixed_query": "What defines Leo's identity, and what is temporary?", + "fixed_query_sha256": "13704c2c9ae999e3f9b3f1c9c5572d062cd7fe49903c2317fa85aaa39490af66", + "gates": { + "answer_and_provenance_explainable": true, + "drift_failed_closed": true, + "drift_process_had_no_orphan_cleanup": true, + "drift_process_stopped": true, + "drift_profile_started_without_sessions": true, + "first_process_stopped": true, + "first_start_passed": true, + "identity_inputs_reproduced": true, + "manifest_generated": true, + "manifest_reproduced": true, + "pre_restart_verification_passed": true, + "query_reproduced": true, + "restart_passed": true, + "restart_process_is_distinct": true, + "restart_process_stopped": true, + "restart_profile_recompiled_from_manifest": true, + "restart_profile_started_without_sessions": true, + "session_excluded_from_identity": true, + "views_compiled_and_bound": true + }, + "generated_at_utc": "2026-07-15T02:46:31.346851+00:00", + "goal_tier_satisfied": true, + "manifest": { + "compiled_views": { + "SOUL.md": { + "generated_from_identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "role": "generated_view_not_authority", + "sha256": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a" + }, + "identity.json": { + "generated_from_identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "role": "generated_view_not_authority", + "sha256": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6" + } + }, + "identity_inputs": { + "canonical_database": { + "authority": "synthetic_noncanonical_fixture", + "canonical_authority_granted": false, + "database": "leo_identity_fixture", + "database_user": "leo_identity_reader", + "fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "source": { + "record_count": 7, + "repo_path": "fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json", + "semantic_sha256": "c5bbf346613270a5e3b17f604f69306ed42f545d4f227d784ec0dcb3d675dcb2" + }, + "structure_sha256": "3333333333333333333333333333333333333333333333333333333333333333", + "system_identifier": "7649789040005668902", + "table_count": 7, + "table_rows_sha256": "2222222222222222222222222222222222222222222222222222222222222222", + "total_rows": 7 + }, + "gatewayrunner_execution_contract": { + "fixed_message_context": { + "auto_skill": null, + "media_or_file_context": null, + "reply_context": null + }, + "profile_surfaces": [ + "config.yaml", + "generated SOUL.md", + "skills", + "plugins", + "bin tools" + ], + "required_absent_or_empty": { + "generated_skill_prompt_cache": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "pairing_store": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "persistent_memory": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "prior_sessions_at_first_start": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "project_context": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945" + }, + "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" + ] + }, + "identity_name": "Leo", + "identity_sources": { + "database_derived_identity": { + "authority": "synthetic_noncanonical_fixture", + "content_sha256": "9de2dfa37529b23ba277348f3d910e967551e490a9b4d2be637bf45bb2aa7dde", + "database_fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "provenance": { + "authority": "synthetic_noncanonical_fixture", + "canonical_authority_granted": false, + "export_receipt_sha256": null, + "mode": "synthetic_noncanonical_fixture", + "same_transaction_as_fingerprint": false + }, + "record_count": 5, + "repo_path": "fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json", + "semantic_sha256": "70aaa7287cc41b52a3c902cf2e1b90fa4d84a5ee0a61e10c642f556fcc9304c3" + }, + "static_constitution": { + "authority": "pinned_static_source", + "content_sha256": "b5477e778a2be0abba783390dac2e04ed199b8ce5679fc324270f808d46543f1", + "record_count": 3, + "repo_path": "fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json", + "semantic_sha256": "b576d5cee08530a95e0b196a3296f4743b70d94bcadffaa36544dd6bf1387f8a" + } + }, + "model": { + "actual_model_required_in_turn_receipt": true, + "default_model": "leo-identity-readback-v1", + "gateway_smart_model_routing": null, + "hosted_weights": "external_provider_managed_not_locally_hashable", + "model_section_sha256": "90b3943ace9ff83b1711002fda8dc9736448e8d3c1c58a17ac90608f4ce58756", + "provider": "fixture-local", + "smart_routing": false, + "smart_routing_model": null + }, + "permissions": { + "authorization_decision_required_in_runtime_receipt": true, + "gateway_permissions_sha256": "eac17ada1d02b5413183587d12fc265538479de00709ed8c275e9045cd720290", + "identity_config_sha256": "e6df9b65bab148ea8502555bbc260df66b4785e7dd527cfd15d2f0a48c2e8b3e", + "runtime_policy": { + "allowed_tools": [ + "identity_readback" + ], + "database_write_enabled": false, + "network_send_enabled": false + }, + "tool_permissions_sha256": "9bb2cec2f65d43efb597a72bdced44076c25aac292f2fccbc4c448b6e7fd3e16" + }, + "runtime": { + "hermes_git_head": "b238fd6fc2636ed3582e14900774014c52732698", + "hermes_source_sha256": "dcf8109051e0b69cafe2e8dd328ff501211b62ea19725416c3781bbdd594d028", + "identity_runtime_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "python": { + "abi_tag": "cpython-311", + "implementation": "CPython", + "python_version": "3.11.15", + "runtime_distributions": { + "PyYAML": "6.0.3" + }, + "runtime_distributions_sha256": "57a91bb880a01800903c1604b6eec1419514864ebd6a0644121920da15a8f165" + }, + "teleo_git_head": "b238fd6fc2636ed3582e14900774014c52732698", + "teleo_source_sha256": "2bd2e659eafb0ac0823f7f51c4296b2eb500b6f74469b3ac5f5287a5d4810aea" + }, + "session_boundary": { + "classification": "temporary_noncanonical", + "included_in_identity_inputs": false, + "may_be_used_as_evidence": false, + "restart_policy": "fresh_process_with_session_state_outside_identity_authority" + }, + "skills": { + "content_sha256": "d3f449ddcd7c88238dc88c6233cf2130875f51bd2e0911b2ce477b7c602ae90f", + "file_count": 1 + }, + "source_commit": "b238fd6fc2636ed3582e14900774014c52732698" + }, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "schema": "livingip.leoIdentityManifest.v1" + }, + "mode": "successful_restart_plus_drift_negative", + "pre_restart_verification": "pass", + "production_database_contacted": false, + "production_profile_replaced": false, + "receipt_sha256": "685641533be36b78e382dfd690bdc588bc95045379b44dd885d1d1d0ee57b507", + "required_tier": "T2_runtime", + "restart": { + "actual_argv_persisted": false, + "command_serialization": "logical_redacted_v1", + "launcher_pid": 80642, + "logical_command": [ + "", + "-m", + "scripts.leo_identity_profile", + "query", + "--profile", + "", + "--source-root", + ".", + "--hermes-root", + "fixtures/working-leo/leo-identity-v1/hermes-runtime", + "--deployment-root", + ".", + "--query", + "What defines Leo's identity, and what is temporary?" + ], + "orphan_cleanup_required": false, + "process_group_absent_after_wait": true, + "profile_role": "restart", + "returncode": 0, + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout": { + "answer": "Leo is defined by 3 pinned static constitutional rules and 5 active database-derived identity rows from a synthetic noncanonical fixture at fingerprint 1111111111111111111111111111111111111111111111111111111111111111. SOUL.md is a generated view; runtime/session memory is temporary, noncanonical, and cannot be evidence.", + "answer_sha256": "e0a21e1fc5357a2f358b4e96c52e971f3addee2468e44748d898adfd62dafa25", + "authorization": { + "authorization_decision_observed": false, + "claim": "local_policy_binding_not_an_authorizer_readback", + "declared_runtime_policy": { + "allowed_tools": [ + "identity_readback" + ], + "database_write_enabled": false, + "network_send_enabled": false + } + }, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "output_provenance": { + "actual_model_call_performed": false, + "compiled_identity_sha256": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6", + "compiled_soul_sha256": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a", + "database_authority": "synthetic_noncanonical_fixture", + "database_canonical_authority_granted": false, + "database_fingerprint_sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "database_identity_sha256": "70aaa7287cc41b52a3c902cf2e1b90fa4d84a5ee0a61e10c642f556fcc9304c3", + "runtime_identity_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "static_constitution_sha256": "b576d5cee08530a95e0b196a3296f4743b70d94bcadffaa36544dd6bf1387f8a" + }, + "process_id": 80642, + "query": "What defines Leo's identity, and what is temporary?", + "query_sha256": "13704c2c9ae999e3f9b3f1c9c5572d062cd7fe49903c2317fa85aaa39490af66", + "runtime_instance_id": "109445cde86f4ac3b266930462fb62c3", + "schema": "livingip.leoIdentityQueryReceipt.v1", + "session": { + "classification": "temporary_noncanonical", + "included_in_output_provenance": false, + "may_be_used_as_evidence": false, + "record_sha256": "56b6a2ea25657a87dc90dc19f49bece0ac62241f5814732ed80ef4f34d426d84" + }, + "started_at_utc": "2026-07-15T02:46:37.036778+00:00", + "status": "pass" + }, + "terminated_with": null, + "timed_out": false + }, + "restart_compile": { + "compiled_view_sha256": { + "SOUL.md": "83d27dba6e1b160085388a5dc84da565abf5736351b1af3f220d13b35aac244a", + "identity.json": "40419152862e7d9bca631c53eaa2201052cf61eafe6d304c8a302f8ea97d6fb6" + }, + "generated_views_are_authority": false, + "identity_inputs_sha256": "c101c488a9f623b4cef841ddb561c0f289a74df54aadfdfe9b28c4b7643fec2e", + "manifest_sha256": "22e66cf39b2c5aac3b4043f37aa0773b4861fec616a435e8b5e0ff8c86586bef", + "nonauthoritative_inputs_omitted": { + ".cursorrules": true, + ".hermes.md": true, + ".skills_prompt_snapshot.json": true, + "AGENTS.md": true, + "CLAUDE.md": true, + "HERMES.md": true, + "MEMORY.md": true, + "USER.md": true, + "memories": true, + "platforms/pairing": true + }, + "profile_static_entries": [ + "config.yaml", + "skills", + "plugins", + "bin" + ], + "runtime_identity_sha256": "5c54248a5d1f00b35912d47d8b3045b8cfbed16ae9b86409b549ab7042f51159", + "schema": "livingip.leoIdentityCompileReceipt.v1", + "session_directories_started_empty": true, + "status": "pass" + }, + "runtime_component_proven": "fresh_profile_recompile_plus_distinct_process_restart", + "runtime_variant": "local_deterministic_identity_compiler_readback", + "schema": "livingip.leoIdentityReconstructionReceipt.v1", + "second_start_attempted": true, + "session_boundary_readback": { + "full_behavior_changed_after_session": true, + "identity_runtime_unchanged_after_session": true, + "session_classification": "temporary_noncanonical", + "session_file_count": 1, + "session_used_as_output_provenance": false + }, + "status": "pass", + "telegram_message_sent": false, + "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." +} 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/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/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/ops/run_local_genesis_ledger_rebuild.py b/ops/run_local_genesis_ledger_rebuild.py index ef871a3..a500fc6 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,18 @@ 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 +82,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 +127,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 +343,116 @@ 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", + ) + reviewed_time = _parse_aware_timestamp(proposal.get("reviewed_at"), "receipt proposal reviewed_at") + applied_time = _parse_aware_timestamp(proposal.get("applied_at"), "receipt proposal applied_at") + if reviewed_time > applied_time: + raise ReconstructionError("invalid_mutating_receipt", "revise_strategy proposal approval is after apply time") + if transaction_time < reviewed_time: + raise ReconstructionError( + "invalid_mutating_receipt", "revise_strategy transaction timestamp is before proposal approval" + ) + if transaction_time > applied_time: + raise ReconstructionError( + "invalid_mutating_receipt", "revise_strategy transaction timestamp is after proposal apply time" + ) + + return {**strategy, "id": strategy_id}, [ + {**row, "id": normalized_id} for row, normalized_id in zip(nodes, node_ids, strict=True) + ] + + def _validate_entry_material( material: dict[str, Any], *, @@ -340,11 +485,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 +498,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 +524,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 +537,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 +557,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 +702,266 @@ 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 +1106,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 +1124,7 @@ def apply_entry( entry: LedgerEntry, summary: dict[str, Any], ) -> None: + mutating_prestate: dict[str, Any] | None = None try: _psql( args, @@ -733,20 +1151,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 +1191,27 @@ 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 +1652,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 +1671,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/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/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_identity_reconstruction_canary.py b/scripts/run_leo_identity_reconstruction_canary.py new file mode 100644 index 0000000..907f1fe --- /dev/null +++ b/scripts/run_leo_identity_reconstruction_canary.py @@ -0,0 +1,517 @@ +#!/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, PurePosixPath, PureWindowsPath +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" +PRIVATE_PATH_PREFIXES = tuple("/" + value for value in ("Users/", "private/tmp/", "var/folders/")) + + +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 _receipt_strings(value: object) -> list[str]: + if isinstance(value, dict): + keys = [key for key in value if isinstance(key, str)] + return keys + [text for item in value.values() for text in _receipt_strings(item)] + if isinstance(value, list): + return [text for item in value for text in _receipt_strings(item)] + return [value] if isinstance(value, str) else [] + + +def _validate_portable_receipt(value: object) -> None: + for text in _receipt_strings(value): + if ( + any(prefix in text for prefix in PRIVATE_PATH_PREFIXES) + or PurePosixPath(text).is_absolute() + or PureWindowsPath(text).is_absolute() + ): + raise CanaryError("receipt contains a private or absolute filesystem path") + + +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 _logical_repo_path(path: Path, *, deployment_root: Path, placeholder: str) -> str: + """Return a stable repo-relative path or a non-identifying placeholder.""" + + try: + relative = path.resolve().relative_to(deployment_root.resolve()) + except (OSError, ValueError): + return placeholder + value = relative.as_posix() + return value if value != "." else "." + + +def _logical_query_command( + *, + deployment_root: Path, + source_root: Path, + hermes_root: Path, +) -> list[str]: + return [ + "", + "-m", + "scripts.leo_identity_profile", + "query", + "--profile", + "", + "--source-root", + _logical_repo_path(source_root, deployment_root=deployment_root, placeholder=""), + "--hermes-root", + _logical_repo_path(hermes_root, deployment_root=deployment_root, placeholder=""), + "--deployment-root", + ".", + "--query", + profile_runtime.FIXED_QUERY, + ] + + +def _run_query_child( + *, + deployment_root: Path, + profile: Path, + source_root: Path, + hermes_root: Path, + profile_role: str, + timeout_seconds: float = 60, +) -> dict[str, Any]: + deployment_root = deployment_root.resolve() + profile = profile.resolve() + source_root = source_root.resolve() + hermes_root = hermes_root.resolve() + actual_argv = [ + 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( + actual_argv, + 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 { + "logical_command": _logical_query_command( + deployment_root=deployment_root, + source_root=source_root, + hermes_root=hermes_root, + ), + "command_serialization": "logical_redacted_v1", + "profile_role": profile_role, + "actual_argv_persisted": False, + "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, + profile_role="drift_negative", + ) + 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, + profile_role="first_start", + ) + 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, + profile_role="pre_restart_drift", + ) + 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, + profile_role="restart", + ) + 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": "details_redacted", + "detail_sha256": behavior.sha256_bytes(str(exc).encode("utf-8")), + } + ) + 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." + ) + _validate_portable_receipt(report) + 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/scripts/run_leo_local_ingestion_proposal_canary.py b/scripts/run_leo_local_ingestion_proposal_canary.py index 89270dc..12854b2 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,526 @@ 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.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" +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 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 + ) + + +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") + 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} + + 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()), + segment_manifest = normalized_segment_manifest(segments) + source_content_sha256 = canonical_sha256(segment_manifest) + extraction_spec_sha256 = canonical_sha256(extraction) + 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)}, + "source": source, + "extractor": extractor_identity, + "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_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), + "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(replay_seed, "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_components": input_receipt["replay_identity_components"], + "replay_identity_sha256": input_receipt["replay_identity_sha256"], + "segments": normalized_segment_manifest(fixture["segments"]), + "row_ids": row_ids, + "candidate_counts": input_receipt["counts"], + } return { "id": row_ids["parent_proposal_id"], "proposal_type": "attach_evidence", @@ -111,47 +557,149 @@ 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 _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 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 +707,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 +723,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 +772,221 @@ 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 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 + 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 +1054,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" ] @@ -372,128 +1102,332 @@ 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 ], - "source_content_hash_matches": any( - row.get("hash") == input_receipt["source_content_sha256"] for row in source_rows - ), - "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 + "planned_edge_keys": [ + {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(claim_rows), + "sources": len(source_rows), + "evidence": len(evidence_rows), + "edges": len(edge_rows), + }, + "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": ( + links["manifest_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 {} - proposal = readback.get("staged_proposal") or {} - links = proposal_links(readback, input_receipt) + 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) + 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"], + "evidence_extractions": counts["evidence_candidates"], + "duplicate_assessments": counts["duplicate_judgments"], + "conflict_assessments": counts["conflict_relationships"], + "staged_proposals": 1, + } + 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 { - "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_row.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"] + "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_row.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 claim_rows_are_objects + and all( + isinstance(row.get("metadata"), dict) and row["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}, - "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"], - "no_review_or_apply_metadata": all( - proposal.get(key) is None - for key in ("reviewed_by_handle", "reviewed_at", "applied_by_handle", "applied_at") + "all_claim_candidates_persisted_once": ( + 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_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_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": pending_state["status_matches"], + "proposal_has_replayable_ingestion_manifest": ( + 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": ( + 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) + ), + "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"]) - 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": "isolated_realistic_ingestion_to_proposal", - "input": input_receipt, - "row_ids": row_ids, + "required_tier": "T2_runtime_to_review_queue_staging", + "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()) 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, - "row_link_fields_proven": proposal_links(readback, input_receipt), + "candidate_counts": input_receipt["counts"], + "row_link_fields_proven": proposal_link_proof(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, + 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" - ) - receipt["cleanup"]["no_production_target_referenced"] = receipt[ - "cleanup" - ]["network_mode_none"] - if not all( - ( - receipt["cleanup"]["container_absent"], - receipt["cleanup"]["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"] = ( - "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." - ) + 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 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 +1435,98 @@ 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": 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), + "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: + 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_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_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..e421ffb --- /dev/null +++ b/tests/test_leo_identity_manifest.py @@ -0,0 +1,794 @@ +from __future__ import annotations + +import argparse +import copy +import json +import os +import subprocess +import time +from pathlib import Path, PureWindowsPath + +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] +COMMITTED_RECEIPTS = ( + ROOT / "docs/reports/leo-working-state-20260709/leo-reproducible-identity-t2-current.json", + ROOT / "docs/reports/leo-working-state-20260709/leo-reproducible-identity-drift-negative-current.json", +) + + +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 _assert_path_portable_receipt(value: object) -> None: + canary._validate_portable_receipt(value) + + +def _assert_logical_child_execution(child: dict, *, profile_role: str) -> None: + logical_command = child["logical_command"] + assert "command" not in child + assert child["profile_role"] == profile_role + assert child["command_serialization"] == "logical_redacted_v1" + assert child["actual_argv_persisted"] is False + assert logical_command[0] == "" + assert logical_command[logical_command.index("--profile") + 1] == "" + _assert_path_portable_receipt(child) + + +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, + ) + + +@pytest.mark.parametrize( + "private_path", + [ + "/" + "Users/operator/checkout/profile", + "/" + "private/tmp/lane/.venv/bin/python", + "/" + "var/folders/cache/temporary-profile", + "/" + "tmp/temporary-profile", + "/" + "opt/checkout/interpreter", + "/" + "workspace/checkout/temporary-profile", + str(PureWindowsPath("C:/operator/checkout/temporary-profile")), + "\\\\" + "server\\share\\checkout\\temporary-profile", + ], +) +def test_receipt_path_guard_recursively_rejects_private_and_absolute_paths(private_path: str) -> None: + with pytest.raises(canary.CanaryError, match="private or absolute filesystem path"): + _assert_path_portable_receipt({"outer": [{"logical_command": ["", private_path]}]}) + + +def test_receipt_path_guard_rejects_an_absolute_path_in_a_nested_key() -> None: + private_key = "/" + "tmp/checkout/temporary-profile" + with pytest.raises(canary.CanaryError, match="private or absolute filesystem path"): + _assert_path_portable_receipt({"outer": [{private_key: ""}]}) + + +def test_logical_command_uses_placeholders_for_paths_outside_the_deployment_root(tmp_path: Path) -> None: + logical_command = canary._logical_query_command( + deployment_root=tmp_path / "checkout", + source_root=tmp_path / "outside-source", + hermes_root=tmp_path / "outside-hermes", + ) + + assert logical_command[logical_command.index("--source-root") + 1] == "" + assert logical_command[logical_command.index("--hermes-root") + 1] == "" + _assert_path_portable_receipt(logical_command) + + +@pytest.mark.parametrize("receipt_path", COMMITTED_RECEIPTS, ids=lambda path: path.name) +def test_committed_identity_receipts_are_path_portable_and_self_hashed(receipt_path: Path) -> None: + receipt = json.loads(receipt_path.read_text(encoding="utf-8")) + _assert_path_portable_receipt(receipt) + stable = {key: value for key, value in receipt.items() if key != "receipt_sha256"} + assert behavior.canonical_sha256(stable) == receipt["receipt_sha256"] + + +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("