use anyhow::{anyhow, Result};
use chrono::{DateTime, Timelike, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info, warn};
use uuid::Uuid;
pub struct EnhancedPolicyEngine {
rules: HashMap<String, PolicyRule>,
default_effect: PolicyEffect,
}
impl EnhancedPolicyEngine {
pub fn new() -> Self {
Self {
rules: HashMap::new(),
default_effect: PolicyEffect::Deny,
}
}
pub fn add_rule(&mut self, rule: PolicyRule) -> Result<String> {
let rule_id = rule.id.clone();
for existing in self.rules.values() {
if existing.priority == rule.priority
&& Self::rules_overlap(&existing.actions, &rule.actions)
{
warn!("Policy rule {} may conflict with {}", rule_id, existing.id);
}
}
self.rules.insert(rule_id.clone(), rule);
debug!("Added policy rule: {}", rule_id);
Ok(rule_id)
}
pub fn remove_rule(&mut self, rule_id: &str) -> Result<()> {
self.rules
.remove(rule_id)
.ok_or_else(|| anyhow!("Rule not found: {}", rule_id))?;
Ok(())
}
pub fn evaluate_action(&self, action: &str, context: &PolicyContext) -> PolicyDecision {
debug!(
"Evaluating action: {} for agent: {}",
action, context.agent_id
);
let mut matching_rules: Vec<&PolicyRule> = self
.rules
.values()
.filter(|rule| rule.matches(action, context))
.collect();
matching_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
if let Some(rule) = matching_rules.first() {
let allowed = matches!(rule.effect, PolicyEffect::Allow);
info!(
"Action {} {} by rule {}",
action,
if allowed { "allowed" } else { "denied" },
rule.id
);
PolicyDecision {
allowed,
rule_id: Some(rule.id.clone()),
reason: rule.description.clone(),
}
} else {
let allowed = matches!(self.default_effect, PolicyEffect::Allow);
PolicyDecision {
allowed,
rule_id: None,
reason: format!("No matching rule, default: {:?}", self.default_effect),
}
}
}
pub fn list_rules(&self) -> Vec<(String, PolicyRule)> {
self.rules
.iter()
.map(|(id, rule)| (id.clone(), rule.clone()))
.collect()
}
pub fn get_rule(&self, rule_id: &str) -> Option<&PolicyRule> {
self.rules.get(rule_id)
}
pub fn set_default_effect(&mut self, effect: PolicyEffect) {
self.default_effect = effect;
}
fn rules_overlap(actions1: &[String], actions2: &[String]) -> bool {
for a1 in actions1 {
for a2 in actions2 {
if a1 == a2 || a1 == "*" || a2 == "*" {
return true;
}
if (a1.ends_with("*") && a2.starts_with(a1.trim_end_matches('*')))
|| (a2.ends_with("*") && a1.starts_with(a2.trim_end_matches('*')))
{
return true;
}
}
}
false
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PolicyRule {
pub id: String,
pub name: String,
pub description: String,
pub effect: PolicyEffect,
pub priority: u32,
pub actions: Vec<String>,
pub conditions: Vec<PolicyCondition>,
}
impl PolicyRule {
pub fn new(name: String, effect: PolicyEffect) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name,
description: String::new(),
effect,
priority: 100,
actions: Vec::new(),
conditions: Vec::new(),
}
}
pub fn with_description(mut self, desc: String) -> Self {
self.description = desc;
self
}
pub fn with_priority(mut self, priority: u32) -> Self {
self.priority = priority;
self
}
pub fn with_actions(mut self, actions: Vec<String>) -> Self {
self.actions = actions;
self
}
pub fn with_condition(mut self, condition: PolicyCondition) -> Self {
self.conditions.push(condition);
self
}
pub fn matches(&self, action: &str, context: &PolicyContext) -> bool {
let action_matches = self.actions.iter().any(|a| {
a == "*"
|| a == action
|| (a.ends_with("*") && action.starts_with(a.trim_end_matches('*')))
});
if !action_matches {
return false;
}
self.conditions
.iter()
.all(|condition| condition.evaluate(context))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PolicyCondition {
pub field: String,
pub operator: ConditionOperator,
pub value: serde_json::Value,
}
impl PolicyCondition {
pub fn evaluate(&self, context: &PolicyContext) -> bool {
let field_value = match self.field.as_str() {
"agent_id" => Some(serde_json::Value::String(context.agent_id.clone())),
"time_of_day" => {
let hour = Utc::now().hour();
Some(serde_json::Value::Number(hour.into()))
}
"resource_type" => context.metadata.get("resource_type").cloned(),
"environment" => context.metadata.get("environment").cloned(),
_ => context.metadata.get(&self.field).cloned(),
};
match (&self.operator, field_value) {
(ConditionOperator::Equals, Some(val)) => val == self.value,
(ConditionOperator::NotEquals, Some(val)) => val != self.value,
(ConditionOperator::In, Some(val)) => {
if let Some(array) = self.value.as_array() {
array.contains(&val)
} else {
false
}
}
(ConditionOperator::NotIn, Some(val)) => {
if let Some(array) = self.value.as_array() {
!array.contains(&val)
} else {
true
}
}
(ConditionOperator::GreaterThan, Some(val)) => {
if let (Some(v1), Some(v2)) = (val.as_f64(), self.value.as_f64()) {
v1 > v2
} else {
false
}
}
(ConditionOperator::LessThan, Some(val)) => {
if let (Some(v1), Some(v2)) = (val.as_f64(), self.value.as_f64()) {
v1 < v2
} else {
false
}
}
(ConditionOperator::Contains, Some(val)) => {
if let (Some(s1), Some(s2)) = (val.as_str(), self.value.as_str()) {
s1.contains(s2)
} else {
false
}
}
_ => false,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ConditionOperator {
Equals,
NotEquals,
In,
NotIn,
GreaterThan,
LessThan,
Contains,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum PolicyEffect {
Allow,
Deny,
}
#[derive(Clone, Debug, Default)]
pub struct PolicyContext {
pub agent_id: String,
pub timestamp: Option<DateTime<Utc>>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl PolicyContext {
pub fn new(agent_id: String) -> Self {
Self {
agent_id,
timestamp: Some(Utc::now()),
metadata: HashMap::new(),
}
}
pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
self.metadata.insert(key, value);
self
}
}
#[derive(Debug)]
pub struct PolicyDecision {
pub allowed: bool,
pub rule_id: Option<String>,
pub reason: String,
}
pub fn create_default_policies() -> Vec<PolicyRule> {
vec![
PolicyRule::new("allow_document_loaders".to_string(), PolicyEffect::Allow)
.with_description("Allow document loader tools for file processing".to_string())
.with_priority(400) .with_actions(vec![
"tool:csv_loader".to_string(),
"tool:json_yaml_loader".to_string(),
"tool:html_loader".to_string(),
"tool:pdf_loader".to_string(),
]),
PolicyRule::new("allow_test_tools".to_string(), PolicyEffect::Allow)
.with_description("Allow test tools and mock tools for testing".to_string())
.with_priority(300) .with_actions(vec![
"tool:test_tool".to_string(),
"tool:failing_tool".to_string(),
"tool:param_tool".to_string(),
"tool:timing_tool".to_string(),
]),
PolicyRule::new("allow_safe_commands".to_string(), PolicyEffect::Allow)
.with_description("Allow safe system commands".to_string())
.with_priority(150)
.with_actions(vec!["tool:command".to_string()])
.with_condition(PolicyCondition {
field: "command".to_string(),
operator: ConditionOperator::In,
value: serde_json::json!(["echo", "ls", "dir", "pwd", "whoami", "date"]),
}),
PolicyRule::new("allow_all_file_ops".to_string(), PolicyEffect::Allow)
.with_description("Allow all file operations for testing".to_string())
.with_priority(250) .with_actions(vec![
"tool:create_file".to_string(),
"tool:edit_file".to_string(),
]),
PolicyRule::new("safe_file_ops".to_string(), PolicyEffect::Allow)
.with_description("Allow file operations in safe directories".to_string())
.with_priority(100)
.with_actions(vec![
"tool:create_file".to_string(),
"tool:edit_file".to_string(),
])
.with_condition(PolicyCondition {
field: "path".to_string(),
operator: ConditionOperator::NotIn,
value: serde_json::json!(["/etc", "/bin", "/sbin", "C:\\Windows"]),
}),
PolicyRule::new("deny_dangerous_commands".to_string(), PolicyEffect::Deny)
.with_description("Block dangerous system commands".to_string())
.with_priority(200)
.with_actions(vec!["tool:command".to_string()])
.with_condition(PolicyCondition {
field: "command".to_string(),
operator: ConditionOperator::In,
value: serde_json::json!(["rm -rf", "sudo", "format", "mkfs"]),
}),
PolicyRule::new("safe_http".to_string(), PolicyEffect::Allow)
.with_description("Allow HTTP requests to safe domains".to_string())
.with_priority(100)
.with_actions(vec!["tool:http".to_string()])
.with_condition(PolicyCondition {
field: "domain".to_string(),
operator: ConditionOperator::In,
value: serde_json::json!(["localhost", "127.0.0.1", "api.openai.com"]),
}),
PolicyRule::new("business_hours_only".to_string(), PolicyEffect::Allow)
.with_description("Allow operations only during business hours".to_string())
.with_priority(50)
.with_actions(vec!["*".to_string()])
.with_condition(PolicyCondition {
field: "time_of_day".to_string(),
operator: ConditionOperator::GreaterThan,
value: serde_json::json!(8),
})
.with_condition(PolicyCondition {
field: "time_of_day".to_string(),
operator: ConditionOperator::LessThan,
value: serde_json::json!(18),
}),
]
}
pub struct PolicyEngine {
policies: Vec<String>,
}
impl PolicyEngine {
pub fn new() -> Self {
Self {
policies: Vec::new(),
}
}
pub fn validate(&self, action: &str) -> bool {
!self.policies.iter().any(|p| action.contains(p))
}
pub fn add_policy(&mut self, policy: String) {
self.policies.push(policy);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_enhanced_policy_engine_creation() {
let engine = EnhancedPolicyEngine::new();
assert_eq!(engine.rules.len(), 0);
assert!(matches!(engine.default_effect, PolicyEffect::Deny));
}
#[test]
fn test_policy_rule_creation() {
let rule = PolicyRule::new("test_rule".to_string(), PolicyEffect::Allow)
.with_description("Test rule description".to_string())
.with_priority(150)
.with_actions(vec!["test_action".to_string()]);
assert_eq!(rule.name, "test_rule");
assert!(matches!(rule.effect, PolicyEffect::Allow));
assert_eq!(rule.description, "Test rule description");
assert_eq!(rule.priority, 150);
assert_eq!(rule.actions, vec!["test_action"]);
}
#[test]
fn test_policy_context_creation() {
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("environment".to_string(), json!("production"))
.with_metadata("resource_type".to_string(), json!("file"));
assert_eq!(context.agent_id, "agent_123");
assert!(context.timestamp.is_some());
assert_eq!(
context.metadata.get("environment"),
Some(&json!("production"))
);
assert_eq!(context.metadata.get("resource_type"), Some(&json!("file")));
}
#[test]
fn test_add_rule_to_engine() {
let mut engine = EnhancedPolicyEngine::new();
let rule = PolicyRule::new("test_rule".to_string(), PolicyEffect::Allow);
let rule_id = engine.add_rule(rule).unwrap();
assert_eq!(engine.rules.len(), 1);
assert!(engine.rules.contains_key(&rule_id));
}
#[test]
fn test_remove_rule_from_engine() {
let mut engine = EnhancedPolicyEngine::new();
let rule = PolicyRule::new("test_rule".to_string(), PolicyEffect::Allow);
let rule_id = engine.add_rule(rule).unwrap();
let result = engine.remove_rule(&rule_id);
assert!(result.is_ok());
assert_eq!(engine.rules.len(), 0);
let result = engine.remove_rule("nonexistent");
assert!(result.is_err());
}
#[test]
fn test_policy_rule_action_matching() {
let rule = PolicyRule::new("test_rule".to_string(), PolicyEffect::Allow)
.with_actions(vec!["tool:create_file".to_string()]);
let context = PolicyContext::new("agent_123".to_string());
assert!(rule.matches("tool:create_file", &context));
assert!(!rule.matches("tool:delete_file", &context));
}
#[test]
fn test_policy_rule_wildcard_matching() {
let rule = PolicyRule::new("test_rule".to_string(), PolicyEffect::Allow)
.with_actions(vec!["tool:*".to_string()]);
let context = PolicyContext::new("agent_123".to_string());
assert!(rule.matches("tool:create_file", &context));
assert!(rule.matches("tool:delete_file", &context));
assert!(!rule.matches("llm:complete", &context));
}
#[test]
fn test_policy_rule_universal_wildcard() {
let rule = PolicyRule::new("test_rule".to_string(), PolicyEffect::Allow)
.with_actions(vec!["*".to_string()]);
let context = PolicyContext::new("agent_123".to_string());
assert!(rule.matches("tool:create_file", &context));
assert!(rule.matches("llm:complete", &context));
assert!(rule.matches("any_action", &context));
}
#[test]
fn test_policy_condition_equals() {
let condition = PolicyCondition {
field: "agent_id".to_string(),
operator: ConditionOperator::Equals,
value: json!("agent_123"),
};
let context = PolicyContext::new("agent_123".to_string());
assert!(condition.evaluate(&context));
let context = PolicyContext::new("agent_456".to_string());
assert!(!condition.evaluate(&context));
}
#[test]
fn test_policy_condition_not_equals() {
let condition = PolicyCondition {
field: "agent_id".to_string(),
operator: ConditionOperator::NotEquals,
value: json!("agent_123"),
};
let context = PolicyContext::new("agent_456".to_string());
assert!(condition.evaluate(&context));
let context = PolicyContext::new("agent_123".to_string());
assert!(!condition.evaluate(&context));
}
#[test]
fn test_policy_condition_in() {
let condition = PolicyCondition {
field: "environment".to_string(),
operator: ConditionOperator::In,
value: json!(["production", "staging"]),
};
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("environment".to_string(), json!("production"));
assert!(condition.evaluate(&context));
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("environment".to_string(), json!("development"));
assert!(!condition.evaluate(&context));
}
#[test]
fn test_policy_condition_not_in() {
let condition = PolicyCondition {
field: "environment".to_string(),
operator: ConditionOperator::NotIn,
value: json!(["production", "staging"]),
};
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("environment".to_string(), json!("development"));
assert!(condition.evaluate(&context));
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("environment".to_string(), json!("production"));
assert!(!condition.evaluate(&context));
}
#[test]
fn test_policy_condition_greater_than() {
let condition = PolicyCondition {
field: "score".to_string(),
operator: ConditionOperator::GreaterThan,
value: json!(80),
};
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("score".to_string(), json!(90));
assert!(condition.evaluate(&context));
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("score".to_string(), json!(70));
assert!(!condition.evaluate(&context));
}
#[test]
fn test_policy_condition_less_than() {
let condition = PolicyCondition {
field: "score".to_string(),
operator: ConditionOperator::LessThan,
value: json!(80),
};
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("score".to_string(), json!(70));
assert!(condition.evaluate(&context));
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("score".to_string(), json!(90));
assert!(!condition.evaluate(&context));
}
#[test]
fn test_policy_condition_contains() {
let condition = PolicyCondition {
field: "path".to_string(),
operator: ConditionOperator::Contains,
value: json!("/safe/"),
};
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("path".to_string(), json!("/safe/documents/file.txt"));
assert!(condition.evaluate(&context));
let context = PolicyContext::new("agent_123".to_string())
.with_metadata("path".to_string(), json!("/etc/passwd"));
assert!(!condition.evaluate(&context));
}
#[test]
fn test_policy_condition_time_of_day() {
let condition = PolicyCondition {
field: "time_of_day".to_string(),
operator: ConditionOperator::GreaterThan,
value: json!(-1), };
let context = PolicyContext::new("agent_123".to_string());
assert!(condition.evaluate(&context));
}
#[test]
fn test_policy_rule_with_conditions() {
let rule = PolicyRule::new("safe_file_ops".to_string(), PolicyEffect::Allow)
.with_actions(vec!["tool:create_file".to_string()])
.with_condition(PolicyCondition {
field: "path".to_string(),
operator: ConditionOperator::Contains,
value: json!("/safe/"),
});
let safe_context = PolicyContext::new("agent_123".to_string())
.with_metadata("path".to_string(), json!("/safe/file.txt"));
assert!(rule.matches("tool:create_file", &safe_context));
let unsafe_context = PolicyContext::new("agent_123".to_string())
.with_metadata("path".to_string(), json!("/etc/passwd"));
assert!(!rule.matches("tool:create_file", &unsafe_context));
}
#[test]
fn test_policy_rule_multiple_conditions() {
let rule = PolicyRule::new("restricted_access".to_string(), PolicyEffect::Allow)
.with_actions(vec!["tool:command".to_string()])
.with_condition(PolicyCondition {
field: "agent_id".to_string(),
operator: ConditionOperator::Equals,
value: json!("admin_agent"),
})
.with_condition(PolicyCondition {
field: "environment".to_string(),
operator: ConditionOperator::Equals,
value: json!("production"),
});
let valid_context = PolicyContext::new("admin_agent".to_string())
.with_metadata("environment".to_string(), json!("production"));
assert!(rule.matches("tool:command", &valid_context));
let invalid_context = PolicyContext::new("admin_agent".to_string())
.with_metadata("environment".to_string(), json!("development"));
assert!(!rule.matches("tool:command", &invalid_context));
}
#[test]
fn test_engine_evaluate_action_allow() {
let mut engine = EnhancedPolicyEngine::new();
let rule = PolicyRule::new("allow_rule".to_string(), PolicyEffect::Allow)
.with_actions(vec!["test_action".to_string()]);
engine.add_rule(rule).unwrap();
let context = PolicyContext::new("agent_123".to_string());
let decision = engine.evaluate_action("test_action", &context);
assert!(decision.allowed);
assert!(decision.rule_id.is_some());
}
#[test]
fn test_engine_evaluate_action_deny() {
let mut engine = EnhancedPolicyEngine::new();
let rule = PolicyRule::new("deny_rule".to_string(), PolicyEffect::Deny)
.with_actions(vec!["test_action".to_string()]);
engine.add_rule(rule).unwrap();
let context = PolicyContext::new("agent_123".to_string());
let decision = engine.evaluate_action("test_action", &context);
assert!(!decision.allowed);
assert!(decision.rule_id.is_some());
}
#[test]
fn test_engine_evaluate_action_no_match() {
let engine = EnhancedPolicyEngine::new();
let context = PolicyContext::new("agent_123".to_string());
let decision = engine.evaluate_action("test_action", &context);
assert!(!decision.allowed);
assert!(decision.rule_id.is_none());
assert!(decision.reason.contains("default"));
}
#[test]
fn test_engine_priority_handling() {
let mut engine = EnhancedPolicyEngine::new();
let low_priority = PolicyRule::new("low_priority".to_string(), PolicyEffect::Allow)
.with_priority(50)
.with_actions(vec!["test_action".to_string()]);
engine.add_rule(low_priority).unwrap();
let high_priority = PolicyRule::new("high_priority".to_string(), PolicyEffect::Deny)
.with_priority(100)
.with_actions(vec!["test_action".to_string()]);
engine.add_rule(high_priority).unwrap();
let context = PolicyContext::new("agent_123".to_string());
let decision = engine.evaluate_action("test_action", &context);
assert!(!decision.allowed);
assert!(decision.rule_id.is_some());
}
#[test]
fn test_engine_set_default_effect() {
let mut engine = EnhancedPolicyEngine::new();
engine.set_default_effect(PolicyEffect::Allow);
let context = PolicyContext::new("agent_123".to_string());
let decision = engine.evaluate_action("unknown_action", &context);
assert!(decision.allowed);
assert!(decision.rule_id.is_none());
}
#[test]
fn test_engine_list_rules() {
let mut engine = EnhancedPolicyEngine::new();
let rule1 = PolicyRule::new("rule1".to_string(), PolicyEffect::Allow);
let rule2 = PolicyRule::new("rule2".to_string(), PolicyEffect::Deny);
engine.add_rule(rule1).unwrap();
engine.add_rule(rule2).unwrap();
let rules = engine.list_rules();
assert_eq!(rules.len(), 2);
assert!(rules.iter().any(|(_, rule)| rule.name == "rule1"));
assert!(rules.iter().any(|(_, rule)| rule.name == "rule2"));
}
#[test]
fn test_engine_get_rule() {
let mut engine = EnhancedPolicyEngine::new();
let rule = PolicyRule::new("test_rule".to_string(), PolicyEffect::Allow);
let rule_id = engine.add_rule(rule).unwrap();
let retrieved = engine.get_rule(&rule_id);
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().name, "test_rule");
let not_found = engine.get_rule("nonexistent");
assert!(not_found.is_none());
}
#[test]
fn test_rules_overlap_detection() {
let actions1 = vec!["tool:create_file".to_string()];
let actions2 = vec!["tool:create_file".to_string()];
assert!(EnhancedPolicyEngine::rules_overlap(&actions1, &actions2));
let actions1 = vec!["tool:*".to_string()];
let actions2 = vec!["tool:create_file".to_string()];
assert!(EnhancedPolicyEngine::rules_overlap(&actions1, &actions2));
let actions1 = vec!["*".to_string()];
let actions2 = vec!["anything".to_string()];
assert!(EnhancedPolicyEngine::rules_overlap(&actions1, &actions2));
let actions1 = vec!["tool:create_file".to_string()];
let actions2 = vec!["llm:complete".to_string()];
assert!(!EnhancedPolicyEngine::rules_overlap(&actions1, &actions2));
}
#[test]
fn test_create_default_policies() {
let policies = create_default_policies();
assert!(!policies.is_empty());
assert!(policies.iter().any(|p| p.name == "allow_document_loaders"));
assert!(policies.iter().any(|p| p.name == "safe_file_ops"));
assert!(policies.iter().any(|p| p.name == "deny_dangerous_commands"));
assert!(policies.iter().any(|p| p.name == "safe_http"));
assert!(policies.iter().any(|p| p.name == "business_hours_only"));
}
#[test]
fn test_default_policy_safe_file_ops() {
let policies = create_default_policies();
let safe_file_rule = policies.iter().find(|p| p.name == "safe_file_ops").unwrap();
let safe_context = PolicyContext::new("agent_123".to_string())
.with_metadata("path".to_string(), json!("/home/user/file.txt"));
assert!(safe_file_rule.matches("tool:create_file", &safe_context));
let unsafe_context = PolicyContext::new("agent_123".to_string())
.with_metadata("path".to_string(), json!("/etc"));
assert!(!safe_file_rule.matches("tool:create_file", &unsafe_context));
}
#[test]
fn test_default_policy_deny_dangerous_commands() {
let policies = create_default_policies();
let dangerous_rule = policies
.iter()
.find(|p| p.name == "deny_dangerous_commands")
.unwrap();
let dangerous_context = PolicyContext::new("agent_123".to_string())
.with_metadata("command".to_string(), json!("rm -rf"));
assert!(dangerous_rule.matches("tool:command", &dangerous_context));
assert!(matches!(dangerous_rule.effect, PolicyEffect::Deny));
}
#[test]
fn test_legacy_policy_engine() {
let mut engine = PolicyEngine::new();
assert!(engine.validate("test_action"));
engine.add_policy("dangerous".to_string());
assert!(!engine.validate("dangerous_action"));
assert!(engine.validate("safe_action"));
}
#[test]
fn test_policy_decision_debug() {
let decision = PolicyDecision {
allowed: true,
rule_id: Some("test_rule".to_string()),
reason: "Test reason".to_string(),
};
let debug_str = format!("{:?}", decision);
assert!(debug_str.contains("allowed: true"));
assert!(debug_str.contains("test_rule"));
assert!(debug_str.contains("Test reason"));
}
}