Mandates & Capabilities
A capability token is the only thing that lets an agent invoke a tool. There is no ambient authority, no role lookup, no implicit trust. The kernel demands a signed, scoped, time-bounded token on every request and denies the call if any check fails. This page is the operator contract for that surface: which fields the token carries, which grants and constraints narrow it, how delegation chains attenuate scope, and what verification actually checks.
Source
crates/chio-core-types/src/capability.rs and crates/chio-kernel-core/src/capability_verify.rs. Field names, enum variants, and defaults are taken from those files. If this page disagrees with source, source wins.CapabilityToken
The on-the-wire token shape:
pub struct CapabilityToken {
pub id: String, // UUIDv7 recommended; used for revocation
pub issuer: PublicKey, // Capability authority or delegating agent
pub subject: PublicKey, // Agent this token is bound to (DPoP sender constraint)
pub scope: ChioScope, // What the token authorizes
pub issued_at: u64, // Unix seconds
pub expires_at: u64, // Unix seconds
pub delegation_chain: Vec<DelegationLink>,
pub algorithm: Option<SigningAlgorithm>, // Absent means Ed25519 (legacy default)
pub signature: Signature, // Over canonical JSON of all fields above
}Three properties matter for operators:
- Signature covers everything except itself. The body type is
CapabilityTokenBodyand its canonical JSON is the signing input. Tampering with any field (scope, expiry, delegation_chain) invalidates the signature. - Subject binding is enforced via DPoP. The token names the agent it is intended for. The kernel checks DPoP proof against this subject when
dpop_requiredis set on a grant. - Time bounds are inclusive of issued_at and exclusive of expires_at. A token is valid when
issued_at <= now < expires_at.
ChioScope and Grant Variants
ChioScope is a flat container holding three optional grant lists. Empty lists are skipped from canonical JSON, so a tool-only token round-trips byte-identical to its pre-resource-grant ancestors.
pub struct ChioScope {
pub grants: Vec<ToolGrant>,
pub resource_grants: Vec<ResourceGrant>,
pub prompt_grants: Vec<PromptGrant>,
}ToolGrant
The most heavily used grant. Each ToolGrant authorizes a single tool on a single server, with optional invocation, cost, and constraint attenuation:
pub struct ToolGrant {
pub server_id: String,
pub tool_name: String,
pub operations: Vec<Operation>,
pub constraints: Vec<Constraint>,
pub max_invocations: Option<u32>,
pub max_cost_per_invocation: Option<MonetaryAmount>,
pub max_total_cost: Option<MonetaryAmount>,
pub dpop_required: Option<bool>,
}server_id and tool_name support a literal "*" as a parent wildcard during delegation, but a leaf token must name a concrete tool.
ResourceGrant
pub struct ResourceGrant {
pub uri_pattern: String,
pub operations: Vec<Operation>,
}URI pattern matching is literal-equal, full-match *, or trailing-prefix prefix*. There is no glob engine; if you need richer patterns, narrow at issuance time.
PromptGrant
pub struct PromptGrant {
pub prompt_name: String,
pub operations: Vec<Operation>,
}Operations
The Operation enum is shared across all three grant kinds:
pub enum Operation {
Invoke, // Execute the tool
ReadResult, // Read a prior invocation result
Read, // Read a resource
Subscribe, // Subscribe to resource updates
Get, // Retrieve a prompt
Delegate, // Delegate this grant to another agent
}A grant with Delegate in its operations list is the precondition for further delegation. Without it, the holder cannot extend the chain.
Constraint Variants
Constraints narrow tool parameters or impose runtime requirements. They serialize via #[serde(tag = "type", content = "value")], so the wire form is {"type": "path_prefix", "value": "/var/log"}. The full enum, taken from source:
| Variant | Payload | Semantics |
|---|---|---|
PathPrefix | String | File path parameter must start with this prefix |
DomainExact | String | Network domain must match exactly |
DomainGlob | String | Network domain matches a glob |
RegexMatch | String | Parameter must match regex |
MaxLength | usize | String parameter length cap |
MaxArgsSize | usize | Serialized arg payload byte cap |
GovernedIntentRequired | unit | Request must carry a governed transaction intent |
RequireApprovalAbove | { threshold_units: u64 } | Approval token required above threshold |
SellerExact | String | Commerce approval bound to this seller |
MinimumRuntimeAssurance | RuntimeAssuranceTier | Floor on runtime attestation tier |
MinimumAutonomyTier | GovernedAutonomyTier | Floor on bonded autonomy tier |
TableAllowlist | Vec<String> | SQL tables this grant may reference |
ColumnDenylist | Vec<String> | Forbidden table.column identifiers |
MaxRowsReturned | u64 | Row cap, enforced post-invocation |
OperationClass | SqlOperationClass | read_only / read_write / admin |
AudienceAllowlist | Vec<String> | Allowed recipient channels or IDs |
ContentReviewTier | ContentReviewTier | none / basic / strict review demand |
MaxTransactionAmountUsd | String (decimal) | Per-transaction USD cap |
RequireDualApproval | bool | Two-party approval required |
ModelConstraint | { allowed_model_ids, min_safety_tier } | Constrain executing model identity / safety tier |
MemoryStoreAllowlist | Vec<String> | Memory stores this grant may write to |
MemoryWriteDenyPatterns | Vec<String> | Regex patterns that block writes |
Custom | (String, String) | Extensibility key / value pair |
Constraint subset semantics during delegation
PathPrefix("/") by replacing it with PathPrefix("/etc"); the parent constraint must appear unchanged in the child, alongside any further restrictions.Signing Algorithms
Three algorithms ship today. Backward-compatibility is preserved via the optional algorithm envelope field: when absent, consumers MUST treat the algorithm as Ed25519 so legacy artifacts deserialize unchanged.
pub enum SigningAlgorithm {
Ed25519, // Default, non-FIPS. No feature flag required.
P256, // ECDSA P-256 / SHA-256. Requires the `fips` feature.
P384, // ECDSA P-384 / SHA-384. Requires the `fips` feature.
}Verification dispatches off the self-describing prefix on PublicKey and Signature, not the envelope field. The algorithm field is informational. Building a chio binary without the fips feature causes P-256 and P-384 verification to return Ok(false) rather than attempt the algorithm.
Delegation Chains
Delegation lets one agent narrow its own capability and hand the attenuated form to another agent. The chain is recorded inline on the leaf token:
pub struct DelegationLink {
pub capability_id: String, // ID of the ancestor token at this step
pub delegator: PublicKey,
pub delegatee: PublicKey,
pub attenuations: Vec<Attenuation>, // How the scope was narrowed
pub timestamp: u64,
pub signature: Signature, // Delegator signs canonical(body)
}Verification reads the chain root-to-leaf:
- The first link's
delegatormust equal the issuer of the original capability (the root of trust). - Each subsequent link's
delegatormust equal the previous link'sdelegatee. - Each link's signature is verified against its delegator key.
- The leaf token's
subjectmust equal the final link'sdelegatee. - The leaf scope must be a subset of every ancestor scope. The subset check uses
ChioScope::is_subset_of, which composes per-grant checks over tools, resources, and prompts.
Verification
The portable, IO-free check lives in chio-kernel-core::capability_verify:
pub fn verify_capability(
token: &CapabilityToken,
trusted_issuers: &[PublicKey],
clock: &dyn Clock,
) -> Result<VerifiedCapability, CapabilityError>It performs three checks and nothing else:
- Issuer trust.
token.issuermust appear in the supplied trusted-issuer set. - Signature.
token.verify_signature()re-canonicalizes the body and dispatches verification off the signature's self-describing encoding. - Time bounds.
classify_time_window(now, issued_at, expires_at)returns one ofValid,NotYetValid,Expired.
Revocation, delegation lineage, scope match against a request, and DPoP subject binding are deliberately out of scope here. They live in chio-kernel because they require IO or transport state. The full kernel orchestrates all of them in ChioKernel::evaluate_tool_call_sync.
Revocation
Revocation is a separate signal layered on top of valid-signature tokens. The kernel queries a RevocationStore:
pub trait RevocationStore: Send + Sync {
fn is_revoked(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;
fn revoke(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;
}Two implementations ship: an InMemoryRevocationStore suitable for tests and short-lived processes, and a SQLite-backed store in chio-store-sqlite for production. The lookup happens once per evaluation, before the guard pipeline runs. A revoked token denies with a fixed reason regardless of scope.
Distributed revocation
DPoP Subject Binding
DPoP (Demonstration of Proof-of-Possession) binds a single tool invocation to the agent named in token.subject. The agent signs a per-request proof; the kernel verifies that signature against the subject key.
pub struct DpopProof {
pub body: DpopProofBody,
pub signature: Signature,
}DPoP is opt-in per grant via ToolGrant::dpop_required. The semantics:
Some(true): the kernel requires a valid DPoP proof on every invocation.Some(false)orNone: DPoP is not enforced.- During delegation, a parent that requires DPoP forces every child grant to also require DPoP. A child cannot relax this requirement.
Schema: chio.dpop_proof.v1. The proof is namespaced and replay-protected via a nonce store owned by the kernel; consult crates/chio-kernel/src/dpop.rs for the verification logic.
Worked Example: Sign and Verify
End-to-end flow: build a body, sign with an Ed25519 keypair, then verify against a trusted-issuer set.
use chio_core_types::capability::{
CapabilityToken, CapabilityTokenBody, ChioScope, ToolGrant, Operation,
Constraint,
};
use chio_core_types::crypto::Keypair;
use chio_kernel_core::capability_verify::verify_capability;
use chio_kernel_core::clock::SystemClock;
let issuer_kp = Keypair::generate();
let agent_kp = Keypair::generate();
let body = CapabilityTokenBody {
id: "01HXJ...".into(),
issuer: issuer_kp.public_key(),
subject: agent_kp.public_key(),
scope: ChioScope {
grants: vec![ToolGrant {
server_id: "fs".into(),
tool_name: "read_file".into(),
operations: vec![Operation::Invoke],
constraints: vec![Constraint::PathPrefix("/var/log".into())],
max_invocations: Some(1000),
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: Some(true),
}],
resource_grants: vec![],
prompt_grants: vec![],
},
issued_at: 1_700_000_000,
expires_at: 1_700_086_400, // +24h
delegation_chain: vec![],
};
let token = CapabilityToken::sign(body, &issuer_kp).expect("sign");
let trusted = [issuer_kp.public_key()];
let clock = SystemClock;
let verified = verify_capability(&token, &trusted, &clock)
.expect("token verifies");
assert_eq!(verified.id, token.id);The result is a VerifiedCapability carrying just enough state for downstream scope-match and revocation checks. Adapters that drop the original token after verification still hold the captured scope.
Next Steps
- Receipts & Audit · what gets recorded for each verified or denied capability use
- Guard Trait · how guards run after capability verification succeeds
- Rotate Keys, Revoke · operational workflow for revocation and key rotation
- Capabilities (Concept) · why chio uses capabilities instead of role-based access