Run your first process
Run a Python producer and a Node consumer under one process host, then stop a worker and keep the handoff.
On this page
Two workers in two languages hand one batch through a durable mailbox, and the work survives when a worker stops halfway. The host keeps each process, its grant, and its calls; a worker is one attempt at running that process.
A Python producer sends [2, 3, 5]. A Node consumer totals the batch, checkpoints the result, and acknowledges the message. Download report.zip, extract it, and run it from report/ on Linux with Python 3.11 or newer, Node 22 or newer, uv, and the installed Chio CLI. No model or API key is involved.
The batch crosses the mailbox once and totals ten
uv sync
npm ci
chio process init --config host.json --state .chio
chio process run --state .chio --plan process.json
cat .chio/result.json{
"item_count": 3,
"total": 10
}You should see a count of 3 and a total of 10. host.json declares the mailbox and each worker's grants, and process.json declares the commands, the input, and the order. chio process init creates private host state in .chio/ and saves the host's public key there as kernel.pub.
Run the same process run command again. Completed workers keep their original attempts, so nothing runs twice. The checkpoint is the recovery record; result.json is an export for you to read.
One host runs both workers under one root grant
Each worker is an attempt at a process the host owns. The process holds the grant, the checkpoint, and the record of every named call; the worker holds a credential that selects it.
One message, sent once, saved before it is retired
The producer sends [2, 3, 5] under the key send-order. Its first attempt exits with code 77; the second repeats send-order and gets the original receipt and the same message back. The consumer checkpoints count 3 and total 10 before ack-order retires the message. Calls and recovery.
report.zip: the root grant κ, processes p₁ and p₂, the jobs mailbox m behind the kernel, and the receipts ρ it signs. The exit and the two refusals come from the instrumented recovery workers; the host journal and the tool server that holds the mailbox are left out.Root gives each child half of its delegation share, and both children draw from one ten-call ceiling. The shares are authority allocations and do not reserve five calls each. The application makes three distinct tool calls, and a replay of a known call returns its original decision.
Work started through the Computer facade, such as fixer.run(...) in the Quickstart, runs on the same process model. This example drives the process host directly, so each record it keeps is a file you can open.
The report is one function that knows nothing about Chio
The application logic accepts up to 16 nonnegative integers, checks that their sum is a safe integer, and returns the count and total. Process credentials, retries, and receipt verification live in the workers around it.
/** Application logic: a bounded batch of nonnegative integers. */
export function summarize(items) {
if (!Array.isArray(items) || items.length > 16) {
throw new TypeError("A batch must contain at most 16 items");
}
if (!items.every((item) => Number.isSafeInteger(item) && item >= 0)) {
throw new TypeError("Items must be nonnegative safe integers");
}
const total = items.reduce((sum, item) => sum + item, 0);
if (!Number.isSafeInteger(total)) {
throw new RangeError("Batch total exceeds the safe integer range");
}
return { item_count: items.length, total };
}The rest of the project connects that function to the host:
producer.pysends one batch.consumer.mjsreceives, calculates, checkpoints, and acknowledges.outcomes.mjschecks decisions and mailbox results.host.jsondeclares the mailbox and the worker grants.process.jsondeclares the commands, the input, and the dependency order.policy.yamldeclares the root authority and durable admission.
The producer sends under a key that outlives every attempt
The producer names its logical send send-order and its mailbox message order. Both stay the same on every attempt, so a replacement worker that repeats the send resolves the call the first attempt made.
"""Send one batch to the report consumer."""
import json
import sys
from chio_process import ProcessClient
from artifacts import save_receipt
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(f"Send refused: {sent.get('reason')}")
if sent["terminal_state"]["state"] != "completed":
raise RuntimeError("Send outcome is uncertain; retain send-order")
if sent["output"]["value"]["status"] != "sent":
raise RuntimeError("Mailbox did not accept the batch")
return sent
def main():
bootstrap = json.load(sys.stdin)
if bootstrap["schema"] != "chio.process.worker-bootstrap.v1":
raise RuntimeError("Unsupported worker bootstrap")
connection = bootstrap["connection"]
client = ProcessClient(connection["socket_path"], connection["credential"])
sent = send_batch(client, bootstrap["input"]["items"])
save_receipt(bootstrap["input"]["directory"], sent["receipt_json"])
# The host captures stdout for this attempt. Keep signed text unchanged.
print(sent["receipt_json"])
if __name__ == "__main__":
main()The worker accepts only an allow verdict, a completed terminal state, and a sent mailbox status. Anything else stops it with send-order retained for recovery. It prints the signed receipt text unchanged, because the host captures stdout for each attempt.
The consumer saves the result before it acknowledges the message
The consumer receives the batch, calls summarize, and checkpoints the result. Only then does it acknowledge the returned sequence, in a separate operation.
const { message, receipt } = await receiveBatch(client);
const result = summarize(message.payload.items);
await saveResult(client, result);
const acknowledged = await client.invoke(
"ack-order",
"chio-ipc",
"ack_jobs",
{ through_sequence: message.sequence },
);
completedValue(acknowledged, "acknowledged");A restart between the checkpoint and the acknowledgement repeats the original receive, compares the saved result, and finishes the acknowledgement. A denial, an uncertain outcome, an unexpected sender, or a conflicting checkpoint stops the worker. The example's completedValue function checks the kernel verdict, the terminal state, and the mailbox status.
Read consumer.mjs
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { isDeepStrictEqual } from "node:util";
import { pathToFileURL } from "node:url";
import { ProcessClient } from "@chio-protocol/process";
import { saveReceipts } from "./artifacts.mjs";
import { summarize } from "./report.mjs";
import { completedValue } from "./outcomes.mjs";
async function receiveBatch(client) {
const received = await client.invoke(
"receive-order",
"chio-ipc",
"receive_jobs",
{ after_sequence: "0", limit: 1 },
);
const { messages } = completedValue(received, "received");
if (messages.length !== 1 || messages[0].sender !== "producer") {
throw new Error("Expected one batch from producer");
}
return { message: messages[0], receipt: received.receipt_json };
}
async function saveResult(client, result) {
const { checkpoint } = await client.inspect();
if (checkpoint.revision === "0") {
await client.checkpoint("0", result);
} else if (!isDeepStrictEqual(checkpoint.value, result)) {
throw new Error("Retained result differs from this batch");
}
}
export async function consumeBatch(client) {
const { message, receipt } = await receiveBatch(client);
const result = summarize(message.payload.items);
// Persist the work before retiring its delivery. These are separate commits.
await saveResult(client, result);
const acknowledged = await client.invoke(
"ack-order",
"chio-ipc",
"ack_jobs",
{ through_sequence: message.sequence },
);
completedValue(acknowledged, "acknowledged");
return { result, receipts: [receipt, acknowledged.receipt_json] };
}
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 { result, receipts } = await consumeBatch(client);
// A local export for the reader; the checkpoint is the recovery record.
writeFileSync(
join(bootstrap.input.directory, "result.json"),
`${JSON.stringify(result, null, 2)}\n`,
{ mode: 0o600, flush: true },
);
saveReceipts(bootstrap.input.directory, receipts);
for (const receipt of receipts) console.log(receipt);
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
await main();
}The host grants each worker only its own mailbox operations
The host declaration fixes the tree before any worker starts: one mailbox, at most three processes at depth one, ten calls, and two children of root with the tools each may call.
{
"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"
}
]
}
]
}In process.json the consumer depends on the producer, so its first receive happens after a message has been sent. An early empty receive would itself be a retained result under receive-order, and repeating that key would return empty again. A polling application uses successive poll identities.
The plan runs two workers at a time, each with at most two attempts and a 30-second timeout. The policy caps capability lifetime at 3,600 seconds, limits delegation depth to 1, and makes every admission durable.
Read process.json and policy.yaml
{
"schema": "chio.process.run.v2",
"max_parallel": 2,
"workers": [
{
"process": "producer",
"command": [
".venv/bin/python",
"producer.py"
],
"cwd": ".",
"input": {
"directory": ".chio",
"items": [
2,
3,
5
]
},
"depends_on": [],
"max_attempts": 2,
"timeout_seconds": 30
},
{
"process": "consumer",
"command": [
"node",
"consumer.mjs"
],
"cwd": ".",
"input": {
"directory": ".chio",
"items": [
2,
3,
5
]
},
"depends_on": [
"producer"
],
"max_attempts": 2,
"timeout_seconds": 30
}
]
}kernel:
max_capability_ttl: 3600
delegation_depth_limit: 1
durable_admission_mode: all
capabilities:
default:
tools:
- server: chio-ipc
tool: '*'
operations: [invoke, delegate]
ttl: 3600A producer that exits after its send gets the original receipt back
The included process-recovery.json runs the same send, receive, checkpoint, and acknowledgement through instrumented workers in exercises/ that call the same application functions. The producer exits with code 77 right after it saves its send receipt, and each worker also attempts one operation outside its grant. Fresh state keeps the normal run available for comparison.
chio process init --config host.json --state .chio-recovery
chio process run --state .chio-recovery --plan process-recovery.json
uv run python check-result.py .chio-recoveryReport total and original handoff verified
You should see two producer attempts, one consumer attempt, and the same total of ten. The replacement producer repeated send-order, so the kernel returned the original receipt and the original message. The consumer received one batch.
The run records five distinct decisions: send, receive, acknowledgement, and two scope refusals. Repeating the run adds no attempts, and the replayed send receipt is retained again with its original bytes.
Captured recovery run · selected fields
{
"result": {
"item_count": 3,
"total": 10
},
"runner": {
"schema": "chio.process.run-report.v1",
"complete": true,
"pending_container_records": 0,
"abandoned_socket_intents": 0,
"workers": [
{
"process": "consumer",
"state": "completed",
"attempts": 1,
"suspensions": 0,
"outcome": "exit_0",
"peak_resident_bytes": 48365568,
"cpu_ms": 43
},
{
"process": "producer",
"state": "completed",
"attempts": 2,
"suspensions": 0,
"outcome": "exit_0",
"peak_resident_bytes": 25448448,
"cpu_ms": 88
}
]
},
"verification": {
"schema": "chio.receipt.signatures.v1",
"receipts_verified": 5,
"trusted_kernel_key": "0154a3c0e5e75d860c7298bbc1868a59477853c6a0324dba2914c4e46431cfd7",
"checks": [
"signature",
"signer_pin",
"action_parameter_hash"
]
},
"known_handoff_replayed": true
}The receipts verify against the key the host saved
chio receipt verify --input .chio-recovery/receipts.ndjson \
--trusted-kernel-pubkey .chio-recovery/kernel.pub
chio process status --state .chio-recovery
chio process logs --state .chio-recovery --process producer --attempt 1The verifier checks each signature, the pinned signer, and the action parameter hash. The arithmetic and the completeness of the export are check-result.py's job: it checks the total, then compares the send receipt across both attempts as text, since parsing and rebuilding its JSON could change signed bytes.
"""Check the report recovery exports without parsing and rewriting signed text."""
import json
import sys
from pathlib import Path
def verify_handoff(state):
result = json.loads((state / "result.json").read_text())
assert result == {"item_count": 3, "total": 10}
attempts = [json.loads(path.read_text()) for path in sorted(state.glob("producer-*.json"))]
assert len(attempts) == 2
assert attempts[0]["send_receipt"] == attempts[1]["send_receipt"]
receipts = (state / "receipts.ndjson").read_text().splitlines()
assert len(receipts) == 6 and len(set(receipts)) == 5
if __name__ == "__main__":
verify_handoff(Path(sys.argv[1]))
print("Report total and original handoff verified")When a run stops, keep its state directory and read the status and attempt logs before resuming. The example's grants last one hour and are not renewed. A changed artifact, an expired grant, or a partially initialized host needs diagnosis, and deleting the journal discards the evidence needed to resolve an uncertain call.
These workers are trusted programs under one Linux account. Tool grants restrict kernel-mediated calls, while direct filesystem and network access follow the account's permissions. Run untrusted programs under process confinement.
A new batch is a new run with its own state
The included process-changed.json sends [4, 6, 8]. Give the new work its own state directory.
chio process init --config host.json --state .chio-changed
chio process run --state .chio-changed --plan process-changed.json
cat .chio-changed/result.json{
"item_count": 3,
"total": 18
}You should see a total of 18. To try another batch, edit that plan, choose a fresh state directory, and update its output directory. The host refuses a changed plan against existing state before it starts any worker: a changed request is new work, and the original message stays as it was.
Five experiments cover the contract, each on fresh state:
| Experiment | Run or change | You should see |
|---|---|---|
| Clean | process.json on fresh .chio | One producer attempt; count 3, total 10; three decisions |
| Crash after send | process-recovery.json on fresh .chio-recovery | Two producer attempts; identical send_receipt strings |
| Completed replay | The exact recovery command, repeated | Same attempt counts, result, and original receipt text |
| Authority refusal | The instrumented workers in exercises/ | Producer cannot receive; consumer cannot send; no output |
| Changed request | New items in a completed run's plan | Plan refused before workers start; message unchanged |