Delegation Specification
Version: 0.1.0 Status: Draft
This document specifies the delegation and invocation protocol for Kanoniv Agent Auth. Delegations allow one agent to grant another agent a subset of its authority, with constraints that can only be narrowed at each step.
1. Delegation Structure
{
"issuer_did": "did:agent:<issuer-id>",
"delegate_did": "did:agent:<delegate-id>",
"issuer_public_key": [<32 bytes>],
"caveats": [<Caveat>, ...],
"parent_proof": <Delegation | null>,
"proof": <SignedMessage>
}| Field | Type | Description |
|---|---|---|
issuer_did | String | DID of the agent granting authority. |
delegate_did | String | DID of the agent receiving authority. |
issuer_public_key | Byte array (32 bytes) | Issuer's Ed25519 public key. Enables chain verification without external key resolution. Serialized as a JSON array of integers. |
caveats | Array of Caveat | Constraints on the delegated authority. |
parent_proof | Delegation or null | The parent delegation proving the issuer's own authority. Null for root delegations. |
proof | SignedMessage | Cryptographic proof signed by the issuer. |
1.1 Proof Payload
The proof field is a SignedMessage (see Signing Specification) whose payload contains:
{
"issuer_did": "did:agent:<issuer-id>",
"delegate_did": "did:agent:<delegate-id>",
"caveats": [<Caveat>, ...],
"parent_hash": "<content-hash-of-parent | null>"
}| Field | Type | Description |
|---|---|---|
issuer_did | String | Same as outer field. |
delegate_did | String | Same as outer field. |
caveats | Array of Caveat | The authoritative caveat list - verification reads caveats from here, not from the outer field. |
parent_hash | String or null | Content hash of the parent delegation's proof, or null for root delegations. |
1.2 Root vs Chained Delegations
A root delegation has parent_proof: null and parent_hash: null. The issuer is the root authority and does not need a parent to prove its own authority.
A chained delegation has a non-null parent_proof. The issuer MUST be the delegate_did of the parent delegation. The parent_hash MUST equal the content hash of the parent's proof.
1.3 Depth
The depth of a delegation is the number of parent links:
- Root delegation: depth 0
- First child: depth 1
- Second child: depth 2
The maximum allowed depth is 32 (MAX_CHAIN_DEPTH).
2. Invocation Structure
An invocation represents an agent exercising delegated authority:
{
"invoker_did": "did:agent:<invoker-id>",
"action": "<action-string>",
"args": <any JSON value>,
"delegation": <Delegation>,
"proof": <SignedMessage>
}| Field | Type | Description |
|---|---|---|
invoker_did | String | DID of the agent performing the action. |
action | String | The action being performed (e.g., "resolve", "merge"). |
args | Any JSON value | Arguments for the action. |
delegation | Delegation | The delegation chain proving authority. |
proof | SignedMessage | Cryptographic proof signed by the invoker. |
2.1 Invocation Proof Payload
{
"invoker_did": "did:agent:<invoker-id>",
"action": "<action-string>",
"args": <any JSON value>,
"delegation_hash": "<content-hash-of-delegation-proof>"
}The delegation_hash binds the invocation to a specific delegation. Changing the delegation (e.g., substituting one with wider caveats) changes the hash and invalidates the invocation signature.
2.2 Constraint
The invoker MUST be the delegate_did of the invocation's delegation. Creating an invocation where the invoker is not the delegation's delegate MUST fail.
3. Caveat Types
Caveats are serialized as tagged unions with type and value fields:
3.1 ActionScope
Restricts the delegation to specific actions.
{ "type": "action_scope", "value": ["resolve", "search"] }Enforcement: The invocation's action MUST be present in the value array. If not, verification fails with CaveatViolation.
3.2 ExpiresAt
Time-bounds the delegation.
{ "type": "expires_at", "value": "2026-12-31T23:59:59.999Z" }Enforcement: The current time (at verification) MUST be less than or equal to the value timestamp. String comparison on RFC 3339 timestamps is sufficient because the format is lexicographically ordered. If expired, verification fails with CaveatViolation.
3.3 MaxCost
Sets a cost ceiling on the delegated operation.
{ "type": "max_cost", "value": 5.0 }Enforcement: The invocation args MUST contain a "cost" field with a numeric value. If args.cost > value, verification fails. If args.cost is missing, verification fails (fail-closed).
3.4 Resource
Restricts the delegation to resources matching a glob pattern.
{ "type": "resource", "value": "entity:customer:*" }Enforcement: The invocation args MUST contain a "resource" field with a string value. The value is matched against the pattern using trailing-wildcard glob matching:
- If the pattern ends with
*, check that the resource string starts with the pattern prefix (everything before the*). - Otherwise, check exact equality.
If the resource does not match, or the resource field is missing, verification fails.
3.5 Context
Binds the delegation to a specific context value.
{ "type": "context", "value": { "key": "session_id", "value": "sess-abc" } }Enforcement: The invocation args MUST contain a field with key equal to the caveat's key, and its string value MUST equal the caveat's value. If missing or mismatched, verification fails.
3.6 Custom
Application-defined caveat.
{ "type": "custom", "value": { "key": "org", "value": "acme" } }Enforcement: The invocation args MUST contain a field with key equal to the caveat's key, and its JSON value MUST equal the caveat's value (deep equality). If missing or mismatched, verification fails.
4. Caveat Accumulation
When an agent re-delegates (creates a chained delegation from a parent), caveats accumulate:
- Start with all caveats from the parent delegation.
- Append any additional caveats specified by the re-delegating agent.
There is no mechanism to remove or weaken a parent's caveats. Each delegation can only narrow the authority.
Example: Root grants B [action_scope: [resolve, search], max_cost: 10]. B delegates to C with additional [action_scope: [resolve]]. C's effective caveats are [action_scope: [resolve, search], max_cost: 10, action_scope: [resolve]]. During verification, both action_scope caveats are checked - the action must appear in both lists. The net effect is that C can only resolve.
5. Chain Verification Algorithm
Given an Invocation, an invoker_identity (AgentIdentity), and a root_identity (AgentIdentity):
Step 1: Verify Invocation Signature
Verify invocation.proof against invoker_identity using SignedMessage::verify(). If invalid, return SignatureInvalid.
Step 2: Check Invoker-Delegation Binding
Verify invocation.invoker_did == invocation.delegation.delegate_did. If not, return DelegationChainBroken("invoker is not the delegate").
Step 3: Walk the Delegation Chain
Initialize:
chain = [invocation.invoker_did]current = invocation.delegationall_caveats = []steps = 0
Loop:
3a. Increment steps. If steps > MAX_CHAIN_DEPTH (32), return DelegationChainBroken("chain depth exceeds maximum").
3b. Push current.issuer_did to chain.
3c. Reconstruct issuer_identity from current.issuer_public_key (32 bytes -> AgentIdentity). If the bytes are invalid, return DelegationChainBroken("invalid embedded public key").
3d. Verify the embedded public key matches the claimed DID: recompute did:agent:hex(SHA-256(issuer_public_key)[0..16]) and check against current.issuer_did. If mismatch, return DelegationChainBroken("embedded public key DID mismatch").
3e. Verify current.proof against the reconstructed issuer_identity. If invalid, return SignatureInvalid.
3f. Check revocation: call is_revoked(current.proof.content_hash()). If revoked, return DelegationRevoked(hash).
3g. Extract caveats from current.proof.payload["caveats"] (the signed payload, not the outer field). Append to all_caveats.
3h. If current.issuer_did == root_identity.did:
- Verify
issuer_identity.public_key_bytes == root_identity.public_key_bytes. If mismatch, returnDelegationChainBroken("root public key mismatch"). - Break out of loop (chain terminates at root).
3i. If current.parent_proof is null, return DelegationChainBroken("chain terminates before reaching root").
3j. Verify current.parent_proof.delegate_did == current.issuer_did. If not, return DelegationChainBroken("chain linkage broken").
3k. Set current = current.parent_proof. Continue loop.
Step 4: Check All Caveats
For each caveat in all_caveats, call check_caveat(caveat, invocation.action, invocation.args, now). If any caveat fails, return CaveatViolation(reason).
Step 5: Return Result
Return VerificationResult { invoker_did, root_did, chain, depth }.
6. Delegation-Only Chain Verification
The chain can be verified without an invocation (e.g., to check if a delegation is valid before using it). The algorithm is the same as Steps 3a-3k above, without Step 1, Step 2, or Step 4.
7. Revocation
Delegations are identified by their content hash (delegation.proof.content_hash()). The verification functions accept an is_revoked callback:
is_revoked: fn(content_hash: &str) -> boolIf is_revoked returns true for any delegation in the chain, verification fails with DelegationRevoked(hash).
The revocation mechanism (database lookup, in-memory set, external service) is application-defined. The protocol provides the hook; the implementation provides the backend.
Chain-wide effect: Revoking a delegation anywhere in the chain invalidates all downstream invocations. If Root -> B -> C and B's delegation is revoked, C's invocations fail because the chain walks through B's revoked delegation.
To skip revocation checking, pass |_| false as the callback.
8. Depth Limit
The maximum delegation chain depth is 32 (MAX_CHAIN_DEPTH). This limit is enforced at both creation time and verification time:
- Creation:
Delegation::delegate()checksparent.depth() >= MAX_CHAIN_DEPTHbefore creating a new delegation. If exceeded, returnsDelegationChainBroken. - Verification: The chain walk tracks
stepsand rejects chains exceeding 32 levels.
32 levels supports practical hierarchies (organization -> department -> team -> agent -> sub-agent -> task-specific agent) while preventing denial-of-service via deeply nested chains.
9. Serialization
Delegations, Invocations, and Caveats serialize to and from JSON. Implementations MUST support round-trip serialization: deserialize(serialize(delegation)) MUST produce a delegation that verifies identically to the original.
Caveat serialization uses serde's tagged union format:
{"type": "action_scope", "value": ["resolve"]}
{"type": "expires_at", "value": "2026-12-31T23:59:59.999Z"}
{"type": "max_cost", "value": 5.0}
{"type": "resource", "value": "entity:*"}
{"type": "context", "value": {"key": "task_id", "value": "t1"}}
{"type": "custom", "value": {"key": "org", "value": "acme"}}The issuer_public_key is serialized as a JSON array of byte values (e.g., [215, 90, 152, ...]).
