Chio/Docs
LOGIN · JOIN

LearnAnatomy of a Governed Call

Capabilities

How a capability token carries scoped, time-bounded authority to one agent: its fields, its grants, delegation, and revocation.

What a capability token is

A signed capability token grants scoped authority to an agent. The agent presents the token when requesting a governed call. The autonomous-commerce page explains capabilities as spending authorizations, and the Assurance Model page states which assumptions bound them. The token fields, bindings, and validation rules follow.

A capability token answers four questions:

  1. Who issued it? The issuer public key identifies the Capability Authority or delegating agent.
  2. Who is it for? The subject public key binds the token to a specific agent.
  3. What does it allow? The scope declares which tools, resources, and prompts the bearer can use.
  4. When does it expire? issued_at and expires_at timestamps bound the token's validity window.

CapabilityToken structure

The signature covers the canonical JSON, per RFC 8785, of every other field. Verification re-serializes the token without its signature, canonicalizes that, and checks the result against issuer.

crates/core/chio-core-types/src/capability/token.rsrust
/// A Chio capability token. Scoped, time-bounded, cryptographically signed.
///
/// The `signature` field covers the canonical JSON of all other fields.
/// Verification re-serializes the token (excluding the signature), computes
/// the canonical form, and checks the signature against `issuer` using the
/// algorithm declared by the `algorithm` field (defaulting to Ed25519 when
/// absent).
#[derive(Debug, Clone, Serialize, Deserialize)]
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,
}

Delegation and attenuation ride on their own fields. scope_attenuations records the high-level narrowing requested when a child token is minted; attenuation_proof is the wire witness the kernel checks to confirm that the child scope is a subset of its parent; budget_share_bps pins a sub-agent's share of a parent budget in basis points, and validation rejects a value above 10000; and caveats holds typed side conditions that narrow use further. Volume and version have fields of their own: aggregate_invocation_budget sets an invocation ceiling the capability shares with its delegation family, and schema is the versioned wire identifier, which defaults to chio.capability.v1 when a token omits it.

Signing algorithms

Ed25519 is the default signing algorithm, and an Ed25519 token omits the algorithm field on the wire; a consumer that finds the field absent must read it as Ed25519. The other values are feature-gated in the crate: p256 and p384 need the fips feature and route through aws-lc-rs, and hybrid, one classical signature plus ML-DSA-65, needs the pq feature.

ChioScope

The scope field is a ChioScope holding one grant vector each for tools, resources, and prompts. An empty vector is omitted on the wire.

crates/core/chio-core-types/src/capability/scope.rsrust
/// What a capability token authorizes.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ChioScope {
    /// Individual tool grants.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub grants: Vec<ToolGrant>,

    /// Individual resource grants.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub resource_grants: Vec<ResourceGrant>,

    /// Individual prompt grants.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub prompt_grants: Vec<PromptGrant>,
}

A child scope is a subset of its parent when every child grant, in each vector, is a subset of some parent grant in the same vector.

ToolGrant

Each ToolGrant names the server, the tool, and the allowed operations, then bounds them with parameter constraints, an invocation cap, two cost caps, and a proof-of-possession switch.

crates/core/chio-core-types/src/capability/scope.rsrust
/// Authorization for a single tool on a single server.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolGrant {
    /// Which tool server (by server_id from the manifest).
    pub server_id: String,
    /// Which tool on that server.
    pub tool_name: String,
    /// Allowed operations.
    pub operations: Vec<Operation>,
    /// Parameter constraints that narrow the tool's input space.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub constraints: Vec<Constraint>,
    /// Maximum number of invocations allowed under this grant.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_invocations: Option<u32>,
    /// Maximum monetary cost per single invocation under this grant.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_cost_per_invocation: Option<MonetaryAmount>,
    /// Maximum aggregate monetary cost across all invocations under this grant.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_total_cost: Option<MonetaryAmount>,
    /// If Some(true), the kernel requires a valid DPoP proof for every invocation.
    /// None and Some(false) both mean DPoP is not required.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dpop_required: Option<bool>,
}

MonetaryAmount carries a units field in the currency's smallest unit, cents for USD, and a currency field holding an ISO 4217 code. A comparison between a child cap and a parent cap requires the same currency string, so the caps never cross denominations.


Constraints on a grant

Constraint has 27 variants. The portable matcher in chio-kernel-core decides 8 of them from the request arguments alone, among them path_prefix, domain_glob, and max_args_size. It returns a ScopeMatchError::ConstraintError naming the other 19, which denies the call rather than admitting a constraint it cannot evaluate. The refused set holds the data-layer constraints that chio-data-guards evaluates against parsed SQL, the thresholds an approval path resolves, and the markers the output-aware delivery terminal enforces.

The enum uses adjacent tagging, so a constraint is an object with a type field holding the snake_case variant name and a value field holding its payload. It also denies unknown fields, so a token carrying a variant a kernel does not know fails deserialization, and that kernel denies the call.

tests/bindings/vectors/capability/v1.jsonjson
{
  "expires_at": 1710000800,
  "id": "cap-bindings-complex-constraints",
  "issued_at": 1710000200,
  "issuer": "66be7e332c7a453332bd9d0a7f7db055f5c5ef1a06ada66d98b39fb6810c473a",
  "scope": {
    "grants": [
      {
        "constraints": [
          {
            "type": "domain_glob",
            "value": "*.chio-protocol.dev"
          },
          {
            "type": "max_length",
            "value": 1024
          },
          {
            "type": "max_args_size",
            "value": 4096
          },
          {
            "type": "custom",
            "value": [
              "rate_limit",
              "10/min"
            ]
          }
        ],
        "max_invocations": 50,
        "operations": [
          "invoke",
          "read_result"
        ],
        "server_id": "srv-net",
        "tool_name": "http_get"
      }
    ]
  },
  "signature": "f07e2f37668b446180eefaf981bbc06ac380d9f716ff25a9e4236ee30a5b4f3b395dcf2fd2ee9fe31ea36aa612dea37b8a2e29789b6162ab713ae19b489d7901",
  "subject": "0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d"
}

The delivery variants bind the output rather than the arguments: OutputDigestSha256, RequireFindingPurchase, and RequireFindingRecovery, with wire tags output_digest_sha256, require_finding_purchase, and require_finding_recovery. The request matcher rejects Constraint::Custom("output_digest_sha256", ...) as a carrier, and it rejects the whole call even when an unconstrained sibling grant would match, so the digest has no Custom spelling, the one form an unaware kernel would accept without enforcing it. Under delegation a child token must keep each parent digest or marker with an identical value; the attenuation check in crates/core/chio-core-types/src/capability/scope.rs compares them by equality. See The Delivery Contract for what the kernel does with the digest.


Time-bounded tokens

Every capability token has an issued_at and expires_at timestamp, both Unix seconds. The type has no field for an unbounded grant, and the kernel rejects a token whose verification time falls outside that window.

Short-lived tokens limit the effect of a compromise. A stolen token expires on its own; revocation can invalidate it sooner. The shorter interval limits its use.

Subject binding

The subject field binds a token to one agent by its public key. The kernel checks the subject against the requesting agent's identity in the admission path, before it runs any guard, so a token issued to agent A does not admit a call from agent B. DPoP is the per-invocation replay-resistance hardening on top of this subject binding.

DPoP sender constraint

Subject binding alone proves that a token was intended for an agent. DPoP, demonstration of proof of possession, proves the agent currently holds the corresponding private key. When dpop_required is Some(true) on a grant, the kernel requires a fresh, signed DPoP proof with each invocation. None and Some(false) both mean the proof is not required.

This sender constraint limits replay of an intercepted token. An attacker who lacks the agent's private key cannot produce the required signed DPoP proof.

DPoP is per-grant

DPoP is configured on the grant, not on the token. One token can carry some grants that require DPoP and others that do not. A child grant under a parent that sets Some(true) must set it too, or the subset check fails.

Delegation chains

An agent holding a token can delegate a child token to another agent. The child token must attenuate, or narrow, the parent's permissions.

Each link names the ancestor capability id, the delegator, the delegatee, how the scope was narrowed at that hop, and the delegator's signature over the canonical form of the other fields in the link. When a sub-agent spends under a delegated capability, the receipt carries the root_budget_holder and the delegation depth, which traces delegated authority and cost back to the original grant that reconciliation settles against.

A delegated grant must have:

  • The same server_id and tool_name, unless the parent holds *
  • Operations that are a subset of the parent's operations
  • Every parent constraint preserved, and no cumulative-approval constraint the parent does not carry
  • A max_invocations value that is set and no greater than the parent's, whenever the parent sets one
  • A max_cost_per_invocation and a max_total_cost in the parent's currency and no greater in units, whenever the parent sets one

The token as a whole must also expire no later than its parent. The proof manifest records these as P1 capability attenuation, carried by a Lean theorem imported into the root module plus a Rust projection, an Aeneas equivalence, a public Kani harness, and a differential test. The theorem does not reach the full Rust runtime. The Assurance Model page shows what evidence the manifest records for each property.

The delegation_chain field records the ordered list of delegation links from the root CA to the current token. The kernel walks this chain, checks that the leaf subject matches the final delegatee, and checks each ancestor's stored depth and parent link against the chain it was handed.

tests/bindings/vectors/capability/v1.jsonjson
{
  "delegation_chain": [
    {
      "capability_id": "cap-bindings-valid",
      "delegatee": "91a28a0b74381593a4d9469579208926afc8ad82c8839b7644359b9eba9a4b3a",
      "delegator": "66be7e332c7a453332bd9d0a7f7db055f5c5ef1a06ada66d98b39fb6810c473a",
      "signature": "a4c297402d3bef255c8a9648215d9bb9c7fcbfb0a2e4009cfc6202459c10b2e4066de4ef60d4118f612993b6ceb2197095483e94c8a2ccc658ad7ef5b865be00",
      "timestamp": 1710000250
    }
  ],
  "expires_at": 1710000800,
  "id": "cap-bindings-valid",
  "issued_at": 1710000200,
  "issuer": "66be7e332c7a453332bd9d0a7f7db055f5c5ef1a06ada66d98b39fb6810c473a",
  "scope": {
    "grants": [
      {
        "constraints": [
          {
            "type": "path_prefix",
            "value": "/workspace/"
          }
        ],
        "max_invocations": 3,
        "operations": [
          "invoke",
          "read_result"
        ],
        "server_id": "srv-files",
        "tool_name": "file_read"
      }
    ]
  },
  "signature": "a16f5a447da2a5dbd840e44c7c1a5093474e4a1b9826ae80ca85f197b18e8ad05607745ac9e0ff063db7b49ff4da637b630cf10d55c1ef1e64fb5c27d81ed505",
  "subject": "0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d"
}

The token is the binding vector the Chio test suite verifies as a valid delegated capability. Its link carries the ancestor id, the two keys, the timestamp, and the delegator signature, and its grant is bounded by a path_prefix constraint and an invocation cap.


Revocation

The Capability Authority can revoke a token by its id. The kernel consults the revocation store for the token and for every capability id in the delegation chain the caller presented: a revoked leaf returns CapabilityRevoked, and a revoked ancestor returns DelegationChainRevoked. The proof manifest records this as P2 presented revocation coverage, and the name states the boundary: the check covers the chain the token presents.

The kernel checks revocation during token validation, before any guard runs. A revoked token resolves the same way as an expired one: the request is denied and the kernel signs a deny receipt.

Revocation requires a new token

The revocation flow does not reinstate a revoked token. Issue a new token if the agent needs access again.

Next steps

  • Autonomous Commerce: capabilities as spending authorizations
  • Guards: how the guard pipeline enforces policy on every invocation
  • Receipts: signed records of kernel decisions
  • Economics: budgets, metering, and settlement