Chio/Docs

BuildIdentity

Delegate Between Agents

Give another agent a narrowed capability token with a signed delegation link.

What delegation adds

Delegation appends a signed link to a capability token. The link names the delegator and delegatee and records the scope reduction. The kernel validates the complete chain when the delegatee presents the token.

Why Delegate

A supervisor agent often needs to fan work out to a pool of subagents without handing each subagent all of the supervisor's authority. 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. Do not 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.

Use delegation to apply least privilege across a multi-hop call graph: supervisor to worker, orchestrator to tool-calling agent, operator to short-lived automation, or a long-lived agent to a per-task token.


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,
    /// Chain-binding: SHA-256 hash of the scope authorized at this hop.
    /// The next hop's attenuation proof must carry a parent_scope_hash
    /// equal to this value, which stops a child from inflating the scope
    /// it inherited. Absent on older links; enforced under a feature gate.
    pub scope_hash: Option<ScopeHash>,
    /// 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 not enough. The kernel also evaluates the Attenuation entries on each link against the effective scope so far, and refuses any link that widens authority.

Each link also carries a scope_hash: the SHA-256 hash of the scope authorized at that hop. The trust-root-aware validator (validate_delegation_chain_with_trust_root, feature-gated) binds every hop to the next by requiring the child's attenuation_proof.parent_scope_hash to equal the parent link's scope_hash, and rejects a chain whose hops omit it. Without that binding a token could present an internally consistent attenuation witness that was never tied to the scope its parent actually held, which is the parent-scope-inflation attack the protocol spec calls out.


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. Use the delegate() helper, which validates and signs the hop in one call. It is gated behind the delegation feature.

rust
use chio_core_types::capability::attenuation::delegate;
use chio_core_types::capability::{Attenuation, Operation};
use chio_core_types::delegation_receipt::ScopeAttenuation;

// Supervisor holds `parent_token` (its subject == supervisor_kp.public_key()).
let attenuation = ScopeAttenuation {
    steps: 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,
        },
    ],
    child_expires_at: Some(now + 300),   // never beyond parent.expires_at
    ..Default::default()
};

let receipt = delegate(
    &parent_token,
    &narrowed_scope,      // reduce-only against parent_token.scope
    &supervisor_kp,       // must match parent_token.subject
    &subagent_pubkey,
    attenuation,
    now,                  // signed_at (rejected if < parent.issued_at)
    nonce,                // [u8; 16], disambiguates same-second receipts
)?;

// receipt.link is the freshly-signed DelegationLink for this hop;
// receipt.complete_chain() is the full chain to attach to the child token.

delegate() refuses to emit a receipt whose signed link and child scope disagree. Before it signs, it verifies the parent's signature, confirms delegator_keypair matches parent.subject, rejects a signed_at earlier than the parent's issued_at, and validates the child scope and every attenuation step as reduce-only against the parent scope, expiry, and budget share. It returns a signed DelegationReceipt.

delegate() wraps the lower-level primitives. When you need explicit control over link and token assembly, construct them by hand: build a DelegationLinkBody, sign it with your own keypair, append the link to the existing chain, and produce a new CapabilityToken bound to the subagent's subject key. The manual path leaves the checks above to the caller.

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

An attenuation can only narrow scope. TheAttenuation enum lists the allowed reductions. Any other change, including a scope expansion, 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 during capability verification, 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:

  • Each hop narrows the effective scope. Deep chains make incident investigation 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

Each invocation produces a signed receipt that records the delegation context of its authorizing token. Auditors can use this lineage to determine who delegated which authority.

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 also includes delegation_depth and root_budget_holder, so cost attribution follows the cryptographic chain without a separate 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.

Revoking a parent denies child tokens derived from it without requiring the CA to enumerate them. Revoke the parent token to deny its descendants.

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 during capability validation. The revocation check runs before the guard pipeline 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 the tools it needs, scoped to the sub-task duration, 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 that bounds task duration. A leaked token, including one exposed through a prompt-injected tool call or compromised subprocess, remains scoped to that task.

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.

Chio provides a federated issuance command for this. After the partner presents a challenge-bound passport, the receiving org mints a fresh capability from that presentation with chio trust federated-issue. The new token is issued under the receiver's own authority and capability policy, not carried across as a raw delegation link.

bash
# Receiving org: issue a capability from a verified partner presentation
chio trust federated-issue \
  --presentation-response presentation.json \
  --challenge challenge.json \
  --capability-policy capability-policy.yaml \
  --enterprise-identity partner-identity.json \
  --delegation-policy signed-delegation-policy.json \
  --upstream-capability-id cap-partner-7b0f

The --delegation-policy file is a signed file the issuing partner creates with chio trust federated-delegation-policy-create. It names the issuer, the partner, the verifier endpoint, and the capability policy that bounds what the partner may request, with an explicit expiry.

bash
# Issuing org: publish a signed federated delegation policy
chio trust federated-delegation-policy-create \
  --output signed-delegation-policy.json \
  --signing-seed-file issuer.seed \
  --issuer did:chio:issuer... \
  --partner acme-corp \
  --verifier https://verify.acme.example \
  --capability-policy capability-policy.yaml \
  --expires-at 1776302557

CrewAI Multi-Agent Crews

The chio-crewai package is the supervisor/subagent pattern applied to a CrewAI crew. You map each role to a ChioScope, hand the map to a ChioCrew, then call provision_capabilities() to mint the narrowed per-role capabilities before the crew runs. Each tool call an agent attempts is then evaluated by the sidecar kernel.

python
from chio_crewai import ChioCrew
from chio_sdk.client import ChioClient
from chio_sdk.models import ChioScope

async with ChioClient("http://127.0.0.1:9090") as chio:
    crew = ChioCrew(
        capability_scope={
            "researcher": ChioScope(grants=[search_grant()]),
            "writer": ChioScope(grants=[write_grant()]),
        },
        chio_client=chio,
        agents=[researcher, writer],
        tasks=[task],
    )
    await crew.provision_capabilities()   # mint per-role scoped capabilities
    result = crew.kickoff()

Delegation between roles reuses the same reduce-only rule. The crew mints an attenuated child token with crew.attenuate_for_delegation(delegator_role, delegate_role, new_scope), and the SDK raises ChioValidationError if the new scope tries to broaden what the delegator holds.


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