Portable Kernel Core
See which crates embed the portable decision core, what its build gate checks, and what each host must supply.
On this page
chio-kernel-core compiles as a no_std crate with no default features, for the host and for wasm32-unknown-unknown, and four crates link that one decision core: the hosted shell, a browser adapter, a mobile adapter, and a C ABI.
The core evaluates the context it is handed and returns a verdict. Time and entropy arrive through two one-method traits the caller implements. The host owns persistence, signing keys, revocation, proof-of-possession checks, and every effect, and each adapter’s qualification script covers its own wire mapping. For a browser integration, start with the Browser Kernel guide.
Four crates link one decision core
The crate declares #![no_std] with extern crate alloc and #![deny(unsafe_code)], and names no std::* item. Each crate that links it adds the platform around the same evaluator.
Three adapters call one core function
The browser, mobile, and C ABI adapters route a governed action through evaluate_with_full_floor_and_root. The hosted shell's production admission enters the core at verify_capability_full_with_root and runs the rest of dispatch in chio-kernel. Link targets.
chio-kernel. The host binary and rlib, on hostedstdwith workspace default features, sostdandrevocation-vieware on. Production admission goes throughverify_capability_full_pre_admit, which enters the core atverify_capability_full_with_rootand runs the rest of dispatch inchio-kernel. The shell also wraps the adapters’ entry point asevaluate_portable_verdict, whichformal/proof-manifest.tomlregisters as the shell entry point inside the bounded verified core.chio-kernel-browser. Acdylibplusrlibforwasm32-unknown-unknown, built bywasm-pack build --target webwithdefault-features = false. The crate is itself#![no_std]and pulls instdonly undercfg(not(target_arch = "wasm32")), so host tests link. It exposes seven#[wasm_bindgen]functions, gated onwasm32.chio-kernel-mobile. Astaticlibfor Xcode, acdylibfor the Android NDK, and anrlib, on hostedstdwith workspace defaults andchio-core-typesdefault features. Eleven functions declared insrc/chio_kernel_mobile.udlare projected to Swift and Kotlin by UniFFI 0.28.chio-cpp-kernel-ffi. Astaticlib,cdylib, andrlibfor host triples, on hostedstdwith workspace defaults. It exports nineextern "C"symbols against the checked-in cbindgen headerinclude/chio/chio_kernel_ffi.h: six kernel operations, pluschio_kernel_ffi_abi_version,chio_kernel_build_info, andchio_kernel_buffer_freeas ABI support functions.
Only the browser adapter is no_std end to end. Mobile and the C ABI are hosted builds that link a portable core and use std::collections::BTreeMap and std::time::SystemTime in their own code; the core spares them tokio, rusqlite, and sockets.
The TypeScript, Python, and Go SDKs embed no core at all: they open a computer against a running kernel with Computer.open, their lower-level ChioClient posts JSON-RPC to a kernel’s base_url with a bearer token, and the Go SDK needs neither CGO nor native bindings. Each SDK’s invariants module implements canonical JSON, SHA-256, Ed25519 signing and verification, and structural checks for capabilities, receipts, and signed manifests natively, so a client verifies what a kernel produced and reads the decision a receipt records without running evaluate.
Unsafe-code rules per crate
The core and the browser adapter carry #![deny(unsafe_code)]. The mobile crate root holds UniFFI’s build-script-generated scaffolding, which declares the #[no_mangle] extern "C" symbols Swift and Kotlin link against, so a crate-root deny would reject generated code the crate does not author. Its three hand-written submodules (clock, rng, errors) carry #![forbid(unsafe_code)]; the crate root, where all eleven entry points live and include_scaffolding! pulls in the generated shim, contains no unsafe block of its own.
The C ABI holds exactly two unsafe blocks, both SAFETY-commented: CStr::from_ptr after a null check, and Vec::from_raw_parts in chio_kernel_buffer_free.
Read more in Core & Shell, which draws the boundary between the core and everything the shell adds.
The portability gate builds the core for two targets
The portability gate compiles the core twice with no default features: once for the host, once for wasm32-unknown-unknown. Every optional hosted feature implies std, so disabling them selects the portable build.
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-target/portable-kernel}"
rustup target add wasm32-unknown-unknown >/dev/null
echo "[portable-kernel] building host target with --no-default-features"
cargo build -p chio-kernel-core --no-default-features
echo "[portable-kernel] building wasm32-unknown-unknown with --no-default-features"
cargo build -p chio-kernel-core --target wasm32-unknown-unknown --no-default-features
echo "[portable-kernel] ok"Every Chio release passes the portable-kernel gate before it is tagged, and workspace qualification runs it on every change. chio release verify --evidence qualification shows its result for the release you run; Qualification: security gates runs it from source. Your own embedding gets the same check from the published crate:
cargo add chio-kernel-core@0.2.0 --no-default-features
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown Updating crates.io index
Adding chio-kernel-core v0.2.0 to dependencies
info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date
Compiling chio-core-types v0.2.0
Compiling chio-kernel-core v0.2.0
Finished `dev` profile [unoptimized + debuginfo] target(s) in 18.60sA dependency that pulls std back in fails this build, which is the cheapest way to find it before a browser or device does.
The gate checks compilation. Behavior on each target belongs to the adapters’ qualification lanes. The browser qualification lane runs release WebAssembly tests in headless Chrome, discovered with its chromedriver, and records artifact size and evaluation latency. The mobile qualification lane reports each target as pass, fail, or environment_dependent: its host_ffi lane runs the FFI round-trip test under cargo test, and its iOS, iOS-simulator, and Android lanes cross-compile release libraries.
A host without the Apple SDK targets, or without cargo-ndk and an NDK, reports those lanes as environment_dependent. A cross-compile establishes a build result; execution on a device or simulator is a separate validation.
Clock and Rng are the only platform inputs
A core that computes verdicts still needs two things it cannot compute: the time and fresh bytes. Both arrive as traits with one method each.
/// Abstract monotonic wall-clock exposing Unix seconds.
///
/// Implementations MUST return a value consistent with the signed
/// `issued_at` / `expires_at` fields on capabilities. The verdict path is
/// fail-closed against clock errors: if `now_unix_secs` returns a value in
/// the past of `issued_at` or past `expires_at`, the capability is rejected.
pub trait Clock {
/// Current Unix timestamp in seconds.
fn now_unix_secs(&self) -> u64;
}Rng has the same shape, fn fill_bytes(&self, dest: &mut [u8]), with a doc comment requiring cryptographically strong output, so an adapter can route it to a native CSPRNG. Three further traits are injected the same way and carry policy instead of platform: Guard, BudgetRegistry, and TrustRootResolver. Each target supplies the seams as follows:
chio-kernel. Implements neither. It reads its own hosted time as au64and wraps it in the core’sFixedClockper call, so the second a hosted decision is evaluated at is fixed before the core is entered.evaluate_portable_verdicttakes&dyn Clockfrom its caller.chio-kernel-browser.BrowserClockoverjs_sys::Date::now(), andWebCryptoRngoverweb_sys::Crypto::get_random_values_with_u8_array.chio-kernel-mobile.MobileClockoverSystemTime::now(), andMobileRngovergetrandom, exported from the crate root and called by no entry point.chio-cpp-kernel-ffi. A privateSystemClock, and no Rng.
Mobile and the C ABI take a 32-byte Ed25519 signing seed from the caller as lowercase hex, so the only adapter that reads its own Rng is the browser, in mint_signing_seed_hex. The Rng trait exists so a portable target can mint a key without importing an OS. The evaluation time is a caller input on every FFI adapter: clock_override_unix_secs on the browser and now_secs on mobile and the C ABI build a FixedClock in place of the platform clock, in every build. Mobile requires a positive value; the browser and the C ABI accept any u64, including zero. Time-bound checks are as trustworthy as the host that supplies that value.
Clock and entropy edge cases
BrowserClock divides milliseconds by 1000 and truncates to the token’s integer-second time. A non-finite or non-positive Date.now() returns 0, an f64 past u64::MAX saturates, and MobileClock clamps to 0 before the epoch. Zero is evaluated against the supplied token’s time bounds like any other time. Both browser types have host-target stubs so the rlib links for native tests: BrowserClock returns 0 and WebCryptoRng::try_new always errors, reachable only off wasm, where the entry points are cfg-gated out.
Rng has no error channel, so every implementation zero-fills on failure, including the core’s test-only NullRng. The callers hold the safety property: mint_signing_seed_hex rejects an all-zero seed with weak_entropy, and both browser signers and backend_from_seed_hex on mobile check for it before building a keypair. The C ABI signers take their seed from the caller, so a C++ embedder generates it from a trusted CSPRNG and discards a failed or zero-filled result before calling the signer.
Three adapters share one evaluation and differ on the wire
Each FFI adapter parses a JSON envelope, refuses authorization extensions it cannot authenticate, decodes trusted issuer keys from hex, seeds a per-request InMemoryBudgetRegistry from caller-supplied parent-budget snapshots, pins CapabilityCryptoFloor::AllowClassical, passes an empty guard slice, and calls evaluate_with_full_floor_and_root. The registry is built inside the call and dropped when it returns, so sibling-sum admission runs against the snapshot the caller supplied and nothing accumulates between calls.
| Core verdict | Browser | Mobile UniFFI | C ABI |
|---|---|---|---|
Allow | pending_approval, basis capability_only | allow | allow |
Deny | deny, core reason | deny, core reason | deny, core reason |
PendingApproval | pending_approval | deny, names the downgrade | deny, names the downgrade |
The browser row is a policy decision. Two fields of EvaluationVerdictJson are constants regardless of the verdict, authorized: false and guards_evaluated: false; the raw verdict survives in capability_verdict, and the reason substituted on an allow reads capability-only browser evaluation requires a mediated prevent receipt before execution. No browser caller can obtain a response from this adapter that claims authorization. The core itself never emits PendingApproval: a guard that returns it is rewritten to GuardDenied, as the EvaluationVerdict doc comment states. The adapters keep the arm because their wire enums mirror the shell’s three-valued one, so match on Allow and Deny.
crates/kernel/chio-kernel-mobile/tests/cross_ffi_parity.rs holds mobile and the C ABI byte-identical. It pulls the C ABI crate in as a source module with #[path = "../../../sdk/chio-cpp-kernel-ffi/src/lib.rs"], drives both entry points over the three cases in tests/fixtures/parity/evaluate_cases.json (allow_echo, deny_unknown_tool, deny_unknown_server), and asserts mobile.as_bytes() == cpp.as_bytes() on the verdict JSON in mobile_uniffi_and_cpp_c_abi_return_byte_equal_verdicts. Two further tests assert that both refuse a negotiated aggregate-invocation or cumulative-approval family whose direct root token does not match. The C++ SDK separately links the static library against the checked-in header and passes its own ctest suite in the path-scoped chio-cpp workflow.
The parity test compiles the C ABI as Rust and covers mobile and the C ABI. The browser adapter maps the allow row on its own, and its signer replaces body.kernel_key with the key derived from the seed, where mobile and the C ABI refuse a body whose kernel_key does not already match. verify_passport is exposed by the mobile UDL and the C ABI. Hardware attestation is mobile only: App Attest and Play Integrity challenge and verification entry points come from chio-custody-hw, and verify_mobile_receipt answers "status": "shape_only", authoritative: false, authorized: false, a shape check.
Read more in Verdicts & Approval, which defines the enum these adapters map.
A portable allow answers the capability question and leaves state to the host
An allow from a portable adapter says the capability verified and matched at the supplied time. Every adapter refuses five kinds of input before or during evaluation:
- Authorization it cannot authenticate. A request carrying a
governed_intent, anapproval_tokenorapproval_tokens, athreshold_approval_proposal, or asupplemental_authorizationis parsed and rejected before evaluation begins. Each wire type declares those fields explicitly so they are refused instead of silently dropped. - Two capability features. A token carrying an
aggregate_invocation_budgetor a cumulative-approval scope is denied inside the core before subject binding, with the reasoncapability feature unsupported on this runtime: aggregate invocation enforcementor its cumulative-approval counterpart. Enforcing either needs durable cross-call state, so the portable path refuses the token; the cross-FFI parity test pins the exact string. - A guard pipeline. All three adapters pass
&[]. Capability signature, crypto floor, time bounds, subject binding, portable scope match, and sibling-sum admission run; the seven-guard default pipeline runs in the hosted kernel. - Signing without the preimage. Each adapter’s public signer recomputes
content_hashover the caller-supplied canonical content inside the trust boundary and refuses on mismatch; the browser refuses outright when the preimage is absent. A separately named relay entry point serves transports that forward a body an upstream trusted producer already minted, and it is the default on none of the three. - A stale ABI.
CHIO_CPP_KERNEL_FFI_ABI_VERSIONis 2, bumped when the signer gained its third pointer argument under an unchanged symbol name. A C++ client that gates onchio_kernel_ffi_abi_version()fails closed instead of calling the three-argument symbol with a dangling pointer.
The core has no revocation lookup, and none of the adapters installs a revocation view, checks a DPoP proof, or persists a receipt. The host checks that the capability is still live and records the receipt, through the node runtime or its own stores.