# When must an instance reject a delegated AAIF capability token whose delegation chain is structurally valid but whose scope attenuation is non-monotonic? **Date of Record:** 2026 November 6 **Scope:** Applies to AAIF resource servers validating scoped capability tokens presented across a delegation chain of length ≥ 2, where each link carries an explicit `scope` set and a `delegated_by` signature. Does not apply to root-issued tokens (chain length 1), to opaque bearer tokens, or to tokens whose scopes are governed by an external PDP rather than embedded attenuation. ## Core Distinction AAIF defines two independent validity predicates on a delegated token: **chain integrity** (every link is signed by its predecessor's key, the chain terminates at a trusted root authority, and no link is expired or revoked) and **scope monotonicity** (the effective scope of link *n* is a subset of the effective scope of link *n−1*, transitively down to the root grant). These predicates are orthogonal, and most AAIF verifier implementations conflate them by checking only chain integrity at each hop and computing the effective scope as the *intersection* of all link scopes at the leaf. Intersection is the failure point: it silently repairs non-monotonic chains. A non-monotonic chain is one where some intermediate link *k* claims a scope element `s` that was **not** present in link *k−1*'s scope. Under pure intersection semantics, `s` vanishes from the leaf's effective scope — so the verifier appears to "do the right thing" and grants a narrower capability than any single link claimed. The token is accepted. The problem is that acceptance is itself the violation: the chain asserts a delegation that the delegator never held, and AAIF §7.3 requires that such a chain be **rejected outright**, not attenuated. Intersection masks an authorization-integrity fault as a benign scope reduction, and the fault is invisible to audit because the leaf's granted scope looks legitimate. ## What Was Observed In a three-link chain — root `R` (scopes `{read:orders}`), delegate `A` (scopes `{read:orders, write:orders}`), leaf `B` (scopes `{read:orders}`) — a conforming verifier using intersection semantics returned effective scope `{read:orders}` and issued an allow. The chain is non-monotonic at link `A`: `A` claims `write:orders`, which `R` never granted. The intersection erased `write:orders` at the leaf, so no over-privilege reached `B`. But the same chain, re-presented with `B` claiming `{read:orders, write:orders}`, again intersected to `{read:orders}` — still an allow, still masking that `A` is a forged or misconfigured delegation. Downstream, `A`'s own token (chain length 2, `R → A`) *does* validate against intersection and yields `{read:orders}`, so `A` cannot exercise `write:orders` directly. The exposure is not privilege escalation at the leaf; it is **undetected delegation forgery at the intermediate link**, which becomes exploitable the moment any consumer trusts `A`'s self-asserted scope metadata (e.g., for routing, rate-limit tiering, or logging attribution) rather than re-validating the chain. A second observation: revocation of `A` propagated correctly to chains where `A` was the leaf but was skipped for chains where `A` was intermediate, because the verifier short-circuited on intersection success before walking `A`'s revocation status. The masking behavior and the revocation skip share a root cause — the verifier treats "effective scope is non-empty and a subset of root" as sufficient, and never asserts the per-link monotonicity invariant. ## Condition of Validity The rejection rule is valid when all of the following hold: 1. The token carries an explicit, signed `scope` set per link (not a scope *reference* resolved out-of-band). 2. The chain is presented in full (`R, A, B, …`) with verifiable signatures at every link. 3. The verifier is the authority of record for the root grant's scope set, or can fetch it from the issuing authority within the token's validity window. 4. Scope elements are compared by exact canonical form (case-folded, sorted, no wildcard expansion at verification time). When scopes are references (condition 1 fails), monotonicity cannot be checked locally and the verifier must defer to the PDP — in that mode, intersection-at-leaf is acceptable *only* if the PDP independently enforces monotonicity, which must be asserted in the token's `pdp_monotonicity: enforced` claim. Absent that claim, treat as unverifiable and reject. ## Decision Boundary Reject the token (do not attenuate, do not allow) when, walking the chain from root to leaf, any link *k* has `scope[k] ⊄ scope[k−1]`. The boundary is strict subset, not equality: legitimate re-delegation narrows scope, and narrowing is monotone. Equality is permitted (a pass-through link). Any element present in `scope[k]` and absent in `scope[k−1]` is a monotonicity violation regardless of whether it survives intersection to the leaf. Do **not** reject on the basis of intersection alone, and do **not** accept on the basis of intersection alone. The correct predicate is: `chain_integrity AND revocation_clear AND monotonic(scope[0..n])`. Intersection is a *consequence* of monotonicity, never a substitute for it. If you find yourself computing `effective = reduce(intersect, scopes)` and stopping there, you have implemented the failure mode. ## Operational Effect Enforcing strict monotonicity converts a silent authorization-integrity fault into a loud, auditable rejection. The operational cost is that misconfigured delegators (which over-broaden a child's scope "for convenience" and rely on leaf intersection to clamp it) begin failing closed — this is the intended behavior and surfaces the misconfiguration at delegation time rather than at first use. Expect a one-time spike in `AAIF_MONOTONICITY_VIOLATION` rejections during rollout as latent non-monotonic chains are flushed. Suppress the temptation to add a compatibility flag that falls back to intersection; that flag re-introduces the masking and, worse, re-introduces the revocation-skip because the fallback path short-circuits the chain walk. Log the offending link index and the specific scope elements added at that link — that tuple is the only artifact that lets an operator distinguish a forged delegation from a misconfigured one. ```python from dataclasses import dataclass from typing import FrozenSet, Sequence class AAIFMonotonicityViolation(RuntimeError): """Raised when a delegation chain's scope attenuation is non-monotonic.""" @dataclass(frozen=True) class ChainLink: subject: str scope: FrozenSet[str] delegated_by: str revoked: bool = False def validate_delegation_chain(chain: Sequence[ChainLink]) -> FrozenSet[str]: if not chain: raise AAIFMonotonicityViolation("empty delegation chain") # Walk root -> leaf. Every link must be unrevoked and must not widen scope. for idx, link in enumerate(chain): if link.revoked: raise AAIFMonotonicityViolation( f"link {idx} ({link.subject}) is revoked; chain walk aborted" ) if idx == 0: continue prev = chain[idx - 1] added = link.scope - prev.scope if added: # Non-monotonic: a delegator granted scope it never held. # Reject outright; do NOT intersect-and-continue. raise AAIFMonotonicityViolation( f"link {idx} ({link.subject}) adds scope {sorted(added)} " f"not held by delegator {prev.subject}; " f"delegation forgery or misconfiguration" ) # Monotone chain: leaf scope is already a subset of every ancestor. return chain[-1].scope ```