PlatformDecision Contract
Kernel
Core & Shell
The core crate names what pure evaluation excludes. That list is the Kernel-to-Node seam, and this page reads it out.
The other half of this split
Overview
Chio evaluation ships as two crates. chio-kernel-core is the portable decision core: #![no_std] with extern crate alloc, #![deny(unsafe_code)], and no dependency that wants a filesystem, a socket, or an async runtime. chio-kernel is the hosted shell that wires that core to platform I/O.
The boundary is written down in the code. The module doc comment on crates/kernel/chio-kernel-core/src/evaluate.rs carries a heading, What it does NOT do, followed by eight bullets. That list is the Kernel-to-Node seam. Every item on it is Node content or climbs higher on the ladder; everything the module does perform is Kernel.
Portability is scripted rather than asserted. scripts/check-portable-kernel.sh builds the crate twice with --no-default-features, for the host and for wasm32-unknown-unknown. Four crates route a decision through the core's evaluate_with_full_floor_and_root entry point: the hosted shell, the browser kernel, the mobile FFI, and the C++ FFI. chio-ag-ui-proxy depends on the core but does not call evaluate; it composes verify_capability_full_with_root and resolve_capability_grants itself. Features differ too: the browser and AG-UI crates take default-features = false, the mobile and C++ crates take the default std build. Portable Kernel Core is the other half of this split: the four build shapes, the two platform seams they implement, and what that build gate does and does not prove.
What is fenced out
//! What it does NOT do (fenced into `chio-kernel` proper):
//!
//! - Revocation membership lookup (stateful `RevocationStore`).
//! - Budget mutation (stateful `BudgetStore`).
//! - Delegation-chain ancestor inspection against the receipt store.
//! - DPoP proof verification with nonce replay (LRU-backed).
//! - Governed-transaction policy evaluation (pulls in chio-governance).
//! - Payment authorisation (async adapter trait).
//! - Tool dispatch to wrapped servers (async transport).
//! - Receipt persistence / Merkle checkpointing (SQL / IO).Each item fails the Kernel filing test for the same reason: it needs state that outlives one mediated call, or an effect that leaves the process. Where the implementation then lands is a per-subject question, and the answers are not all Node.
| Fenced-out step | Where it files, and what makes it impure |
|---|---|
| Revocation membership lookup | Node. A RevocationStore the process owns and keeps current. |
| Budget mutation | Economy for the authorize, capture, release, and reconcile accounting; Node for the BudgetStore this process opens. |
| Delegation-chain ancestor inspection | Node. Joins every link against capability snapshots in the receipt store. |
| DPoP proof verification with nonce replay | Node. An LRU nonce cache that remembers what it already accepted. |
| Governed-transaction policy evaluation | Swarm. Pulls the chio-governance charter and lease chain, and its approval tokens are signed outside the deciding process. |
| Payment authorization | Economy. An async adapter trait to a rail outside the process. |
| Tool dispatch to wrapped servers | Node. Async transport to another process. |
| Receipt persistence and Merkle checkpointing | Node, for the SQL and I/O. The receipt body, its canonical JSON, and its signature stay Kernel: see Receipts & Audit. |
What the core keeps
evaluate takes one struct and returns one verdict. There is no builder, no session, and no handle to anything that persists.
pub struct EvaluateInput<'a> {
pub request: &'a PortableToolCallRequest,
pub capability: &'a CapabilityToken,
pub trusted_issuers: &'a [PublicKey],
pub clock: &'a dyn Clock,
pub guards: &'a [&'a dyn Guard],
pub session_filesystem_roots: Option<&'a [String]>,
}| Step | Check | Deny carries |
|---|---|---|
| 1 | Issuer trust, signature, crypto floor, and time bounds. | InvalidCapability(CapabilityError) |
| 2 | Subject binding: request.agent_id against the verified subject hex. | SubjectMismatch |
| 3 | Portable scope match, most specific grant first. | OutOfScope or ConstraintError |
| 4 | The guard pipeline, in registration order, fail-closed on deny or error. | GuardDenied or GuardError, both naming the guard |
| 5 | Sibling-sum admission for a delegated token, against an injected registry. | InvalidCapability(BudgetSplitRejected) |
Step 5 runs last on purpose: a validly signed token for the wrong request must not consume its parent's share and starve later valid siblings. Signature first, admit last. budget_admission_waits_until_subject_scope_and_guards_allow pins it, evaluating a delegated capability against the wrong agent and asserting the parent split still shows zero committed child share.
Step 3 is narrower than the token it reads. resolve_matching_grants walks scope.grants only; resource grants and prompt grants are not matched on this path. Of the 24 Constraint variants it can meet, it decides 8 from request arguments alone and fails closed on the other 16, including RegexMatch, GovernedIntentRequired, RequireApprovalAbove, MaxTransactionAmountUsd, and ModelConstraint. Each yields ConstraintError reading portable kernel cannot safely evaluate plus the constraint name. A grant carrying one is denied, never admitted with the constraint skipped.
Two defaults on the plain entry point
evaluate pins CapabilityCryptoFloor::AllowClassical, so a kernel that loads policy.crypto_floor must call evaluate_with_crypto_floor or the post-quantum floor is never applied to capability tokens. It also passes a NoopBudgetRegistry, whose verify_child_admission returns Ok(()) unconditionally, which makes step 5 inert. Sibling-sum admission does work only when the caller injects a real registry.Sibling-sum admission is not the budget ledger
Step 5 mutates something, which looks like it contradicts budget mutation on the exclusion list. Two different objects share the word. The fenced-out one is the BudgetStore: durable spend accounting on the authorize, capture, release, and reconcile lifecycle. The one the core keeps is BudgetRegistry, a pure basis-points check that a parent's admitted children do not jointly claim more authority than the parent holds. It owns no clock and no I/O, and it is injected by the caller. The core admits through verify_child_admission without taking a holder lease, because no pure caller has a cleanup path that would release one; the hosted shell's admit_capability_budget takes the releasable lease and owns the matching release. Either way an unregistered parent fails closed with BudgetSplitError::UnknownParent, so callers must register_parent from verified lineage first rather than have a missing share defaulted to MAX_BUDGET_SHARE_BPS.
What it refuses to do
The interesting part of a fenced core is the input it will not pretend to handle. Four refusals are worth knowing before you call it.
- Attenuated tokens on the default entry point.
evaluatedenies any capability carrying anattenuation_proof, with the reason capability rejected by chain binding: chain-binding requires a trust-root resolver on the evaluate path. Production kernels callevaluate_with_full_floor, which threads aTrustRootResolverand the peer-negotiated feature profile. - Capability features whose enforcement is stateful. On the full-floor path, a token carrying an
aggregate_invocation_budgetor a scope with a cumulative-approval constraint is denied withUnsupportedCapabilityFeaturerather than admitted unenforced. The reason names the feature: capability feature unsupported on this runtime: aggregate invocation enforcement. - A third verdict from a sync guard.
Verdict::PendingApprovalexists in the core's enum because it mirrors the shell's, but the core never emits it and treats a guard that returns it as a deny. The Three Verdicts reads the enum out in full: who may construct each value, what every runner does with the third one, and why the signed receipt has no pending state. See Approval & HITL for the shell path that does produce it. - A guard result it cannot reconcile. Continuation requires both the pure projection
guard_step_admitsand the observed result to say allow. A mismatch is fail-closed. See Fail-Closed Semantics.
Attribution survives every refusal. guard_projection_preserves_deny_and_pending_error_attribution pins that a denying guard and a PendingApproval guard both yield a reason naming the guard; guard_projection_preserves_fail_closed_error_reason pins that a guard error carries its underlying reason through the fail-closed wrapper.
The mirror list: what the shell keeps
The mirror list sits in the same file as the exclusion list. A What stays in chio-kernel heading in chio-kernel-core/src/lib.rs names it: tokio tasks, rusqlite receipt, revocation, and budget stores, the price-oracle HTTP client, the LRU DPoP nonce cache, async session operations, HTTP and stdio transport, nested-flow bridges, and tool-server dispatch. The ChioKernel struct is that list as fields: an Arc<dyn BudgetStore>, an Arc<dyn RevocationStore>, the tool-server and session maps, a Mutex<InMemoryBudgetRegistry> for sibling sums, the emergency kill switch, and a lock-poison flag that denies fail-closed after a panic unwinds inside the trusted computing base. Six pieces are Option fields rather than requirements: the receipt store and its retention worker, the payment adapter, the price oracle, the DPoP nonce store, and the execution-nonce store. A kernel can be constructed with none of them.
The request type differs on the same seam. The core reads a PortableToolCallRequest with five plain fields and takes the capability separately in EvaluateInput. The shell's ToolCallRequest carries the signed CapabilityToken inline and adds the DPoP proof, the execution nonce, the governed intent, the approval token and threshold approval set, and model metadata, among others. The Guard Trait covers which of the two to implement and why almost every guard in the catalog picks the shell trait.
Crossing the seam
One shell method delegates straight into the core. ChioKernel::evaluate_portable_verdict resolves trusted issuers, the negotiated peer profile, and the trust-root snapshot, then hands the tuple to evaluate_with_full_floor_and_root. It is a synchronous method with no tokio runtime behind it, and every failure to resolve its own inputs denies rather than falling back: a negotiation error, a root-resolution error, and a poisoned budget-registry lock each produce a Verdict::Deny with a reason naming the failure.
It is not database-free in every configuration. When the peer profile negotiates aggregate_invocation_budget or cumulative_approval_budget, negotiated_capability_root reads the root link's snapshot out of the receipt store and denies when the row is missing or carries no signed_capability evidence. Snapshots written before signed-token retention have none, so enabling either feature against an old store denies until the roots are backfilled.
The hosted hot path does not call evaluate()
ChioKernel::evaluate_tool_call_* shares the core's capability verifier, then re-implements subject binding and scope matching in chio-kernel/src/request_matching.rs, where the matcher additionally reads model metadata the portable projection does not carry. These are two separate implementations, and no test asserts they produce the same verdict on the same input. When you need the pure decision, call the core or evaluate_portable_verdict.The hosted order slots the fenced-out checks back in around the pure ones. Five gates run first and each denies or sheds on its own: the emergency stop, an RSS soft ceiling, receipt-version negotiation, trusted-computing-base lock health, and receipt-persistence and revocation-durability readiness. Abridged, in source order after those:
| Hosted step | Owner | On the exclusion list? |
|---|---|---|
verify_capability_full_pre_admit | Core verifier, called by the shell | No |
check_time_bounds | Shell | No |
check_tool_call_revocation_admission | Shell | Yes, item 1 |
validate_delegation_admission | Shell | Yes, item 3 |
check_subject_binding | Shell | No |
resolve_required_matching_grants | Shell | No |
| DPoP verification when a matched grant requires it | Shell | Yes, item 4 |
run_guards_within_budget | Shell orchestration around Kernel guards. The budget here is the guard-pipeline time deadline, not money. | No |
admit_capability_budget | Shell, against the same BudgetRegistry trait the core uses, this time taking a releasable lease | No |
BudgetStore charge, dispatch, receipt append | Shell | Yes, items 2, 7, and 8 |
The browser kernel shows what the exclusion list costs when nothing fills it in. It refuses outright any request carrying governed-approval or supplemental-authorization extensions, runs the core with an empty guard pipeline, then downgrades a core Allow to pending_approval on the wire, with authorized: false, guards_evaluated: false, and authorization_basis: "capability_only". The raw core verdict survives in a separate capability_verdict field, labeled in the wire type as diagnostic and not execution authorization. Portable Kernel compares the three adapters field by field.
The list is about state, not knowledge
Reading the eight bullets as “the core knows nothing about these” gets the design backwards. It holds the pure admission predicate for several of the excluded checks in formal_core.rs; what it declines to hold is the state those predicates read. revocation_snapshot_denies, dpop_admits, nonce_admits, budget_precheck, and budget_commit are all defined there, and budget_commit, dpop_admits, guard_pipeline_allows, and receipt_fields_coupled are named in the covered-symbol list of formal/proof-manifest.toml.
Read the boundary that sits around them. formal_core is declared pub(crate) in chio-kernel-core/src/lib.rs, so no predicate in it is callable from outside the crate and none of them sits on a stateful call path. What a covered symbol buys is a guarantee about the decision a set of booleans implies, not about the code that produced the booleans.
One module complicates the picture in the other direction. revocation_view ships inside chio-kernel-core, holding an arc-swap snapshot of the most recent signed revocation epoch root with a monotone-epoch install rule. It is gated behind the revocation-view feature, which implies std, so the portable build leaves it out entirely. The pure evaluate path never consults it; the shell's validate_delegation_admission does, under #[cfg(feature = "delegation")] and only when a view has actually been installed, since the field is an Option and the consultation returns Ok(()) otherwise. The feature is default-on for chio-kernel, and with a view present the check is strict: a snapshot older than the 500 ms default staleness window denies before any membership test runs. Shipping in the core crate is not the same as being on the pure path.
Guarantees and limits
| Status | Claim | Evidence | Limit |
|---|---|---|---|
| Shipped | The core compiles with no std and no I/O dependency, for the host and for wasm. | scripts/check-portable-kernel.sh; #![no_std] and #![deny(unsafe_code)] in lib.rs. | Two default-on features (std, revocation-view) are outside that proof. Hosted builds get them. |
| Shipped | Every deny from the core names its cause, including which guard fired. | KernelCoreError::deny_reason and the attribution tests in evaluate.rs. | The reason is a human-readable string, not a stable machine code. |
| Proved, bounded | chio_kernel_core::evaluate::evaluate is inside the current implementation-linked verified core. | formal/proof-manifest.toml; the boundary map in docs/architecture/CHIO_RUNTIME_BOUNDARIES.md. | Coverage stops at capability verification, subject binding, portable scope matching, and the sync guard pipeline. Revocation lookups, budget mutation, DPoP, and dispatch are outside the present claim, and the module says so. |
| Proved, bounded | The shell delegation into the core is itself covered. | The shell_entrypoints array in formal/proof-manifest.toml carries evaluate_portable_verdict and build_and_sign_receipt, and the prose table in docs/architecture/CHIO_RUNTIME_BOUNDARIES.md carries the same two. | Two names, and they are the delegating methods rather than the paths that reach them. check_revocation, consult_revocation_view_at, run_guards, the DPoP nonce store, both budget stores, and every evaluate_tool_call_* are outside the array. |
| Modeled | Excluded checks have pure admission predicates, and the state machines around them have their own models. | formal_core.rs and the harnesses over it in chio-kernel-core/src/kani_public_harnesses.rs; formal/tla/RevocationPropagation.tla for propagation and formal/apalache/KernelTransitionCancelSafe.tla for a canceled in-flight transition. | Modeling, not refinement. A model constrains its own state machine; nothing derives the shell’s behavior from one. |
| Assumed | Concrete Ed25519, SHA-256, canonical JSON, TLS, OS clock, and SQLite behave as specified. | formal/assumptions.toml registers 15 assumption IDs; CHIO_RUNTIME_BOUNDARIES.md summarizes them. | Audited assumptions, not first-principles theorems. Several are narrower than their names suggest: ASSUME-SQLITE-ATOMICITY covers single-row writes and explicitly leaves cross-row crash recovery, ordering, and conservation outside the claim. So is anything a tool server does after the decision allows a call. |
Read evaluate.rs, not the design document
docs/protocols/PORTABLE-KERNEL-ARCHITECTURE.md is the extraction design note the crate points at, and it sketches a wider core than the one that shipped: DPoP proof verification, in-memory revocation checking, in-memory budget accounting, and Merkle checkpoint construction inside chio-kernel-core, behind a KernelCore struct, with a full feature gating I/O on the shell. None of that shipped. There is no KernelCore type, the shell has no full feature, and all four of those items are on the exclusion list. Where the two disagree, the module doc comment on evaluate.rs is authoritative, because it sits next to the function it describes.Next steps
- Node Overview · the other half of this split, and everything the eight bullets hand off
- The Guard Trait · the two
Guardtraits this seam produces, and which to implement - The Kernel · the filing tests that turn this list into a rule for every Platform page
- Portable Kernel · what the sidecar, mobile, and browser adapters each supply around the core
- Assumptions & TCB · what the bounded proof claim covers and what it treats as audited
- Capabilities · the token the first three steps verify, attenuate, and match