Expand description
Evidence bundle export (ticket .kranz/tickets/evidence-bundle-export.md,
KRZ-326 — the governance evidence layer’s packaging step): assemble ONE
mission’s portable audit package — inputs, gate results, diffs, reviewers,
escalations, cost, and the provenance chain — self-contained and suitable
for handing to an auditor who has no access to the repo.
The bundle is a plain DIRECTORY, not an archive:
<out>/
manifest.json — machine index: every entry with its sha256 + source
summary.md — the human-readable audit summary
chain.json — the provenance chain (provenance-replay's machine form)
escalations.json — the mission's escalation-ledger rows
cost.json — the mission's cost fold
events.jsonl — the raw (already-scrubbed) event log, verbatim
artefacts/… — bytes for every resolvable `file:` artefact ref,
plus the well-known mission documents (plan, report)WHY a directory and not a .tar: neither tar nor zip is anywhere in
the dependency tree, and the ticket blesses the directory form — it is
also the MORE auditable container: every entry greps, diffs, and opens in
any tool with no extraction step, and there is no archive metadata
(mtimes, uid/gid, ordering) whose normalization would be a second
determinism surface. Determinism is therefore ENTRY identity: the same
log yields the same (relative-path → bytes) set, byte for byte. Nothing in
assembly consults a clock, a hash map, or a host path — the log’s own
event timestamps travel as DATA (escalation rows), which is exactly what
“same log → same bundle” requires.
The substrate’s own rules, kept:
- Everything derives from the already-scrubbed log. The bundle never
reintroduces scrubbed values:
events.jsonlcrossed the redact-at-write boundary when it was appended, and every derived file folds FROM it. A log carryingsecret.redactedaudits yields a bundle with fingerprints only (test-pinned). Artefact bytes are the one half that did NOT cross a write boundary the engine controls — they are ordinary files in a worker-writable tree — so [read_artefact] scrubs them here, as text, and the manifest digests the redacted form (audit H5). - Missing evidence is named, never omitted and never an error. A
file:reference whose bytes are gone (a cleanedruns/, a pruned mission) becomes a manifest entry markedunresolvedcarrying the original reference — the same total-classifier discipline ascrate::gate_results::resolve_artefact. - No host paths. References stay mission-relative; the absolute path
the resolver probed never crosses into the bundle (the same reason the
provenance chain records only the classification — a host path would
leak the machine layout into the audit record). Bundle-relative paths
are always
/-joined so the package is host-platform neutral. - Read-only against the mission dir; the write target is outside it.
No lock (§4.3 read-only observers);
export_evidence_bundlerefuses an--outinside the mission dir before writing anything.
WHY the raw log ships beside the folds: the chain, escalations, and cost
are all pure folds of events.jsonl; an auditor with no repo access can
only RE-CHECK that claim if the primary record is in the package. The log
is the one entry that is never unresolved — a mission without its log is
not a mission (the CLI’s require_mission rule), so a missing/unreadable
log fails the export outright.
Structs§
- Bundle
File - One bundle payload: a
/-joined bundle-relative path and its bytes. Logical paths (never host paths), so the in-memory form is already platform-neutral. - Evidence
Bundle - The assembled bundle: the manifest plus every NON-manifest file’s bytes,
in write order.
manifest.jsonitself is serialized at write time (it cannot list its own hash). Held in memory so two assemblies can be compared for byte identity before anything touches disk. - Evidence
Manifest - The machine index (
manifest.json): every bundle entry with its sha256 and source reference, in bundle order — generated files first (fixed order), then artefacts in first-appearance order across the chain (gates, then sessions, then the well-known documents), each unique reference appearing exactly once. - Export
Outcome - What
export_evidence_bundlewrote, for the CLI’s one-line report. - Manifest
Entry - One row of the machine index.
path/sha256are absent exactly when the entry is an unresolved artefact — there are no bytes to point at, and a fabricated path would be a lie. - Mission
Cost Summary - The mission’s cost fold, bundled (
cost.json). All fields come fromcrate::outcomes::mission_outcomes— the same fold the flight-surgeon surfaces use, so the bundle can never disagree with them.
Enums§
- Entry
Kind - What one manifest entry is. Serde lowercase (the
ArtefactStatusidiom).
Constants§
- ARTEFACTS_
DIR - BUNDLE_
FORMAT_ VERSION manifest.json’sversionfield: the bundle format version. Bump on any layout/schema change so a reader can tell what it is holding.- CHAIN_
FILE - COST_
FILE - ESCALATIONS_
FILE - LOG_
FILE - MANIFEST_
FILE - SUMMARY_
FILE
Functions§
- assemble_
evidence_ bundle - Assemble one mission’s evidence bundle in memory. Read-only against the mission dir (no lock — §4.3 read-only observers), no clock, no network, no git: the same log and artefact bytes always assemble the same bundle.
- export_
evidence_ bundle - Assemble + write the bundle, with the one placement rule enforced: the write target must be OUTSIDE the mission dir (a bundle written into the tree it audits would both mutate the read-only surface and risk shipping itself as evidence).
- write_
evidence_ bundle - Write an assembled bundle to
out_dir, returning the number of files written (includingmanifest.json). The directory must not already hold anything: silently mixing two exports would leave stale artefacts no manifest entry names — the same honesty discipline as unresolved entries. The out dir is created and written through a pinned no-follow capability ([plan_out_dir] / [pin_out_dir]): a symlinked existing component is refused, and nothing written ever crosses a symlink.