BuildTopologiesnew
In-Process Library
Link chio-kernel into your host binary. This removes the socket hop but puts the receipt-signing key in the host process address space.
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.
In-process trust boundary
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 in-process API is the chio-kernel crate (lib target chio_kernel) and its chio-core dependency. The kernel is the trusted compute base, so treat every dependency bump as a deliberate, reviewed change rather than tracking the latest minor automatically.
Useful feature flags:
| Feature | Effect |
|---|---|
delegation | Default. Recursive-delegation kernel integration: the verifier consults the installed RevocationView snapshot on every delegated dispatch and denies a capability if any link's capability_id is in the revoked set. |
pq | Off by default. Pulls the pq feature on chio-core-types and chio-core so the boot path can construct a hybrid ML-DSA-65 signing backend. See Post-Quantum and Hybrid Signing below. |
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
Post-Quantum and Hybrid Signing
The kernel can sign receipts and capability tokens with a hybrid classical-plus-ML-DSA-65 backend. Build chio-kernel with the pq cargo feature, which pulls the matching feature on chio-core-types and chio-core so the boot path can construct a HybridBackend from a rolled ML-DSA-65 key.
A deployment sets its cryptographic floor with a HybridSigningConfig. The crypto_floor is one of KernelCryptoFloor::AllowClassical (default; accept classical-only Ed25519 envelopes), AllowHybrid (accept hybrid envelopes), or PqRequired (reject classical-only envelopes, require hybrid signing on each signed envelope). Both non-classical floors require a 32-byte ML-DSA-65 keygen seed.
use chio_kernel::{HybridSigningConfig, KernelCryptoFloor};
let hybrid = HybridSigningConfig {
crypto_floor: KernelCryptoFloor::AllowHybrid,
// 32-byte ML-DSA-65 keygen seed, loaded from the secret backend.
pq_signing_seed: Some(pq_seed),
};After the kernel self-quote gate accepts the non-classical floor, ChioKernel::with_hybrid_signing_backend threads the classical Ed25519 keypair together with an ML-DSA-65 backend derived from the seed into a Box<dyn SigningBackend>. The hybrid config is a separate input from KernelConfig, so enabling it changes no KernelConfig wire bytes.
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. It also carries two durability gates — allow_ephemeral_receipt_log and allow_ephemeral_revocation_store, both left false so a successful dispatch requires durable persistence — and two resource-budget structs: memory_budget (bounded-structure caps plus an RSS soft ceiling) and deadlines (wall-clock budgets for the guard pipeline, dispatch, and receipt append).
use chio_core::crypto::Keypair;
use chio_kernel::kernel::{
ChioKernel, HotPathDeadlineConfig, KernelConfig, MemoryBudgetConfig,
};
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("signing seed 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,
allow_ephemeral_receipt_log: false, // require durable receipts
allow_ephemeral_revocation_store: false, // require durable revocation
checkpoint_batch_size: 100,
retention_config: None,
memory_budget: MemoryBudgetConfig::defaults(),
deadlines: HotPathDeadlineConfig::default(),
};
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.
Exported default limits
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 defined ownership and shutdown behavior.
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. The registries have no API for live mutation.
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, HotPathDeadlineConfig, KernelConfig, MemoryBudgetConfig,
};
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,
allow_ephemeral_receipt_log: false,
allow_ephemeral_revocation_store: false,
checkpoint_batch_size: 100,
retention_config: None,
memory_budget: MemoryBudgetConfig::defaults(),
deadlines: HotPathDeadlineConfig::default(),
})
}
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 walkthrough that wires a tool server, 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 by explicitly dropping the guard, 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 shows how to link the kernel into a small Rust service.
- Trust Model explains the signing-key risk in the in-process build.