Skip to main content

ai_agents_tools/security/
config.rs

1use ai_agents_core::PermissionOutcome;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Runtime security configuration for tool execution.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ToolSecurityConfig {
8    /// Enables runtime tool security checks.
9    #[serde(default)]
10    pub enabled: bool,
11    /// Blocks tools without explicit policy when security is enabled.
12    #[serde(default)]
13    pub fail_closed: bool,
14    /// Default timeout for tool execution in milliseconds.
15    #[serde(default = "default_tool_timeout")]
16    pub default_timeout_ms: u64,
17    /// Per-tool policies keyed by canonical tool ID.
18    #[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/// Behavior when a mutation tool has no explicit write policy.
34#[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/// Exact argv command allowed by command policy.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct CommandRuleConfig {
45    /// Full argv vector, including executable name.
46    #[serde(default)]
47    pub argv: Vec<String>,
48}
49
50/// Argv command template with literal and wildcard variable segments.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct CommandTemplateConfig {
53    /// Template name used in evidence and diagnostics.
54    pub name: String,
55    /// Argv segments. Values in {braces} are template variables.
56    #[serde(default)]
57    pub argv: Vec<String>,
58}
59
60/// Per-tool policy configuration.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ToolPolicyConfig {
63    /// Enables this tool policy.
64    #[serde(default = "default_true")]
65    pub enabled: bool,
66    /// Requires approval for this tool after hard denials pass.
67    #[serde(default, alias = "require_approval")]
68    pub require_confirmation: bool,
69    /// Explicitly permits side-effecting calls to skip classification-default approval.
70    #[serde(default)]
71    pub allow_without_confirmation: bool,
72    /// Message shown when tool-level approval is required.
73    #[serde(default)]
74    pub confirmation_message: Option<String>,
75    /// Maximum allowed calls per minute.
76    #[serde(default)]
77    pub rate_limit: Option<u32>,
78    /// Tool-specific timeout in milliseconds.
79    #[serde(default)]
80    pub timeout_ms: Option<u64>,
81    /// Legacy domain allowlist mapped to domain policy.
82    #[serde(default)]
83    pub allowed_domains: Vec<String>,
84    /// Legacy domain blocklist mapped to domain policy.
85    #[serde(default)]
86    pub blocked_domains: Vec<String>,
87    /// Legacy path allowlist mapped to path policy.
88    #[serde(default)]
89    pub allowed_paths: Vec<String>,
90    /// Explicit read path allowlist for local read-only tools.
91    #[serde(default)]
92    pub read_paths: Vec<String>,
93    /// Explicit write path allowlist for local mutation tools.
94    #[serde(default)]
95    pub write_paths: Vec<String>,
96    /// Paths that override any allowlist.
97    #[serde(default)]
98    pub blocked_paths: Vec<String>,
99    /// Maximum file size read or searched by local tools.
100    #[serde(default)]
101    pub max_file_size_bytes: Option<u64>,
102    /// Maximum model-facing output characters.
103    #[serde(default)]
104    pub max_output_chars: Option<usize>,
105    /// Maximum rows or entries for list/search tools.
106    #[serde(default)]
107    pub max_results: Option<usize>,
108    /// Maximum response bytes for web fetch tools.
109    #[serde(default)]
110    pub max_response_bytes: Option<usize>,
111    /// Blocks private, localhost, link-local, and metadata network targets.
112    #[serde(default = "default_true")]
113    pub blocked_private_networks: bool,
114    /// Allowed URL schemes for network tools.
115    #[serde(default)]
116    pub allowed_schemes: Vec<String>,
117    /// Allowed URL ports for network tools.
118    #[serde(default)]
119    pub allowed_ports: Vec<u16>,
120    /// Maximum redirect count for network tools.
121    #[serde(default)]
122    pub max_redirects: Option<usize>,
123    /// Maximum files a mutation tool may change.
124    #[serde(default)]
125    pub max_changed_files: Option<usize>,
126    /// Maximum changed lines a mutation tool may produce.
127    #[serde(default)]
128    pub max_changed_lines: Option<usize>,
129    /// Maximum exact replacements a mutation tool may perform.
130    #[serde(default)]
131    pub max_replacements: Option<usize>,
132    /// Requires a matching file-read version before mutating an existing file.
133    #[serde(default)]
134    pub require_read_before_write: bool,
135    /// Allows overwriting existing files for mutation tools.
136    #[serde(default)]
137    pub overwrite_existing: bool,
138    /// Allows mutation tools to create missing parent directories.
139    #[serde(default)]
140    pub create_parent_dirs: bool,
141    /// Behavior when no write_paths allowlist is configured.
142    #[serde(default)]
143    pub no_write_policy: NoWritePolicyBehavior,
144    /// Exact argv allowlist for the command tool.
145    #[serde(default)]
146    pub allowed_commands: Vec<CommandRuleConfig>,
147    /// Argv templates for the command tool.
148    #[serde(default)]
149    pub command_templates: Vec<CommandTemplateConfig>,
150    /// Working directories allowed for command execution.
151    #[serde(default)]
152    pub working_dirs: Vec<String>,
153    /// Environment variables that may be passed from tool arguments.
154    #[serde(default)]
155    pub env_passthrough: Vec<String>,
156    /// Environment variables redacted from evidence.
157    #[serde(default)]
158    pub redact_env: Vec<String>,
159    /// Reject shell-like command strings and metacharacters.
160    #[serde(default = "default_true")]
161    pub deny_shell: bool,
162    /// Reject interactive command execution.
163    #[serde(default = "default_true")]
164    pub deny_interactive: bool,
165    /// Allows approval-based command escalation beyond the allowlist.
166    #[serde(default)]
167    pub allow_command_escalation: bool,
168    /// Parsed domain policy.
169    #[serde(default)]
170    pub domains: DomainPolicyConfig,
171    /// Normalized path policy.
172    #[serde(default)]
173    pub paths: PathPolicyConfig,
174    /// Command policy for process-backed tools.
175    #[serde(default)]
176    pub commands: CommandPolicyConfig,
177    /// Operation policy based on arguments such as operation, function, or method.
178    #[serde(default)]
179    pub operations: OperationPolicyConfig,
180    /// Custom tool settings exposed through ToolExecutionContext.custom_config.
181    #[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/// Domain allow, deny, approval, and unavailable policy lists.
233#[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/// Path allow, deny, approval, and unavailable policy lists.
246#[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/// Command allow, deny, approval, unavailable, and typed execution policy.
259#[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/// Operation allow, deny, approval, and unavailable policy lists.
304#[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/// Security decision returned by the tool security engine.
317#[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    /// Returns true when execution may continue without blocking.
328    pub fn is_allowed(&self) -> bool {
329        matches!(
330            self,
331            SecurityCheckResult::Allow | SecurityCheckResult::Warn { .. }
332        )
333    }
334
335    /// Returns true when execution must not invoke the tool.
336    pub fn is_blocked(&self) -> bool {
337        matches!(
338            self,
339            SecurityCheckResult::Block { .. } | SecurityCheckResult::Unavailable { .. }
340        )
341    }
342
343    /// Converts the security result to a stable permission outcome.
344    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    /// Returns the human-readable reason or warning message.
356    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    /// Returns true when a human approval request is required.
367    pub fn requires_approval(&self) -> bool {
368        matches!(self, SecurityCheckResult::RequireConfirmation { .. })
369    }
370
371    /// Returns true when the tool or required host policy is unavailable.
372    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}