Delegating credentials to sub-agents
When your agent spawns a worker — a VM, a sub-process, a CI runner — it can share a subset of its permissions without exposing its own identity.
When to delegate
- Multi-agent workflows
Parent agent orchestrates, sub-agents execute specific API calls. - Ephemeral VMs
Spin up a worker VM that needs GitHub access for 2 hours, then auto-expires. - CI / CD pipelines
Give your pipeline scoped API access without storing long-lived secrets. - Sandboxed execution
Run untrusted code with minimal permissions — it can't escalate beyond its delegate scope.
Create a scoped delegate
A scoped delegate gets a fixed subset of the parent's approved scopes. No dynamic expansion — what you give is what it gets.
$ keychains delegates add pr_a1b2c3 --scopes "github::repo" --ttl 3600
> Delegate created: del_x7y8z9
> Mode: scoped
> Scopes: github::repo
> Expires in: 1 hour
> Ship KEYCHAINS_DELEGATE_ID and KEYCHAINS_DELEGATE_KEY to the remote system
Create a wildcard delegate
A wildcard delegate starts with zero scopes and accumulates them as the user approves. The parent's scopes act as a hard ceiling.
$ keychains delegates add pr_a1b2c3 --wildcard --ttl 7200
> Delegate created: del_w1c2a3
> Mode: wildcard
> Scopes: (none — approved on demand)
> Parent ceiling: github::repo, google::gmail.readonly
Note: The wildcard delegate can only request scopes the parent has already approved. Requesting a scope outside the parent's set returns a hard scope_refused error — no approval URL.
The two-tier scope check
When a wildcard delegate requests a scope, two checks happen:
- Tier 1 — Parent Ceiling
Is the scope in the parent'sapprovedScopes? If no: hard refuse. No approval URL. - Tier 2 — Delegate Approval
Scope is in parent but not the delegate? Ask user — returns 403 with approval URL.
Using delegates from code
import { mintDelegateToken, proxyFetch } from '@keychains.dev/machine-sdk';
// On the remote system — env vars shipped from parent
const { token } = await mintDelegateToken(
process.env.KEYCHAINS_DELEGATE_ID,
process.env.KEYCHAINS_DELEGATE_KEY
);
const res = await proxyFetch('https://api.github.com/user/repos', { token });
Warning: Delegate private keys are secrets. Always use short TTLs and revoke delegates as soon as the task completes.
Revoking a delegate
Revoke from the CLI, SDK, or dashboard. Takes effect immediately.
$ keychains delegates rm del_x7y8z9
> Delegate del_x7y8z9 revoked
Tip: Revoking the parent permission automatically revokes all its delegates via cascade revocation.
Delegation mode matrix
| Parent | Delegate | Behavior |
|---|---|---|
| scoped | scoped | Fixed subset. No expansion. |
| scoped | wildcard | Accumulates up to parent's fixed set. |
| wildcard | scoped | Fixed subset of parent's dynamic pool. |
| wildcard | wildcard | Dynamic accumulation. Parent is hard ceiling. |