pub fn degrades(success_count: u64, fail_count: u64, threshold: u64) -> bool {
fail_count > success_count + threshold
}
pub const DEGRADE_THRESHOLD: u64 = 2;
pub mod agent_permissions;
pub mod flow_gate;
pub mod inspectors;
pub mod intent_gate;
pub mod permission;
pub mod rules;
pub mod skill_trust;
pub mod tool_gate;
pub use agent_permissions::{AgentPermissionPolicy, ApprovalMode, ApprovalPreset, TierPosture};
pub use flow_gate::{enforce_flow, flow_fingerprint, FlowEnforcement, PendingFlowApproval};
pub use intent_gate::{
enforce_intent, intent_fingerprint, IntentEnforcement, PendingIntentApproval,
};
pub use inspectors::{
load_adversary_rules_from, AdversaryInspector, EgressInspector, InspectionResult, Inspector,
InspectorChain, RepetitionInspector,
};
pub use permission::{
action_fingerprint, action_text, classify_reversibility, classify_reversibility_with_haystack,
ActionAxes, ApprovalDecision, ApprovalLedger, ApprovalRecord, GateDecision, PermissionGate,
PermissionTier, RiskClassifier,
};
pub use rules::{load_policy_dir, DenyToolParam, PolicyLoadError, PolicyRules};
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 unregister(&mut self, name: &str) -> usize {
let before = self.policies.len();
self.policies.retain(|(n, _, _)| n != name);
before - self.policies.len()
}
pub fn clear(&mut self) -> usize {
let n = self.policies.len();
self.policies.clear();
n
}
pub fn policy_names(&self) -> Vec<String> {
self.policies.iter().map(|(n, _, _)| n.clone()).collect()
}
pub fn policy_details(&self) -> Vec<(String, String)> {
self.policies
.iter()
.map(|(n, d, _)| (n.clone(), d.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;
use serde_json::Value;
fn make_action(tool: &str) -> Action {
{
let mut a = Action::new(ActionType::ToolCall);
a.id = "test".to_string();
a.tool = Some(tool.to_string());
a
}
}
#[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"]);
}
#[test]
fn unregister_removes_the_policy_and_stops_enforcement() {
let mut engine = PolicyEngine::new();
engine.register("deny", Box::new(|_, _| Some("nope".to_string())), "");
engine.register("keep", Box::new(|_, _| None), "");
let state = StateStore::new();
assert_eq!(engine.check(&make_action("x"), &state).len(), 1);
assert_eq!(engine.unregister("deny"), 1);
assert_eq!(engine.policy_names(), vec!["keep"]);
assert!(
engine.check(&make_action("x"), &state).is_empty(),
"an unregistered policy must stop being enforced"
);
}
#[test]
fn unregister_reports_zero_when_nothing_matched() {
let mut engine = PolicyEngine::new();
engine.register("alpha", Box::new(|_, _| None), "");
assert_eq!(engine.unregister("nosuch"), 0);
assert_eq!(engine.policy_names(), vec!["alpha"]);
}
#[test]
fn unregister_removes_every_policy_sharing_the_name() {
let mut engine = PolicyEngine::new();
engine.register("dup", Box::new(|_, _| Some("a".to_string())), "");
engine.register("dup", Box::new(|_, _| Some("b".to_string())), "");
let state = StateStore::new();
assert_eq!(engine.check(&make_action("x"), &state).len(), 2);
assert_eq!(engine.unregister("dup"), 2);
assert!(engine.is_empty());
assert!(engine.check(&make_action("x"), &state).is_empty());
}
#[test]
fn clear_drops_everything() {
let mut engine = PolicyEngine::new();
engine.register("a", Box::new(|_, _| None), "");
engine.register("b", Box::new(|_, _| None), "");
assert_eq!(engine.clear(), 2);
assert!(engine.is_empty());
}
#[test]
fn policy_details_carries_descriptions() {
let mut engine = PolicyEngine::new();
engine.register("alpha", Box::new(|_, _| None), "first one");
assert_eq!(
engine.policy_details(),
vec![("alpha".to_string(), "first one".to_string())]
);
}
}