Swarm delegation
Multi-agent UCAN chains. Parent delegates to child; scope can only narrow. PDP enforces depth + attenuation.
A swarm is a tree of Apps with one root and N children. Children call upstream SaaS on behalf of the parent — every call carries the parent's UCAN as a proof, and scope can only narrow downstream (UCAN attenuation).
How the chain works
Each child UCAN is issued by the parent (parent's aud becomes child's iss).
The PDP validates every level on every request:
- Signature chain from leaf back to root is valid.
- Each parent's capability set is a strict superset of its child's.
- The chain depth doesn't exceed
NOMOS_MAX_CHAIN_DEPTH(default 8).
If any check fails, the request denies with chain_invalid or chain_too_deep.
Wire format
Three environment variables, orchestrator-agnostic — LangGraph, CrewAI, AutoGen, Claude sub-agents all work without importing the SDK:
NOMOS_PARENT_UCAN_CHAIN='["<rootJWT>","<midJWT>"]' # JSON array, root-first
NOMOS_PARENT_RECEIPT_ID='evt_…' # parent's last allow receipt
NOMOS_SWARM_ID='swm_…' # optional grouping
NOMOS_MAX_CHAIN_DEPTH=8 # default; PDP enforces
For chains that would exceed the env-var size limit (~128KB on Linux), use the file fallback:
NOMOS_PARENT_UCAN_CHAIN_FILE=/tmp/nomos-chain.json
Fork a child (TypeScript)
import { forkChild, createIntentClient } from '@auto-nomos/sdk';
import { mintUcan } from '@auto-nomos/ucan';
const parentChain = readParentChainFromEnv(process.env);
const childUcan = await mintUcan({
audience: 'did:key:z6Mk…researcher',
capabilities: [{ with: 'github://acme/app', can: 'repo:read' }],
});
const childChain = forkChild({
parentChain,
childUcanJwt: childUcan.jwt,
parentReceiptId: process.env.NOMOS_PARENT_RECEIPT_ID,
swarmId: process.env.NOMOS_SWARM_ID,
});
spawn('node', ['./researcher.js'], {
env: {
...process.env,
NOMOS_PARENT_UCAN_CHAIN: JSON.stringify(childChain.chain),
NOMOS_PARENT_RECEIPT_ID: childChain.parentReceiptId ?? '',
NOMOS_SWARM_ID: childChain.swarmId ?? '',
},
});
Fork a child (Python)
from nomos import fork_child, read_parent_chain_from_env
parent = read_parent_chain_from_env(os.environ)
child = fork_child(
parent_chain=parent.chain,
audience_did="did:key:z6Mk…researcher",
capabilities=[{"with":"github://acme/app","can":"repo:read"}],
)
env = {
**os.environ,
"NOMOS_PARENT_UCAN_CHAIN": json.dumps(child["chain"]),
"NOMOS_PARENT_RECEIPT_ID": child.get("parentReceiptId",""),
"NOMOS_SWARM_ID": child.get("swarmId",""),
}
subprocess.Popen(["python","./researcher.py"], env=env)
Cedar fragments for swarms
The swarm-safe schema-pack ships four templates that read chain attributes
delegationDepth, rootAgent, invokedBy:
// 1. cap chain depth
forbid ( principal, action, resource )
when { principal.delegationDepth > 3 };
// 2. pin root agent (lock a swarm to one owner)
forbid ( principal, action, resource )
unless { principal.rootAgent == "did:key:z6Mk…planner" };
// 3. block tainted ancestors
forbid ( principal, action, resource )
when { principal.invokedBy.contains("did:key:z6Mk…quarantined") };
// 4. require direct call for the most sensitive actions
forbid ( principal, action == Action::"/github/repo/put_file", resource )
when { principal.delegationDepth > 0 };
Swarm view
Swarms renders the tree:
- Agent tree — parent/child DAG built from
agents.parentAgentId. - Attach child agent — metadata only; the runtime UCAN chain must validate independently.
- Approve for chain — snapshot approval: covers the current set of agents in the tree. Children forked after approval are NOT covered.
- Scope containment — per-agent quick check (last decision, depth, last command).
- Recent receipts — last 100 authorize calls scoped to this swarm.

Observability — track what the swarm actually did
The swarm's Observability tab is the governance view: every tool call any agent in the chain made, who made it, what it cost, and whether it was allowed — reconstructed from signed receipts, not app logs.
Action graph
Each conversation flows left to right; a fork marks a sub-agent spawn. Spans are
colored per agent (the legend at the top maps each agent's DID to a color), and every
span shows its command, status, HTTP code, and latency. The graph polls every 5s over
the last 60 minutes. Failures stand out in red — here a /google/drive/list 403 and an
/azure/subscriptions/list 401 surfaced inline, so a denied or errored call in a deep
chain is visible without grepping logs.


Span detail
Click any span for the Span detail drawer — post-execution telemetry, not the raw
payload. The Summary tab shows the agent + DID, the receipt id, the parent span,
start/end, the declared intent, and — for delegations — the Handoff block
(MATCHED when the child's task lines up with the parent's), including the task,
expected output, and rationale. The request and response are stored as sha256
hashes plus an allowlisted summary.

Hashes, not bodies.
Only sha256 hashes plus an allowlisted summary are persisted — never raw request or response bodies. The hashes still let you prove, offline, exactly which payload ran.
Prompt capture + PII redaction
The Prompt tab captures the agent's prompt and reasoning. PII is redacted by
default — emails and phone numbers are detected and replaced with
[REDACTED:email] / [REDACTED:phone], with a count pill for each. The stored,
shareable view is the redacted one.
![Span detail Prompt tab showing prompt with [REDACTED:email] and [REDACTED:phone] and a show raw button](/_next/image?url=%2Fdocs%2Fscreenshots%2Fswarm-span-prompt-redacted.png&w=3840&q=75)
Show raw reveals the original — and that reveal is itself audit-logged
(RAW — READ AUDIT-LOGGED). Looking at unredacted prompt data is a governed action,
recorded like any other.

Action timeline
Below the graph, the Action timeline lists the last N tool invocations across the
whole scope — each with its decision (SUCCESS / FAILURE), command, receipt id,
timestamp, and latency. It's the flat, chronological companion to the graph.
Walking the audit tree
Every receipt carries parent_receipt_id. Recursively unwind any chain offline:
pnpm dlx @auto-nomos/audit-verify \
--chain writerReceipt.json \
--pubkey $AUDIT_VERIFY_KEY
# OK: 3 events, hash chain verified.
# ALLOW github://acme/app agent=planner depth=0 id=8c1f…
# └── ALLOW github://acme/app agent=researcher depth=1 id=92ab…
# └── STEPUP github://acme/app agent=writer depth=2 id=7fde…
Beta status
The wire format and Cedar templates are stable for the documented patterns. Edge cases (cyclic forks, leaf-only attenuation, cross-swarm chains) are still being spec'd. Open an issue if you hit something weird.