Delegation Quickstart
Delegation lets one agent grant another agent a subset of its authority. The grant is cryptographic - a signed statement saying "I authorize agent X to do Y, subject to constraints Z." Constraints (called caveats) can only narrow authority, never widen it. Verification walks the entire chain back to the root, checking every signature and every caveat. No server calls needed.
This guide covers: create a root delegation, add caveats, build a chain, create an invocation, verify the chain, and see caveat enforcement block unauthorized actions.
Prerequisites
This guide assumes you have completed the Agent Identity Quickstart and have kanoniv-agent-auth installed.
1. Create a root delegation
A root delegation is the starting point of every authority chain. A root authority (typically representing a human or system owner) delegates to an agent with specific caveats.
use kanoniv_agent_auth::{AgentKeyPair, Delegation, Caveat};
fn main() {
// The root authority (human or system owner)
let root = AgentKeyPair::generate();
// The agent receiving authority
let agent = AgentKeyPair::generate();
// Root delegates to agent: can only resolve and search
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![
Caveat::ActionScope(vec!["resolve".into(), "search".into()]),
Caveat::MaxCost(10.0),
],
).unwrap();
println!("Root DID: {}", delegation.issuer_did);
println!("Agent DID: {}", delegation.delegate_did);
println!("Chain depth: {}", delegation.depth());
// Output: Chain depth: 0 (root delegation)
}import { generateKeyPair, createRootDelegation } from "@kanoniv/agent-auth";
// The root authority (human or system owner)
const root = generateKeyPair();
// The agent receiving authority
const agent = generateKeyPair();
// Root delegates to agent: can only resolve and search
const delegation = createRootDelegation(root, agent.identity.did, [
{ type: "action_scope", value: ["resolve", "search"] },
{ type: "max_cost", value: 10.0 },
]);
console.log("Root DID:", delegation.issuer_did);
console.log("Agent DID:", delegation.delegate_did);
console.log("Chain depth:", delegation.parent_proof === null ? 0 : 1);
// Output: Chain depth: 0 (root delegation)import json
from kanoniv_agent_auth import AgentKeyPair, Delegation
# The root authority (human or system owner)
root = AgentKeyPair.generate()
# The agent receiving authority
agent = AgentKeyPair.generate()
# Root delegates to agent: can only resolve and search
caveats = json.dumps([
{"type": "action_scope", "value": ["resolve", "search"]},
{"type": "max_cost", "value": 10.0},
])
delegation = Delegation.create_root(root, agent.identity().did, caveats)
print(f"Root DID: {delegation.issuer_did}")
print(f"Agent DID: {delegation.delegate_did}")
print(f"Chain depth: {delegation.depth}")
# Output: Chain depth: 0 (root delegation)2. Agent invokes the delegated authority
When an agent needs to perform an action, it creates an invocation - a signed proof that it has the authority to do so. The invocation includes the action name, the arguments, and the entire delegation chain.
use kanoniv_agent_auth::{AgentKeyPair, Delegation, Invocation, Caveat, verify_invocation};
fn main() {
let root = AgentKeyPair::generate();
let agent = AgentKeyPair::generate();
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
).unwrap();
// Agent creates an invocation to exercise its delegated power
let invocation = Invocation::create(
&agent,
"resolve",
serde_json::json!({"source": "crm", "external_id": "contact-42"}),
delegation,
).unwrap();
// Anyone can verify the full chain (no server calls)
let result = verify_invocation(
&invocation,
&agent.identity(),
&root.identity(),
).unwrap();
println!("Invoker: {}", result.invoker_did);
println!("Root: {}", result.root_did);
println!("Chain depth: {}", result.depth);
println!("Chain: {:?}", result.chain);
}import {
generateKeyPair,
createRootDelegation,
createInvocation,
verifyInvocation,
} from "@kanoniv/agent-auth";
const root = generateKeyPair();
const agent = generateKeyPair();
const delegation = createRootDelegation(root, agent.identity.did, [
{ type: "action_scope", value: ["resolve"] },
]);
// Agent creates an invocation to exercise its delegated power
const invocation = createInvocation(
agent,
"resolve",
{ source: "crm", external_id: "contact-42" },
delegation,
);
// Anyone can verify the full chain (no server calls)
const result = verifyInvocation(invocation, agent.identity, root.identity);
console.log("Invoker:", result.invoker_did);
console.log("Root:", result.root_did);
console.log("Chain depth:", result.depth);
console.log("Chain:", result.chain);import json
from kanoniv_agent_auth import AgentKeyPair, Delegation, Invocation, verify_invocation
root = AgentKeyPair.generate()
agent = AgentKeyPair.generate()
caveats = json.dumps([{"type": "action_scope", "value": ["resolve"]}])
delegation = Delegation.create_root(root, agent.identity().did, caveats)
# Agent creates an invocation to exercise its delegated power
args = json.dumps({"source": "crm", "external_id": "contact-42"})
invocation = Invocation.create(agent, "resolve", args, delegation)
# Anyone can verify the full chain (no server calls)
invoker_did, root_did, chain, depth = verify_invocation(
invocation,
agent.identity(),
root.identity(),
)
print(f"Invoker: {invoker_did}")
print(f"Root: {root_did}")
print(f"Chain depth: {depth}")
print(f"Chain: {chain}")3. Caveats block unauthorized actions
Caveats are the power of the delegation model. They are checked during verification - if any caveat is violated, verification fails. Here is an agent trying to perform an action outside its scope.
use kanoniv_agent_auth::{AgentKeyPair, Delegation, Invocation, Caveat, verify_invocation};
fn main() {
let root = AgentKeyPair::generate();
let agent = AgentKeyPair::generate();
// Agent is only allowed to resolve
let delegation = Delegation::create_root(
&root,
&agent.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
).unwrap();
// Agent tries to merge - outside its scope
let invocation = Invocation::create(
&agent,
"merge",
serde_json::json!({"entity_ids": ["a", "b"]}),
delegation,
).unwrap();
match verify_invocation(&invocation, &agent.identity(), &root.identity()) {
Ok(_) => println!("This should not happen"),
Err(e) => println!("Blocked: {}", e),
// Output: Blocked: caveat violation: action 'merge' not in allowed scope ["resolve"]
}
}import {
generateKeyPair,
createRootDelegation,
createInvocation,
verifyInvocation,
} from "@kanoniv/agent-auth";
const root = generateKeyPair();
const agent = generateKeyPair();
// Agent is only allowed to resolve
const delegation = createRootDelegation(root, agent.identity.did, [
{ type: "action_scope", value: ["resolve"] },
]);
// Agent tries to merge - outside its scope
const invocation = createInvocation(
agent,
"merge",
{ entity_ids: ["a", "b"] },
delegation,
);
try {
verifyInvocation(invocation, agent.identity, root.identity);
console.log("This should not happen");
} catch (e) {
console.log("Blocked:", (e as Error).message);
// Output: Blocked: Caveat violation: action 'merge' not in allowed scope
}import json
from kanoniv_agent_auth import AgentKeyPair, Delegation, Invocation, verify_invocation
root = AgentKeyPair.generate()
agent = AgentKeyPair.generate()
# Agent is only allowed to resolve
caveats = json.dumps([{"type": "action_scope", "value": ["resolve"]}])
delegation = Delegation.create_root(root, agent.identity().did, caveats)
# Agent tries to merge - outside its scope
args = json.dumps({"entity_ids": ["a", "b"]})
invocation = Invocation.create(agent, "merge", args, delegation)
try:
verify_invocation(invocation, agent.identity(), root.identity())
print("This should not happen")
except ValueError as e:
print(f"Blocked: {e}")
# Output: Blocked: caveat violation: action 'merge' not in allowed scope ["resolve"]4. Build a delegation chain
Authority can flow through multiple agents. Each step can add restrictions but never remove them. A manager delegates to a worker with a narrower scope - the worker can only do a subset of what the manager can do.
use kanoniv_agent_auth::{AgentKeyPair, Delegation, Invocation, Caveat, verify_invocation};
fn main() {
let root = AgentKeyPair::generate();
let manager = AgentKeyPair::generate();
let worker = AgentKeyPair::generate();
// Root gives manager: resolve, search, merge
let d1 = Delegation::create_root(
&root,
&manager.identity().did,
vec![Caveat::ActionScope(vec![
"resolve".into(),
"search".into(),
"merge".into(),
])],
).unwrap();
// Manager gives worker: only resolve (narrower)
let d2 = Delegation::delegate(
&manager,
&worker.identity().did,
vec![Caveat::ActionScope(vec!["resolve".into()])],
d1,
).unwrap();
println!("Chain depth: {}", d2.depth());
// Output: Chain depth: 1
// Worker resolves - allowed
let inv_ok = Invocation::create(
&worker,
"resolve",
serde_json::json!({"source": "crm"}),
d2.clone(),
).unwrap();
let result = verify_invocation(&inv_ok, &worker.identity(), &root.identity()).unwrap();
println!("Resolve allowed, depth: {}", result.depth);
// Output: Resolve allowed, depth: 2
// Worker tries to search - blocked by worker's narrower scope
let inv_bad = Invocation::create(
&worker,
"search",
serde_json::json!({"query": "John"}),
d2,
).unwrap();
match verify_invocation(&inv_bad, &worker.identity(), &root.identity()) {
Err(e) => println!("Search blocked: {}", e),
Ok(_) => println!("This should not happen"),
}
}import {
generateKeyPair,
createRootDelegation,
delegateAuthority,
createInvocation,
verifyInvocation,
} from "@kanoniv/agent-auth";
const root = generateKeyPair();
const manager = generateKeyPair();
const worker = generateKeyPair();
// Root gives manager: resolve, search, merge
const d1 = createRootDelegation(root, manager.identity.did, [
{ type: "action_scope", value: ["resolve", "search", "merge"] },
]);
// Manager gives worker: only resolve (narrower)
const d2 = delegateAuthority(manager, worker.identity.did, [
{ type: "action_scope", value: ["resolve"] },
], d1);
// Worker resolves - allowed
const invOk = createInvocation(
worker,
"resolve",
{ source: "crm" },
d2,
);
const result = verifyInvocation(invOk, worker.identity, root.identity);
console.log("Resolve allowed, depth:", result.depth);
// Output: Resolve allowed, depth: 2
// Worker tries to search - blocked by worker's narrower scope
const invBad = createInvocation(
worker,
"search",
{ query: "John" },
d2,
);
try {
verifyInvocation(invBad, worker.identity, root.identity);
console.log("This should not happen");
} catch (e) {
console.log("Search blocked:", (e as Error).message);
}import json
from kanoniv_agent_auth import AgentKeyPair, Delegation, Invocation, verify_invocation
root = AgentKeyPair.generate()
manager = AgentKeyPair.generate()
worker = AgentKeyPair.generate()
# Root gives manager: resolve, search, merge
caveats_mgr = json.dumps([
{"type": "action_scope", "value": ["resolve", "search", "merge"]}
])
d1 = Delegation.create_root(root, manager.identity().did, caveats_mgr)
# Manager gives worker: only resolve (narrower)
caveats_wkr = json.dumps([
{"type": "action_scope", "value": ["resolve"]}
])
d2 = Delegation.delegate(manager, worker.identity().did, caveats_wkr, d1)
print(f"Chain depth: {d2.depth}")
# Output: Chain depth: 1
# Worker resolves - allowed
inv_ok = Invocation.create(
worker,
"resolve",
json.dumps({"source": "crm"}),
d2,
)
invoker_did, root_did, chain, depth = verify_invocation(
inv_ok,
worker.identity(),
root.identity(),
)
print(f"Resolve allowed, depth: {depth}")
# Output: Resolve allowed, depth: 2
# Worker tries to search - blocked by worker's narrower scope
inv_bad = Invocation.create(
worker,
"search",
json.dumps({"query": "John"}),
d2,
)
try:
verify_invocation(inv_bad, worker.identity(), root.identity())
print("This should not happen")
except ValueError as e:
print(f"Search blocked: {e}")Caveat types
Six built-in caveat types cover the most common authorization patterns:
| Caveat | Description | Example |
|---|---|---|
action_scope | Restrict to specific actions | ["resolve", "search"] |
expires_at | Delegation expires at an RFC 3339 timestamp | "2026-12-31T23:59:59.000Z" |
max_cost | Cost ceiling - requires cost field in args | 5.0 |
resource | Glob pattern on a resource field in args | "entity:customer:*" |
context | Exact match on a key/value pair in args | {"key": "session_id", "value": "sess-abc"} |
custom | Arbitrary key/value constraint | {"key": "org", "value": "acme"} |
Caveats accumulate through the chain. If root says action_scope: [resolve, search] and a middle agent adds action_scope: [resolve], the leaf agent can only resolve. You cannot add a caveat that widens what a parent granted.
Security properties
- Every signature in the chain is verified. Not just the root and leaf - every intermediate delegation is checked against its issuer's embedded public key.
- Caveats are read from the signed payload, not from outer (tamperable) fields. If someone modifies the caveats on the Delegation object, verification reads the original caveats from inside the cryptographic envelope and enforces those.
- Chain depth is capped at 32 to prevent denial-of-service via deeply nested chains.
- No external lookups. Public keys are embedded in each delegation, so the full chain can be verified offline.
Next steps
- MCP Server Auth - Use delegation proofs to authenticate MCP tool calls.
- Agent Identity Quickstart - If you have not already, learn about keypair generation and message signing.
- API Quickstart - Use your agent identity to resolve entities in the Kanoniv Cloud.
