Chio/Docs
LOGIN · JOIN

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.

Concept chio@5f63e6e

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.

EvaluationVerdictReturned reason
Valid contextClock 1710000400, issuer trustedpending_approvalcapability-only browser evaluation requires a mediated prevent receipt before execution
Same context againSame request, token, issuer, and clockpending_approvalcapability-only browser evaluation requires a mediated prevent receipt before execution
After expiryClock 1710000801denycapability has expired
Issuer removedTrusted issuer list emptieddenycapability issuer is not a trusted CA
Different toolTool changed to file_writedenyrequested 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.

Change context, retain the requestjavascript
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.

InputThe question it answers
Request and capabilityWho is asking to use which tool, with what arguments, under which signed grant?
Trusted issuers and crypto floorWhich issuing keys and signature algorithms does this verifier accept?
ClockIs the capability within its signed time window when verification runs?
Ordered guards and session rootsWhich synchronous checks apply, and which filesystem roots belong to the session?
Negotiation and trust-root evidenceAre the token’s feature family and its delegation or attenuation bindings accepted?
Sibling-budget registryCan 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
crates/kernel/chio-kernel-core/src/evaluate.rs57-71rust
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.

The evaluation steps in order, the state each reads, and the sibling-share registry it writes lastChio’s order: admission last1verifyissuersclockroots1subjectagent_id0scopegrantsguardsguard configshareregistrythe only writev = 0Denyparent allocation · 10,000 bpssibling-share registryc1 4,000c2 3,500c3 1,500
v=verify∧subject∧scope∧⋀jgj∧admitv = \text{verify} \wedge \text{subject} \wedge \text{scope} \wedge {\textstyle\bigwedge_j} g_j \wedge \htmlClass{role-seal}{\text{admit}}
v=1∧1∧0∧⋯=0\phantom{v}= 1 \wedge 1 \wedge \htmlClass{role-deny}{0} \wedge \cdots = \htmlClass{role-deny}{0}
4,000+3,500=7,500  registry unchanged4{,}000 + 3{,}500 = 7{,}500\ \ \text{registry unchanged}
click a step to make it refuse; click c₃ to change its share
The evaluation steps in order, the state each reads, and the sibling-share registry it writes lastChio’s order: admission last1verifyreads issuers, clock, roots1subjectreads agent_id0scopereads grantsguardsreads guard configsharereads and writes the registryv = 0 Denyparent allocation · 10,000 bpssibling-share registryc1c2c3c₁ 4,000 · c₂ 3,500 · c₃ 1,500tap a step to make it refuse; tap c₃ to change its share

Denied at scope. The registry is unchanged at 7,500 of 10,000 basis points.

v=verify∧subject∧scope∧⋀jgj∧admit=1∧1∧0∧⋯=04,000+3,500=7,500  registry unchanged\begin{aligned} &v = \text{verify} \wedge \text{subject} \wedge \text{scope} \wedge {\textstyle\bigwedge_j} g_j \wedge \htmlClass{role-seal}{\text{admit}} \\ &= 1 \wedge 1 \wedge \htmlClass{role-deny}{0} \wedge \cdots = \htmlClass{role-deny}{0} \\[4pt] &4{,}000 + 3{,}500 = 7{,}500\ \ \text{registry unchanged} \end{aligned}
In Rust · 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));
}
One delegated child with illustrative shares in basis points of its parent’s allocation, the family from Capabilities. The registry tracks allocation; charging a call belongs to the hosted budget store. The admission-first order is drawn only for contrast.

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 dependencyWhy it matters
Revocation store and configured viewsChecks whether the capability or its ancestors have been revoked, using the deployment’s available state.
Session journalRecords the session’s earlier calls, so guards on tool order and cumulative bytes decide from that history.
Retained information labelsCarry the restrictions of what the principal, its lineage, and the session have read, checked against the destination’s clearance.
Replay and approval stateEnforces nonce use and binds approval evidence to the relevant request and policy.
Budget store and payment adapterReserves spending authority and performs the configured authorization, capture, release, or reconciliation.
Tool connection and output policyExecutes allowed work and evaluates the returned result, subject to the integration’s visibility.
Signing keys and receipt storeBinds 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.

Control and data planesChoose an interface.
Trust control plane · chio trust serveData plane · each node links chio-kernelverifies with cached keys, signs receiptsTrust control planeData plane · a kernel per 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-url and a bearer token for chio 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-url is 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(())
}
One operator’s deployment with every node pointed at the trust control plane by --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.Architecture

Each 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
crates/kernel/chio-kernel/src/runtime.rs30-38rust
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

  1. Match the invocation. Compare the server, tool, caller, parameters, and signed capability. The same intent in prose is not the same request.
  2. Identify the evaluator. Record the deployed code, policy configuration, selected entry point, and any unsupported constraint.
  3. Check authority and time. Compare trusted issuers, token validity, the applicable revocation reads, and the actual clock source.
  4. 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.
  5. 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

CarryBecause
The evaluation entry pointPortable 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 stateTrusted 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 viewA cached view can establish a decision against that view. It cannot establish that no later revocation or policy change exists.
The scope of any proofThe 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.

Follow the state you need