Skip to content

Delegation Chains

A delegation chain is a linked list of signed delegations that traces authority from an invoking agent back to a trusted root. Each link in the chain is cryptographically signed by its issuer and carries the caveats that constrain the delegated authority.

Root vs Chained Delegations

Root Delegations

A root delegation is issued by the root authority directly. It has no parent. The issuer does not need to prove that it was delegated authority - it is the authority.

Root (depth 0)
  issuer_did:  did:agent:root...
  delegate_did: did:agent:agentA...
  parent_proof: null

Root delegations are created with create_root:

rust
let root_kp = AgentKeyPair::generate();
let agent_a = AgentKeyPair::generate();

let delegation = Delegation::create_root(
    &root_kp,
    &agent_a.identity().did,
    vec![Caveat::ActionScope(vec!["resolve".into(), "search".into()])],
).unwrap();

assert_eq!(delegation.depth(), 0);
typescript
const rootKp = generateKeyPair();
const agentA = generateKeyPair();

const delegation = createRootDelegation(rootKp, agentA.identity.did, [
  { type: "action_scope", value: ["resolve", "search"] },
]);
python
root_kp = AgentKeyPair.generate()
agent_a = AgentKeyPair.generate()

delegation = Delegation.create_root(root_kp, agent_a.identity().did,
    json.dumps([{"type": "action_scope", "value": ["resolve", "search"]}]))

Chained Delegations

A chained delegation is issued by an agent that was itself delegated authority. The issuer must be the delegate_did of its parent delegation. The parent delegation is embedded in the parent_proof field, forming the chain.

Root delegation (depth 0)
  issuer:   root
  delegate: Agent A
  caveats:  [resolve, search]
       |
       v
Chained delegation (depth 1)
  issuer:   Agent A
  delegate: Agent B
  parent_proof: -> root delegation
  caveats:  [resolve, search] + [resource: entity:customer:*]

When chaining, the new delegation inherits all parent caveats and can add more. It cannot remove or weaken existing caveats.

rust
// Agent A sub-delegates to Agent B, adding a resource restriction
let delegation_b = Delegation::delegate(
    &agent_a_kp,
    &agent_b.identity().did,
    vec![Caveat::Resource("entity:customer:*".into())],
    root_delegation,  // the delegation A received from root
).unwrap();

assert_eq!(delegation_b.depth(), 1);
// delegation_b.caveats now contains BOTH the parent's ActionScope
// AND the new Resource caveat
typescript
const delegationB = delegateAuthority(
  agentAKp,
  agentB.identity.did,
  [{ type: "resource", value: "entity:customer:*" }],
  rootDelegation,
);
python
delegation_b = Delegation.delegate(agent_a_kp, agent_b.identity().did,
    json.dumps([{"type": "resource", "value": "entity:customer:*"}]),
    root_delegation)

The parent_proof Linkage

The parent_proof field embeds the entire parent delegation - not a reference to it, but the full object. This makes every delegation chain self-contained. A verifier does not need access to any external store to walk the chain.

json
{
  "issuer_did": "did:agent:agentA...",
  "delegate_did": "did:agent:agentB...",
  "caveats": [
    { "type": "action_scope", "value": ["resolve", "search"] },
    { "type": "resource", "value": "entity:customer:*" }
  ],
  "parent_proof": {
    "issuer_did": "did:agent:root...",
    "delegate_did": "did:agent:agentA...",
    "caveats": [
      { "type": "action_scope", "value": ["resolve", "search"] }
    ],
    "parent_proof": null,
    "proof": { "...root's signature..." }
  },
  "proof": { "...Agent A's signature..." }
}

During chain creation, the parent delegation's content hash is included in the signed payload. This cryptographically binds each link to its predecessor. Tampering with any link in the chain invalidates all downstream signatures.

Linkage Integrity

The delegate function enforces a critical invariant: the issuer of the new delegation must be the delegate_did of the parent. If Agent C tries to sub-delegate using Agent A's delegation (where Agent A delegated to Agent B), the call fails:

rust
// Agent C is NOT the delegate of the root delegation (Agent A is)
let result = Delegation::delegate(
    &agent_c_kp,
    &agent_d.identity().did,
    vec![],
    root_delegation, // delegate_did is Agent A, not Agent C
);
// Error: "issuer is not the delegate of parent delegation"

Embedded Public Keys

Every delegation embeds the issuer's raw public key bytes in the issuer_public_key field. This serves two purposes:

  1. Self-verifying chains - The verifier reconstructs the issuer's identity from the embedded key and checks the signature. No key resolver or directory lookup needed.

  2. DID binding - The verifier re-derives the DID from the embedded public key and confirms it matches the claimed issuer_did. A forged DID is detected immediately.

Verification of one link:
  1. Read issuer_public_key bytes
  2. Compute did:agent:{hash(pubkey)} from those bytes
  3. Confirm it equals issuer_did
  4. Verify proof signature using the public key
  -> If any step fails, the chain is broken

Chain Depth

The maximum chain depth is 32. This limit prevents denial-of-service attacks via deeply nested chains that consume unbounded verification time.

rust
pub const MAX_CHAIN_DEPTH = 32;

Depth is counted from the root:

DelegationDepth
Root creates delegation for A0
A delegates to B1
B delegates to C2
......
Delegation at depth 32Rejected

In practice, most chains are 1-3 levels deep:

  • Depth 0: Platform root delegates to a service agent
  • Depth 1: Service agent delegates to a task-specific worker
  • Depth 2: Worker delegates to a specialized sub-agent

If you are approaching depth 32, you likely have a design problem - not a depth limit problem.

Building a 3-Level Chain

Here is a complete example: a platform root delegates to a manager agent, who delegates to a worker, who performs a resolve action.

rust
use kanoniv_agent_auth::{
    AgentKeyPair, Delegation, Invocation, Caveat, verify_invocation,
};

// Three agents in a hierarchy
let root = AgentKeyPair::generate();
let manager = AgentKeyPair::generate();
let worker = AgentKeyPair::generate();

// Level 0: Root -> Manager
// Manager can resolve, search, and merge customer entities
let d1 = Delegation::create_root(
    &root,
    &manager.identity().did,
    vec![
        Caveat::ActionScope(vec![
            "resolve".into(), "search".into(), "merge".into(),
        ]),
        Caveat::Resource("entity:customer:*".into()),
        Caveat::ExpiresAt("2026-04-01T00:00:00.000Z".into()),
    ],
).unwrap();

// Level 1: Manager -> Worker
// Worker can only resolve (not search or merge), with a cost cap
let d2 = Delegation::delegate(
    &manager,
    &worker.identity().did,
    vec![
        Caveat::ActionScope(vec!["resolve".into()]),
        Caveat::MaxCost(2.0),
    ],
    d1,
).unwrap();

assert_eq!(d2.depth(), 1);

// Worker invokes its authority
let invocation = Invocation::create(
    &worker,
    "resolve",
    serde_json::json!({
        "entity_id": "cust-42",
        "resource": "entity:customer:cust-42",
        "cost": 0.50
    }),
    d2,
).unwrap();

// Verify the full chain: Worker -> Manager -> Root
let result = verify_invocation(
    &invocation,
    &worker.identity(),
    &root.identity(),
).unwrap();

assert_eq!(result.depth, 2);        // Worker -> Manager -> Root
assert_eq!(result.chain.len(), 3);  // [worker_did, manager_did, root_did]
assert_eq!(result.root_did, root.identity().did);
typescript
import {
  generateKeyPair,
  createRootDelegation,
  delegateAuthority,
  createInvocation,
  verifyInvocation,
} from "@kanoniv/agent-auth";

// Three agents in a hierarchy
const root = generateKeyPair();
const manager = generateKeyPair();
const worker = generateKeyPair();

// Level 0: Root -> Manager
const d1 = createRootDelegation(root, manager.identity.did, [
  { type: "action_scope", value: ["resolve", "search", "merge"] },
  { type: "resource", value: "entity:customer:*" },
  { type: "expires_at", value: "2026-04-01T00:00:00.000Z" },
]);

// Level 1: Manager -> Worker (narrowed to resolve only, with cost cap)
const d2 = delegateAuthority(manager, worker.identity.did, [
  { type: "action_scope", value: ["resolve"] },
  { type: "max_cost", value: 2.0 },
], d1);

// Worker invokes its authority
const invocation = createInvocation(worker, "resolve", {
  entity_id: "cust-42",
  resource: "entity:customer:cust-42",
  cost: 0.50,
}, d2);

// Verify the full chain
const result = verifyInvocation(invocation, worker.identity, root.identity);
console.log(result.depth);    // 2
console.log(result.chain);    // [worker_did, manager_did, root_did]
console.log(result.root_did); // did:agent:...
python
from kanoniv_agent_auth import AgentKeyPair, Delegation, Invocation, verify_invocation
import json

# Three agents in a hierarchy
root = AgentKeyPair.generate()
manager = AgentKeyPair.generate()
worker = AgentKeyPair.generate()

# Level 0: Root -> Manager
d1 = Delegation.create_root(root, manager.identity().did, json.dumps([
    {"type": "action_scope", "value": ["resolve", "search", "merge"]},
    {"type": "resource", "value": "entity:customer:*"},
    {"type": "expires_at", "value": "2026-04-01T00:00:00.000Z"},
]))

# Level 1: Manager -> Worker (narrowed to resolve only, with cost cap)
d2 = Delegation.delegate(manager, worker.identity().did, json.dumps([
    {"type": "action_scope", "value": ["resolve"]},
    {"type": "max_cost", "value": 2.0},
]), d1)

# Worker invokes its authority
invocation = Invocation.create(worker, "resolve", json.dumps({
    "entity_id": "cust-42",
    "resource": "entity:customer:cust-42",
    "cost": 0.50,
}), d2)

# Verify the full chain
invoker_did, root_did, chain, depth = verify_invocation(
    invocation, worker.identity(), root.identity())

print(f"Depth: {depth}")       # 2
print(f"Chain: {chain}")       # [worker_did, manager_did, root_did]
print(f"Root: {root_did}")     # did:agent:...

Chain Size Considerations

Because parent_proof embeds the full parent delegation (including its own parent_proof), chain size grows linearly with depth. A depth-3 chain contains 4 nested delegation objects.

For most use cases this is fine - delegation chains are created once and verified many times. But if you are building a system that creates thousands of delegations per second, keep chains shallow (depth 1-2) and prefer root delegations where possible.

DepthApproximate JSON size
0~500 bytes
1~1.2 KB
2~2.0 KB
3~2.8 KB

Common Patterns

Hub-and-Spoke

One root delegates directly to many workers. Every worker is at depth 0. Simple and flat.

        Root
       / | \
      A  B  C    (all depth-0 root delegations)

Hierarchical

Root delegates to managers, managers delegate to workers. Managers add restrictions specific to their domain.

          Root
         /    \
   Sales Mgr   Support Mgr
    /   \          |
  SDR  AE      Triage Agent

Ephemeral Task Delegation

An agent creates a short-lived delegation for a single task, with a tight expiry and context caveat binding it to a specific task ID.

rust
let task_delegation = Delegation::delegate(
    &manager,
    &worker.identity().did,
    vec![
        Caveat::ExpiresAt("2026-03-15T12:30:00.000Z".into()),  // 30-minute window
        Caveat::Context { key: "task_id".into(), value: "task-7f3a".into() },
    ],
    manager_delegation,
).unwrap();

The identity and delegation layer for AI agents.