1use serde::{Deserialize, Serialize};
2
3use crate::permissions::{AutonomyLevel, PermissionPolicy, PermissionsConfig};
4
5fn default_true() -> bool {
6 true
7}
8
9fn default_timeout() -> u64 {
10 30
11}
12
13fn default_confirm_patterns() -> Vec<String> {
14 vec![
15 "rm ".into(),
16 "git push -f".into(),
17 "git push --force".into(),
18 "drop table".into(),
19 "drop database".into(),
20 "truncate ".into(),
21 "$(".into(),
22 "`".into(),
23 ]
24}
25
26fn default_audit_destination() -> String {
27 "stdout".into()
28}
29
30#[derive(Debug, Deserialize, Serialize)]
32pub struct ToolsConfig {
33 #[serde(default = "default_true")]
34 pub enabled: bool,
35 #[serde(default = "default_true")]
36 pub summarize_output: bool,
37 #[serde(default)]
38 pub shell: ShellConfig,
39 #[serde(default)]
40 pub scrape: ScrapeConfig,
41 #[serde(default)]
42 pub audit: AuditConfig,
43 #[serde(default)]
44 pub permissions: Option<PermissionsConfig>,
45 #[serde(default)]
46 pub filters: crate::filter::FilterConfig,
47}
48
49impl ToolsConfig {
50 #[must_use]
52 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
53 let policy = if let Some(ref perms) = self.permissions {
54 PermissionPolicy::from(perms.clone())
55 } else {
56 PermissionPolicy::from_legacy(
57 &self.shell.blocked_commands,
58 &self.shell.confirm_patterns,
59 )
60 };
61 policy.with_autonomy(autonomy_level)
62 }
63}
64
65#[derive(Debug, Deserialize, Serialize)]
67pub struct ShellConfig {
68 #[serde(default = "default_timeout")]
69 pub timeout: u64,
70 #[serde(default)]
71 pub blocked_commands: Vec<String>,
72 #[serde(default)]
73 pub allowed_commands: Vec<String>,
74 #[serde(default)]
75 pub allowed_paths: Vec<String>,
76 #[serde(default = "default_true")]
77 pub allow_network: bool,
78 #[serde(default = "default_confirm_patterns")]
79 pub confirm_patterns: Vec<String>,
80}
81
82#[derive(Debug, Deserialize, Serialize)]
84pub struct AuditConfig {
85 #[serde(default)]
86 pub enabled: bool,
87 #[serde(default = "default_audit_destination")]
88 pub destination: String,
89}
90
91impl Default for ToolsConfig {
92 fn default() -> Self {
93 Self {
94 enabled: true,
95 summarize_output: true,
96 shell: ShellConfig::default(),
97 scrape: ScrapeConfig::default(),
98 audit: AuditConfig::default(),
99 permissions: None,
100 filters: crate::filter::FilterConfig::default(),
101 }
102 }
103}
104
105impl Default for ShellConfig {
106 fn default() -> Self {
107 Self {
108 timeout: default_timeout(),
109 blocked_commands: Vec::new(),
110 allowed_commands: Vec::new(),
111 allowed_paths: Vec::new(),
112 allow_network: true,
113 confirm_patterns: default_confirm_patterns(),
114 }
115 }
116}
117
118impl Default for AuditConfig {
119 fn default() -> Self {
120 Self {
121 enabled: false,
122 destination: default_audit_destination(),
123 }
124 }
125}
126
127fn default_scrape_timeout() -> u64 {
128 15
129}
130
131fn default_max_body_bytes() -> usize {
132 1_048_576
133}
134
135#[derive(Debug, Deserialize, Serialize)]
137pub struct ScrapeConfig {
138 #[serde(default = "default_scrape_timeout")]
139 pub timeout: u64,
140 #[serde(default = "default_max_body_bytes")]
141 pub max_body_bytes: usize,
142}
143
144impl Default for ScrapeConfig {
145 fn default() -> Self {
146 Self {
147 timeout: default_scrape_timeout(),
148 max_body_bytes: default_max_body_bytes(),
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn deserialize_default_config() {
159 let toml_str = r#"
160 enabled = true
161
162 [shell]
163 timeout = 60
164 blocked_commands = ["rm -rf /", "sudo"]
165 "#;
166
167 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
168 assert!(config.enabled);
169 assert_eq!(config.shell.timeout, 60);
170 assert_eq!(config.shell.blocked_commands.len(), 2);
171 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
172 assert_eq!(config.shell.blocked_commands[1], "sudo");
173 }
174
175 #[test]
176 fn empty_blocked_commands() {
177 let toml_str = r#"
178 [shell]
179 timeout = 30
180 "#;
181
182 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
183 assert!(config.enabled);
184 assert_eq!(config.shell.timeout, 30);
185 assert!(config.shell.blocked_commands.is_empty());
186 }
187
188 #[test]
189 fn default_tools_config() {
190 let config = ToolsConfig::default();
191 assert!(config.enabled);
192 assert!(config.summarize_output);
193 assert_eq!(config.shell.timeout, 30);
194 assert!(config.shell.blocked_commands.is_empty());
195 assert!(!config.audit.enabled);
196 }
197
198 #[test]
199 fn tools_summarize_output_default_true() {
200 let config = ToolsConfig::default();
201 assert!(config.summarize_output);
202 }
203
204 #[test]
205 fn tools_summarize_output_parsing() {
206 let toml_str = r#"
207 summarize_output = true
208 "#;
209 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
210 assert!(config.summarize_output);
211 }
212
213 #[test]
214 fn default_shell_config() {
215 let config = ShellConfig::default();
216 assert_eq!(config.timeout, 30);
217 assert!(config.blocked_commands.is_empty());
218 assert!(config.allowed_paths.is_empty());
219 assert!(config.allow_network);
220 assert!(!config.confirm_patterns.is_empty());
221 }
222
223 #[test]
224 fn deserialize_omitted_fields_use_defaults() {
225 let toml_str = "";
226 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
227 assert!(config.enabled);
228 assert_eq!(config.shell.timeout, 30);
229 assert!(config.shell.blocked_commands.is_empty());
230 assert!(config.shell.allow_network);
231 assert!(!config.shell.confirm_patterns.is_empty());
232 assert_eq!(config.scrape.timeout, 15);
233 assert_eq!(config.scrape.max_body_bytes, 1_048_576);
234 assert!(!config.audit.enabled);
235 assert_eq!(config.audit.destination, "stdout");
236 assert!(config.summarize_output);
237 }
238
239 #[test]
240 fn default_scrape_config() {
241 let config = ScrapeConfig::default();
242 assert_eq!(config.timeout, 15);
243 assert_eq!(config.max_body_bytes, 1_048_576);
244 }
245
246 #[test]
247 fn deserialize_scrape_config() {
248 let toml_str = r#"
249 [scrape]
250 timeout = 30
251 max_body_bytes = 2097152
252 "#;
253
254 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
255 assert_eq!(config.scrape.timeout, 30);
256 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
257 }
258
259 #[test]
260 fn tools_config_default_includes_scrape() {
261 let config = ToolsConfig::default();
262 assert_eq!(config.scrape.timeout, 15);
263 assert_eq!(config.scrape.max_body_bytes, 1_048_576);
264 }
265
266 #[test]
267 fn deserialize_allowed_commands() {
268 let toml_str = r#"
269 [shell]
270 timeout = 30
271 allowed_commands = ["curl", "wget"]
272 "#;
273
274 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
275 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
276 }
277
278 #[test]
279 fn default_allowed_commands_empty() {
280 let config = ShellConfig::default();
281 assert!(config.allowed_commands.is_empty());
282 }
283
284 #[test]
285 fn deserialize_shell_security_fields() {
286 let toml_str = r#"
287 [shell]
288 allowed_paths = ["/tmp", "/home/user"]
289 allow_network = false
290 confirm_patterns = ["rm ", "drop table"]
291 "#;
292
293 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
294 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
295 assert!(!config.shell.allow_network);
296 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
297 }
298
299 #[test]
300 fn deserialize_audit_config() {
301 let toml_str = r#"
302 [audit]
303 enabled = true
304 destination = "/var/log/zeph-audit.log"
305 "#;
306
307 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
308 assert!(config.audit.enabled);
309 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
310 }
311
312 #[test]
313 fn default_audit_config() {
314 let config = AuditConfig::default();
315 assert!(!config.enabled);
316 assert_eq!(config.destination, "stdout");
317 }
318
319 #[test]
320 fn permission_policy_from_legacy_fields() {
321 let config = ToolsConfig {
322 shell: ShellConfig {
323 blocked_commands: vec!["sudo".to_owned()],
324 confirm_patterns: vec!["rm ".to_owned()],
325 ..ShellConfig::default()
326 },
327 ..ToolsConfig::default()
328 };
329 let policy = config.permission_policy(AutonomyLevel::Supervised);
330 assert_eq!(
331 policy.check("bash", "sudo apt"),
332 crate::permissions::PermissionAction::Deny
333 );
334 assert_eq!(
335 policy.check("bash", "rm file"),
336 crate::permissions::PermissionAction::Ask
337 );
338 }
339
340 #[test]
341 fn permission_policy_from_explicit_config() {
342 let toml_str = r#"
343 [permissions]
344 [[permissions.bash]]
345 pattern = "*sudo*"
346 action = "deny"
347 "#;
348 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
349 let policy = config.permission_policy(AutonomyLevel::Supervised);
350 assert_eq!(
351 policy.check("bash", "sudo rm"),
352 crate::permissions::PermissionAction::Deny
353 );
354 }
355
356 #[test]
357 fn permission_policy_default_uses_legacy() {
358 let config = ToolsConfig::default();
359 assert!(config.permissions.is_none());
360 let policy = config.permission_policy(AutonomyLevel::Supervised);
361 assert!(!config.shell.confirm_patterns.is_empty());
363 assert!(policy.rules().contains_key("bash"));
364 }
365}