Host & Worker Protocol
Initialize a process host, give each worker a private connection, and call governed tools as that worker's own process.
On this page
A worker holds one private connection to the process host and acts only as the process that connection names. The host keeps the signing keys, the capability, and kernel administration; the worker invokes its granted tools, inspects itself, checkpoints, and stores blobs.
The steps use the report project, which ships policy.yaml, host.json, and process.json. Its producer sends to a jobs mailbox that is native to Chio and its consumer receives from it, so the project runs without an external tool server or a launch policy.
A worker connection selects retained authority
The worker connects over a Unix socket with a private credential. That credential selects one retained process record, and the host reconstructs the process authority from it. Signing keys and configuration never enter the worker’s descriptor.
Drawn fromreport/run.py
1. Initialize the host around the report's tools
The policy grants the root the tools it may delegate. host.json creates the jobs mailbox and gives each child a narrower role: the producer can send, and the consumer can receive and acknowledge. This is the complete file.
{
"schema": "chio.process.host.v1",
"policy": "policy.yaml",
"mailboxes": [
{
"id": "jobs"
}
],
"limits": {
"max_processes": 3,
"max_depth": 1,
"max_calls": 10
},
"children": [
{
"id": "producer",
"parent": "root",
"budget_share_bps": 5000,
"tools": [
{
"server_id": "chio-ipc",
"tool_name": "send_jobs"
}
]
},
{
"id": "consumer",
"parent": "root",
"budget_share_bps": 5000,
"tools": [
{
"server_id": "chio-ipc",
"tool_name": "receive_jobs"
},
{
"server_id": "chio-ipc",
"tool_name": "ack_jobs"
}
]
}
]
}You should see two children under one root, each with half the root's budget share and only its own mailbox tools. For a trusted local MCP tool, the AI SDK project supplies a portable host.v2 configuration instead, and chio process init --local-tools authorizes discovery and signs the selected local launch.
That local launch carries no OS confinement. Deployed hosts use an operator-signed launch policy with a pinned signer, described in verified tool launch.
2. Choose who launches workers
Three modes deliver a worker its connection. They differ in who owns the executable's lifecycle.
| Mode | Connection arrives as | Lifecycle owner |
|---|---|---|
process serve | A private descriptor the operator issues for one socket | Your service manager starts workers and restores input |
process run | One worker-bootstrap JSON object on private stdin, then EOF | Chio binds commands, attempts, dependencies, and fresh credentials |
| Native container worker | The bootstrap plus the mounted socket inode | The runner reconciles its exact container; the deployment sets isolation |
A serve worker reads the descriptor file the operator chose. A run worker reads bootstrap.connection and bootstrap.input from stdin. Log neither object: the connection holds a bearer secret and the input can hold application data. Keep the descriptor out of model messages, graph checkpoints, and tracing metadata.
{
"socket_path": "/run/chio/host.sock",
"credential": "<operator-issued bearer credential>"
}socket_path locates the serving host on a Unix-domain socket. credential selects the process identity on every request and carries no operator signing key. The host rebuilds the process's authority from its retained state and rechecks the credential before it delivers a result.
3. Issue a descriptor when your application launches workers
The runner delivers credentials on its own. An application that launches its own workers runs a separate host and issues each worker a private descriptor.
chio process init --config host.json --state .chio-manual
mkdir -m 700 worker-sockets worker-connections
chio process credential --state .chio-manual --process producer \
--socket worker-sockets/host.sock --out worker-connections/producer.json
chio process serve --state .chio-manual --socket worker-sockets/host.sockInitialization takes a new private state directory and binds the policy, the tool definitions, the kernel key, and the process identities; a failed initialization leaves no serving host. Stop the host before an administrative change or before issuing a descriptor for a new socket path. The exclusive host lock keeps a second serving owner from reconciling the same state.
uv run python inspect-worker.py worker-connections/producer.json"""Inspect one process through an operator-supplied private descriptor."""
import json
import sys
from pathlib import Path
from chio_process import ProcessClient
def inspect_process(descriptor):
connection = json.loads(descriptor.read_text())
client = ProcessClient(connection["socket_path"], connection["credential"])
snapshot = client.inspect()
return {"checkpoint": snapshot["checkpoint"], "storage": snapshot.get("storage")}
if __name__ == "__main__":
print(json.dumps(inspect_process(Path(sys.argv[1])), indent=2))You should see the producer's checkpoint and its advertised storage usage printed as JSON. The descriptor authenticated the worker as producer, and the host stays up in terminal 1.
The same inspection as a runner worker in Node
import { readFileSync } from "node:fs";
import { ProcessClient } from "@chio-protocol/process";
async function main() {
const bootstrap = JSON.parse(readFileSync(0, "utf8"));
if (bootstrap.schema !== "chio.process.worker-bootstrap.v1") {
throw new Error("Unsupported worker bootstrap");
}
const { socket_path: socketPath, credential } = bootstrap.connection;
const client = new ProcessClient(socketPath, credential);
const snapshot = await client.inspect();
console.log(JSON.stringify({
checkpoint: snapshot.checkpoint,
storage: snapshot.storage,
}, null, 2));
}
await main();4. Connect from Python, TypeScript, or Rust
Every worker client opens the same connection and inspects the same identity. These recipes come from the three-language recipe project, which runs each one against the same host protocol. The Rust recipe uses the client crate in that project and reads no trusted runtime journal.
def connect(bootstrap):
if bootstrap["schema"] != "chio.process.worker-bootstrap.v1":
raise ValueError("Unsupported worker bootstrap")
connection = bootstrap["connection"]
client = ProcessClient(connection["socket_path"], connection["credential"])
snapshot = client.inspect()
print(snapshot["process_id"], snapshot["checkpoint"]["revision"])
return clientexport async function connect(bootstrap: Bootstrap): Promise<ProcessClient> {
assert.equal(bootstrap.schema, "chio.process.worker-bootstrap.v1");
const { socket_path, credential } = bootstrap.connection;
const client = new ProcessClient(socket_path, credential);
const snapshot = await client.inspect();
console.log(snapshot.process_id, snapshot.checkpoint.revision);
return client;
}async fn connect(bootstrap: Bootstrap<Value>) -> Result<ProcessClient> {
let client = ProcessClient::new(bootstrap.connection, Duration::from_secs(60))?;
let snapshot = client.inspect().await?;
println!("{} {}", snapshot.process_id, snapshot.checkpoint.revision);
Ok(client)
}You should see the process ID and its checkpoint revision. Your language choice carries to every tabbed example in the docs.
5. Call a governed tool
The producer sends its batch through the kernel under the operation key send-order. Check the verdict, then the terminal state, before you keep the receipt.
async function sendBatch(client, items) {
const sent = await client.invoke(
"send-order",
"chio-ipc",
"send_jobs",
{ message_key: "order", payload: { items } },
);
if (sent.verdict !== "allow") throw new Error(sent.reason);
if (sent.terminal_state.state !== "completed") {
throw new Error("Send outcome is unresolved");
}
return sent.receipt_json;
}def send_batch(client, items):
sent = client.invoke(
"send-order",
"chio-ipc",
"send_jobs",
{"message_key": "order", "payload": {"items": items}},
)
if sent["verdict"] != "allow":
raise RuntimeError(sent["reason"])
if sent["terminal_state"]["state"] != "completed":
raise RuntimeError("Send outcome is unresolved")
return sent["receipt_json"]A kernel denial returns a signed result. A transport or authentication error can return no receipt, and neither client retries on its own. Keep receipt_json as its original text, because JavaScript number conversion can change signed integer fields, and verify it with the selected kernel public key.
A timeout leaves the send's outcome open. Resolve it by the same operation key, as Calls and recovery shows.
6. Rotate the credential and confirm the same process
Run this on a disposable serve host. It proves that identity lives in the host's records, never in the credential.
- Save. Keep a private copy of the old descriptor.
- Revoke. Stop the host and revoke the process credentials.
- Reissue. Issue a new descriptor for a fresh private socket and restart serving.
- Check. The old credential fails authentication. The new one inspects the same checkpoint and resolves the same logical operation to its original receipt.
Then send a malformed frame and an expired credential, one at a time. Each fails as a protocol error, never as a signed tool denial. The runner rotates credentials at startup and on every attempt; manual issuance belongs to the externally managed serve path.
The host holds the keys, and the deployment holds isolation
Four parties share a running process. Each owns a different part of it.
- Host. Private keys, process binding, tool manifests, worker credentials, and durable state.
- Kernel. Authority, admission, dispatch, guards, and signed receipts.
- Worker. Application logic, stable operation keys, and the reading of results.
- Deployment. OS isolation, the inherited environment, direct file and network access, and code identity.
Protocol authentication establishes which process made a mediated call. A same-user worker can still reach files and the network outside the worker protocol, because isolation comes from the host. When that matters, choose an isolated worker profile in process confinement.
Worker protocol reference
Try invoke, checkpoint_conflict, or 2 MiB.
Frame and limits
Each connection carries one UTF-8 JSON request followed by a newline. The request ceiling is 2 MiB including that newline, and the response ceiling is 8 MiB. The default listener admits at most 32 active connections, with five seconds to read the frame and five seconds to write the response.
Kernel execution has its own deadlines, so an SDK timeout can arrive while an admitted kernel call continues. Reconnect with the original operation identity after you diagnose the failure.
{
"protocol": "chio.process.v1",
"credential": "PRIVATE_BEARER",
"operation": {
"op": "inspect"
}
}Operations
The worker protocol has seven operations, including prepare_invocation. A worker acts on its own process with these six.
| Operation | Required fields | Result |
|---|---|---|
inspect | None | Own identity, lineage, limits, checkpoint, and advertised storage usage. |
invoke | operation_key, server_id, tool_name, arguments | Kernel decision, terminal state, output, and original receipt_json. |
checkpoint | expected_ | New revision and value, or checkpoint_ |
blob_put | sha256 and canonical padded data_base64 | Immutable digest and byte count. |
blob_read | sha256 | Owned bytes with digest and byte count. |
cancel | None | Permanently cancels this authenticated process subtree. |
Result envelopes
{"protocol":"chio.process.v1","ok":true,"result":{"verdict":"deny","output":null,"terminal_state":{"state":"completed"},"receipt_json":"ORIGINAL_SIGNED_TEXT"}}
{"protocol":"chio.process.v1","ok":false,"error":{"code":"unauthenticated"}}An ok envelope means the protocol operation produced a result. Whether the kernel allowed the tool, and whether the application succeeded, comes next: check verdict, terminal_state.state, output.kind, and the tool's own result schema, in that order.
The wire materializes a stream as kind: stream with a chunks array, delivered whole. Keep decimal-string revisions and mailbox sequence values as strings. Parse receipt_json only to inspect it, and pass its original text to a verifier.
Error codes
| Code family | Meaning | Response |
|---|---|---|
invalid_frame / invalid_request / frame_timeout | Frame or schema rejected | Fix encoding, size, or fields; keep authorization extensions intact. |
unauthenticated / cancelled | Connection or process cannot act | Check stopped-host administration and the original authority lifetime. |
conflict / checkpoint_ | Logical request binding or state revision differs | Restore the original request or reconcile the current checkpoint. |
limit_reached / response_ | A bounded resource cannot satisfy the request | Inspect quota or output shape; an effect may already exist. |
blob_missing / blob_corrupt | Retained application data cannot be read reliably | Preserve state; never regenerate decision history. |
runtime_error / SDK transport_error | No usable result delivered | Recover the original operation after diagnosing the host or transport. |