The receipt spine, in detail.
You can't audit what you can't verify. Thunderline's trust is one cryptographic chain: sealed receipts, lineage edges, and the policy gate that governs them. This page is for your security team.
The 17-field seal
Every consequential agent action in Thunderline produces one
ActionReceipt row, written by
Thunderline.Thunderbolt.ExecutionRecorder.
The seal is computed over exactly 17 fields. Any post-write modification
of those fields changes the hash. The sealed hash is stored in the same
row and checked on every lineage-edge traversal.
The write path: the recorder calls
compute_seal_hash/1 before inserting the row.
After insertion, Thundervine.Resources.LineageEdge
persists a lineage edge linking the receipt into the causal chain.
On read, verify_receipt_seal/1 recomputes the hash and
compares; then AcornAuthorityDecisionCorrelation.verify_persisted_edge/4
cross-checks the lineage edge itself.
The 17 sealed fields (@seal_fields)
These fields are passed to compute_seal_hash/1 in the order
below. Three additional fields on the receipt row sit
outside the seal — noted at the end of the table.
| Field | Role in the receipt |
|---|---|
| receipt_id | UUID v4 primary key. Uniquely identifies this receipt row. Included in the seal so the hash binds to this exact row, not a copy. |
| envelope_version | Schema version integer. Lets the classifier know which hash algorithm and field-set to expect. Mismatches route to legacy paths, not false positives. |
| action_id | Identifies the Crown executor module that produced this receipt. Links the receipt to the policy-gate action definition. |
| domain | The Thunderline domain that owns this action (e.g., :acorn, :comsec). Part of the authorization perimeter. |
| mission_id | Parent mission UUID. Anchors the receipt to its operational context. Required for the lineage-edge cross-check. |
| saga_run_id | Saga run UUID if this action belongs to a multi-step saga. Enables saga-level replay and rollback verification. |
| step_id | Monotonically ordered step within the saga. Together with saga_run_id, identifies position in the causal chain. |
| correlation_id | Request correlation token propagated from the inbound event. Links to distributed traces and audit logs outside Thunderline. |
| causation_id | The receipt_id of the receipt that caused this action. Enables full causal graph traversal without external log correlation. |
| actor | Identity of the principal that authorized this action. Bound by the policy gate's ActorProvenance resolution — not the raw request actor claim. See §4. |
| inputs_hash | SHA-256 of the canonicalized input payload. Locks what evidence the action saw. Tampered inputs change this hash and break the seal. |
| status | Terminal outcome atom (:completed, :failed, :compensated, etc.). Sealed so status cannot be upgraded post-write without detection. |
| attempt | Retry count integer. Part of the seal so idempotency keys are replay-safe — a replayed attempt at a different count produces a different seal. |
| traceparent | W3C Trace Context header value. Included in the seal to bind the receipt to a specific distributed trace span. |
| timing | Map of wall-clock timestamps (started_at, completed_at). Seals the observed execution window; retroactive timestamp edits break the seal. |
| cost | Token or compute cost record for AI invocations. Sealed so cost cannot be scrubbed from the audit trail. |
| artifacts | List of artifact references (hashes + storage keys) produced by the action. Seals the output set so artifacts cannot be silently swapped. |
governance, output_redacted, and
input_redacted are stored on the receipt row but are
not included in @seal_fields. This is the design basis
for the authority-decision cross-check described in §2: because
output_redacted sits outside the seal, a one-column
update of authority_decision_status to
:current_valid would not break the seal hash — only the
lineage-edge cross-check catches it.
Keyed vs unkeyed seals
AUTHORITY_LINEAGE_KEYED_SEAL_V0 (GH #3173).
They raise the price of tampering — they do not forbid it.
A party with write access to the receipts table can recompute a valid
seal after editing a sealed field.
The current design hashes the 17 fields using a deterministic canonical
encoding without a secret key. The hash proves internal consistency:
the receipt row has not been modified since compute_seal_hash/1
ran. It does not prove authenticity against an adversary who has
database write access.
What actually defends ACORN authority status
The lineage-edge cross-check at
AcornAuthorityDecisionCorrelation.verify_persisted_edge/4
is the primary defense for ACORN authority decisions — not the seal alone.
Here is why the distinction matters:
output_redacted sits outside the seal. A single-column
UPDATE of authority_decision_status to
:current_valid on an ActionReceipt row would not change
the seal_hash — because that field is not in
@seal_fields. The seal check would still pass. The
cross-check does not: it verifies the lineage edge independently.
An authority decision without a corresponding, valid lineage edge is
rejected regardless of the seal status.
The defense model is therefore: seal as tamper evidence for the 17 sealed fields, plus lineage edge as an independent structural proof that the decision was produced by the governed flow, not injected post-hoc.
compute_seal_hash/1
would make the seal unforgeable without the key. This is tracked but
not shipped. Keyed seals would close the write-access gap; they are
not a replacement for the lineage cross-check.
AcornReceiptSealClassification.classify/1
Not all receipts in the database were written by the current encoder. The Thunderline codebase has gone through at least two distinct seal encoding regimes. The classifier returns one of four atoms to distinguish them:
The receipt was written by the current encoder. The seal hash recomputes correctly against the stored fields using the current canonical encoding. This is the only value that means the record has been verified against the current standard.
The seal is internally consistent with the V0 encoding that was in force when the row was written. The row has not been modified since it was written — but it was written under a different encoding than the current standard.
The same as :legacy_v0_consistent but written by
the K1 cell's own receipt writer under the K1-specific V0
encoding. Structurally consistent with the encoding in use at
write time.
The seal hash does not match under any known encoding. The row has been modified after the seal was computed, the encoder used at write time is unknown, or the row is corrupted.
Three rules your security team must enforce
:legacy_v0_consistent and :legacy_k1_v0_consistent
are not extensible verdict classes. New encoding dialects go through
a migration, not through adding a new legacy arm. An open legacy list
would let a tampered row claim "consistent with unknown encoding X."
unavailable is not :invalid.
If the classifier cannot run — because the receipt row is missing
fields, the hashing module is unavailable, or the database query
times out — the result is unavailable, not
:invalid. Treat unavailable as a circuit breaker:
surface the error, do not route to a fallback that silently accepts
the record.
:legacy_v0_consistent means "internally consistent with
the encoding in force when the row was written." It does not mean
authentic. It does not mean the row is unmodified relative to the
current standard. Operator UI must display legacy receipts with a
clearly distinct label — not the same green checkmark used for
:current_valid.
Policy gate flow
All agent actions in Thunderline pass through
Thundercrown.PolicyGate.validate_and_execute/2.
The gate enforces the 4-tier capability model, resolves actor
provenance, and emits a sealed ActionReceipt on success or failure.
The 4 capability tiers
Observe state. No mutations. Permitted to all authenticated principals within their scope. Read receipts are still sealed to prove what evidence the agent saw.
Modify durable state (write to memory, update records). Requires explicit grant in the action's capability declaration. Each mutation produces a sealed receipt.
Modify policy, role, or boundary configuration.
Restricted to operator-level principals. Admin actions require
the :admin capability and go through the authority
decision path.
Run code in a hermetic execution cell (K1 WASM or equivalent). The most constrained tier. Zero network, bounded compute, sealed I/O manifest. See §5 for the K1 cell constraints.
42 executor modules — the real count
As of the codebase canon, Thundercrown contains 42 executor modules
under Thundercrown.Actions.Catalog. Each module
declares its capability tier, required actor claims, evidence
references it consumes, and the receipt shape it produces.
The policy gate loads this catalog at startup and refuses to execute
any action not in the catalog. Undeclared actions cannot produce
receipts; they simply do not run.
Actor provenance: the authz vs telemetry distinction
The actor: field in an ActionReceipt is resolved by
ActorProvenance inside the policy gate. It is
not taken from request.actor as
supplied by the caller. A caller claiming to be a different principal
does not change the sealed actor field: the gate resolves provenance
from the verified session context, then stamps the result.
This matters for telemetry correlation. A telemetry event may carry a session-scoped actor label that differs from the governance-grade actor in the receipt. The sealed actor in the receipt is the governance truth. Telemetry actor labels are for operational visibility, not audit.
The validate_and_execute/2 contract
# Simplified call shape (Thundercrown.PolicyGate)
def validate_and_execute(action_ref, opts) do
# opts[:actor] → resolved via ActorProvenance, not trusted verbatim
# opts[:evidence_refs] → hashed into inputs_hash for the seal
# opts[:mission_id] → required; gates lineage-edge creation
#
# Returns {:ok, %ActionReceipt{}} or {:error, reason}
# Never raises; all failures produce a sealed failure receipt
end
K1: hermetic WASM execution
The K1 cell runs untrusted WASM modules in a hermetic execution
environment. It is implemented as a wasmtime Rustler NIF — not
wasmex. The distinction matters: wasmtime gives us direct control
over StoreLimits, fuel metering, and epoch interruption
from the host side.
Zero-import verification
Before a WASM module executes, the K1 cell verifies that the module
declares zero imports. A module that requests host functions is
refused at load time with :wasm_imports_present. This
is a hard gate: there is no allowlist of "safe" imports. Zero imports
means zero surface area for a guest module to reach host state.
Resource bounds
The StoreLimits applied to every execution:
# K1WasmCell — StoreLimits configuration
@max_memory_pages 17 # 17 pages × 64 KB = 1,088 KB max linear memory
@max_memories 1 # one Memory instance; multiple is a vector for side-channels
@max_instances 1 # one module instance per execution; no recursive spawning
@fuel_limit 10_000_000 # wasmtime instruction fuel; exhaustion = :fuel_exhausted
@wall_clock_ms 5_000 # epoch_interruption deadline in ms; hit = :wall_clock_exhausted
Interruption and termination atoms
The K1 cell distinguishes four terminal error conditions. Each maps to a distinct atom. Collapsing them to a single generic error would hide whether a module was malicious (importing, trapping) or merely expensive (fuel, wall-clock):
-
:wasm_imports_present— module declared imports; refused at load, before any execution. -
:fuel_exhausted— instruction fuel consumed before completion; execution halted by wasmtime. -
:wall_clock_exhausted— epoch interruption deadline exceeded; execution halted by host epoch tick. -
:wasm_trap— the module produced a WASM trap (unreachable, out-of-bounds, etc.); the module itself signaled failure.
Entry point: Thunderblock.Runtime.K1WasmCell.execute/3
# K1WasmCell.execute/3 — surface contract
Thunderblock.Runtime.K1WasmCell.execute(
wasm_binary, # <> — the WASM module; imports checked first
input_payload, # map — serialised to linear memory; included in inputs_hash
opts # [fuel: integer, wall_clock_ms: integer] — override limits
)
# Returns:
# {:ok, output_payload}
# {:error, :wasm_imports_present}
# {:error, :fuel_exhausted}
# {:error, :wall_clock_exhausted}
# {:error, :wasm_trap}
COMSEC: recommendation-only governance
The COMSEC traffic-budget lane is the canonical example of a governed-recommendation pattern in Thunderline. It demonstrates a key architectural constraint: an agent lane can produce governance evidence without being permitted to act on it.
What the lane does — and does not do
The COMSEC budget lane computes a traffic recommendation based on
fixed-point basis-point arithmetic applied to current traffic state.
It persists governance evidence (one sealed ActionReceipt, one
Thundervine :used lineage edge). It does not — by
design — mutate traffic, keys, sessions, or topology.
Determinism properties
The budget computation uses fixed-point basis-point arithmetic
(not floating-point) so the result is reproducible across nodes and
versions. The input payload is canonicalized deterministically before
hashing so the inputs_hash in the sealed receipt is
stable for identical inputs.
Idempotency is enforced at the database level via
pg_advisory_xact_lock on the budget key. A duplicate
computation for the same budget period acquires the same lock,
finds the existing receipt, and returns it without writing a second
row.
Evidence footprint per execution
-
One sealed
ActionReceiptwith the full 17-field seal. -
One Thundervine lineage edge with relationship type
:used, linking the budget computation receipt to the traffic-state evidence it consumed.
An auditor querying for all COMSEC budget decisions in a time window gets both the recommendation and the exact state snapshot the recommendation was based on, via the lineage edge. Nothing is implicit.
29-agent refute-by-default sweep
On 2026-07-30, Thunderline's trust spine was subjected to a 29-agent adversarial anneal with a refute-by-default mandate: each agent tried to break, bypass, or falsify some part of the receipt chain. The full run surfaced 31 real defects.
Agents
Real defects
Critical
High
Medium
Low
Defect classes and current trunk status
Seal-hash multi-dialect inconsistency
PARTLY FIXED
Multiple writers were computing seal hashes using different
serialization methods (:erlang.term_to_binary/1,
Kernel.inspect/1, canonical JSON). Only the first
three produced identical hashes for identical inputs. As of trunk,
the three main writers have been normalized to the canonical encoder.
Two open hazards remain:
thunderwall/seal.ex still uses term_to_binary/1
(non-portable across node versions);
thunderchief/receipts/agent_action_receipt.ex still
uses inspect/1 (non-deterministic for maps with
floating-point values).
CI authorization coverage
PARTLY CLOSED
The anneal found authorize?: false widespread in the
test suite, meaning test-time actions bypassed the policy gate entirely.
The lean merge gate now enforces: 5 governed-path steps with
authorize?: true plus a negative canary test that
asserts rejection. The wider test suite still has widespread
authorize?: false outside the lean gate. This is an
open coverage gap, not a production runtime gap — but it means
tests can pass while covering ungoverned paths. See also §8
non-claims.
Upserts carrying authority into ON CONFLICT
OPEN · GH #3073
Several upsert patterns were using ON CONFLICT DO UPDATE
clauses that included authority-status or state columns. This means
a race-write could overwrite a valid authority decision with a
conflicting one without triggering the lineage cross-check, because
the upsert bypasses the Ecto changeset pipeline. GH #3073 is open.
Fail-open at membranes
OPEN · GH #3081
Several membrane enforcement points returned :ok when
the boundary check raised an exception rather than returning an
explicit verdict. A boundary that fails open under error conditions
is not a boundary. GH #3081 tracks the systematic fix across all
membrane modules.
What we are not claiming
The following are verbatim non-claims. They are here because a security team doing diligence deserves to know the limits of the model before they see its strengths.
- Seals are not unforgeable (unkeyed; GH #3173).
- No formal verification claim.
- No SOC2 Type II audit. Roadmap target: Q1 2027.
authorize?: falseis still widespread in the wider test suite outside the lean merge gate.- Fail-open at membranes remains an open hazard class (GH #3081).
- Upserts carrying authority into ON CONFLICT remains open (GH #3073).
- Thunderline is not AGI. It governs agent actions, not agent reasoning.
- A legacy seal verdict means internally consistent with the encoding in force when the row was written — not verified, not authentic. It must never render as VERIFIED to an operator.
Report a security issue
If you find a vulnerability in Thunderline's receipt spine, policy gate, K1 cell, membrane enforcement, or any component described on this page, contact us directly.
Security contact
[email protected]We aim to acknowledge within 2 business days. Do not publish details publicly until we have had a chance to assess and remediate. There is no current bug-bounty program.
For general inquiries, reach Mo at [email protected]. For platform access and ThunderBlock onboarding, see thunderline.net/blockstrap.
This page is grounded in Thunderline codebase canon and adversarial anneal findings as of the review date. Claims are updated when the underlying architecture changes. Open issues are tracked in the private Thunderline repository; GH numbers reference that tracker.