Lineage v1 - user reference
August 30, 2026 · View on GitHub
Lineage v1 is Bernstein's per-artefact transparency log. Every agent write to a tracked file produces an append-only, content-addressed, Ed25519-signed entry that an external auditor can verify without any Bernstein install.
This page is the user-facing reference. For the design rationale see ADR-009.
TL;DR
| Question | Answer |
|---|---|
| What gets logged? | Every agent write to a tracked file or config. |
| How is an entry keyed? | By an artifact key: a repo-relative path, or a pr:// / pkg:// / deploy:// / doc:// URI. |
| Where does it live? | .sdd/lineage/log.jsonl (append-only) + per-entry detached JWS sidecars. |
| Who signs? | The agent itself, with an Ed25519 key issued at spawn time. |
| Who verifies? | Anyone with bernstein-verify (a standalone PyPI wheel). No Bernstein install required. |
| What gets shipped to auditors? | bernstein compliance pack - a single self-contained ZIP. |
Quickstart
Enable lineage
Lineage is on by default (since 1.10.9). To confirm:
bernstein config get lineage.enabled
# true
By default lineage.strict is false, so lineage failures warn but don't
block writes:
bernstein config get lineage.strict
# false
Once your team is confident, flip to strict:
bernstein config set lineage.strict true
View the log
# Full chain for one artefact
bernstein lineage chain src/payments/flow.py
# Walk the ancestry of one artefact
bernstein lineage walk src/payments/flow.py
# Active forks (none should exist on a clean main)
bernstein lineage forks
Generate a compliance pack
bernstein compliance pack \
--since 2026-01-01 --until 2026-03-31 \
--org "Acme Bank" \
--output ./acme-q1-evidence.zip
Pack contents:
| File | Purpose |
|---|---|
README.md | Cover page. |
article12-evidence.csv | Machine-readable: every artefact write, agent, ts, content hash. |
lineage-log.jsonl | Raw log slice for re-verification. |
signatures/ | Per-entry detached JWS. |
agent-cards/ | A2A v1.0 Agent Cards seen during the window. |
pack-manifest.json | Provenance: who packed, when, hashes. Operator-signed. |
verify-instructions.md | One-pager for the auditor. |
Auditor flow
The auditor never has to install Bernstein. They install one tool:
pipx install bernstein-verify
Then point it at the pack:
bernstein-verify pack ./acme-q1-evidence.zip
Exit code semantics:
| Exit | Meaning |
|---|---|
| 0 | All signatures valid, chain complete, no unresolved forks. Pack PASSES. |
| 1 | Any failure. Human summary on stdout, structured JSON on stderr. |
Sub-commands:
| Command | Purpose |
|---|---|
bernstein-verify pack <zip> | Full pack verification end-to-end. |
bernstein-verify chain <path> --lineage-dir DIR | Single artefact chain. |
bernstein-verify forks <path> --lineage-dir DIR | Report unresolved forks (CI use). |
The verifier is air-gap-safe - no network calls, no remote registry
lookups. Every public key it needs is in agent-cards/ inside the pack.
Article 12 paragraph mapping reference
EU Regulation 2024/1689 Article 12 paragraph numbers → what in the pack satisfies them.
| Paragraph | Obligation | Pack artefact | Pack location |
|---|---|---|---|
| 12(1) | Automatic recording over lifetime. | The lineage log itself. | lineage-log.jsonl |
| 12(2)(a) | Traceability appropriate to intended purpose. | Each entry carries artefact_path, content_hash, agent_id, ts_ns. | lineage-log.jsonl + signatures/ |
| 12(2)(b) | Identification of risk situations / substantial modifications. | Entries whose narrative changes risk parameters. | article12-evidence.csv (filter on artefact_path) |
| 12(2)(c) | Post-market monitoring (re. Article 72). | Cross-link via tool_call_id to audit log. | lineage-log.jsonl (tool_call_id column) |
| 12(2)(d) | Monitoring per Article 14(3) human oversight. | Reviewer-agent entries. | lineage-log.jsonl (filter on agent_id) |
| 12(3) | At least 6-month retention. | Pack itself is a retainable artefact; cold-storage path documented. | Pack ZIP + retention chain. |
For Annex IV (technical documentation) and Article 11 (technical
documentation obligations), see the healthcare demo:
examples/lineage/healthcare/article12-mapping.md.
For the 10-year cold-storage round-trip path used in industrial-machinery
contexts:
examples/lineage/eu-manufacturer/cold-storage-roundtrip.md.
Demo bundles
Three reference scenarios ship under examples/lineage/:
| Demo | Story | Path |
|---|---|---|
| Fintech | SOC2 + 4 agents editing payments code; rogue parallel-edit detection. | examples/lineage/fintech/ |
| Healthcare | HIPAA + EU AI Act Article 11 + 12 over a triage config. | examples/lineage/healthcare/ |
| EU manufacturer | Annex III high-risk machinery; 10-year cold-storage round-trip. | examples/lineage/eu-manufacturer/ |
Run any of them:
make -C examples/lineage demo-fintech
make -C examples/lineage demo-healthcare
make -C examples/lineage demo-eu-mfg
Foreign attestations
An external attestation is typed metadata beside a lineage record, never an
entry in Bernstein's HMAC chain. The first verifier slice accepts a
protocol-neutral envelope containing an issuer, issuer key id, content hash,
claimed subject, trust class, format, and payload hash. It preserves the
foreign trust class and reports a structurally valid envelope as
unverifiable until an issuer- and format-specific verifier is available.
unverifiable does not mean Bernstein rejected the foreign claim, and it
does not mean Bernstein verified it. A malformed envelope fails closed at
public trust. Unknown formats remain unverifiable rather than being
parsed as Bernstein data. Neither result changes whether Bernstein's own
lineage chain verifies or upgrades a foreign claim into a Bernstein finding.
Failure modes you should know
| Symptom | Likely cause | Action |
|---|---|---|
bernstein-verify pack exits 1 with "fork detected" | Two agents wrote the same artefact in parallel without a Steward merge. | Run bernstein lineage merge <path> to record a merge entry; re-pack. |
verify exits 1 with "invalid signature" | The log was edited after the fact, or the Agent Card was swapped. | Treat as a security incident; do not ship the pack. |
verify exits 1 with "kid binding cannot be established" | No Agent Card on disk matches the (agent_id, agent_card_kid) the entry signed - typically a card for the entry's key id is missing after a rotation. | Restore the card for that key id under the per-kid layout (see Key rotation); re-verify. |
verify exits 1 with "kid binding mismatch" | An entry's signed body names one key id while its JWS header names another - a key-substitution attempt. | Treat as a security incident; do not ship the pack. |
verify exits 1 with "HMAC mismatch" | Operator HMAC key rotated mid-window. | Re-pack with the correct key context; consult ADR-009 §6. |
bernstein lineage gate reports "non-canonical line bytes" | A log.jsonl line's raw bytes differ from the canonical form the writer emits - reordered keys, inserted whitespace, or a stray \r. A generic JSONL tool or a hand-edit that pretty-printed or reformatted the log triggers this even when the field values are unchanged. The gate binds verification to the on-disk bytes, so a non-canonical rewrite is rejected rather than silently re-canonicalised. | The log is the provenance anchor and is never edited in place by Bernstein. Restore log.jsonl from a trusted copy and re-run; if no trusted copy exists, treat as a tamper incident. |
bernstein lineage gate reports "missing trailing newline" | The final record was truncated or its terminating \n was stripped/flipped at EOF (e.g. an editor that drops the trailing newline, or a partial write). | Restore the full log from a trusted copy; a truncated final record is tamper-evidence. |
Genesis entry shows up with parent_hashes: [] | First-time write of a file that existed before lineage was enabled. | Expected - see ADR-009 §11 on bootstrap. |
on_fail recovery receipts
When a workflow DSL DAG routes control to a recovery node because an upstream
node failed under a guard (the fix-bugs depends-on run-tests with
condition: "status == 'failed'" pattern), the recovery task carries a
lineage-attested failure receipt rather than a bare description.
The receipt is the recovery task's primary artefact. It captures the failure the run already observed:
| Field | Source |
|---|---|
source_status | The failing task's terminal status. |
condition_context | The guard evaluation context (status / result / output). |
gate_report | The failing task's quality gate findings. |
journal_tail | The tail of the run's Merkle event journal, filtered to the failing task and stripped of wall-clock envelope. |
The canonical, sorted-key serialization is content-addressed. Recording it on
the run's lineage spine returns a Merkle-chained, HMAC-tagged entry hash, which
is stamped on the recovery task (metadata.recovery_receipt_hash) and injected
into the recovery agent's prompt. The recovery agent continues from the
captured failure instead of rediscovering it.
Because the receipt bytes are the spine entry's content, the anchored entry binds cryptographically to the exact failure summary. Verify that a recovery task's receipt resolves to a valid spine entry:
# Resolve the embedded receipt hash against the run's spine.
bernstein lineage verify <run_id> --receipt-hash sha256:<entry-hash>
# Also content-address the receipt artefact against the anchored entry.
bernstein lineage verify <run_id> \
--receipt-hash sha256:<entry-hash> \
--receipt-file .sdd/lineage/receipts/<content-hash>.json
Exit codes: 0 the receipt resolves on an intact chain (and, with
--receipt-file, its content matches the anchored entry); 2 it does not;
3 the audit key is missing so the chain cannot be verified. Verification
loads the HMAC key read-only and never mints one -- pass --key-path to point
at the key the chain was written under.
Guarantees:
- Determinism. Two runs over identical fixtures produce a byte-identical receipt content hash and an identical spine entry hash.
- Tamper evidence. Mutating any receipt field changes the content hash, so it no longer matches the anchored entry; rewriting the spine row to cover it up breaks the HMAC chain, which needs a key an editor does not have.
- Isolation. The receipt's
artifact_pathstays repo-relative (.sdd/lineage/receipts/<content-hash>.json).
See on_fail recovery receipts (workflow DSL) for the DAG authoring side.
Key rotation
Each lineage entry signs the key id (agent_card_kid) it was produced under, and the gate verifies the signature against the Agent Card for that exact (agent_id, kid) pair - not against whatever card currently sits at the agent id. This keeps historical entries verifiable after an agent rotates its key.
The gate reads two on-disk Agent Card layouts:
| Layout | Path | Use |
|---|---|---|
| Single-card | <cards-dir>/<agent-id>/card.json | One key per agent (default). |
| Per-kid | <cards-dir>/<agent-id>/<kid>/card.json | Multiple historical keys for one agent, one card per key id. |
To rotate a key without invalidating prior entries, keep the old card and add the new one under the per-kid layout:
.sdd/agents/
agent:claude-worker-3/
k-2025-01/card.json # old key - retained so old entries still verify
k-2025-06/card.json # new key - signs entries from the rotation onward
Entries signed under k-2025-01 continue to verify against the retained card; new entries under k-2025-06 verify against the new one. Removing a card for a key id that historical entries still reference makes those entries fail the gate with a kid-binding error.
See also
- Artifact keys - the URI grammar, external-artifact anchoring, declared outputs, the
artifact list|log|healthsurfaces, and production events. - ADR-009: Lineage v1 - design rationale, schema, threat model.
- Compliance - EU AI Act Article 12 bundle - the bundle format detail.
- Regulatory lineage export - operator export guide.
- A2A v1.0 Agent Card spec; RFC 7515 JWS; RFC 8785 JCS.