PlatformCapabilities & Receipts
Kernel
Capabilities
Capability tokens authorize tool invocations through signed scopes, time bounds, revocation checks, and optional DPoP binding.
Source
crates/core/chio-core-types/src/capability/ module (a directory of submodules: token.rs, scope.rs, attenuation.rs, caveat.rs, and others) and crates/kernel/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 {
/// Versioned signed-artifact schema. Wire schema identifier; tokens that
/// omit this field default to `chio.capability.v1`.
#[serde(default = "default_capability_schema")]
pub schema: String,
/// Unique token ID (UUIDv7 recommended, used for revocation).
pub id: String,
/// Capability Authority (or delegating agent) that issued this token.
pub issuer: PublicKey,
/// Agent this capability is bound to (DPoP sender constraint).
pub subject: PublicKey,
/// What this token authorizes.
pub scope: ChioScope,
/// Unix timestamp (seconds) when the token was issued.
pub issued_at: u64,
/// Unix timestamp (seconds) when the token expires.
pub expires_at: u64,
/// Ordered list of delegation links from the root CA to this token.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub delegation_chain: Vec<DelegationLink>,
/// Optional invocation ceiling shared by this capability or its delegation family.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aggregate_invocation_budget: Option<AggregateInvocationBudget>,
/// Signing algorithm. Absent means Ed25519 (the default).
#[serde(default, skip_serializing_if = "is_default_optional_algorithm")]
pub algorithm: Option<SigningAlgorithm>,
/// Typed caveats. Empty tokens omit this on the wire.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub caveats: Vec<Caveat>,
/// High-level attenuation request exposed on attenuated tokens.
#[serde(default, skip_serializing_if = "is_none_or_empty")]
pub scope_attenuations: Option<Vec<Attenuation>>,
/// Wire witness proving child-scope attenuation.
#[serde(default, skip_serializing_if = "is_none_or_empty_attenuation_proof")]
pub attenuation_proof: Option<AttenuationProof>,
/// Fixed-point sub-agent budget share in basis points. Values above
/// 10000 are rejected by validation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub budget_share_bps: Option<u16>,
/// Signature over canonical JSON of all fields above.
pub signature: Signature,
}The schema field carries the constant CHIO_CAPABILITY_SCHEMA ("chio.capability.v1"); tokens that omit it on the wire default to that value. The caveats, scope_attenuations, attenuation_proof, and budget_share_bps fields are the support caveats, scope attenuation, attenuation proofs, and shared budgets. A tool token that uses none of them round-trips byte-identical to a pre-subsystem token because each is skipped from canonical JSON when empty.
Three token properties:
- Signature covers everything except itself. Newly issued tokens sign the schema-aware
CapabilityTokenSigningBody(schema, core body, caveats, scope attenuations, attenuation proof, and budget share). Legacy caveat-free, attenuation-free tokens on the default schema fall back to signing the plainCapabilityTokenBody; the selection is made bypermits_plain_body_signature(). Verification tries the schema-aware body first and only then the plain body, so tampering with any field 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 specific 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 the tool (execute it).
Invoke,
/// Read the result of a previous invocation.
ReadResult,
/// Read a resource.
Read,
/// Subscribe to resource updates.
Subscribe,
/// Retrieve a prompt.
Get,
/// Delegate this grant to another agent.
Delegate,
}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", rename_all = "snake_case", deny_unknown_fields)], so the wire form is {"type": "path_prefix", "value": "/var/log"}. All 27 variants are below, rendered from the enum rather than transcribed. Whether a given variant is decided or refused at request time is a separate question, answered in Scope Matching.
| Variant | Wire tag | Payload | Semantics |
|---|---|---|---|
PathPrefix | path_prefix | String | File path parameter must start with this prefix. |
DomainExact | domain_exact | String | Network domain must match exactly. |
DomainGlob | domain_glob | String | Network domain must match a glob pattern. |
RegexMatch | regex_match | String | Parameter must match a regular expression. |
MaxLength | max_length | usize | String parameter must not exceed this length. |
MaxArgsSize | max_args_size | usize | Serialized argument payload must not exceed this many bytes. |
GovernedIntentRequired | governed_intent_required | no payload | Requests must carry a governed transaction intent. |
RequireApprovalAbove | require_approval_above | { threshold_units: u64 } | Requests at or above this threshold require a valid approval token. |
RequireCumulativeApprovalAbove | require_cumulative_approval_above | { threshold: MonetaryAmount, approval_budget_id: String, approval_budget_epoch: u64, cumulative_approval_root_binding: Option<Box<CumulativeApprovalRootBinding>> } | Cumulative authorized spend at or above this threshold requires approval. |
SellerExact | seller_exact | String | Requests must carry commerce approval context for this exact seller. |
MinimumRuntimeAssurance | minimum_runtime_assurance | RuntimeAssuranceTier | Governed requests must carry valid runtime attestation at or above this tier. |
MinimumAutonomyTier | minimum_autonomy_tier | GovernedAutonomyTier | Governed requests at or above this autonomy tier must carry autonomy context and pass bond gating. |
Custom | custom | String, String | Extensibility: arbitrary key-value constraint. |
TableAllowlist | table_allowlist | Vec<String> | Data layer: database tables the grant may reference. Evaluated against parsed SQL by `chio-data-guards`; the kernel records the constraint and leaves enforcement to that guard. |
ColumnDenylist | column_denylist | Vec<String> | Data layer: forbidden columns, formatted as `"table.column"`. Evaluated by `chio-data-guards`; kernel treats it as an advisory constraint and does not reject at the request-matching stage. |
MaxRowsReturned | max_rows_returned | u64 | Data layer: maximum number of rows a query may return. Enforced post-invocation by downstream result-shaping guards. |
OperationClass | operation_class | SqlOperationClass | Data layer: operation class the grant authorises. |
AudienceAllowlist | audience_allowlist | Vec<String> | Communication: allowed recipient channels or IDs. |
ContentReviewTier | content_review_tier | ContentReviewTier | Communication: content review tier demanded of downstream guards. |
MaxTransactionAmountUsd | max_transaction_amount_usd | String | Financial: maximum transaction amount in USD. The value is a decimal string (e.g. `"100.00"`) because `rust_decimal` is not in the workspace. |
RequireDualApproval | require_dual_approval | bool | Financial: whether the grant requires dual approval before execution. |
ModelConstraint | model_constraint | { allowed_model_ids: Vec<String>, min_safety_tier: Option<ModelSafetyTier> } | Model routing: constrain the models this grant may execute under. |
MemoryStoreAllowlist | memory_store_allowlist | Vec<String> | Memory governance: memory stores the grant may write to. |
MemoryWriteDenyPatterns | memory_write_deny_patterns | Vec<String> | Memory governance: regex patterns that block writes. Patterns are compiled lazily during kernel evaluation so invalid regexes do not break construction or round-trip serialization. |
OutputDigestSha256 | output_digest_sha256 | String | Delivery: an Allow for this grant is valid only if the final post-transform output content hash equals this expected digest, otherwise the request is denied before any irreversible money movement. The value is a canonical lowercase 64-character hex SHA-256 digest. Unlike the argument-side constraints above, this is enforced at the output-aware durable terminal, not at request matching; the request matcher only admits the carrier. Every surface that cannot prove the output-aware terminal ordering rejects the constraint before it mutates budget or payment. |
RequireFindingPurchase | require_finding_purchase | Box<FindingPurchaseMarkerV1> | Delivery: this grant authorizes exactly one purchased finding reveal. The provider-signed marker names the exact finding and listing being sold and closes over the settlement rail, so omitted purchase context can never downgrade into a generic digest-constrained call. Admission requires the request to name the same finding, a verified purchase context, and the settlement profile the selector admits. Surfaces without purchase-aware admission reject the marker before any budget or payment mutation. |
RequireFindingRecovery | require_finding_recovery | Box<FindingRecoveryMarkerV1> | Delivery: this grant authorizes a bounded no-charge redelivery of a finding whose paid delivery was already settled. Recovery is a distinct authorization profile: it never satisfies a purchase marker and never enters a payment path. Admission requires the dedicated recovery carrier named by this marker and an exact top-level `finding_id` argument. Surfaces without recovery-aware admission reject the marker before dispatch. |
The keys custom is not allowed to spell
Custom is the escape hatch: a key and an expected value, matched against the request arguments. The portable matcher refuses 5 keys outright rather than matching them, each with {key} must use its first-class constraint, not Custom:
output_digest_sha256is the wire tag ofOutputDigestSha256.require_finding_purchaseis the wire tag ofRequireFindingPurchase.require_finding_recoveryis the wire tag ofRequireFindingRecovery.recovery_of_receipt_idcarries recovery authority and has no variant of its own.recovery_of_capability_idcarries recovery authority and has no variant of its own.
The refusal is deliberate, and the reason is worth stating: merely failing the grant would let an unconstrained sibling grant serve the same call, which would silently downgrade the boundary the issuer set. Refusing the spelling means a caller who reaches for the escape hatch finds out rather than getting a weaker grant that still works.
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
Four algorithms ship today. The enum serializes as a short lowercase identifier ("ed25519", "p256", "p384", "hybrid"). Backward-compatibility is preserved via the optional algorithm envelope field: when absent, consumers MUST treat the algorithm as Ed25519 so legacy tokens deserialize unchanged.
pub enum SigningAlgorithm {
/// Edwards-curve signature on Curve25519. Default, non-FIPS.
#[default]
Ed25519,
/// ECDSA on NIST P-256 / secp256r1 with SHA-256. Requires `fips` feature.
P256,
/// ECDSA on NIST P-384 / secp384r1 with SHA-384. Requires `fips` feature.
P384,
/// Classical signature plus ML-DSA-65. Requires `pq` feature.
Hybrid,
}Verification dispatches off the self-describing prefix on PublicKey and Signature, not the envelope field. The algorithm field is informational; when present it MUST agree with the signature's prefix, and a mismatch is rejected fail-closed as a downgrade signal. Without the fips feature, P-256 and P-384 verification returns Ok(false).
Hybrid pairs a classical signature with ML-DSA-65 (FIPS 204) post-quantum material and is the algorithm the kernel-wide CryptoFloor::AllowHybrid and CryptoFloor::PqRequired postures require. Hybrid public keys and signatures carry a hybrid: hex prefix, parallel to p256: and p384:. verify_signature_with_floor enforces the floor before the cryptographic check.
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 {
/// 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,
}scope_hash is the delegation chain-binding mechanism. It records the SHA-256 of the canonical scope authorized at that hop and ties AttenuationProof.parent_scope_hash on the next hop back to it, closing a parent-scope-inflation soundness hole. It is absent on older links and enforced when the delegation_chain_binding feature is active.
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. - When chain binding is active, each link's
scope_hashmust match the next hop'sattenuation_proof.parent_scope_hash, and the first hop'sscope_hashmust match the trust root's scope hash. A link that omitsscope_hashis rejected.
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 {
/// Check if a capability ID has been revoked.
fn is_revoked(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;
/// Revoke a capability. Returns `true` if it was newly revoked.
fn revoke(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;
fn observe_revocation(
&self,
capability_id: &str,
) -> Result<RevocationObservation, RevocationStoreError> {
Ok(RevocationObservation {
revoked: self.is_revoked(capability_id)?,
commit: None,
})
}
/// Whether this store loses its revocation set on process restart. The
/// default is the safe (loud) assumption so an unknown store is treated as
/// ephemeral; durable and remote stores override to `false`.
fn is_ephemeral(&self) -> bool {
true
}
}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.
The third method, is_ephemeral, is default-provided and matters to anyone implementing a custom store. The default returns true, on the safe, loud assumption that an unknown store forgets its revocation set on restart. InMemoryRevocationStore keeps that default; the SQLite-backed store overrides it to false. A kernel built from chio.yaml sets allow_ephemeral_revocation_store = false, so a custom store that has to survive a restart must override is_ephemeral() to return false to be accepted.
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/kernel/chio-kernel/src/dpop.rs for the verification logic.
Worked example: sign and verify
Build a body, sign it with an Ed25519 keypair, then verify it against a trusted-issuer set.
use chio_core_types::capability::scope::{
ChioScope, Constraint, Operation, ToolGrant,
};
use chio_core_types::capability::token::{CapabilityToken, CapabilityTokenBody};
use chio_core_types::crypto::Keypair;
use chio_kernel_core::capability_verify::verify_capability;
use chio_kernel_core::clock::FixedClock;
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()];
// FixedClock is the only concrete Clock chio-kernel-core ships. Production
// callers supply their own Clock impl wrapping std / browser / embedded time.
let clock = FixedClock::new(1_700_000_100); // inside [issued_at, expires_at)
let verified = verify_capability(&token, &trusted, &clock)
.expect("token verifies");
assert_eq!(verified.id, token.id);Verification returns a VerifiedCapability with the state required for downstream scope and revocation checks. Adapters that drop the original token after verification retain the captured scope. chio-kernel-core is deliberately clock-source-agnostic: it never calls SystemTime::now(), so every verifier threads time in through a &dyn Clock.
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