Chio/Docs

PlatformFoundations

The Guard Trait

The synchronous Guard trait, its context and decision types, and the narrower portable-core variant.


Signature

The trait is defined in chio-kernel and implemented by every guard in the catalog. Source:

crates/kernel/chio-kernel/src/kernel/mod.rs
pub trait Guard: Send + Sync {
    /// Human-readable guard name (e.g. "forbidden-path").
    fn name(&self) -> &str;

    /// Evaluate the guard against a tool-call context.
    ///
    /// Returns `Ok(GuardDecision)` - a verdict plus any evidence the guard
    /// recorded - or `Err(KernelError)` to signal an internal guard failure
    /// (which the kernel treats as a fail-closed deny).
    fn evaluate(&self, ctx: &GuardContext<'_>) -> Result<GuardDecision, KernelError>;
}

A guard hands back a GuardDecision: the verdict plus the evidence it chose to record. The constructors cover the common cases without building the struct by hand.

crates/kernel/chio-kernel/src/kernel/mod.rs
pub struct GuardDecision {
    pub verdict: Verdict,
    pub evidence: Vec<GuardEvidence>,
}

impl GuardDecision {
    pub fn allow() -> Self {
        Self { verdict: Verdict::Allow, evidence: Vec::new() }
    }
    pub fn allow_with_evidence(evidence: Vec<GuardEvidence>) -> Self {
        Self { verdict: Verdict::Allow, evidence }
    }
    pub fn deny(evidence: Vec<GuardEvidence>) -> Self {
        Self { verdict: Verdict::Deny, evidence }
    }
    pub fn pending_approval(evidence: Vec<GuardEvidence>) -> Self {
        Self { verdict: Verdict::PendingApproval, evidence }
    }
}

The bound Send + Sync is not optional. Guards are stored as Box<dyn Guard> and evaluated on whichever thread the kernel happens to be running on.

name()

A short kebab-case identifier. The pipeline uses this to label evidence on receipts and to format denial messages. Use stable names; operators reference them in policy YAML and in promotion rules.

evaluate()

The hot-path method. Three things to keep in mind:

  • No I/O. The trait is synchronous. If you need to call an external service, use the ExternalGuard + AsyncGuardAdapter adapter (see External Adapters).
  • Pure on the context. The method takes &self and a borrowed GuardContext. Mutable state should be wired through interior mutability with care, since guards may run concurrently for different requests.
  • Errors mean deny. Returning an Err is equivalent to an Ok(GuardDecision::deny(...)) in the pipeline. See Fail-Closed Semantics.

GuardContext

The context the kernel passes to guards. It is borrowed for the lifetime of the evaluation; guards must not store references past return.

crates/kernel/chio-kernel/src/kernel/mod.rs
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 the request is being
    /// evaluated through the supported session-backed runtime path.
    pub session_filesystem_roots: Option<&'a [String]>,
    /// Index of the matched grant in the capability's scope, populated by
    /// check_and_increment_budget before guards run.
    pub matched_grant_index: Option<usize>,
}
FieldPurpose
requestThe chio-kernel ToolCallRequest: signed capability, tool name, arguments, and, when present, DPoP proof, governed intent, approval token, and model metadata.
scopeThe ChioScope from the capability token. Already verified by the time the guard runs.
agent_idTyped AgentId of the calling agent (wraps its public key).
server_idTyped ServerId of the target tool server.
session_filesystem_rootsWhen the call runs through a session-backed runtime, this carries the enforceable filesystem roots for that session. None on edges that do not maintain a session.
matched_grant_indexIndex into the scope's grant list of the grant that matched this request. Populated before guards run, during the budget check-and-increment step.

Portable-core variant

chio-kernel-core defines a second, narrower Guard trait. It is not the trait the catalog implements; it exists so the one guard that has to run byte-for-byte on wasm32-unknown-unknown, wasm32-wasip1, and the mobile FFI, the session-filesystem-roots check, can compile without thechio-kernel package. It returns a bare Verdict and a KernelCoreError, and reads a PortableToolCallRequest rather than the chio-kernel ToolCallRequest.

crates/kernel/chio-kernel-core/src/guard.rs
pub trait Guard: Send + Sync {
    fn name(&self) -> &str;

    /// Returns `Ok(Verdict::Allow)` to pass, `Ok(Verdict::Deny)` to block,
    /// or `Err(KernelCoreError)` - treated as a fail-closed deny.
    fn evaluate(&self, ctx: &GuardContext<'_>) -> Result<Verdict, KernelCoreError>;
}

pub struct GuardContext<'a> {
    pub request: &'a PortableToolCallRequest,
    pub scope: &'a ChioScope,
    pub agent_id: &'a str,
    pub server_id: &'a str,
    pub session_filesystem_roots: Option<&'a [String]>,
    pub matched_grant_index: Option<usize>,
}

The request projection carries only what the sync core evaluate pipeline reads. There is no DPoP proof, governed intent, approval token, or model metadata: the chio-kernel package handles those values. When it runs the core pipeline, it builds a temporary PortableToolCallRequest from its own ToolCallRequest.

crates/kernel/chio-kernel-core/src/guard.rs
#[derive(Debug, Clone)]
pub struct PortableToolCallRequest {
    /// Unique request identifier.
    pub request_id: String,
    /// The tool to invoke.
    pub tool_name: String,
    /// The server hosting the tool.
    pub server_id: String,
    /// The calling agent's identifier (hex-encoded public key).
    pub agent_id: String,
    /// Tool arguments as canonical JSON.
    pub arguments: serde_json::Value,
}

Selecting a guard trait

Almost always chio-kernel chio_kernel::Guard. Reach for the portable-core trait only when a guard must run inside the browser or mobile kernel, where GuardDecision, the receipt store, and the approval shell are not present.

Verdict

The three-valued outcome. Defined identically in the portable core and chio-kernel, with one nuance: only that package produces PendingApproval.

crates/kernel/chio-kernel-core/src/lib.rs
/// Three-valued outcome of a kernel evaluation step.
///
/// The kernel core never emits `PendingApproval` itself; the full
/// `chio-kernel` orchestration shell wraps the core verdict with the
/// human-in-the-loop approval path where needed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
    /// The action is allowed.
    Allow,
    /// The action is denied.
    Deny,
    /// The action is suspended pending a human decision. Only produced by
    /// the full `chio-kernel` shell, never by `chio-kernel-core` directly.
    PendingApproval,
}

The chio-kernel mirror has matching Copy + Clone semantics:

crates/kernel/chio-kernel/src/runtime.rs
/// Verdict of a guard or capability evaluation.
///
/// Phase 3.4 introduced the `PendingApproval` variant. The variant is a
/// marker: the payload (`ApprovalRequest`) is returned separately via
/// `crate::approval::HitlVerdict` so existing call sites that pattern-
/// match on `Verdict` and rely on its `Copy` semantics keep compiling
/// without change.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
    /// The action is allowed.
    Allow,
    /// The action is denied.
    Deny,
    /// The action is suspended pending a human decision. Look up the
    /// associated `ApprovalRequest` via the HITL API.
    PendingApproval,
}

Properties to remember:

  • No payload. The verdict carries no reason, no guard name, no metadata. Denial reasons are formatted by the pipeline into KernelError::GuardDenied and recorded as evidence.
  • Copy + Clone. Existing call sites pattern-match on the enum without ceremony. Adding a payload would have broken those sites.
  • PendingApproval is shell-only. The portable core never produces it. Custom guards running through the portable-core API should not return it either; it has no approval store to pair the marker with.

Returning Allow when not applicable

If your guard does not apply to the request (for example a network guard looking at a filesystem call), return Ok(GuardDecision::allow()). The pipeline uses conjunctive (AND) logic: every guard must allow for the call to proceed. There is no separate not applicable verdict.

Classifying the Request: ToolAction

A guard rarely reads raw tool names and argument JSON. It classifies the request into a ToolAction first, then pattern-matches on that. The enum is the categorized view of a request, derived by inspecting the tool name and arguments.

crates/guards/chio-guards/src/action.rs
pub enum ToolAction {
    FileAccess(String),
    FileWrite(String, Vec<u8>),
    NetworkEgress(String, u16),
    ShellCommand(String),
    McpTool(String, Value),
    Patch(String, String),
    CodeExecution { language: String, code: String },
    BrowserAction { verb: String, target: Option<String> },
    DatabaseQuery { database: String, query: String },
    ExternalApiCall { service: String, endpoint: String },
    MemoryWrite { store: String, key: String },
    MemoryRead { store: String, key: Option<String> },
    Unknown,
}

Two extraction helpers turn a (tool_name, arguments) pair into a ToolAction:

  • extract_action is best-effort: recognized-but-malformed shapes fall back to ToolAction::Unknown.
  • extract_action_checked returns a MalformedAction error on a type mismatch. Prefer it at guard boundaries and deny on the error: a malformed action is exactly the input a guard should not wave through.

A guard that does not recognize the action returns Ok(GuardDecision::allow()): ToolAction::Unknown, and any variant the guard does not handle, is not its concern.


Lifecycle

chio-kernel evaluates guards in three phases. A guard can participate in any of them, but each phase has its own trait.

PhaseTraitWhen
Pre-invocationGuardBefore the tool runs. Decides Allow / Deny / PendingApproval.
AdvisoryAdvisoryGuardAlongside pre-invocation. Emits AdvisorySignals without blocking, unless a promotion rule converts the signal into a denial.
Post-invocationPostInvocationHookAfter the tool returns. Can Allow, Redact(value), Block, or Escalate.

See Pipelines & Composition for details.


Evidence

Every guard records findings on the receipt through structured GuardEvidence records. A pre-invocation guard attaches them directly on the decision it returns, GuardDecision::deny(evidence) or GuardDecision::allow_with_evidence(evidence), so per-guard evidence is available at the pre-invocation layer, not only from post-invocation hooks or external adapters.

crates/core/chio-core-types/src/receipt/metadata.rs
pub struct GuardEvidence {
    /// Name of the guard.
    pub guard_name: String,
    /// Whether the guard passed (true) or denied (false).
    pub verdict: bool,
    /// Optional details about the guard's decision.
    pub details: Option<String>,
}

The pipeline also folds in evidence the guard did not attach itself: on a deny it appends a GuardEvidence row naming the guard and the reason. The post-invocation sanitizer is the richest example: when it redacts a response it records counts and detector IDs, never raw secrets.


A Minimal Guard

This guard denies when the tool name appears on a deny list. It has no I/O and allocates only what the deny check needs.

rust
use std::collections::HashSet;

use chio_kernel::{Guard, GuardContext, GuardDecision, KernelError};

pub struct DenyByName {
    denied: HashSet<String>,
}

impl DenyByName {
    pub fn new(denied: impl IntoIterator<Item = String>) -> Self {
        Self { denied: denied.into_iter().collect() }
    }
}

impl Guard for DenyByName {
    fn name(&self) -> &str {
        "deny-by-name"
    }

    fn evaluate(&self, ctx: &GuardContext) -> Result<GuardDecision, KernelError> {
        if self.denied.contains(&ctx.request.tool_name) {
            Ok(GuardDecision::deny(vec![]))
        } else {
            Ok(GuardDecision::allow())
        }
    }
}

Register it on the pipeline through pipeline.add(Box::new(DenyByName::new(...))). For more involved patterns see Custom Guards.


Where to Go Next