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)]
35#[serde(rename_all = "snake_case")]
36pub enum NoWritePolicyBehavior {
37 Deny,
38 DryRunOnly,
39}
40
41impl Default for NoWritePolicyBehavior {
42 fn default() -> Self {
43 Self::DryRunOnly
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct CommandRuleConfig {
50 #[serde(default)]
52 pub argv: Vec<String>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct CommandTemplateConfig {
58 pub name: String,
60 #[serde(default)]
62 pub argv: Vec<String>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ToolPolicyConfig {
68 #[serde(default = "default_true")]
70 pub enabled: bool,
71 #[serde(default, alias = "require_approval")]
73 pub require_confirmation: bool,
74 #[serde(default)]
76 pub allow_without_confirmation: bool,
77 #[serde(default)]
79 pub confirmation_message: Option<String>,
80 #[serde(default)]
82 pub rate_limit: Option<u32>,
83 #[serde(default)]
85 pub timeout_ms: Option<u64>,
86 #[serde(default)]
88 pub allowed_domains: Vec<String>,
89 #[serde(default)]
91 pub blocked_domains: Vec<String>,
92 #[serde(default)]
94 pub allowed_paths: Vec<String>,
95 #[serde(default)]
97 pub read_paths: Vec<String>,
98 #[serde(default)]
100 pub write_paths: Vec<String>,
101 #[serde(default)]
103 pub blocked_paths: Vec<String>,
104 #[serde(default)]
106 pub max_file_size_bytes: Option<u64>,
107 #[serde(default)]
109 pub max_output_chars: Option<usize>,
110 #[serde(default)]
112 pub max_results: Option<usize>,
113 #[serde(default)]
115 pub max_response_bytes: Option<usize>,
116 #[serde(default = "default_true")]
118 pub blocked_private_networks: bool,
119 #[serde(default)]
121 pub allowed_schemes: Vec<String>,
122 #[serde(default)]
124 pub allowed_ports: Vec<u16>,
125 #[serde(default)]
127 pub max_redirects: Option<usize>,
128 #[serde(default)]
130 pub max_changed_files: Option<usize>,
131 #[serde(default)]
133 pub max_changed_lines: Option<usize>,
134 #[serde(default)]
136 pub max_replacements: Option<usize>,
137 #[serde(default)]
139 pub require_read_before_write: bool,
140 #[serde(default)]
142 pub overwrite_existing: bool,
143 #[serde(default)]
145 pub create_parent_dirs: bool,
146 #[serde(default)]
148 pub no_write_policy: NoWritePolicyBehavior,
149 #[serde(default)]
151 pub allowed_commands: Vec<CommandRuleConfig>,
152 #[serde(default)]
154 pub command_templates: Vec<CommandTemplateConfig>,
155 #[serde(default)]
157 pub working_dirs: Vec<String>,
158 #[serde(default)]
160 pub env_passthrough: Vec<String>,
161 #[serde(default)]
163 pub redact_env: Vec<String>,
164 #[serde(default = "default_true")]
166 pub deny_shell: bool,
167 #[serde(default = "default_true")]
169 pub deny_interactive: bool,
170 #[serde(default)]
172 pub allow_command_escalation: bool,
173 #[serde(default)]
175 pub domains: DomainPolicyConfig,
176 #[serde(default)]
178 pub paths: PathPolicyConfig,
179 #[serde(default)]
181 pub commands: CommandPolicyConfig,
182 #[serde(default)]
184 pub operations: OperationPolicyConfig,
185 #[serde(default)]
187 pub config: HashMap<String, serde_json::Value>,
188}
189
190impl Default for ToolPolicyConfig {
191 fn default() -> Self {
192 Self {
193 enabled: true,
194 require_confirmation: false,
195 allow_without_confirmation: false,
196 confirmation_message: None,
197 rate_limit: None,
198 timeout_ms: None,
199 allowed_domains: Vec::new(),
200 blocked_domains: Vec::new(),
201 allowed_paths: Vec::new(),
202 read_paths: Vec::new(),
203 write_paths: Vec::new(),
204 blocked_paths: Vec::new(),
205 max_file_size_bytes: None,
206 max_output_chars: None,
207 max_results: None,
208 max_response_bytes: None,
209 blocked_private_networks: true,
210 allowed_schemes: Vec::new(),
211 allowed_ports: Vec::new(),
212 max_redirects: None,
213 max_changed_files: None,
214 max_changed_lines: None,
215 max_replacements: None,
216 require_read_before_write: false,
217 overwrite_existing: false,
218 create_parent_dirs: false,
219 no_write_policy: NoWritePolicyBehavior::default(),
220 allowed_commands: Vec::new(),
221 command_templates: Vec::new(),
222 working_dirs: Vec::new(),
223 env_passthrough: Vec::new(),
224 redact_env: Vec::new(),
225 deny_shell: true,
226 deny_interactive: true,
227 allow_command_escalation: false,
228 domains: DomainPolicyConfig::default(),
229 paths: PathPolicyConfig::default(),
230 commands: CommandPolicyConfig::default(),
231 operations: OperationPolicyConfig::default(),
232 config: HashMap::new(),
233 }
234 }
235}
236
237#[derive(Debug, Clone, Default, Serialize, Deserialize)]
239pub struct DomainPolicyConfig {
240 #[serde(default)]
241 pub allow: Vec<String>,
242 #[serde(default)]
243 pub deny: Vec<String>,
244 #[serde(default)]
245 pub requires_approval: Vec<String>,
246 #[serde(default)]
247 pub unavailable: Vec<String>,
248}
249
250#[derive(Debug, Clone, Default, Serialize, Deserialize)]
252pub struct PathPolicyConfig {
253 #[serde(default)]
254 pub allow: Vec<String>,
255 #[serde(default)]
256 pub deny: Vec<String>,
257 #[serde(default)]
258 pub requires_approval: Vec<String>,
259 #[serde(default)]
260 pub unavailable: Vec<String>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct CommandPolicyConfig {
266 #[serde(default)]
267 pub allow: Vec<String>,
268 #[serde(default)]
269 pub deny: Vec<String>,
270 #[serde(default)]
271 pub requires_approval: Vec<String>,
272 #[serde(default)]
273 pub unavailable: Vec<String>,
274 #[serde(default)]
275 pub allowed_commands: Vec<CommandRuleConfig>,
276 #[serde(default)]
277 pub templates: Vec<CommandTemplateConfig>,
278 #[serde(default)]
279 pub working_dirs: Vec<String>,
280 #[serde(default)]
281 pub env_passthrough: Vec<String>,
282 #[serde(default = "default_true")]
283 pub deny_shell: bool,
284 #[serde(default = "default_true")]
285 pub deny_interactive: bool,
286 #[serde(default)]
287 pub allow_escalation: bool,
288}
289
290impl Default for CommandPolicyConfig {
291 fn default() -> Self {
292 Self {
293 allow: Vec::new(),
294 deny: Vec::new(),
295 requires_approval: Vec::new(),
296 unavailable: Vec::new(),
297 allowed_commands: Vec::new(),
298 templates: Vec::new(),
299 working_dirs: Vec::new(),
300 env_passthrough: Vec::new(),
301 deny_shell: true,
302 deny_interactive: true,
303 allow_escalation: false,
304 }
305 }
306}
307
308#[derive(Debug, Clone, Default, Serialize, Deserialize)]
310pub struct OperationPolicyConfig {
311 #[serde(default)]
312 pub allow: Vec<String>,
313 #[serde(default)]
314 pub deny: Vec<String>,
315 #[serde(default)]
316 pub requires_approval: Vec<String>,
317 #[serde(default)]
318 pub unavailable: Vec<String>,
319}
320
321#[derive(Debug, Clone)]
323pub enum SecurityCheckResult {
324 Allow,
325 Block { reason: String },
326 Warn { message: String },
327 RequireConfirmation { message: String },
328 Unavailable { reason: String },
329}
330
331impl SecurityCheckResult {
332 pub fn is_allowed(&self) -> bool {
334 matches!(
335 self,
336 SecurityCheckResult::Allow | SecurityCheckResult::Warn { .. }
337 )
338 }
339
340 pub fn is_blocked(&self) -> bool {
342 matches!(
343 self,
344 SecurityCheckResult::Block { .. } | SecurityCheckResult::Unavailable { .. }
345 )
346 }
347
348 pub fn outcome(&self) -> PermissionOutcome {
350 match self {
351 SecurityCheckResult::Allow | SecurityCheckResult::Warn { .. } => {
352 PermissionOutcome::Allow
353 }
354 SecurityCheckResult::Block { .. } => PermissionOutcome::Deny,
355 SecurityCheckResult::RequireConfirmation { .. } => PermissionOutcome::RequiresApproval,
356 SecurityCheckResult::Unavailable { .. } => PermissionOutcome::Unavailable,
357 }
358 }
359
360 pub fn reason(&self) -> Option<&str> {
362 match self {
363 SecurityCheckResult::Allow => None,
364 SecurityCheckResult::Block { reason } => Some(reason),
365 SecurityCheckResult::Warn { message } => Some(message),
366 SecurityCheckResult::RequireConfirmation { message } => Some(message),
367 SecurityCheckResult::Unavailable { reason } => Some(reason),
368 }
369 }
370
371 pub fn requires_approval(&self) -> bool {
373 matches!(self, SecurityCheckResult::RequireConfirmation { .. })
374 }
375
376 pub fn is_unavailable(&self) -> bool {
378 matches!(self, SecurityCheckResult::Unavailable { .. })
379 }
380}
381
382fn default_tool_timeout() -> u64 {
383 30000
384}
385
386fn default_true() -> bool {
387 true
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_default_config() {
396 let config = ToolSecurityConfig::default();
397 assert!(!config.enabled);
398 assert_eq!(config.default_timeout_ms, 30000);
399 assert!(config.tools.is_empty());
400 }
401
402 #[test]
403 fn test_yaml_parsing() {
404 let yaml = r#"
405enabled: true
406default_timeout_ms: 10000
407tools:
408 http:
409 rate_limit: 10
410 blocked_domains:
411 - evil.com
412 allowed_domains:
413 - api.example.com
414 file_write:
415 require_confirmation: true
416 confirmation_message: "Are you sure you want to write this file?"
417 allowed_paths:
418 - /tmp/
419"#;
420 let config: ToolSecurityConfig = serde_yaml::from_str(yaml).unwrap();
421 assert!(config.enabled);
422 assert_eq!(config.default_timeout_ms, 10000);
423 assert!(config.tools.contains_key("http"));
424 assert!(config.tools.contains_key("file_write"));
425
426 let http = config.tools.get("http").unwrap();
427 assert_eq!(http.rate_limit, Some(10));
428 assert_eq!(http.blocked_domains, vec!["evil.com"]);
429
430 let file_write = config.tools.get("file_write").unwrap();
431 assert!(file_write.require_confirmation);
432 }
433
434 #[test]
435 fn test_security_check_result() {
436 let allow = SecurityCheckResult::Allow;
437 assert!(allow.is_allowed());
438 assert!(!allow.is_blocked());
439
440 let block = SecurityCheckResult::Block {
441 reason: "test".into(),
442 };
443 assert!(!block.is_allowed());
444 assert!(block.is_blocked());
445
446 let warn = SecurityCheckResult::Warn {
447 message: "warning".into(),
448 };
449 assert!(warn.is_allowed());
450 assert!(!warn.is_blocked());
451 }
452
453 #[test]
454 fn test_tool_policy_defaults() {
455 let policy = ToolPolicyConfig::default();
456 assert!(policy.enabled);
457 assert!(!policy.require_confirmation);
458 assert!(policy.rate_limit.is_none());
459 }
460}