Committed Lineage
Durable decision lineage bound tamper-evidently to the audit chain — what's provable, what's best-effort, and how to verify it. Plus W3C trace context, coverage semantics, the chain payload v2 commitment, and the canonical request-id format.
Overview
Logs tell you what happened. The audit chain proves _what_ was authorized. Committed lineage proves _why_ — the routing decision, the policy verdict, the delegation authority, and the causal trace — and binds that WHY tamper-evidently to the same hash chain that already proves the WHAT.
Two ideas carry the whole design:
- The rich WHY is durable but best-effort. Every routed decision and denial can
produce a decision_lineage row in Postgres: the routing stages, the effective strategy, the memories applied, the guardian/budget state, the W3C trace identity. This is written by a bounded async writer, so under load or crash a row can be _lost_ — but never _silently rewritten_ and never _silently manufactured_.
- The WHY is committed _to_ the chain. A single hashed field,
provenanceDigest,
is added to the audit row. It commits the deny verdict, the trace identity, and a digest of the lineage row. Editing any of the (otherwise mutable) explanation columns after the fact breaks chain verification. Suppressing a lineage row leaves a dangling _hashed_ commitment — verify reports missing_lineage/failed, never a silent NULL.
Committed lineage ships default-off. Every new behavior is gated by a flag that fails safe to off (BR_LINEAGE_PERSIST, BR_TRACE_CONTEXT, BR_CHAIN_PAYLOAD_V2). With the flags off, behavior is byte-identical to before the feature existed.
Provable vs. best-effort
The single most important distinction for an auditor or external verifier:
| Tier | What it is | Trust property |
|---|---|---|
| Provable (committed) | The deny verdict (policy_context), the trace identity, edge provenance, and the _digest_ of the lineage row — all inside the hashed provenanceDigest. | Tamper-evident by construction. A post-hoc edit breaks audit/chain/verify. |
| Best-effort (durable) | The rich lineage row itself (decision jsonb: routing stages, memories, governance flags). | Available for queries; _loss is possible and visible_. Its digest is committed, so it can't be silently altered. |
| Ephemeral (live) | The in-memory ring buffer (recent-window cache) and SSE/response headers. | Convenience only. Never presented as committed evidence (binding: "live"). |
The query surface always tells you which tier a record came from via its binding (bound | unbound | live) and, for the WHY answer, a coverage label.
Coverage semantics
GET /v1/governance/lineage/{request_id} returns a coverage enum. Because the _absence_ of a lineage row is itself committed (the lineage_digest inside provenanceDigest is either a real digest or an honest null), coverage distinguishes honest loss from suppression:
coverage | Meaning |
|---|---|
captured | The lineage row exists and verifies against its committed digest. |
aggregated | A deny-flood rollup row stands in for many identical denials (the per-event audit rows are intact). |
sampled_out | The committed lineage_digest is null on a v2 row — provably sampled out, not suppressed. |
dropped | A real lineage_digest was committed but the row is gone _within the retention window_ — honest writer drop. The commitment proves it _was_ captured then lost. |
expired_by_retention | The committed row aged past the tenant retention horizon and its partition was dropped. |
pre_rollout | A v1 audit row that predates chain payload v2 — no commitment exists to evaluate. |
The difference between dropped (a committed digest with no row in-window) and a silently missing row is the alarm: a missing row with a matching drop counter is honest telemetry loss; a missing row with no honest explanation is an integrity event.
W3C trace context
BrainstormRouter ingests, mints, stores, and propagates W3C Trace Context so a causal episode can be reconstructed across hops — and labels every edge by _who minted it_.
- Ingest (
BR_TRACE_CONTEXT=off|ingest|propagate, defaultoff): atraceparent
header is strictly parsed as 00-<32hex trace>-<16hex span>-<2hex flags>. All-zero ids and version ff are rejected; higher versions are tolerated by parsing the v0 fields. A valid header ⇒ BR adopts the trace_id and records the caller's span as parent_span_id with edge_provenance = "caller_asserted". Absent/invalid ⇒ BR mints a fresh trace with edge_provenance = "none".
- BR's own
span_idalways exists, minted for every request regardless of the flag.
The flag gates header _adoption/echo/propagation_, never id existence.
- The caller's
sampledflag is read, stored, and never trusted. Trace context is
never an authorization, routing, sampling, or rate-limit input.
- Outbound (
BR_TRACE_OUTBOUND=off|mesh|providers|all, defaultoff): BR injects
00- on mesh forwards, provider fetches, and tool/agent egress. Internal children (swarm/agentic steps) link server-side via caused_by_request_id — no header round-trip.
Edge trust labels
Every causal edge in a query carries a derived label, computed from the row's own committed fields — a caller can never upgrade it:
edge_label | When |
|---|---|
verified | Server-minted edge (swarm/agentic/mesh child span), or a caller-asserted edge corroborated by shared grant authority. |
claimed | A well-formed caller-asserted edge, uncorroborated — _regardless of caller auth status_. The common cross-hop label today. |
unverified | Anything else, including the ephemeral ring-buffer (live) records. |
Inbound header edges from any caller — including an authenticated tenant principal — cap at claimed. verified is reserved for edges BR created or can corroborate. The authority_without_causation anomaly fires when a delegated request (grant_context.depth > 0) has no trusted causal edge — and because it anchors on server-trusted lineage, omitting the traceparent header _is_ the signal, not an escape.
claimed is the common cross-hop label until grant corroboration lights up. Most external causal edges read as claims, not evidence, in the interim. Treat a claimed edge as "the caller said so", not "BR verified it".
Chain payload v2 — a note for external verifiers
Committed lineage introduces the first chain-payload version bump in the product's history. The audit row gains a chain_schema_version column (smallint, default 1).
- v1 rows verify byte-identically, forever. The v2 payload builder is dispatched by
the row's own version; mixed-version chains are a supported, tested case.
- v2 adds exactly one flat top-level field,
provenanceDigest(64-hex). The chain
payload stays flat — the field is a hex string, not nested content. The deep canonicalizer is used _only_ to compute the digest, never on the chain payload itself.
- Deploy order: the verify side ships and is confirmed in production on v1 rows
before the sign side flips (BR_CHAIN_PAYLOAD_V2, default off). Per-row versioning makes rollback safe — a sign-flag flip mid-chain still verifies every segment.
The commitment is:
provenanceDigest = sha256hex(deepCanonicalJson({
v: 1,
decision_class, // "route" | "deny" | "would_deny"
policy_context_digest, // sha256 of policy_context bytes, or null
trace_id, span_id, parent_span_id,
edge_provenance, // "server_minted" | "caller_asserted" | "none"
trace_inbound_trusted, // committed — not flippable post-write
caused_by_request_id,
lineage_digest, // sha256 of the lineage row's stored `decision` bytes, or null
request_fingerprint // HMAC-SHA256(tenant key, body) — oracle-resistant
}))
deepCanonicalJson is an RFC 8785-style deterministic encoder: recursive key sort, order-preserving arrays, and fixed number normalization (toFixed(6), mirroring the DB numeric(.,6) round-trip). Everything inside the commitment is also stored as a plain non-hashed column for queryability; the commitment makes any post-hoc edit chain-detectable.
Reproducible test vectors are published in the Decision Provenance — Chain Payload v2 spec so an external verifier can confirm it computes the identical digest BR commits.
The canonical request id (format change)
Committed lineage requires a join key that actually joins. BR now mints one canonical request id per request: req_, set at the edge and threaded to the router, the audit row, the lineage row, and the x-br-request-id response header. The same id keys all three surfaces and is the parameter to the WHY endpoint.
Migration note. Audit rows previously surfaced ids like chatcmpl_ / deny_. Those legacy ids are preserved as public_id on the lineage payload, but the primary key is now req_. Any dashboard or external system keyed on the old chatcmpl_* audit id must migrate to request_id / x-br-request-id. The hash chain covers whatever id is inserted, so no chain bump is needed — but the join key changed.
Querying lineage
Three surfaces, all requiring governance.read (API key) or the audit.read MCP permission:
# The WHY answer for one request — verified block + policy_context + lineage + coverage
curl "https://api.brainstormrouter.com/v1/governance/lineage/req_01HXXXXXX" \
-H "Authorization: Bearer br_live_..."
# Reconstruct a whole causal episode by trace id (edges labeled)
curl "https://api.brainstormrouter.com/v1/governance/lineage?trace_id=0af7651916cd43dd8448eb211c80319c" \
-H "Authorization: Bearer br_live_..."
# Verify the chain, including per-row provenance recompute
curl "https://api.brainstormrouter.com/v1/governance/audit/chain/verify" \
-H "Authorization: Bearer br_live_..."
The response of audit/chain/verify adds a per-row provenanceChecks[] entry recomputing provenanceDigest from the stored non-hashed columns + the bound lineage row's bytes — state is ok | redacted | missing_lineage | expired_by_retention | sampled_out | failed. It is best-effort and never flips the bare chain result.
SDK
const { data, responseHeaders } = await client.governance.lineageGet("req_01HXXXXXX");
console.log(data.coverage, data.verified?.provenance_check, data.anomalies);
console.log(responseHeaders.traceparent, responseHeaders.xBrRequestId);
const trace = await client.governance.lineage({ traceId: "0af7651916cd43dd8448eb211c80319c" });
for (const edge of trace.records) console.log(edge.edge_label, edge.surface);
body, headers = client.governance.lineage_get("req_01HXXXXXX")
print(body["coverage"], body["verified"]["provenance_check"], body["anomalies"])
print(headers["traceparent"], headers["x_br_request_id"])
trace = client.governance.lineage(trace_id="0af7651916cd43dd8448eb211c80319c")
for edge in trace["records"]:
print(edge["edge_label"], edge["surface"])
MCP (agent-facing)
The "why was I denied?" moment is a first-class MCP tool. An agent that gets a denial can immediately ask for the durable, tamper-evident reason:
br_why— the WHY answer for arequest_id: verified block,policy_contextdeny
reason, lineage, coverage, anomalies.
br_lineage_trace— reconstruct a causal episode bytrace_id/request_id, every edge
labeled. Both remain callable during a kill switch (read-only diagnostics).
Related
- Evidence Ledger — the signed decision-record vision.
- Observability — headers, SSE, and the routing event bus.
- Plans vs Grants vs Delegation — the authority axis
that corroborates trace edges.
- Decision Provenance — Chain Payload v2 — the normative
spec + reproducible test vectors.