Chio/Docs
LOGIN · JOIN

Build Systems in Rust

Run the decision laboratory, then follow one Rust invariant per program from borrowed input to verified accounting.

Guide chio@5f63e6e

On this page

Chio decides whether an operation may run. Your Rust application owns the workers, stores, and recovery around that decision, and these programs show how to make each responsibility visible in the type system.

Each program does useful work and isolates one systems invariant: a borrowed decision, a bounded guard, a database owner, a cancellable task, a typed job, an atomic publication, and a checked bill. Source excerpts come directly from the downloadable projects, which carry their manifests, lockfiles, and tests.

Start

1. Run the decision laboratory

Download the laboratory, extract it, and run it with Rust 1.94.1. A fixed clock and public test keys make every decision reproducible, and no API key or Chio service is required.

Terminalbash
cd systems-lab
cargo run --locked
cargo test --locked
cargo check --locked --no-default-features --lib

You should see three decisions on one evaluation path: granted analysis, a request outside the grant, and expired authority. No handler runs in the laboratory, so change the clock or the operation independently to isolate the authority check you want to study.

systems-lab/src/lib.rsL100–111rust
    pub fn evaluate(&self, clock: &dyn Clock) -> EvaluationVerdict {
        let guard = DocumentLimit;
        evaluate(EvaluateInput {
            request: &self.request,
            capability: &self.capability,
            trusted_issuers: core::slice::from_ref(&self.issuer),
            clock,
            guards: &[&guard],
            session_filesystem_roots: None,
        })
    }
}

Open the complete source file

The case owns the request, the signed capability, and the trusted issuer list. Evaluation borrows them for one call, and the host chooses the clock and guard implementations it lends. These lifetimes keep the evaluation input from outliving its owners; a compile-fail rustdoc test exercises that restriction.

The laboratory’s main program and its cost
systems-lab/src/main.rsL1–22rust
use chio_kernel_core::FixedClock;
use chio_systems_lab::{DecisionCase, EXPIRES_AT, ISSUED_AT};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut case = DecisionCase::new("Review the document under this worker's authority.")?;
    for (label, time, operation) in [
        ("granted analysis", ISSUED_AT + 1, "analyze"),
        ("outside grant", ISSUED_AT + 1, "delete"),
        ("expired authority", EXPIRES_AT + 1, "analyze"),
    ] {
        case.request.tool_name = operation.into();
        let result = case.evaluate(&FixedClock::new(time));
        println!(
            "{label}: {:?}; matched grant: {:?}; reason: {}",
            result.verdict,
            result.matched_grant_index,
            result.reason.as_deref().unwrap_or("checks passed")
        );
    }
    println!("This program evaluates decisions. It performs no tool dispatch, quota mutation or receipt persistence.");
    Ok(())
}

Open the complete source file

Constructing the case allocates owned strings, a grant vector, and JSON input. Borrowing the evaluation input avoids cloning those objects to pass them into the core; parsing, cryptography, and the evaluation itself still allocate.

To measure a declared workload, run the benchmark in the laboratory. Its JSON report separates signing, verification, portable admission, SQLite commits, and artifact publication, with p50, p95, and p99 durations, after 100 warm-up iterations per operation.

Terminalbash
CHIO_BENCH_ITERATIONS=2000 cargo bench --locked --bench decision-cost > decision-cost.json

The report records the compiler, operating system, architecture, logical CPU count, document size, grant count, and guard count. Every row runs at concurrency one. Storage rows declare WAL/FULL synchronization or synchronized file publication, and core rows touch no store or protected effect.

For the full HTTP adapter path, build pricing-rust (chio new pricing-rust --example pricing-rust) and run its included measure.py with the service and receipt-verifier binaries. It checks issued quotes, refusals, restart, and signature verification, then reports throughput and tail latency at three concurrency levels. Keep workload, durability, and hardware metadata beside every result: a signature microbenchmark measures signing, and application throughput needs the full path.

2. Give a guard bounded work

The laboratory’s guard accepts a nonempty UTF-8 document of at most 512 bytes. A Rust string already knows its byte length, so the bound needs no scan and no allocation.

systems-lab/src/lib.rsL34–50rust
pub struct DocumentLimit;
impl Guard for DocumentLimit {
    fn name(&self) -> &str {
        "document-bytes"
    }
    fn evaluate(&self, context: &GuardContext<'_>) -> Result<Verdict, KernelCoreError> {
        let text = context
            .request
            .arguments
            .get("text")
            .and_then(serde_json::Value::as_str);
        Ok(match text {
            Some(text) if !text.is_empty() && text.len() <= MAX_DOCUMENT_BYTES => Verdict::Allow,
            _ => Verdict::Deny,
        })
    }
}

Open the complete source file

The guard returns a decision and leaves the document unopened and unanalyzed. A Unicode boundary test distinguishes bytes from characters. The library builds with no_std + alloc when default features are disabled, while the CLI and benchmarks use the standard library. Check the portable target:

Terminalbash
rustup target add wasm32-unknown-unknown --toolchain 1.94.1
cargo check --locked --no-default-features --lib --target wasm32-unknown-unknown

You should see the library compile for wasm32-unknown-unknown. That establishes compilation for the target; a browser host also supplies its clock, storage, and execution integration, as Browser Kernel shows.

3. Own the blocking boundary

The embedded pricing service records every quote it issues through a governed HTTP API, served by the Tower layer. One blocking thread owns its SQLite connection, and a bounded mailbox carries typed commands and one-shot replies across that boundary.

src/database.rs · impl Databaserust
impl Database {
    pub fn open(path: &Path) -> anyhow::Result<Self> {
        let database = rusqlite::Connection::open(path)?;
        database.busy_timeout(std::time::Duration::from_millis(500))?;
        database.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; CREATE TABLE IF NOT EXISTS quotes(id INTEGER PRIMARY KEY, sku TEXT NOT NULL, percent REAL NOT NULL, price TEXT NOT NULL);")?;
        let (sender, mut receiver) = mpsc::channel(16);
        let thread = std::thread::Builder::new()
            .name("quotes-database".into())
            .spawn(move || {
                while let Some(command) = receiver.blocking_recv() {
                    match command {
                        Command::List(reply) => {
                            let _ = reply.send(list(&database));
                        }
                        Command::Insert(sku, percent, price, reply) => {
                            let result = database
                                .execute(
                                    "INSERT INTO quotes(sku, percent, price) VALUES (?1, ?2, ?3)",
                                    rusqlite::params![&sku, percent, &price],
                                )
                                .map(|_| Quote {
                                    id: database.last_insert_rowid(),
                                    sku,
                                    percent,
                                    price,
                                    currency: "USD",
                                })
                                .map_err(Error::from);
                            // A disconnected waiter does not roll back a committed insert.
                            let _ = reply.send(result);
                        }
                        Command::Stop(reply) => {
                            let _ = reply.send(());
                            break;
                        }
                    }
                }
            })?;
        Ok(Self {
            sender,
            thread: Arc::new(Mutex::new(Some(thread))),
        })
    }

A full mailbox returns an explicit overload response. A submitted insert owns its transaction even if the HTTP caller disconnects, so a lost receiver leaves a recorded quote recorded. Admitted requests own their receipt retention: if retention fails after the insert, the service reports that failure and leaves the effect available for inspection.

SQLite runs with WAL and FULL synchronization. Application data and receipt files are separate stores, each with its own transaction. The reopen test checks persisted data after a normal stop; a power-loss guarantee needs a filesystem and storage-device failure test.

4. Keep ownership through cancellation

The repricing host admits four active jobs. Each owned task holds its capacity permit and session through finalization. SIGINT or SIGTERM closes admission, requests cancellation, joins workers, and drains submitted file writes before the host stores are dropped.

rust-runtime/src/tasks.rsL60–93rust
    pub async fn drain(&self, timeout: Duration) -> Result<()> {
        let (mut tasks, mut failure) = {
            let mut state = self
                .0
                .lock()
                .map_err(|_| anyhow!("task registry poisoned"))?;
            state.closed = true;
            (std::mem::take(&mut state.tasks), state.failure.take())
        };
        let deadline = Instant::now() + timeout;
        while !tasks.is_empty() {
            match tokio::time::timeout_at(deadline, tasks.join_next()).await {
                Ok(Some(result)) => {
                    if let Err(error) = result.map_err(anyhow::Error::from).and_then(|value| value)
                    {
                        failure.get_or_insert_with(|| error.to_string());
                    }
                }
                Ok(None) => break,
                Err(_) => {
                    tasks.abort_all();
                    while tasks.join_next().await.is_some() {}
                    anyhow::bail!(
                        "shutdown deadline reached; unfinished effects require reconciliation"
                    );
                }
            }
        }
        if let Some(error) = failure {
            anyhow::bail!("background work failed: {error}");
        }
        Ok(())
    }
}

Open the complete source file

The task group retains join handles and reports worker failure. Reaching the drain deadline aborts outstanding async tasks and returns an error. Blocking work takes its capacity permit inside the blocking closure, so abandoning its async waiter admits no blocking work beyond the configured limit.

Cancellation stops further work. A partial price sheet and a sheet whose acknowledgement was lost remain effects, and the repricing host keeps the stable job ID so recovery can inspect the request, receipt, and sheet together.

5. Express legal states in the type

The persistent work queue claims report sections and reconciles lost acknowledgements. A running job carries its claim owner and token, and completion, refusal, and unresolved execution each retain an outcome.

agentic-os/source/worker-fleet/src/jobs.rsL13–49rust
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum JobState {
    Queued,
    Claimed {
        #[serde(flatten)]
        lease: Claim,
    },
    Running {
        #[serde(flatten)]
        lease: Claim,
    },
    Completed {
        #[serde(flatten)]
        lease: Claim,
        outcome: Value,
    },
    Refused {
        #[serde(flatten)]
        lease: Claim,
        outcome: Value,
    },
    Unresolved {
        #[serde(flatten)]
        lease: Claim,
        outcome: Value,
    },
    CancelledBeforeDispatch,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Job {
    pub operation_id: Uuid,
    pub input: Value,
    #[serde(flatten)]
    pub state: JobState,
}

Open the complete source file

agentic-os/source/worker-fleet/src/jobs.rsL70–82rust
    pub fn begin(&mut self, owner: &str, claim: Uuid) -> Result<()> {
        let JobState::Claimed { lease } = &self.state else {
            anyhow::bail!("claim is no longer executable; inspect the retained operation");
        };
        ensure!(
            lease.owner == owner && lease.claim == claim,
            "claim belongs to another worker"
        );
        self.state = JobState::Running {
            lease: lease.clone(),
        };
        Ok(())
    }

Open the complete source file

Each transition compares the presented claim with the retained owner before execution, so a claim has one owner. Tests cover stolen claims, replay, malformed state, cancellation, and restart recovery. On restart a running job becomes unresolved, and the operator compares the published artifact’s operation ID and input digest with the retained job: a match resolves completion, and a missing artifact leaves the job unresolved.

6. Publish complete artifacts

The software factory prepares and publishes a tested release, with approval bound to the candidate. It writes into a private staging directory, synchronizes complete files, then publishes with an atomic rename that refuses an existing destination.

rust-runtime/src/files.rsL27–39rust
pub fn read_bounded(path: &Path, limit: usize) -> io::Result<Vec<u8>> {
    let file = open_read(path)?;
    let mut bytes = Vec::with_capacity(limit.min(8192));
    file.take((limit as u64).saturating_add(1))
        .read_to_end(&mut bytes)?;
    if bytes.len() > limit {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "file exceeds the input limit",
        ));
    }
    Ok(bytes)
}

Open the complete source file

Concurrent publishers each keep their own release, and ordinary errors remove staging; a hard crash can leave an unpublished staging directory for recovery. Input limits apply to the opened handle, including a file a writer grows after opening. Unix reads refuse a symlink in the final path component and require a regular file. The application owns and trusts the ancestor directories, and access through an attacker-controlled directory tree is outside the contract.

A failure while synchronizing the parent directory can arrive after publication is visible, so inspect the destination before retrying. The tests demonstrate competing writers, preservation of the winner, byte bounds, and cleanup, and the README states the platform-specific publication contract.

7. Verify evidence before accounting

The accounting exporter verifies retained calls and exports settled charges. Every charge it includes rests on a receipt it verified under a trusted signer.

metered-service/src/billing.rsL56–112rust
        let receipt: ChioReceipt = serde_json::from_str(line)
            .with_context(|| format!("invalid receipt on line {}", line_number))?;
        anyhow::ensure!(
            seen.insert(receipt.id.clone()),
            "duplicate receipt {}",
            receipt.id
        );
        anyhow::ensure!(
            receipt.kernel_key == expected,
            "untrusted signer on {}",
            receipt.id
        );
        anyhow::ensure!(
            chio_receipt_id(&receipt.body())? == receipt.id,
            "receipt ID mismatch on {}",
            receipt.id
        );
        anyhow::ensure!(
            receipt.action.verify_hash()?
                && receipt.verify_signature_with_floor(ReceiptCryptoFloor::AllowClassical)?,
            "invalid signed receipt {}",
            receipt.id
        );
        if !receipt.is_allowed() {
            denied += 1;
            continue;
        }
        let metadata = receipt
            .metadata
            .as_ref()
            .context("allowed receipt has no metadata")?;
        let financial: FinancialRecord = serde_json::from_value(
            metadata
                .get("financial")
                .context("allowed receipt has no financial record")?
                .clone(),
        )
        .context("receipt needs settlement review: invalid financial statement")?;
        let Settlement::Settled = financial.settlement_status;
        let units = financial.cost_charged;
        let currency = financial.currency;
        anyhow::ensure!(
            !currency.is_empty() && currency.len() <= 32 && currency.is_ascii(),
            "invalid currency identifier"
        );
        let attribution: Attribution = serde_json::from_value(
            metadata
                .get("attribution")
                .context("missing signed attribution")?
                .clone(),
        )?;
        PublicKey::from_hex(&attribution.subject_key).context("invalid attributed subject key")?;
        let agent = attribution.subject_key;
        let sum = currency_totals.entry(currency.clone()).or_default();
        *sum = sum
            .checked_add(units)
            .context("currency total overflows u64")?;

Open the complete source file

The exporter bounds each record and the whole import, rejects duplicate receipt IDs, verifies the trusted signer and content binding, decodes financial metadata into a typed record, and adds with checked arithmetic. An unknown settlement state or a malformed amount is an error, never an implicit zero charge.

Trust starts from a public key you select independently, so a signature that verifies only under a key the record names for itself fails the import. Verify Receipts Offline covers the retained context and independent effect evidence an audit adds, and Budgets runs the metered service whose receipts it bills.

Next: build around one invariant

Choose one operation your application already performs. Name its owner, input bound, authority check, execution point, and retained outcome, then exercise a refusal through the same path and inspect the protected resource. Add the failure that matters for your system: overload, cancellation, a lost acknowledgement, restart, or concurrent publication. The delegated document worker builds a Markdown outline and inventories links using only the operations in its narrowed grant.