Chio/Docs
LOGIN · JOIN

Capabilities

Define what an agent may do, what it may delegate, and how the kernel enforces those grants.

Concept chio@5f63e6e

On this page

Capabilities let an application organize agents around distinct responsibilities. A coordinator can give a specialist the authority its work requires and keep broader authority for itself, and Chio enforces that separation at execution, however either agent plans its work.

A capability specifies which operations an agent may perform, which resources it may access, and which authority it may delegate. Delegating an assignment cannot create permissions its parent did not hold. In the pricing project, the reviewer reads pull request #421 and searches the repository, and the tester it assigns only reads the tests. Both operate under the same kernel.

Agents share the kernel and hold distinct grants

Each agent presents its own grant, and one kernel admits every call against the grant that agent holds.

Two agents, distinct capabilities, one kernelThe reviewer delegates read access to the tester. Each agent presents its own capability to the same Chio kernel. Permitted operations reach the pricing project’s workspace through an execution adapter. Dashed lines represent delegation; solid lines represent access.pcdelegates read accessChio kernelexecution adapter/workspace

Grants

Tools
read_filesearch
Operations
invokedelegate
Tools
read_file
Operations
invoke
The reviewer delegates read access to the tester. Each presents its own capability to the same kernel when it reads the workspace. Dashed: delegation. Solid: access through the kernel.

Applications share Chio’s rules for authority, delegation, and execution admission while choosing their own models and workflows, and adapters connect those rules to the protocols and resources they use. A capability makes an agent’s authority available for the kernel to check: who granted it, who may exercise it, which operations it covers, and when it expires. The application can change a worker’s assignment or replace its planning logic, and those permissions stay explicit.

The host establishes isolation and supplies the configured storage and worker lifecycle. For a protected resource, the host must make kernel admission mandatory by preventing access through other credentials or handles.

Read more in Architecture.

Text an agent reads cannot add to its grant

Pull request #421 contains a comment telling AI reviewers to delete the failing test for issue #414 “so the suite can run”. The reviewer has to read the text to do its job, and it may treat that text as an instruction. Its capability covers only read_file and search on /workspace, so the kernel refuses the write at the scope check. The Mediated Call follows the comment’s other instructions, from the injected text to each refusal.

In The Confused Deputy, Norm Hardy described a compiler allowed to write usage statistics into a system directory. A user named the billing file there as the destination for debugging output, and the compiler used its own authority to overwrite the billing records. A tool-using agent can combine a request and authority the same way: text from a repository, web page, or message can steer a call made with its operator’s credentials.

Chio keeps the authority decision in the kernel: the agent chooses what to request, and the kernel admits operations under an explicit grant. Text in a pull request cannot add a permission to that grant, and assigning a worker cannot widen it. That makes the grant’s size a design decision: a grant that also authorized writes would pass the scope check, and only the tests-are-evidence rule would stand between the comment and the test. Grant only the authority the task needs and check every use: the least privilege and complete mediation principles, applied across the application.

A grant is signed for one subject

A grant describes the work an agent may perform. For the reviewer, that means reading /workspace without changing it. In the Quickstart project, you fork the project and grant the fixer agent the workspace, your model route, and two dollars for twenty minutes.

quickstart.tsquickstart.pysrc/main.rs · in main
import { Computer, USD } from "@chio-protocol/sdk";

const project = await Computer.open("quickstart");
const fixer = await project.agent("fixer");
const fork = await project.fork();
const authority = fork.grant({
  to: fixer,
  resources: { "/workspace": ["read", "write"], "/models/default": ["invoke"] },
  budget: USD(2),
  expiresIn: "20m",
});
console.log(authority.describe());
Output
grant      cap-01a8…31f2, signed by your kernel
to         fixer, did:chio:z6MkrT2…8wd
on         quickstart, fork 51c07ae
allows     /workspace         read, write
           /models/default    invoke
budget     USD 2.00, charged at your door
expires    in 20 minutes

The issuer creates the permission by signing a token that contains the scope, the caller’s public key, and its validity period. The kernel checks that the issuer is trusted and that the token has not changed. In the output, your kernel is the issuer, the fixer is the subject who may exercise the grant, and the allows lines are the scope of permitted calls.

Knowing a tool’s name or a token’s identifier, or copying the grant code, does not provide that authority. Editing the grant code changes a future grant; it does not rewrite a token already signed.

Read more about grant fields and their constraints in the capability reference.

The kernel checks the token before the call

A valid signature establishes that the token has not changed since its issuer signed it. The kernel still has to decide whether that token authorizes this caller, at this time, to make this call, and its capability checks answer that before guards or tools run.

Capability checksBefore guards or tools run
issuersignaturec1validityexpires_atc2revocationc3delegationc4subjectsubjectc5scopescopec6Any failure stops the callrRequestκtokengGuardspolicyissuersignaturec1validityexpires_atc2revocationc3delegationc4subjectsubjectc5scopescopec6Any failure stopsrRequestcarries token κgGuardspolicy
  • A trusted issuer and a valid signature establish who authorized the grant.
  • The signed issued_at and expires_at fields establish that the token is valid now.
  • The kernel’s revocation records, including the delegation chain, establish that the authority has not been revoked.
  • For a delegated token, each child’s authority must fit within its parent’s delegatable grants.
  • The caller must match the token’s subject.
  • The request must match the token’s scope.
A call reaches guards only when every check passes; the delegation check applies to delegated tokens. The drawing does not fix the order in which the kernel runs the checks.

A failure stops the call. A request that passes still goes on to policy evaluation. When a grant requires proof of possession, the kernel also verifies a fresh proof signed with the caller’s key. In the Quickstart project, the fixer tried to read /secrets/.env, which its grant does not cover.

quickstart.tsquickstart.pysrc/main.rs · in main
const [refusal] = await project.history.receipts({ outcome: "deny" });
console.log(refusal.describe());
Output
receipt    rcpt-01a8…9c04
request    fixer read /secrets/.env
verdict    deny: outside the grant
grant      cap-01a8…31f2
policy     sha256 3c7b…8010
signed     your kernel 4f1c…a2e9, signature verifies

The refusal comes from the grant itself, at the scope check, before a guard needs to inspect the call. The receipt records the decision and identifies the capability that was checked. That identifier connects the decision to a token; it does not contain the grant or establish that a tool executed.

Read more in Receipts.

Delegation only narrows

The reviewer may hand the tester one file of pull request #421 to inspect while keeping responsibility for the whole pull request. The tester needs enough authority to finish that part of the job. Giving it the parent’s entire credential would also give it permissions unrelated to the assignment.

An agent can pass part of its authority to a worker. Every grant in the child scope C must be covered by a grant in the parent scope P. The covering parent grant must also permit delegate.

The parent below has overlapping grants: named operations and a * grant for the repository. A child can have more than one covering parent grant. Add a read under secrets/ and none of those parents preserves its path constraint. The scope check fails for the entire child scope.

One relation, two representationsSelect a child to follow its coverage
Parent grants covering child grantsp0 and p2 cover c0. p1 and p2 cover c1. p3 covers c2. Solid green edges identify the selected child’s covering grants.PCparent grantschild grantsp₀p₁p₂p₃c₀c₁c₂each edge is a covering grant
The same coverage, in a matrix
p₀p₁p₂p₃Row result
p0 covers c0p1 does not cover c0p2 covers c0p3 does not cover c0Covered
p0 does not cover c1p1 covers c1p2 covers c1p3 does not cover c1Covered
p0 does not cover c2p1 does not cover c2p2 does not cover c2p3 covers c2Covered

1 = covers · 0 = does not cover

c₀
read_fileunder repository/

Covered by p₀ and p₂The named grant and the wildcard both cover it.

Every child has a matchDifferent children can use different covering parents.
Inspect the grants
Parent grants P · Invoke + Delegate
GrantToolPath prefix
p₀read_filerepository/
p₁searchrepository/
p₂*repository/
p₃write_reportreports/
Child grants C · Invoke
GrantToolPath prefix
c₀read_filerepository/
c₁searchrepository/
c₂write_reportreports/
Coverage of illustrative grants on the repo adapter. The parent’s * grant covers any tool name while retaining the repository/ path constraint. A positive scope check is one condition of delegation; signatures, lineage, and validity still need checks. The controls change the illustration, not a live capability.

The same check can be written as a rule. For every child grant, find a parent grant that covers it. In the Rust expression, all walks the child grants and any searches the parent grants.

The order of those two quantifiers matters. Finding one matching pair is insufficient: every child row needs a match. Different child grants may be covered by different parent grants. The repository reads have covering grants, but those matches cannot make up for the uncovered secrets/ read.

Follow a role
The rule for tool grantsC⊑P  ⟺  ∀ c∈grants(C),∃ p∈grants(P): c⊑p\begin{aligned} &\htmlData{link=c}{\htmlClass{cor-child}{C}} \sqsubseteq \htmlData{link=p}{\htmlClass{cor-parent}{P}} \iff \\ &\htmlData{link=all}{\forall}\,\htmlData{link=g}{\htmlClass{cor-child}{c}} \in \mathrm{grants}(\htmlData{link=c}{\htmlClass{cor-child}{C}}),\\ &\htmlData{link=any}{\exists}\,\htmlData{link=q}{\htmlClass{cor-parent}{p}} \in \mathrm{grants}(\htmlData{link=p}{\htmlClass{cor-parent}{P}}):\ \htmlData{link=g}{\htmlClass{cor-child}{c}} \mathrel{\htmlData{link=rel}{\htmlClass{cor-relation}{\sqsubseteq}}} \htmlData{link=q}{\htmlClass{cor-parent}{p}} \end{aligned}

⊑ means “is covered by”.

In Rust scope.rs · tool-grant clause
    self.grants.iter().all(|child_grant| {
        other
            .grants
            .iter()
            .any(|parent| child_grant.is_subset_of(parent))
    })
Read the full scope check

The implementation applies the same pattern to tool, resource, and prompt grants.

capability/scope.rs60–77rust
pub fn is_subset_of(&self, other: &ChioScope) -> bool {
    self.grants.iter().all(|child_grant| {
        other
            .grants
            .iter()
            .any(|parent| child_grant.is_subset_of(parent))
    }) && self.resource_grants.iter().all(|child_grant| {
        other
            .resource_grants
            .iter()
            .any(|parent| child_grant.is_subset_of(parent))
    }) && self.prompt_grants.iter().all(|child_grant| {
        other
            .prompt_grants
            .iter()
            .any(|parent| child_grant.is_subset_of(parent))
    })
}

This expression establishes scope coverage. To admit a delegated token, the kernel also requires the covering parent grant to permit delegate and validates the delegation chain and validity periods. A positive result from is_subset_of is one part of that admission decision.

Share a parent’s capacity

Adding specialists means dividing shared capacity as well as assigning operations. Several workers can each pass the scope check and still request more of a shared budget than their parent holds, so the application accounts for the family of admitted children together.

The budget_share_bps field expresses a delegated share in basis points: 10,000 represents a full share. In this example the parent holds 10,000 bps, and two admitted children hold 4,000 and 3,500. A third child can request at most the remaining 2,500.

One parent, a shared limitRibbon thickness represents basis points
A parent budget split between admitted children and a proposed childThe parent holds 10,000 basis points. Child a holds 4,000 and child b holds 3,500. The proposed share fits within the remaining capacity. Ribbon thickness is proportional to basis points.Parent capacity10,000 bps2,500 still availableDelegated shares1,000 bps = 10%0k2.5k5k7.5k10ka 4,00040% · admittedb 3,50035% · admittedc 2,50025% · proposed1,000 bps (10%)solid: admitted · outline: proposal
Parent capacity10,000 bpsa4,000admittedb3,500admittedc2,500proposedSolid: admitted · outline: proposal
Proposed share for child c
4,000 + 3,500 + 2,500≤ 10,000

Within the shared limitAdmission would allocate the parent’s full 10,000 bps.

Inspect the allocation
Illustrative sibling-share accounting, before admission of c
EntryShareState
Parent10,000 bpsCapacity
Child a4,000 bpsAdmitted
Child b3,500 bpsAdmitted
Child c2,500 bpsProposed, fits
Currently allocated7,500 bps2,500 bps available
An illustrative allocation with an enforcing budget registry. Solid ribbons are admitted shares; the outlined ribbon is a proposal. Hatching marks the portion that exceeds the parent’s capacity. The controls compare proposals without allocating a live budget. Basis points express shares, not money spent or work completed.

A request for 3,500 bps is within the token field’s 10,000 bps limit on its own. Adding it to the existing children would allocate 11,000 bps, so the registry rejects that proposal and leaves the admitted siblings unchanged. A request for 1,500 bps fits and would leave 1,000 bps available.

This check requires an enforcing budget registry that knows the parent and its admitted children. The registry supplies the shared accounting; a signed budget_share_bps field by itself does not. Scope, validity, and the other admission checks still apply to each child.

Expiry and revocation end a grant

Set a token’s lifetime to fit the work it authorizes. Its validity period is fixed when it is signed, and once it expires the caller needs another token. Changing the lifetime in the grant code does not extend a token already issued.

Revocation ends authority sooner. It is state maintained by the kernel, separate from the signed token, and the kernel checks both the presented token and every link in its delegation chain against that state. A delegated child’s admission therefore still depends on its ancestors, so an application must preserve the chain and make revocation state available wherever calls are checked. Where revocation records live, and how fresh they are, depend on the deployment.

Read more in Delegation and chain validation.

Size each grant to its task

Authority can follow the work as the coordinator divides it. The reviewer’s grant covers the whole workspace, so for the tester assigned the tests directory it can issue a grant for that assignment alone: one operation, one path prefix, a few calls, and a lifetime measured in minutes. An instruction injected into pull request #421 then reaches only what the current assignment holds.

The Authority application narrows a parent read grant on src/ to src/review/ for its reviewer and src/audit/ for its auditor:

authority.rsrust
let mut scope = parent.scope.clone();
scope.grants[0].operations = vec![Operation::Invoke];
scope.grants[0]
    .constraints
    .push(Constraint::PathPrefix(prefix.into()));
scope.grants[0].max_invocations = Some(2);
let expires_at = now.saturating_add(1800).min(parent.expires_at);

The three edits after the clone each narrow one dimension of the first grant, grants[0]; any other grant in the parent’s scope passes to the child unchanged. Keeping only Operation::Invoke removes onward delegation. Constraint::PathPrefix appends the child’s directory to the parent’s src/ constraint. Every constraint on a grant must hold and a prefix matches whole path segments, so the child’s directory must sit inside the parent’s: a tests/ prefix under src/ would admit nothing. max_invocations allows two calls. The expiry is thirty minutes after issue, clamped to the parent’s expires_at, so the child cannot outlive its parent.

The prefix bounds the requests the kernel admits; the tool server and its confinement bound what the process can open. Task-Scoped Authority extends the pattern to a family of workers, where an aggregate invocation budget caps their combined calls and revoking the root grant closes the subtree.

Policy narrows the calls a grant permits

A grant for read_file lets the reviewer invoke that tool within the grant’s limits. It does not establish that a particular read serves the review, that the answer is correct, or that the task succeeded.

Capability checks sit within a larger call path. Guards can apply additional policy to a call the token permits, and they cannot enlarge the token’s scope. In the Quickstart project, the tests-are-evidence rule refuses the fixer’s write to /workspace/tests/ even though its grant covers writes to /workspace.

Sources and further reading

  1. The Confused DeputyNorm Hardy · 1988

    The original account of a program confusing authority from its invoker with authority of its own.

  2. The Protection of Information in Computer SystemsJerome H. Saltzer and Michael D. Schroeder · 1975

    Section I sets out least privilege, complete mediation, and the design principles behind the discussion above.