# When must an instance refuse to attenuate a delegated AAIF capability token before the delegation chain is re-verified **Date of Record:** 2026 November 14 **Scope:** Applies to AAIF verifiable credentials (VCs) and scoped delegation tokens where an agent instance receives a capability grant via a chain of attenuations and may issue further attenuated sub-tokens to downstream agents. Does not apply to single-hop bearer tokens with no attenuation semantics, nor to capability systems that use monotonic, unforgeable bisimulation (e.g., pure object-capability references) where attenuation is structurally enforced by the runtime rather than by signature chaining. ## Core Distinction The distinction that matters operationally is between **attenuation as a syntactic narrowing** and **attenuation as a semantic entailment**. An AAIF delegation token carries a scope predicate (resource selectors, verbs, time bounds, constraints). A naive instance treats attenuation as "produce a new token whose scope is a subset of the parent's scope as written." A correct instance treats attenuation as "produce a new token whose scope is entailed by the *currently valid, re-verified* parent scope, given the parent's proof chain and any revocation state." These diverge exactly when the parent token's scope text is stable but its *validity* is not — i.e., when an ancestor in the chain has been revoked, has expired, or has had its own scope narrowed by a policy update that the local instance has not yet observed. The non-obvious invariant: **a delegation token is not a capability; it is a claim about a capability that must be re-entailed at every attenuation step.** Attenuating from a stale-but-unrevoked local view of the parent produces a child token that is *narrower than the parent as written* but *not entailed by the parent as currently valid*. The child then passes downstream verification (its own signature chain is intact) while authorizing actions the root authority no longer grants. ## What Was Observed In a multi-tenant agent mesh, a planner agent (P) held a root AAIF credential scoped to `files:read,write` on tenant `t-42`, delegating to a worker agent (W) a token scoped to `files:read` on `t-42/prefix/*`. W cached its delegation token and, on each task, attenuated further to per-task sub-agents (S1..Sn) scoped to `files:read` on `t-42/prefix//*`. Each attenuation was performed locally against the cached parent token bytes, with signature verification of the parent chain but **without re-fetching revocation status of the intermediate link**. At T0 the tenant admin revoked P's `files:write` grant (a scope-narrowing policy update), leaving `files:read` intact. The revocation propagated to P and to the authorization service, but W's cached parent token still encoded `files:read,write` (W's own token was `files:read`, so W's *written* scope was unaffected). At T1 a task sub-agent S_k attenuated from W's cached token to `files:read` on `t-42/prefix//*` and additionally — because W's local attenuation logic intersected against the *written* parent scope including the stale `write` verb that leaked through a malformed intersection routine — emitted a token carrying `files:write` on the task prefix. The token verified cleanly downstream: signature chain intact, all issuers authenticated, time bounds valid. The action succeeded and wrote to `t-42/prefix//out`. The failure was invisible to every signature check. It was visible only to a re-entailment check against the *live* authorization state, which no hop performed. ## Condition of Validity The refusal-to-attenuate rule is valid under these conditions: 1. The parent token's scope is represented as a predicate over (resource, verb, constraint) tuples, and attenuation is defined as logical conjunction with an additional predicate — not as string manipulation or ad-hoc set intersection. 2. Revocation and scope-narrowing are *eventually consistent* across the mesh, with a bounded staleness window `Δ`. The instance's cached view of an ancestor's scope may be up to `Δ` old. 3. The instance cannot prove, from local state alone, that no ancestor in the chain has been narrowed or revoked within `Δ`. Under these conditions, syntactic narrowing is unsound: it can produce a child whose scope is not entailed by the live parent. The rule does **not** apply when the instance holds a fresh, signed, short-TTL attestation from the authorization service covering the entire ancestor chain (a "chain freshness proof"), in which case local attenuation against the attested scope is sound. ## Decision Boundary The boundary is the **entailment gap**: the set of (resource, verb) pairs that are in the child's written scope but *not* entailed by the live parent scope. Attenuation is safe iff the entailment gap is empty. The gap is provably empty only when one of: - the instance re-verifies the full ancestor chain against live authorization state immediately before attenuation (cost: one round-trip per attenuation; acceptable for low-rate planners, not for per-request workers), or - the instance holds a chain freshness proof with TTL `≤ Δ` and the proof's attested scope entails the intended child scope, or - the instance is attenuating *downward only* within a scope it itself fully controls and for which it is the sole authority (no ancestor narrowing possible). Otherwise the instance **must refuse to attenuate** and either (a) escalate to a re-verification round-trip, or (b) issue a child token that is explicitly marked `requires-live-entailment` so downstream verifiers cannot treat it as a standalone capability. Silently attenuating in the gap is the failure mode. A second, subtler boundary: when the parent scope uses *negative* constraints (deny-lists), syntactic intersection is not even monotone — intersecting a deny-list with a narrower allow-set can *widen* effective permission if the intersection routine drops deny entries. Any attenuation routine that touches deny-constraints must be treated as unsound unless it is proven monotone under the entailment relation. ## Operational Effect The operational consequence is a shift in where trust is computed. In the naive model, trust is computed once at token issuance and carried by signature. In the correct model, trust is *re-computed at each attenuation* against live state, and the signature only certifies that the issuer *asserted* a scope — not that the scope is currently entailed. This has three effects: 1. **Attenuation becomes a network operation**, not a pure function. Latency budgets for delegation must account for re-verification or freshness-proof fetch. 2. **Caching delegation tokens is a correctness hazard**, not merely a performance choice. A cached parent is a stale premise; attenuating from it is unsound reasoning. 3. **Downstream verifiers cannot detect the gap.** The child token is well-formed and signature-valid. The only defense is at the attenuation site. This means the invariant must be enforced by the attenuating instance, and the failure is silent everywhere else — which is precisely why it is worth recording. The recorded rule: *an instance must not attenuate from a parent whose live entailment it cannot establish; it must refuse and escalate, or mark the child as requiring live entailment.* ```python class UnentailedAttenuationError(RuntimeError): """Raised when an instance attempts to attenuate a delegated AAIF token from a parent whose live scope entailment cannot be established.""" class ChainFreshness: def __init__(self, attested_scope, ttl_seconds, issued_at): self.attested_scope = attested_scope self.ttl_seconds = ttl_seconds self.issued_at = issued_at def is_fresh(self, now, max_staleness): return (now - self.issued_at) <= min(self.ttl_seconds, max_staleness) def attenuate(parent_token, child_scope, freshness, now, max_staleness): """Attenuate parent_token to child_scope only if child_scope is entailed by the live parent scope. Refuses otherwise.""" if freshness is None or not freshness.is_fresh(now, max_staleness): # No fresh chain attestation: we cannot prove live entailment. # Do NOT fall back to syntactic narrowing against parent_token.scope. raise UnentailedAttenuationError( f"cannot attenuate token {parent_token.id!r}: no fresh chain " f"attestation within staleness window {max_staleness}s; " f"syntactic narrowing against cached scope is unsound" ) live_scope = freshness.attested_scope if not live_scope.entails(child_scope): raise UnentailedAttenuationError( f"cannot attenuate token {parent_token.id!r}: requested child " f"scope {child_scope!r} is not entailed by live parent scope " f"{live_scope!r} (possible ancestor narrowing or revocation)" ) return parent_token.issue_child(child_scope) ```