1use serde::{Deserialize, Serialize};
5
6use crate::permissions::{AutonomyLevel, PermissionPolicy, PermissionsConfig};
7#[cfg(feature = "policy-enforcer")]
8use crate::policy::PolicyConfig;
9
10fn default_true() -> bool {
11 true
12}
13
14fn default_timeout() -> u64 {
15 30
16}
17
18fn default_cache_ttl_secs() -> u64 {
19 300
20}
21
22fn default_confirm_patterns() -> Vec<String> {
23 vec![
24 "rm ".into(),
25 "git push -f".into(),
26 "git push --force".into(),
27 "drop table".into(),
28 "drop database".into(),
29 "truncate ".into(),
30 "$(".into(),
31 "`".into(),
32 "<(".into(),
33 ">(".into(),
34 "<<<".into(),
35 "eval ".into(),
36 ]
37}
38
39fn default_audit_destination() -> String {
40 "stdout".into()
41}
42
43fn default_overflow_threshold() -> usize {
44 50_000
45}
46
47fn default_retention_days() -> u64 {
48 7
49}
50
51fn default_max_overflow_bytes() -> usize {
52 10 * 1024 * 1024 }
54
55#[derive(Debug, Clone, Deserialize, Serialize)]
57pub struct OverflowConfig {
58 #[serde(default = "default_overflow_threshold")]
59 pub threshold: usize,
60 #[serde(default = "default_retention_days")]
61 pub retention_days: u64,
62 #[serde(default = "default_max_overflow_bytes")]
64 pub max_overflow_bytes: usize,
65}
66
67impl Default for OverflowConfig {
68 fn default() -> Self {
69 Self {
70 threshold: default_overflow_threshold(),
71 retention_days: default_retention_days(),
72 max_overflow_bytes: default_max_overflow_bytes(),
73 }
74 }
75}
76
77fn default_anomaly_window() -> usize {
78 10
79}
80
81fn default_anomaly_error_threshold() -> f64 {
82 0.5
83}
84
85fn default_anomaly_critical_threshold() -> f64 {
86 0.8
87}
88
89#[derive(Debug, Clone, Deserialize, Serialize)]
91pub struct AnomalyConfig {
92 #[serde(default)]
93 pub enabled: bool,
94 #[serde(default = "default_anomaly_window")]
95 pub window_size: usize,
96 #[serde(default = "default_anomaly_error_threshold")]
97 pub error_threshold: f64,
98 #[serde(default = "default_anomaly_critical_threshold")]
99 pub critical_threshold: f64,
100}
101
102impl Default for AnomalyConfig {
103 fn default() -> Self {
104 Self {
105 enabled: false,
106 window_size: default_anomaly_window(),
107 error_threshold: default_anomaly_error_threshold(),
108 critical_threshold: default_anomaly_critical_threshold(),
109 }
110 }
111}
112
113#[derive(Debug, Clone, Deserialize, Serialize)]
115pub struct ResultCacheConfig {
116 #[serde(default = "default_true")]
118 pub enabled: bool,
119 #[serde(default = "default_cache_ttl_secs")]
121 pub ttl_secs: u64,
122}
123
124impl Default for ResultCacheConfig {
125 fn default() -> Self {
126 Self {
127 enabled: true,
128 ttl_secs: default_cache_ttl_secs(),
129 }
130 }
131}
132
133fn default_tafc_complexity_threshold() -> f64 {
134 0.6
135}
136
137#[derive(Debug, Clone, Deserialize, Serialize)]
139pub struct TafcConfig {
140 #[serde(default)]
142 pub enabled: bool,
143 #[serde(default = "default_tafc_complexity_threshold")]
146 pub complexity_threshold: f64,
147}
148
149impl Default for TafcConfig {
150 fn default() -> Self {
151 Self {
152 enabled: false,
153 complexity_threshold: default_tafc_complexity_threshold(),
154 }
155 }
156}
157
158impl TafcConfig {
159 #[must_use]
161 pub fn validated(mut self) -> Self {
162 if self.complexity_threshold.is_finite() {
163 self.complexity_threshold = self.complexity_threshold.clamp(0.0, 1.0);
164 } else {
165 self.complexity_threshold = 0.6;
166 }
167 self
168 }
169}
170
171fn default_boost_per_dep() -> f32 {
172 0.15
173}
174
175fn default_max_total_boost() -> f32 {
176 0.2
177}
178
179#[derive(Debug, Clone, Default, Deserialize, Serialize)]
181pub struct ToolDependency {
182 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub requires: Vec<String>,
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
187 pub prefers: Vec<String>,
188}
189
190#[derive(Debug, Clone, Deserialize, Serialize)]
192pub struct DependencyConfig {
193 #[serde(default)]
195 pub enabled: bool,
196 #[serde(default = "default_boost_per_dep")]
198 pub boost_per_dep: f32,
199 #[serde(default = "default_max_total_boost")]
201 pub max_total_boost: f32,
202 #[serde(default)]
204 pub rules: std::collections::HashMap<String, ToolDependency>,
205}
206
207impl Default for DependencyConfig {
208 fn default() -> Self {
209 Self {
210 enabled: false,
211 boost_per_dep: default_boost_per_dep(),
212 max_total_boost: default_max_total_boost(),
213 rules: std::collections::HashMap::new(),
214 }
215 }
216}
217
218#[derive(Debug, Deserialize, Serialize)]
220pub struct ToolsConfig {
221 #[serde(default = "default_true")]
222 pub enabled: bool,
223 #[serde(default = "default_true")]
224 pub summarize_output: bool,
225 #[serde(default)]
226 pub shell: ShellConfig,
227 #[serde(default)]
228 pub scrape: ScrapeConfig,
229 #[serde(default)]
230 pub audit: AuditConfig,
231 #[serde(default)]
232 pub permissions: Option<PermissionsConfig>,
233 #[serde(default)]
234 pub filters: crate::filter::FilterConfig,
235 #[serde(default)]
236 pub overflow: OverflowConfig,
237 #[serde(default)]
238 pub anomaly: AnomalyConfig,
239 #[serde(default)]
240 pub result_cache: ResultCacheConfig,
241 #[serde(default)]
242 pub tafc: TafcConfig,
243 #[serde(default)]
244 pub dependencies: DependencyConfig,
245 #[cfg(feature = "policy-enforcer")]
247 #[serde(default)]
248 pub policy: PolicyConfig,
249}
250
251impl ToolsConfig {
252 #[must_use]
254 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
255 let policy = if let Some(ref perms) = self.permissions {
256 PermissionPolicy::from(perms.clone())
257 } else {
258 PermissionPolicy::from_legacy(
259 &self.shell.blocked_commands,
260 &self.shell.confirm_patterns,
261 )
262 };
263 policy.with_autonomy(autonomy_level)
264 }
265}
266
267#[derive(Debug, Deserialize, Serialize)]
269pub struct ShellConfig {
270 #[serde(default = "default_timeout")]
271 pub timeout: u64,
272 #[serde(default)]
273 pub blocked_commands: Vec<String>,
274 #[serde(default)]
275 pub allowed_commands: Vec<String>,
276 #[serde(default)]
277 pub allowed_paths: Vec<String>,
278 #[serde(default = "default_true")]
279 pub allow_network: bool,
280 #[serde(default = "default_confirm_patterns")]
281 pub confirm_patterns: Vec<String>,
282}
283
284#[derive(Debug, Deserialize, Serialize)]
286pub struct AuditConfig {
287 #[serde(default)]
288 pub enabled: bool,
289 #[serde(default = "default_audit_destination")]
290 pub destination: String,
291}
292
293impl Default for ToolsConfig {
294 fn default() -> Self {
295 Self {
296 enabled: true,
297 summarize_output: true,
298 shell: ShellConfig::default(),
299 scrape: ScrapeConfig::default(),
300 audit: AuditConfig::default(),
301 permissions: None,
302 filters: crate::filter::FilterConfig::default(),
303 overflow: OverflowConfig::default(),
304 anomaly: AnomalyConfig::default(),
305 result_cache: ResultCacheConfig::default(),
306 tafc: TafcConfig::default(),
307 dependencies: DependencyConfig::default(),
308 #[cfg(feature = "policy-enforcer")]
309 policy: PolicyConfig::default(),
310 }
311 }
312}
313
314impl Default for ShellConfig {
315 fn default() -> Self {
316 Self {
317 timeout: default_timeout(),
318 blocked_commands: Vec::new(),
319 allowed_commands: Vec::new(),
320 allowed_paths: Vec::new(),
321 allow_network: true,
322 confirm_patterns: default_confirm_patterns(),
323 }
324 }
325}
326
327impl Default for AuditConfig {
328 fn default() -> Self {
329 Self {
330 enabled: false,
331 destination: default_audit_destination(),
332 }
333 }
334}
335
336fn default_scrape_timeout() -> u64 {
337 15
338}
339
340fn default_max_body_bytes() -> usize {
341 4_194_304
342}
343
344#[derive(Debug, Deserialize, Serialize)]
346pub struct ScrapeConfig {
347 #[serde(default = "default_scrape_timeout")]
348 pub timeout: u64,
349 #[serde(default = "default_max_body_bytes")]
350 pub max_body_bytes: usize,
351}
352
353impl Default for ScrapeConfig {
354 fn default() -> Self {
355 Self {
356 timeout: default_scrape_timeout(),
357 max_body_bytes: default_max_body_bytes(),
358 }
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn deserialize_default_config() {
368 let toml_str = r#"
369 enabled = true
370
371 [shell]
372 timeout = 60
373 blocked_commands = ["rm -rf /", "sudo"]
374 "#;
375
376 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
377 assert!(config.enabled);
378 assert_eq!(config.shell.timeout, 60);
379 assert_eq!(config.shell.blocked_commands.len(), 2);
380 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
381 assert_eq!(config.shell.blocked_commands[1], "sudo");
382 }
383
384 #[test]
385 fn empty_blocked_commands() {
386 let toml_str = r"
387 [shell]
388 timeout = 30
389 ";
390
391 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
392 assert!(config.enabled);
393 assert_eq!(config.shell.timeout, 30);
394 assert!(config.shell.blocked_commands.is_empty());
395 }
396
397 #[test]
398 fn default_tools_config() {
399 let config = ToolsConfig::default();
400 assert!(config.enabled);
401 assert!(config.summarize_output);
402 assert_eq!(config.shell.timeout, 30);
403 assert!(config.shell.blocked_commands.is_empty());
404 assert!(!config.audit.enabled);
405 }
406
407 #[test]
408 fn tools_summarize_output_default_true() {
409 let config = ToolsConfig::default();
410 assert!(config.summarize_output);
411 }
412
413 #[test]
414 fn tools_summarize_output_parsing() {
415 let toml_str = r"
416 summarize_output = true
417 ";
418 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
419 assert!(config.summarize_output);
420 }
421
422 #[test]
423 fn default_shell_config() {
424 let config = ShellConfig::default();
425 assert_eq!(config.timeout, 30);
426 assert!(config.blocked_commands.is_empty());
427 assert!(config.allowed_paths.is_empty());
428 assert!(config.allow_network);
429 assert!(!config.confirm_patterns.is_empty());
430 }
431
432 #[test]
433 fn deserialize_omitted_fields_use_defaults() {
434 let toml_str = "";
435 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
436 assert!(config.enabled);
437 assert_eq!(config.shell.timeout, 30);
438 assert!(config.shell.blocked_commands.is_empty());
439 assert!(config.shell.allow_network);
440 assert!(!config.shell.confirm_patterns.is_empty());
441 assert_eq!(config.scrape.timeout, 15);
442 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
443 assert!(!config.audit.enabled);
444 assert_eq!(config.audit.destination, "stdout");
445 assert!(config.summarize_output);
446 }
447
448 #[test]
449 fn default_scrape_config() {
450 let config = ScrapeConfig::default();
451 assert_eq!(config.timeout, 15);
452 assert_eq!(config.max_body_bytes, 4_194_304);
453 }
454
455 #[test]
456 fn deserialize_scrape_config() {
457 let toml_str = r"
458 [scrape]
459 timeout = 30
460 max_body_bytes = 2097152
461 ";
462
463 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
464 assert_eq!(config.scrape.timeout, 30);
465 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
466 }
467
468 #[test]
469 fn tools_config_default_includes_scrape() {
470 let config = ToolsConfig::default();
471 assert_eq!(config.scrape.timeout, 15);
472 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
473 }
474
475 #[test]
476 fn deserialize_allowed_commands() {
477 let toml_str = r#"
478 [shell]
479 timeout = 30
480 allowed_commands = ["curl", "wget"]
481 "#;
482
483 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
484 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
485 }
486
487 #[test]
488 fn default_allowed_commands_empty() {
489 let config = ShellConfig::default();
490 assert!(config.allowed_commands.is_empty());
491 }
492
493 #[test]
494 fn deserialize_shell_security_fields() {
495 let toml_str = r#"
496 [shell]
497 allowed_paths = ["/tmp", "/home/user"]
498 allow_network = false
499 confirm_patterns = ["rm ", "drop table"]
500 "#;
501
502 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
503 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
504 assert!(!config.shell.allow_network);
505 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
506 }
507
508 #[test]
509 fn deserialize_audit_config() {
510 let toml_str = r#"
511 [audit]
512 enabled = true
513 destination = "/var/log/zeph-audit.log"
514 "#;
515
516 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
517 assert!(config.audit.enabled);
518 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
519 }
520
521 #[test]
522 fn default_audit_config() {
523 let config = AuditConfig::default();
524 assert!(!config.enabled);
525 assert_eq!(config.destination, "stdout");
526 }
527
528 #[test]
529 fn permission_policy_from_legacy_fields() {
530 let config = ToolsConfig {
531 shell: ShellConfig {
532 blocked_commands: vec!["sudo".to_owned()],
533 confirm_patterns: vec!["rm ".to_owned()],
534 ..ShellConfig::default()
535 },
536 ..ToolsConfig::default()
537 };
538 let policy = config.permission_policy(AutonomyLevel::Supervised);
539 assert_eq!(
540 policy.check("bash", "sudo apt"),
541 crate::permissions::PermissionAction::Deny
542 );
543 assert_eq!(
544 policy.check("bash", "rm file"),
545 crate::permissions::PermissionAction::Ask
546 );
547 }
548
549 #[test]
550 fn permission_policy_from_explicit_config() {
551 let toml_str = r#"
552 [permissions]
553 [[permissions.bash]]
554 pattern = "*sudo*"
555 action = "deny"
556 "#;
557 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
558 let policy = config.permission_policy(AutonomyLevel::Supervised);
559 assert_eq!(
560 policy.check("bash", "sudo rm"),
561 crate::permissions::PermissionAction::Deny
562 );
563 }
564
565 #[test]
566 fn permission_policy_default_uses_legacy() {
567 let config = ToolsConfig::default();
568 assert!(config.permissions.is_none());
569 let policy = config.permission_policy(AutonomyLevel::Supervised);
570 assert!(!config.shell.confirm_patterns.is_empty());
572 assert!(policy.rules().contains_key("bash"));
573 }
574
575 #[test]
576 fn deserialize_overflow_config_full() {
577 let toml_str = r"
578 [overflow]
579 threshold = 100000
580 retention_days = 14
581 max_overflow_bytes = 5242880
582 ";
583 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
584 assert_eq!(config.overflow.threshold, 100_000);
585 assert_eq!(config.overflow.retention_days, 14);
586 assert_eq!(config.overflow.max_overflow_bytes, 5_242_880);
587 }
588
589 #[test]
590 fn deserialize_overflow_config_unknown_dir_field_is_ignored() {
591 let toml_str = r#"
593 [overflow]
594 threshold = 75000
595 dir = "/tmp/overflow"
596 "#;
597 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
598 assert_eq!(config.overflow.threshold, 75_000);
599 }
600
601 #[test]
602 fn deserialize_overflow_config_partial_uses_defaults() {
603 let toml_str = r"
604 [overflow]
605 threshold = 75000
606 ";
607 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
608 assert_eq!(config.overflow.threshold, 75_000);
609 assert_eq!(config.overflow.retention_days, 7);
610 }
611
612 #[test]
613 fn deserialize_overflow_config_omitted_uses_defaults() {
614 let config: ToolsConfig = toml::from_str("").unwrap();
615 assert_eq!(config.overflow.threshold, 50_000);
616 assert_eq!(config.overflow.retention_days, 7);
617 assert_eq!(config.overflow.max_overflow_bytes, 10 * 1024 * 1024);
618 }
619
620 #[test]
621 fn result_cache_config_defaults() {
622 let config = ResultCacheConfig::default();
623 assert!(config.enabled);
624 assert_eq!(config.ttl_secs, 300);
625 }
626
627 #[test]
628 fn deserialize_result_cache_config() {
629 let toml_str = r"
630 [result_cache]
631 enabled = false
632 ttl_secs = 60
633 ";
634 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
635 assert!(!config.result_cache.enabled);
636 assert_eq!(config.result_cache.ttl_secs, 60);
637 }
638
639 #[test]
640 fn result_cache_omitted_uses_defaults() {
641 let config: ToolsConfig = toml::from_str("").unwrap();
642 assert!(config.result_cache.enabled);
643 assert_eq!(config.result_cache.ttl_secs, 300);
644 }
645
646 #[test]
647 fn result_cache_ttl_zero_is_valid() {
648 let toml_str = r"
649 [result_cache]
650 ttl_secs = 0
651 ";
652 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
653 assert_eq!(config.result_cache.ttl_secs, 0);
654 }
655}