Six months ago, a production incident landed in my inbox. An AI agent, authorized to manage a customer's cloud infrastructure, had spawned a sub-agent to handle cleanup work. That sub-agent deleted a production database. The logs said "an agent did it." Which agent, on behalf of which user, with what authorization, was not clear at all.
The team spent four hours reconstructing what had happened from application logs that were never designed for this kind of forensics. The root problem was not the deletion itself. It was that the system had no model for delegation. The sub-agent inherited the parent's full permissions because that was the path of least resistance.
I have seen variations of this story several times now. The specifics differ. The underlying pattern is always the same: someone reached for a fast shortcut when wiring up agents to each other, and that shortcut stored up a problem for later. This post covers the three shortcuts and what to do instead.
01
Pattern 1: shared API key
The fastest way to get a parent agent to call a sub-agent is to give both of them the same API key. One environment variable, both agents can reach everything the key permits. This takes about two minutes to set up.
The audit trail from this setup logs every action under the same identity. You cannot tell which agent initiated a deletion. You cannot revoke the sub-agent without also revoking the parent. If the key leaks anywhere in the chain, every agent using it is compromised simultaneously.
// DO NOT DO THIS
const sharedKey = process.env.API_KEY;
const parentAgent = new Agent({ apiKey: sharedKey });
const subAgent = new Agent({ apiKey: sharedKey }); // same key
// Both agents have identical permissions.
// You cannot revoke one without the other.
// Your audit trail will say "API key abc123" for every action.02
Pattern 2: cloned parent token
A slightly more thoughtful version: the parent agent receives a JWT from the authorization server, then passes that token directly to the sub-agent it spawns. The sub-agent uses the same token to make calls.
This feels better because the token is user-specific. But it has the same core problem. The sub-agent acts with the full scope of the parent's token. The token was not issued to the sub-agent. The authorization server has no record that a sub-agent exists. If the token expires mid-task, the sub-agent crashes with no way to refresh without exposing the parent's credentials.
// DO NOT DO THIS
async function parentWork(userToken: string) {
// Parent passes its own token to the sub-agent
const sub = spawnSubAgent({ token: userToken });
await sub.doWork();
// Sub-agent has parent's full scope.
// If it misbehaves, you cannot distinguish parent vs sub in logs.
}03
Pattern 3: flat service accounts
The third pattern is more structured but breaks in a different way. You create a dedicated service account for each agent type. The search agent has its own credentials, the PR agent has its own credentials, the comment agent has its own.
The service accounts are not connected to the user who authorized the original action. They exist independently at the platform level. If a user revokes the parent agent's authorization, the sub-agent service accounts keep running. You now have orphaned agents with live credentials acting on behalf of a user who has explicitly withdrawn consent.
04
The correct model: scoped delegation with TTL
The right model has four properties. Scope narrowing: a sub-agent can only receive a subset of the permissions its parent holds. No sub-agent can escalate beyond the parent. Explicit TTL: every delegated credential expires. The sub-agent cannot outlive its task. Chain linkage: every token in the chain carries a reference back to the token that spawned it, all the way back to the original user authorization. Cascade revocation: revoking any link in the chain revokes all downstream links.
Here is what that looks like in practice. A user authorizes a coding assistant. The assistant calls kavachos.agents.delegate() to spawn a search sub-agent with only read:repos scope and a 30-minute TTL. The sub-agent gets a fresh token issued specifically to it, tied to the parent's token in the authorization chain.
import { createKavach } from 'kavachos';
const kavach = createKavach({
apiKey: process.env.KAVACHOS_API_KEY,
});
// Parent agent has broad scope from user authorization
const parentAgent = await kavach.agents.create({
name: 'coding-assistant',
permissions: ['read:repos', 'write:prs', 'read:issues'],
delegatedFrom: session.userId,
});
// Sub-agent receives only the permissions it actually needs
const searchAgent = await kavach.agents.delegate({
from: parentAgent.id,
permissions: ['read:repos'], // subset only
ttl: '30m', // hard expiry
maxDepth: 1, // cannot delegate further
name: 'search-subagent',
});
// searchAgent.token is a fresh JWT:
// - issued to searchAgent specifically
// - audience = searchAgent.id
// - parent claim points to parentAgent.id
// - scopes are a strict subset
// - exp is 30 minutes from now
// When parentAgent is revoked, searchAgent.token is
// immediately invalid on the next request.The audit trail from this setup is unambiguous. Every call the search agent makes logs to the audit trail with the full chain: user ID, parent agent ID, sub-agent ID, action, scope used, and timestamp. When something goes wrong, you can trace exactly which agent made which call and why it had the permission to do so.
05
Depth limits and why they matter
When you let agents delegate to sub-agents without a depth cap, you can end up with chains four or five levels deep. Each level requires the authorization server to validate the entire ancestry. Performance aside, deep chains are hard to reason about when something goes wrong.
kavachOS enforces a configurable depth limit. The default is 3. If an agent at depth 3 tries to call delegate(), it receives a 403 with DELEGATION_DEPTH_EXCEEDED. You can raise the limit per-project if you genuinely need deeper chains, but three is usually where architectural problems surface before you actually need level four. See permissions for the full configuration reference.
06
Broken vs. correct side by side
- Shared API key: one revocation kills all agents.
- Cloned parent token: sub-agent has full parent scope, no independent identity.
- Flat service accounts: user revocation does not propagate to sub-agents.
- All three: audit trail says "an agent" with no chain context.
- Each sub-agent gets a unique token scoped to its task.
- Permissions narrow at each delegation. No escalation possible.
- TTL caps exposure. Sub-agent credentials expire with the task.
- Revocation cascades: one call revokes the full downstream chain.
The full delegation API is documented under delegation chains. The agent identity page covers how tokens are structured and what claims the authorization server adds to each one in the chain.
Topics
- #agent delegation
- #sub-agents
- #agent auth patterns
- #AI agent permissions
- #delegation chain
Keep going in the docs
Agents
Delegation chains
Let a root agent spawn sub-agents with narrowed permissions, depth limits, and audit cascade.
Agents
Agent identity
Cryptographic bearer tokens, wildcard permissions, and per-agent budgets. The core primitive.
Agents
Permissions
Wildcard matching, scope contracts, and inline policy checks. How to say what an agent can do.
Agents
Audit trails
Tamper-evident logs of every token issue, call, and denial. EU AI Act and SOC 2 ready.
Read next
- Engineering
Why AI agents need their own auth
User auth was never designed for software that makes API calls while you sleep. Here is what is different.
5 min read - Deep Dive
MCP OAuth 2.1 explained for every MCP server in 2026
The four RFCs that define MCP auth, why they matter, and a flow diagram you can read on a Tuesday.
10 min read
Share this post
Get started
Delegation chains that actually work
Scoped sub-agent credentials, cascade revocation, and a full audit trail. Free up to 1,000 MAU.