use super::{PolicyContext, PolicyDecision, PolicyEvaluator};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionLimits {
pub max_duration: Option<Duration>,
pub max_steps: Option<usize>,
pub max_llm_calls: Option<usize>,
pub max_tool_invocations: Option<usize>,
pub max_input_size: Option<usize>,
pub max_output_size: Option<usize>,
pub max_concurrent_branches: Option<usize>,
}
impl Default for ExecutionLimits {
fn default() -> Self {
Self {
max_duration: Some(Duration::from_secs(300)), max_steps: Some(100),
max_llm_calls: Some(50),
max_tool_invocations: Some(100),
max_input_size: Some(1024 * 1024), max_output_size: Some(10 * 1024 * 1024), max_concurrent_branches: Some(10),
}
}
}
impl ExecutionLimits {
pub fn unlimited() -> Self {
Self {
max_duration: None,
max_steps: None,
max_llm_calls: None,
max_tool_invocations: None,
max_input_size: None,
max_output_size: None,
max_concurrent_branches: None,
}
}
pub fn strict() -> Self {
Self {
max_duration: Some(Duration::from_secs(60)), max_steps: Some(20),
max_llm_calls: Some(10),
max_tool_invocations: Some(20),
max_input_size: Some(64 * 1024), max_output_size: Some(1024 * 1024), max_concurrent_branches: Some(3),
}
}
}
#[derive(Debug, Clone)]
pub struct ExecutionPolicy {
pub limits: ExecutionLimits,
pub allow_nested: bool,
pub allow_parallel: bool,
pub require_approval: Vec<String>,
}
impl Default for ExecutionPolicy {
fn default() -> Self {
Self {
limits: ExecutionLimits::default(),
allow_nested: true,
allow_parallel: true,
require_approval: Vec::new(),
}
}
}
impl ExecutionPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn with_limits(mut self, limits: ExecutionLimits) -> Self {
self.limits = limits;
self
}
pub fn no_nested(mut self) -> Self {
self.allow_nested = false;
self
}
pub fn no_parallel(mut self) -> Self {
self.allow_parallel = false;
self
}
pub fn require_approval_for(mut self, action: impl Into<String>) -> Self {
self.require_approval.push(action.into());
self
}
}
impl PolicyEvaluator for ExecutionPolicy {
fn evaluate(&self, context: &PolicyContext) -> PolicyDecision {
match &context.action {
super::PolicyAction::StartExecution { .. } => {
if !self.allow_nested && context.metadata.contains_key("parent_execution_id") {
return PolicyDecision::Deny {
reason: "Nested executions are not allowed".to_string(),
};
}
PolicyDecision::Allow
}
_ => PolicyDecision::Allow,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_execution_limits_default() {
let limits = ExecutionLimits::default();
assert_eq!(limits.max_duration, Some(Duration::from_secs(300)));
assert_eq!(limits.max_steps, Some(100));
assert_eq!(limits.max_llm_calls, Some(50));
assert_eq!(limits.max_tool_invocations, Some(100));
assert_eq!(limits.max_input_size, Some(1024 * 1024));
assert_eq!(limits.max_output_size, Some(10 * 1024 * 1024));
assert_eq!(limits.max_concurrent_branches, Some(10));
}
#[test]
fn test_execution_limits_unlimited() {
let limits = ExecutionLimits::unlimited();
assert!(limits.max_duration.is_none());
assert!(limits.max_steps.is_none());
assert!(limits.max_llm_calls.is_none());
assert!(limits.max_tool_invocations.is_none());
assert!(limits.max_input_size.is_none());
assert!(limits.max_output_size.is_none());
assert!(limits.max_concurrent_branches.is_none());
}
#[test]
fn test_execution_limits_strict() {
let limits = ExecutionLimits::strict();
assert_eq!(limits.max_duration, Some(Duration::from_secs(60)));
assert_eq!(limits.max_steps, Some(20));
assert_eq!(limits.max_llm_calls, Some(10));
assert_eq!(limits.max_tool_invocations, Some(20));
assert_eq!(limits.max_input_size, Some(64 * 1024));
assert_eq!(limits.max_output_size, Some(1024 * 1024));
assert_eq!(limits.max_concurrent_branches, Some(3));
}
#[test]
fn test_execution_policy_default() {
let policy = ExecutionPolicy::default();
assert!(policy.allow_nested);
assert!(policy.allow_parallel);
assert!(policy.require_approval.is_empty());
}
#[test]
fn test_execution_policy_new() {
let policy = ExecutionPolicy::new();
assert!(policy.allow_nested);
assert!(policy.allow_parallel);
}
#[test]
fn test_execution_policy_with_limits() {
let policy = ExecutionPolicy::new().with_limits(ExecutionLimits::strict());
assert_eq!(policy.limits.max_steps, Some(20));
}
#[test]
fn test_execution_policy_no_nested() {
let policy = ExecutionPolicy::new().no_nested();
assert!(!policy.allow_nested);
}
#[test]
fn test_execution_policy_no_parallel() {
let policy = ExecutionPolicy::new().no_parallel();
assert!(!policy.allow_parallel);
}
#[test]
fn test_execution_policy_require_approval() {
let policy = ExecutionPolicy::new()
.require_approval_for("external_api")
.require_approval_for("filesystem_write");
assert!(policy
.require_approval
.contains(&"external_api".to_string()));
assert!(policy
.require_approval
.contains(&"filesystem_write".to_string()));
}
#[test]
fn test_execution_policy_evaluate_start_allowed() {
let policy = ExecutionPolicy::new();
let context = PolicyContext {
tenant_id: None,
user_id: None,
action: super::super::PolicyAction::StartExecution {
graph_id: Some("graph-1".to_string()),
},
metadata: HashMap::new(),
};
let decision = policy.evaluate(&context);
assert!(decision.is_allowed());
}
#[test]
fn test_execution_policy_evaluate_nested_allowed() {
let policy = ExecutionPolicy::new(); let mut metadata = HashMap::new();
metadata.insert("parent_execution_id".to_string(), "exec-123".to_string());
let context = PolicyContext {
tenant_id: None,
user_id: None,
action: super::super::PolicyAction::StartExecution { graph_id: None },
metadata,
};
let decision = policy.evaluate(&context);
assert!(decision.is_allowed());
}
#[test]
fn test_execution_policy_evaluate_nested_denied() {
let policy = ExecutionPolicy::new().no_nested();
let mut metadata = HashMap::new();
metadata.insert("parent_execution_id".to_string(), "exec-123".to_string());
let context = PolicyContext {
tenant_id: None,
user_id: None,
action: super::super::PolicyAction::StartExecution { graph_id: None },
metadata,
};
let decision = policy.evaluate(&context);
assert!(decision.is_denied());
}
#[test]
fn test_execution_policy_evaluate_other_actions_allowed() {
let policy = ExecutionPolicy::new().no_nested().no_parallel();
let context = PolicyContext {
tenant_id: None,
user_id: None,
action: super::super::PolicyAction::LlmCall {
model: "gpt-4".to_string(),
},
metadata: HashMap::new(),
};
let decision = policy.evaluate(&context);
assert!(decision.is_allowed());
}
}