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