Trust bundles — receipts a third party re-checks by re-execution¶
Milestone 20 (specs 0131–0134, ADR 0031). One file per grounded answer;
tessera verify re-derives its verdicts offline, from the file alone.
Every Tessera answer is claims with provenance, checked by a
deterministic verifier. Until now that check was only observable where it
ran: on the machine, with the corpus, inside the engine. A trust
bundle makes it portable — a single .tsb file carrying the answer,
the evidence it stands on, and everything needed for a stranger, offline,
to run the verification again and re-derive every verdict from the
file's own content.
That last part is the point, so it is worth stating against the obvious
alternative. Signed audit trails, hash chains, and Merkle-committed logs
prove integrity: the file was not altered since sealing. They prove
nothing about whether the content was ever true — a perfectly sealed
record of a false claim seals just as well. A trust bundle carries both
layers, and tessera verify reports them separately:
| layer | question it answers | how |
|---|---|---|
| integrity | is the file the file? | recompute every leaf hash + the root (names the exact record on mismatch) |
| semantic | is the record true? | re-execute the verification: re-run the deterministic verifier and the router over the packaged evidence |
Quickstart¶
$ uv run tessera bundle "Compare Müller Logistik and Nordwind Logistik totals." \
--domain business -o answer.tsb
outcome: grounded — 3/3 claim(s) verified
root: sha256:5cc2…4903
wrote: answer.tsb (404,340 bytes)
$ uv run tessera verify answer.tsb
bundle: answer.tsb — tessera-trust-bundle v1
engine: domain business, sealed under tessera 0.0.0 (installed: 0.0.0)
integrity: intact — every leaf and the root re-computed
signature: UNSIGNED — integrity proves the file is the file, not who made it
semantic: RE-DERIVED — 3/3 recorded claim verdict(s) re-executed and matched
[ok] claim 0: supported — 'Nordwind Logistik GmbH': total net order value across 3 order(s): EUR 84,500.00.
[ok] claim 1: supported — 'Mueller Logistik Gmbh': total net order value across 5 order(s): EUR 77,500.00.
[ok] claim 2: supported — 'Nordwind Logistik GmbH' (EUR 84,500.00 across 3 order(s)) exceeds …
answer: re-derives — the packaged corpus yields exactly this answer for this question
verdict: PASS (exit 0)
No network, no engine cache, no credentials: verify reads the file,
rebuilds the claims and the graph from it, re-sums the cited rows with
the same is_supported the CI-gated eval runs, and re-routes the
question over the packaged corpus. It works identically on a machine in
airplane mode.
Reading a bundle¶
verify proves a bundle re-derives; tessera bundle explain <file>
shows a person what it says — the question, each claim with its
re-derived verdict, the evidence each claim cites (source, locator,
snippet), and, for an action bundle, the wire request with its per-slot
provenance:
$ uv run tessera bundle explain answer.tsb
verdict: PASS — RE-DERIVED
Q: Compare Müller Logistik and Nordwind Logistik totals.
[✓ re-derived] claim 0: 'Nordwind Logistik GmbH': total net order value across 3 order(s): EUR 84,500.00.
↳ salt_synthetic/I_SalesDocument.csv (table I_SalesDocument, row 50) — "Sales document 0000500050: … net value EUR 24,000.00 …"
…
evidence: 8 record(s) cited of 363 packaged in the closure
It is a read-only projection of what verify computes — the verdict is
rendered first, so a tampered bundle is shown as FAIL with the broken
claim marked UNSUPPORTED; explain never dresses a failing bundle as
sound. --json emits the structured view; --full disables truncation.
The flip-a-byte walkthrough¶
Tamper one digit of one packaged sales row that a claim cites, then re-seal — recompute the manifest and root, which is exactly what a content-blind attacker can always do to an unsigned artifact:
$ uv run python - <<'EOF'
import json
from tessera.bundle.format import seal
bundle = json.load(open("answer.tsb"))
cited = bundle["result"]["claims"][0]["support"][0]["id"]
for node in bundle["evidence_closure"]["graph"]["nodes"]:
if node["record"]["id"] == cited:
old = dict(node["attributes"])["net_amount"]
new = str(int(float(old)) + 30000) + ".00"
node["attributes"] = [[k, new if k == "net_amount" else v]
for k, v in node["attributes"]]
node["record"]["text"] = node["record"]["text"].replace(old, new)
del bundle["integrity"]
with open("tampered.tsb", "w") as out:
json.dump(seal(bundle), out, sort_keys=True, ensure_ascii=False,
separators=(",", ":"))
EOF
Integrity-only verification — the whole of what signature/hash-chain receipt systems check, packaged here as an honest foil — is satisfied:
$ uv run python scripts/foil_integrity_only.py tampered.tsb
INTACT — every hash checks out. (Nothing here checked the content.)
Re-execution is not:
$ uv run tessera verify tampered.tsb
…
integrity: intact — every leaf and the root re-computed
semantic: RE-DERIVED — 1/3 recorded claim verdict(s) re-executed and matched
[!!] claim 0: UNSUPPORTED — 'Nordwind Logistik GmbH': total net order value across 3 order(s): EUR 84,500.00.
[ok] claim 1: supported — 'Mueller Logistik Gmbh': total net order value across 5 order(s): EUR 77,500.00.
[!!] claim 2: UNSUPPORTED — 'Nordwind Logistik GmbH' (EUR 84,500.00 across 3 order(s)) exceeds …
answer: DOES NOT RE-DERIVE — the recorded claims are not the answer this packaged corpus yields …
verdict: FAIL (exit 2)
One changed digit; the exact dependent claims fail — the aggregate whose
cited rows no longer sum, and the comparison conclusion that rested
on it — while the untouched claim still re-derives. Per-claim
localization like this is the tell that verdicts are recomputed, not
pattern-matched; it is a property of the business grammar, whose
shapes recompute over the packaged graph (see the domain note under
honest limits). (Tamper without re-sealing and the integrity layer names
the exact record instead: leaf 'node:I_SalesDocument:…' does not match
its content, exit 4.)
What verify actually re-executes¶
Two semantic checks, both against the file's own content (spec 0134):
- Claim-vs-evidence re-verification. Every recorded claim is rebuilt
and re-checked with the eval's own deterministic
is_supportedunder the bundle's pinned claim grammars: aggregates re-summed over exactly the cited rows, superlatives re-ranked over the whole packaged graph, containment re-checked. The re-derived verdict is compared with the recorded one. - Answer re-derivation. The domain's deterministic router re-runs
the recorded question over the packaged graph + knowledge base and
must re-yield the recorded answer — mode, route, claim texts,
verdicts, refusal reason, and every serialized provenance field
(the comparison is over canonical bytes, so a fabricated display
pointer or
all_verifiedsummary fails too). This binds question → answer → claims to the corpus: without it, a re-sealed bundle whose claims were swapped for different, individually true claims from the same corpus would pass check 1. It is also what makes a refusal bundle re-derivable: the packaged corpus itself re-yields the refusal and its reason.
Which evidence is load-bearing depends on the domain. For the business grammar, the claim shapes recompute over the packaged graph (aggregates re-summed, superlatives re-ranked), so the graph is the re-executed evidence and per-claim failure is localized. For the lexical-route domains (devex, github_actions) the answer re-derives from the knowledge base and each claim's cited evidence text; there the graph snapshot is auxiliary, check 1 is a containment check against the cited records, and a tampered cited record surfaces as a whole-answer divergence (check 2) rather than a single localized claim. In every case a bundle whose cited evidence is absent from the packaged closure fails referential validation before re-execution — a named semantic failure, never a crash and never a pass.
Action bundles¶
A bundle can also carry an action — the wire request Tessera would send for a grounded action (a GitHub issue, a PR comment), packaged from the simulated actuator (nothing is sent):
$ uv run tessera bundle "Why did run R-1042 fail, and has this happened before?" \
--domain devex --action incident -o action.tsb
outcome: grounded — 8/8 claim(s) verified
action: incident — POST (9 grounded slot(s))
root: sha256:…
wrote: action.tsb
tessera verify then re-derives the action too, from the packaged
evidence: it re-runs the deterministic drafting-and-rendering pipeline
over the re-derived answer and requires the recorded request — the
method, the endpoint path, the entire body (every key), and the
grounded slots — to equal what the evidence produces. Because the request
is a deterministic function of the answer, this binds everything at once:
inject an extra body field (a labels or assignees the endpoint would
honour), repoint the method or path, or splice one claim's evidence under
another's section, and even after re-sealing the request no longer matches
what the evidence re-derives — exit 2, named. That is the action-layer
analogue of the flip-a-byte catch. What a re-executed action bundle proves is that every
value that would go on the wire was grounded and the request adds nothing;
it does not assert that a send happened (simulated bundles record
sent: false by construction — the one recorded real send is the
Milestone 15 receipt fixture, not a bundle).
Verdict taxonomy and exit codes¶
Degradation is always visible, never a false PASS:
| taxonomy | meaning |
|---|---|
RE-DERIVED |
re-execution ran; per-claim verdicts + recorded/derived match reported |
INTEGRITY-ONLY |
the evidence closure is not fully packaged: hashes checked, content not re-derivable — stated, not glossed |
NOT-EVALUABLE |
the installed engine cannot honestly judge: unknown domain, engine-version or claim-grammar mismatch (both sides named) |
| exit | meaning |
|---|---|
0 |
integrity intact; every verdict re-derived, matching, verified; the answer re-derives |
2 |
semantic failure: a verdict that does not re-derive, an answer divergence, a structural violation |
3 |
degraded, nothing failed: NOT-EVALUABLE / INTEGRITY-ONLY, or an honestly-recorded unverified claim |
4 |
envelope unreadable or broken: malformed file, wrong format major, any hash mismatch |
Precedence: 4 > 2 > 3 > 0. --json emits the full machine-readable
report.
The file, briefly¶
Canonical JSON (tessera-canonical-json-1 — deliberately not RFC 8785;
ADR 0031 records why, and its 2026-07-18 addendum states the number rule
an independent implementation needs: canonical bytes preserve a number's
lexical form, so 1.0 stays 1.0), sections format / engine / result /
evidence_closure / integrity / action / signature, plus anchor
reserved for transparency anchoring (a later unit). action (the wire
request) and signature (the Ed25519 signature over the root) both ship
today; only anchor remains reserved and must be null until its verifier
exists. The integrity manifest carries one leaf per packaged record,
so tampering is named, not just detected; the root is sha256 over the
sorted manifest, and the verifier also rejects any section outside the
fixed set so the root commits to the section set, not only the leaf
contents. The evidence closure is the full corpus snapshot —
whole-graph claim shapes (superlatives, comparisons) cannot be re-derived
from cited records alone, and packaging everything closes the omit-a-row
attack by construction rather than by policy. Measured sizes on the
committed corpora: business 404,340 bytes, devex 150,706,
github_actions 37,412.
Honest limits¶
- Signing binds a bundle to a key, not to an identity. A bundle may
carry an Ed25519 signature over its root (
tessera bundle … --sign, aftertessera bundle keygen; needs the optionalsignextra). Verification stays stdlib-only — a pure-Python RFC 8032 check, no extra required — so a stranger still verifies offline with nothing installed. A signature closes the re-seal gap: a content tamperer can recompute the manifest and root, but cannot produce the keyholder's signature over the new root, so a re-sealed signed bundle fails (exit 4). What a signature proves is only the holder of this key made this: the report prints the public key so you can compare it to one you trust; key distribution, transparency, and rotation are out of scope for this work.--require-signedrejects unsigned bundles for consumers whose policy demands origin. - Unsigned bundles still prove integrity and re-derivability, not origin. Anyone can produce a valid unsigned bundle over their own corpus. What no attacker can do, signed or not, is make a false claim re-derive from evidence that contradicts it.
- Verdicts are functions of the engine version. Bundles pin the
tessera version and the claim-grammar identifiers;
verifyunder a different version reportsNOT-EVALUABLEnaming both sides rather than re-deriving under a different grammar and calling it the same verdict. The identifier pin is a proxy — a grammar's body could change under a stable name at the same version — so version equality plus identifier equality together are what count as "the same grammar" (ADR 0031 §5). - A PASS is not a recency claim. A bundle re-derives against the
evidence snapshot it packages, frozen at emission — nothing binds it to
wall-clock time or to the current state of the underlying data. A
PASS says "these claims follow from this packaged evidence," not
"this evidence is still current," and there is no anti-replay binding
yet: an old bundle re-verifies forever. Trusted time and public
inclusion are the job of transparency anchoring (the reserved
anchorsection, a later unit); until then, freshness is out of scope and a consumer must not read a PASS as a currency guarantee. - The grammars are the boundary. Claims inside the engine's checkable grammars are recomputed (sums, rankings, counts, conflicts); anything else falls to normalized containment against the cited records. This is not a "verify any LLM output" tool — that constraint is what makes third-party re-execution possible at all.
- Two questions with the same answer are interchangeable. If the router yields the identical answer for two questions, a question swap between them is undetectable — and the record remains true: the packaged corpus genuinely gives that answer to the recorded question. Nothing false can be attested this way.
- Domains that share a router and grammars are verification-equivalent.
The
domainlabel selects which router and claim grammars re-derive the answer; two domains with the same router and the same grammars will both re-derive the same answer over the same corpus, so relabelling between them is a cosmetic mislabel, not a false attestation — the packaged evidence (its source paths) is the ground truth and is inspectable. A domain whose grammars differ (e.g. business, with six shapes) is not interchangeable and degrades toNOT-EVALUABLE. - A degraded bundle is a visible downgrade, not a bypass. Editing the pins or the closure kind and re-sealing yields exit 3, never a PASS.
- The graph is auxiliary for lexical-route domains. As noted above, a devex or github_actions answer re-derives from the knowledge base and the cited evidence text, not the graph; the packaged graph for those domains is context, not the load-bearing evidence. The docs above say which is which rather than implying the graph is always what gets re-executed.
- Check 2's guarantee for non-grammar claims rests on the router echoing cited evidence into the answer. On all three committed corpora every cited-record tamper surfaces into the re-derived answer (so it is caught); a future domain whose router summarized rather than quoted its evidence would need its own claim grammar to keep that guarantee — a claim outside a checkable grammar cannot ship a stronger promise than containment.
- The cryptographic envelope here (hashing, manifests, roots) is standard machinery, not the contribution; the re-execution of claim-vs-evidence verification from the packaged closure is the part we have not seen elsewhere (the prior-art record lives with the Milestone 22 write-up).