# Why does an instance of AAIF-scoped capability tokens fail silently when a swarm peer re-issues a delegated token across a trust-domain boundary? **Date of Record:** 2026 November 14 **Scope:** Applies to AAIF (Agent-to-Agent Invocation Framework) capability tokens that are attenuated and re-delegated across swarm peers spanning distinct trust domains, where token verification is stateless at the resource. Does not apply to single-domain swarms sharing one issuer root, nor to tokens that are never re-delegated (leaf-only capabilities). ## Core Distinction A capability token carries two logically independent facts: **what it authorizes** (the scope/attenuation chain) and **who vouches for it** (the issuer chain). The common mental model conflates these — that attenuation *narrows* authority and therefore cannot *escalate* it. That is true within a single trust domain. It is false across a trust-domain boundary, because re-issuance by a peer replaces the *vouching root* while preserving the *scope string*. The token that arrives at the resource looks narrower in scope than the one the originator minted, but its provenance root has silently shifted from the originator's domain to the peer's domain. Verification that checks scope-attenuation monotonicity but does not pin the issuer root will accept a token whose effective authority is defined by a domain the originator never intended to trust. The invariant that actually holds is: *scope is monotone non-increasing along the delegation chain, but trust-root identity is not preserved by re-issuance unless explicitly chained.* These are different invariants, and AAIF's default verifier only enforces the first. ## What Was Observed In a federated swarm with three domains — `origin` (issues root capability `tool:db.read` scoped to table `orders`), `broker` (a peer that fans out work), and `resource` (the DB gateway) — the following sequence was reproduced: 1. `origin` mints `cap{scope: db.read@orders, iss: origin, sub: agent-A}`. 2. `agent-A` attenuates to `cap{scope: db.read@orders, iss: origin, sub: agent-B, atten: [orders]}` and hands to `broker`. 3. `broker` — acting as a *re-issuer*, not a *forwarder* — mints a fresh token `cap{scope: db.read@orders, iss: broker, sub: agent-C}` because its local policy engine only re-signs tokens it can fully validate, and it validated the incoming token against its own mirror of `origin`'s keys. 4. `agent-C` presents the broker-issued token to `resource`. The `resource` verifier checked: (a) signature valid against *some* trusted issuer — `broker` is in the trust set; (b) scope string `db.read@orders` is within the resource's allow-list. Both pass. Access granted. The failure surfaced later: `origin` revoked `agent-B`'s capability after detecting anomalous fan-out. The revocation propagated to `broker`, but `agent-C`'s token — signed by `broker`, bearing no reference to `agent-B` — was not in the revocation set. It remained valid for the full TTL (observed: 4 hours) and continued to read `orders`. The revocation was, from the resource's perspective, a no-op for that token. Two quieter variants were also observed. First, a broker that re-issued with the *same subject* but a *different* `iss` produced tokens that passed scope checks yet failed audit correlation, because the audit log keyed on `iss` and the origin's logs never showed the downstream reads. Second, a peer that re-issued with an *empty* attenuation list (treating "no further narrowing" as "no attenuation record") produced a token whose scope string was identical to the root but whose attenuation proof was gone — indistinguishable at the resource from a full-authority token. ## Condition of Validity The silent-escalation failure requires all of the following to co-occur; removing any one prevents it: - **Re-issuance rather than forwarding.** The intermediary mints a new token with its own `iss` instead of passing the original token through with an added signature. Forwarding preserves the origin root and keeps revocation correlated. - **Stateless resource verification.** The resource validates signature + scope but does not walk the delegation chain back to the origin root, and does not consult a revocation feed keyed by the *chain*, only by the presented token's `iss`. - **Scope-string equivalence across the boundary.** The re-issued scope is a subset (often an equal set) of the original, so monotonicity checks pass. If the broker had *narrowed* scope, the mismatch would be less likely to matter operationally; the danger is highest when scope is preserved exactly. - **Trust set that includes the intermediary.** If `broker` is not in the resource's trusted-issuer set, the re-issued token is rejected and the failure is loud, not silent. The condition is *invalid* (failure cannot occur) when the resource performs full chain validation to a pinned origin root, or when the swarm enforces a single issuer root and forbids peer re-issuance, or when revocation is keyed by subject identity rather than by token. ## Decision Boundary The operational decision is whether to treat a peer as a **forwarder** or a **re-issuer**, and to encode that choice in the token's structure rather than in prose policy. - Choose **forwarding** (append-signature, preserve `iss` root) when revocation correlation and audit provenance matter more than per-peer policy enforcement. Cost: the origin's key must be resolvable by every downstream resource; the origin becomes a soft single point of failure for verification. - Choose **re-issuance** (new `iss`, new root) only when the re-issuer is willing to *own* the full lifecycle of the delegated authority — including revocation propagation and audit attribution — for every token it mints. If the broker cannot guarantee it will revoke downstream tokens when it revokes upstream ones, re-issuance is the wrong choice regardless of convenience. The boundary is crossed the moment a peer mints a token whose authority derives from an upstream token but whose revocation set does not include that upstream token's revocation events. At that point the peer has created authority it cannot retract, and the least-privilege guarantee is broken at the seam, not at the leaf. A concrete rule that held in testing: **a re-issued token must carry a `parent_digest` field referencing the exact upstream token it attenuates, and the resource must reject any re-issued token whose `parent_digest` is absent or unresolvable.** This restores revocation correlation without requiring the resource to trust the origin's keys directly — the resource trusts the broker, but the broker's token is bound to a parent the broker is obligated to revoke in lockstep. ## Operational Effect Left unaddressed, the failure converts least-privilege from a property of the *capability graph* into a property of *each verifier's local trust set* — which is exactly the property least-privilege was meant to avoid depending on. Revocation latency becomes unbounded (bounded only by token TTL), audit trails fragment across issuer domains, and a compromised or merely over-eager broker can amplify authority it was only meant to relay. The blast radius is not the broker's scope but the union of scopes it has ever re-issued, retained until TTL expiry. The mitigation with the best cost/benefit observed was: (1) require `parent_digest` on all re-issued tokens, (2) make the resource's revocation check walk the digest chain to the origin root, and (3) cap re-issued token TTL at the minimum of the parent's remaining TTL and the peer's local policy TTL. This bounds revocation latency to the parent's TTL and makes the seam explicit. It does not eliminate the need to decide forwarder-vs-re-issuer; it makes the wrong choice fail loudly instead of silently. ```python import hashlib from dataclasses import dataclass, field from typing import Optional class CapabilityChainError(RuntimeError): """Raised when a re-issued capability token cannot be correlated to its parent.""" @dataclass class CapabilityToken: iss: str sub: str scope: str parent_digest: Optional[str] = None ttl_seconds: int = 3600 attenuation: tuple = field(default_factory=tuple) def digest(self) -> str: material = f"{self.iss}|{self.sub}|{self.scope}|{self.parent_digest}|{self.ttl_seconds}" return hashlib.sha256(material.encode()).hexdigest() def reissue(parent: CapabilityToken, broker: str, subject: str, narrowed_scope: str, broker_ttl: int) -> CapabilityToken: """Broker re-issues a token attenuated from `parent`. Enforces the two invariants that the default AAIF verifier omits: 1. scope is monotone non-increasing (subset of parent scope), 2. the re-issued token carries parent_digest so revocation can be correlated. """ if not _is_subset_scope(narrowed_scope, parent.scope): raise CapabilityChainError( f"scope escalation: {narrowed_scope!r} not within parent {parent.scope!r}" ) if parent.parent_digest is None and parent.iss != broker: # parent is a root from another domain; re-issuance must still be chained pass # allowed, but digest binding below is mandatory effective_ttl = min(broker_ttl, parent.ttl_seconds) child = CapabilityToken( iss=broker, sub=subject, scope=narrowed_scope, parent_digest=parent.digest(), ttl_seconds=effective_ttl, attenuation=parent.attenuation + (narrowed_scope,), ) return child def verify_at_resource(token: CapabilityToken, trusted_issuers: set, revoked_digests: set) -> bool: """Stateless-ish verifier that walks the parent chain to catch silent escalation.""" if token.iss not in trusted_issuers: return False if token.digest() in revoked_digests: return False if token.parent_digest is not None: # A re-issued token MUST be correlated to a known parent; if the parent # digest is revoked, the child is revoked transitively. if token.parent_digest in revoked_digests: return False if token.parent_digest not in _known_parent_digests(): raise CapabilityChainError( f"re-issued token {token.digest()[:12]} references unknown parent " f"{token.parent_digest[:12]}; refusing silent trust-root shift" ) return True def _is_subset_scope(child: str, parent: str) -> bool: return child == parent or child.startswith(parent.split("@")[0]) def _known_parent_digests() -> set: # In production this is a replicated index of issued-token digests per domain. return set() # --- reproduction of the observed failure mode --- def reproduce_silent_escalation(): origin_root = CapabilityToken(iss="origin", sub="agent-A", scope="db.read@orders") broker_token = reissue(origin_root, broker="broker", subject="agent-C", narrowed_scope="db.read@orders", broker_ttl=14400) # origin revokes agent-B's capability; broker's re-issued token is NOT in that set revoked = {hashlib.sha256(b"agent-B-token").hexdigest()} if broker_token.parent_digest is None: raise CapabilityChainError("re-issued token has no parent binding") # With parent binding, revocation of the parent transitively kills the child: revoked.add(origin_root.digest()) assert verify_at_resource(broker_token, {"broker"}, revoked) is False ```