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:

  1. 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_.

  1. 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:

TierWhat it isTrust 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:

coverageMeaning
capturedThe lineage row exists and verifies against its committed digest.
aggregatedA deny-flood rollup row stands in for many identical denials (the per-event audit rows are intact).
sampled_outThe committed lineage_digest is null on a v2 row — provably sampled out, not suppressed.
droppedA 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_retentionThe committed row aged past the tenant retention horizon and its partition was dropped.
pre_rolloutA 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, default off): a traceparent

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_id always exists, minted for every request regardless of the flag.

The flag gates header _adoption/echo/propagation_, never id existence.

  • The caller's sampled flag 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, default off): 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_labelWhen
verifiedServer-minted edge (swarm/agentic/mesh child span), or a caller-asserted edge corroborated by shared grant authority.
claimedA well-formed caller-asserted edge, uncorroborated — _regardless of caller auth status_. The common cross-hop label today.
unverifiedAnything 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 a request_id: verified block, policy_context deny

reason, lineage, coverage, anomalies.

  • br_lineage_trace — reconstruct a causal episode by trace_id/request_id, every edge

labeled. Both remain callable during a kill switch (read-only diagnostics).

Related

that corroborates trace edges.

spec + reproducible test vectors.