Capabilities
Define what an agent may do, what it may delegate, and how the kernel enforces those grants.
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.
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.
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());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 minutesfrom chio import Computer, USD
project = Computer.open("quickstart")
fixer = project.agent("fixer")
fork = project.fork()
authority = fork.grant(
to=fixer,
resources={"/workspace": ["read", "write"], "/models/default": ["invoke"]},
budget=USD(2),
expires_in="20m",
)
print(authority.describe())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 minutesuse chio::{Computer, Grant, USD};
let project = Computer::open("quickstart").await?;
let fixer = project.agent("fixer").await?;
let fork = project.fork().await?;
let authority = fork.grant(
Grant::to(&fixer)
.resource("/workspace", ["read", "write"])
.resource("/models/default", ["invoke"])
.budget(USD(2))
.expires_in("20m"),
)?;
println!("{}", authority.describe());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 minutesThe 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.
- A trusted
issuerand a validsignatureestablish who authorized the grant. - The signed
issued_atandexpires_atfields 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 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.
const [refusal] = await project.history.receipts({ outcome: "deny" });
console.log(refusal.describe());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
refusal = project.history.receipts(outcome="deny")[0]
print(refusal.describe())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
let refusal = project.history().receipts().outcome("deny").first().await?;
println!("{}", refusal.describe());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.
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.
⊑ means “is covered by”.
Read the full scope check
The implementation applies the same pattern to tool, resource, and prompt grants.
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.
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:
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
- The Confused DeputyNorm Hardy · 1988
The original account of a program confusing authority from its invoker with authority of its own.
- 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.