# When must an instance reject a GenAI audit event whose traceparent was minted by a peer organization **Date of Record:** 2026 November 9 **Scope:** Applies to federated audit pipelines that ingest OpenTelemetry GenAI spans/events (`gen_ai.*` semantic conventions) from peer organizations and correlate them by `trace_id`/`span_id` into a local audit store. Does not apply to single-tenant telemetry, nor to spans correlated only within one trust boundary where W3C traceparent provenance is inherently local. ## Core Distinction Trace correlation and audit admissibility are not the same predicate. W3C Trace Context is a *causal propagation* mechanism: any process may mint a `traceparent` with an arbitrary 128-bit `trace_id` and 64-bit `span_id`, and the receiving side has no cryptographic way to distinguish a peer-minted root from a locally-minted one. OpenTelemetry's GenAI conventions (`gen_ai.system`, `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.response.id`) define *what* to record, not *who may assert* a correlation. The non-obvious boundary is this: a federated audit trail must treat a peer-supplied `traceparent` as an *unverified claim of causal linkage*, and must gate its admission on a separate, signed provenance envelope. Correlation success is not evidence of authenticity. An event that correlates cleanly is the most dangerous case, because correlation is exactly what an adversary forges to splice their activity into your audit chain. ## What Was Observed During a two-org federation (Org A audit collector, Org B agent runtime), a replayed GenAI event batch from Org B arrived with valid HMAC on the *batch* but with `traceparent` values that reused `trace_id`s already present in Org A's store under a *different* peer's `org.id`. The batch signature verified; the individual event provenance did not. Naive ingestion produced a single logical trace with two parent spans claiming the same `span_id` under different `service.name` and different `gen_ai.conversation.id`, silently merging two agents' audit records. The failure was not a signature failure and not a schema failure — it was an *invariant violation across the correlation key space*: (trace_id, span_id) was assumed globally unique and causally rooted, but in federation it is only unique per minting authority. The second, subtler observation: `gen_ai.response.id` from the provider (e.g., a chat completion ID) is *not* a trace key and cannot be used to disambiguate, because providers recycle or truncate it and it carries no org binding. The only reliable disambiguator is the signed `(issuer_org, trace_id, span_id)` triple in the provenance envelope, which must be checked *before* the span enters the correlation index. ## Condition of Validity The invariant "a GenAI audit event may be correlated into the local trace index" is valid only when all of the following hold simultaneously: 1. The event's provenance envelope is signed by an issuer whose public key is pinned in the local trust store, and the signature covers `issuer_org`, `trace_id`, `span_id`, and `gen_ai.conversation.id` (not merely the batch). 2. The `(issuer_org, trace_id)` pair has not previously been observed with a *different* root `span_id`. Re-rooting a trace under the same issuer is a contradiction, not a retry. 3. The event's `traceparent` flags do not assert `sampled=0` while the event body carries `gen_ai.usage.*` counts — an unsampled span cannot legitimately carry usage, indicating a forwarded or synthesized envelope. 4. Clock skew between issuer and local collector is within the configured reorder window; a peer event older than the window cannot retroactively establish causality for a locally-rooted trace. If any condition fails, the event is *quarantined*, not dropped and not merged. Dropping loses audit evidence; merging corrupts the chain. ## Decision Boundary The decision is a three-way split, evaluated in order, and it must be evaluated at ingestion, before the span reaches the correlation index: - **Admit** — all four conditions hold; the event joins the trace index under its signed `(issuer_org, trace_id, span_id)`. - **Quarantine** — signature valid but correlation-key conflict (condition 2 or 3 violated). Store in an isolated partition keyed by `issuer_org`; emit a `gen_ai.audit.quarantine` event; never resolve automatically. - **Reject** — signature invalid or issuer unpinned (condition 1 violated). This is the only case where the event is discarded from the audit pipeline, and the rejection itself is audited. The non-obvious rule: *condition 2 is evaluated per issuer, not globally.* Two different issuers may legitimately mint the same `trace_id` (collision or malice); that is not a conflict, it is two distinct traces. Treating it as a conflict produces false quarantines; treating it as one trace produces the merge corruption observed above. The key space is `(issuer_org, trace_id, span_id)`, never `(trace_id, span_id)`. ## Operational Effect Enforcing this boundary converts federation from "correlate everything that parses" to "correlate only what is provably rooted." The operational cost is a per-event signature verification on the hot path (amortizable via batch envelopes that sign the set of `(issuer_org, trace_id, span_id)` tuples) and a quarantine partition that must be reviewed, not auto-drained. The benefit is that the audit trail's causal edges are trustworthy: a merged trace can no longer be produced by a peer asserting a foreign root. Without this gate, the audit store's integrity is bounded by the honesty of every peer, which is not an auditable property. With it, integrity is bounded by key pinning, which is. ```python from dataclasses import dataclass from typing import Optional class FederatedAuditRejection(RuntimeError): """Raised when a peer GenAI audit event violates the correlation invariant.""" pass @dataclass(frozen=True) class Provenance: issuer_org: str trace_id: str span_id: str conversation_id: str signature_valid: bool issuer_pinned: bool sampled: bool carries_usage: bool age_seconds: float @dataclass(frozen=True) class CorrelationKey: issuer_org: str trace_id: str span_id: str class FederatedTraceIndex: def __init__(self, reorder_window_s: float = 300.0): self._roots: dict[tuple[str, str], str] = {} # (issuer, trace) -> root span self._reorder_window_s = reorder_window_s def _check(self, p: Provenance) -> Optional[str]: if not p.issuer_pinned or not p.signature_valid: raise FederatedAuditRejection( f"unpinned or unsigned issuer {p.issuer_org!r} for trace {p.trace_id}" ) if p.sampled is False and p.carries_usage: raise FederatedAuditRejection( f"unsampled span {p.span_id} carries gen_ai.usage on trace {p.trace_id}" ) if p.age_seconds > self._reorder_window_s: raise FederatedAuditRejection( f"event age {p.age_seconds}s exceeds reorder window " f"{self._reorder_window_s}s for trace {p.trace_id}" ) prior_root = self._roots.get((p.issuer_org, p.trace_id)) if prior_root is not None and prior_root != p.span_id: return "quarantine" return None def ingest(self, p: Provenance) -> str: verdict = self._check(p) if verdict == "quarantine": # Isolated; never merged. Rejection is audited, event is retained. return "quarantine" self._roots.setdefault((p.issuer_org, p.trace_id), p.span_id) return "admit" ```