Skip to content

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

json
{
  "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>
}
FieldTypeDescription
issuer_didStringDID of the agent granting authority.
delegate_didStringDID of the agent receiving authority.
issuer_public_keyByte array (32 bytes)Issuer's Ed25519 public key. Enables chain verification without external key resolution. Serialized as a JSON array of integers.
caveatsArray of CaveatConstraints on the delegated authority.
parent_proofDelegation or nullThe parent delegation proving the issuer's own authority. Null for root delegations.
proofSignedMessageCryptographic proof signed by the issuer.

1.1 Proof Payload

The proof field is a SignedMessage (see Signing Specification) whose payload contains:

json
{
  "issuer_did": "did:agent:<issuer-id>",
  "delegate_did": "did:agent:<delegate-id>",
  "caveats": [<Caveat>, ...],
  "parent_hash": "<content-hash-of-parent | null>"
}
FieldTypeDescription
issuer_didStringSame as outer field.
delegate_didStringSame as outer field.
caveatsArray of CaveatThe authoritative caveat list - verification reads caveats from here, not from the outer field.
parent_hashString or nullContent 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:

json
{
  "invoker_did": "did:agent:<invoker-id>",
  "action": "<action-string>",
  "args": <any JSON value>,
  "delegation": <Delegation>,
  "proof": <SignedMessage>
}
FieldTypeDescription
invoker_didStringDID of the agent performing the action.
actionStringThe action being performed (e.g., "resolve", "merge").
argsAny JSON valueArguments for the action.
delegationDelegationThe delegation chain proving authority.
proofSignedMessageCryptographic proof signed by the invoker.

2.1 Invocation Proof Payload

json
{
  "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.

json
{ "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.

json
{ "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.

json
{ "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.

json
{ "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.

json
{ "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.

json
{ "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:

  1. Start with all caveats from the parent delegation.
  2. 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.delegation
  • all_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, return DelegationChainBroken("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) -> bool

If 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() checks parent.depth() >= MAX_CHAIN_DEPTH before creating a new delegation. If exceeded, returns DelegationChainBroken.
  • Verification: The chain walk tracks steps and 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:

json
{"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, ...]).

The identity and delegation layer for AI agents.