# In what scenarios must an instance reject a federated capability attestation whose trust anchor chain resolves but whose policy-translation closure diverges **Date of Record:** 2026 November 8 **Scope:** Applies to cross-org multi-agent federations where agent A in org X invokes agent B in org Y under a delegated capability, and where B's authorization decision depends on a policy translated from X's namespace into Y's namespace. Does not apply to single-org agent meshes, nor to federations that share a single canonical policy engine (in which case there is no translation closure to diverge). ## Core Distinction Trust-anchor validation and policy-translation closure are two independent gates, and the common failure is treating the first as sufficient for the second. A capability attestation may carry a perfectly valid chain of signatures up to a mutually agreed trust anchor (root CA, org-level signing key, or DID document), and every link may verify, and yet the *effective permission set* that the attestation implies in the relying party's namespace may be empty, wider than intended, or contradictory once the relying party applies its own policy translation. The non-obvious boundary is this: **a resolved trust chain proves provenance, not authorization equivalence.** Capability attestation answers "did org X's agent really assert this capability?" Policy translation answers "what does that capability mean in org Y?" These are decoupled, and the federation must gate on both. The failure mode is a relying party that treats `attestation.verify() == True` as a green light and then evaluates the translated policy against a *default* or *permissive* fallback when translation produces no matching rule. ## What Was Observed A three-org federation: X (issuer of a `read:telemetry` capability), Y (relying party holding the data), Z (a policy-authority org whose translation rules Y imports). Agent A in X obtained a capability attestation signed by X's root, requested `read:telemetry` scoped to `dataset:prod-metrics`. Y verified the chain to the X↔Y trust anchor — valid. Y then ran its policy translator, which maps X's capability vocabulary into Y's local RBAC/ABAC model. The translator produced a *partial* translation: the capability name mapped to `metrics.read`, but the scope predicate `dataset:prod-metrics` had no translation rule in Y's imported Z-policy because Z's vocabulary used `resource:production.*` and the mapping table had a gap for the `prod-metrics` alias. The translator returned an empty constraint set. Y's evaluation engine, encountering an empty constraint set, treated it as "unconstrained" rather than "unmapped," and granted read access to *all* datasets. The attestation was valid; the translation silently widened scope by three orders of magnitude. A second observation: the same translator, when run against a *different* X capability (`write:config`), produced two conflicting rules because Y imported translation tables from both Z and a stale Z' mirror. The rules were `deny` and `allow` for the same predicate. Y's engine resolved conflicts by "last rule wins" on import order, making the decision non-deterministic across restarts. ## Condition of Validity The rejection rule is valid only under these conditions, and the federation must verify them before applying it: 1. The relying party performs its own translation; it does not accept the issuer's translated claims at face value. (If Y trusts X to emit already-translated claims, the closure gate moves to X and the failure mode relocates, it does not disappear.) 2. Translation is a total function over the *observed* capability vocabulary, or the translator explicitly signals partiality. A translator that returns empty-on-unmapped is the defect; a translator that raises is correct. 3. The imported policy tables are version-pinned and content-addressed. If Y imports Z-policy by mutable reference, closure cannot be evaluated deterministically. If any of these fail, the correct action is not "reject the specific attestation" but "reject the translation configuration itself" and fail the federation handshake. ## Decision Boundary An instance must reject the attestation (or halt the federation) when: - The trust chain resolves **and** the translation closure is partial: at least one capability token or scope predicate in the attestation has no total mapping in the relying party's imported policy. - The translation closure is contradictory: two or more rules with the same predicate and opposite effects, where neither is more specific. - The translation result is *wider* than the attested scope (the observed widening). Any translation that expands a scope predicate is a defect, not a feature, unless the expansion is explicitly authorized by a signed scope-widening grant from the issuer. - The imported policy tables cannot be resolved to a pinned content hash at evaluation time. An instance must **not** reject when translation is partial but the unmapped portion is provably outside the requested scope (e.g., the attestation requests only `dataset:prod-metrics` and the unmapped rule concerns `dataset:staging-*`). Closure must be evaluated relative to the *requested* scope, not the whole vocabulary — otherwise every federation fails on irrelevant gaps. ## Operational Effect The practical consequence is that federation admission becomes a two-phase gate with distinct failure semantics: trust failures are per-attestation and retryable after re-issuance; closure failures are per-configuration and require a policy-table fix plus a handshake re-run. Conflating them causes operators to rotate keys when they should be patching translation tables, and vice versa. Instrumenting the two gates separately — `trust_chain.resolve()` and `translation.closure(requested_scope)` — makes the distinction observable and lets the federation fail closed on the second gate without invalidating valid provenance. ```python from dataclasses import dataclass, field from typing import Optional class FederationClosureError(RuntimeError): """Raised when a valid trust chain yields a non-total or widening translation.""" @dataclass class Attestation: issuer: str capability: str scope_predicates: list[str] signature_chain: list[str] @dataclass class TranslationTable: version: str content_hash: str # maps (capability, scope_predicate) -> local rule or None if unmapped rules: dict[tuple[str, str], Optional[str]] = field(default_factory=dict) def resolve_trust_chain(att: Attestation, anchors: set[str]) -> bool: return bool(att.signature_chain) and att.signature_chain[-1] in anchors def check_translation_closure( att: Attestation, table: TranslationTable, expected_hash: str, ) -> dict[str, str]: if table.content_hash != expected_hash: raise FederationClosureError( f"policy table {table.version} hash {table.content_hash} " f"!= pinned {expected_hash}; closure not evaluable" ) resolved: dict[str, str] = {} for pred in att.scope_predicates: key = (att.capability, pred) if key not in table.rules: # partial closure: unmapped predicate inside requested scope raise FederationClosureError( f"no translation rule for capability={att.capability!r} " f"scope={pred!r} in table {table.version}; refusing to " f"default to unconstrained" ) local = table.rules[key] if local is None: raise FederationClosureError( f"translation for {pred!r} is explicitly null; " f"treating as deny, not as allow-all" ) # widening check: local rule must be at least as narrow as attested if _widens(local, pred): raise FederationClosureError( f"translation widens scope: attested {pred!r} -> local " f"{local!r}; requires signed scope-widening grant" ) resolved[pred] = local return resolved def _widens(local: str, attested: str) -> bool: # placeholder for a real subsumption check; conservative default = no widen return local == "*" and attested != "*" def admit_federated_call( att: Attestation, anchors: set[str], table: TranslationTable, expected_hash: str, ) -> dict[str, str]: if not resolve_trust_chain(att, anchors): raise FederationClosureError( f"trust chain for issuer {att.issuer!r} does not resolve to an anchor" ) # trust resolved; closure is an independent gate return check_translation_closure(att, table, expected_hash) ```