Child agents
A parent agent can mint a child identity for a subagent it’s about to spawn, via POST /v1/agents/children, issue_child in the Python SDK, or the issue_child_agent MCP tool. The child gets its own agent id, its own bearer token, and its own row in the audit log; it is never handed the parent’s key.
The mint flow
The parent calls the endpoint with the scopes it wants the child to have:
POST /v1/agents/children
{
"name": "research-worker",
"ttl_seconds": 600,
"grants": [
{ "platform": "github", "scopes": ["github:repo.read"] }
]
}The proxy attenuates that request against the parent’s live grants (not what the parent was granted historically, what it currently holds) and writes the intersection as the child’s own permissions rows at mint time. A grant the parent doesn’t currently hold is dropped from the child silently; if a whole platform attenuates to nothing, the mint fails with no_scopes_after_attenuation rather than issuing an empty grant. The response carries the child’s bearer token and its expiry:
{
"child_agent_id": "agt_...",
"bearer_token": "...",
"expires_at": "2026-08-21T13:15:00Z",
"granted": [
{ "platform": "github", "scopes": ["github:repo.read"], "require_approval": false }
]
}Put only bearer_token into the subagent’s hands. It builds its own client from that one string, using AgentValet.from_bearer() or the equivalent constructor.
Attenuation, not just a filter at mint time
The intersection with the parent’s grants isn’t a one-time check. services/child-liveness.ts re-derives it on every call the child makes, and again when a queued approval for a child’s action replays. Concretely, each governed call checks:
- The child hasn’t expired.
- The parent’s
agents.statusis stillactive. - The requested scope is still present in the parent’s current permission row for that platform.
That last check is what makes child ≤ parent an invariant of the read path rather than a property that only held at mint time. Narrow the parent’s grant after the child was minted, and the child loses that scope on its very next call, no re-mint required, no stale credential to chase down.
TTL
ttl_seconds is bounded to 60–3600 seconds (900 by default). A child’s grant expiry is also clamped to the parent’s own grant expiry, whichever is sooner: a child grant can never outlive the grant it was sliced from.
Depth is capped at 1
A child cannot itself call POST /v1/agents/children. If the parent has a parent_agent_id set, the mint request is rejected with delegation_depth_exceeded. If a worker needs to delegate further, route back through the parent agent, which is the one holding a real key.
Revoke the parent, contain every child
Suspending or revoking the parent doesn’t require walking the tree. Because liveness is re-derived from the parent’s row on every call, revoking or suspending the parent denies every one of its children on their very next call, with no cascading sweep needed. (A DB-level status cascade and a pg_cron sweep also exist for dashboard hygiene and garbage collection, but they aren’t what makes revocation effective; the call-time check is.)
Audit attribution
Every call a child makes is attributed to the child’s own agent_id in the audit log: “which subagent read that file” is a column, not a reconstruction from timestamps. The mint itself writes an agent.child_issued audit row against the parent, carrying the child id and the granted slice, so the parent-child pair survives even after the child row is eventually garbage-collected.
Honest caveats
This mechanism is only as good as the harness’s ability to hold a per-child credential. Two constraints ride every claim about it:
- Claude Code’s stdio Task-tool subagents share the parent’s MCP server. There’s no per-subagent hook the parent process can use to hand a distinct bearer to a Task-tool subprocess today, so those subagents are attributed to the parent connection. The Phase-1
session_idmarker is the attribution story there, not a child identity. Child identities are the real mechanism for harnesses whose orchestrator code holds the child bearer directly, including the Agent SDK, LangGraph, and CrewAI. - Approval replay doesn’t re-check mid-approval scope narrowing. If an owner narrows the parent’s grant after a child’s action is queued for approval but before the owner decides, the replay still checks the child’s expiry and the parent’s
activestatus, but not the specific scope against the parent’s current grants. This matches how adult agents already behave (an adult’s own grant narrowing isn’t re-checked at replay either), and it’s bounded by the approval queue’s expiry window and the fact that a human is approving the exact action, not a blanket permission.
Next
- LangGraph: the full pattern with code, supervisor and worker
POST /v1/agents/children: the raw endpoint- Scopes and permissions: how attenuation fits into the wider grant model
- Audit and revocation: killing an agent, parent or child