Skip to main content

Crate actionguard

Crate actionguard 

Source
Expand description

§actionguard

crates.io docs.rs CI license

Policy-as-code for AI agent tool calls, in Rust — the pattern security teams already use for cloud infrastructure (OPA/Rego, AWS Cedar: a policy decision point in front of every action, deny-overrides, fail-closed by default) applied to agent tool calls instead of API requests. guardflow validates what an agent says; actionguard validates what it’s about to do, before it does it.

§Install

cargo add actionguard

§Usage

use actionguard::{PolicySet, ToolCall};
use actionguard::policies::{AllowList, DenyList, ArgMatchesRegex};

let policies = PolicySet::new()
    .with(AllowList::new(["read_file", "search", "send_email"]))
    .with(DenyList::new(["rm_rf", "drop_table", "shell_exec"]))
    .with(ArgMatchesRegex::new("read_file", "path", r"^/workspace/.*"));

let call = ToolCall::new("read_file", serde_json::json!({ "path": "/workspace/notes.txt" }));

match policies.check(&call) {
    actionguard::Decision::Allow => run_the_tool(call),
    actionguard::Decision::Deny(reason) => println!("blocked: {reason}"),
}

Fail-closed by default: if no policy explicitly allows a call, it’s denied — the same default OPA and every serious authorization system ships with, and the opposite of what most hand-rolled “if command contains rm” checks do.

Deny-overrides: any policy voting Deny blocks the call outright, even if another policy voted Allow — you can’t accidentally allowlist your way past an explicit deny rule.

§Async policies

For checks that need a model call — “does this action match what the user actually asked for” (see Intent-Governed Tool Authorization) — AsyncPolicy wraps an async closure, evaluated only after every sync policy has already voted:

use actionguard::AsyncPolicySet;
use actionguard::policies::CustomAsyncPolicy;

let policies = AsyncPolicySet::from_sync(policies).with_async(
    CustomAsyncPolicy::new("matches_intent", |call| async move {
        if call_is_consistent_with(&user_request, &call).await {
            actionguard::Vote::Allow
        } else {
            actionguard::Vote::Deny("not consistent with the user's request".into())
        }
    }),
);

§Examples

cargo run --example agent_dispatcher   # sync path-scoping + async intent check, full story

§Benchmarks

cargo bench (benches/overhead.rs):

ScenarioTime
PolicySet::check, 3 policies, allowed~104 ns

§License

MIT

Modules§

policies

Structs§

AsyncPolicySet
Like PolicySet, but also runs AsyncPolicys. Sync policies run first (cheap); an explicit sync Deny short-circuits before any network call.
PolicySet
A set of Policys evaluated deny-overrides, fail-closed by default: any Deny vote wins outright regardless of Allow votes elsewhere; if nothing explicitly Allows (every policy abstained), the call is denied.
ToolCall
A tool/action an agent is about to take.

Enums§

Decision
The outcome of PolicySet::check / AsyncPolicySet::check.
Vote
One policy’s vote on a ToolCall.

Traits§

AsyncPolicy
A check that needs a network call — an LLM-as-judge asking whether an action matches the user’s actual intent, a call to an external policy service.
Policy
A sync check that votes on whether a ToolCall should proceed.