The State a Verdict Depends On
Identical arguments can meet different authority. A verdict depends on the token, the clock, trusted keys, policy, and live state.
On this page
An operator can change an agent’s authority without changing its plan. A capability may expire, a parent grant may be revoked, or another call may consume the remaining budget. The session’s history also counts: an earlier read can leave restrictions that a later destination is not cleared to receive. The same request can therefore receive a different decision later. To explain a verdict, you need the request and the state the kernel read when it decided.
Chio separates portable decision logic from the services that maintain operational state. The Rust core checks supplied authority and context without owning a database or a network connection. The hosted runtime adds live revocation, spending admission, approval orchestration, tool execution, and evidence storage. One decision core can serve several platforms while each deployment’s state dependencies stay visible.
Change one input and the verdict follows
This signed test grant authorizes srv-files/file_read under /workspace/. Its validity window is 1710000200 to 1710000800 Unix seconds. The real browser adapter evaluated it with a clock fixed inside that window, then again with only the time or the issuer trust changed. A final case changes the requested tool.
| Evaluation | Verdict | Returned reason |
|---|---|---|
| Valid contextClock 1710000400, issuer trusted | pending_approval | capability-only browser evaluation requires a mediated prevent receipt before execution |
| Same context againSame request, token, issuer, and clock | pending_approval | capability-only browser evaluation requires a mediated prevent receipt before execution |
| After expiryClock 1710000801 | deny | capability has expired |
| Issuer removedTrusted issuer list emptied | deny | capability issuer is not a trusted CA |
| Different toolTool changed to file_write | deny | requested tool file_write on server srv-files is not in capability scope |
The two unchanged evaluations return the same result. Advancing only the clock to 1710000801 returns capability has expired. Removing the trusted issuer returns capability issuer is not a trusted CA. These are returned reasons from executed cases, not predictions.
A successful capability-only check comes back as pending_approval with authorized: false. The browser adapter does not run guards or tools, so its capability verdict of allow is diagnostic; the hosted runtime can impose further requirements before anything executes.
const expired = evaluate(JSON.stringify({
...envelope,
clock_override_unix_secs: envelope.capability.expires_at + 1,
}));
const untrusted = evaluate(JSON.stringify({
...envelope,
trusted_issuers_hex: [],
}));The complete runnable exercise covers module initialization and the signed fixture.
Portable evaluation reads only what the caller supplies
Portable evaluation combines caller-supplied request and decision context, then admits a delegated share through an explicit, mutable registry. Its Allow or Deny result does not dispatch a tool or persist a receipt.
| Input | The question it answers |
|---|---|
| Request and capability | Who is asking to use which tool, with what arguments, under which signed grant? |
| Trusted issuers and crypto floor | Which issuing keys and signature algorithms does this verifier accept? |
| Clock | Is the capability within its signed time window when verification runs? |
| Ordered guards and session roots | Which synchronous checks apply, and which filesystem roots belong to the session? |
| Negotiation and trust-root evidence | Are the token’s feature family and its delegation or attenuation bindings accepted? |
| Sibling-budget registry | Can this delegated child be admitted without exceeding the parent’s allocated share? |
The request and context fields are grouped in EvaluateInput. Richer entry points also take a crypto floor, a negotiated feature profile, a trust-root resolver, optional direct-root evidence, and a mutable BudgetRegistry. A clock is injected behavior, not automatically a frozen timestamp. The registry is explicit state the caller owns, not an immutable argument.
Inspect the portable input type
pub struct EvaluateInput<'a> {
/// Tool call request being evaluated.
pub request: &'a PortableToolCallRequest,
/// The capability token authorising this call.
pub capability: &'a CapabilityToken,
/// Trusted issuer public keys (typically CA + kernel + authority).
pub trusted_issuers: &'a [PublicKey],
/// Clock used for time-bound enforcement.
pub clock: &'a dyn Clock,
/// Guard pipeline. Evaluated in order, fail-closed on deny or error.
pub guards: &'a [&'a dyn Guard],
/// Optional filesystem roots from the owning session, passed through to
/// guards that enforce root-based resource protection.
pub session_filesystem_roots: Option<&'a [String]>,
}The crate also exposes an entropy interface for operations that need randomness. Randomness is not a field of this evaluation input.
Portable evaluation writes only after its checks pass
The full portable entry point first verifies the capability’s issuer, signature, time bounds, negotiated features, and required root evidence. It then binds the request’s agent to the verified subject, selects a matching scope grant, and runs the supplied guards in order. An unsupported constraint or a guard error denies. A synchronous guard asking for human approval also denies here; this function cannot run the approval workflow.
Denied at scope. The registry is unchanged at 7,500 of 10,000 basis points.
evaluate.rs:352// Step 1: capability verification with all defenses except
// persistent sibling-sum admission. Admission mutates the supplied
// registry, so defer it until subject, scope, and guard checks have
// passed. Otherwise a validly signed token for the wrong request can
// consume sibling share and starve later valid siblings.
// … steps 1 to 4: verify, subject, scope, guards
if let Err(error) = admit_delegated_budget(input.capability, budgets) {
let core_err = KernelCoreError::InvalidCapability(error);
return deny(core_err, Some(matched_grant_index), Some(verified));
}
Only after those checks pass does evaluation call the sibling-budget registry, and that call is the only step that changes state. The order prevents a validly signed token presented for the wrong agent or tool from consuming another child’s allocation. An unknown parent or an oversubscribed sibling can still deny at this final step, and the admitted siblings keep their shares.
Allocation is different from charging a call. BudgetRegistry tracks delegated sibling shares; the hosted BudgetStore manages monetary holds and accounting. Passing the portable checks does not establish that a payment was authorized or a tool was executed.
For an outside buyer’s pricing/quote request, the capability must cover that server and tool, and the caller must match its subject. An otherwise identical request made after expiry can fail at time verification; changing the requested tool can fail at scope matching. Capabilities and Guards connect these checks to the captured example.
The runtime reads live state at each stage
| Runtime dependency | Why it matters |
|---|---|
| Revocation store and configured views | Checks whether the capability or its ancestors have been revoked, using the deployment’s available state. |
| Session journal | Records the session’s earlier calls, so guards on tool order and cumulative bytes decide from that history. |
| Retained information labels | Carry the restrictions of what the principal, its lineage, and the session have read, checked against the destination’s clearance. |
| Replay and approval state | Enforces nonce use and binds approval evidence to the relevant request and policy. |
| Budget store and payment adapter | Reserves spending authority and performs the configured authorization, capture, release, or reconciliation. |
| Tool connection and output policy | Executes allowed work and evaluates the returned result, subject to the integration’s visibility. |
| Signing keys and receipt store | Binds the runtime decision to evidence and persists the resulting record. |
These services participate at their own stages. The hosted call is not one immutable snapshot read by a pure function: revocation validation, for example, queries the token and its delegation ancestors separately. The consistency, concurrency, and recovery contracts of each store matter.
Two of these sources record the session’s history. The kernel appends each governed call to the session journal, and the guards behind session rules read it: a publish call that requires the test tool as its predecessor is refused until the session has called the test tool. Chio also joins the label of each classified read into retained state for the principal, its lineage, and the session.
A request to post a summary to a public destination is allowed before the agent reads an internal file and refused after it, with identical arguments both times. Information flow across calls follows that case. The journal is held in the serving process’s memory. Retained labels are written to durable state before a read’s result is released, so they reach delegated work and a restarted worker as persistent taint.
A system can expose resources through several kernel nodes while sharing the authority state its work depends on. Each serving node, whether the agent runner, the MCP edge, or the HTTP sidecar, links its own kernel and admits its own calls. Started with --control-url, its authority, revocation, budget, and receipt adapters call the trust-control service instead of local SQLite files. Every interface starts at a node: nodes call the service, and the service never calls a node.
Every interface starts at a node
Each serving node links its own kernel and admits its own calls in process, so no tool call passes through the service. Started with --control-url and --control-token, a node’s authority, revocation, budget, and receipt adapters call the trust control plane; without them it keeps the same state in local SQLite files. Naming both a local file and the URL for the same state is rejected. Nodes call the service, and the service never calls a node. Architecture.
- agents and callers
- Reach a node over MCP, HTTP, or a subprocess, never the control plane.
- tool servers
- MCP servers, upstream APIs, and agent tools receive only calls a node has admitted.
- node configuration
- Policy files, OpenAPI specs, and guard modules load on each node. The control plane and peer replication leave them where they are, so roll them out separately.
- chio CLI
- The operator reaches the service with
--control-urland a bearer token forchio trust revoke, receipt queries, and evidence export. - peer node
- Another trust-control node (
--peer-url) pulls revocations, budget history, receipts, and lineage every--cluster-sync-interval-ms, 500 by default, and forwards covered writes to its leader. A kernel using--control-urlis a client and receives no deltas.
In Rust · lib.rs:50730 lines
pub fn configure_revocation_store(
kernel: &mut ChioKernel,
revocation_db_path: Option<&Path>,
control_url: Option<&str>,
control_token: Option<&str>,
) -> Result<(), CliError> {
match (revocation_db_path, control_url) {
(Some(_), Some(_)) => {
return Err(CliError::cli_other_error(
"use either --revocation-db or --control-url for revocation state, not both"
.to_string(),
));
}
(Some(path), None) => {
kernel.set_revocation_store(Box::new(chio_store_sqlite::SqliteRevocationStore::open(
path,
)?));
}
(None, Some(url)) => {
let token = require_control_token(control_token)?;
kernel.set_revocation_store(
trust_control::service_runtime::remote_stores::build_remote_revocation_store(
url, token,
)?,
);
}
(None, None) => {}
}
Ok(())
}
--control-url. Durable admission and child receipts have remote adapters too and are left out; a node without the URL keeps the same state in local SQLite files.ArchitectureEach interface has its own freshness contract. Before admission and again before finalizing an allow, the kernel checks revocation for the token and each delegation link. Each id gets an uncached lookup; a failed lookup denies the call. Trusted issuer keys are cached for two seconds, and a failed refresh retains the cached keys. Budget holds occur before dispatch and fail if the service cannot answer. A receipt append that exceeds its deadline, 5,000 ms by default, fails closed with no allow response.
Each node loads its own policy; these interfaces do not distribute it. To explain a verdict from a shared deployment, keep the policy each node ran beside the decisions it produced.
Compare portable and hosted outcomes
pub enum Verdict {
/// The action is allowed.
Allow,
/// The action is denied.
Deny,
/// The action is suspended pending a human decision. Look up the
/// associated `ApprovalRequest` via the HITL API.
PendingApproval,
}The shared vocabulary includes PendingApproval. Portable evaluation returns only Allow or Deny; the hosted runtime owns the stateful approval path.
Investigate a changed verdict
- Match the invocation. Compare the server, tool, caller, parameters, and signed capability. The same intent in prose is not the same request.
- Identify the evaluator. Record the deployed code, policy configuration, selected entry point, and any unsupported constraint.
- Check authority and time. Compare trusted issuers, token validity, the applicable revocation reads, and the actual clock source.
- Follow state changes at admission. Inspect the relevant delegated share, monetary hold, approval, or nonce record. A single receipt does not contain all of them.
- Read the session’s history. Compare the calls the session made before this one and the labels retained for its principal, lineage, and session. Neither appears in the request.
Preserve the available inputs, and store observations with their provenance. Some state is read, some is consumed or reserved, and different stores may be consulted at different times. Chio does not turn those observations into one universal historical snapshot automatically.
What must accompany a decision claim
| Carry | Because |
|---|---|
| The evaluation entry point | Portable verification does not itself consult a live revocation store, dispatch a tool, or persist a receipt. The current full portable evaluator also rejects capabilities that require aggregate-invocation or cumulative-approval enforcement it cannot provide. |
| The provenance of its state | Trusted keys, clock behavior, guard configuration, root evidence, registry contents, live store reads, the session journal, and retained labels affect the result. Reusing the same request bytes does not restore them. |
| The freshness of each view | A cached view can establish a decision against that view. It cannot establish that no later revocation or policy change exists. |
| The scope of any proof | The formal program covers specified functions and models. It does not turn every implementation of an injected trait into verified storage, or a receipt into a complete replay image. |