Chio/Docs

Building Custom Guards

Two runnable guard examples ship under examples/guards. Both compile to wasm32-unknown-unknown through the chio-guard-sdk crate and the #[chio_guard] proc macro. They cover the two ends of the WASM-guard surface: a tiny tool-name gate and an enriched-field inspector that calls into host functions for logging and config.

Prerequisites

Rust toolchain with the wasm32-unknown-unknown target installed (rustup target add wasm32-unknown-unknown). The chio workspace built locally. See Installation for setup, Custom Guards for the native-Rust Guard trait, and WASM guards for the runtime model these examples plug into.

What It Shows

The two examples are the canonical starting points for the WASM guard surface:

ExamplePathSurfaces taught
tool-gateexamples/guards/tool-gateBasic tool-name inspection; the smallest possible #[chio_guard] body
enriched-inspectorexamples/guards/enriched-inspectorEnriched request fields ( action_type, extracted_path) plus host functions ( chio.log, chio.get_config)

Both crates declare crate-type = ["cdylib"] and depend on chio-guard-sdk plus chio-guard-sdk-macros. Both deny unwrap_used and expect_used via clippy: guards run inside the kernel hot path, so panicking is not on the table.

Native Guard trait or WASM module?

Pick the native Guard trait for guards that ship with the kernel binary and live alongside built-ins. Pick a WASM module when you want to ship a guard separately from the kernel: hot reload without restarting, distribute through a manifest, or run third-party policy that does not have access to the kernel internals. The two coexist in the same pipeline.

Run It

Build either example to wasm32-unknown-unknown:

terminal
# Tool-gate
cargo build -p chio-example-tool-gate \
  --target wasm32-unknown-unknown --release

# Enriched-inspector
cargo build -p chio-example-enriched-inspector \
  --target wasm32-unknown-unknown --release

The output .wasm lands under target/wasm32-unknown-unknown/release/. That artifact is what the kernel loads.


Tool-Gate: The Smallest Useful Guard

tool-gate is one match statement. It denies three named tools and allows everything else. The #[chio_guard] macro wires up the WASM exports the host runtime expects (evaluate, chio_alloc, chio_free, chio_deny_reason) so the author writes a plain Rust function over GuardRequest returning GuardVerdict.

examples/guards/tool-gate/src/lib.rs
use chio_guard_sdk::prelude::*;
use chio_guard_sdk_macros::chio_guard;

#[chio_guard]
fn evaluate(req: GuardRequest) -> GuardVerdict {
    match req.tool_name.as_str() {
        "dangerous_tool" | "rm_rf" | "drop_database" => {
            GuardVerdict::deny("tool is blocked by policy")
        }
        _ => GuardVerdict::allow(),
    }
}

Cargo manifest:

examples/guards/tool-gate/Cargo.toml
[package]
name = "chio-example-tool-gate"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
chio-guard-sdk = { path = "../../../crates/chio-guard-sdk" }
chio-guard-sdk-macros = { path = "../../../crates/chio-guard-sdk-macros" }

[lints.clippy]
unwrap_used = "deny"
expect_used = "deny"

Enriched-Inspector: Reading Extracted Fields and Calling Host Functions

enriched-inspector targets two surface goals from the guard SDK that tool-gate skips:

  • Enriched request fields. GuardRequest.action_type and GuardRequest.extracted_path are populated by the kernel before evaluation. The guard reads them to write rules in terms of what the call would do rather than the raw tool name.
  • Host functions. log(level, msg) emits a structured log entry through chio.log; get_config(key) reads per-deployment guard configuration through chio.get_config.

The example denies any file_write action whose extracted path begins with the configured blocked_path (or /etc as a built-in fallback) and allows everything else.

examples/guards/enriched-inspector/src/lib.rs
use chio_guard_sdk::prelude::*;
use chio_guard_sdk_macros::chio_guard;

#[chio_guard]
fn evaluate(req: GuardRequest) -> GuardVerdict {
    log(log_level::INFO, "enriched inspector evaluating request");
    let blocked_path = get_config("blocked_path");

    if let Some(ref action) = req.action_type {
        if action == "file_write" {
            if let Some(ref path) = req.extracted_path {
                log(log_level::WARN, "file write detected");
                if let Some(ref bp) = blocked_path {
                    if path.starts_with(bp.as_str()) {
                        return GuardVerdict::deny("write to protected path blocked by policy");
                    }
                }
                if path.starts_with("/etc") {
                    return GuardVerdict::deny("write to /etc blocked");
                }
            }
        }
    }
    GuardVerdict::allow()
}

The SDK call surface used here:

  • log(level, message) and the log_level constants ( INFO, WARN) wrap the chio.log host import.
  • get_config(key) -> Option<String> wraps chio.get_config and returns None when the deployment has no value for that key.

The full prelude exposes more: a get_time wrapper for chio.get_time_unix_secs, a fetch_blob wrapper for chio:guard/host.fetch-blob, and a PolicyContext resource wrapper for the policy-context bundle handle. See the WASM guard reference for the full host ABI.


Building and Loading a WASM Guard

Build

WASM guards are cdylib crates targeting wasm32-unknown-unknown. On native targets the SDK keeps no-op fallbacks for host imports so cargo test runs without a WASM runtime; the production build is the WASM target.

terminal
rustup target add wasm32-unknown-unknown

cargo build -p chio-example-enriched-inspector \
  --target wasm32-unknown-unknown --release

ls target/wasm32-unknown-unknown/release/chio_example_enriched_inspector.wasm

Manifest and Hot Reload

WASM guards are loaded by the kernel through a guard manifest that points at the .wasm artifact and carries any per-deployment config the guard expects (such as the blocked_path key the enriched-inspector reads). The manifest path, hot-reload model, and signing rules are normative in the WASM guard reference.

Once the manifest is registered, the kernel hands every GuardRequest to the guest, deserializes the returned GuardVerdict, and folds the result into the same conjunctive pipeline as built-in guards. Errors from the guest run fail-closed: a guard that crashes or returns an undecodable verdict denies the request, exactly as the native Guard trait does.

Inspect After Build

After the build runs, the artifact lands at the cargo output path. Check it with:

terminal
ls -la target/wasm32-unknown-unknown/release/chio_example_tool_gate.wasm
file target/wasm32-unknown-unknown/release/chio_example_tool_gate.wasm

# Expected output (from file):
# ... WebAssembly (wasm) binary module version 0x1 (MVP)

That .wasm is the artifact the kernel loads through a guard manifest. The enriched-inspector build produces chio_example_enriched_inspector.wasm at the same path.


Decision rule

Use the WASM path when you want a guard that ships separately from the kernel binary, hot-reloads through manifest registration, or runs third-party policy without access to kernel internals. Pick the native Guard trait when the guard lives in-tree alongside built-ins and needs full access to kernel types; see Custom Guards. Pick a HushSpec rule when the policy is just allow/deny lists or regex matching; see HushSpec.

Testing Locally

Because the SDK provides no-op fallbacks for host imports on native targets, you can write unit tests that call evaluate directly with constructed GuardRequest values:

src/lib.rs (test scaffold)
#[cfg(test)]
mod tests {
    use super::*;
    use chio_guard_sdk::types::GuardRequest;

    fn req(tool: &str) -> GuardRequest {
        GuardRequest {
            tool_name: tool.to_string(),
            action_type: None,
            extracted_path: None,
            // ... other fields zeroed for the test ...
        }
    }

    #[test]
    fn allows_unknown_tool() {
        let v = evaluate(req("safe_tool"));
        assert_eq!(v.outcome, VERDICT_ALLOW);
    }

    #[test]
    fn denies_listed_tool() {
        let v = evaluate(req("drop_database"));
        assert_eq!(v.outcome, VERDICT_DENY);
    }
}

Real GuardRequest construction in tests needs every field; refer to chio_guard_sdk::types for the canonical shape. The tests run on the host target with cargo test and exercise the same evaluate body the WASM build exports.


Picking the Right Path

Three guard surfaces are available; choose by where you want the code to live and how you want it distributed.

SurfaceWhere it livesWhen to use
Built-in HushSpec ruleYAML policy fileAllow/deny lists, regex argument matching, egress allowlists; covered by the default pipeline
Native Guard traitCompiled into the kernel binaryIn-tree guards alongside built-ins; access to kernel types and full Rust ecosystem; see the Guard trait reference
WASM module (chio-guard-sdk)External .wasm artifactDistributed separately, hot-reloadable, third-party policy; what these examples build

Next Steps

  • Custom Guards guide · the native Rust Guard trait, with two worked examples
  • WASM guards · normative reference for the host ABI, manifest format, and hot reload
  • Guard trait reference · the in-process trait the kernel pipeline runs against, native and WASM alike
Building Custom Guards · Chio Docs