# In what scenarios must an instance refuse delegation in AAIF credential chains: the attenuation-inversion boundary at tool-scope intersection **Date of Record:** 2026 November 12 **Scope:** Applies to AAIF delegated credential chains where a parent agent issues per-tool scoped tokens to child agents and the child's effective authority is computed as the intersection of inherited scopes and locally granted tool scopes. Does not apply to flat (non-delegated) service-account models, nor to chains that terminate at a human principal without machine re-delegation. ## Core Distinction The load-bearing distinction is between **scope attenuation** (the child's authority is a subset of the parent's) and **scope *intersection* semantics** (the child's authority is the intersection of two independently-authored scope sets). These are identical when the parent's scope set is a superset of the child's request, which is the happy path every conformance test exercises. They diverge precisely when the child's locally granted tool scopes and the parent's inherited scopes are *disjoint on some axis* — the intersection is empty or narrower than either input, and the resulting token is well-formed, signature-valid, and *silently more restrictive than the delegating agent intended*. The non-obvious failure is not privilege escalation. It is **attenuation inversion**: a child agent presenting a credential that is cryptographically valid and semantically *less* capable than the parent believed it granted, causing the child to fail closed on a tool call the parent's policy explicitly authorized, while the parent's audit log records the delegation as successful. The chain does not break; it narrows in a direction no participant authored. ## What Was Observed A three-hop chain: orchestrator `O` → planner `P` → executor `E`. `O` holds `tools:read, tools:write, tools:exec` on resource class `R`. `P` is granted `tools:read, tools:exec` (a legitimate attenuation). `E` is granted, by `P`, `tools:exec` scoped to `R` with a *resource constraint* of `dataset=prod`. `O`'s original grant carried `dataset=prod,dataset=staging`. The AAIF token issued to `E` validated on signature and expiry. It carried `tools:exec` and `dataset=prod`. This is correct. The failure surfaced one hop earlier: `P`'s own token, minted by `O`, had been attenuated to `tools:read, tools:exec` — and the attenuation step, implemented as a set intersection over the `(tool, resource-constraint)` product space rather than over tools alone, dropped `dataset=staging` from `P` because `O`'s policy for `P` listed only `dataset=prod` on the `exec` tool but both datasets on `read`. When `P` attempted to delegate `exec@staging` downstream to a sibling executor `E2`, the intersection of `{prod}` and `{staging}` was empty, `E2` received a token with an empty resource set, and every call failed with `ScopeEmpty` at the tool gateway — three hops from where the policy author made the decision. The audit trail showed three successful delegations and one anomalous runtime denial with no causal link. Root-cause took 40 minutes because every individual token was individually valid. ## Condition of Validity Attenuation inversion is only possible when all of the following hold: 1. The chain uses **intersection** (not subset-check) semantics to compute child authority. If the implementation *rejects* a delegation whose requested scope is not a subset of the parent's, inversion cannot occur — the bad delegation fails at mint time, loudly. 2. At least one scope axis (tool, resource, action, time-window) is **multi-valued** and the parent's grant and child's request are **partially overlapping** on that axis. Disjoint-or-superset cases are safe. 3. The intersection is computed **per-axis independently** rather than over the full `(tool × resource × action)` tuple product. Per-axis intersection of `{prod,staging}` with `{prod}` yields `{prod}` on resource and `{exec}` on tool — a token that *looks* correct but whose cross-product is narrower than either author intended when the axes are correlated. 4. No hop performs a **post-intersection emptiness check** on each axis and escalates to the delegating principal. Remove any one condition and the failure degrades to a clean rejection. ## Decision Boundary An instance must **refuse to mint** a delegated token — raising and halting the chain — rather than emit an attenuated token, when the computed intersection on any scope axis is **empty while the parent's grant on that axis was non-empty**. Emitting an empty-axis token is the inversion: it converts a policy conflict into a downstream runtime denial attributed to the wrong hop. An instance must **mint and proceed** when the intersection is a non-empty strict subset on every axis (normal attenuation) or equals the parent's grant (identity delegation). The subtle middle case: intersection is non-empty on every axis but the *cross-product cardinality* dropped versus both inputs (e.g., `exec@{prod,staging}` ∩ `{read,exec}@{prod}` = `exec@{prod}`, cardinality 1, down from 2 and 2). This is legitimate attenuation and must proceed — refusing here would break every real multi-tool chain. The discriminator is **per-axis emptiness**, not total cardinality. ## Operational Effect An instance that applies the per-axis emptiness refusal converts a silent three-hop denial into a single-hop `DelegationScopeConflict` at mint time, carrying both the parent grant and the child request on the offending axis. The delegating principal receives the conflict synchronously and can widen the parent grant or narrow the child request — the decision returns to the party who authored the policy. Chains that skip this check accumulate empty-axis tokens that surface as tool-gateway denials with no attribution path, and the resulting mean-time-to-diagnosis scales with chain depth rather than with the number of distinct policies. Enforce the check at every mint, not only at the root, because the inversion is introduced by the *intermediate* attenuation, not the leaf. ```python from dataclasses import dataclass, field from typing import FrozenSet class DelegationScopeConflict(RuntimeError): """Raised when a delegated scope intersects to empty on an axis whose parent grant was non-empty. Must halt the chain; do not emit the token.""" @dataclass(frozen=True) class Scope: tools: FrozenSet[str] = field(default_factory=frozenset) resources: FrozenSet[str] = field(default_factory=frozenset) def attenuate(parent: Scope, requested: Scope) -> Scope: """Compute child authority as per-axis intersection of parent and request. Refuses (raises) if any axis that was non-empty in the parent becomes empty after intersection -- the attenuation-inversion boundary. """ child = Scope( tools=parent.tools & requested.tools, resources=parent.resources & requested.resources, ) for axis in ("tools", "resources"): parent_vals = getattr(parent, axis) child_vals = getattr(child, axis) if parent_vals and not child_vals: raise DelegationScopeConflict( f"attenuation inversion on axis '{axis}': " f"parent={sorted(parent_vals)} requested={sorted(getattr(requested, axis))} " f"-> intersection empty; refusing to mint delegated token" ) return child def mint_delegated_token(parent: Scope, requested: Scope) -> Scope: return attenuate(parent, requested) ```