# Why does an instance-bound AAIF delegated authorization token fail silently when an autonomous agent's identity is re-attested mid-token-lifetime **Date of Record:** 2026 November 13 **Scope:** Applies to AAIF (Agent Authorization & Identity Framework) delegated token exchanges where a subject agent instance presents a token whose `instance_binding` claim was minted against a prior attestation digest, and the issuing authority re-attests the instance (new measurement, new nonce, or re-keying) before the token's `exp`. Does not apply to long-lived service-identity tokens without instance binding, nor to purely capability-scoped tokens where the subject is a stable principal rather than a live process instance. ## Core Distinction There are two failure surfaces in delegated token exchange: **authorization denial** (the token is rejected — loud, observable, recoverable) and **authorization drift** (the token is *accepted* but no longer authorizes what its holder believes it authorizes). The AAIF spec's delegated-exchange path is designed to make denial loud: signature checks, audience checks, and `exp`/`nbf` checks all fail closed. But the `instance_binding` claim is validated against the *current* attestation state of the subject, not against the attestation state *at mint time*. When an instance is re-attested mid-lifetime, the binding that the token was minted under ceases to exist, yet the token continues to validate against the new binding if — and only if — the re-attestation preserved the same `instance_id` and the binding check is defined as "does the presented instance's current attestation satisfy the token's binding predicate" rather than "does the token's recorded attestation digest match the presented instance's current digest." The non-obvious invariant: **an AAIF delegated token is only valid for the exact `(instance_id, attestation_digest)` pair recorded at mint time; any re-attestation that changes the digest silently re-scopes the token to the new measurement without re-issuance.** The token does not fail. It continues to authorize the *new* instance under the *old* token's grants, and downstream resource servers that cache the token's resolved claims will not observe the change. ## What Was Observed A fleet of autonomous agents performing delegated data-retrieval tasks under AAIF v1.3. The orchestrator mints an instance-bound delegated token for each agent worker, with `instance_binding = {instance_id, attestation_digest, binding_nonce}` and a 45-minute `exp`. Workers are scheduled onto a node pool that performs periodic TPM-based re-attestation every 20 minutes (a platform hygiene policy, independent of AAIF). During a rolling platform update, the re-attestation policy was tightened: the measurement now included the runtime's loaded module set, which changed when a patched TLS library was injected. Every worker's `attestation_digest` rotated at the 20-minute mark. The AAIF issuer's binding validator, as implemented, compared the token's `binding_nonce` and `instance_id` against the live instance's *current* attestation and treated a digest mismatch as a *soft* signal — it logged `binding_digest_rotated` at `INFO` and proceeded, because the validator's contract was "the instance presenting the token must be the same instance that was attested," and `instance_id` is stable across re-attestation. Consequence: for the remaining 25 minutes of each token's lifetime, workers held tokens whose recorded attestation digest no longer described the running code. Two of those workers had, between re-attestation events, loaded a plugin that the original attestation had explicitly excluded from its measured module set. The tokens continued to pass delegated-exchange at three downstream resource servers, each of which resolved the token to the *original* grant set (read-only on dataset A, no write on dataset B). No denial occurred. The over-grant was bounded only by the original token's scopes — which, critically, had been minted for a *different* module set that the policy author had assumed was the only code capable of exercising those scopes. The audit trail was misleading in a specific way: the issuer logged the digest rotation, the resource servers logged successful exchanges, and the token's `jti` was unchanged. A post-hoc query for "tokens used after their binding digest changed" returned zero results because the query joined on `attestation_digest` equality, which was never true after rotation but was also never checked as a hard condition. ## Condition of Validity The silent re-scoping occurs if and only if all of the following hold: 1. The delegated token carries an `instance_binding` claim with an `attestation_digest` (or equivalent measurement hash) recorded at mint time. 2. The issuing authority's binding validator treats `instance_id` continuity as sufficient for binding, and treats digest divergence as advisory rather than fatal. 3. Re-attestation can occur within the token's `exp` window without triggering token revocation or re-issuance. 4. Downstream resource servers resolve grants from the token's claims (cached or re-resolved) rather than re-querying the issuer's live binding state per request. If any one of these is false, the failure degrades to a loud denial (2, 4) or does not arise (1, 3). The dangerous configuration is (2) true and (4) true: the issuer is permissive and the resource servers are trusting, so no component in the path is positioned to notice that the token's authority has drifted from its recorded basis. ## Decision Boundary The operational choice is whether the binding predicate should be **identity-continuity** (same `instance_id`, digest advisory) or **measurement-equality** (same `instance_id` *and* same `attestation_digest`, digest fatal). AAIF v1.3 leaves this to the issuer's policy, which is the root of the ambiguity. Choose measurement-equality (fatal on digest divergence) when the token's grants are scoped to *what the attested code is permitted to do*. This is the correct default for autonomous agents, because the whole point of instance binding is to tie authority to a specific measured code image. Under measurement-equality, a mid-lifetime re-attestation forces token re-issuance — a 200–400 ms cost per worker per rotation, and a brief window where in-flight requests must be retried. That cost is the price of the invariant. Choose identity-continuity only when the grants are scoped to the *principal* (the agent's logical role) and the instance binding exists solely for replay/anti-cloning, not for code-authority. In that case the digest is a freshness signal, not an authority boundary, and advisory treatment is defensible — but then the token should not be described as "instance-bound" in policy documentation, because operators will assume the stronger semantics. The boundary is crossed silently in the common case where a platform team owns re-attestation cadence and an AAIF team owns token lifetime, and neither team's runbook states which semantics the binding validator implements. The failure is a coordination gap, not a code bug. ## Operational Effect The practical effect is that **token lifetime and attestation cadence become coupled whether or not anyone intended it.** If re-attestation is more frequent than token `exp`, every token will, at some point in its life, describe a measurement that no longer matches the running instance. Under identity-continuity semantics this is invisible; under measurement-equality it is a guaranteed re-issuance storm. Operators who observe the storm under measurement-equality will be tempted to relax to identity-continuity to "fix" the latency, which silently reintroduces the over-grant. The correct resolution is to make re-attestation *trigger* token re-issuance at the issuer (a revocation event keyed on `instance_id` + digest change), so that the token's lifetime is bounded by the attestation's lifetime rather than by wall-clock `exp`. Detect the condition by asserting, at every delegated exchange, that the token's recorded `attestation_digest` equals the presented instance's current digest, and by emitting a hard `binding_digest_mismatch` metric — not a log line — that pages when nonzero. The metric must be emitted at the *resource server*, not only at the issuer, because the resource server is the last point where the drift is observable before authority is exercised. ```python import hashlib from dataclasses import dataclass class BindingDigestMismatch(RuntimeError): """Raised when a delegated token's recorded attestation digest no longer matches the presenting instance's current attestation. This is a hard authority-boundary violation under measurement-equality semantics; it must never be downgraded to a warning, because the token would otherwise silently authorize a code image it was not minted for.""" @dataclass(frozen=True) class DelegatedToken: jti: str instance_id: str attestation_digest: str # recorded at mint time grants: frozenset[str] exp: int @dataclass(frozen=True) class LiveAttestation: instance_id: str attestation_digest: str # current, post-re-attestation measured_modules: frozenset[str] def validate_binding( token: DelegatedToken, live: LiveAttestation, *, semantics: str = "measurement_equality", ) -> None: """Enforce the AAIF instance-binding invariant at delegated exchange. semantics='measurement_equality' -> digest divergence is fatal (default). semantics='identity_continuity' -> digest divergence is advisory only; caller accepts the documented risk. """ if semantics not in ("measurement_equality", "identity_continuity"): raise ValueError(f"unknown binding semantics: {semantics!r}") if token.instance_id != live.instance_id: raise BindingDigestMismatch( f"instance_id mismatch for jti={token.jti}: " f"token={token.instance_id!r} live={live.instance_id!r}" ) if token.attestation_digest != live.attestation_digest: # The token was minted against a measurement that no longer exists. # Under measurement_equality this is a hard failure: re-issue required. if semantics == "measurement_equality": raise BindingDigestMismatch( f"attestation digest rotated mid-token-lifetime for jti={token.jti} " f"instance={token.instance_id}: " f"minted={token.attestation_digest} live={live.attestation_digest}; " f"re-issue the delegated token before exercising grants " f"{sorted(token.grants)}" ) # identity_continuity: proceed, but the caller has explicitly opted in. def exchange(token: DelegatedToken, live: LiveAttestation, resource: str) -> frozenset[str]: validate_binding(token, live) # measurement_equality by default if resource not in token.grants: raise PermissionError(f"resource {resource!r} not in grants for jti={token.jti}") return token.grants def _digest(modules: frozenset[str]) -> str: h = hashlib.sha256() for m in sorted(modules): h.update(m.encode()) return h.hexdigest() if __name__ == "__main__": original = frozenset({"core", "http_client"}) patched = frozenset({"core", "http_client", "tls_patched", "plugin_write"}) token = DelegatedToken( jti="tok-7f3a", instance_id="agent-worker-42", attestation_digest=_digest(original), grants=frozenset({"dataset_a:read"}), exp=1_700_000_000, ) live = LiveAttestation( instance_id="agent-worker-42", attestation_digest=_digest(patched), measured_modules=patched, ) # Under default measurement_equality semantics this raises, forcing # re-issuance rather than silently authorizing the patched module set. exchange(token, live, "dataset_a:read") ```