Chio/Docs
LOGIN · JOIN

Host & Worker Protocol

Initialize a process host, give each worker a private connection, and call governed tools as that worker's own process.

Guide chio@5f63e6e

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.

Start
Host and worker
WorkerOperator-owned hostsocket + credentialsignsauthoritydispatchaworkerholds no signing keyσkeys + configurationpprocess recordscredential selects identityKkerneladmits each calldtoolReceipt and outcome retained by the hostOperator-owned hostcredentialsignsauthoritydispatchaworkerno signing keyσkeyspprocess recordsKkerneldtoolReceipt and outcome retained here

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.

The worker authenticates with a private connection credential. Signing material stays with the host, which reconstructs the selected process authority before kernel admission.

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.

report/host.jsonjson
{
  "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.

ModeConnection arrives asLifecycle owner
process serveA private descriptor the operator issues for one socketYour service manager starts workers and restores input
process runOne worker-bootstrap JSON object on private stdin, then EOFChio binds commands, attempts, dependencies, and fresh credentials
Native container workerThe bootstrap plus the mounted socket inodeThe 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.

Connection fields a worker usesjson
{
  "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.

report/ · terminal 1, manual hostbash
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.sock

Initialization 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.

report/ · terminal 2bash
uv run python inspect-worker.py worker-connections/producer.json
inspect-worker.py · complete descriptor-file workerpython
"""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
inspect-worker.mjs · complete runner workerjavascript
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.

python/recipes.pytypescript/src/recipes.tsrust/client/examples/recipes.rs
export 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;
}

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.

Producer call · authenticated client and items suppliedProducer call · authenticated client and items supplied
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;
}

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.

  1. Save. Keep a private copy of the old descriptor.
  2. Revoke. Stop the host and revoke the process credentials.
  3. Reissue. Issue a new descriptor for a fresh private socket and restart serving.
  4. 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

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 shape · the credential is never loggedjson
{
  "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.

OperationRequired fieldsResult
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_revision decimal string, value
New revision and value, or checkpoint_conflict.
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

Two result envelopes · schematic, not captured outputjson
{"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 familyMeaningResponse
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_conflict
Logical request binding or state revision differs
Restore the original request or reconcile the current checkpoint.
limit_reached / response_too_large
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.

Next: let the host run the workers