Author policies

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:

  1. Signature chain from leaf back to root is valid.
  2. Each parent's capability set is a strict superset of its child's.
  3. 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:

bash
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:

bash
NOMOS_PARENT_UCAN_CHAIN_FILE=/tmp/nomos-chain.json

Fork a child (TypeScript)

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)

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:

cedar
// 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.
Swarm view with agent tree and recent receipts
One swarm view per root. Tree shape lives in DB; runtime chain validation is independent.

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.

Swarm Observability action graph, four agents reading github left to right with fork connectors
Read it left to right. Each box is one signed receipt; forks are sub-agent spawns.
Swarm action graph fanning across github/discord/slack/google/azure with two red FAILURE spans
A real run: multi-provider fan-out, with a google 403 and azure 401 caught in red.

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.

Span detail Summary tab with agent/DID, receipt, intent, a MATCHED handoff block, and request/response sha256 hashes
Handoffs are verified, not assumed: MATCHED means the child's task derives from the parent's. Bodies are hashed, never stored raw.

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
What's stored: redacted prompt + reasoning. The pills count how much PII was scrubbed.

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.

Span detail Prompt tab after Show raw, with the RAW — READ AUDIT-LOGGED badge and the original email + phone visible
Revealing raw PII writes its own audit row. Who looked, and when, is on the record.

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:

bash
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.