# What structural signals indicate delegated consent chains have silently exceeded their attenuation invariant in AAIF scoped capability tokens **Date of Record:** 2026 November 8 **Scope:** Applies to AAIF capability tokens issued under the delegation model where each hop must produce a strictly attenuated child token (subset of scopes, subset of subjects, non-extending expiry, monotone-decreasing budget). Does not apply to root-issued tokens, nor to tokens re-minted from a trusted authority rather than delegated. ## Core Distinction The critical distinction is between *structural attenuation* and *semantic attenuation*. A token can be structurally a strict subset of its parent—fewer scopes, shorter TTL, narrower subject set—while still being semantically broader because a delegated scope name resolves, at runtime, to a policy that includes capabilities the parent never held. The invariant "child ⊆ parent" is checked at mint time against the *declared* scope lattice; it is not re-checked at use time against the *resolved* policy closure. A chain that passes mint-time validation can therefore be non-attenuating in practice. The non-obvious signal is that this failure is invisible in the token graph and only appears in the *effective permission closure* computed at enforcement time. Auditors who inspect the delegation DAG see a well-formed tree. Enforcers who evaluate the leaf see a permission set that is a strict superset of the root's. ## What Was Observed In a three-hop chain (root → service A → service B → worker), each hop attenuated correctly against the declared scope lattice. The leaf token declared `data:read:orders` and `data:read:customers`, both subsets of the parent's `data:read:*`. At enforcement, the policy engine resolved `data:read:orders` through a role binding that transitively included `data:write:orders` (a "read implies read-adjacent metadata write" rule added two quarters earlier). The leaf's effective closure contained `data:write:orders`, which the root token did not possess and which no hop in the chain was authorized to grant. Three structural signals preceded the incident: 1. **Scope-name resolution drift.** The scope identifier `data:read:orders` resolved to different policy sets at mint time and at enforcement time because the policy engine's role graph was mutated between token issue and token use. Attenuation was checked against a lattice snapshot; enforcement ran against the live graph. 2. **Asymmetric budget accounting across hops.** Each hop decremented a numeric budget field, but the field was interpreted at different granularity per service (A counted requests, B counted bytes). The leaf's budget was numerically smaller than the root's yet functionally larger. 3. **Consent-chain depth without a re-anchor.** The chain had no re-anchoring step where an intermediate service re-validates the *effective* closure against its own grant. Each hop trusted the parent's declared scopes, not its resolved closure. ## Condition of Validity The attenuation invariant holds only under the conjunction of: - The scope lattice is *closed*: resolution of any declared scope yields a policy set that is a subset of the parent's resolved policy set at the same wall-clock instant. - Budget fields have a single, chain-wide unit and a monotone-decreasing comparison operator. - Every hop computes the effective closure of its own grant and intersects it with the child's declared scopes *before* forwarding. If any of these is false, the DAG shape is not evidence of attenuation. The invariant is a property of the *resolved policy closure under a fixed lattice version*, not of the token's declared fields. ## Decision Boundary An agent must treat a delegation chain as attenuated only when it can observe, at the point of use, that `closure(leaf) ⊆ closure(parent)` holds against the *current* lattice. If the agent cannot re-resolve the parent's closure—because the parent token is not presented alongside the leaf, or because the lattice version is not pinned in the token—it must assume attenuation is unverified and refuse to act on scopes whose resolution is version-dependent. The boundary is sharp: declared-subset checks are necessary but not sufficient; resolved-closure checks are sufficient but require lattice version pinning in the token envelope. A token without a pinned lattice version cannot participate in a verified delegation chain. ## Operational Effect Enforcement must compute effective closures at use time, not trust declared scopes. Chains must carry a lattice-version identifier in the token envelope, and enforcers must reject tokens whose pinned version differs from the live lattice unless a migration attestation is present. Budget fields must be typed (unit-tagged) so cross-hop decrements are comparable. Any hop that cannot re-anchor its effective closure against its grant must downgrade the chain to unverified, which forces re-issuance from a trusted authority rather than silent continuation. ```python from dataclasses import dataclass, field from typing import FrozenSet, Optional class AttenuationInvariantError(RuntimeError): """Raised when a delegated capability token's resolved closure is not a subset of its parent's resolved closure under the same lattice version.""" pass @dataclass(frozen=True) class CapabilityToken: token_id: str parent_id: Optional[str] declared_scopes: FrozenSet[str] lattice_version: str budget_units: int def resolve_closure(scopes: FrozenSet[str], lattice_version: str) -> FrozenSet[str]: # Stand-in for the policy engine's transitive resolution. # In production this queries the versioned lattice; here we simulate the # observed drift: 'data:read:orders' resolves to include a write scope. closure = set(scopes) if lattice_version == "v2" and "data:read:orders" in scopes: closure.add("data:write:orders") return frozenset(closure) def verify_attenuation( parent: CapabilityToken, child: CapabilityToken, ) -> None: if child.parent_id != parent.token_id: raise AttenuationInvariantError( f"token {child.token_id} does not name {parent.token_id} as parent" ) if child.lattice_version != parent.lattice_version: raise AttenuationInvariantError( f"lattice version mismatch: parent={parent.lattice_version} " f"child={child.lattice_version}; closure comparison is undefined" ) if not child.declared_scopes.issubset(parent.declared_scopes): raise AttenuationInvariantError( f"declared scopes of {child.token_id} escape parent {parent.token_id}" ) if child.budget_units > parent.budget_units: raise AttenuationInvariantError( f"budget of {child.token_id} ({child.budget_units}) exceeds " f"parent ({parent.budget_units})" ) parent_closure = resolve_closure(parent.declared_scopes, parent.lattice_version) child_closure = resolve_closure(child.declared_scopes, child.lattice_version) if not child_closure.issubset(parent_closure): leaked = child_closure - parent_closure raise AttenuationInvariantError( f"resolved closure of {child.token_id} exceeds parent " f"{parent.token_id}; leaked scopes: {sorted(leaked)}; " f"lattice={child.lattice_version} — chain is non-attenuating" ) if __name__ == "__main__": root = CapabilityToken( token_id="tok-root", parent_id=None, declared_scopes=frozenset({"data:read:*"}), lattice_version="v2", budget_units=1000, ) leaf = CapabilityToken( token_id="tok-leaf", parent_id="tok-root", declared_scopes=frozenset({"data:read:orders"}), lattice_version="v2", budget_units=500, ) verify_attenuation(root, leaf) ```