Verification evidence bundles¶
A completed task's proof-of-done artefacts - test output, coverage, lint, an optional screenshot or recording - are content-addressed, sealed into a signed bundle, and anchored in a dedicated lineage spine, so the bundle a reviewer inspects is the receipt rather than a log line describing one.
Why¶
"Done" is normally a status plus scattered logs: the test-runner output, the coverage report, and any screenshot die with the worktree once the task is reaped. This module makes the artefact a reviewer consumes at review time be the proof - every producer output is stored content-addressed, bound into a signed record, and anchored in the evidence lineage spine, so a tampered evidence file is detected exactly like a tampered chain entry.
Declaring evidence producers¶
A task opts in by declaring evidence_producers in its spec - a list of producers, each with a name, a kind, a command (argv, no shell), and a required flag:
| Kind | Notes |
|---|---|
test, coverage, lint, generic | Plain producers; output is captured stdout+stderr. |
screenshot, recording | Media producers; additionally sealed with a signed C2PA content credential. |
required: true (the default) means a non-zero exit blocks the gate verdict; required: false (advisory) never blocks - a failure only attaches a failure record. A task that declares no producers is a zero-touch no-op: no gate runs, no directory is created.
Sealing (automatic, fail-open)¶
The orchestrator seals a bundle for every task that reaches the terminal completed state, right before its worktree is reclaimed (seal_evidence_on_completion in core/evidence/completion_gate.py). Each declared producer's command runs (default timeout 600s), its output is stored content-addressed and capped at 1 MiB per blob, and the items are bound into a canonical, Ed25519-signed record anchored in the evidence lineage spine (core/lineage/spine.py). The bundle is mirrored into the HMAC audit chain as an evidence-bundle event.
Sealing is deliberately fail-open: it must never block, delay, or fail a task completion. Any error raised while running or sealing producers is caught, logged, and swallowed, so completion proceeds unchanged. The gate verdict (gate_passed) is recorded in the bundle for the reviewer but is not retroactively enforced against the task the orchestrator already accepted.
The gate verdict itself is gate_passed = all(required producers passed); advisory producer failures never affect it.
Inspecting and verifying a bundle¶
bernstein evidence show <task> renders the sealed bundle: gate verdict, bundle hash, spine anchor, and a per-producer table (kind, required or advisory, pass/fail, exit code, stored size, content hash). -w/--workdir sets the project root (default .). Exit 0 when a bundle exists, 1 when there is none.
bernstein evidence verify <task> recomputes the bundle offline from the sealed record and the stored blobs alone:
- Checks the Ed25519 signature over the canonical binding.
- Verifies the evidence lineage spine and the bundle's spine anchor.
- Re-hashes every stored evidence blob against the sealed manifest - naming any item whose content diverges.
- For media items, re-verifies the signed C2PA content credential against the stored media bytes.
Exit codes: 0 verified, 1 no bundle, 2 mismatch (a tampered evidence file, bundle, or spine). bernstein audit verify runs the same integrity check across every evidence bundle in a project, alongside every other chained receipt.
Where evidence lives¶
.sdd/evidence/bundles/<task>.json # sealed bundle (one per task)
.sdd/evidence/blobs/<hex[:2]>/<hex> # content-addressed producer output
.sdd/lineage/evidence/spine.jsonl # evidence lineage spine (Merkle + HMAC)
Blob storage is idempotent (an identical blob is written once) and garbage collected: EvidenceStore.gc() removes any blob not referenced by a live bundle.
Relationship to the in-process verification gate¶
bernstein evidence show / verify is the offline-authoritative surface for a bundle; the in-process verification-gate hooks (which enforce a task's write-path allowlist from owned_files and its required evidence_producers at spawn time) are a separate, earlier-stage enforcement layer. See Hook gate.
Limitations¶
- Producer output is capped at 1 MiB per blob; output beyond the cap is truncated before it is hashed and stored (the truncation is recorded on the item, not silently dropped).
- Sealing is fail-open by design: a producer or sealing failure never fails the task completion it was meant to attach evidence to - it only means no bundle (or a partial one) exists for a reviewer to inspect.
Source¶
src/bernstein/core/evidence/bundle.py-EvidenceProducer,EvidenceBundle,EvidenceStore,build_evidence_bundle,run_evidence_gate,verify_evidence_bundle.src/bernstein/core/evidence/completion_gate.py-seal_evidence_on_completion, the fail-open orchestrator wiring.src/bernstein/cli/commands/evidence_cmd.py-bernstein evidence show/verify.