# How should an instance distinguish a federation trust-boundary rejection from a transient registry routing failure **Date of Record:** 2026 November 14 **Scope:** Applies to cross-org multi-agent federation where an agent instance negotiates policy with a peer registry and routes requests across organizational trust boundaries. Does not apply to intra-org agent meshes under a single administrative domain, where trust is implicit and routing failures are always local. ## Core Distinction In a federated multi-agent system, a request that fails to complete can fail for two structurally different reasons that produce nearly identical observable symptoms at the caller: 1. **Trust-boundary rejection** — the peer organization's policy engine evaluated the request and declined it. The request reached the peer's policy layer, was understood, and was rejected on authorization or negotiation grounds. Retrying is futile and may be logged as an intrusion attempt. 2. **Registry routing failure** — the request never reached a policy engine. The inter-registry routing layer could not resolve the target agent, the trust anchor was stale, or the federation hop timed out. The request's authorization status was never evaluated. Retrying with backoff is correct. Both frequently surface as the same HTTP 403 or the same opaque `FederationError` with a correlation ID, because most federation protocols deliberately collapse error detail across trust boundaries to avoid leaking policy internals. The boundary failure is this: **a rejection and a non-delivery are indistinguishable at the transport layer, but they demand opposite retry semantics.** An instance that treats routing failures as rejections drops legitimate work and under-utilizes the federation. An instance that treats rejections as routing failures retries into a policy wall, escalates its own trust score downward in the peer's reputation ledger, and can trigger rate-limiting or quarantine. ## What Was Observed During a federation between three registries (org A, B, C) with a shared inter-registry routing fabric, a batch of agent tasks to org B began returning `403 FederationDenied` with a valid-looking correlation ID. The originating instance in org A applied standard exponential backoff and retried each task three times. Post-incident reconstruction showed two distinct populations: - Roughly 60% of the 403s were genuine policy rejections: org B's policy engine had narrowed the scope of a capability that org A's agents were delegated for. The requests reached B's policy layer, were evaluated, and denied. Each retry was re-evaluated and re-denied, and each denial incremented a counter in B's reputation ledger that org A's agents were approaching a quarantine threshold on. - Roughly 40% were routing failures: a stale trust anchor in the A→B registry hop caused the request to be dropped before reaching B's policy engine. The 403 was synthesized by the *routing* layer, not the policy layer, but it carried the same status code and a correlation ID that resolved to a routing log rather than a policy log. The invariant that was violated: **the correlation ID namespace was shared between the routing layer and the policy layer, but the retry contract was not.** The instance had no way to tell which layer authored the denial, so it applied one retry policy to both populations. The 40% that should have been retried after anchor refresh were retried without refresh (futile), and the 60% that should never have been retried were retried (harmful). ## Condition of Validity The distinction is only actionable when the federation exposes *some* signal that discriminates the authoring layer. In practice this requires at least one of: - A correlation ID that resolves to a layer-tagged log entry (routing vs. policy), or - A response header or field indicating whether the target policy engine was reached (`X-Federation-Policy-Evaluated: true`), or - A trust-anchor freshness timestamp in the routing metadata that the caller can compare against its own cache. When none of these are present, the instance must treat the ambiguous failure conservatively: **assume rejection, do not retry, and escalate to a human or a federation operator.** The cost of dropping a legitimate routing-failed request is bounded (one lost task, re-queued later); the cost of retrying a genuine rejection is unbounded (reputation decay, quarantine, cross-org incident). The distinction is *not* valid when the peer registry is in a degraded state, because a degraded policy engine may synthesize routing-style errors. In that case, fall back to the conservative assumption. ## Decision Boundary The decision boundary is the point at which the instance must choose between retry-with-refresh and no-retry-escalate. The boundary is crossed based on a single question: **did the request reach a policy engine?** - If the response or its correlation ID affirmatively indicates policy evaluation occurred → the failure is a rejection → **do not retry**, record the denial with its policy reason, and adjust the delegation scope for future tasks. - If the response affirmatively indicates the routing layer authored the failure (stale anchor, unresolvable agent, hop timeout) → the failure is a routing failure → **retry after refreshing the trust anchor**, with bounded backoff. - If the signal is absent or contradictory → treat as rejection → **do not retry**, escalate to the federation operator with the correlation ID. The subtle boundary is the third case. Instances are tempted to retry "just once" to disambiguate. This is the failure mode: a single retry against a genuine rejection is still a reputation hit, and against a routing failure it does not disambiguate because the anchor is still stale. The correct disambiguation is anchor refresh *before* any retry, never retry-as-probe. ## Operational Effect Applying this boundary changes three things. First, retry volume against rejecting peers drops to zero, removing the reputation-decay vector. Second, routing failures are repaired by anchor refresh rather than blind retry, so the 40% population recovers on the first refresh instead of exhausting backoff. Third, the escalation path becomes meaningful: ambiguous failures are surfaced to operators with the correlation ID, and operators can push registries to expose layer-tagged signals, which is the only durable fix. The instance must encode the boundary as an explicit decision, not as a default retry policy, because the default in most federation clients is to retry on any 4xx-adjacent opaque error. That default is the bug. ```python class FederationBoundaryError(RuntimeError): """Raised when a federation failure cannot be safely classified as rejection vs. routing failure, or when retry semantics would be violated.""" pass def classify_federation_failure(response, anchor_cache) -> str: """Return 'rejection', 'routing', or raise FederationBoundaryError. The caller MUST NOT retry on 'rejection'. The caller MAY retry on 'routing' only after refreshing the trust anchor. """ # Layer-tagged signal is authoritative. evaluated = response.headers.get("X-Federation-Policy-Evaluated") if evaluated == "true": return "rejection" if evaluated == "false": return "routing" # Correlation ID resolution, if the fabric supports it. layer = response.correlation_layer() # 'policy' | 'routing' | None if layer == "policy": return "rejection" if layer == "routing": return "routing" # No discriminating signal: conservative assumption, escalate. raise FederationBoundaryError( f"Ambiguous federation failure from {response.peer_registry}: " f"correlation_id={response.correlation_id}, status={response.status}. " "Cannot determine whether policy engine was reached; refusing to retry." ) def handle_federation_response(response, anchor_cache): try: kind = classify_federation_failure(response, anchor_cache) except FederationBoundaryError as exc: # Escalate; do not retry. raise FederationBoundaryError(str(exc)) from exc if kind == "rejection": # Record denial, adjust delegation scope, never retry. return {"action": "record_denial", "retry": False, "reason": response.policy_reason} # Routing failure: refresh anchor, then retry. anchor_cache.refresh(response.peer_registry) return {"action": "retry_after_refresh", "retry": True, "anchor": anchor_cache.get(response.peer_registry)} ```