# Which invariants prevent delegated capability tokens from crossing issuer trust boundaries when chain depth exceeds the verifier's policy horizon **Date of Record:** 2026 November 14 **Scope:** Applies to AAIF delegated capability tokens (DCTs) presented across two or more issuers where the verifier's trust-chain evaluator is configured with a bounded depth horizon and per-issuer attenuation policy. Does not apply to single-issuer delegation, to root-anchored tokens whose entire chain resolves within one trust domain, or to opaque bearer tokens with no delegation semantics. ## Core Distinction The failure is not signature verification failure, and it is not expiry. It is a *structural* mismatch between two quantities that are both individually valid: the **claimed chain depth** (the number of `iss`→`sub`→`act` hops a DCT asserts, encoded in the token's delegation chain) and the **verifier's evaluation horizon** (the maximum number of trust-domain transitions the verifier is willing to walk before deciding). When claimed depth ≤ horizon, the token is evaluated normally. When claimed depth > horizon, most naive implementations do not reject — they *truncate*, treating the last evaluated link as if it were the root anchor. The resulting authorization decision is made against a principal that is not the actual delegating principal. This is the non-obvious boundary: an over-deep chain does not fail closed by default; it fails *open to the wrong subject* unless the verifier explicitly enforces a depth-exceeded invariant. ## What Was Observed In a cross-issuer deployment, agent A (issued by IdP-1) delegated a capability to agent B (issued by IdP-2), which delegated to agent C (issued by IdP-3). The verifier V was configured with `max_chain_depth = 2` and a trust bundle containing IdP-1 and IdP-2 but not IdP-3. The DCT presented by C carried a three-hop `act` chain and a valid signature from B. V's evaluator walked the chain, resolved IdP-1 → IdP-2 within its bundle, and — because the loop terminated on horizon exhaustion rather than on anchor mismatch — emitted an `Allow` decision bound to B's identity, not C's. The `act` claim for C was silently dropped from the effective principal set. Downstream audit logs recorded the request as originating from B. The authorization was *correct for B's rights* but *wrong for C's request*, and C's actual capability set (attenuated twice) was never consulted. The defect surfaced only when a policy later denied B a resource that C legitimately held, producing a denial that was inexplicable from the logs. ## Condition of Validity This behavior is only reachable when all of the following hold simultaneously: (1) the chain crosses at least one issuer-trust-domain boundary; (2) the verifier applies a finite depth horizon; (3) the evaluator's termination condition is "horizon reached" rather than "anchor resolved or irreducible mismatch"; and (4) the intermediate principal at the horizon has *broader* rights than the terminal principal, so truncation yields a permissive superset. If the intermediate principal is strictly narrower, truncation fails closed by accident and the bug is invisible. The invariant that must hold — and that was violated — is: *the effective principal of a decision must equal the terminal subject of the presented chain, or the decision must be `Deny` with reason `depth_exceeded`.* Horizon exhaustion is not evidence of chain completion. ## Decision Boundary The correct boundary is a hard split between **anchor resolution** and **horizon exhaustion**. Anchor resolution means the evaluator reached a trust anchor it holds and the final hop's issuer matches that anchor: allow evaluation to proceed. Horizon exhaustion means the evaluator ran out of budget before reaching an anchor: this is a *distinct terminal state* and must map to `Deny` (or, in explicitly configured soft-fail deployments, to a `step_up` challenge), never to `Allow`. The two states must not share a code path. Additionally, the verifier must compare `claimed_depth` against `max_chain_depth` *before* walking, and reject with a typed depth error if `claimed_depth > max_chain_depth` — this converts a silent truncation into an explicit, loggable refusal. Attenuation monotonicity must be checked at every hop, not only at the horizon, because a truncated chain skips the hops where the most restrictive attenuation lives. ## Operational Effect Enforcing the split changes three operational properties. First, over-deep cross-issuer chains now produce a deterministic `depth_exceeded` denial with the claimed depth and the horizon both recorded, making the failure diagnosable from a single log line. Second, the effective principal in every audit record is provably the terminal subject, restoring the invariant that audit identity equals decision identity. Third. verifiers can safely lower `max_chain_depth` for latency or blast-radius reasons without silently widening authorization, because the lowering now manifests as denials rather than truncations. The cost is an increase in `depth_exceeded` denials during rollout, which is the correct signal that issuer trust bundles or horizons need adjustment — not a regression to be suppressed. ```python from dataclasses import dataclass from typing import Sequence class ChainDepthExceededError(RuntimeError): """Raised when a presented delegation chain cannot be fully resolved within the verifier's trust horizon. Must never be downgraded to Allow.""" @dataclass(frozen=True) class Hop: issuer: str subject: str attenuated_rights: frozenset[str] @dataclass(frozen=True) class Decision: allow: bool effective_principal: str | None reason: str def evaluate_chain( chain: Sequence[Hop], trust_anchors: frozenset[str], max_chain_depth: int, ) -> Decision: if not chain: return Decision(False, None, "empty_chain") claimed_depth = len(chain) if claimed_depth > max_chain_depth: # Pre-walk rejection: never truncate to the horizon. raise ChainDepthExceededError( f"claimed_depth={claimed_depth} exceeds horizon={max_chain_depth}; " f"terminal_subject={chain[-1].subject!r} must not be silently dropped" ) # Walk the chain, enforcing attenuation monotonicity at every hop. prev_rights = chain[0].attenuated_rights for i, hop in enumerate(chain): if i > 0 and not hop.attenuated_rights.issubset(prev_rights): return Decision(False, None, f"attenuation_widened_at_hop_{i}") prev_rights = hop.attenuated_rights # Anchor resolution is a distinct terminal state from horizon exhaustion. final_issuer = chain[-1].issuer if final_issuer not in trust_anchors: # We reached the end of the presented chain but did not land on an # anchor we hold. This is an irreducible mismatch, not a success. raise ChainDepthExceededError( f"chain terminated at issuer={final_issuer!r} which is not a held " f"trust anchor; cannot bind decision to terminal subject " f"{chain[-1].subject!r}" ) return Decision( allow=True, effective_principal=chain[-1].subject, reason="anchor_resolved", ) def authorize(request_principal: str, decision: Decision) -> None: if not decision.allow: raise ChainDepthExceededError( f"authorization denied for request_principal={request_principal!r}: " f"{decision.reason}" ) if decision.effective_principal != request_principal: # Invariant: decision identity must equal terminal subject identity. raise ChainDepthExceededError( f"principal_mismatch: decision bound to " f"{decision.effective_principal!r} but request claims " f"{request_principal!r}" ) ```