# Under what conditions must an instance refuse to propagate a traceparent across an organizational handoff **Date of Record:** 2026 November 14 **Scope:** Applies to distributed trace correlation when a span originating in one trust domain (org A) is handed to an agent or service operating in a different trust domain (org B), where the trace context is carried via W3C `traceparent`/`tracestate` headers or equivalent. Does not apply to intra-organizational handoffs, nor to handoffs where both parties share a single signing authority for trace context. ## Core Distinction The non-obvious failure is not "trace context is lost." Loss is loud and recoverable. The boundary failure is **trace context is silently accepted and re-emitted under a foreign trust root**, producing a trace that appears complete (single trace ID, contiguous parent chain, plausible timing) but whose causal claims are unverifiable because the `traceparent` was minted by an authority the receiving side cannot authenticate. There are two distinct operations that are routinely conflated: 1. **Correlation** — the ability to join spans by trace ID for observability within a domain that vouches for the ID. 2. **Attestation** — the ability to assert that a given `traceparent` was produced by the party it claims to represent, at the time it claims. Across an organizational boundary, org B can almost always perform (1) and almost never perform (2) without an out-of-band trust anchor. Accepting a foreign `traceparent` for *correlation* is fine. Accepting it as *attestation* — e.g., feeding it into an audit trail, a billing ledger, or a cross-agent causal graph used for debugging — is the invariant violation. ## What Was Observed In a multi-agent handoff where agent A (org A) delegated a subtask to agent B (org B) via a tool-call envelope, the envelope carried `traceparent: 00---01` with the `sampled` flag set. Agent B correctly parented its spans to the received `span-id` and returned a completed sub-trace. Downstream, org A's collector stitched the two into one trace. Three defects surfaced: - **Span ID collision under adversarial replay.** Because org B does not validate that `` is globally unique against org A's minting namespace, a malicious or buggy org B could reuse a `span-id` that org A had already emitted elsewhere in the same trace. The collector's parent-child index silently overwrote the earlier edge. The trace remained *structurally valid* — one root, no cycles — but the causal edge was wrong. - **Clock skew masquerading as causality.** Org B's spans carried timestamps from a clock with no shared time source. The merged trace showed a child span *ending before* its parent began. Collectors that tolerate negative durations (many do, by design, to absorb skew) accepted this; the resulting trace looked plausible but encoded a causality claim that was physically impossible. - **`tracestate` mutation.** Org B appended vendor keys to `tracestate` and, in one path, truncated it past the 32-entry / 512-byte limit, evicting org A's keys. Org A's downstream sampler then made a sampling decision without the context it thought it had. None of these produced an error. All three produced a trace that *looked* correct. ## Condition of Validity A foreign `traceparent` may be propagated and joined into a shared trace **if and only if** at least one of the following holds: - The receiving domain has an out-of-band trust anchor tying the claimed originating authority to the `trace-id` (e.g., a signed handoff envelope, or a pre-shared trace-context signing key per W3C Trace Context Level 2 semantics). - The trace is explicitly marked as *correlation-only* — used for joining, never for attestation — and the consumer is configured to treat foreign spans as untrusted for causal or billing purposes. - The handoff is wrapped in a boundary envelope that re-mints a fresh local `traceparent` (new `trace-id` or a documented link via `tracestate` `link` key) so that the foreign context is a *reference*, not a *parent*. If none hold, the instance must **not** parent foreign spans directly into the local trace, and must **not** emit a merged trace that implies attestation. It may emit a linked trace with an explicit boundary marker. The critical invariant: **a span's parent pointer is an attestation claim.** Joining a foreign span as a child of a local span asserts "this work was caused by that work, under our authority." That assertion is false across a trust boundary without a trust anchor. ## Decision Boundary The decision to refuse propagation is triggered by the conjunction: ``` foreign_traceparent_present AND NOT trust_anchor_verifies(origin_authority, trace_id) AND consumer_requires_attestation(trace) ``` Note the third clause. If the consumer only needs correlation (dashboards, latency histograms), propagation is safe and refusal would be pedantic. The refusal is mandatory only when the trace feeds a system that treats parentage as fact: audit logs, billing attribution, cross-agent causal replay, or incident post-mortems where "which agent caused this" is load-bearing. A second, narrower trigger: **`tracestate` integrity failure on ingress.** If the received `tracestate` exceeds limits or was truncated such that the local domain's keys are absent, the instance must not silently re-emit — it must either drop `tracestate` and flag, or refuse the join. Silent eviction is the failure. ## Operational Effect Refusing direct parentage and falling back to a linked, boundary-marked trace costs you a single trace tree. It buys you a causal graph whose edges are true. In practice: - **Cost:** Dashboards that assumed one trace per user request now show two (or a tree plus a link set). Tooling must understand `link` semantics. This is a real integration cost and is why the boundary is routinely violated. - **Benefit:** Post-mortems stop producing phantom causality. Billing attribution stops leaking across orgs. Replay-based debugging can distinguish "org B's span was a child" from "org B's span was correlated." - **Failure mode if ignored:** The trace is *more* dangerous than no trace, because it is confidently wrong. A missing trace triggers investigation; a plausible-but-forged trace terminates it. The operational rule: **correlate freely, attest never, without a trust anchor.** Treat the `traceparent` header as a hint on ingress and as a claim on egress. The asymmetry is the whole discipline. ```python class TraceBoundaryViolation(RuntimeError): """Raised when a foreign traceparent would be joined as attestation without a trust anchor.""" pass def join_foreign_span( traceparent: str, origin_authority: str, trust_anchor_verifies: bool, consumer_requires_attestation: bool, ) -> dict: if not traceparent or not traceparent.startswith("00-"): raise TraceBoundaryViolation(f"malformed traceparent: {traceparent!r}") parts = traceparent.split("-") if len(parts) != 4: raise TraceBoundaryViolation(f"traceparent must have 4 fields: {traceparent!r}") _, trace_id, span_id, flags = parts if consumer_requires_attestation and not trust_anchor_verifies: # Refuse direct parentage. Emit a link, not a child edge. return { "mode": "link", "trace_id": trace_id, "linked_span_id": span_id, "origin_authority": origin_authority, "attestation": "unverified", "reason": "foreign_traceparent_without_trust_anchor", } return { "mode": "child", "trace_id": trace_id, "parent_span_id": span_id, "origin_authority": origin_authority, "attestation": "verified" if trust_anchor_verifies else "correlation_only", } def enforce_tracestate_integrity(tracestate: str, local_keys: set[str]) -> str: entries = [e.strip() for e in tracestate.split(",") if e.strip()] if len(entries) > 32 or len(tracestate.encode("utf-8")) > 512: raise TraceBoundaryViolation("tracestate exceeds W3C limits on ingress") present = {e.split("=", 1)[0] for e in entries if "=" in e} missing = local_keys - present if missing: raise TraceBoundaryViolation( f"local tracestate keys evicted by foreign authority: {sorted(missing)}" ) return tracestate ```