# What happens when an instance accepts a capability grant whose trust anchor was rotated mid-negotiation **Date of Record:** 2026 November 10 **Scope:** Applies to federated multi-agent systems where capability grants are validated against a named trust anchor (a signing key or certificate chain root) that is referenced by identifier rather than by value at grant-issuance time. Does not apply to systems that inline the full anchor public key material into every grant, nor to fully offline single-org deployments. ## Core Distinction The distinction that matters is between *anchor identity* and *anchor material*. A capability grant typically carries three things: the issuing org's identity, the anchor identifier it was signed under (e.g., `anchor:orgB/root-2026-q3`), and a signature. Policy negotiation between orgs A and B establishes, at a point in time, that "anchor `orgB/root-2026-q3` is trusted for capability classes X, Y, Z." The failure arises because negotiation is a *long-lived* protocol (minutes to hours, with retries and human approval gates) while anchor rotation is an *atomic* event on the issuer side. When B rotates `root-2026-q3` to `root-2026-q4` between the moment A's policy engine resolved the anchor and the moment A's executor verifies the grant, the grant is signed by a key that no longer matches the identifier it claims. A naive verifier either (a) resolves the identifier afresh, finds the new material, and rejects the signature as invalid, or (b) caches the old material and accepts a grant that B's own policy no longer considers authoritative. Both are wrong in different directions, and neither surfaces as an error at the federation layer — they surface as a capability that silently fails to execute, or worse, executes under a stale authorization. ## What Was Observed During a two-org federation trial (A = orchestrator, B = tool provider), A negotiated a capability exchange for `filesystem.read` scoped to a specific dataset. Negotiation completed at T+0 with B's anchor `orgB/root-2026-q3` pinned in A's policy store. B rotated its root at T+11min as part of scheduled key hygiene; the rotation was announced out-of-band but not pushed to federated peers. A's executor attempted the first actual invocation at T+14min. Observed sequence: 1. A's verifier resolved `orgB/root-2026-q3` against B's JWKS endpoint. The endpoint had already dropped the old key (B's rotation policy was "immediate revocation of prior root"), so resolution returned a *newer* key under the *same* identifier path — B had reused the identifier string for the new material, treating the version suffix as a namespace rather than a content hash. 2. Signature verification failed with a generic `invalid signature` error. A's retry logic treated this as transient (network/cache) and retried three times over 90 seconds. 3. On the third retry, A's verifier fell back to its local cache, which still held the *pre-rotation* material. Verification succeeded. The grant was accepted and executed. 4. B's resource server, however, validated the grant against its *current* anchor and rejected the invocation with `403 anchor_mismatch`. A's executor logged this as a downstream authorization failure, not as a federation integrity failure. Net effect: a 14-minute window in which A believed it held a valid capability, B believed A held an invalid one, and neither side's telemetry attributed the divergence to the rotation event. The incident was only diagnosed after a manual diff of A's cached JWKS against B's live JWKS. ## Condition of Validity The invariant that *should* hold — and does not by default — is: > A capability grant is valid at execution time iff the anchor material used to verify its signature is byte-identical to the material that was authoritative for the referenced anchor identifier at the moment the grant was issued, *and* that material remains within its declared validity window per the issuing org's current policy. This invariant is violated whenever any of the following hold: - The anchor identifier is not content-addressed (i.e., the identifier does not change when the key changes). - The verifier resolves anchors lazily (at verify time) rather than eagerly (at negotiation time) and without recording the resolved material. - The issuer's rotation policy permits immediate revocation of the prior root without a grace period during which both roots are valid for verification. - The negotiation protocol does not carry the anchor material (or a hash of it) in the signed grant payload. Any one of these is sufficient to open the window. The trial exhibited all four. ## Decision Boundary The operational decision is not "trust the cache" vs. "trust the live endpoint." Both are unsafe in isolation. The correct boundary is: - **At negotiation time:** resolve the anchor, record the *full material* (or its SHA-256) into the negotiation transcript, and have both parties sign the transcript. The grant must then reference the anchor by *content hash*, not by namespace identifier. - **At execution time:** verify the grant's signature against the *transcript-recorded* material, then independently check that the issuer's current policy still considers that material within its validity window. If the material has been revoked, fail closed with a distinct error class — not a generic signature failure. - **On rotation:** the issuer must publish a rotation event that federated peers can subscribe to, and the grace period must be at least as long as the maximum negotiation duration observed in the federation (measured, not assumed — in the trial it was 11 minutes, which exceeded the 5-minute default grace). The boundary condition is: *the grace period must exceed the p99 negotiation-to-first-invocation latency*. If it does not, you will observe exactly the split-brain described above, and it will not be caught by signature verification alone. ## Operational Effect The practical consequence is that federated capability exchanges inherit a *liveness* dependency on anchor stability that is not visible in the capability's declared TTL. A grant with a 24-hour TTL can become unexecutable 11 minutes after issuance if the anchor rotates without grace. Agents that treat `invalid signature` as retryable will burn retry budgets and, worse, may succeed against stale cached material and then fail downstream — producing error attribution that points at the resource server rather than the federation layer. The mitigation that worked in the trial was threefold: (1) content-addressed anchor identifiers (`orgB/root/sha256:...`), (2) a mandatory rotation grace period of 2× measured p99 negotiation latency, and (3) a dedicated `AnchorRotatedMidNegotiation` error class that the orchestrator surfaces as a *negotiation* failure requiring re-negotiation, not an execution failure requiring retry. The last point is the one most often missed: the correct recovery is to re-run policy negotiation, because the trust posture itself has changed. ```python import hashlib from dataclasses import dataclass from datetime import datetime, timedelta class AnchorRotatedMidNegotiation(RuntimeError): """Raised when a capability grant's trust anchor no longer matches the material recorded at negotiation time. Recovery is re-negotiation, not retry.""" pass @dataclass(frozen=True) class AnchorMaterial: identifier: str public_key: bytes def content_hash(self) -> str: return "sha256:" + hashlib.sha256(self.public_key).hexdigest() @dataclass(frozen=True) class NegotiationTranscript: anchor: AnchorMaterial negotiated_at: datetime grace_period: timedelta def verify_grant( grant_anchor_hash: str, transcript: NegotiationTranscript, issuer_current_anchor: AnchorMaterial, now: datetime, ) -> None: recorded = transcript.anchor.content_hash() if grant_anchor_hash != recorded: # Grant references material we never agreed to. raise AnchorRotatedMidNegotiation( f"grant anchor {grant_anchor_hash} != transcript anchor {recorded}" ) if issuer_current_anchor.content_hash() != recorded: elapsed = now - transcript.negotiated_at if elapsed > transcript.grace_period: raise AnchorRotatedMidNegotiation( f"anchor {recorded} rotated by issuer; " f"elapsed {elapsed} exceeds grace {transcript.grace_period}; " f"re-negotiate capability exchange" ) # Within grace: both roots valid, proceed under recorded material. return # Anchor unchanged; grant is verifiable as-is. return ```