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
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:
| Example | Path | Surfaces taught |
|---|---|---|
tool-gate | examples/guards/tool-gate | Basic tool-name inspection; the smallest possible #[chio_guard] body |
enriched-inspector | examples/guards/enriched-inspector | Enriched 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?
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:
# 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 --releaseThe 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.
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:
[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_typeandGuardRequest.extracted_pathare 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 throughchio.log;get_config(key)reads per-deployment guard configuration throughchio.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.
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 thelog_levelconstants (INFO,WARN) wrap thechio.loghost import.get_config(key) -> Option<String>wrapschio.get_configand returnsNonewhen 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.
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.wasmManifest 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:
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
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:
#[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.
| Surface | Where it lives | When to use |
|---|---|---|
| Built-in HushSpec rule | YAML policy file | Allow/deny lists, regex argument matching, egress allowlists; covered by the default pipeline |
Native Guard trait | Compiled into the kernel binary | In-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 artifact | Distributed separately, hot-reloadable, third-party policy; what these examples build |
Next Steps
- Custom Guards guide · the native Rust
Guardtrait, 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