Delegation boundaries
Know what a delegated agent can never gain from its parent, and which check refuses any widening of scope, lifetime, budget, or proof requirements.
On this page
A coordinator can hand part of a task to another agent without handing over its own authority. The child holds a signed subset of its parent, and the kernel checks that relationship each time the child's call reaches it.
The running example is the grant tree from the Authority application: a parent that may read src/ and delegate, and two children, the reviewer and the tester, that each read one subdirectory and may delegate no further. The children share one family allowance, and revoking the parent reaches both.
A child inside every bound reaches the tool
The reviewer's grant names repository/read_file under src/review/, a grant limit of 2, and an expiry no later than the parent's. Each relationship to the signed parent holds, so the kernel dispatches the read and signs an allow receipt. Least privilege.
Each child carries one part of the task
Delegation pays off when workers need different parts of a task. A coordinator asks one child to inspect code and another to check dependencies; each child receives authority for its own assignment, and its result stays attributable to that child. Passing the coordinator's credential to both workers would erase that separation.
The relationship matters after issuance too. Chio retains the signed derivation and the shared accounting, so the runtime reads a child as part of the original authority, and the application can withdraw the whole investigation or bound its combined work. Independent tokens with similar fields describe no such family.
Draw a delegation boundary where responsibility separates: a different scope, an independent lifetime, or an identifiable contribution to the larger task. A child for every model turn adds lifecycle work and clarifies nothing. The grant tree says who may do which work; the application's scheduler decides when.
A child stays inside its parent on every dimension
A child grant preserves or narrows its parent in scope, lifetime, budget share, and proof requirements. A reviewer that receives a child token gains no longer task window, no publication destination, and no larger invocation pool. The kernel holds every child to these rules:
- Scope. The child's tool, resource, and prompt grants stay within the parent's scope, and the child keeps the parent's
dpop_requiredproof-of-possession requirement. - The delegate operation. A child exists only when a parent grant carries the delegate operation and covers the child's tool grant.
- Lifetime. Each link is signed inside its parent's validity window, and the child expires no later than its parent.
- Budget share. The child's share is at most its parent's, and the shares of admitted siblings together stay within the parent's share.
- Use count. Under
AggregateInvocationScope::DelegationFamily, every descendant of one root draws on a single allowance.
verify_aggregate_invocation_budget verifies the family scope, and the kernel holds the allowance as the durable admission quota BudgetQuotaProfile::AggregateFamilyInvocation: siblings draw down one tracked pool and a child receives no fresh one. The BudgetRegistry trait tracks each parent's admitted children and refuses a new child whose share, added to its admitted siblings' shares, would exceed the parent's own.
The capability monotonicity walkthrough states the formal property. Containment comes from the signed capability chain and its validation at use; nesting configuration objects establishes none of it.
Where each rule is enforced
- Scope outside the parent.
crates/core/chio-core-types/src/capability/attenuation.rs - Dropped proof-of-possession requirement.
crates/core/chio-core-types/src/capability/scope.rs - Link timestamp outside the parent's window, or a child that outlives its parent.
crates/kernel/chio-kernel-core/src/capability_verify/lineage.rsandcrates/kernel/chio-kernel/src/kernel/validation.rs - Larger budget share.
crates/kernel/chio-kernel-core/src/capability_verify/lineage.rsandcrates/kernel/chio-kernel-core/src/budget_split.rs - Missing delegate operation.
crates/kernel/chio-kernel-core/src/capability_verify/lineage.rsandcrates/kernel/chio-kernel/src/kernel/mod.rs
Read more in Invocation limits, which compares the family allowance with the capability-local alternative, and swarm budgets, where a fan-out graph shares one allowance across many agents.
The kernel walks the signed chain to a trusted root
Each signed DelegationLink binds a delegator, a delegatee, and an authorized scope hash. A token requires chain binding when it carries an attenuation proof, scope attenuations, or a budget share. Its attenuation proof then connects the leaf's scope to the last link, and the lineage connects that link to independently trusted upstream authority.
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.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attenuations: Vec<Attenuation>,
/// Unix timestamp of the delegation.
pub timestamp: u64,
/// Delegation chain-binding: SHA-256 hash of the canonical scope authorized
/// at this hop. Absent on older links; verifiers can enforce presence via
/// feature gate. Verifiers gated behind the `delegation_chain_binding`
/// feature flag enforce that this matches the parent_scope_hash carried by
/// the next hop.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope_hash: Option<ScopeHash>,
/// Authenticated preservation marker for a delegation-family invocation budget.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aggregate_budget: Option<AggregateBudgetDelegationMarker>,
/// Authenticated preservation marker for cumulative approval root bindings.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cumulative_approval: Option<CumulativeApprovalDelegationMarker>,
/// Ed25519 signature by the delegator over the canonical form of the
/// other fields in this link.
pub signature: Signature,
}A single-link chain-binding token is checked against the pinned trust root. With two or more links, the kernel needs the signed ancestor tokens from its receipt store and refuses when one is missing, so an invented but internally consistent parent is insufficient. The signed-lineage verifier walks at most 64 ancestors, the deployment's configured delegation-depth limit applies as well, and the verifier checks lifetime and budget share together, reporting one reason for either.
The same ancestry is queryable from the lineage graph your kernel records. Walking back from the reviewer's grant names each ancestor the kernel must hold a signed token for:
chio lineage query --live --seed cap-01e6…5b18 --direction reversewalk reverse from cap-01e6…5b18, depth limit 20 depth 0 cap-01e6…5b18 capability reviewer depth 1 cap-01e6…20d4 capability parent delegated to cap-01e6…5b18 root cap-01e6…20d4, issued by your kernel 4f1c…a2e9 2 nodes, 1 edge, walk complete
You should see one link and a root your kernel issued. Walk forward from cap-01e6…20d4 instead and the result lists the reviewer and the tester: the set a revocation of the parent reaches.
Delegation admission separately checks every delegated link against its retained parent snapshot, and sibling-sum budget admission refuses a share larger than the parent's. A token without chain binding skips both chain-binding paths and meets these two checks alone. Keep the signed ancestor records with the child: a local scope comparison carries none of their authority.
Read more in Delegation and attenuation, which covers the proof and cryptographic contracts.
The issuer signs the link, then issues the child
The Authority issuer holds the parent token, its subject's key, the accepted issuer key, and two separately generated child subject keys. It narrows the scope as Least privilege shows, and the parent's subject signs the delegation link:
let receipt = delegate(
parent,
&scope,
parent_subject,
&subject.public_key(),
ScopeAttenuation {
steps: vec![
Attenuation::RemoveOperation {
server_id: "workspace".into(),
tool_name: "read_file".into(),
operation: Operation::Delegate,
},
Attenuation::AddConstraint {
server_id: "workspace".into(),
tool_name: "read_file".into(),
constraint: Constraint::PathPrefix(prefix.into()),
},
Attenuation::ReduceBudget {
server_id: "workspace".into(),
tool_name: "read_file".into(),
max_invocations: 2,
},
Attenuation::ShortenExpiry {
new_expires_at: expires_at,
},
],
child_expires_at: Some(expires_at),
budget_share_bps: Some(5000),
},
now,
nonce,
)?;Each child requests 5,000 basis points. That share belongs to delegation accounting and creates no fresh three-call family allowance. The nonce distinguishes this signed delegation, while its scope and expiry stay bound to the parent. delegate returns a signed delegation receipt and link; the accepted issuer then creates the child capability that runtime.spawn consumes:
Ok(CapabilityToken::sign_attenuated(
CapabilityTokenAttenuationBody {
body: CapabilityTokenBody {
id: id.into(),
issuer: issuer.public_key(),
subject: subject.public_key(),
issued_at: now,
expires_at,
scope: scope.clone(),
delegation_chain: receipt.complete_chain(),
aggregate_invocation_budget: parent.aggregate_invocation_budget.clone(),
},
caveats: vec![],
scope_attenuations: vec![],
budget_share_bps: Some(5000),
attenuation_proof: AttenuationProof {
parent_scope_hash: scope_hash(&parent.scope)?,
child_scope_hash: scope_hash(&scope)?,
normalized_subset_proof: compute_attenuation_witness(&parent.scope, &scope)?,
},
},
issuer,
)?)The child carries the complete chain and the parent's signed aggregate-family binding. Its attenuation proof commits to both scope hashes and their normalized subset relationship, so a family ceiling copied as a number into an unrelated token carries none of the shared authority.
Sign a review link by hand
A host can sign the link itself. Start with a signed parent whose subject owns the review: it may invoke and delegate the repository read tool six times under the selected path and proof requirement. The reviewer receives a two-use source-tree scope, loses onward delegation, and keeps both the parent's path constraint and its proof requirement.
use chio_core::capability::{
attenuation::{delegate, Attenuation}, scope::{Constraint, Operation}, token::CapabilityToken,
};
use chio_core::crypto::{Keypair, PublicKey};
use chio_core_types::{DelegationReceipt, ScopeAttenuation};
use super::review_scope::review_scopes;
/// Produces a signed receipt/link, not an issued child capability token.
/// The host supplies the original parent, its subject's key and a fresh nonce.
pub fn sign_review_delegation(
parent: &CapabilityToken,
delegator: &Keypair,
reviewer: &PublicKey,
now: u64,
nonce: [u8; 16],
) -> chio_core_types::error::Result<DelegationReceipt> {
let (_, child_scope) = review_scopes();
let expires_at = now.saturating_add(120).min(parent.expires_at);
let attenuation = ScopeAttenuation {
steps: vec![
Attenuation::RemoveOperation {
server_id: "workspace".into(), tool_name: "read_file".into(),
operation: Operation::Delegate,
},
Attenuation::AddConstraint {
server_id: "workspace".into(), tool_name: "read_file".into(),
constraint: Constraint::PathPrefix("/srv/larkfield/pricing/src/".into()),
},
Attenuation::ReduceBudget {
server_id: "workspace".into(), tool_name: "read_file".into(),
max_invocations: 2,
},
Attenuation::ShortenExpiry { new_expires_at: expires_at },
],
child_expires_at: Some(expires_at),
budget_share_bps: Some(1600),
};
delegate(parent, &child_scope, delegator, reviewer, attenuation, now, nonce)
}The host supplies the signed parent, the key of that parent's subject, and a fresh per-receipt nonce. The link carries explicit steps that remove onward delegation, add the source-tree path constraint, reduce the grant limit, and shorten expiry to 120 seconds. The requested 1,600-basis-point share is receipt metadata that link signature verification leaves unbound; the subsequently signed child and runtime budget admission bind and enforce it. The authority that issues and admits the child preserves the receipt's chain, the signed ancestor, and the budget commitments.
SIGN delegation: reviewer read subset and 120-second expiry
CHECK share: 1600 bps requested; child token and admission must bind it
REFUSE widening: parent did not authorize write_file
REFUSE wrong delegator: reviewer does not own the parentThe complete executed comparison signs a disposable parent, checks the resulting link signature and recipient, then tries a write scope and the wrong delegator key. Both attempts are refused.
The issued reviewer reads its own tree and nothing beside it
Once a child is issued, the serving host owns its current admission. Start from the initialized Authority state in its setup chapter, before the allowance is spent, and run these trusted operator commands from the prepared Execution example directory. They reopen the reviewer and call the repository connector:
authority read src/discount/rules.py --as reviewer --operation review-1
authority read src/refund/rules.py --as reviewer --operation wrong-tree
authority inspectYou should see the reviewer's read succeed and its request for the sibling tree refused. Inspection shows the reviewer's grant ceiling of two and the family ceiling of three, shared by parent, reviewer, and tester. Invocation limits follows reservation, capture, and the exhaustion a sibling meets while its own grant still has allowance left.
A widened, orphaned, or revoked child stops at admission
Each row is the deny reason a tool call receives, the condition behind it, and the check that refuses.
| Deny reason | Condition | Check |
|---|---|---|
capability verification failed: capability rejected by chain binding: delegated validity or budget share widens its signed parent | A link's validity or share exceeds what its parent signed | Signed lineage, two or more links |
capability verification failed: missing signed ancestor <capability id> | The kernel holds no signed token for a named ancestor | Signed lineage, two or more links |
capability verification failed: delegation chain exceeds configured maximum depth | A chain-binding chain exceeds the maximum depth | Signed lineage, before delegation admission |
delegation admission failed: delegation depth <depth> exceeds maximum <max> | Any other delegated chain exceeds the maximum depth | Delegation admission |
delegation admission failed: delegation link <index> timestamp <timestamp> is outside parent capability <capability id> validity window [<issued at> , <expires at>) | A link was signed outside its parent's window | Delegation admission |
delegation admission failed: child capability <capability id> expires after parent capability <capability id> | A child outlives its parent | Delegation admission |
delegation admission failed: parent capability <capability id> does not authorize delegated tool grant <server>/<tool> on child capability <capability id> | No parent grant carries delegate and covers the child's grant, proof requirement included | Delegation admission |
delegation chain revoked at ancestor <capability id> | An ancestor in the chain is revoked | Revocation |
sibling-sum budget admission failed: child <capability id> share <n> bps + sibling sum <n> bps > parent share <n> bps | The child's share plus admitted siblings' shares exceeds the parent's | Sibling-sum budget admission |
A resource, prompt, or completion request runs the same verification, revocation, and delegation admission checks and surfaces the same reason inside a typed KernelError, without the tool-call prefix "capability verification failed: " on verification refusals. It skips sibling-sum budget admission.
Revocation reaches the whole chain. Admission, the revalidation immediately before dispatch, and allow finalization each run check_revocation against the leaf's ID and every ancestor's capability ID, so revoking an ancestor refuses all descendants that chain through it, including ones issued after the revocation. An embedding host can install a RevocationView snapshot with set_revocation_view, and consult_revocation_view runs the same check against it.
To repair a refusal, compare the child with its signed parent: scope, validity, budget share, ancestry, and depth must sit inside it. Restore a missing signed ancestor through the host path that retained it, or ask the issuer for a narrower child. Raising a local limit leaves what the parent signed unchanged.
Refusals between children in a swarm
The Security swarm example gives six child processes scoped access to one jobs mailbox, each with a signed planned call. A planner sends its own job; it cannot submit the writer's task or change its route to receive jobs:
planner = swarm.calls["triage-417"]
writer = swarm.calls["fixer-417"]
peer = swarm.invoke("triage-417", writer)
widened = swarm.invoke("triage-418", swarm.calls["triage-418"],
tool_name="receive_jobs")
original = swarm.run_planned("triage-417")
same_key = swarm.run_planned("triage-417")
reused = swarm.invoke("triage-417", planner, operation_key="reused-authority")
assert runtime_failure_code(peer) == "chio_swarm_authority_rejected"
assert refused(widened, "requested tool receive_jobs on server chio-ipc is not in capability scope")
assert same_key == original
assert runtime_failure_code(reused) == "request_binding_mismatch"python3 run.py
python3 inspect.pypeer: deny; chio_swarm_authority_rejected widened: deny; requested tool receive_jobs on server chio-ipc is not in capability scope reused: deny; request_binding_mismatch
The three refusals answer three questions: whether this child owns that planned task, whether the target route is in its scope, and whether the original task authority can be relabeled as a new operation. The matching retry returns the original result, and the stopped mailbox holds exactly the two legitimate sends. A job payload that mentions repository maintenance grants no filesystem authority. Swarm authority follows the same chain across agents another operator runs.
Read more in Revocation, which covers how a revocation enters the store and propagates.
A partner delegates under a ceiling the operator signs
An operator bounds how far a partner may delegate by signing a ceiling with chio trust federated-delegation-policy-create, which names the partner and verifier, sets an expiry, and takes its scope and TTL from the single capability in the file passed to --capability-policy. The receiving operator presents that signed policy to chio trust federated-issue --delegation-policy, which enforces it as the parent scope and TTL ceiling for the capability it issues.
A federated grant also requires a DPoP proof of possession at the door that admits it, so its holder proves control of the bound key there before the kernel admits a call. Federation covers the trust relationship a delegation policy depends on.