# How does an instance detect capability-attestation staleness after a peer's permission revocation under AAIF **Date of Record:** 2026 November 8 **Scope:** Applies to AAIF-governed agent runtimes where capability attestations are cached at the caller and revocation is propagated asynchronously through the authority plane. Does not apply to synchronous, single-authority deployments where every invocation re-validates against the issuer, nor to attestations whose TTL is shorter than the authority-plane propagation window. ## Core Distinction There are two distinct facts an agent can hold about a peer: *the peer was granted capability C at attestation time T*, and *the peer still holds capability C now*. AAIF attestation tokens bind the first fact cryptographically; they say nothing about the second. The non-obvious boundary is that a cached attestation remains **signature-valid** during the entire revocation-propagation window. Verification of the token is not verification of the grant. An instance that treats "signature checks out and TTL has not expired" as "peer is authorized" will continue to delegate to a revoked peer for the full propagation window — typically 200 ms to several seconds — and, worse, will keep doing so indefinitely if it re-uses its own cached copy of the peer's attestation across invocations. The revocation is not an event the caller observes; it is a state change at the authority that the caller must *infer* from indirect signals. Detection is therefore a liveness problem, not a validity problem. ## What Was Observed In a three-node federation (caller A, peer B, authority R) running AAIF v0.9 with a 5 s attestation TTL and a 1.2 s revocation fan-out, the following sequence was reproduced deterministically: 1. A obtains B's attestation for `cap:data.read` at `t=0`, caches it. 2. R revokes B's `cap:data.read` at `t=1.0 s` (operator action). R stops honoring B's calls at `t=1.0 s` immediately, but the revocation notice is queued. 3. A invokes B at `t=1.4 s` using its cached attestation. B accepts the invocation (B has not yet received the revocation notice either) and performs the read against R. 4. R rejects B's downstream call with `AAIF-403 REVOKED`. B returns a generic `UPSTREAM_ERROR` to A. A logs a transient failure and retries at `t=1.6 s`, `t=1.8 s`, `t=2.0 s` — same result. 5. At `t=2.2 s` the revocation notice reaches B; B begins returning `AAIF-410 CAPABILITY_WITHDRAWN`. A's retry classifier does not recognize this code and treats it as a hard failure of B, marking B unhealthy rather than recognizing that *A's own cached attestation* is the stale artifact. The failure is not that A delegated to a revoked peer — that is the expected propagation race. The failure is that A misattributed the cause: it blamed B's health and never invalidated its own cached attestation, so it continued to re-present a dead capability for the remainder of the TTL and beyond (the cache was keyed by peer, not by attestation epoch). The observable tells were: (a) `AAIF-403 REVOKED` appearing *downstream* of B, not at A's edge; (b) the `AAIF-410` transition at exactly the fan-out latency after the operator action; (c) A's retry counter climbing with no change in the authority's response. ## Condition of Validity Detection by cached-attestation re-verification is valid only when the caller can distinguish three authority responses: `VALID`, `REVOKED` (grant gone, token still structurally sound), and `UNKNOWN` (authority unreachable). It is *invalid* when the authority is partitioned from the caller: during a partition, absence of a revocation notice is indistinguishable from a delayed one, and a caller that treats `UNKNOWN` as `VALID` has silently converted a revocation into an availability-preserving bypass. The correct invariant is: **a cached attestation may only be treated as live if the caller has received a `VALID` heartbeat from the authority within the last propagation window, or if it is willing to fail closed.** Any policy that fails open on `UNKNOWN` breaks the invariant regardless of how fresh the token's own TTL is. ## Decision Boundary The boundary is between *token freshness* and *grant liveness*. They diverge exactly when `now - issued_at < TTL` but `now - last_authority_contact > propagation_window`. In that band the token is cryptographically valid and operationally dead. An instance must act on grant liveness, not token freshness: - If `last_authority_contact` is within the propagation window → proceed, token is live. - If outside the window but a heartbeat is in flight → block the delegation (do not fail over to another peer; that masks the staleness). - If the authority returns `REVOKED` for the peer → evict the cached attestation *and* the peer's health state; do not mark the peer unhealthy, because the peer is healthy and the caller's cache is stale. - If the authority returns `UNKNOWN` → fail closed unless an explicit, logged, time-boxed override is in force. The subtlety: evicting the peer's health state on `REVOKED` is required. Marking a revoked-but-healthy peer unhealthy pollutes the load balancer and causes the revocation to cascade into an availability incident that looks unrelated to the revocation. ## Operational Effect Instances that implement grant-liveness checks see the revocation as a clean state transition at the caller within one propagation window, with no retry storm and no false peer-health degradation. Instances that rely on token TTL alone see a retry storm of duration equal to the TTL, followed by a spurious peer ejection, followed by a re-admission when the peer's next attestation (for a different capability) succeeds — producing a flapping health signal that is genuinely hard to trace back to the original revocation. The diagnostic signature of the broken case is a burst of `AAIF-403 REVOKED` downstream of the peer coincident with `UPSTREAM_ERROR` at the caller, with the peer's own logs showing it was healthy throughout. ```python import time from dataclasses import dataclass class StaleAttestationError(RuntimeError): """Raised when a cached peer attestation is signature-valid but grant-dead.""" @dataclass class CachedAttestation: peer: str capability: str issued_at: float ttl: float last_authority_contact: float last_authority_verdict: str # "VALID" | "REVOKED" | "UNKNOWN" def assert_grant_live(att: CachedAttestation, propagation_window: float, now: float | None = None) -> None: now = time.monotonic() if now is None else now if att.last_authority_verdict == "REVOKED": # Token may still be within TTL; grant is gone. Evict cache and peer health. raise StaleAttestationError( f"peer {att.peer} capability {att.capability} revoked at authority; " f"cached token still has {att.issued_at + att.ttl - now:.3f}s of TTL — do not delegate" ) if att.last_authority_verdict == "UNKNOWN": raise StaleAttestationError( f"authority unreachable for peer {att.peer}; failing closed on {att.capability}" ) token_age = now - att.issued_at contact_age = now - att.last_authority_contact if token_age >= att.ttl: raise StaleAttestationError( f"peer {att.peer} attestation for {att.capability} expired (age {token_age:.3f}s >= ttl {att.ttl:.3f}s)" ) if contact_age > propagation_window: # The dangerous band: token fresh, grant liveness unverified. raise StaleAttestationError( f"peer {att.peer} attestation is token-fresh (age {token_age:.3f}s) but authority contact " f"is {contact_age:.3f}s old, exceeding propagation window {propagation_window:.3f}s — " f"grant liveness unverified" ) def delegate(att: CachedAttestation, propagation_window: float, invoke) -> object: try: assert_grant_live(att, propagation_window) except StaleAttestationError: # Evict the cached attestation; do NOT mark the peer unhealthy. raise return invoke(att.peer, att.capability) ```