enact_core/policy/
execution_policy.rs1use super::{PolicyContext, PolicyDecision, PolicyEvaluator};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ExecutionLimits {
10 pub max_duration: Option<Duration>,
12 pub max_steps: Option<usize>,
14 pub max_llm_calls: Option<usize>,
16 pub max_tool_invocations: Option<usize>,
18 pub max_input_size: Option<usize>,
20 pub max_output_size: Option<usize>,
22 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)), max_steps: Some(100),
31 max_llm_calls: Some(50),
32 max_tool_invocations: Some(100),
33 max_input_size: Some(1024 * 1024), max_output_size: Some(10 * 1024 * 1024), max_concurrent_branches: Some(10),
36 }
37 }
38}
39
40impl ExecutionLimits {
41 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 pub fn strict() -> Self {
56 Self {
57 max_duration: Some(Duration::from_secs(60)), max_steps: Some(20),
59 max_llm_calls: Some(10),
60 max_tool_invocations: Some(20),
61 max_input_size: Some(64 * 1024), max_output_size: Some(1024 * 1024), max_concurrent_branches: Some(3),
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct ExecutionPolicy {
71 pub limits: ExecutionLimits,
73 pub allow_nested: bool,
75 pub allow_parallel: bool,
77 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 pub fn new() -> Self {
95 Self::default()
96 }
97
98 pub fn with_limits(mut self, limits: ExecutionLimits) -> Self {
100 self.limits = limits;
101 self
102 }
103
104 pub fn no_nested(mut self) -> Self {
106 self.allow_nested = false;
107 self
108 }
109
110 pub fn no_parallel(mut self) -> Self {
112 self.allow_parallel = false;
113 self
114 }
115
116 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 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 #[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 #[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 #[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(); 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 let decision = policy.evaluate(&context);
298 assert!(decision.is_allowed());
299 }
300}