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