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 = "default_true")]
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: true,
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
218fn default_retry_max_attempts() -> usize {
219 2
220}
221
222fn default_retry_base_ms() -> u64 {
223 500
224}
225
226fn default_retry_max_ms() -> u64 {
227 5_000
228}
229
230fn default_retry_budget_secs() -> u64 {
231 30
232}
233
234#[derive(Debug, Clone, Deserialize, Serialize)]
236pub struct RetryConfig {
237 #[serde(default = "default_retry_max_attempts")]
239 pub max_attempts: usize,
240 #[serde(default = "default_retry_base_ms")]
242 pub base_ms: u64,
243 #[serde(default = "default_retry_max_ms")]
245 pub max_ms: u64,
246 #[serde(default = "default_retry_budget_secs")]
248 pub budget_secs: u64,
249 #[serde(default)]
252 pub parameter_reformat_provider: String,
253}
254
255impl Default for RetryConfig {
256 fn default() -> Self {
257 Self {
258 max_attempts: default_retry_max_attempts(),
259 base_ms: default_retry_base_ms(),
260 max_ms: default_retry_max_ms(),
261 budget_secs: default_retry_budget_secs(),
262 parameter_reformat_provider: String::new(),
263 }
264 }
265}
266
267#[derive(Debug, Deserialize, Serialize)]
269pub struct ToolsConfig {
270 #[serde(default = "default_true")]
271 pub enabled: bool,
272 #[serde(default = "default_true")]
273 pub summarize_output: bool,
274 #[serde(default)]
275 pub shell: ShellConfig,
276 #[serde(default)]
277 pub scrape: ScrapeConfig,
278 #[serde(default)]
279 pub audit: AuditConfig,
280 #[serde(default)]
281 pub permissions: Option<PermissionsConfig>,
282 #[serde(default)]
283 pub filters: crate::filter::FilterConfig,
284 #[serde(default)]
285 pub overflow: OverflowConfig,
286 #[serde(default)]
287 pub anomaly: AnomalyConfig,
288 #[serde(default)]
289 pub result_cache: ResultCacheConfig,
290 #[serde(default)]
291 pub tafc: TafcConfig,
292 #[serde(default)]
293 pub dependencies: DependencyConfig,
294 #[serde(default)]
295 pub retry: RetryConfig,
296 #[cfg(feature = "policy-enforcer")]
298 #[serde(default)]
299 pub policy: PolicyConfig,
300}
301
302impl ToolsConfig {
303 #[must_use]
305 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
306 let policy = if let Some(ref perms) = self.permissions {
307 PermissionPolicy::from(perms.clone())
308 } else {
309 PermissionPolicy::from_legacy(
310 &self.shell.blocked_commands,
311 &self.shell.confirm_patterns,
312 )
313 };
314 policy.with_autonomy(autonomy_level)
315 }
316}
317
318#[derive(Debug, Deserialize, Serialize)]
320pub struct ShellConfig {
321 #[serde(default = "default_timeout")]
322 pub timeout: u64,
323 #[serde(default)]
324 pub blocked_commands: Vec<String>,
325 #[serde(default)]
326 pub allowed_commands: Vec<String>,
327 #[serde(default)]
328 pub allowed_paths: Vec<String>,
329 #[serde(default = "default_true")]
330 pub allow_network: bool,
331 #[serde(default = "default_confirm_patterns")]
332 pub confirm_patterns: Vec<String>,
333}
334
335#[derive(Debug, Deserialize, Serialize)]
337pub struct AuditConfig {
338 #[serde(default = "default_true")]
339 pub enabled: bool,
340 #[serde(default = "default_audit_destination")]
341 pub destination: String,
342}
343
344impl Default for ToolsConfig {
345 fn default() -> Self {
346 Self {
347 enabled: true,
348 summarize_output: true,
349 shell: ShellConfig::default(),
350 scrape: ScrapeConfig::default(),
351 audit: AuditConfig::default(),
352 permissions: None,
353 filters: crate::filter::FilterConfig::default(),
354 overflow: OverflowConfig::default(),
355 anomaly: AnomalyConfig::default(),
356 result_cache: ResultCacheConfig::default(),
357 tafc: TafcConfig::default(),
358 dependencies: DependencyConfig::default(),
359 retry: RetryConfig::default(),
360 #[cfg(feature = "policy-enforcer")]
361 policy: PolicyConfig::default(),
362 }
363 }
364}
365
366impl Default for ShellConfig {
367 fn default() -> Self {
368 Self {
369 timeout: default_timeout(),
370 blocked_commands: Vec::new(),
371 allowed_commands: Vec::new(),
372 allowed_paths: Vec::new(),
373 allow_network: true,
374 confirm_patterns: default_confirm_patterns(),
375 }
376 }
377}
378
379impl Default for AuditConfig {
380 fn default() -> Self {
381 Self {
382 enabled: true,
383 destination: default_audit_destination(),
384 }
385 }
386}
387
388fn default_scrape_timeout() -> u64 {
389 15
390}
391
392fn default_max_body_bytes() -> usize {
393 4_194_304
394}
395
396#[derive(Debug, Deserialize, Serialize)]
398pub struct ScrapeConfig {
399 #[serde(default = "default_scrape_timeout")]
400 pub timeout: u64,
401 #[serde(default = "default_max_body_bytes")]
402 pub max_body_bytes: usize,
403}
404
405impl Default for ScrapeConfig {
406 fn default() -> Self {
407 Self {
408 timeout: default_scrape_timeout(),
409 max_body_bytes: default_max_body_bytes(),
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn deserialize_default_config() {
420 let toml_str = r#"
421 enabled = true
422
423 [shell]
424 timeout = 60
425 blocked_commands = ["rm -rf /", "sudo"]
426 "#;
427
428 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
429 assert!(config.enabled);
430 assert_eq!(config.shell.timeout, 60);
431 assert_eq!(config.shell.blocked_commands.len(), 2);
432 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
433 assert_eq!(config.shell.blocked_commands[1], "sudo");
434 }
435
436 #[test]
437 fn empty_blocked_commands() {
438 let toml_str = r"
439 [shell]
440 timeout = 30
441 ";
442
443 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
444 assert!(config.enabled);
445 assert_eq!(config.shell.timeout, 30);
446 assert!(config.shell.blocked_commands.is_empty());
447 }
448
449 #[test]
450 fn default_tools_config() {
451 let config = ToolsConfig::default();
452 assert!(config.enabled);
453 assert!(config.summarize_output);
454 assert_eq!(config.shell.timeout, 30);
455 assert!(config.shell.blocked_commands.is_empty());
456 assert!(config.audit.enabled);
457 }
458
459 #[test]
460 fn tools_summarize_output_default_true() {
461 let config = ToolsConfig::default();
462 assert!(config.summarize_output);
463 }
464
465 #[test]
466 fn tools_summarize_output_parsing() {
467 let toml_str = r"
468 summarize_output = true
469 ";
470 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
471 assert!(config.summarize_output);
472 }
473
474 #[test]
475 fn default_shell_config() {
476 let config = ShellConfig::default();
477 assert_eq!(config.timeout, 30);
478 assert!(config.blocked_commands.is_empty());
479 assert!(config.allowed_paths.is_empty());
480 assert!(config.allow_network);
481 assert!(!config.confirm_patterns.is_empty());
482 }
483
484 #[test]
485 fn deserialize_omitted_fields_use_defaults() {
486 let toml_str = "";
487 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
488 assert!(config.enabled);
489 assert_eq!(config.shell.timeout, 30);
490 assert!(config.shell.blocked_commands.is_empty());
491 assert!(config.shell.allow_network);
492 assert!(!config.shell.confirm_patterns.is_empty());
493 assert_eq!(config.scrape.timeout, 15);
494 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
495 assert!(config.audit.enabled);
496 assert_eq!(config.audit.destination, "stdout");
497 assert!(config.summarize_output);
498 }
499
500 #[test]
501 fn default_scrape_config() {
502 let config = ScrapeConfig::default();
503 assert_eq!(config.timeout, 15);
504 assert_eq!(config.max_body_bytes, 4_194_304);
505 }
506
507 #[test]
508 fn deserialize_scrape_config() {
509 let toml_str = r"
510 [scrape]
511 timeout = 30
512 max_body_bytes = 2097152
513 ";
514
515 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
516 assert_eq!(config.scrape.timeout, 30);
517 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
518 }
519
520 #[test]
521 fn tools_config_default_includes_scrape() {
522 let config = ToolsConfig::default();
523 assert_eq!(config.scrape.timeout, 15);
524 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
525 }
526
527 #[test]
528 fn deserialize_allowed_commands() {
529 let toml_str = r#"
530 [shell]
531 timeout = 30
532 allowed_commands = ["curl", "wget"]
533 "#;
534
535 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
536 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
537 }
538
539 #[test]
540 fn default_allowed_commands_empty() {
541 let config = ShellConfig::default();
542 assert!(config.allowed_commands.is_empty());
543 }
544
545 #[test]
546 fn deserialize_shell_security_fields() {
547 let toml_str = r#"
548 [shell]
549 allowed_paths = ["/tmp", "/home/user"]
550 allow_network = false
551 confirm_patterns = ["rm ", "drop table"]
552 "#;
553
554 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
555 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
556 assert!(!config.shell.allow_network);
557 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
558 }
559
560 #[test]
561 fn deserialize_audit_config() {
562 let toml_str = r#"
563 [audit]
564 enabled = true
565 destination = "/var/log/zeph-audit.log"
566 "#;
567
568 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
569 assert!(config.audit.enabled);
570 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
571 }
572
573 #[test]
574 fn default_audit_config() {
575 let config = AuditConfig::default();
576 assert!(config.enabled);
577 assert_eq!(config.destination, "stdout");
578 }
579
580 #[test]
581 fn permission_policy_from_legacy_fields() {
582 let config = ToolsConfig {
583 shell: ShellConfig {
584 blocked_commands: vec!["sudo".to_owned()],
585 confirm_patterns: vec!["rm ".to_owned()],
586 ..ShellConfig::default()
587 },
588 ..ToolsConfig::default()
589 };
590 let policy = config.permission_policy(AutonomyLevel::Supervised);
591 assert_eq!(
592 policy.check("bash", "sudo apt"),
593 crate::permissions::PermissionAction::Deny
594 );
595 assert_eq!(
596 policy.check("bash", "rm file"),
597 crate::permissions::PermissionAction::Ask
598 );
599 }
600
601 #[test]
602 fn permission_policy_from_explicit_config() {
603 let toml_str = r#"
604 [permissions]
605 [[permissions.bash]]
606 pattern = "*sudo*"
607 action = "deny"
608 "#;
609 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
610 let policy = config.permission_policy(AutonomyLevel::Supervised);
611 assert_eq!(
612 policy.check("bash", "sudo rm"),
613 crate::permissions::PermissionAction::Deny
614 );
615 }
616
617 #[test]
618 fn permission_policy_default_uses_legacy() {
619 let config = ToolsConfig::default();
620 assert!(config.permissions.is_none());
621 let policy = config.permission_policy(AutonomyLevel::Supervised);
622 assert!(!config.shell.confirm_patterns.is_empty());
624 assert!(policy.rules().contains_key("bash"));
625 }
626
627 #[test]
628 fn deserialize_overflow_config_full() {
629 let toml_str = r"
630 [overflow]
631 threshold = 100000
632 retention_days = 14
633 max_overflow_bytes = 5242880
634 ";
635 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
636 assert_eq!(config.overflow.threshold, 100_000);
637 assert_eq!(config.overflow.retention_days, 14);
638 assert_eq!(config.overflow.max_overflow_bytes, 5_242_880);
639 }
640
641 #[test]
642 fn deserialize_overflow_config_unknown_dir_field_is_ignored() {
643 let toml_str = r#"
645 [overflow]
646 threshold = 75000
647 dir = "/tmp/overflow"
648 "#;
649 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
650 assert_eq!(config.overflow.threshold, 75_000);
651 }
652
653 #[test]
654 fn deserialize_overflow_config_partial_uses_defaults() {
655 let toml_str = r"
656 [overflow]
657 threshold = 75000
658 ";
659 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
660 assert_eq!(config.overflow.threshold, 75_000);
661 assert_eq!(config.overflow.retention_days, 7);
662 }
663
664 #[test]
665 fn deserialize_overflow_config_omitted_uses_defaults() {
666 let config: ToolsConfig = toml::from_str("").unwrap();
667 assert_eq!(config.overflow.threshold, 50_000);
668 assert_eq!(config.overflow.retention_days, 7);
669 assert_eq!(config.overflow.max_overflow_bytes, 10 * 1024 * 1024);
670 }
671
672 #[test]
673 fn result_cache_config_defaults() {
674 let config = ResultCacheConfig::default();
675 assert!(config.enabled);
676 assert_eq!(config.ttl_secs, 300);
677 }
678
679 #[test]
680 fn deserialize_result_cache_config() {
681 let toml_str = r"
682 [result_cache]
683 enabled = false
684 ttl_secs = 60
685 ";
686 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
687 assert!(!config.result_cache.enabled);
688 assert_eq!(config.result_cache.ttl_secs, 60);
689 }
690
691 #[test]
692 fn result_cache_omitted_uses_defaults() {
693 let config: ToolsConfig = toml::from_str("").unwrap();
694 assert!(config.result_cache.enabled);
695 assert_eq!(config.result_cache.ttl_secs, 300);
696 }
697
698 #[test]
699 fn result_cache_ttl_zero_is_valid() {
700 let toml_str = r"
701 [result_cache]
702 ttl_secs = 0
703 ";
704 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
705 assert_eq!(config.result_cache.ttl_secs, 0);
706 }
707}