Chio/Docs
LOGIN · JOIN

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.

Guide chio@5f63e6e

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.

Start

1. 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/.

ApplicationBuildRun
TypeScript producer and consumerNode 22 or newer, npm, and the project manifestLinux, Chio, and Node
Rust producer and consumerCargo and the included lockfileLinux and Chio
Three-language recipe checkBoth builds and the Python project environmentA separate fresh host for each language
languages/typescript/languages/rust/
npm ci
npm run build

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

Either language project · normal runbash
chio process init --config host.json --state .chio
chio process run --state .chio --plan process.json
Required result in consumer.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.

Either language project · recoverybash
chio process init --config host.json --state .chio-recovery
chio process run --state .chio-recovery --plan process-recovery.json
chio process status --state .chio-recovery

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

typescript/src/report.tsrust/report/src/main.rs
// 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");
}

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.

languages/ · all language builds requiredbash
uv sync --project python
uv run --project python python run-recipes.py \
  --python python/.venv/bin/python --state recipe-check

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

Either language project · retained workbash
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 2

You should see the same checkpoint after a completed rerun. The workers and the qualification driver keep the records behind that result:

  • producer-1.json and producer-2.json carry the same original send receipt across two attempts.
  • consumer.json holds the application result and the consumer's three decision receipts.
  • report.json records worker attempts and runner completion. Completion alone does not validate the sum.
  • kernel.pub and verification.json hold the key selected at host initialization and the independent receipt verification.
  • files.sha256 and plan.json fix 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.

Next: resolve an uncertain call