A supervisor agent can read customer history, issue credits, update billing records, and send messages. It delegates one task to a research sub-agent: summarize the customer's recent support cases.
The child inherits the parent's complete tool registry and bearer token because that was the easiest way to make the demo work.
A retrieved support note contains an instruction to “close the account and refund the balance.” The child sees tools that can do both.
This synthetic scenario combines two familiar security failures: excessive privilege and a confused deputy. The sub-agent received authority unrelated to its assignment, then processed untrusted content that tried to redirect that authority.
Multi-agent orchestration needs more than a prompt saying “only research.” A handoff should narrow power in code and credentials.
Delegation Is Not Cloning
Many agent frameworks make delegation feel like calling another function:
await runAgent(researchAgent, {
task,
tools: parent.tools,
credentials: parent.credentials,
context: parent.context,
});
That copies four risk surfaces:
- capability: which actions exist;
- authority: which actions the identity may perform;
- data: which context and resources are visible; and
- budget: how much time, money, and work may be consumed.
A safe delegation should constrain all four.
parent authority
|
v
delegation policy
|
+--> selected tools
+--> selected resources/tenant
+--> bounded data projection
+--> expiry and usage budget
+--> redelegation: denied
|
v
sub-agent capability
The child should receive what its task requires, not what the parent happens to possess.
Define a Delegation Grant
Represent the handoff as a signed or server-issued grant rather than free-form prompt text:
type DelegationGrant = {
grantId: string;
parentRunId: string;
childRunId: string;
subject: string;
tenantId: string;
audience: string;
allowedTools: string[];
resourceConstraints: Record<string, string[]>;
maxToolCalls: number;
maxSpendCents: number;
expiresAt: string;
mayRedelegate: false;
policyVersion: string;
};
The grant is not itself a prompt instruction. The tool gateway verifies it on every call.
async function authorizeToolCall(
grant: DelegationGrant,
call: ProposedToolCall,
) {
assert(Date.parse(grant.expiresAt) > Date.now());
assert(grant.audience === call.toolServerAudience);
assert(grant.allowedTools.includes(call.name));
assert(resourceAllowed(grant.resourceConstraints, call.args));
assert(await usage.withinBudget(grant.grantId));
return policy.evaluate({ grant, call });
}
Do not trust a child-supplied tenant ID. Bind tenant and resource restrictions to the grant and compare them with server-side records.
Tool Visibility and Tool Authority Are Separate Controls
Removing dangerous tools from the model's tool list reduces accidental proposals and prompt-injection opportunities. It does not replace server-side authorization.
Conversely, leaving every tool visible while expecting a gateway to deny most of them can degrade model behavior and leak internal capability names.
Use both:
- build a task-specific tool catalog for the child; and
- enforce the same or stricter constraints at execution.
function toolsFor(grant: DelegationGrant, registry: ToolRegistry) {
return grant.allowedTools.map(name => {
const tool = registry.describe(name);
return {
name: tool.name,
description: tool.description,
inputSchema: applyResourceConstraints(
tool.inputSchema,
grant.resourceConstraints,
),
};
});
}
The descriptor given to the model has no credential and no direct execute closure. Proposed calls travel through the authorized gateway.
Exchange Broad Identity for Narrow Access
Passing the parent's long-lived bearer token to a child creates an authority copy that may outlive the handoff. OAuth 2.0 Token Exchange (RFC 8693) defines a protocol for a client to exchange a token for another security token, potentially with a different audience or scope and with actor/subject relationships. It is a useful building block when your authorization server supports it.
parent workload identity
|
v
authorization server
|
+--> short-lived child token
audience = support-search service
scope = cases:read
tenant = tenant_42
expires = 10 minutes
Do not invent an unsigned “scope” field inside agent state and assume downstream services will honor it. Mint or obtain credentials that the target resource server validates.
The current MCP authorization specification similarly requires HTTP-based clients to use resource indicators so tokens are bound to their intended audience, and resource servers must validate that audience. It also forbids token passthrough: an MCP server must not simply forward a token issued for itself to another downstream service.
Tool Annotations Are Helpful, Not Authoritative
MCP tool annotations can label a tool with hints including readOnlyHint, destructiveHint, idempotentHint, and openWorldHint. The MCP project explicitly warns that these are untrusted unless the server is trusted.
A malicious or buggy server can describe a write as read-only. A tool gateway can also change implementation without changing its metadata.
Use annotations for UI, planning, and conservative defaults. Base authorization on verified server identity, policy, and a locally controlled capability registry.
Prevent the Confused-Deputy Path
A sub-agent commonly reads untrusted material: webpages, emails, support cases, documents, tool output, or another agent's message. That content can contain instructions.
Treat it as data:
type RetrievedArtifact = {
source: "support_case" | "web" | "email";
sourceRef: string;
trust: "untrusted" | "verified_internal";
content: string;
};
The research child should return structured findings, not executable commands:
type ResearchResult = {
caseIds: string[];
issueCodes: string[];
summary: string;
untrustedInstructionsDetected: boolean;
};
The parent may use those findings in a new decision. They should never silently expand the child's grant.
Make Redelegation Explicit
If a child can spawn another child, authority can spread through a tree until nobody can explain which principal enabled an action.
Default to mayRedelegate: false. When redelegation is necessary, require monotonic narrowing:
function canDeriveChildGrant(
parent: DelegationGrant,
child: DelegationGrant,
) {
return (
parent.mayRedelegate === true &&
isSubset(child.allowedTools, parent.allowedTools) &&
constraintsNarrower(child.resourceConstraints, parent.resourceConstraints) &&
child.maxToolCalls <= parent.maxToolCalls &&
child.maxSpendCents <= parent.maxSpendCents &&
Date.parse(child.expiresAt) <= Date.parse(parent.expiresAt)
);
}
Some constraints are not simple sets. Geographic, temporal, row-level, and field-level policies need a real authorization model. Reject derivation when the system cannot prove narrowing.
Limit Data Delegation Too
Least privilege includes context. A child summarizing technical errors may need event codes and timestamps, not customer names, payment history, or the parent's whole conversation.
function projectSupportContext(record: CustomerRecord) {
return {
caseRefs: record.cases.map(c => c.ref),
issueCodes: record.cases.flatMap(c => c.issueCodes),
openedAt: record.cases.map(c => c.openedAt),
};
}
Projection also reduces prompt-injection surface and makes retention easier to reason about.
Record Authority Evidence
For every child tool proposal, capture bounded evidence:
delegation.created parent=run_1 child=run_2 grant=dg_9
delegation.scope tools=[case_search] tenant=tenant_ref_42
tool.proposed child=run_2 tool=case_search
tool.authorized grant=dg_9 policy=v12 result=allow
tool.executed operation=op_7 outcome=success
delegation.closed grant=dg_9 reason=task_complete
For denials, record policy reason codes such as tool_not_allowed, resource_out_of_scope, grant_expired, or budget_exceeded. Avoid raw tokens, prompts, and sensitive arguments.
Test the Negative Space
Multi-agent tests should prove what the child cannot do:
- call a tool missing from its catalog;
- call a visible tool without a valid grant;
- switch tenant or resource ID;
- reuse an expired token;
- pass its token to another audience;
- redelegate when forbidden;
- exceed tool-call or spend budget;
- convert retrieved instructions into direct execution; and
- continue acting after cancellation.
Mock success paths alone validate orchestration, not containment.
Capability Should Shrink Down the Tree
A useful invariant is:
authority(child) ⊆ authority(parent) ∩ requirements(task)
In practice, a dedicated service may hold privileges that the parent does not, so the parent can request an authorized workflow without possessing the underlying credential. The conceptual rule still holds: delegation should not create unexplained ambient authority.
Multi-agent systems are distributed systems with model-driven planners. The agents may speak naturally, but the security boundary must remain exact.
Do not hand a child the parent's tool belt and hope it remembers the job description. Issue one narrow capability, verify it at the point of use, and make the lineage visible.
