Embed a process host in Rust
Run the kernel and the durable process runtime inside your Rust binary, and serve workers over a private authenticated socket.
On this page
Your Rust application can own process creation, tool registration, and durable authority itself. The embedded host runs the kernel, its SQLite stores, and the report resource in one trusted executable, then serves a separate worker over a private Unix socket.
An ordinary Rust worker needs none of these APIs; it uses the Rust process client. Embed when the stores, signing keys, and listener lifecycle belong in your binary. The process architecture covers the worker, durable identity, and kernel boundary in general.
StartEmbedding moves host construction into your Rust binary
Your binary constructs the host: it holds the signing keys, the stores, and the listener. A worker still connects over the Unix socket with a private credential, and the host rebuilds that process’s authority from its records before the kernel admits a call.
Drawn fromlanguages/embedded-host/src/main.rs
The embedded host owns the signing keys, the root capability, the authority and receipt stores, the process journal, resource assignment, worker credential issuance, and the listener lifecycle. The worker owns its private connection, its stable application requests, and the restoration of its own progress. The resource service owns one atomic comparison of kernel-selected caller, assignment, and document version.
The host’s code-defined policy grants one report tool and binds it to the trusted signing key, and the resource checks the selected capability hash inside its transaction. The worker never reads the root capability or the database files. The local exercise runs everything under one Unix account for convenience; for untrusted workers, protect the host’s files with a separate OS identity or a verified worker sandbox, because isolation comes from the host and socket authentication alone supplies none.
1. Build and start the host
Use Linux and Rust 1.94 or newer. Download languages.zip and open languages/embedded-host/, which carries its Cargo manifest, lockfile, and Chio runtime source dependencies; Cargo fetches the rest.
cargo run --locked -- .chioYou should see a readiness message naming the worker descriptor. The host creates .chio/ with private permissions and saves its public kernel key in .chio/kernel.pub. The descriptor holds a bearer credential: pass it through standard input and keep it out of logs and source control.
2. Publish from a separate Rust worker
In a second terminal, run the worker with the descriptor on standard input, then verify the receipt it prints against the host’s kernel key.
cargo run --locked --manifest-path ../rust/Cargo.toml --example publish \
< .chio/connection.json > first-receipt.json
chio receipt verify --input first-receipt.json \
--trusted-kernel-pubkey .chio/kernel.pubuse chio_process_client::{Bootstrap, ProcessClient};
use serde_json::{json, Value};
use std::{io::Write, time::Duration};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let bootstrap = Bootstrap::<Value>::read(std::io::stdin().lock())?;
let client = ProcessClient::new(bootstrap.connection, Duration::from_secs(60))?;
let response=client.invoke_known_only("publish-report-42","reports","publish",
json!({"operation":"publish-report-42","generation":1,"version":0,"body":"A durable Rust report"})).await?;
let value = response.completed_value().ok_or("incomplete publication")?;
if value["status"] != "published" {
return Err("publication refused".into());
}
// stdout carries only the original receipt; redirect it to a private file.
std::io::stdout().write_all(response.receipt_json.as_bytes())?;
println!();
Ok(())
}You should see the publication succeed and the receipt verify. The request carries an application operation key as well as the host’s logical operation key: the resource uses its key to retain the mutation outcome in SQLite, and the process runtime retains the kernel decision and original receipt. The worker’s known-outcome-only policy allows the first dispatch and refuses a fresh dispatch after an unknown outcome.
An independent recipient selects the expected public key through its own trusted channel.
3. Read how the host restores authority before serving
The host persists randomly generated keys, restores the exact root capability, opens durable authority and receipt stores, reconciles admission, and only then exposes a worker listener. An exclusive lock refuses a second host owner.
#[tokio::main]
async fn main() -> Result<()> {
let state = PathBuf::from(
std::env::args()
.nth(1)
.context("usage: chio-embedded-process-example PRIVATE_STATE")?,
);
private_dir(&state)?;
let state = state.canonicalize()?;
let lock = lock_state(&state)?;
let (kernel, capability) = kernel::open(&state)?;
let public_key = kernel.public_key().to_hex();
let public_key_path = state.join("kernel.pub");
if !public_key_path.try_exists()? {
write_new(&public_key_path, format!("{public_key}\n").as_bytes())?;
}
ensure!(std::fs::read_to_string(&public_key_path)?.trim() == public_key,
"saved public key differs from retained host identity");
let service = worker_service(&state, kernel, &capability)?;
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
ensure!(
now < capability.expires_at,
"capability expired; do not silently replace its identity"
);
let credential = service.issue_credential("reporter", capability.expires_at)?;
// A fresh name avoids unlinking an unknown socket after a crash.
let socket = state.join(format!("worker-{}-{}.sock", std::process::id(), now));
let listener = WorkerServer::bind(&socket, service)?;
let connection_file = publish_connection(&state, &socket, credential.expose_secret())?;
println!(
"{}",
json!({
"ready": true,
"connection_file": connection_file,
"kernel_key": public_key,
})
);
listener
.serve(async {
let _ = tokio::signal::ctrl_c().await;
})
.await?;
File::open(&state)?.sync_all()?;
drop(lock);
Ok(())
}The resource adapter handles the context-bearing cost path explicitly, because a default context-free fallback would lose its authenticated caller. The entry point owns the lifecycle, and three modules keep its supporting responsibilities apart: kernel.rs restores authority and reconciles admission, reports.rs binds each publication to its authenticated caller, and storage.rs retains keys and writes private files.
The complete host source: main.rs, kernel.rs, reports.rs, storage.rs
//! A trusted host and a separate, credentialed worker share one durable process.
mod kernel;
mod reports;
mod storage;
use anyhow::{ensure, Context, Result};
use chio_core_types::capability::token::CapabilityToken;
use chio_kernel::ChioKernel;
use chio_process::{
worker::{WorkerServer, WorkerService},
ProcessLimits, ProcessRuntime,
};
use serde_json::json;
use std::os::unix::fs::OpenOptionsExt;
use std::{
fs::{File, OpenOptions},
path::{Path, PathBuf},
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use storage::{private_dir, write_new};
fn lock_state(state: &Path) -> Result<File> {
let lock = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.mode(0o600)
.open(state.join("host.lock"))?;
fs2::FileExt::try_lock_exclusive(&lock).context("another host owns this state")?;
Ok(lock)
}
fn worker_service(
state: &Path,
kernel: ChioKernel,
capability: &CapabilityToken,
) -> Result<WorkerService> {
let runtime = ProcessRuntime::open(state.join("process.db"), Arc::new(kernel))?;
runtime.create_root(
"reporter",
capability,
ProcessLimits {
max_processes: 1,
max_depth: 0,
max_calls: 16,
state: Default::default(),
},
)?;
let service = WorkerService::new(runtime);
service.revoke_credentials("reporter")?;
Ok(service)
}
fn publish_connection(state: &Path, socket: &Path, credential: &str) -> Result<PathBuf> {
let descriptor = json!({
"schema": "chio.process.worker-bootstrap.v1",
"attempt": 1,
"input": {},
"connection": { "socket_path": socket, "credential": credential },
});
let temporary = state.join("connection.new");
let destination = state.join("connection.json");
write_new(&temporary, &serde_json::to_vec(&descriptor)?)?;
std::fs::rename(temporary, &destination)?;
File::open(state)?.sync_all()?;
Ok(destination)
}
#[tokio::main]
async fn main() -> Result<()> {
let state = PathBuf::from(
std::env::args()
.nth(1)
.context("usage: chio-embedded-process-example PRIVATE_STATE")?,
);
private_dir(&state)?;
let state = state.canonicalize()?;
let lock = lock_state(&state)?;
let (kernel, capability) = kernel::open(&state)?;
let public_key = kernel.public_key().to_hex();
let public_key_path = state.join("kernel.pub");
if !public_key_path.try_exists()? {
write_new(&public_key_path, format!("{public_key}\n").as_bytes())?;
}
ensure!(std::fs::read_to_string(&public_key_path)?.trim() == public_key,
"saved public key differs from retained host identity");
let service = worker_service(&state, kernel, &capability)?;
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
ensure!(
now < capability.expires_at,
"capability expired; do not silently replace its identity"
);
let credential = service.issue_credential("reporter", capability.expires_at)?;
// A fresh name avoids unlinking an unknown socket after a crash.
let socket = state.join(format!("worker-{}-{}.sock", std::process::id(), now));
let listener = WorkerServer::bind(&socket, service)?;
let connection_file = publish_connection(&state, &socket, credential.expose_secret())?;
println!(
"{}",
json!({
"ready": true,
"connection_file": connection_file,
"kernel_key": public_key,
})
);
listener
.serve(async {
let _ = tokio::signal::ctrl_c().await;
})
.await?;
File::open(&state)?.sync_all()?;
drop(lock);
Ok(())
}//! Restore authority and reconcile interrupted admissions before serving workers.
use crate::{
reports::Reports,
storage::{key, write_new},
};
use anyhow::Result;
use chio_core_types::{
capability::{
attenuation::scope_hash,
scope::{ChioScope, Operation, ToolGrant},
token::CapabilityToken,
},
crypto::{canonical_json_bytes, sha256_hex},
};
use chio_kernel::{admission_operation::DurableAdmissionMode, ChioKernel, KernelConfig};
use chio_resource_example::Resource;
use chio_store_sqlite::{SqliteAuthorityStore, SqliteReceiptStore};
use std::os::unix::fs::PermissionsExt;
use std::{
path::Path,
sync::{Arc, Mutex},
};
fn scope() -> ChioScope {
ChioScope {
grants: vec![ToolGrant {
server_id: "reports".into(),
tool_name: "publish".into(),
operations: vec![Operation::Invoke],
constraints: vec![],
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
..Default::default()
}
}
pub fn open(state: &Path) -> Result<(ChioKernel, CapabilityToken)> {
let locks = state.join("locks");
if !locks.exists() {
std::fs::create_dir(&locks)?;
std::fs::set_permissions(&locks, std::fs::Permissions::from_mode(0o700))?;
}
let issuer = key(&state.join("kernel.key"))?;
let subject = key(&state.join("worker.key"))?;
let authority_path = state.join("authority.db");
if !authority_path.exists() {
SqliteAuthorityStore::provision(&authority_path, &locks)?;
}
let authority = SqliteAuthorityStore::open_serving(&authority_path, &locks)?;
let mut kernel = ChioKernel::new(KernelConfig {
keypair: issuer,
max_delegation_depth: 1,
ca_public_keys: vec![],
policy_hash: sha256_hex(b"embedded-report-policy-v1"),
allow_sampling: false,
allow_sampling_tool_use: false,
allow_elicitation: false,
max_stream_duration_secs: 30,
max_stream_total_bytes: 1048576,
require_web3_evidence: false,
allow_ephemeral_receipt_log: false,
allow_ephemeral_revocation_store: false,
checkpoint_batch_size: 0,
retention_config: None,
memory_budget: chio_kernel::MemoryBudgetConfig::defaults(),
deadlines: Default::default(),
});
kernel.set_capability_trust_root(
key(&state.join("kernel.key"))?.public_key(),
scope_hash(&scope())?,
);
kernel.set_receipt_store(Box::new(SqliteReceiptStore::open(
state.join("receipts.db"),
)?))?;
kernel.set_revocation_store(Box::new(authority.revocation_store()));
kernel.set_budget_store(Box::new(authority.budget_store()));
kernel.set_durable_admission_store(
Arc::new(authority.admission_operation_store()),
Arc::new(authority.tool_outcome_store()),
authority.mutation_fence(),
)?;
kernel.configure_durable_admission(DurableAdmissionMode::All, false)?;
let capability_path = state.join("root-capability.json");
let capability: CapabilityToken = if capability_path.exists() {
serde_json::from_slice(&std::fs::read(&capability_path)?)?
} else {
let value = kernel.issue_capability(&subject.public_key(), scope(), 3600)?;
write_new(&capability_path, &serde_json::to_vec(&value)?)?;
value
};
let resource_path = state.join("resource.db");
if !resource_path.exists() {
Resource::initialize(&resource_path)?;
Resource::open(&resource_path)?
.assign(0, &sha256_hex(&canonical_json_bytes(&capability)?))?;
}
kernel.register_tool_server(Box::new(Reports(Mutex::new(Resource::open(
&resource_path,
)?))));
kernel.reconcile_durable_admission_startup()?;
Ok((kernel, capability))
}//! Bind the resource transaction to the kernel-selected caller.
use anyhow::{ensure, Result};
use chio_kernel::{
KernelError, NestedFlowBridge, ToolInvocationContext, ToolInvocationCost, ToolServerConnection,
};
use chio_resource_example::{Publish, Resource};
use serde_json::Value;
use std::sync::Mutex;
pub struct Reports(pub Mutex<Resource>);
#[async_trait::async_trait]
impl ToolServerConnection for Reports {
fn server_id(&self) -> &str {
"reports"
}
fn tool_names(&self) -> Vec<String> {
vec!["publish".into()]
}
async fn invoke(
&self,
_: &str,
_: Value,
_: Option<&mut dyn NestedFlowBridge>,
) -> std::result::Result<Value, KernelError> {
Err(KernelError::ToolServerError(
"authenticated invocation context required".into(),
))
}
// Both entry points must use the kernel-selected context. A default cost
// implementation would otherwise fall back to the context-free method.
async fn invoke_with_context(
&self,
context: &ToolInvocationContext,
arguments: Value,
_: Option<&mut dyn NestedFlowBridge>,
) -> std::result::Result<Value, KernelError> {
let run = || -> Result<Value> {
ensure!(context.tool_name() == "publish");
let request: Publish = serde_json::from_value(arguments)?;
let mut resource = self
.0
.lock()
.map_err(|_| anyhow::anyhow!("resource lock poisoned"))?;
Ok(serde_json::to_value(
resource.publish(context.capability_hash(), &request)?,
)?)
};
run().map_err(|e| KernelError::ToolServerError(e.to_string()))
}
async fn invoke_with_cost_and_context(
&self,
context: &ToolInvocationContext,
arguments: Value,
bridge: Option<&mut dyn NestedFlowBridge>,
) -> std::result::Result<(Value, Option<ToolInvocationCost>), KernelError> {
Ok((
self.invoke_with_context(context, arguments, bridge).await?,
None,
))
}
}//! Private files and retained keys. No worker receives access to this module.
use anyhow::{ensure, Result};
use chio_core_types::crypto::Keypair;
use rand::RngCore;
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt};
use std::{fs::OpenOptions, io::Write, path::Path};
pub fn private_dir(path: &Path) -> Result<()> {
if !path.try_exists()? {
std::fs::DirBuilder::new().mode(0o700).create(path)?;
}
let metadata = path.symlink_metadata()?;
ensure!(metadata.is_dir() && !metadata.file_type().is_symlink(), "state must be a real directory");
ensure!(
metadata.permissions().mode() & 0o077 == 0,
"existing state directory must have mode 0700"
);
Ok(())
}
pub fn write_new(path: &Path, bytes: &[u8]) -> Result<()> {
let mut f = OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)?;
f.write_all(bytes)?;
f.sync_all()?;
Ok(())
}
pub fn key(path: &Path) -> Result<Keypair> {
if !path.exists() {
let mut seed = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut seed);
write_new(path, &seed)?;
}
let seed: [u8; 32] = std::fs::read(path)?
.try_into()
.map_err(|_| anyhow::anyhow!("invalid retained key"))?;
Ok(Keypair::from_seed(&seed))
}4. Restart and recover the original publication
Press Ctrl-C in the host terminal: the listener stops accepting and drains admitted calls. Start the same host with the same directory, then run the worker again with the newly written descriptor.
cargo run --locked --manifest-path ../rust/Cargo.toml --example publish \
< .chio/connection.json > resumed-receipt.json
cmp first-receipt.json resumed-receipt.json
chio receipt verify --input resumed-receipt.json \
--trusted-kernel-pubkey .chio/kernel.pubYou should see cmp succeed, the resumed receipt verify, and the resource still hold one mutation. The host revoked its old worker credential and issued a replacement, keeping the process identity and operation history. Keep the original key even after a socket timeout: a different process key or resource operation key asks for different work and leaves the first call’s uncertainty unresolved.
Your binary inherits the host’s operating decisions
Embedding hands your application the host’s operational contract along with its stores. The example keeps initialization separate from cleanup and favors inspection over deletion.
- Expired root capability. The host refuses it and issues no new identity on restart.
- Partial initialization, an invalid retained key, or an unexpected socket entry. Each requires inspection before anything is removed.
- Socket names. Fresh names avoid unlinking a path that may belong to another process after a crash.
The process-host CLI supplies run-plan supervision, ABI migration checks, and administrative runbooks. An embedded host owns the underlying runtime and a local tool, and it configures deployment, export and import, and sandboxing in its own code. Read execution security before hosting untrusted workers.