# In what scenarios must an instance reject cross-origin span links in federated GenAI agent traces **Date of Record:** 2026 November 13 **Scope:** Applies to OpenTelemetry-style GenAI semantic convention span linking where an agent trace (`gen_ai.agent.*`) is assembled from spans emitted by more than one trust origin—e.g., a parent orchestrator linking `gen_ai.tool.call` spans produced by a remote sub-agent. Does not apply to single-origin traces where all spans share one emitter identity and one span-ID namespace. ## Core Distinction A span link is a *reference*, not a *parent*. The GenAI semantic conventions permit an agent span to carry `links[]` entries pointing at spans it causally influenced but does not nest under—tool calls, retrieval fetches, delegated sub-agent invocations. The non-obvious invariant is this: **link validity is a function of the target span's origin, not of the target span's existence.** In federated traces, a link target may resolve (the span ID exists in the collector) yet still be illegitimate to accept because the link crosses a trust boundary the emitter is not authorized to assert. Practitioners routinely conflate "the link resolves" with "the link is valid." In a single-origin trace these coincide. In a federated trace they diverge, and the divergence is silent: the trace assembles, the link renders, and the causal graph is wrong. ## What Was Observed During assembly of a three-party agent trace—orchestrator `O`, retrieval service `R`, and a delegated sub-agent `S` operating under a distinct tenant—`S` emitted a `gen_ai.agent.invoke` span whose `links[]` contained a `trace_id`/`span_id` pair pointing at an `R` span. The link resolved cleanly against the collector's span store. The assembled trace showed a direct causal edge `S → R` that never existed: `R` was invoked by `O`, and `S` had merely observed a cached identifier of that span in a shared context blob. The failure was not a resolution failure. It was an *authority* failure. `S`, as an emitter, asserted a causal relationship it had no standing to assert, and the assembler accepted it because it validated only referential integrity (does the span ID exist?) and never validated origin authority (is `S` permitted to link to a span in `R`'s origin?). A second observation: the same link, when `S` and `R` shared an origin, was legitimate. The identical bytes were valid or invalid purely as a function of the origin pair. Any validation that inspects the link payload alone cannot distinguish the two cases. ## Condition of Validity Accept a cross-origin link `L` from emitter origin `E` to target span `T` in origin `O` only when **all** of the following hold: 1. `E` and `O` are the same origin, **or** `O` has published an explicit link-authorization grant naming `E` (a directed, per-origin-pair allowlist—not a global "federation enabled" flag). 2. The link's `trace_id` equals the enclosing span's `trace_id`. A cross-trace link in a federated agent trace is always suspect; it is the signature of an identifier leaking across a context boundary. 3. The target span's `gen_ai.operation.name` and origin are consistent with the emitter's declared role. A sub-agent asserting a link to a retrieval span it did not invoke is a role violation even if origins are authorized. Condition 1 is the load-bearing one and the one most often omitted. Referential integrity (the span exists) is necessary but never sufficient. The absence of a grant is not a soft signal; it is a hard rejection. ## Decision Boundary The boundary is between *link acceptance* and *link quarantine*, and it is drawn on origin authority, not on resolvability: - **Same origin, resolves, same trace** → accept. - **Cross origin, no grant** → reject the link, retain the span, and emit a `link.rejected` annotation. Do **not** drop the span; dropping it destroys evidence of the emitter's overreach. - **Cross origin, grant present, same trace** → accept, but tag the link `federated=true` so downstream causal queries can weight it. - **Cross trace, any origin** → reject unconditionally. This is the only case with no grant-based override, because a cross-trace link in an agent trace indicates context contamination, not legitimate federation. Do not implement this as a warning. A rejected link that is merely logged will be re-ingested by the next assembler pass and the false edge will reappear. Rejection must be terminal for that link. ## Operational Effect Enforcing origin-authority validation changed the assembled graph materially: the spurious `S → R` edge disappeared, and the true path `O → R`, `O → S` was restored. The cost was one rejected link and one annotation per federated trace—negligible against the cost of a corrupted causal graph driving a downstream root-cause or attribution query. The invariant to carry forward: **a span link's authority is scoped to its origin pair, and an assembler that validates only span-ID resolution will silently admit forged causal edges across tenant boundaries.** ```python class UnauthorizedSpanLinkError(RuntimeError): """Raised when a cross-origin span link lacks an explicit origin grant.""" pass def validate_link(link, enclosing_span, grants): """ link: dict with trace_id, span_id, target_origin enclosing_span: dict with trace_id, emitter_origin grants: set of (emitter_origin, target_origin) authorized pairs """ if link["trace_id"] != enclosing_span["trace_id"]: # Cross-trace link in an agent trace: context contamination, no override. raise UnauthorizedSpanLinkError( f"cross-trace link rejected: link.trace_id={link['trace_id']} " f"!= span.trace_id={enclosing_span['trace_id']}" ) emitter = enclosing_span["emitter_origin"] target = link["target_origin"] if emitter != target and (emitter, target) not in grants: raise UnauthorizedSpanLinkError( f"no origin grant for {emitter!r} -> {target!r}; " f"link to span {link['span_id']} quarantined, span retained" ) return {"accepted": True, "federated": emitter != target} ```