In-Process Library
The fastest way to ship chio is to link chio-kernel directly into the host binary as a Rust crate. There is no socket hop, no sidecar process, and no second container to operate. The trade-off is that the receipt-signing key lives in the same address space as host code, so the host code itself sits inside the trust boundary.
When to read this page
When to Choose In-Process
In-process is the right answer when three things hold at once:
- Host code is trusted. The binary that links the kernel is built, signed, and operated by the same team that owns the signing key. Third-party agent code does not run inside the host process.
- Latency budget is tight. You are guarding hot paths where the localhost-HTTP cost of a sidecar (about 100 microseconds per call) would matter.
- Single-tenant runtime. One signing key, one policy, one process lifecycle. No need to roll policy or rotate keys without restarting the host.
Pick the sidecar instead when any of the following is true:
- The host loads untrusted code (third-party agent runtimes, scripted plugins, model-generated code). A compromised host process can read the signing key and forge receipts.
- The deployment is multi-tenant: one process must enforce different policies or different signing identities per tenant.
- You want to hot-reload policy without restarting the host. The in-process build does not support dynamic policy reload.
- You want to scale the kernel independently of the host, or to roll the kernel forward without redeploying the host.
Trust the binary, not the runtime
Cargo Dependency
The in-process kernel ships as the chio-kernel crate. The lib target is named chio_kernel. Add it to the host crate's Cargo.toml:
[dependencies]
chio-kernel = "0.1"
chio-core = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tracing = "0.1"The kernel and core types are published on crates.io as chio-kernel and chio-core. Pin a concrete version in production rather than tracking the latest minor; the kernel is the trusted compute base, so a deliberate dependency bump is the right cadence.
Useful feature flags:
| Feature | Effect |
|---|---|
legacy-sync | Default. Keeps the synchronous receipt-signing surface for callers that have not migrated to the async signing-task handle. |
otel | Enables OpenTelemetry semantic conventions for kernel spans. Pairs with the host's tracing-otel exporter. |
tokio-console-smoke | Test-only. Pulls in tokio/tracing for the console-smoke test target. |
The Embedded Signing Key
Every receipt is signed with an Ed25519 keypair held by the kernel. In the in-process build, that keypair is constructed from raw seed bytes and lives on the heap inside the host process for as long as the kernel does. Two consequences follow:
- The seed must come from a managed secret backend, not a checked-in file or a build-time constant. See Secrets & Keys for the supported backends and the rotation workflow.
- The host process must zeroize the seed on shutdown if it survives across reloads. The kernel itself takes ownership of the keypair via
chio_core::crypto::Keypair; the host code must not retain its own copy of the raw seed bytes.
Never check in a signing seed
Initialization
The kernel is constructed with a KernelConfig and exposed through the ChioKernel struct. The config carries the signing keypair, trusted capability authority public keys, the policy hash, and a handful of safety knobs.
use chio_core::crypto::Keypair;
use chio_kernel::kernel::{ChioKernel, KernelConfig};
fn build_kernel(seed_hex: &str, policy_hash: String) -> ChioKernel {
// Load the signing seed from the host's secret backend.
// `Keypair::from_seed_hex` rejects malformed input.
let keypair = Keypair::from_seed_hex(seed_hex)
.expect("CHIO_SIGNING_KEY must be a 64-char hex Ed25519 seed");
let config = KernelConfig {
keypair,
ca_public_keys: Vec::new(), // populate from your CA roster
max_delegation_depth: 4,
policy_hash,
allow_sampling: false,
allow_sampling_tool_use: false,
allow_elicitation: false,
max_stream_duration_secs: 300,
max_stream_total_bytes: 256 * 1024 * 1024,
require_web3_evidence: false,
checkpoint_batch_size: 100,
retention_config: None,
};
ChioKernel::new(config)
}ChioKernel::new is constructible from a synchronous context. It does not start the async signing task immediately; the task is spawned lazily on the first signing call, by which time a tokio runtime must be active. That means you can build the kernel inside a non-async constructor and pass it into the runtime, or build it inside #[tokio::main]. Both work.
Defaults that are sensible
DEFAULT_MAX_STREAM_DURATION_SECS, DEFAULT_MAX_STREAM_TOTAL_BYTES, and DEFAULT_CHECKPOINT_BATCH_SIZE are exported from the kernel crate. Use them as the floor for production deployments rather than copying the literal numbers.Lifecycle
The in-process kernel goes through three phases: build, serve, drain. Each phase has explicit invariants you can rely on.
Build
- Load the signing seed from a secret backend. Construct the
Keypair. - Compute or load the policy hash. The policy hash is embedded in every receipt, so changing the policy requires rebuilding the kernel (no live reload).
- Construct
KernelConfigand callChioKernel::new. - Register tool servers, resource providers, and prompt providers.
Serve
The agent connects through open_session, the kernel issues initial capabilities, and tool calls flow through the guard pipeline. Every allow or deny decision produces a signed receipt. The kernel is Send + Sync, so a single instance can be wrapped in Arc<ChioKernel> and shared across tasks.
Drain
On shutdown the host calls begin_draining_session followed by close_session for every active session. The kernel flushes pending receipts to its configured store, drains the signing task, and emits a final checkpoint if the receipt count crosses a checkpoint boundary (checkpoint_batch_size).
Drain before exit
Threading and Runtime
The kernel is built for the multi-threaded tokio runtime.
- Send + Sync:
ChioKernelcan be wrapped inArcand called from any thread. - Async signing path: the signing task is mpsc-backed and lives on the tokio runtime. It is spawned lazily on the first sign call. The runtime must be alive for the full lifetime of the kernel.
- Bounded backpressure: callers
.awaiton a bounded channel rather than on the keypair itself, so concurrent tool calls never serialize on a single mutex. - Single-thread runtimes: building the kernel inside a
tokio::runtime::Builder::new_current_thread()works for CLI-style hosts. The signing task still runs on that runtime; do not block it from another thread.
Hot-Reload Limitations
The in-process build does not hot-reload policy. Two reasons:
- The policy hash is part of every receipt. Rotating it underneath live sessions would invalidate the receipt-to-policy correspondence mid-session.
- The kernel registers tool servers, resource providers, and prompt providers at construction time. There is no live mutation surface for those registries.
To roll a new policy, restart the host process. If you need live policy reload, run chio as a sidecar and roll the sidecar container forward independently of the host.
Worked Example
A small Rust service that links chio and protects a function call. The host owns a signing seed loaded from CHIO_SIGNING_KEY, builds a kernel, opens a session, and drains on SIGTERM.
use std::sync::Arc;
use chio_core::crypto::Keypair;
use chio_kernel::kernel::{ChioKernel, KernelConfig};
use tokio::signal::unix::{signal, SignalKind};
use tracing::{info, warn};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let seed_hex = std::env::var("CHIO_SIGNING_KEY")
.map_err(|_| anyhow::anyhow!("CHIO_SIGNING_KEY must be set"))?;
let policy_hash = std::env::var("CHIO_POLICY_HASH")
.unwrap_or_else(|_| "dev-policy-hash".to_string());
let kernel = Arc::new(build_kernel(&seed_hex, policy_hash));
info!("chio kernel ready");
// ... register tool servers, open agent sessions, serve traffic ...
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
tokio::select! {
_ = sigterm.recv() => info!("SIGTERM received"),
_ = sigint.recv() => info!("SIGINT received"),
}
drain(&kernel).await;
info!("chio kernel drained, exiting");
Ok(())
}
fn build_kernel(seed_hex: &str, policy_hash: String) -> ChioKernel {
let keypair = Keypair::from_seed_hex(seed_hex)
.expect("CHIO_SIGNING_KEY must be a 64-char hex Ed25519 seed");
ChioKernel::new(KernelConfig {
keypair,
ca_public_keys: Vec::new(),
max_delegation_depth: 4,
policy_hash,
allow_sampling: false,
allow_sampling_tool_use: false,
allow_elicitation: false,
max_stream_duration_secs: 300,
max_stream_total_bytes: 256 * 1024 * 1024,
require_web3_evidence: false,
checkpoint_batch_size: 100,
retention_config: None,
})
}
async fn drain(kernel: &ChioKernel) {
// Walk every active session, mark draining, then close.
// Exact session enumeration depends on how the host tracked them.
// For a single global session, calls look like:
// kernel.begin_draining_session(&session_id).ok();
// kernel.close_session(&session_id).ok();
let _ = kernel; // placeholder
if let Err(error) = tokio::task::yield_now().await {
// never reached, here only to keep the type inference happy
warn!(?error, "yield interrupted");
}
}Build, run, observe:
# Build a release binary.
$ cargo build --release --bin host-service
# Run with a signing seed loaded from a managed secret store.
$ CHIO_SIGNING_KEY="$(gcloud secrets versions access latest \
--secret=chio-signing-key)" \
CHIO_POLICY_HASH="$(sha256sum policy.yaml | cut -d' ' -f1)" \
./target/release/host-service
# Watch receipts append to the local store as tool calls happen.
$ chio receipt list --receipt-db ./state/receipts.sqlite --tailFor a fuller walkthrough that wires a real tool server end to end, see Hello Tool.
Operational Concerns
Log flush on shutdown
The kernel emits structured tracing spans. The host must install a tracing subscriber that flushes on shutdown (e.g. tracing_appender::non_blocking with an explicit guard drop, or tracing_subscriber::fmt which is synchronous). A subscriber that buffers without a flush guarantee can lose the final allow/deny decisions made during drain.
Signal handling
Listen for both SIGTERM and SIGINT. On either, stop accepting new sessions, drain active sessions, then exit. Do not exit on SIGHUP for live reload: the kernel does not support live reload, so a SIGHUP handler that rebuilds the kernel is a footgun.
PID-file conventions
For systemd-supervised hosts, write a PID file under /run/<service>.pid and reference it from the unit's PIDFile= directive. Container deployments do not need a PID file; the orchestrator tracks PID 1 directly.
One kernel per process
Next Steps
- Deployment Topologies covers the trust-boundary trade-off between in-process and sidecar.
- Sidecar HTTP Service is the alternative when host code is not fully trusted.
- Secrets & Keys documents the supported secret backends and the rotation workflow for the in-process signing seed.
- Hello Tool walks through linking the kernel into a small Rust service end to end.
- Trust Model explains why the signing key is the load-bearing secret in the in-process build.