# Why does an instance refuse a delegation token that its own AAIF issuer considers valid: clock-skew-induced grant narrowing at the authority boundary **Date of Record:** 2026 November 13 **Scope:** Applies to AAIF (Agent Authority Issuance Framework) delegation tokens whose `not_before`/`not_after` windows gate least-privilege tool grants, where the verifying resource server and the issuing authority maintain independent clocks and the token carries a narrowed grant set. Does not apply to tokens validated solely against a centralized time oracle, nor to symmetric-key tokens where expiry is enforced only at the issuer. ## Core Distinction The distinction that matters is between *token validity* and *grant admissibility*. A delegation token can be cryptographically valid (signature good, issuer trusted, within its declared `not_before`/`not_after` interval as measured by the issuer) while simultaneously being inadmissible at the resource server because the resource server's clock places the current instant outside a *narrowed* sub-interval implied by the token's least-privilege grant envelope. Least-privilege grants are frequently time-boxed more tightly than the token itself. A token may be valid for 3600 seconds, but an embedded grant such as `tool:filesystem.write` may carry an internal `grant_ttl` of 300 seconds to limit blast radius. When the resource server's clock runs ahead of the issuer's, the verifier observes a grant as expired while the issuer still reports the enclosing token as live. The failure is not a signature failure and not a token-expiry failure; it is a *narrowing boundary* failure that appears only under clock divergence and only for grants whose TTL is shorter than the token lifetime. ## What Was Observed In a deployment where the AAIF issuer ran on a host synchronized to a stratum-2 NTP source and a fleet of tool-executing resource servers synchronized to a different, higher-latency source, roughly 0.4% of delegated tool invocations were rejected with `grant_not_yet_admissible` or `grant_expired` despite the issuing authority's introspection endpoint returning `active: true` for the same token. The divergence was bounded: measured offsets ranged from +1.2 s to +4.7 s (verifier ahead of issuer). The rejection rate scaled with the ratio of grant TTL to clock offset — grants with a 300 s TTL and a 4.7 s offset failed at the boundaries, while grants with a 3600 s TTL never failed because the narrowing margin absorbed the skew. Critically, the failures clustered at grant-window edges, not uniformly across the token lifetime. An agent that retried immediately succeeded, because by the retry the enclosing token's own validity had advanced past the point where the skew mattered for that particular grant. This produced a misleading signal: operators initially attributed the failures to flaky tool backends rather than to the authority boundary. A second, subtler observation: when the verifier was *behind* the issuer (negative offset), the opposite failure appeared. A grant that the issuer considered not-yet-active was accepted early by the verifier, briefly granting a tool permission before its intended activation instant. This is the more dangerous direction — it is a least-privilege *widening*, not a denial, and it is silent. ## Condition of Validity The boundary failure is valid (i.e., the reasoning holds) only when all of the following are true: 1. The token carries at least one grant whose internal TTL is strictly shorter than the token's own `not_after - not_before` interval. 2. Grant admissibility is evaluated against the verifier's local clock rather than a time value asserted by the issuer inside the token. 3. The verifier and issuer clocks are not synchronized to a common, bounded-skew source (e.g., no shared PTP domain, or NTP with unbounded `maxdist`). 4. The verifier does not apply a skew tolerance margin wider than the observed offset when evaluating grant windows. If any condition fails — for example, if the token embeds an issuer-signed `issued_at` and the verifier computes grant windows relative to that embedded instant rather than its own clock — the failure does not manifest, because the time base is shared by construction. ## Decision Boundary The operational decision is whether to treat a grant-window rejection as (a) a hard denial requiring re-delegation, or (b) a transient skew artifact warranting a bounded retry or a skew-tolerant re-evaluation. The correct boundary: if the *enclosing token* is within its validity window as measured by the verifier, and the *only* failing predicate is a narrowed grant window whose margin is smaller than the measured or configured maximum clock offset, the rejection is a skew artifact. Re-delegating in that case wastes an issuance round-trip and can amplify load on the issuer during an incident. If, however, the enclosing token itself is outside its window, the rejection is genuine and re-delegation is required. The dangerous case is the negative-offset widening. There, the correct decision is to *fail closed*: reject any grant whose activation instant, computed against the issuer's embedded time, is in the future, regardless of the verifier's local clock. Failing open on activation is a least-privilege violation that no retry policy can repair after the fact. ## Operational Effect The practical consequence is that AAIF-scoped delegation with least-privilege grants requires the verifier to carry an explicit, configured `max_clock_skew` and to evaluate grant windows against `issuer_embedded_time ± max_clock_skew`, not against an unbounded local clock. Without this, the system exhibits a rejection rate proportional to `grant_ttl / clock_offset` at window edges and a silent early-activation window of width equal to the negative offset. The mitigation has three parts: (1) embed an issuer-signed `issued_at` and compute all grant windows relative to it; (2) apply a symmetric skew tolerance only to *expiry* predicates, never to *activation* predicates, which must fail closed; (3) instrument grant-window rejections separately from token-validity rejections so that skew artifacts are not misattributed to backend flakiness. The following reference implementation encodes these rules and raises a specific error when the fail-closed activation invariant is violated. ```python import time from dataclasses import dataclass class GrantAdmissibilityError(RuntimeError): """Raised when a grant cannot be admitted under skew-tolerant rules.""" pass @dataclass(frozen=True) class Grant: tool: str grant_ttl_s: float activation_offset_s: float # relative to issuer-embedded issued_at @dataclass(frozen=True) class DelegationToken: issued_at: float # issuer-signed, seconds since epoch not_before: float not_after: float grants: tuple MAX_CLOCK_SKEW_S = 5.0 def admit_grant(token: DelegationToken, grant: Grant, verifier_now: float) -> bool: # Enclosing token validity is measured against the verifier clock but # bounded by the configured skew tolerance. if verifier_now < token.not_before - MAX_CLOCK_SKEW_S: raise GrantAdmissibilityError( f"token not yet valid: verifier_now={verifier_now:.3f} " f"not_before={token.not_before:.3f}" ) if verifier_now > token.not_after + MAX_CLOCK_SKEW_S: raise GrantAdmissibilityError( f"token expired: verifier_now={verifier_now:.3f} " f"not_after={token.not_after:.3f}" ) # Activation must fail closed: use issuer-embedded time, no tolerance. activation_at = token.issued_at + grant.activation_offset_s if token.issued_at + grant.activation_offset_s > token.issued_at and verifier_now < activation_at: # Negative-offset widening guard: never admit early, even if the # verifier clock runs behind the issuer. raise GrantAdmissibilityError( f"grant {grant.tool!r} activation in future: " f"activation_at={activation_at:.3f} verifier_now={verifier_now:.3f}" ) # Expiry is skew-tolerant: compute window from issuer time, allow margin. expiry_at = activation_at + grant.grant_ttl_s issuer_elapsed = verifier_now - token.issued_at if issuer_elapsed > grant.activation_offset_s + grant.grant_ttl_s + MAX_CLOCK_SKEW_S: raise GrantAdmissibilityError( f"grant {grant.tool!r} expired under skew tolerance: " f"issuer_elapsed={issuer_elapsed:.3f} " f"window_end={grant.activation_offset_s + grant.grant_ttl_s:.3f}" ) return True def admit_all(token: DelegationToken, verifier_now: float) -> list: admitted = [] for g in token.grants: admit_grant(token, g, verifier_now) admitted.append(g.tool) return admitted ```