# KRINEIA — Receipt Schema v2.1 **Status**: PROPOSED (pending constitutional receipt and human ratification) **Authority**: This document specifies the v2.1 receipt format emitted by new KRINEIA writers and the verification rules retained for historical v0.1 and v2.0 chains. Receipts that do not satisfy their declared version are not valid KRINEIA receipts. --- ## 0. Why this exists KRINEIA is the append-only evidence layer of the HUMMBL stack. A KRINEIA **receipt** is an atomic governance record that binds an observer's signed claim about a decision, actor, time, state, and deviation to exact bytes and the preceding chain. That binding is evidence of provenance and integrity; it does not by itself prove that the claimed event occurred or was truthful. Receipts are **not** generic logs. They are post-mortem-only audit artifacts that NEVER feed back into any agent's reward, gradient, or policy-update path. Agents may read the chain for coordination; agents may not learn from it. For the five governing invariants, see `INVARIANTS.md`. --- ## 1. Legacy v0.1 receipt envelope Each receipt is a single JSON object on a single line of a JSON-Lines (`.jsonl`) file. All receipts have exactly six top-level fields: | Field | Type | Description | |---|---|---| | `id` | string (UUIDv4) | Globally unique identifier for this receipt | | `time` | string (ISO 8601 UTC, `Z` suffix) | When the event happened | | `state` | object | Structured event state (see §3) | | `drift` | number \| null | Deviation from setpoint (null when not measured) | | `prev_hash` | string (64 hex) | SHA-256 hash of the prior receipt's canonical form | | `hash` | string (64 hex) | SHA-256 hash of THIS receipt's canonical form | The first five fields map 1:1 to the KRINEIA 4-field model (`id`, `time`, `state`, `drift`) plus the chain link (`prev_hash`). The `hash` field closes the chain forward. ### Example (genesis receipt) ```json {"id":"90569814-c21b-43af-9f9b-825e722b9377","time":"2026-04-27T09:36:10Z","state":{"event":"daemon.start","payload":{"pid":27860,"args":{"once":true,"max_topics":0}}},"drift":null,"prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","hash":"bda150122731e1b697b269802bd37c4d57a87cf7495f959d824690c4fbdcfab3"} ``` --- ## 2. Legacy v0.1 hash chain rule ### Canonicalization The hash of a receipt is the SHA-256 of its **canonical form**, defined as: ```python canonical = json.dumps( {"id": id, "time": time, "state": state, "drift": drift, "prev_hash": prev_hash}, sort_keys=True, separators=(",", ":"), ) hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest() ``` Three rules to note: 1. The `hash` field itself is **not** part of the canonical form. 2. `sort_keys=True` sorts keys recursively at every depth — payloads must be deterministically serializable. 3. `separators=(",", ":")` strips whitespace — no pretty-printing in the canonical form. ### Genesis The first receipt in any chain has `prev_hash` = `"0" * 64` (sixty-four ASCII zeros). ### Linkage For every subsequent receipt at index `i`, `prev_hash[i] == hash[i-1]`. ### Validation algorithm A chain is valid iff: - The first receipt has `prev_hash == "0" * 64`. - For every adjacent pair `(prev, curr)`, `curr.prev_hash == prev.hash`. - For every receipt, `hash == sha256(canonical_form_without_hash_field)`. An internal break demonstrates modification, a fork, or removal before a retained successor. A self-consistent prefix cannot by itself demonstrate that no terminal receipts were removed. Completeness decisions therefore require a trusted expected head hash and receipt count held outside the presented JSONL artifact. --- ## 3. Event taxonomy Receipts come from two trace levels, depending on which process emitted them. Every event below was enumerated against the v0.1 reference implementation; payload keys are exhaustive (no silent fields). ### 3.1 Daemon-level (`krineia_daemon_trace.jsonl`) Emitted by the long-running daemon. One file per daemon installation. **14 event types.** | Event | Payload keys | Trigger | |---|---|---| | `daemon.start` | `pid`, `args` | Daemon process boot | | `daemon.ollama_unreachable_at_start` | *(empty)* | Boot-time Ollama probe failed | | `daemon.heartbeat` | `pending`, `done`, `failed`, `topics_processed_this_run`, `current_backoff_s` | 15-min idle heartbeat (no topic in progress) | | `daemon.stale_lock_reclaimed` | `prior_pid`, `prior_start` | Found a dead daemon's lock file at boot, took it over | | `daemon.stop_requested` | `topics_processed_this_run` | SIGTERM/SIGINT received | | `daemon.fatal` | `error_type`, `error` | Unhandled exception in main loop | | `daemon.stop` | `topics_processed_this_run` | Daemon final exit (always emitted, even after fatal) | | `queue.reclaim_stale` | `file` | A topic was sitting in `in_progress/` from a prior dead daemon, moved back to `pending/` | | `topic.claim` | `topic_id`, `run_id`, `topic`, `submitted_by`, `priority` | Daemon claimed a pending topic | | `topic.bad_json` | `file`, `error` | Pending file failed to parse — moved straight to `failed/` | | `topic.empty` | `file`, `topic_id` | Pending file had no `topic` field — moved to `failed/` | | `topic.complete` | `topic_id`, `run_id`, `elapsed_s` | Orchestrator subprocess returned exit 0 | | `topic.fail` | `topic_id`, `run_id`, `exit_code`, `elapsed_s` | Orchestrator subprocess returned non-zero | | `topic.timeout` | `topic_id`, `run_id`, `elapsed_s` | Orchestrator subprocess exceeded `ORCHESTRATOR_TIMEOUT` (default 7200s) | ### 3.2 Orchestrator-level (`/krineia_trace.jsonl`) Emitted by `nodezero_orchestrator.py` once per topic run. One file per run dir. **18 event types.** | Event | Payload keys | Trigger | |---|---|---| | `orchestrator.start` | `run_id`, `run_dir`, `mode`, `topic_arg`, `topics_file` | Orchestrator entry point | | `topic.start` | `topic_idx`, `slug`, `topic`, `lenses_planned` | Begin processing a topic | | `lens.start` | `module`, `lens_id`, `topic_slug`, `model` | Begin a lens generation | | `lens.ok` | `module`, `lens_id`, `topic_slug`, `out_size`, `elapsed_s` | Lens completed | | `lens.fail` | `module`, `lens_id`, `topic_slug`, `elapsed_s` | Lens model call errored | | `lens.skip_cached` | `module`, `lens_id`, `topic_slug`, `out_size` | Lens output existed on disk, skipped re-generation | | `lens.missing_in_registry` | `module`, `lens_id`, `topic_slug` | Planned lens not in registry — skipped | | `synthesis.start` | `topic_slug`, `model`, `lens_inputs` | Begin synthesis generation | | `synthesis.ok` | `topic_slug`, `out_size`, `elapsed_s` | Synthesis completed | | `synthesis.fail` | `topic_slug`, `elapsed_s` | Synthesis model call errored | | `synthesis.skip_cached` | `topic_slug` | Synthesis output existed, skipped | | `paideia.start` | `topic_slug`, `model` | Begin PAIDEIA-9 scoring | | `paideia.ok` | `topic_slug`, `signature`, `scores` | Score completed; scores object has 9 axis keys (M, D, E, V, C, L, S, P, A) | | `paideia.ok_raw` | `topic_slug` | Scoring returned raw (unparsed) text — partial success | | `paideia.fail` | `topic_slug` | Score model call errored | | `paideia.skip_no_synth` | `topic_slug` | Synthesis missing — skipped scoring | | `topic.complete` | `topic_idx`, `slug` | All lenses + synthesis + score done | | `orchestrator.complete` | `run_id`, `counters`, `sitrep_path` | Orchestrator final exit | ### 3.3 Chain semantics The two trace levels are **independent chains**. The daemon trace chains daemon events; each per-run trace chains its own orchestrator events. They are linked at the **data layer** via shared `run_id` and `topic_id`, not at the **hash layer**. A future spec may unify them under a single Merkle root (Phase 2). ### 3.4 Conventions - **Skip events** (`*.skip_cached`, `*.skip_no_synth`) are part of the chain even though no model work was done — they record the cache-hit decision for audit. - **Fail events** (`*.fail`, `topic.fail`, `topic.timeout`, `topic.bad_json`, `topic.empty`, `daemon.fatal`) record state transitions, NOT error details. Stack traces / stderr go to plain-text logs (`orchestrator.log`, `krineia_daemon.log`) and the per-topic FAILED envelope (§5), not into the chain. Rationale: receipts are governance artifacts, not debugging aids; keeping them small and structurally stable matters more than capturing every byte of an error. --- ## 4. Per-topic outputs When a topic completes, the run dir at `//` contains: | File | Purpose | |---|---| | `_meta.json` | Topic, slug, lenses attempted, completion timestamp | | `__.md` | Raw model output for each lens (one file per lens) | | `synthesis.md` | Cross-lens synthesis (the model's converged answer) | | `paideia_score.json` | PAIDEIA-9 9-axis content quality score | The run-level files are: | File | Purpose | |---|---| | `krineia_trace.jsonl` | Per-run hash chain (§3.2) | | `lenses_used.json` | Final lens roster after rotation | | `topics.json` | Topic list as input to this run | | `orchestrator.log` | Plain-text human-readable log | | `SITREP.md` | Generated summary report | --- ## 5. Per-topic envelope When a topic is processed end-to-end, the input envelope at `queue/pending/.json` is moved to `queue/done/.json` (or `queue/failed/.json`) with completion metadata appended: | Field | Source | Description | |---|---|---| | `id` | submitter | 8-hex slug; primary topic key | | `topic` | submitter | The question text | | `submitted_utc` | submitter | When the topic was queued | | `submitted_by` | submitter | Actor identifier | | `priority` | submitter | Integer priority (higher = sooner) | | `_completed_utc` | daemon | When orchestrator returned | | `_run_id` | daemon | Run dir slug (used to find outputs) | | `_run_dir` | daemon | Absolute path to run dir | | `_exit_code` | daemon | Orchestrator subprocess exit code | | `_elapsed_s` | daemon | Wall time of orchestrator subprocess | Underscore-prefixed fields are appended by the daemon and were not present at submission time. --- ## 6. v2.1 receipt envelope and cryptography New writers MUST emit schema `2.1.0`. Every receipt has exactly ten top-level fields: | Field | Type | Requirement | |---|---|---| | `schema_version` | string | Exactly `"2.1.0"` | | `id` | string | Canonical lowercase UUIDv4 | | `session_id` | string | Non-empty opaque session identifier, at most 256 characters | | `actor_identity` | string | Bare identity of the observed actor | | `time` | string | Valid ISO 8601 UTC timestamp with `Z` suffix | | `state` | object | Exactly `event` and `payload` | | `drift` | number \| null | Finite value in `[0,1]`, or null | | `prev_hash` | string | Prior receipt hash, or 64 zeroes for genesis | | `signature_envelope` | object | Exactly the four fields below | | `hash` | string | SHA-256 digest defined below | `signature_envelope` contains `key_id`, `signer_identity`, `algorithm`, and `signature`. `algorithm` MUST be exactly `Ed25519`. `signature` is the raw 64-byte Ed25519 signature encoded as 128 lowercase hexadecimal characters. The verifier's public-key registry MUST bind `key_id` to the same `signer_identity` carried by the receipt. `signer_identity` MUST differ from `actor_identity`; these structural checks do not by themselves establish operational trust-root separation. The v2.1 public-key registry is a JSON object keyed by the receipt's `key_id`. A usable key identifier is 1–256 characters drawn only from `A-Z`, `a-z`, `0-9`, `_`, `.`, `/`, `#`, and `-`. Every referenced registry entry MUST contain exactly `algorithm`, `public_key`, and `signer_identity`: `algorithm` is exactly `"Ed25519"`; `public_key` is the 32 raw Ed25519 public key bytes encoded as 64 lowercase hexadecimal characters; and `signer_identity` exactly equals the receipt's claimed bare signer identity. Unknown fields, duplicate JSON members, malformed identifiers, or any other public-key representation are nonconforming. ### 6.1 Canonical JSON KRINEIA canonical JSON v2.1 is the UTF-8 encoding of JSON serialized with these exact rules: ```python json.dumps( value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False, ) ``` Object keys are sorted recursively; insignificant whitespace is absent; non-ASCII Unicode is emitted directly; duplicate object names, unpaired Unicode surrogates, NaN, and infinities are invalid. No Unicode normalization is applied: code points are cryptographically significant. ### 6.2 Signed body The signed body contains every top-level receipt field except `hash`. Within `signature_envelope`, it contains exactly `key_id`, `signer_identity`, and `algorithm`, excluding only `signature`: ```python signature_metadata = { "key_id": key_id, "signer_identity": signer_identity, "algorithm": "Ed25519", } signed_body = {all_top_level_fields_except_hash} signed_body["signature_envelope"] = signature_metadata signature = Ed25519.sign(private_key, canonical_json(signed_body).encode("utf-8")) ``` This binds signer identity, key identifier, and algorithm without creating a circular dependency on the signature value. ### 6.3 Receipt hash After inserting `signature`, compute: ```python hash_body = {all_top_level_fields_except_hash} # includes the complete signature_envelope hash = sha256(canonical_json(hash_body).encode("utf-8")).hexdigest() ``` The receipt hash therefore commits to both the signed body and the signature. A v2.1 receipt is invalid unless all ten fields are present, its hash and chain link are correct, its Ed25519 public key is available under a registry entry bound to the claimed signer identity, and its signature verifies. Missing cryptographic support, key material, or identity binding is a fatal verification error, never a warning. ### 6.4 Frozen known-answer vector `test-vectors/v2.1-ed25519-unicode-numbers.json` freezes one complete v2.1 receipt, canonical signed-body text, Ed25519 public key and signature, canonical hash body, and terminal hash. It exercises recursive key ordering, integers and finite decimals, escaped text, non-ASCII Unicode, and distinct precomposed/decomposed code-point sequences. Implementations must reproduce the bytes and values exactly before claiming v2.1 conformance. The fixture contains public verification material only; it is not a signing-key example. ### 6.5 Legacy v2.0 verification-only rule Schema `2.0.0` is deprecated and MUST NOT be emitted by new writers. The verifier retains its historical algorithm solely to audit existing chains: hash all top-level fields except `signature_envelope` and `hash`, then—when a signature exists—verify the receipt hash under the declared HMAC-SHA256 or Ed25519 algorithm. This legacy rule does **not** bind signature metadata or signature bytes into the receipt hash. Verification emits a deprecation warning. ### 6.6 Chain-level assurance policy Receipt versions are not negotiated by untrusted input. The public verifier uses these chain-level rules: 1. Unsigned v0.1 is rejected by default. An explicit legacy-structural policy may reproduce its frozen hash/link rule, but the result is labeled unauthenticated and emits a warning. 2. Once a v2.1 receipt appears, every later receipt MUST be v2.1. Omitting `schema_version` or selecting v2.0 after that point is a fatal downgrade. 3. Tail completeness is an external property. An assurance decision MUST pin the expected terminal hash and receipt count in an independently trusted checkpoint, release attestation, transparency witness, or equivalent authority. A manifest stored only beside the JSONL file is not an independent anchor. 4. The reference writer does not extend an unsigned legacy chain under its default policy. Migration requires a separately reviewed path bound to an independently admitted legacy checkpoint. The standalone verifier exposes `--allow-legacy-structural`, `--expected-head`, and `--expected-count` so archival compatibility and anchored assurance cannot be confused accidentally. Both its library API and CLI require the head and count as one inseparable pair; `--allow-unanchored` is an explicit internal-consistency-only escape hatch and cannot support a completeness claim. ### 6.7 Reference watcher source binding The reference watcher emits the fixed event identity `skill.invoked`; the complete colon-qualified skill name remains in `state.payload.skill`. Its payload also binds `mode`, `args_hash`, `session_id`, `bus_timestamp`, the source byte offset, a SHA-256 digest of the exact newline-terminated TSV bytes, and a deterministic source-record identifier derived from offset plus digest. The source message grammar requires a first `host=` metadata field, unique bounded `key=value` metadata, and exactly one bracketed `skill`, `mode`, `args_hash`, and `session` tag. `args_hash` is exactly 64 lowercase hexadecimal characters. A persistent byte cursor advances only after the corresponding receipt or rejection disposition is flushed. Restart replays unacknowledged records, and a replay whose source digest is already in the verified receipt chain is acknowledged without a duplicate receipt. Malformed records receive a durable rejection disposition before the cursor advances. Rejections are committed to a cumulative SHA-256 accumulator plus a fixed-size rotating marker ring, so attacker-controlled malformed input cannot exhaust an ever-growing directory and permanently pin later records. The accumulator state retains the complete rejection sequence commitment while the ring retains only its configured recent window; it is not a full rejection history. State is persisted before the ring slot and cursor, allowing a restart to reconstruct the last slot after a crash without incrementing the sequence twice. Corrupt or inconsistent cursor, accumulator, or marker state fails closed. `--start-at-end` is a first-start opt-out that deliberately skips existing bus records. Its bounded backward scan authenticates only the final complete record needed to establish the initial byte cursor; it does not inspect or attest to earlier source history and cannot support a completeness claim. Records larger than the ordinary line limit are discovered incrementally under the per-poll byte budget. Between polls, the watcher revalidates a bounded suffix anchor at the scan watermark and fails closed on truncation or rewrite. Before any completed oversized rejection is persisted, and again before its cursor advances, the watcher rehashes the exact completed source range. This final linear check prevents a digest assembled from old and replacement bytes from consuming later source records. The reference watcher also imposes a 16 MiB hard bound on any single recovery scan; a record that reaches the bound without a terminating newline fails closed for operator intervention. This keeps the final exact-byte checks bounded instead of turning an attacker-sized line into unbounded disposition work. The same hard bound permits each poll and restart to rehash the checkpointed prior record, including an oversized rejection, against its stored digest. A mutation in the narrow interval after disposition validation but before cursor persistence is therefore detected on the next access rather than silently becoming the new source history. When a writer opens a non-empty receipt chain, its expected existing head and count must be supplied from a checkpoint outside that chain. The writer then advances that in-memory checkpoint after each durable append. This requirement prevents a restart from silently extending a cleanly truncated prefix; durable publication assurance still requires an independently retained checkpoint. The writer also refuses implicit genesis creation. A first deployment must select `--initialize-new-chain` exactly once on a missing or zero-byte receipt path, or supply a trusted zero digest and zero count. The flag is not a restart mechanism: after the first append, restarts must supply the independently retained nonzero checkpoint. --- ## 7. Reference implementations The historical v0.1 schema was emitted by: - `krineia_daemon.py` — daemon-level chain (§3.1). KRINEIATrace class, lines 100-135. - `nodezero_orchestrator.py` — per-run chain (§3.2). The v2.1 reference writer lives at `daemon/krineia_watcher.py`. The version-dispatching standalone validator and normative v2.1 canonicalization functions live at `tools/verify_chain.py`. The machine-readable strict write schema is `krineia_receipt.schema.json`. --- ## 8. Versioning - **v0.1** (2026-04-27) — initial spec, captures emitted format end-to-end. 6 top-level fields. - **v2.0.0** (2026-05-15) — historical runtime envelope. Its prose and implementation disagreed about signature/hash coverage. Deprecated and verification-only. - **v2.1.0** (proposed 2026-08-30) — strict Ed25519 envelope with signed metadata, signature-bound receipt hashes, and fail-closed verification. The v0.1 canonicalization rule remains frozen. Schema `2.1.0` is admitted through the constitutional amendment and ADR referenced in `SCHEMA_CHANGELOG.md`; it does not retroactively alter legacy receipt bytes. --- ## 9. Source receipt The v0.1 document reported derivation from a single end-to-end daemon run on 2026-04-27, run_id `20260427-093612-0c874676`; the underlying historical source was not admitted to this repository. The historical derivation referenced uncommitted daemon and orchestrator prototypes that are not part of this repository. They are provenance context, not reproducible source evidence. The admitted reference writer is now `daemon/krineia_watcher.py`; its behavior is covered by the repository tests.