Chio/Docs

Delegate Between Agents

Delegation is how one agent hands a narrowed slice of its authority to another agent. In chio, every delegation is a signed link in an ordered chain carried inside the capability token itself. The kernel refuses to let a child widen what its parent granted, so you can hand capability tokens around a swarm without ever creating ambient authority.

Mental model

Think of a capability token as a signed coupon. When you delegate, you staple a new signed slip to the coupon that says I got this from Alice, and I am narrowing it before I pass it to Bob. The kernel walks every slip on the chain when the coupon is redeemed.

Why Delegate

A supervisor agent often needs to fan work out to a pool of subagents without handing each of them the full power of the supervisor. You have three options:

  • Ask the Capability Authority for a fresh token per subagent. Correct, but adds a round trip and requires the CA to be available and to know about every subagent ahead of time.
  • Share the supervisor's token. Never do this. The token is DPoP-bound to the supervisor's subject key. The kernel will deny a subagent that presents it.
  • Delegate. The supervisor signs a new delegation link that narrows scope, binds the subagent's public key as the new subject, and hands back a self-contained token the subagent can present directly to the kernel.

Delegation matters any time you want least privilege across a multi-hop call graph: supervisor to worker, orchestrator to tool-calling agent, human operator to short-lived automation, or a long-lived agent issuing a per-task sub-token that expires in minutes.


How Delegation Works

A capability token carries an ordered delegation_chain of DelegationLink records. Each link is a signed statement: delegator granted a narrowed capability to delegatee at timestamp, applying the listed attenuations.

rust
pub struct DelegationLink {
    /// Capability ID of the ancestor token delegated at this step.
    pub capability_id: String,
    /// Public key of the agent that delegated.
    pub delegator: PublicKey,
    /// Public key of the agent that received the delegation.
    pub delegatee: PublicKey,
    /// How the scope was narrowed in this delegation step.
    pub attenuations: Vec<Attenuation>,
    /// Unix timestamp of the delegation.
    pub timestamp: u64,
    /// Ed25519 signature by the delegator over the canonical body.
    pub signature: Signature,
}

When a token is presented, the kernel calls validate_delegation_chain which walks the chain and enforces four structural properties:

  • Each link's Ed25519 signature verifies against the declared delegator public key.
  • Adjacent links are connected: link[i].delegatee must equal link[i+1].delegator. No gaps, no forks.
  • Timestamps are non-decreasing across the chain. Back-dated links are rejected as a broken chain.
  • The chain length does not exceed the configured max_depth. Over the limit returns DelegationDepthExceeded.

Structural validity is only part of the contract. The kernel also evaluates the Attenuation entries on each link against the effective scope so far, and refuses any link that widens authority.


Issuing a Delegated Token

You delegate from an agent, not from the CA. The supervisor already holds a token with Operation::Delegate in its grant. It constructs a new DelegationLinkBody, signs it with its own keypair, appends the link to the existing chain, and produces a new CapabilityToken bound to the subagent's subject key.

rust
use chio_core_types::capability::{
    Attenuation, CapabilityToken, CapabilityTokenBody,
    DelegationLink, DelegationLinkBody, Operation,
};

// Supervisor has `parent_token` with Operation::Delegate on a tool grant.
let link_body = DelegationLinkBody {
    capability_id: parent_token.id.clone(),
    delegator: supervisor_kp.public_key(),
    delegatee: subagent_pubkey.clone(),
    attenuations: vec![
        // Drop the Delegate operation so the subagent cannot re-delegate.
        Attenuation::RemoveOperation {
            server_id: "mcp-github".into(),
            tool_name: "get_file".into(),
            operation: Operation::Delegate,
        },
        // Tighten the expiry to the length of the sub-task.
        Attenuation::ShortenExpiry { new_expires_at: now + 300 },
        // Give the subagent only a slice of the parent budget.
        Attenuation::ReduceTotalCost {
            server_id: "mcp-github".into(),
            tool_name: "get_file".into(),
            max_total_cost: MonetaryAmount::usd_cents(500),
        },
    ],
    timestamp: now,
};
let link = DelegationLink::sign(link_body, &supervisor_kp)?;

let mut chain = parent_token.delegation_chain.clone();
chain.push(link);

let child_body = CapabilityTokenBody {
    id: format!("cap-{}", uuid_v7()),
    issuer: supervisor_kp.public_key(),
    subject: subagent_pubkey,
    scope: narrowed_scope,        // must be a subset of parent_token.scope
    issued_at: now,
    expires_at: now + 300,         // never exceed parent expires_at
    delegation_chain: chain,
};
let child_token = CapabilityToken::sign(child_body, &supervisor_kp)?;

The kernel uses validate_attenuation to confirm that child_body.scope is a subset of the effective parent scope. If the child carries a grant, operation, or constraint that is not present in the parent, the kernel returns AttenuationViolation and the request is denied. No tool call fires.

Delegate requires the Delegate operation

A token without Operation::Delegate on a grant cannot delegate that grant. If you want a subagent to be able to fan out further, you must leave Delegate on the grant. If you want the chain to stop at the subagent, attenuate Delegate off before handing the token over.

Attenuation Rules

Every attenuation is strictly one-directional: it can only narrow. The Attenuation enum is the closed set of legal narrowings. Anything outside it, or anything that would expand scope, is a protocol violation.

AttenuationNarrowsNotes
RemoveToolDrops a tool grant entirelySubagent cannot call the tool at all
RemoveOperationDrops one operation from a grantCommon: strip Delegate to end the chain
AddConstraintAdds a policy constraint to a grantFor example, a tighter path allowlist or autonomy tier
ReduceBudgetLowers max_invocationsMust be strictly less than the parent cap
ShortenExpirySets a closer expires_atMust be on or before the parent expiry
ReduceCostPerInvocationTightens max_cost_per_invocationPer-call cost cap, monetary
ReduceTotalCostCarves a sub-budget from the parentSum of sibling sub-budgets must not exceed parent

The protocol pins this rule as safety property P1 capability attenuation: supported delegated capability issuance can only narrow scope relative to the issuing parent. A child token whose scope is not a subset of its parent is not a valid chio capability, and the kernel denies it at step 5 of the seven-step verification sequence, before any guard runs or any side effect occurs.

Scope subset check

The validate_attenuation(parent, child) helper uses ChioScope::is_subset_of. If you need to compute a narrowed scope programmatically, apply attenuations to a clone of the parent scope and feed the result into the same check. Do not hand-build the child scope independently of the parent.

Delegation Depth Limits

validate_delegation_chain(chain, max_depth) accepts an optional maximum depth. Operators set this in kernel configuration; the default deployment profile uses a small bound (typically 3 to 5 hops) because longer chains raise three concerns:

  • Every hop shrinks the effective scope, but none of them can add observability. Deep chains make incident forensics harder.
  • Revocation has to traverse the entire ancestry, so longer chains cost more on every presentation.
  • Deep chains usually indicate a control-flow problem: a long-lived agent that should have asked the CA for a fresh token is instead re-delegating from a months-old root.

Exceeding the bound raises Error::DelegationDepthExceeded{ depth, max } and the kernel fails closed.


Lineage & Receipts

Every invocation produces a signed receipt, and the receipt records the delegation context of the token that authorized it. Lineage is what lets an auditor reconstruct, after the fact, exactly who delegated what to whom.

A persisted capability lineage record looks like this (from a live incident-network run):

json
[
  {
    "capability_id": "cap-019d93c6-924d-70b0-be41-4f9c3c7f5a0b",
    "subject_key": "130014d225711198837ad7d0a8326d162a1fcb569c2b2949a14d16b326bfa5ba",
    "issuer_key": "77527a20b865ed8e77f9eed6d2a433c06b15e47ac3425365d8b57066518eed5f",
    "issued_at": 1776300757,
    "expires_at": 1776302557,
    "delegation_depth": 0,
    "parent_capability_id": null
  },
  {
    "capability_id": "inc-meridian-inference-gw-2026-04-15-0317z-triage",
    "subject_key": "de325149398358f894a782efc7cd67fda9229c8a5add38ec9af22b1f8d82e421",
    "issuer_key": "130014d225711198837ad7d0a8326d162a1fcb569c2b2949a14d16b326bfa5ba",
    "issued_at": 1776300757,
    "expires_at": 1776301657,
    "delegation_depth": 1,
    "parent_capability_id": "cap-019d93c6-924d-70b0-be41-4f9c3c7f5a0b"
  }
]

Two invariants show up here. First, subject_key of the parent equals issuer_key of the child: the supervisor is the subject of its own token and the issuer of the subagent's token. Second, expires_at shrinks as you descend. The child cannot outlive its parent.

For governed transactions, receipts also carry a call_chain block. The fields that travel with a delegated governed request are:

  • chain_id: stable identifier for the delegated transaction or call chain
  • parent_request_id and parent_receipt_id: link into the prior hop's audit trail
  • origin_subject: the root delegator visible in capability lineage
  • delegator_subject: the immediate delegator that handed control to the current subject

Financial receipt metadata additionally carries delegation_depth and root_budget_holder, so cost attribution follows the cryptographic chain rather than a separately maintained ledger.

Asserted vs. verified provenance

Chio distinguishes asserted from verified call-chain context. A subagent can assert what chain it thinks it is on; the kernel only marks the chain as verified when it observed the local parent edge, when a receipt-lineage statement signed by a trusted kernel exists, or when an upstream handoff proof verifies against the capability's delegator key. Reports never label asserted lineage as verified (safety property P10).

Revocation Cascade

Revocation in chio is ancestry-aware. When the kernel evaluates a presented token, it checks revocation state for the presented capability id and for every ancestor id referenced in the delegation_chain. This is safety property P2 presented revocation coverage: a revoked capability, or a revoked presented delegation ancestor id, is denied.

The practical consequence: revoking a parent implicitly revokes every child token derived from it, without requiring the CA to enumerate them. You do not need to know who Alice handed a coupon to. Revoke Alice's token and every descendant is dead on arrival.

bash
# Revoke the supervisor's root capability. All subagent tokens derived
# from it start failing at the next kernel verification, regardless of
# how deep in the delegation chain they sit.
chio --revocation-db revocations.sqlite3 \
  trust revoke \
  --capability-id cap-019d93c6-924d-70b0-be41-4f9c3c7f5a0b

A subagent that re-presents its token after the parent is revoked receives a denied verdict at verification step 5 (revocation check), before scope matching, before any guard pipeline runs, and before any tool server is contacted.

Plan for cascade

Before you revoke a token mid-flight, consider which children depend on it. Cascade is the right default for security incidents. For planned rotations, issue the new root first, migrate subagents to tokens derived from the new root, and only then revoke the old root. See Rotate Keys & Revoke for the full runbook.

Common Patterns

Supervisor and Subagent

The supervisor holds a long-lived token and fans out work to short-lived subagents. Each subagent gets a token that is tool-scoped to exactly the tools it needs, duration-scoped to the sub-task, and has Delegate attenuated off so the chain stops there.

typescript
import { delegateCapability, Attenuation } from "@chio-protocol/sdk";

// supervisor has parentToken with broad scope + Operation.Delegate
const childToken = await delegateCapability({
  parentToken,
  delegateePublicKey: subagentPubKey,
  signingKey: supervisorKeypair,
  attenuations: [
    // Keep only the one tool this subagent needs.
    ...tools.filter(t => t !== "query_spans").map(t => Attenuation.removeTool({
      serverId: "mcp-observability",
      toolName: t,
    })),
    // End the chain: no further delegation.
    Attenuation.removeOperation({
      serverId: "mcp-observability",
      toolName: "query_spans",
      operation: "delegate",
    }),
    // Fifteen-minute window.
    Attenuation.shortenExpiry({ newExpiresAt: now + 900 }),
  ],
});

await dispatchSubagent(subagent, childToken);

Per-Task Capability

A long-lived agent that processes a queue of heterogeneous tasks can mint a per-task capability from its root token before running each task. The per-task token carries only the tools and budget that specific task needs, with an expiry set to a conservative upper bound on task duration. Anything that leaks (a prompt-injected tool call, a compromised subprocess) is scoped to the task, not the agent.

Human-Approval Step

For sensitive operations, the root token can require MinimumAutonomyTier(Delegated) as a constraint. The agent delegates to itself with a governed approval token attached, bound to a specific intent hash. The kernel treats Delegated as requiring a delegation bond on the governed request, which surfaces the call for operator review before the tool fires.

Cross-Org Handoff

When delegating across organizational boundaries, combine the capability token with an Agent Passport. The receiving side verifies the chain structurally (all the rules in this guide apply), and independently evaluates the delegating agent's passport against its own verifier policy before honoring the token.


Summary

ConceptMeaning
DelegationLinkSigned record: delegator granted a narrowed capability to delegatee at timestamp
delegation_chainOrdered list of links from the root CA to the presented token
AttenuationClosed enum of legal narrowings: remove tool or operation, add constraint, reduce budget, shorten expiry, cap cost
validate_delegation_chainChecks per-link signatures, connectivity, timestamp monotonicity, and max depth
validate_attenuationConfirms that the child scope is a subset of the parent scope
P1 attenuationSafety property: delegated issuance can only narrow, never widen
P2 revocation coverageRevoking an ancestor denies every descendant presentation
Lineage in receiptsdelegation_depth, parent_capability_id, and call-chain fields persist the hop

Next Steps

  • Rotate Keys & Revoke · the planned-rotation flow and the incident-response cascade
  • Capabilities · the underlying token model and scope structure
  • Receipts · how call-chain and lineage metadata land in signed audit evidence
  • CLI Reference · chio trust commands for issuing, delegating, and revoking tokens