Chio/Docs
LOGIN · JOIN

Authority

Split one task among agents with narrower grants and one shared limit, and read how the kernel refuses what exceeds them.

Concept chio@5f63e6e

On this page

An operator can split one task among several agents and keep the whole task inside one bound. Each agent holds a grant narrower than its parent, and every grant in the family draws on one shared invocation limit, so adding agents adds no calls to the task's total.

Chio represents permission as a signed capability: an accepted issuer grants a subject a bounded set of operations, and a parent can delegate part of that authority to a child. In the running example, a parent grant reads src/, the reviewer reads the discount rules for issue #417 and the tester reads the refund rules it must cover, each in its own subdirectory, and the family may spend three calls in total. The kernel verifies each child's derivation and checks every request against policy, withdrawal state, and the remaining allowance before work proceeds.

Grant familyChoose a command.
signsnarrowsσissuerκsrc/ · limit 6κ₁reviewer src/discount/κ₂tester src/refund/refund-2src/refund/rules.pyfamily spentKdread_fileBfamily · 3 / 3narrowsσissuerκsrc/ · limit 6κ₁reviewer κ₂tester refund-2family spentKdread_fileBfamily · 3 / 3

The family refuses a call the child's grant still allows

The auditor's second read fits its own grant, which has one slot left, and the kernel refuses it with invocation budget exhausted for capability tester: the family's three calls are captured. Invocation limits.

One delegation family from the prepared authority example: the accepted issuer, a parent grant for src/, two narrower child grants, and the kernel's per-grant and family counts after each captured command. Process-tree limits, approvals, and receipts are left out.

Scope answers which requests a grant covers; accounting answers whether a covered request may proceed now. The parent's six-call limit bounds the parent grant, each child's two-call limit bounds that child, and the signed family limit of three bounds them all; a call proceeds only when every applicable limit permits it.

Both children carry the same signed family commitment beside their own narrower grants. The host's durable admission owner reserves and captures the applicable quotas together, which two independent child counters could never do. Restarting a worker or signing another child creates no fresh copy of the allowance. The 5,000-basis-point allocations constrain delegation shares, separately from invocation counts.

Four questions decide whether a call proceeds

Every call routed through the kernel answers four questions, and the Authority pages are grouped the same way.

  1. Identity. Who is calling, and which issuer may vouch for that caller's authority. The trusted host binds the caller, tenant, session, and authentication epoch (Authenticated identity); operator configuration selects accepted issuers (Trust roots); a grant that requires it carries the subject's proof over the exact call (Proof of possession).
  2. Grants. What the capability permits: tools, resources, lifetime, and limits (Least privilege), kept inside the parent on every derivation (Delegation boundaries), issued under tenant and lineage admission (Issuance governance), and counted per grant and per family (Invocation limits).
  3. Admission. Whether this operation may proceed now, given reservations, captured use, nonces, and required approvals (Admission and execution nonces, Threshold approvals).
  4. Withdrawal. When authority ends. Revocation reaches dispatch, output release, and recovery (Revocation), and an exact retry keeps its original identity (Replay protection).

A signature protects a signed grant from alteration. Three questions stay with separate checks that read different inputs: whether the host trusts the issuer, whether the presenter is the subject, and whether the authority can still be used. A signed child stays cryptographically valid after its parent is revoked, and its next admission is refused.

Each object in a grant makes one claim

A task grant replaces inherited application credentials, under which a request to review a directory reaches as far as one to administer the repository. The operator decides which subject may invoke which operations, on which resources, for how long, and under which limits, so the reviewer, the tester, and the agent that posts to a pull request each get room for their own assignment under separately governed authority. That authority can be inspected apart from the instructions given to the model.

ObjectWhat it bindsWho supplies or checks it
Issuer trustThe key accepted to issue this class of capabilityHost configuration, chosen apart from the token
Capability tokenSubject, tool and resource scope, issue time, expirySigned by the issuer, verified by the kernel
Delegation linkParent, delegatee, attenuation, lifetime, shareSigned by the parent subject; verified at attachment and admission
Family commitmentThe original root and one aggregate invocation limitRoot binding signed by the issuer; quota selected by durable admission
Authenticated contextCaller, tenant, session, isolation epochThe trusted host, outside the tool arguments
Current authority stateRevocation, reservations, captured use, approvalsThe serving authority and its retained stores

A CapabilityToken binds a subject key, a ChioScope, a lifetime (issued_at and expires_at), and a delegation chain; the scope carries tool, resource, and prompt grants, each naming its allowed operations. Every issuing authority implements the CapabilityAuthority trait, which binds the subject key, scope, and lifetime before a token reaches a caller. Temporary credentials are an established design (AWS IAM roles); Chio adds each child's signed derivation, the shared limits, and the check of current authority at use.

A child grant narrows its parent and keeps the family binding

The complete example extends the Execution download with an authority program. Initialization creates a parent and two child tokens, retains their keys and original signed bytes, and attaches all three to one process tree. Later commands reopen the same stores and grants; an expired grant stays expired and a missing child stays missing.

The reviewer narrows the parent's src/ read grant to src/discount/, removes delegation, reduces the per-grant limit from six to two, and shortens expiry. Both path constraints stay in force.

security-execution/src/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);

delegate signs the parent subject's delegation link and returns a delegation receipt, a separate object from the child capability. The accepted issuer then signs the child token with the complete chain, an attenuation proof, and the unchanged family commitment. The host attaches it with runtime.spawn("parent", id, &child), which checks the exact parent relationship, issuer, lifetime, scope, and family binding.

At invocation, the runtime restores the verified ancestors before admitting the leaf, so the parent makes no extra call to establish its child's lineage. The complete source covers issuance, persisted request construction, current-context invocation, withdrawal, and quota inspection. Issuance governance adds tenant and lineage admission before an issuing authority signs.

Parent scope, delegation, issuance, invocation, and withdrawal
security-execution/src/authority.rsrust
fn parent_scope() -> ChioScope {
    ChioScope {
        grants: vec![ToolGrant {
            server_id: "workspace".into(),
            tool_name: "read_file".into(),
            operations: vec![Operation::Invoke, Operation::Delegate],
            constraints: vec![Constraint::PathPrefix("src/".into())],
            max_invocations: Some(6),
            max_cost_per_invocation: None,
            max_total_cost: None,
            dpop_required: None,
        }],
        ..Default::default()
    }
}
security-execution/src/authority.rsrust
let receipt = delegate(
    parent,
    &scope,
    parent_subject,
    &subject.public_key(),
    ScopeAttenuation {
        steps: vec![
            Attenuation::RemoveOperation {
                server_id: "workspace".into(),
                tool_name: "read_file".into(),
                operation: Operation::Delegate,
            },
            Attenuation::AddConstraint {
                server_id: "workspace".into(),
                tool_name: "read_file".into(),
                constraint: Constraint::PathPrefix(prefix.into()),
            },
            Attenuation::ReduceBudget {
                server_id: "workspace".into(),
                tool_name: "read_file".into(),
                max_invocations: 2,
            },
            Attenuation::ShortenExpiry {
                new_expires_at: expires_at,
            },
        ],
        child_expires_at: Some(expires_at),
        budget_share_bps: Some(5000),
    },
    now,
    nonce,
)?;
security-execution/src/authority.rsrust
Ok(CapabilityToken::sign_attenuated(
    CapabilityTokenAttenuationBody {
        body: CapabilityTokenBody {
            id: id.into(),
            issuer: issuer.public_key(),
            subject: subject.public_key(),
            issued_at: now,
            expires_at,
            scope: scope.clone(),
            delegation_chain: receipt.complete_chain(),
            aggregate_invocation_budget: parent.aggregate_invocation_budget.clone(),
        },
        caveats: vec![],
        scope_attenuations: vec![],
        budget_share_bps: Some(5000),
        attenuation_proof: AttenuationProof {
            parent_scope_hash: scope_hash(&parent.scope)?,
            child_scope_hash: scope_hash(&scope)?,
            normalized_subset_proof: compute_attenuation_witness(&parent.scope, &scope)?,
        },
    },
    issuer,
)?)
security-execution/src/authority.rsrust
let req = request(&runtime, &state, &args[3], &args[4], &args[5])?;
let response = runtime.invoke_known_only(&args[3], &args[4], &req).await?;
security-execution/src/authority.rsrust
let selected = runtime.process(&args[3])?.capability;
kernel.revoke_capability(&selected.id)?;
ensure!(
    kernel.is_capability_revoked(&selected.id)?,
    "revocation readback missing"
);

The prepared example spends three family calls and refuses the fourth

Complete the example setup, then work in security-execution/. The authority executable is the example's trusted operator program, and its process selector is an administrative input, so the command and its private state stay with the host. Agents reach the same runtime through the authenticated worker protocol of the Execution host, where a private worker credential selects the process.

Terminalbash
authority init --repository repository
authority read src/discount/rules.py \
  --as reviewer --operation review-1
Output
Allowed: src/discount/rules.py
Caller: reviewer
Operation: review-1

SKU_TIERS = {"sku-118": 10, "sku-204": 15, "sku-301": 80}

Initialization retains the repository, issuer, and child grants in .authority/, independent of any Execution session. The summary names the decision, caller, and operation, and --json adds the guarded value and retained receipt pathname; a successful command can report either decision, so read decision.verdict, while an invocation or storage error exits unsuccessfully. The host persists the request before invoking and retains the receipt before presenting the result.

Next, try the tester's path as the reviewer, perform the reviewer's second read and the tester's first, then ask for the tester's second. Each call keeps its own operation key.

Terminalbash
authority read src/refund/rules.py \
  --as reviewer --operation wrong-tree
authority read src/discount/rules.py \
  --as reviewer --operation review-2
authority read src/refund/rules.py \
  --as tester --operation refund-1
authority read src/refund/rules.py \
  --as tester --operation refund-2
authority inspect
Output
Family calls: 3 / 3
Reserved calls: 0
Parent revoked: false
parent: 0 / 6 calls
reviewer: 2 / 2 calls
tester: 1 / 2 calls

You should see the family at three of three. Scope matching refuses the wrong-directory request before it reaches the file connector, and the next two reads succeed. The last read is refused with invocation budget exhausted for capability tester while the tester has used one of its two calls; that reason leaves open which quota ran out, and the counters settle it: reviewer 2 / 2, auditor 1 / 2, family 3 / 3.

The example reads the family through get_invocation_quota_usage, with the owner ID derived from the verified signed root binding; a sum of child counters is never treated as the family budget. Its resource journal independently holds three file reads, and those three admitted reads stay captured across each command's host restart, because reopening SQLite leaves the family spent. The process tree's separate 32-call ceiling for new logical keys counts denied and uncertain calls and is deliberately higher here; an exact replay keeps its original identity under the rules in Replay protection.

Inspect the signed token, the family owner, and the receipts

The reviewer token's serialized fields; the original signature stays in the complete file:

Terminalbash
jq '{id, issuer, subject, issued_at, expires_at,
     scope, delegation_chain, budget_share_bps,
     aggregate_invocation_budget}' .authority/reviewer-capability.json
  • issuer and subject: the accepted issuing key and the distinct reviewer key. The host's retained issuer configuration supplies the trust pin.
  • scope.grants[0]: workspace/read_file, invoke only, both path prefixes, and a two-call ceiling.
  • delegation_chain[0]: the signed link from the parent's subject to this reviewer, including the parent capability identity and scope commitment.
  • expires_at: the child's deadline, no later than the parent's. Starting another OS process leaves it unchanged.
  • aggregate_invocation_budget: delegation_family, limit three, and the original signed root binding shared by both children.

A native capability has no standalone audience field: its accepted issuer, scoped server and tool, and authenticated host context select the boundary. Protocols such as the credential broker bind an explicit service audience of their own.

security-execution/src/authority.rsrust
let verified =
    verify_aggregate_invocation_budget(&parent, &[issuer.public_key()], None)?
        .context("family root")?;
let family_key = BudgetQuotaKey {
    profile: BudgetQuotaProfile::AggregateFamilyInvocation,
    owner_id: verified.owner_id.clone(),
    grant_index: None,
};
let budget = authority.budget_store();
let family_usage = budget.get_invocation_quota_usage(&family_key)?;
let family = json!({"owner_id":verified.owner_id,"limit":verified.max_invocations,
    "captured":family_usage.as_ref().map_or(0,|u|u.captured_invocations),
    "reserved":family_usage.as_ref().map_or(0,|u|u.reserved_invocations)});
Terminalbash
wc -l .authority/resource-reads.jsonl
chio --receipt-db .authority/receipts.db receipt list \
  --admin-all --tool-server workspace --limit 10

The --admin-all selection fits here because the trusted operator owns this disposable host and its receipt store. A shared deployment selects the intended tenant scope.

Revoking the parent stops its children while their signatures still verify

Withdrawal runs on fresh state, so budget exhaustion cannot explain the refusal. Read once, revoke the parent, and ask the same reviewer for new work; authority revoke performs the durable mutation and checks readback before it reports withdrawal.

Terminalbash
authority init --repository repository --state .withdrawal
authority read src/discount/rules.py --state .withdrawal \
  --as reviewer --operation before
authority revoke parent --state .withdrawal
authority read src/discount/rules.py --state .withdrawal \
  --as reviewer --operation after
authority inspect --state .withdrawal

In the captured run, all three capability signatures still verify, the family has used one of three invocations, and the reviewer one of two. The second read is refused because the parent capability is revoked, and the resource journal stays at one read. Revocation changed current authority; the child's signed bytes are as they were.

Withdrawal ends new use. The first read stands, and the outcome of an effect already dispatched is a recovery question, so keep original operations and receipts. Revocation follows the check through dispatch, output release, and recovery; the durable emergency stop and subtree cancellation are separate controls with their own lifetimes.

Each refusal names the question that stopped the call

A tool call receives its refusal as a deny reason in the signed response. The reason identifies the question that failed and the record to inspect next.

QuestionWhat the tool call receivesInspect next
Identity
capability verification failed: capability issuer is not a trusted CA
Configured trusted issuers and the original token
Grants: scope
requested tool <tool> on server <server> is not in capability scope
Server, tool, operation, and every matched constraint
Grants: limit
invocation budget exhausted for capability <id>
Grant and aggregate quota records for the family
Admission
PendingApproval, held for a decision
The bound proposal and the authorized approver set
Withdrawal
capability has been revoked: <capability id>
Retained lineage and the current revocation owner

Resource, prompt, and completion requests run the same checks and surface the reason directly as a typed KernelError, without the capability verification failed: prefix. Widening caught at delegation admission carries delegation admission failed: instead. Keep the original decision and the interface that returned it; a generic permission-denied message loses the distinction an operator needs.

Read scope and accounting separately. A worker can hold the right directory and no remaining family allowance, or allowance and the wrong path, and fixing one leaves the other as it was. Trusting the key a token supplies, rewording the worker's task, restarting the worker, creating a sibling, re-signing the request, rotating a bearer connection, or reusing an approval for different request bytes repairs none of these refusals.

A smaller comparison: route scope against guard policy

The governed notes tutorial answers one narrower question. Its policy-read-only.yaml exposes only the read route, while its original policy exposes read and write and denies the write through a guard. Both leave the note unchanged, with different reasons and evidence. Run these commands from that tutorial's prepared directory:

Terminalbash
python3 run.py --policy policy-read-only.yaml
jq '.tools.result.tools | map(.name)' .chio/responses.json
jq '.deny.result' .chio/responses.json

The tutorial demonstrates route scope and guard policy; the signed delegation family and shared counter belong to the example above.

Next: design the grant for each task