1use ai_agents_core::PermissionOutcome;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ToolSecurityConfig {
8 #[serde(default)]
10 pub enabled: bool,
11 #[serde(default)]
13 pub fail_closed: bool,
14 #[serde(default = "default_tool_timeout")]
16 pub default_timeout_ms: u64,
17 #[serde(default)]
19 pub tools: HashMap<String, ToolPolicyConfig>,
20}
21
22impl Default for ToolSecurityConfig {
23 fn default() -> Self {
24 Self {
25 enabled: false,
26 fail_closed: false,
27 default_timeout_ms: default_tool_timeout(),
28 tools: HashMap::new(),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
35#[serde(rename_all = "snake_case")]
36pub enum NoWritePolicyBehavior {
37 Deny,
38 #[default]
39 DryRunOnly,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct CommandRuleConfig {
45 #[serde(default)]
47 pub argv: Vec<String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct CommandTemplateConfig {
53 pub name: String,
55 #[serde(default)]
57 pub argv: Vec<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ToolPolicyConfig {
63 #[serde(default = "default_true")]
65 pub enabled: bool,
66 #[serde(default, alias = "require_approval")]
68 pub require_confirmation: bool,
69 #[serde(default)]
71 pub allow_without_confirmation: bool,
72 #[serde(default)]
74 pub confirmation_message: Option<String>,
75 #[serde(default)]
77 pub rate_limit: Option<u32>,
78 #[serde(default)]
80 pub timeout_ms: Option<u64>,
81 #[serde(default)]
83 pub allowed_domains: Vec<String>,
84 #[serde(default)]
86 pub blocked_domains: Vec<String>,
87 #[serde(default)]
89 pub allowed_paths: Vec<String>,
90 #[serde(default)]
92 pub read_paths: Vec<String>,
93 #[serde(default)]
95 pub write_paths: Vec<String>,
96 #[serde(default)]
98 pub blocked_paths: Vec<String>,
99 #[serde(default)]
101 pub max_file_size_bytes: Option<u64>,
102 #[serde(default)]
104 pub max_output_chars: Option<usize>,
105 #[serde(default)]
107 pub max_results: Option<usize>,
108 #[serde(default)]
110 pub max_response_bytes: Option<usize>,
111 #[serde(default = "default_true")]
113 pub blocked_private_networks: bool,
114 #[serde(default)]
116 pub allowed_schemes: Vec<String>,
117 #[serde(default)]
119 pub allowed_ports: Vec<u16>,
120 #[serde(default)]
122 pub max_redirects: Option<usize>,
123 #[serde(default)]
125 pub max_changed_files: Option<usize>,
126 #[serde(default)]
128 pub max_changed_lines: Option<usize>,
129 #[serde(default)]
131 pub max_replacements: Option<usize>,
132 #[serde(default)]
134 pub require_read_before_write: bool,
135 #[serde(default)]
137 pub overwrite_existing: bool,
138 #[serde(default)]
140 pub create_parent_dirs: bool,
141 #[serde(default)]
143 pub no_write_policy: NoWritePolicyBehavior,
144 #[serde(default)]
146 pub allowed_commands: Vec<CommandRuleConfig>,
147 #[serde(default)]
149 pub command_templates: Vec<CommandTemplateConfig>,
150 #[serde(default)]
152 pub working_dirs: Vec<String>,
153 #[serde(default)]
155 pub env_passthrough: Vec<String>,
156 #[serde(default)]
158 pub redact_env: Vec<String>,
159 #[serde(default = "default_true")]
161 pub deny_shell: bool,
162 #[serde(default = "default_true")]
164 pub deny_interactive: bool,
165 #[serde(default)]
167 pub allow_command_escalation: bool,
168 #[serde(default)]
170 pub domains: DomainPolicyConfig,
171 #[serde(default)]
173 pub paths: PathPolicyConfig,
174 #[serde(default)]
176 pub commands: CommandPolicyConfig,
177 #[serde(default)]
179 pub operations: OperationPolicyConfig,
180 #[serde(default)]
182 pub config: HashMap<String, serde_json::Value>,
183}
184
185impl Default for ToolPolicyConfig {
186 fn default() -> Self {
187 Self {
188 enabled: true,
189 require_confirmation: false,
190 allow_without_confirmation: false,
191 confirmation_message: None,
192 rate_limit: None,
193 timeout_ms: None,
194 allowed_domains: Vec::new(),
195 blocked_domains: Vec::new(),
196 allowed_paths: Vec::new(),
197 read_paths: Vec::new(),
198 write_paths: Vec::new(),
199 blocked_paths: Vec::new(),
200 max_file_size_bytes: None,
201 max_output_chars: None,
202 max_results: None,
203 max_response_bytes: None,
204 blocked_private_networks: true,
205 allowed_schemes: Vec::new(),
206 allowed_ports: Vec::new(),
207 max_redirects: None,
208 max_changed_files: None,
209 max_changed_lines: None,
210 max_replacements: None,
211 require_read_before_write: false,
212 overwrite_existing: false,
213 create_parent_dirs: false,
214 no_write_policy: NoWritePolicyBehavior::default(),
215 allowed_commands: Vec::new(),
216 command_templates: Vec::new(),
217 working_dirs: Vec::new(),
218 env_passthrough: Vec::new(),
219 redact_env: Vec::new(),
220 deny_shell: true,
221 deny_interactive: true,
222 allow_command_escalation: false,
223 domains: DomainPolicyConfig::default(),
224 paths: PathPolicyConfig::default(),
225 commands: CommandPolicyConfig::default(),
226 operations: OperationPolicyConfig::default(),
227 config: HashMap::new(),
228 }
229 }
230}
231
232#[derive(Debug, Clone, Default, Serialize, Deserialize)]
234pub struct DomainPolicyConfig {
235 #[serde(default)]
236 pub allow: Vec<String>,
237 #[serde(default)]
238 pub deny: Vec<String>,
239 #[serde(default)]
240 pub requires_approval: Vec<String>,
241 #[serde(default)]
242 pub unavailable: Vec<String>,
243}
244
245#[derive(Debug, Clone, Default, Serialize, Deserialize)]
247pub struct PathPolicyConfig {
248 #[serde(default)]
249 pub allow: Vec<String>,
250 #[serde(default)]
251 pub deny: Vec<String>,
252 #[serde(default)]
253 pub requires_approval: Vec<String>,
254 #[serde(default)]
255 pub unavailable: Vec<String>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct CommandPolicyConfig {
261 #[serde(default)]
262 pub allow: Vec<String>,
263 #[serde(default)]
264 pub deny: Vec<String>,
265 #[serde(default)]
266 pub requires_approval: Vec<String>,
267 #[serde(default)]
268 pub unavailable: Vec<String>,
269 #[serde(default)]
270 pub allowed_commands: Vec<CommandRuleConfig>,
271 #[serde(default)]
272 pub templates: Vec<CommandTemplateConfig>,
273 #[serde(default)]
274 pub working_dirs: Vec<String>,
275 #[serde(default)]
276 pub env_passthrough: Vec<String>,
277 #[serde(default = "default_true")]
278 pub deny_shell: bool,
279 #[serde(default = "default_true")]
280 pub deny_interactive: bool,
281 #[serde(default)]
282 pub allow_escalation: bool,
283}
284
285impl Default for CommandPolicyConfig {
286 fn default() -> Self {
287 Self {
288 allow: Vec::new(),
289 deny: Vec::new(),
290 requires_approval: Vec::new(),
291 unavailable: Vec::new(),
292 allowed_commands: Vec::new(),
293 templates: Vec::new(),
294 working_dirs: Vec::new(),
295 env_passthrough: Vec::new(),
296 deny_shell: true,
297 deny_interactive: true,
298 allow_escalation: false,
299 }
300 }
301}
302
303#[derive(Debug, Clone, Default, Serialize, Deserialize)]
305pub struct OperationPolicyConfig {
306 #[serde(default)]
307 pub allow: Vec<String>,
308 #[serde(default)]
309 pub deny: Vec<String>,
310 #[serde(default)]
311 pub requires_approval: Vec<String>,
312 #[serde(default)]
313 pub unavailable: Vec<String>,
314}
315
316#[derive(Debug, Clone)]
318pub enum SecurityCheckResult {
319 Allow,
320 Block { reason: String },
321 Warn { message: String },
322 RequireConfirmation { message: String },
323 Unavailable { reason: String },
324}
325
326impl SecurityCheckResult {
327 pub fn is_allowed(&self) -> bool {
329 matches!(
330 self,
331 SecurityCheckResult::Allow | SecurityCheckResult::Warn { .. }
332 )
333 }
334
335 pub fn is_blocked(&self) -> bool {
337 matches!(
338 self,
339 SecurityCheckResult::Block { .. } | SecurityCheckResult::Unavailable { .. }
340 )
341 }
342
343 pub fn outcome(&self) -> PermissionOutcome {
345 match self {
346 SecurityCheckResult::Allow | SecurityCheckResult::Warn { .. } => {
347 PermissionOutcome::Allow
348 }
349 SecurityCheckResult::Block { .. } => PermissionOutcome::Deny,
350 SecurityCheckResult::RequireConfirmation { .. } => PermissionOutcome::RequiresApproval,
351 SecurityCheckResult::Unavailable { .. } => PermissionOutcome::Unavailable,
352 }
353 }
354
355 pub fn reason(&self) -> Option<&str> {
357 match self {
358 SecurityCheckResult::Allow => None,
359 SecurityCheckResult::Block { reason } => Some(reason),
360 SecurityCheckResult::Warn { message } => Some(message),
361 SecurityCheckResult::RequireConfirmation { message } => Some(message),
362 SecurityCheckResult::Unavailable { reason } => Some(reason),
363 }
364 }
365
366 pub fn requires_approval(&self) -> bool {
368 matches!(self, SecurityCheckResult::RequireConfirmation { .. })
369 }
370
371 pub fn is_unavailable(&self) -> bool {
373 matches!(self, SecurityCheckResult::Unavailable { .. })
374 }
375}
376
377fn default_tool_timeout() -> u64 {
378 30000
379}
380
381fn default_true() -> bool {
382 true
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_default_config() {
391 let config = ToolSecurityConfig::default();
392 assert!(!config.enabled);
393 assert_eq!(config.default_timeout_ms, 30000);
394 assert!(config.tools.is_empty());
395 }
396
397 #[test]
398 fn test_yaml_parsing() {
399 let yaml = r#"
400enabled: true
401default_timeout_ms: 10000
402tools:
403 http:
404 rate_limit: 10
405 blocked_domains:
406 - evil.com
407 allowed_domains:
408 - api.example.com
409 file_write:
410 require_confirmation: true
411 confirmation_message: "Are you sure you want to write this file?"
412 allowed_paths:
413 - /tmp/
414"#;
415 let config: ToolSecurityConfig = serde_yaml::from_str(yaml).unwrap();
416 assert!(config.enabled);
417 assert_eq!(config.default_timeout_ms, 10000);
418 assert!(config.tools.contains_key("http"));
419 assert!(config.tools.contains_key("file_write"));
420
421 let http = config.tools.get("http").unwrap();
422 assert_eq!(http.rate_limit, Some(10));
423 assert_eq!(http.blocked_domains, vec!["evil.com"]);
424
425 let file_write = config.tools.get("file_write").unwrap();
426 assert!(file_write.require_confirmation);
427 }
428
429 #[test]
430 fn test_security_check_result() {
431 let allow = SecurityCheckResult::Allow;
432 assert!(allow.is_allowed());
433 assert!(!allow.is_blocked());
434
435 let block = SecurityCheckResult::Block {
436 reason: "test".into(),
437 };
438 assert!(!block.is_allowed());
439 assert!(block.is_blocked());
440
441 let warn = SecurityCheckResult::Warn {
442 message: "warning".into(),
443 };
444 assert!(warn.is_allowed());
445 assert!(!warn.is_blocked());
446 }
447
448 #[test]
449 fn test_tool_policy_defaults() {
450 let policy = ToolPolicyConfig::default();
451 assert!(policy.enabled);
452 assert!(!policy.require_confirmation);
453 assert!(policy.rate_limit.is_none());
454 }
455}