The Guard Trait
Find a method, a context field, or a verdict rule for the synchronous Guard trait and its portable-core variant.
On this page
Guard is the trait every guard in the catalog implements, defined in chio-kernel. A guard reads a borrowed GuardContext and returns a GuardDecision: a verdict plus the evidence the guard chose to record. The kernel calls it inside admission, before dispatch, on whichever thread evaluates the call.
The smallest useful guard denies a call by tool name. It implements the two required methods, performs no I/O, and allocates only what the deny check needs.
A minimal guard
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 a pipeline with pipeline.add(Box::new(DenyByName::new(...))). A guard that does not apply to a request, such as a network guard looking at a filesystem call, returns Ok(GuardDecision::allow()): the pipeline is a conjunction in which every guard must allow, and the verdict has no separate not-applicable value. Custom Guards covers more involved patterns.
Try evaluate, matched_grant_index, or PendingApproval. Each entry has its own link.
Trait signature
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>;
/// Return true when mutable guard state must be checked immediately before
/// dispatch even if runtime readiness never suspended.
fn requires_dispatch_revalidation(&self) -> bool {
false
}
/// Run the opt-in immediate dispatch check. Composite guards should
/// override this method and apply it recursively to their children.
fn revalidate_required_before_dispatch(&self, ctx: &GuardContext) -> Result<(), KernelError> {
if self.requires_dispatch_revalidation() {
self.revalidate_before_dispatch(ctx)
} else {
Ok(())
}
}
/// Revalidate mutable guard state without consuming a second quota,
/// approval, or rate-limit token.
fn revalidate_before_dispatch(&self, _ctx: &GuardContext) -> Result<(), KernelError> {
Ok(())
}
/// Name the authenticated Finding status feed that must authorize a
/// governed memory write. The kernel checks this against the delivery
/// receipt before invoking the tool so a write cannot enter a quarantine
/// domain whose resolver will later reject it.
fn required_finding_status_feed_id(
&self,
_ctx: &GuardContext,
) -> Result<Option<String>, KernelError> {
Ok(None)
}
/// Validate the exact output after the tool returns and before it can be
/// released or committed as a durable tool return. Stateful guards use
/// this seam to bind an admission decision to the value actually read.
fn validate_output_before_release(
&self,
_ctx: &GuardContext,
_output: &ToolServerOutput,
) -> Result<(), KernelError> {
Ok(())
}
/// Return true when output validation binds the exact released value and
/// therefore must run after every configured output transform.
fn requires_exact_released_output(&self, _ctx: &GuardContext) -> bool {
false
}
}Only name and evaluate are left to the implementor. Every other method ships a default body, so a guard that holds no mutable state, names no Finding status feed, and leaves tool output alone writes two methods and inherits the rest.
Guard requires Send + Sync. Guards are stored as Box<dyn Guard> and evaluated on whichever thread the kernel is running on.
name()
A short kebab-case identifier. The pipeline uses it to label evidence on receipts and to format denial messages. Keep names stable: operators reference them in policy YAML and in promotion rules.
evaluate()
The hot-path method, called once per evaluated request.
- Synchronous execution. The trait permits I/O, and blocking work occupies its evaluation thread. To call an external service, use the
ExternalGuardandAsyncGuardAdapteradapter described in External Adapters. - Shared context. The method takes
&selfand a borrowedGuardContext. Guards may run concurrently for different requests, so wire mutable state through interior mutability with care. - Errors mean deny. Returning an
Erris equivalent toOk(GuardDecision::deny(...))in the pipeline. See Fail-Closed Semantics.
GuardDecision
The value a guard hands back: the verdict plus the evidence it chose to record.
pub struct GuardDecision {
pub verdict: Verdict,
pub evidence: Vec<GuardEvidence>,
}The constructors cover the common cases without building the struct by hand. from_verdict lifts a bare verdict into a decision with no evidence.
impl GuardDecision {
#[must_use]
pub fn allow() -> Self {
Self {
verdict: Verdict::Allow,
evidence: Vec::new(),
}
}
#[must_use]
pub fn allow_with_evidence(evidence: Vec<GuardEvidence>) -> Self {
Self {
verdict: Verdict::Allow,
evidence,
}
}
#[must_use]
pub fn deny(evidence: Vec<GuardEvidence>) -> Self {
Self {
verdict: Verdict::Deny,
evidence,
}
}
#[must_use]
pub fn pending_approval(evidence: Vec<GuardEvidence>) -> Self {
Self {
verdict: Verdict::PendingApproval,
evidence,
}
}
#[must_use]
pub fn from_verdict(verdict: Verdict) -> Self {
match verdict {
Verdict::Allow => Self::allow(),
Verdict::Deny => Self::deny(Vec::new()),
Verdict::PendingApproval => Self::pending_approval(Vec::new()),
}
}
}GuardContext
The context the kernel passes to every guard. It is borrowed for the lifetime of one evaluation, and a guard keeps no reference to it after returning.
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>,
}request
The chio-kernel ToolCallRequest: the signed capability, tool name, and arguments, plus the DPoP proof, governed intent, approval token, and model metadata when present.
scope
The ChioScope from the capability token, already verified by the time the guard runs.
agent_id
The typed AgentId of the calling agent, which wraps its public key.
server_id
The typed ServerId of the target tool server.
session_filesystem_roots
The enforceable filesystem roots of the session, when the call runs through a session-backed runtime. None on edges that maintain no session.
matched_grant_index
Index into the scope’s grant list of the grant that matched this request. The hosted scope-match loop supplies it before guards run; budget admission follows the guard checks.
Verdict
The three-valued outcome, defined identically in the portable core and in chio-kernel. Only chio-kernel produces PendingApproval.
/// Three-valued outcome of a kernel evaluation step.
///
/// This mirrors `chio_kernel::runtime::Verdict` exactly. 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.
/// Verdict of a guard or capability evaluation.
///
/// This is the kernel's own verdict type, distinct from `chio_core::receipt::decision::Decision`.
/// The kernel uses this internally; it maps to `chio_core::receipt::decision::Decision` when
/// building receipts.
///
/// The `PendingApproval` 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. The public contract is: `Allow`, `Deny`, and
/// `PendingApproval` are the three possible outcomes of guard evaluation, and
/// callers receive the full approval request via the richer HITL API surface
/// when they need it.
#[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,
}Verdict is distinct from chio_core::receipt::decision::Decision and maps to it only when a receipt is built. Its public contract is three outcomes; the approval payload travels through the richer human-in-the-loop interface instead of the verdict, which keeps Verdict Copy while an approval request is not.
- No payload. The verdict carries no reason, guard name, or metadata. The pipeline formats denial reasons into
KernelError::GuardDeniedand records them as evidence. - Copy + Clone. Call sites pattern-match on the enum without ceremony; a payload would break them.
- PendingApproval is shell-only. The portable core never produces it, and a custom guard running through the portable-core API returns Allow or Deny, because that path has no approval store to pair the marker with.
Evaluation phases
chio-kernel evaluates guards in three phases. A guard can take part in any of them, and each phase has its own trait.
| Phase | Trait | Runs | Outcomes |
|---|---|---|---|
Pre-invocation | Guard | Before the tool runs | Allow, Deny, PendingApproval |
Advisory | AdvisoryGuard | Alongside pre-invocation | AdvisorySignals |
Post-invocation | Post | After the tool returns | Pass, Redact(value), Block, Escalate |
In synchronous admission only Allow continues; Deny, errors, and unsupported pending values refuse. Advisory signals do not block unless a promotion rule converts a signal into a denial. Pipelines & Composition covers how the phases combine.
Guard evidence
A guard attaches findings to its decision as structured GuardEvidence records. A pre-invocation guard attaches them on the decision it returns, through GuardDecision::deny(evidence) or GuardDecision::allow_with_evidence(evidence), so per-guard evidence exists at the pre-invocation layer as well as in post-invocation hooks and external adapters.
pub struct GuardEvidence {
/// Name of the guard (e.g. "ForbiddenPathGuard").
pub guard_name: String,
/// Whether the guard passed (true) or denied (false).
pub verdict: bool,
/// Optional details about the guard's decision.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}The pipeline also folds in evidence the guard did not attach: on a deny it appends a GuardEvidence row naming the guard and the reason. When the post-invocation sanitizer redacts a response, it records counts and detector IDs, never raw secrets. Guard Evidence follows each row into the signed receipt.
ToolAction
A native guard receives the raw request inside GuardContext. Catalog guards commonly call extract_action_checked to derive a ToolAction from its name and arguments. The classifier returns a specialized action or a generic McpTool, and recognized malformed shapes produce an error.
Requests & Action Classification covers the extraction order, field aliases, malformed-input behavior, and test examples. Use it to confirm that your guard handles the request your adapter sends.
Portable-core variant
chio-kernel-core defines a second, narrower Guard trait for a custom guard supplied directly to portable evaluation. It returns a bare Verdict and a KernelCoreError, and reads a PortableToolCallRequest in place of the chio-kernel ToolCallRequest.
pub trait Guard: Send + Sync {
/// Human-readable guard name (e.g. `forbidden-path`).
fn name(&self) -> &str;
/// Evaluate this guard against a tool-call context.
///
/// Returns `Ok(Verdict::Allow)` to pass, `Ok(Verdict::Deny)` to block,
/// or `Err(KernelCoreError)` to signal an internal guard failure (which
/// the kernel core treats as a fail-closed deny).
fn evaluate(&self, ctx: &GuardContext<'_>) -> Result<Verdict, crate::KernelCoreError>;
}Use chio_kernel::Guard for almost every guard. Reach for the portable-core trait when a guard must run inside the browser or mobile kernel, where GuardDecision, the receipt store, and the approval shell are absent. Catalog guards implement the hosted trait, so importing one installs nothing in a browser, WASI, or mobile adapter.
Its context is the portable mirror of the kernel one, with the agent and server identifiers narrowed to &str.
pub struct GuardContext<'a> {
/// The tool call request being evaluated.
pub request: &'a PortableToolCallRequest,
/// The verified capability scope.
pub scope: &'a ChioScope,
/// The agent making the request.
pub agent_id: &'a str,
/// The target server.
pub server_id: &'a str,
/// 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
/// [`crate::evaluate`] before guards run.
pub matched_grant_index: Option<usize>,
}The request projection carries only what the synchronous core evaluate pipeline reads, without the 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.
#[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,
}