@aroha-sdk/delegationNo wire changesFails closed

Agent webs — multi-hop delegation

Build networks of agents — A delegates to B, which delegates to C and D, which may call MCP servers — where every hop carries signed, verifiable, shrinking authority, and a receipt tree returns to whoever started the chain.

A (root issuer) ──► B (orchestrator) ──► C (searcher)
                                     └─► D (summariser) ──► MCP tools

A node in the web

serveDelegated() wraps serve() from @aroha-sdk/run. Requests without a valid mandate chain are rejected with 400 before your handler runs.

import { serveDelegated, registryResolver } from "@aroha-sdk/delegation";

serveDelegated("orchestrator", {
  identity: { did: "did:aroha:acme:orchestrator", privateKey: myKey },
  trustAnchors: { "did:aroha:human:alice": alicePublicKeyB64 },
  resolvePublicKey: registryResolver(),  // staticResolver({...}) for closed networks
}, async (ctx) => {
  ctx.assertCapability("research");   // throws unless the verified mandate allows it

  // Delegate onward — authority is attenuated automatically:
  // subset scope, depth − 1, blocked list carried forward, same correlationId
  const search  = await ctx.delegate(searcherDidHash, ctx.message,
                                     { allowed: ["web-search"] });
  const summary = await ctx.delegate(summariserDidHash, search.message,
                                     { allowed: ["summarise"] });
  return summary.message;
}).start(8000);

Starting a chain

The root issuer — a personal agent, a CLI, your backend — grants the first mandate and calls the first agent. maxDelegationDepth bounds how deep the web below it may grow.

import { issueDelegation, callDelegated } from "@aroha-sdk/delegation";

const { envelope } = await issueDelegation(
  { did: "did:aroha:human:alice", privateKey: aliceKey },
  orchestratorDid,
  {
    allowed: ["research", "web-search", "summarise"],
    constraints: { maxDelegationDepth: 1 },  // B may delegate once; C/D may not
    ttlMs: 60_000,
  },
);

const res = await callDelegated(orchestratorEndpoint, "history of agent protocols", envelope);

console.log(res.message);
console.log(res.receipts[0]);   // B's receipt, with C's and D's nested in .children

What the chain guarantees

Each hop appends one signed mandate to the envelope. Every node runs verifyMandateChain() over the whole path, root → leaf:

CheckAttack it stops
Every link's Ed25519 signatureForged or tampered mandates
Root key pinned to your trustAnchorsAn attacker minting their own “root” authority
grantor(i) === grantee(i−1) + parentMandateId linkageSplicing a mandate from another chain
allowed(i) ⊆ allowed(i−1)Scope widening mid-chain
blocked list must survive every hopLaundering a ban through a sub-agent
Child expiry ≤ parent expiryZombie authority outliving its grant
maxDelegationDepth strictly decrementsRunaway agent-spawns-agent recursion

Depth is a hard stop. maxDelegationDepth: 0 makes a mandate a dead end — ctx.delegate() refuses with DELEGATION_DEPTH_EXCEEDED before a child token is even signed, and every downstream verifier independently re-checks the decrement.

Receipts come back as a tree

Every node builds a TaskReceipt automatically — actions, violations, timings — and ctx.delegate() nests downstream receipts, so the root issuer receives the whole execution tree under one correlationId:

orchestrator (complete)
 ├─ searcher   (complete) — actions: web-search
 └─ summariser (complete) — actions: summarise

Run the full web locally

The SDK ships a complete A → B → (C, D) example — including a blocked over-delegation, a rejected forged root, and an out-of-grant refusal. Install @aroha-sdk/delegation and adapt it, or read the mandate model first: