Write workers in Rust or TypeScript
Build the report workers in Rust or TypeScript, run them under the same host, and recover a producer that exits.
On this page
Pick the language your team ships; the process keeps the same identity, call history, and authority either way. Those belong to the host, so a compiled Rust worker and a typed TypeScript worker run one report under one contract.
Each producer sends three numbers, and each consumer checks the sender, computes the total, commits a checkpoint, and acknowledges the message. Download languages.zip for both projects, each with its own host and launch plans, plus the protocol recipes and the Rust resource and host projects. Use the installed Chio CLI on Linux. For two languages cooperating in one run, the mixed Python and Node starter is shorter.
Start1. Build the workers in one language
Each language is its own project, and neither build depends on the other. Open languages/typescript/ or languages/rust/.
| Application | Build | Run |
|---|---|---|
| TypeScript producer and consumer | Node 22 or newer, npm, and the project manifest | Linux, Chio, and Node |
| Rust producer and consumer | Cargo and the included lockfile | Linux and Chio |
| Three-language recipe check | Both builds and the Python project environment | A separate fresh host for each language |
npm ci
npm run buildcargo build --locked --bins --examples
cargo test --locked --all-targetsThe TypeScript source uses strict checking and validates tool output at runtime. A type describes the expected response, so both implementations still check the verdict, the completed operation, and the application data explicitly.
The Rust worker uses the included chio-process-client path dependency: a source-distributed worker client, separate from the trusted chio-process runtime crate. It connects through the host socket and receives only the authority attached to its credential. The build needs no registry publication.
2. Run the report once
From the project you built, initialize the included host and run its plan. Both projects use the same mailbox, grants, and role names, and each plan selects its own compiled workers.
chio process init --config host.json --state .chio
chio process run --state .chio --plan process.json{
"item_count": 3,
"total": 10
}You should see both workers complete once, and the consumer record a count of 3 and a total of 10.
3. Make the producer exit after its send
A separate recovery plan tells the producer to exit after its first send, so you can compare attempts without changing the normal plan.
chio process init --config host.json --state .chio-recovery
chio process run --state .chio-recovery --plan process-recovery.json
chio process status --state .chio-recoveryYou should see two producer attempts and one consumer attempt. The first producer saves its send receipt and exits with code 77. The second sends the exact original request and gets the original receipt back. The consumer waits for the producer to complete, so it sees the retained message once.
Compiling a worker adds no authority: the Rust producer's forbidden receive is refused exactly as the TypeScript producer's is. The independent qualification test compares the original receipt strings, verifies five distinct decisions, and checks the total.
4. Compare the two implementations
Each tab is the exact compiled source file. Choose a language once, and language tabs across the docs follow your choice where they offer it.
// Recovery and scope exercise; the mixed-language guide shows the application path.
import assert from "node:assert/strict";
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
ProcessClient,
type Json,
type ToolResult,
} from "@chio-protocol/process";
interface Input {
directory: string;
exercise_recovery: boolean;
role: "producer" | "consumer";
}
interface Bootstrap {
schema: string;
connection: { socket_path: string; credential: string };
input: Input;
attempt: number;
}
function record(value: Json): Record<string, Json> {
assert.ok(
value !== null && typeof value === "object" && !Array.isArray(value),
);
return value;
}
function completed(result: ToolResult): Record<string, Json> {
assert.equal(result.verdict, "allow");
assert.equal(result.terminal_state.state, "completed");
assert.ok(result.output?.kind === "value");
return record(result.output.value);
}
function readBootstrap(): Bootstrap {
const raw: unknown = JSON.parse(readFileSync(0, "utf8"));
assert.ok(raw && typeof raw === "object");
const bootstrap = raw as Bootstrap;
assert.equal(bootstrap.schema, "chio.process.worker-bootstrap.v1");
assert.equal(typeof bootstrap.connection.socket_path, "string");
assert.equal(typeof bootstrap.connection.credential, "string");
assert.equal(typeof bootstrap.input.directory, "string");
assert.equal(typeof bootstrap.input.exercise_recovery, "boolean");
assert.ok(Number.isSafeInteger(bootstrap.attempt) && bootstrap.attempt > 0);
return bootstrap;
}
const save = (directory: string, name: string, value: unknown): void =>
writeFileSync(join(directory, name), JSON.stringify(value), {
mode: 0o600,
flush: true,
});
function summarize(payload: Json) {
const items = record(payload).items;
assert.ok(Array.isArray(items) && items.length <= 16);
let total = 0;
for (const n of items) {
assert.ok(typeof n === "number" && Number.isSafeInteger(n) && n >= 0);
total += n;
}
assert.ok(Number.isSafeInteger(total));
return { item_count: items.length, total };
}
async function produce(client: ProcessClient, bootstrap: Bootstrap) {
const sent = await client.invoke("send-order", "chio-ipc", "send_jobs", {
message_key: "order",
payload: { items: [2, 3, 5] },
});
assert.equal(completed(sent).status, "sent");
const filename = `producer-${bootstrap.attempt}.json`;
save(bootstrap.input.directory, filename, {
send_receipt: sent.receipt_json,
});
if (bootstrap.input.exercise_recovery && bootstrap.attempt === 1)
process.exit(77);
const denied = await client.invoke(
"check-receive-scope",
"chio-ipc",
"receive_jobs",
{ after_sequence: "0" },
);
assert.equal(denied.verdict, "deny");
assert.equal(denied.output, null);
save(bootstrap.input.directory, filename, {
send_receipt: sent.receipt_json,
scope_receipt: denied.receipt_json,
});
}
async function consume(client: ProcessClient, bootstrap: Bootstrap) {
const denied = await client.invoke(
"check-send-scope",
"chio-ipc",
"send_jobs",
{ message_key: "forbidden", payload: { items: [] } },
);
assert.equal(denied.verdict, "deny");
assert.equal(denied.output, null);
const received = await client.invoke(
"receive-order",
"chio-ipc",
"receive_jobs",
{ after_sequence: "0", limit: 1 },
);
const value = completed(received);
assert.equal(value.status, "received");
assert.ok(Array.isArray(value.messages) && value.messages.length === 1);
const message = record(value.messages[0]!);
assert.equal(message.sender, "producer");
const result = summarize(message.payload!);
const snapshot = await client.inspect();
if (snapshot.checkpoint.revision === "0")
await client.checkpoint("0", result);
else assert.deepEqual(snapshot.checkpoint.value, result);
assert.ok(typeof message.sequence === "string");
const ack = await client.invoke("ack-order", "chio-ipc", "ack_jobs", {
through_sequence: message.sequence,
});
assert.equal(completed(ack).status, "acknowledged");
save(bootstrap.input.directory, "consumer.json", {
result,
receipts: [denied.receipt_json, received.receipt_json, ack.receipt_json],
});
}
const bootstrap = readBootstrap();
const client = new ProcessClient(
bootstrap.connection.socket_path,
bootstrap.connection.credential,
);
switch (bootstrap.input.role) {
case "producer":
await produce(client, bootstrap);
break;
case "consumer":
await consume(client, bootstrap);
break;
default:
throw new Error("Unknown worker role");
}//! Recovery and scope exercise. See the mixed-language guide for the application path.
use anyhow::{ensure, Context, Result};
use chio_process_client::{Bootstrap, ProcessClient, Verdict};
use serde::Deserialize;
use serde_json::{json, Value};
use std::{
fs::OpenOptions,
io::Write,
path::{Path, PathBuf},
time::Duration,
};
#[derive(Deserialize)]
struct Input {
directory: PathBuf,
exercise_recovery: bool,
role: String,
}
fn save(path: &Path, value: Value) -> Result<()> {
use std::os::unix::fs::OpenOptionsExt;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)?;
file.write_all(serde_json::to_string(&value)?.as_bytes())?;
file.sync_all()?;
Ok(())
}
fn summarize(payload: &Value) -> Result<Value> {
let items = payload["items"]
.as_array()
.context("items must be an array")?;
ensure!(items.len() <= 16);
let mut total = 0u64;
for item in items {
let n = item.as_u64().context("nonnegative integer required")?;
total = total.checked_add(n).context("sum overflow")?;
ensure!(
total <= 9_007_199_254_740_991,
"cross-language safe integer required"
);
}
Ok(json!({"item_count": items.len(), "total": total}))
}
async fn produce(client: &ProcessClient, input: &Input, attempt: u64) -> Result<()> {
let sent = client
.invoke(
"send-order",
"chio-ipc",
"send_jobs",
json!({"message_key":"order","payload":{"items":[2,3,5]}}),
None,
)
.await?;
ensure!(sent.completed_value().context("send did not complete")?["status"] == "sent");
let path = input.directory.join(format!("producer-{}.json", attempt));
save(&path, json!({"send_receipt":sent.receipt_json}))?;
// The attempt identifies a worker launch, never a logical operation.
if input.exercise_recovery && attempt == 1 {
std::process::exit(77);
}
let denied = client
.invoke(
"check-receive-scope",
"chio-ipc",
"receive_jobs",
json!({"after_sequence":"0"}),
None,
)
.await?;
ensure!(matches!(denied.verdict, Verdict::Deny) && denied.output.is_none());
save(
&path,
json!({"send_receipt":sent.receipt_json,"scope_receipt":denied.receipt_json}),
)?;
Ok(())
}
async fn consume(client: &ProcessClient, input: &Input) -> Result<()> {
let denied = client
.invoke(
"check-send-scope",
"chio-ipc",
"send_jobs",
json!({"message_key":"forbidden","payload":{"items":[]}}),
None,
)
.await?;
ensure!(matches!(denied.verdict, Verdict::Deny) && denied.output.is_none());
let received = client
.invoke(
"receive-order",
"chio-ipc",
"receive_jobs",
json!({"after_sequence":"0","limit":1}),
None,
)
.await?;
let value = received
.completed_value()
.context("receive did not complete")?;
ensure!(value["status"] == "received");
let messages = value["messages"].as_array().context("missing messages")?;
ensure!(messages.len() == 1 && messages[0]["sender"] == "producer");
let result = summarize(&messages[0]["payload"])?;
let snapshot = client.inspect().await?;
if snapshot.checkpoint.revision == "0" {
client.checkpoint("0", result.clone()).await?;
} else {
ensure!(
snapshot.checkpoint.value == result,
"checkpoint belongs to different work"
);
}
let ack = client
.invoke(
"ack-order",
"chio-ipc",
"ack_jobs",
json!({"through_sequence":messages[0]["sequence"]}),
None,
)
.await?;
ensure!(ack.completed_value().context("ack did not complete")?["status"] == "acknowledged");
save(
&input.directory.join("consumer.json"),
json!({"result":result,"receipts":[denied.receipt_json,received.receipt_json,ack.receipt_json]}),
)?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
let bootstrap = Bootstrap::<Input>::read(std::io::stdin().lock())?;
let client = ProcessClient::new(bootstrap.connection, Duration::from_secs(60))?;
match bootstrap.input.role.as_str() {
"producer" => produce(&client, &bootstrap.input, bootstrap.attempt).await,
"consumer" => consume(&client, &bootstrap.input).await,
_ => anyhow::bail!("unknown worker role"),
}
}In both, connection.socket_path names the host socket and connection.credential selects the retained process. The attempt number describes one execution and stays out of every durable operation key. produce and consume hold the two roles, and summarize holds the calculation.
These are instrumented contract exercises: the deliberate scope refusals and the recovery flag make all five signed decisions observable. The mixed-language application keeps that instrumentation in separate exercise entry points, and its report system figure draws the same restart.
5. Check the recipes in all three languages
The connection, checkpoint, blob, and mailbox recipes come from executable files in this archive. The included run-recipes.py starts a separate native host for each language and runs the recipe functions against it. Build both projects first, and pass the Python interpreter that has the matching client installed.
uv sync --project python
uv run --project python python run-recipes.py \
--python python/.venv/bin/python --state recipe-checkThe driver provisions identical hosts and runs the Python, TypeScript, and Rust versions. It checks original send and renewal receipts, reentrant checkpoint behavior, blob bytes, and sender attribution. Its self-send scenario uses one process as sender and receiver; the report application supplies the stronger producer and consumer separation.
6. Verify the records that explain the recovery
Read the consumer checkpoint from the stopped host, then the log of each producer attempt.
chio process state --state .chio-recovery --process consumer
chio process logs --state .chio-recovery --process producer --attempt 1
chio process logs --state .chio-recovery --process producer --attempt 2You should see the same checkpoint after a completed rerun. The workers and the qualification driver keep the records behind that result:
producer-1.jsonandproducer-2.jsoncarry the same original send receipt across two attempts.consumer.jsonholds the application result and the consumer's three decision receipts.report.jsonrecords worker attempts and runner completion. Completion alone does not validate the sum.kernel.pubandverification.jsonhold the key selected at host initialization and the independent receipt verification.files.sha256andplan.jsonfix the application files and the original scheduling plan used for replay.
New work needs an intentionally new run with fresh operation identities. Editing the plan never makes an existing run execute again.