Skip to main content

enact_core/policy/
execution_policy.rs

1//! Execution Policy - Limits and constraints on execution
2
3use super::{PolicyContext, PolicyDecision, PolicyEvaluator};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7/// Execution limits
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ExecutionLimits {
10    /// Maximum execution time
11    pub max_duration: Option<Duration>,
12    /// Maximum number of steps
13    pub max_steps: Option<usize>,
14    /// Maximum number of LLM calls
15    pub max_llm_calls: Option<usize>,
16    /// Maximum number of tool invocations
17    pub max_tool_invocations: Option<usize>,
18    /// Maximum input size in bytes
19    pub max_input_size: Option<usize>,
20    /// Maximum output size in bytes
21    pub max_output_size: Option<usize>,
22    /// Maximum concurrent branches (for parallel execution)
23    pub max_concurrent_branches: Option<usize>,
24}
25
26impl Default for ExecutionLimits {
27    fn default() -> Self {
28        Self {
29            max_duration: Some(Duration::from_secs(300)), // 5 minutes
30            max_steps: Some(100),
31            max_llm_calls: Some(50),
32            max_tool_invocations: Some(100),
33            max_input_size: Some(1024 * 1024),       // 1MB
34            max_output_size: Some(10 * 1024 * 1024), // 10MB
35            max_concurrent_branches: Some(10),
36        }
37    }
38}
39
40impl ExecutionLimits {
41    /// Create unlimited limits (for testing/development)
42    pub fn unlimited() -> Self {
43        Self {
44            max_duration: None,
45            max_steps: None,
46            max_llm_calls: None,
47            max_tool_invocations: None,
48            max_input_size: None,
49            max_output_size: None,
50            max_concurrent_branches: None,
51        }
52    }
53
54    /// Create strict limits (for production)
55    pub fn strict() -> Self {
56        Self {
57            max_duration: Some(Duration::from_secs(60)), // 1 minute
58            max_steps: Some(20),
59            max_llm_calls: Some(10),
60            max_tool_invocations: Some(20),
61            max_input_size: Some(64 * 1024),    // 64KB
62            max_output_size: Some(1024 * 1024), // 1MB
63            max_concurrent_branches: Some(3),
64        }
65    }
66}
67
68/// Execution policy
69#[derive(Debug, Clone)]
70pub struct ExecutionPolicy {
71    /// Execution limits
72    pub limits: ExecutionLimits,
73    /// Allow nested executions
74    pub allow_nested: bool,
75    /// Allow parallel execution
76    pub allow_parallel: bool,
77    /// Require approval for certain actions
78    pub require_approval: Vec<String>,
79}
80
81impl Default for ExecutionPolicy {
82    fn default() -> Self {
83        Self {
84            limits: ExecutionLimits::default(),
85            allow_nested: true,
86            allow_parallel: true,
87            require_approval: Vec::new(),
88        }
89    }
90}
91
92impl ExecutionPolicy {
93    /// Create a new execution policy
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Set limits
99    pub fn with_limits(mut self, limits: ExecutionLimits) -> Self {
100        self.limits = limits;
101        self
102    }
103
104    /// Disallow nested executions
105    pub fn no_nested(mut self) -> Self {
106        self.allow_nested = false;
107        self
108    }
109
110    /// Disallow parallel execution
111    pub fn no_parallel(mut self) -> Self {
112        self.allow_parallel = false;
113        self
114    }
115
116    /// Require approval for specific action types
117    pub fn require_approval_for(mut self, action: impl Into<String>) -> Self {
118        self.require_approval.push(action.into());
119        self
120    }
121}
122
123impl PolicyEvaluator for ExecutionPolicy {
124    fn evaluate(&self, context: &PolicyContext) -> PolicyDecision {
125        match &context.action {
126            super::PolicyAction::StartExecution { .. } => {
127                // Check if nested execution is allowed
128                if !self.allow_nested && context.metadata.contains_key("parent_execution_id") {
129                    return PolicyDecision::Deny {
130                        reason: "Nested executions are not allowed".to_string(),
131                    };
132                }
133                PolicyDecision::Allow
134            }
135            _ => PolicyDecision::Allow,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use std::collections::HashMap;
144
145    // ============ ExecutionLimits Tests ============
146
147    #[test]
148    fn test_execution_limits_default() {
149        let limits = ExecutionLimits::default();
150        assert_eq!(limits.max_duration, Some(Duration::from_secs(300)));
151        assert_eq!(limits.max_steps, Some(100));
152        assert_eq!(limits.max_llm_calls, Some(50));
153        assert_eq!(limits.max_tool_invocations, Some(100));
154        assert_eq!(limits.max_input_size, Some(1024 * 1024));
155        assert_eq!(limits.max_output_size, Some(10 * 1024 * 1024));
156        assert_eq!(limits.max_concurrent_branches, Some(10));
157    }
158
159    #[test]
160    fn test_execution_limits_unlimited() {
161        let limits = ExecutionLimits::unlimited();
162        assert!(limits.max_duration.is_none());
163        assert!(limits.max_steps.is_none());
164        assert!(limits.max_llm_calls.is_none());
165        assert!(limits.max_tool_invocations.is_none());
166        assert!(limits.max_input_size.is_none());
167        assert!(limits.max_output_size.is_none());
168        assert!(limits.max_concurrent_branches.is_none());
169    }
170
171    #[test]
172    fn test_execution_limits_strict() {
173        let limits = ExecutionLimits::strict();
174        assert_eq!(limits.max_duration, Some(Duration::from_secs(60)));
175        assert_eq!(limits.max_steps, Some(20));
176        assert_eq!(limits.max_llm_calls, Some(10));
177        assert_eq!(limits.max_tool_invocations, Some(20));
178        assert_eq!(limits.max_input_size, Some(64 * 1024));
179        assert_eq!(limits.max_output_size, Some(1024 * 1024));
180        assert_eq!(limits.max_concurrent_branches, Some(3));
181    }
182
183    // ============ ExecutionPolicy Tests ============
184
185    #[test]
186    fn test_execution_policy_default() {
187        let policy = ExecutionPolicy::default();
188        assert!(policy.allow_nested);
189        assert!(policy.allow_parallel);
190        assert!(policy.require_approval.is_empty());
191    }
192
193    #[test]
194    fn test_execution_policy_new() {
195        let policy = ExecutionPolicy::new();
196        assert!(policy.allow_nested);
197        assert!(policy.allow_parallel);
198    }
199
200    #[test]
201    fn test_execution_policy_with_limits() {
202        let policy = ExecutionPolicy::new().with_limits(ExecutionLimits::strict());
203        assert_eq!(policy.limits.max_steps, Some(20));
204    }
205
206    #[test]
207    fn test_execution_policy_no_nested() {
208        let policy = ExecutionPolicy::new().no_nested();
209        assert!(!policy.allow_nested);
210    }
211
212    #[test]
213    fn test_execution_policy_no_parallel() {
214        let policy = ExecutionPolicy::new().no_parallel();
215        assert!(!policy.allow_parallel);
216    }
217
218    #[test]
219    fn test_execution_policy_require_approval() {
220        let policy = ExecutionPolicy::new()
221            .require_approval_for("external_api")
222            .require_approval_for("filesystem_write");
223
224        assert!(policy
225            .require_approval
226            .contains(&"external_api".to_string()));
227        assert!(policy
228            .require_approval
229            .contains(&"filesystem_write".to_string()));
230    }
231
232    // ============ PolicyEvaluator Tests ============
233
234    #[test]
235    fn test_execution_policy_evaluate_start_allowed() {
236        let policy = ExecutionPolicy::new();
237        let context = PolicyContext {
238            tenant_id: None,
239            user_id: None,
240            action: super::super::PolicyAction::StartExecution {
241                graph_id: Some("graph-1".to_string()),
242            },
243            metadata: HashMap::new(),
244        };
245
246        let decision = policy.evaluate(&context);
247        assert!(decision.is_allowed());
248    }
249
250    #[test]
251    fn test_execution_policy_evaluate_nested_allowed() {
252        let policy = ExecutionPolicy::new(); // allow_nested = true by default
253        let mut metadata = HashMap::new();
254        metadata.insert("parent_execution_id".to_string(), "exec-123".to_string());
255
256        let context = PolicyContext {
257            tenant_id: None,
258            user_id: None,
259            action: super::super::PolicyAction::StartExecution { graph_id: None },
260            metadata,
261        };
262
263        let decision = policy.evaluate(&context);
264        assert!(decision.is_allowed());
265    }
266
267    #[test]
268    fn test_execution_policy_evaluate_nested_denied() {
269        let policy = ExecutionPolicy::new().no_nested();
270        let mut metadata = HashMap::new();
271        metadata.insert("parent_execution_id".to_string(), "exec-123".to_string());
272
273        let context = PolicyContext {
274            tenant_id: None,
275            user_id: None,
276            action: super::super::PolicyAction::StartExecution { graph_id: None },
277            metadata,
278        };
279
280        let decision = policy.evaluate(&context);
281        assert!(decision.is_denied());
282    }
283
284    #[test]
285    fn test_execution_policy_evaluate_other_actions_allowed() {
286        let policy = ExecutionPolicy::new().no_nested().no_parallel();
287        let context = PolicyContext {
288            tenant_id: None,
289            user_id: None,
290            action: super::super::PolicyAction::LlmCall {
291                model: "gpt-4".to_string(),
292            },
293            metadata: HashMap::new(),
294        };
295
296        // Non-execution actions are always allowed by ExecutionPolicy
297        let decision = policy.evaluate(&context);
298        assert!(decision.is_allowed());
299    }
300}