Chio/Docs

BuildPolicy

Custom Guards

Add a sandboxed WASM guard when built-in guards do not cover your policy.

Choose a guard implementation

Chio provides two ways to write guards. Use WASM guest guards for organization-specific enforcement. They run in a sandbox, have a fuel limit, are declared in chio.yaml, and managed through the chio guard CLI. The native Rust Guard trait is for the guards shipped inside chio-guards and chio-data-guards — and is used for built-ins that need kernel types or lower overhead. Adding one requires changes to those crates and recompiling the kernel.

WASM Guest Guards

The chio-wasm-guards crate lets you write guards in any language that compiles to WebAssembly — the protocol specification names Rust, AssemblyScript, Go, and C — and load them into the kernel at runtime. A guest guard runs inside an isolated linear-memory sandbox with no access to the host filesystem, network, or kernel state, and terminates within a bounded fuel budget. Guards are declared per deployment in chio.yaml; they are not compiled into the kernel binary.

rendering…
A WASM guest guard evaluates alongside the built-in pipeline. The host hands the guard a GuardRequest, calls evaluate, and reads back a verdict. Any error or fuel exhaustion denies.

Two Guard Binary Formats

chio-wasm-guards accepts two WebAssembly binary shapes and auto-detects which one a module is at load time (detect_wasm_format):

  • Core module. A raw wasm32 module that exports evaluate(request_ptr, request_len) -> i32 over the JSON ABI below. It routes to WasmtimeBackend. The Rust SDK (chio-guard-sdk with #[chio_guard]) compiles to this shape.
  • Component Model component. A component built against the chio:guard@0.2.0 WIT world (wit/chio-guard/world.wit), which exports a typed evaluate(request: guard-request) -> verdict directly — no JSON, no manual pointer/length handling. It routes to ComponentBackend. The four cross-language SDKs — chio-guard-cpp, chio-guard-go, chio-guard-py, and chio-guard-ts — target this world.

Both formats use the same request, verdict, and fail-closed behavior. They differ in how requests cross the sandbox boundary. The next section documents the core-module ABI. Cross-language SDKs generate the boundary code from the WIT world.

The Core-Module ABI

A core-module guard exports evaluate. The host serializes a GuardRequest as JSON, places the bytes in guest linear memory, and calls evaluate with the pointer and length. The guest reads the request, decides, and returns a verdict code.

text
evaluate(request_ptr: i32, request_len: i32) -> i32

Return codes:
  0                Allow
  1                Deny
  any negative     Error (fail-closed -> denial)

Where the request lands, and how the deny reason comes back, depends on which exports the module carries. The host probes for them at evaluation time:

  • Allocator path (SDK default). If the module exports chio_alloc(request_len) -> ptr, the host calls it, validates the returned pointer is in bounds, and writes the request there. The deny reason is retrieved actively: on a Deny return the host calls the module's chio_deny_reason(65536, 4096) export, and the guest writes a JSON GuestDenyResponse into that buffer and returns the byte count. Every guard built with #[chio_guard] takes this path — the macro re-exports chio_alloc, chio_free, and chio_deny_reason.
  • Hand-rolled fallback. A module with no chio_alloc export gets the request written at offset 0. For the deny reason, a module with no chio_deny_reason export MAY passively leave a NUL-terminated UTF-8 string at linear-memory offset 65536 (64 KiB); the host reads up to 4096 bytes. An absent, empty, or malformed region yields a generic denial message.

GuardRequest

The JSON payload the host writes into guest memory:

FieldTypeDescription
tool_namestringTool being invoked.
server_idstringServer hosting the tool.
agent_idstringAgent making the request.
argumentsobjectTool arguments (opaque JSON).
scopesstring[]Granted scope names, formatted "server_id:tool_name".
session_metadataobject or nullOptional session context for stateful guards.

Authoring with chio-guard-sdk

You do not write the raw ABI by hand. The #[chio_guard] attribute macro from chio-guard-sdk-macros validates a plain fn evaluate(req: GuardRequest) -> GuardVerdict at compile time and expands it into the ABI exports (the evaluate entry point plus the chio_alloc, chio_free, and chio_deny_reason exports the host expects).

src/lib.rs
use chio_guard_sdk::prelude::*;
use chio_guard_sdk_macros::chio_guard;

#[chio_guard]
fn evaluate(req: GuardRequest) -> GuardVerdict {
    // Restrict browser automation to an approved host.
    if req.tool_name == "browser_navigate" {
        let host_ok = req
            .arguments
            .get("url")
            .and_then(|v| v.as_str())
            .map(|url| url.starts_with("https://app.example.com/"))
            .unwrap_or(false);

        if !host_ok {
            return GuardVerdict::deny("navigation target is not on the allowlist");
        }
    }

    GuardVerdict::allow()
}

The macro renames your function, generates the #[no_mangle] pub extern "C" fn evaluate(ptr: i32, len: i32) -> i32 entry point plus the allocator and deny-reason exports, and encodes the verdict for you. A signature mismatch is a compile error. Four cross-language guest SDKs also ship under sdks/guard/ chio-guard-cpp, chio-guard-go, chio-guard-py, and chio-guard-ts. Each compiles to a Component Model component against the chio:guard@0.2.0 WIT world. They pass requests and verdicts as typed WIT values instead of hand-serialized JSON.

Fuel Metering and Fail-Closed

Guest guards execute under a fuel budget that bounds CPU consumption. The runtime tracks fuel per instruction and terminates the guest when the budget is exhausted. The following failures deny the call:

  • Fuel exhaustion. Default fuel_limit is 10,000,000. Running out raises WasmGuardError::FuelExhausted, which the kernel treats as a denial.
  • Traps. Any WASM trap — memory access violation, stack overflow, unreachable instruction — results in denial.
  • Missing exports. If the module does not export evaluate or memory, the load fails and the guard is never registered — a config-time reject, not a runtime deny.

Declaring Guards in chio.yaml

A compiled guard is attached to a deployment by listing it under wasm_guards:

chio.yaml
wasm_guards:
  - name: custom-pii-guard
    path: /etc/chio/guards/pii_guard.wasm
    fuel_limit: 5000000
    priority: 100
    advisory: false
OptionTypeDefaultDescription
namestringrequiredHuman-readable name, recorded in receipts and logs.
pathstringrequiredFilesystem path to the .wasm module.
fuel_limitu6410,000,000Maximum fuel units per invocation.
priorityu321000Evaluation order; lower runs earlier.
advisoryboolfalseIf true, denials are logged but not enforced.

Ship new guards advisory-first

Set advisory: true to run a new guard in production without blocking traffic: it logs its denials and errors but returns Verdict::Allow. Watch the logs, confirm the guard fires where you expect and nowhere you do not, then flip advisory to false to enforce.

The Guard Lifecycle

The chio guard subcommand covers authoring through distribution:

bash
# Scaffold, compile, and validate locally
$ chio guard new custom-pii-guard        # Cargo.toml, src/lib.rs, guard-manifest.yaml
$ chio guard build                       # -> wasm32-unknown-unknown
$ chio guard inspect ./pii_guard.wasm    # exports, ABI compatibility, memory config
$ chio guard test --wasm ./pii_guard.wasm ./fixtures/*.yaml
$ chio guard bench ./pii_guard.wasm --iterations 100

# Package and sign
$ chio guard pack                        # -> .arcguard archive
$ chio guard install ./custom-pii-guard.arcguard --target-dir ./guards
$ chio guard sign ./pii_guard.wasm --key ./signer.seed \
    --name custom-pii-guard --version 1.0.0   # writes pii_guard.wasm.sig
$ chio guard verify ./pii_guard.wasm

# Distribute over OCI with Sigstore verification
$ chio guard publish ./custom-pii-guard \
    --ref oci://ghcr.io/acme/pii-guard:v1 --epoch-id-seed ./epoch.seed
$ chio guard pull \
    --ref oci://ghcr.io/acme/pii-guard@sha256:<digest> \
    --sigstore-identity-regex '^https://github\.com/acme/' \
    --sigstore-oidc-issuer https://token.actions.githubusercontent.com

Two additional commands support the lifecycle: chio guard blocklist remove <digest> manages the local digest blocklist, and chio guard market (list, info, install) browses and installs priced guards from a catalog.

Session-aware guards read the session journal

Guards that need cross-call state read the append-only, SHA-256 hash-chained session journal (chio-http-session). Each entry carries the hash of the previous one — the first uses the 64-hex-zero seed — and verify_integrity() walks the chain to detect tampering. This is the shared state layer for session-aware and advisory guards.

Native Built-in Guards

Chio uses the native Guard trait for built-in guards compiled into chio-guards and chio-data-guards. Use it when a guard needs kernel types or low overhead. It requires building the guard into those crates and recompiling the kernel. Use a WASM guest guard for organization-specific logic. This section also helps when reading built-in guards or wiring a pipeline in an embedded host.

The Guard Trait

Every guard implements a two-method trait defined in chio-kernel. evaluate returns a GuardDecision, or Err for an internal failure (which the kernel treats as a deny):

rust
pub trait Guard: Send + Sync {
    /// Human-readable guard name (e.g., "forbidden-path").
    fn name(&self) -> &str;

    /// Evaluate the guard against a tool call request. Returns an allow or
    /// deny decision with optional evidence, or Err on internal failure
    /// (which the kernel treats as deny).
    fn evaluate(&self, ctx: &GuardContext) -> Result<GuardDecision, KernelError>;
}

The trait requires Send + Sync because guards are shared across threads in the kernel runtime.

GuardContext

The kernel passes a GuardContext to each evaluation. It includes the request and the data the guard needs:

rust
pub struct GuardContext<'a> {
    /// The tool call request being evaluated.
    pub request: &'a ToolCallRequest,
    /// The verified capability scope.
    pub scope: &'a ChioScope,
    /// The agent making the request.
    pub agent_id: &'a AgentId,
    /// The target server.
    pub server_id: &'a ServerId,
    /// Session-scoped enforceable filesystem roots, when available.
    pub session_filesystem_roots: Option<&'a [String]>,
    /// Index of the matched grant in the capability's scope.
    pub matched_grant_index: Option<usize>,
}

Through ctx.request you reach the complete ToolCallRequest — tool name, server ID, agent ID, the serde_json::Value arguments, and the signed capability token. A guard can inspect the action, actor, and authority.

GuardDecision and Verdict

evaluate returns a GuardDecision: a Verdict plus a vector of evidence. The Verdict enum has three unit variants — there is no Pass, and deny detail travels on the evidence, not inline on the verdict:

rust
pub struct GuardDecision {
    pub verdict: Verdict,
    pub evidence: Vec<GuardEvidence>,
}

pub enum Verdict {
    /// The action is allowed.
    Allow,
    /// The action is denied.
    Deny,
    /// Suspended pending a human decision. Only produced by the full
    /// chio-kernel shell, never by chio-kernel-core directly.
    PendingApproval,
}

pub struct GuardEvidence {
    pub guard_name: String,
    pub verdict: bool,          // true = passed, false = denied
    pub details: Option<String>,
}

Construct decisions through the helpers: GuardDecision::allow(), GuardDecision::deny(evidence), GuardDecision::pending_approval(evidence), or GuardDecision::from_verdict(v). A guard that does not apply to a request must not block it — return Ok(GuardDecision::allow()), since the pipeline is conjunctive and each guard must allow for a call to proceed.

rust
fn evaluate(&self, ctx: &GuardContext) -> Result<GuardDecision, KernelError> {
    let Some(ip_str) = ctx
        .request
        .arguments
        .get("_source_ip")
        .and_then(|v| v.as_str())
    else {
        // Not applicable to this request: allow.
        return Ok(GuardDecision::allow());
    };

    let ip: IpAddr = ip_str
        .parse()
        .map_err(|e| KernelError::Internal(format!("invalid source IP: {e}")))?;

    if self.allowed_ips.contains(&ip) {
        Ok(GuardDecision::allow())
    } else {
        Ok(GuardDecision::deny(vec![GuardEvidence {
            guard_name: self.name().to_string(),
            verdict: false,
            details: Some(format!("source IP {ip} not on allowlist")),
        }]))
    }
}

Err means deny

If evaluate returns Err, the pipeline treats it as a denial — the fail-closed guarantee. Return Err only for genuine internal failures (configuration errors, I/O problems). If the guard can evaluate the request but the request violates policy, return Ok(GuardDecision::deny(...)).

The Default Pipeline

GuardPipeline::default_pipeline() registers seven guards, in this order, with their default configurations: ForbiddenPathGuard, ShellCommandGuard, EgressAllowlistGuard, PathAllowlistGuard, McpToolGuard, SecretLeakGuard, and PatchIntegrityGuard. Velocity and the session-aware guards are wired by the policy compiler from a HushSpec document, not by default_pipeline().

rust
use chio_guards::GuardPipeline;

// The seven stateless built-ins, cheapest-first.
let pipeline = GuardPipeline::default_pipeline();

// Register the pipeline as a single guard on the kernel.
kernel.add_guard(Box::new(pipeline));

Guards evaluate synchronously on each tool call, and the pipeline short-circuits on the first deny, so ordering matters for latency (not correctness). Keep evaluate a fast, allocation-light check against data prepared in new(): pre-compile regex and glob patterns, load any external allowlist at construction time, and avoid blocking I/O.


Next Steps

  • External Guards · wire third-party content-safety and threat-intel providers into the pipeline
  • Agent Passport · portable agent credentials for cross-organizational trust
  • Architecture · how guards fit into the kernel evaluation pipeline
  • Native Tool Server · build a tool server that your guards will protect