1use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8use crate::permissions::{AutonomyLevel, PermissionPolicy, PermissionsConfig};
9
10fn default_true() -> bool {
11 true
12}
13
14fn default_timeout() -> u64 {
15 30
16}
17
18fn default_confirm_patterns() -> Vec<String> {
19 vec![
20 "rm ".into(),
21 "git push -f".into(),
22 "git push --force".into(),
23 "drop table".into(),
24 "drop database".into(),
25 "truncate ".into(),
26 "$(".into(),
27 "`".into(),
28 "<(".into(),
29 ">(".into(),
30 "<<<".into(),
31 "eval ".into(),
32 ]
33}
34
35fn default_audit_destination() -> String {
36 "stdout".into()
37}
38
39fn default_overflow_threshold() -> usize {
40 50_000
41}
42
43fn default_retention_days() -> u64 {
44 7
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct OverflowConfig {
50 #[serde(default = "default_overflow_threshold")]
51 pub threshold: usize,
52 #[serde(default = "default_retention_days")]
53 pub retention_days: u64,
54 #[serde(default)]
55 pub dir: Option<PathBuf>,
56}
57
58impl Default for OverflowConfig {
59 fn default() -> Self {
60 Self {
61 threshold: default_overflow_threshold(),
62 retention_days: default_retention_days(),
63 dir: None,
64 }
65 }
66}
67
68fn default_anomaly_window() -> usize {
69 10
70}
71
72fn default_anomaly_error_threshold() -> f64 {
73 0.5
74}
75
76fn default_anomaly_critical_threshold() -> f64 {
77 0.8
78}
79
80#[derive(Debug, Clone, Deserialize, Serialize)]
82pub struct AnomalyConfig {
83 #[serde(default)]
84 pub enabled: bool,
85 #[serde(default = "default_anomaly_window")]
86 pub window_size: usize,
87 #[serde(default = "default_anomaly_error_threshold")]
88 pub error_threshold: f64,
89 #[serde(default = "default_anomaly_critical_threshold")]
90 pub critical_threshold: f64,
91}
92
93impl Default for AnomalyConfig {
94 fn default() -> Self {
95 Self {
96 enabled: false,
97 window_size: default_anomaly_window(),
98 error_threshold: default_anomaly_error_threshold(),
99 critical_threshold: default_anomaly_critical_threshold(),
100 }
101 }
102}
103
104#[derive(Debug, Deserialize, Serialize)]
106pub struct ToolsConfig {
107 #[serde(default = "default_true")]
108 pub enabled: bool,
109 #[serde(default = "default_true")]
110 pub summarize_output: bool,
111 #[serde(default)]
112 pub shell: ShellConfig,
113 #[serde(default)]
114 pub scrape: ScrapeConfig,
115 #[serde(default)]
116 pub audit: AuditConfig,
117 #[serde(default)]
118 pub permissions: Option<PermissionsConfig>,
119 #[serde(default)]
120 pub filters: crate::filter::FilterConfig,
121 #[serde(default)]
122 pub overflow: OverflowConfig,
123 #[serde(default)]
124 pub anomaly: AnomalyConfig,
125}
126
127impl ToolsConfig {
128 #[must_use]
130 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
131 let policy = if let Some(ref perms) = self.permissions {
132 PermissionPolicy::from(perms.clone())
133 } else {
134 PermissionPolicy::from_legacy(
135 &self.shell.blocked_commands,
136 &self.shell.confirm_patterns,
137 )
138 };
139 policy.with_autonomy(autonomy_level)
140 }
141}
142
143#[derive(Debug, Deserialize, Serialize)]
145pub struct ShellConfig {
146 #[serde(default = "default_timeout")]
147 pub timeout: u64,
148 #[serde(default)]
149 pub blocked_commands: Vec<String>,
150 #[serde(default)]
151 pub allowed_commands: Vec<String>,
152 #[serde(default)]
153 pub allowed_paths: Vec<String>,
154 #[serde(default = "default_true")]
155 pub allow_network: bool,
156 #[serde(default = "default_confirm_patterns")]
157 pub confirm_patterns: Vec<String>,
158}
159
160#[derive(Debug, Deserialize, Serialize)]
162pub struct AuditConfig {
163 #[serde(default)]
164 pub enabled: bool,
165 #[serde(default = "default_audit_destination")]
166 pub destination: String,
167}
168
169impl Default for ToolsConfig {
170 fn default() -> Self {
171 Self {
172 enabled: true,
173 summarize_output: true,
174 shell: ShellConfig::default(),
175 scrape: ScrapeConfig::default(),
176 audit: AuditConfig::default(),
177 permissions: None,
178 filters: crate::filter::FilterConfig::default(),
179 overflow: OverflowConfig::default(),
180 anomaly: AnomalyConfig::default(),
181 }
182 }
183}
184
185impl Default for ShellConfig {
186 fn default() -> Self {
187 Self {
188 timeout: default_timeout(),
189 blocked_commands: Vec::new(),
190 allowed_commands: Vec::new(),
191 allowed_paths: Vec::new(),
192 allow_network: true,
193 confirm_patterns: default_confirm_patterns(),
194 }
195 }
196}
197
198impl Default for AuditConfig {
199 fn default() -> Self {
200 Self {
201 enabled: false,
202 destination: default_audit_destination(),
203 }
204 }
205}
206
207fn default_scrape_timeout() -> u64 {
208 15
209}
210
211fn default_max_body_bytes() -> usize {
212 1_048_576
213}
214
215#[derive(Debug, Deserialize, Serialize)]
217pub struct ScrapeConfig {
218 #[serde(default = "default_scrape_timeout")]
219 pub timeout: u64,
220 #[serde(default = "default_max_body_bytes")]
221 pub max_body_bytes: usize,
222}
223
224impl Default for ScrapeConfig {
225 fn default() -> Self {
226 Self {
227 timeout: default_scrape_timeout(),
228 max_body_bytes: default_max_body_bytes(),
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn deserialize_default_config() {
239 let toml_str = r#"
240 enabled = true
241
242 [shell]
243 timeout = 60
244 blocked_commands = ["rm -rf /", "sudo"]
245 "#;
246
247 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
248 assert!(config.enabled);
249 assert_eq!(config.shell.timeout, 60);
250 assert_eq!(config.shell.blocked_commands.len(), 2);
251 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
252 assert_eq!(config.shell.blocked_commands[1], "sudo");
253 }
254
255 #[test]
256 fn empty_blocked_commands() {
257 let toml_str = r#"
258 [shell]
259 timeout = 30
260 "#;
261
262 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
263 assert!(config.enabled);
264 assert_eq!(config.shell.timeout, 30);
265 assert!(config.shell.blocked_commands.is_empty());
266 }
267
268 #[test]
269 fn default_tools_config() {
270 let config = ToolsConfig::default();
271 assert!(config.enabled);
272 assert!(config.summarize_output);
273 assert_eq!(config.shell.timeout, 30);
274 assert!(config.shell.blocked_commands.is_empty());
275 assert!(!config.audit.enabled);
276 }
277
278 #[test]
279 fn tools_summarize_output_default_true() {
280 let config = ToolsConfig::default();
281 assert!(config.summarize_output);
282 }
283
284 #[test]
285 fn tools_summarize_output_parsing() {
286 let toml_str = r#"
287 summarize_output = true
288 "#;
289 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
290 assert!(config.summarize_output);
291 }
292
293 #[test]
294 fn default_shell_config() {
295 let config = ShellConfig::default();
296 assert_eq!(config.timeout, 30);
297 assert!(config.blocked_commands.is_empty());
298 assert!(config.allowed_paths.is_empty());
299 assert!(config.allow_network);
300 assert!(!config.confirm_patterns.is_empty());
301 }
302
303 #[test]
304 fn deserialize_omitted_fields_use_defaults() {
305 let toml_str = "";
306 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
307 assert!(config.enabled);
308 assert_eq!(config.shell.timeout, 30);
309 assert!(config.shell.blocked_commands.is_empty());
310 assert!(config.shell.allow_network);
311 assert!(!config.shell.confirm_patterns.is_empty());
312 assert_eq!(config.scrape.timeout, 15);
313 assert_eq!(config.scrape.max_body_bytes, 1_048_576);
314 assert!(!config.audit.enabled);
315 assert_eq!(config.audit.destination, "stdout");
316 assert!(config.summarize_output);
317 }
318
319 #[test]
320 fn default_scrape_config() {
321 let config = ScrapeConfig::default();
322 assert_eq!(config.timeout, 15);
323 assert_eq!(config.max_body_bytes, 1_048_576);
324 }
325
326 #[test]
327 fn deserialize_scrape_config() {
328 let toml_str = r#"
329 [scrape]
330 timeout = 30
331 max_body_bytes = 2097152
332 "#;
333
334 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
335 assert_eq!(config.scrape.timeout, 30);
336 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
337 }
338
339 #[test]
340 fn tools_config_default_includes_scrape() {
341 let config = ToolsConfig::default();
342 assert_eq!(config.scrape.timeout, 15);
343 assert_eq!(config.scrape.max_body_bytes, 1_048_576);
344 }
345
346 #[test]
347 fn deserialize_allowed_commands() {
348 let toml_str = r#"
349 [shell]
350 timeout = 30
351 allowed_commands = ["curl", "wget"]
352 "#;
353
354 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
355 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
356 }
357
358 #[test]
359 fn default_allowed_commands_empty() {
360 let config = ShellConfig::default();
361 assert!(config.allowed_commands.is_empty());
362 }
363
364 #[test]
365 fn deserialize_shell_security_fields() {
366 let toml_str = r#"
367 [shell]
368 allowed_paths = ["/tmp", "/home/user"]
369 allow_network = false
370 confirm_patterns = ["rm ", "drop table"]
371 "#;
372
373 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
374 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
375 assert!(!config.shell.allow_network);
376 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
377 }
378
379 #[test]
380 fn deserialize_audit_config() {
381 let toml_str = r#"
382 [audit]
383 enabled = true
384 destination = "/var/log/zeph-audit.log"
385 "#;
386
387 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
388 assert!(config.audit.enabled);
389 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
390 }
391
392 #[test]
393 fn default_audit_config() {
394 let config = AuditConfig::default();
395 assert!(!config.enabled);
396 assert_eq!(config.destination, "stdout");
397 }
398
399 #[test]
400 fn permission_policy_from_legacy_fields() {
401 let config = ToolsConfig {
402 shell: ShellConfig {
403 blocked_commands: vec!["sudo".to_owned()],
404 confirm_patterns: vec!["rm ".to_owned()],
405 ..ShellConfig::default()
406 },
407 ..ToolsConfig::default()
408 };
409 let policy = config.permission_policy(AutonomyLevel::Supervised);
410 assert_eq!(
411 policy.check("bash", "sudo apt"),
412 crate::permissions::PermissionAction::Deny
413 );
414 assert_eq!(
415 policy.check("bash", "rm file"),
416 crate::permissions::PermissionAction::Ask
417 );
418 }
419
420 #[test]
421 fn permission_policy_from_explicit_config() {
422 let toml_str = r#"
423 [permissions]
424 [[permissions.bash]]
425 pattern = "*sudo*"
426 action = "deny"
427 "#;
428 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
429 let policy = config.permission_policy(AutonomyLevel::Supervised);
430 assert_eq!(
431 policy.check("bash", "sudo rm"),
432 crate::permissions::PermissionAction::Deny
433 );
434 }
435
436 #[test]
437 fn permission_policy_default_uses_legacy() {
438 let config = ToolsConfig::default();
439 assert!(config.permissions.is_none());
440 let policy = config.permission_policy(AutonomyLevel::Supervised);
441 assert!(!config.shell.confirm_patterns.is_empty());
443 assert!(policy.rules().contains_key("bash"));
444 }
445
446 #[test]
447 fn deserialize_overflow_config_full() {
448 let toml_str = r#"
449 [overflow]
450 threshold = 100000
451 retention_days = 14
452 dir = "/tmp/overflow"
453 "#;
454 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
455 assert_eq!(config.overflow.threshold, 100_000);
456 assert_eq!(config.overflow.retention_days, 14);
457 assert_eq!(
458 config.overflow.dir.unwrap().to_str().unwrap(),
459 "/tmp/overflow"
460 );
461 }
462
463 #[test]
464 fn deserialize_overflow_config_partial_uses_defaults() {
465 let toml_str = r#"
466 [overflow]
467 threshold = 75000
468 "#;
469 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
470 assert_eq!(config.overflow.threshold, 75_000);
471 assert_eq!(config.overflow.retention_days, 7);
472 assert!(config.overflow.dir.is_none());
473 }
474
475 #[test]
476 fn deserialize_overflow_config_omitted_uses_defaults() {
477 let config: ToolsConfig = toml::from_str("").unwrap();
478 assert_eq!(config.overflow.threshold, 50_000);
479 assert_eq!(config.overflow.retention_days, 7);
480 assert!(config.overflow.dir.is_none());
481 }
482}