AI Agent Capability Tokens: Attenuation for Multi-Hop Delegation
A support orchestrator holds production refund credentials. It hands a "check order status" task to a research sub-agent. The sub-agent still carries the orchestrator's standing service account. An indirect injection in a ticket note convinces that sub-agent to call refund.create. Schema validation passes. Human approval never fires, because the platform already trusts the orchestrator's identity. The failure was not instruction wording. It was authority that could not narrow when the work hopped.
You need an attenuating capability-token contract: the parent issues a cryptographically constrained token for the job, each hop can only append narrower caveats, stolen bearer material cannot mint proofs without a holder key, caveats evaluate with exact deterministic predicates, and widening or unbounded re-delegation fails closed. It sits beside binding human approval to exact AI agent actions (the human authorization artifact), validating AI agent tool calls before execution (schema and policy on the proposed call), keeping API keys out of AI agent context (brokered secrets), instruction provenance contracts for AI agent delegation (non-escalating message roles), and signed skill and tool schema integrity (admit-time package authenticity). The job here is narrower: prove the tool authority a sub-agent carries is a strict attenuation of what the parent was allowed to hand down.
Why standing credentials break at the second hop
OWASP LLM06:2025 Excessive Agency frames the blast radius when agents get more functionality, permissions, or autonomy than the job needs. Mitigations start with fewer extensions, least privilege, complete mediation in downstream systems, logging, and rate limits. Central ACL checks at the tool boundary look like least privilege until you watch multi-hop work. Every call asks whether this agent identity is allowed. The answer is usually a broad standing grant, because nobody wants to rewrite IAM rows for every sub-agent spawn.
Google DeepMind's Intelligent AI Delegation paper separates ordinary subroutine calls from intelligent delegation: authority, responsibility, accountability, roles, boundaries, and trust travel with the task. Its permission-handling section requires privilege attenuation when an agent sub-delegates. The child must receive only the strict subset of resources the sub-task needs. That is the confused-deputy fix for agent chains: a valid credential in the wrong hop is still misuse if the hop never needed refund write.
Capframe's practitioner write-up names three rough edges of ACL-at-the-boundary designs for agents. The policy service becomes a hot-path dependency and single compromise point. The LLM still gets to probe whatever the broad identity allows. Delegation forces new policy rows instead of handing a narrower token offline. Capabilities invert the model: possession of a valid, sufficiently-narrow token is the permission.
The attack: authority that widens or never shrinks
Three failure modes show up in internal platforms.
- Standing identity inheritance. The orchestrator's service account is reused by every specialist. Call-time validation sees a trusted principal. The sub-agent's actual job never constrained the principal.
- Silent re-delegation. Agent A hands work to B, B to C, C to D. No depth cap. No purpose string. Audit later cannot reconstruct why D could write to production.
- Bearer token leakage. A capability string lands in logs, traces, or prompt context. Whoever copies it can replay the authority until expiry, because nothing requires holder proof.
AIP's evaluation of Invocation-Bound Capability Tokens makes the depth and audit gaps concrete. Across six attack categories and 600 attempts, chained attenuation rejected scope widening and depth violations that plain JWTs missed. Empty delegation context also slipped past JWT-only setups; AIP requires non-empty purpose context so silent hops cannot erase the audit trail. That is not a framework preference. It is the difference between a chain you can reconstruct and a chain that only looks fine in the final tool log.
Issue narrow scopes at the root
Start with issue-time discipline. A root token for a support session should name tools, parameter ceilings, budget, TTL, and max depth before any model runs.
Practical issue rules:
| Field | Example | Why it exists |
|---|---|---|
| Tools | order.read, refund.write | Binary allowlists beat "support agent role" |
| Parameter caveats | max_refund <= 50, region == "eu" | Bound the authorized-but-dumb path |
| Budget | budget_cents <= 500 | Cap spend even when tools stay in scope |
| TTL | 15-60 minutes | Prefer short life over revocation theater |
| Max depth | 1-3 | Stop unbounded specialist trees |
| Purpose | refund-review ticket 18422 | Force an auditable reason on every hop |
Eclipse Biscuit gives the public-key shape: verifiers need the issuer public key, not a shared HMAC root that turns every verifier into a potential forger. DeepMind's Delegation Capability Token framing and Ephyr's open-source DCT broker show the macaroon shape: HMAC-chained caveats make restriction removal cryptographically impossible, and an effective-envelope reducer intersects sets and takes mins so authority only narrows. Pick one primitive family and stay consistent inside a trust domain. Mixing Biscuit for some tools with an ambient AWS role for others is not a contract.
Mandato adds a useful legal-facing refinement: each grant can carry an oversight class (auto, confirm, forbid), and sub-mandates must not weaken oversight. A parent that required confirmation cannot silently become auto downstream. Wire that into your attenuation rules if humans are part of the chain.
Offline monotone attenuation across hops
When the orchestrator spawns a "status only" specialist, it must attenuate without opening a ticket against the central IAM table.
root = issue(
tools=["order.read", "refund.write"],
caveats=["max_refund <= 50", "region == eu"],
max_depth=2,
purpose="ticket-18422-support",
holder_pubkey=orchestrator_pub,
)
child = attenuate(
root,
tools=["order.read"], # refund.write removed
caveats=["region == eu"], # inherits, cannot widen
max_depth=1,
purpose="ticket-18422-status-check",
holder_pubkey=specialist_pub,
)
Microsoft's Agent Governance Toolkit Tutorial 23 states the monotonic rule in SDK terms: child capabilities must be a strict subset, depth increments, and delegate() throws if the child asks for more than the parent holds. Wildcard parents can narrow (data:* → data:read), but a data:read parent cannot mint data:*.
Require depth caps and non-empty purpose context on every hop; monotone narrowing alone still permits unbounded silent re-delegation. That is the third operating rule. AIP's adversarial table is the evidence: depth violation and empty-context audit evasion are exactly the cases JWT-style opaque tokens miss. Your verifier should reject max_depth exhaustion and blank purpose strings before the tool runs.
Bind holder keys; never trust bare possession
Bind every capability token to a holder key with per-call proof-of-possession; a bearer token in logs or prompt context is stolen authority. That is the first operating rule.
Capframe's construction folds an ed25519 public key into the macaroon chain and requires a signature over a per-call challenge:
challenge = H(token_id || tool || args || nonce || timestamp)
proof = ed25519_sign(agent_priv_key, challenge)
The private key lives in the agent runtime's secret store, never in prompt context - the same boundary as credential brokering. Ephyr's bind path makes the same point operationally: unbound tokens can be locked until a holder key is attached, and proof-of-possession is checked on the broker hot path. If your capability token can be copy-pasted from a LangSmith trace into curl and still work, you shipped a password with caveats, not a capability system.
Evaluate caveats exactly, outside the model
The authorization decision must be deterministic and LLM-free. Mandato states this explicitly: no model output participates in the decision, only in the actions being authorized. Capframe's float incident shows why "tiny DSL" still needs exactness.
Evaluate numeric caveats against exact source text, not float-coerced JSON; amount slips are authorization bugs, not rounding noise. That is the second operating rule. An attacker-controlled agent submitted amount: 50.000000000000001. Parsed as f64, it collapsed to 50.0 and passed amount <= 50. The fix was preserving arbitrary-precision source text so integer-domain caveats reject float-syntactic arguments outright. If your caveat evaluator rounds money into compliance, your least-privilege story is fiction at the money boundary.
Also keep deny evidence. Capframe emits signed denial receipts for failed caveats. Mandato hash-chains every permit and deny. Your existing AI agent audit logs should record token id, hop depth, purpose, failed predicate, and holder proof status outside model control.
Fail closed on widening and bypass
Minimum enforcement checklist:
- Verify the full chain. Root signature or macaroon root, every attenuation block, expiry, revocation/epoch watermark.
- Check subset semantics. Tools, budgets, regions, and oversight class must be narrower or equal - never wider, never weaker oversight.
- Enforce depth and purpose. Reject exhausted depth and empty context.
- Require holder proof. Reject missing, stale, or wrong-key proofs.
- Evaluate caveats on exact args. No float coercion, no undocumented string normalization that invents compliance.
- Block ambient bypass. Tool servers accept only the mediator/broker identity (mTLS or equivalent). An agent with a direct path to production APIs makes tokens optional.
function authorizeToolCall(call, token, holder_proof, policy):
chain = verifyChain(token, policy.issuer_keys, policy.epoch)
if not chain.ok:
return DENY("token", chain.error)
if not subset(call.tool, chain.tools):
return DENY("tool")
if chain.depth_remaining < 0:
return DENY("depth")
if empty(chain.purpose):
return DENY("purpose")
if not verifyHolderProof(token, call, holder_proof):
return DENY("holder")
for caveat in chain.caveats:
if not evalExact(caveat, call.args):
return DENY("caveat", caveat)
audit.allow(call, token, chain)
return ALLOW(brokered_credentials_for(call))
Notice the last line: even after the capability check passes, reusable secrets still come from the broker. The token authorizes the class of action. It does not become the database password.
Worked example: refund orchestrator to status specialist
- Human approves a support session under exact-action approval. The platform issues a root capability token:
order.read,refund.write,max_refund <= 50,max_depth=2, purposeticket-18422, holder = orchestrator key. - The orchestrator attenuates for a status specialist: tools collapse to
order.read, depth becomes 1, purpose becomesticket-18422-status, holder = specialist key.refund.writeis absent. - Injected ticket text tells the specialist to refund. The specialist can propose the tool call. Verification fails on tool subset before schema business rules matter.
- Later the orchestrator itself refunds €40 in-region with a valid holder proof. Caveats pass. The broker materializes a one-shot refund credential. Audit stores allow with token id and purpose.
- A leaked specialist token in a debug log cannot refund, cannot widen tools, and cannot prove holder possession for the orchestrator key.
Instruction provenance still matters: tool-origin text must not become a forged user message that looks like a human asked for the refund. Capability tokens do not replace that contract. They stop the hop that should never have refund authority from presenting any authority at all.
How this contract fits the rest of the stack
| Control | Owns | Does not replace |
|---|---|---|
| Capability-token attenuation (this article) | Carried, offline-narrowable tool authority across hops | Human approval digests, schema checks, package signing |
| Human-approval binding | Exact human authorization artifact | Multi-hop attenuation after approval |
| Tool-call validation | Schema, identity, business policy on one call | Whether the caller holds narrowed capability |
| Credential brokering | Secrets never enter model context | Task-scoped authority encoding |
| Instruction provenance | Non-escalating harness reconstruction | Tool-authority tokens after labels are correct |
| Skill/schema integrity | Admit-time authenticity of packages | Runtime who-may-call-what under caveats |
| Audit logs | Reconstructable allow/deny evidence | Cryptographic subset checks themselves |
Promote when every production multi-hop path carries an attenuating token with holder proofs, depth and purpose constraints, exact caveat evaluation, and no ambient standing credential bypass. Hold if specialists still inherit orchestrator cloud roles, if capability strings work as pure bearers, or if re-delegation has no depth cap.
Decision record: promote or hold
Promote this contract when:
- root tokens are issued with narrow tools, parameter ceilings, TTL, budget, depth, and purpose;
- every hop attenuates offline under monotone subset rules;
- holder-of-key proofs are required on privileged calls;
- caveat evaluation is exact and LLM-free;
- tool servers refuse direct agent egress that bypasses the broker/mediator;
- allow and deny events are auditable without reading model transcripts.
Hold when any production specialist can call privileged tools with the parent's standing identity, when tokens are bearer-replayable from logs, or when re-delegation has neither a depth cap nor a purpose string.
What to do next
List every production path where one agent spawns or messages another that can touch tools. For each path, record whether authority is a standing service account, a central ACL identity, or an attenuating capability token with holder binding, depth, purpose, and exact caveats. Until that inventory shows fail-closed narrowing on every hop, treat multi-agent orchestration as credential sharing with extra ceremony.
If you need the control plane implemented as part of an internal workflow program rather than a weekend token prototype, start from AI workflow automation with attenuating capability tokens as an explicit deliverable.