# What happens when an instance negotiates capabilities against a stale trust anchor: the silent widening of the accepted capability set **Date of Record:** 2026 November 14 **Scope:** Applies to cross-organizational agent federation where peer agents present signed capability manifests bound to a trust anchor (root key or delegated attestation) and where capability negotiation admits the intersection of declared and accepted capabilities. Does not apply to intra-organizational federation with a single administrative domain, nor to stateless capability tokens re-verified per request without cached anchor state. ## Core Distinction The failure is not a rejected handshake. It is a *successful* handshake that admits a capability the remote organization has already revoked. The distinction that matters operationally is between **anchor freshness** and **manifest freshness**. A capability manifest is signed by a key; the key is vouched for by a trust anchor; the anchor has a validity window and a revocation epoch. Most federation implementations validate the manifest's signature against the anchor as currently cached, and validate the manifest's own expiry. Almost none validate that the *anchor itself* was refreshed after the remote organization's last revocation publication. When the anchor is stale but still within its own cryptographic validity, the signature check passes, the manifest's expiry passes, and negotiation intersects against a capability set that the remote party believes it has narrowed. The local agent accepts a capability the remote party thinks it withdrew. Both sides log a successful federation event. ## What Was Observed During a cross-org rollout, Organization B revoked a `data:export:bulk` capability from a delegated sub-anchor at epoch 4471 and republished its anchor chain. Organization A's federation cache held the prior anchor chain, refreshed on a 24-hour cycle with no epoch-pinning. A negotiating agent at A presented its manifest; B's gateway validated A's manifest against B's *own* current anchor state (correct) but did not transmit its revocation epoch in the negotiation response. A then intersected B's declared capability set — which B's gateway had computed from a cached, pre-revocation view of *A's* capabilities, because the gateway's peer-capability cache for A was keyed on anchor fingerprint, not epoch — and admitted `data:export:bulk` for the session. The session succeeded. The first indication of the boundary breach was an audit reconciliation three days later: B's export logs showed bulk exports that B's policy engine had marked as revoked. Neither side's negotiation logs showed an error. The only signal was a mismatch between the epoch B believed it had published and the epoch A's session had been admitted under — a field that no component was checking because it was not in the negotiation schema. The secondary observation: the widening was *asymmetric and monotonic*. Each refresh cycle that missed a revocation epoch added capabilities rather than removing them, because the intersection was computed against the older, broader set. The breach did not oscillate; it accumulated. ## Condition of Validity This boundary condition holds — and the failure is reachable — precisely when all of the following are true: 1. Capability manifests are signed and validated against a trust anchor that is itself cached with an independent TTL. 2. The negotiation protocol exchanges declared capabilities but does **not** carry the anchor revocation epoch (or an equivalent monotonic anchor version) as a first-class field. 3. The intersection of capability sets is computed from a cached peer view keyed on identity (fingerprint) rather than on `(identity, anchor_epoch)`. 4. Revocation is expressed by republishing the anchor chain rather than by per-capability signed revocation lists that the peer must fetch. Remove any one condition and the failure mode collapses to an ordinary rejection. The dangerous configuration is the one where all four hold silently — which is the default shape of most 1.0 federation protocols, because each individual choice is reasonable in isolation. ## Decision Boundary The operative question for an agent at negotiation time is not "is this manifest valid?" but "is the anchor I am validating against at least as new as the anchor the peer is currently publishing?" If the answer cannot be established from the negotiation exchange itself, the agent must treat the peer's declared capability set as *unbounded above* and refuse to admit any capability that is not independently re-verifiable at request time. Concretely: if the negotiation response does not carry an anchor epoch, or carries one older than the local cache's last-known epoch for that peer, the correct action is to **fail closed on the widening**, not to proceed with the cached intersection. Proceeding is the boundary violation. The threshold is not a timeout or a percentage; it is a strict monotonicity check — `peer_epoch >= local_known_epoch` — where equality is the only safe pass. ## Operational Effect The observable consequence is a session that is admitted, logged as healthy, and violates the remote organization's revocation policy for the duration of the anchor TTL minus the revocation propagation delay. The window is bounded by the slower of the two caches, not by either one alone. Audit reconciliation detects it *after* the fact, because the successful-negotiation log line and the policy-violation log line live in different systems with different clocks and no shared epoch field to join on. The remediation that actually works is to add the anchor epoch to the negotiation schema and make intersection keyed on `(identity, anchor_epoch)`, so that a stale anchor produces a *cache miss* rather than a *widened intersection*. Until that schema change lands, the only safe local behavior is to re-verify any capability above a sensitivity threshold against the peer's live anchor at request time, accepting the latency cost. The following guard encodes the decision boundary as an executable check: it refuses to admit a negotiated capability when the peer's anchor epoch is behind the locally known epoch, and it treats an absent epoch as stale rather than fresh. ```python from dataclasses import dataclass class StaleTrustAnchorError(RuntimeError): """Raised when a peer's anchor epoch is behind the locally known epoch, or is absent, meaning the negotiated capability set may be widened relative to the peer's current revocation state.""" @dataclass(frozen=True) class PeerAnchorState: peer_id: str anchor_epoch: int | None # None means the peer did not publish one @dataclass(frozen=True) class NegotiatedCapability: name: str sensitive: bool def admit_capability( peer: PeerAnchorState, local_known_epoch: int, cap: NegotiatedCapability, ) -> bool: """ Admit a negotiated capability only when the peer's anchor epoch is provably at least as new as our locally known epoch. An absent epoch is treated as stale, never as fresh. """ if peer.anchor_epoch is None: raise StaleTrustAnchorError( f"peer {peer.peer_id!r} did not publish an anchor epoch; " f"cannot prove capability {cap.name!r} is not widened" ) if peer.anchor_epoch < local_known_epoch: raise StaleTrustAnchorError( f"peer {peer.peer_id!r} anchor epoch {peer.anchor_epoch} is behind " f"local known epoch {local_known_epoch}; capability {cap.name!r} " f"may be revoked at the peer" ) # Epoch is current; sensitive capabilities still require a live re-check # at request time, but negotiation admission is safe here. return True def negotiate( peer: PeerAnchorState, local_known_epoch: int, declared: list[NegotiatedCapability], ) -> list[NegotiatedCapability]: admitted: list[NegotiatedCapability] = [] for cap in declared: if admit_capability(peer, local_known_epoch, cap): admitted.append(cap) return admitted ```