pub mod inspectors;
pub use inspectors::{
load_adversary_rules_from, AdversaryInspector, EgressInspector, InspectionResult, Inspector,
InspectorChain, RepetitionInspector,
};
use car_ir::Action;
use car_state::StateStore;
use std::panic::{self, AssertUnwindSafe};
#[derive(Debug, Clone)]
pub struct PolicyViolation {
pub policy_name: String,
pub action_id: String,
pub reason: String,
}
pub type PolicyCheck = Box<dyn Fn(&Action, &StateStore) -> Option<String> + Send + Sync>;
pub struct PolicyEngine {
policies: Vec<(String, String, PolicyCheck)>, }
impl PolicyEngine {
pub fn new() -> Self {
Self {
policies: Vec::new(),
}
}
pub fn register(&mut self, name: &str, check: PolicyCheck, description: &str) {
self.policies.push((
name.to_string(),
description.to_string(),
check,
));
}
pub fn check(&self, action: &Action, state: &StateStore) -> Vec<PolicyViolation> {
let mut violations = Vec::new();
for (name, _, check_fn) in &self.policies {
let result = panic::catch_unwind(AssertUnwindSafe(|| check_fn(action, state)));
match result {
Ok(Some(reason)) => {
violations.push(PolicyViolation {
policy_name: name.clone(),
action_id: action.id.clone(),
reason,
});
}
Ok(None) => {} Err(_) => {
violations.push(PolicyViolation {
policy_name: name.clone(),
action_id: action.id.clone(),
reason: format!("policy '{}' panicked during check", name),
});
}
}
}
violations
}
pub fn policy_names(&self) -> Vec<String> {
self.policies.iter().map(|(n, _, _)| n.clone()).collect()
}
pub fn is_empty(&self) -> bool {
self.policies.is_empty()
}
}
impl Default for PolicyEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use car_ir::{ActionType, FailureBehavior};
use serde_json::Value;
use std::collections::HashMap;
fn make_action(tool: &str) -> Action {
Action {
id: "test".to_string(),
action_type: ActionType::ToolCall,
tool: Some(tool.to_string()),
parameters: HashMap::new(),
preconditions: vec![],
expected_effects: HashMap::new(),
state_dependencies: vec![],
idempotent: false,
max_retries: 3,
failure_behavior: FailureBehavior::Abort,
timeout_ms: None,
metadata: HashMap::new(),
}
}
#[test]
fn no_policies_passes() {
let engine = PolicyEngine::new();
let state = StateStore::new();
let violations = engine.check(&make_action("echo"), &state);
assert!(violations.is_empty());
}
#[test]
fn policy_blocks_action() {
let mut engine = PolicyEngine::new();
engine.register(
"no_echo",
Box::new(|action, _state| {
if action.tool.as_deref() == Some("echo") {
Some("echo is forbidden".to_string())
} else {
None
}
}),
"Block echo tool",
);
let state = StateStore::new();
let violations = engine.check(&make_action("echo"), &state);
assert_eq!(violations.len(), 1);
assert!(violations[0].reason.contains("forbidden"));
}
#[test]
fn policy_allows_other_tools() {
let mut engine = PolicyEngine::new();
engine.register(
"no_echo",
Box::new(|action, _state| {
if action.tool.as_deref() == Some("echo") {
Some("forbidden".to_string())
} else {
None
}
}),
"",
);
let state = StateStore::new();
let violations = engine.check(&make_action("add"), &state);
assert!(violations.is_empty());
}
#[test]
fn policy_checks_state() {
let mut engine = PolicyEngine::new();
engine.register(
"require_auth",
Box::new(|_action, state| {
if state.get("auth") != Some(Value::Bool(true)) {
Some("auth required".to_string())
} else {
None
}
}),
"",
);
let state = StateStore::new();
let violations = engine.check(&make_action("deploy"), &state);
assert_eq!(violations.len(), 1);
state.set("auth", Value::Bool(true), "setup");
let violations2 = engine.check(&make_action("deploy"), &state);
assert!(violations2.is_empty());
}
#[test]
fn panicking_policy_caught() {
let mut engine = PolicyEngine::new();
engine.register(
"crasher",
Box::new(|_action, _state| {
panic!("policy crashed");
}),
"",
);
let state = StateStore::new();
let violations = engine.check(&make_action("anything"), &state);
assert_eq!(violations.len(), 1);
assert!(violations[0].reason.contains("panicked"));
}
#[test]
fn multiple_policies() {
let mut engine = PolicyEngine::new();
engine.register("p1", Box::new(|_, _| Some("fail 1".to_string())), "");
engine.register("p2", Box::new(|_, _| None), "");
engine.register("p3", Box::new(|_, _| Some("fail 3".to_string())), "");
let state = StateStore::new();
let violations = engine.check(&make_action("x"), &state);
assert_eq!(violations.len(), 2);
}
#[test]
fn policy_names() {
let mut engine = PolicyEngine::new();
engine.register("alpha", Box::new(|_, _| None), "");
engine.register("beta", Box::new(|_, _| None), "");
assert_eq!(engine.policy_names(), vec!["alpha", "beta"]);
}
}