1use serde::{Deserialize, Serialize};
5
6use crate::permissions::{AutonomyLevel, PermissionPolicy, PermissionsConfig};
7use crate::policy::{PolicyConfig, PolicyRuleConfig};
8
9fn default_true() -> bool {
10 true
11}
12fn default_adversarial_timeout_ms() -> u64 {
13 3_000
14}
15
16fn default_timeout() -> u64 {
17 30
18}
19
20fn default_max_background_runs() -> usize {
21 8
22}
23
24fn default_background_timeout_secs() -> u64 {
25 1800
26}
27
28fn default_cache_ttl_secs() -> u64 {
29 300
30}
31
32fn default_confirm_patterns() -> Vec<String> {
33 vec![
34 "rm ".into(),
35 "git push -f".into(),
36 "git push --force".into(),
37 "drop table".into(),
38 "drop database".into(),
39 "truncate ".into(),
40 "$(".into(),
41 "`".into(),
42 "<(".into(),
43 ">(".into(),
44 "<<<".into(),
45 "eval ".into(),
46 ]
47}
48
49fn default_audit_destination() -> String {
50 "stdout".into()
51}
52
53fn default_overflow_threshold() -> usize {
54 50_000
55}
56
57fn default_retention_days() -> u64 {
58 7
59}
60
61fn default_max_overflow_bytes() -> usize {
62 10 * 1024 * 1024 }
64
65#[derive(Debug, Clone, Deserialize, Serialize)]
67pub struct OverflowConfig {
68 #[serde(default = "default_overflow_threshold")]
69 pub threshold: usize,
70 #[serde(default = "default_retention_days")]
71 pub retention_days: u64,
72 #[serde(default = "default_max_overflow_bytes")]
74 pub max_overflow_bytes: usize,
75}
76
77impl Default for OverflowConfig {
78 fn default() -> Self {
79 Self {
80 threshold: default_overflow_threshold(),
81 retention_days: default_retention_days(),
82 max_overflow_bytes: default_max_overflow_bytes(),
83 }
84 }
85}
86
87fn default_anomaly_window() -> usize {
88 10
89}
90
91fn default_anomaly_error_threshold() -> f64 {
92 0.5
93}
94
95fn default_anomaly_critical_threshold() -> f64 {
96 0.8
97}
98
99#[derive(Debug, Clone, Deserialize, Serialize)]
101pub struct AnomalyConfig {
102 #[serde(default = "default_true")]
103 pub enabled: bool,
104 #[serde(default = "default_anomaly_window")]
105 pub window_size: usize,
106 #[serde(default = "default_anomaly_error_threshold")]
107 pub error_threshold: f64,
108 #[serde(default = "default_anomaly_critical_threshold")]
109 pub critical_threshold: f64,
110 #[serde(default = "default_true")]
115 pub reasoning_model_warning: bool,
116}
117
118impl Default for AnomalyConfig {
119 fn default() -> Self {
120 Self {
121 enabled: true,
122 window_size: default_anomaly_window(),
123 error_threshold: default_anomaly_error_threshold(),
124 critical_threshold: default_anomaly_critical_threshold(),
125 reasoning_model_warning: true,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Deserialize, Serialize)]
132pub struct ResultCacheConfig {
133 #[serde(default = "default_true")]
135 pub enabled: bool,
136 #[serde(default = "default_cache_ttl_secs")]
138 pub ttl_secs: u64,
139}
140
141impl Default for ResultCacheConfig {
142 fn default() -> Self {
143 Self {
144 enabled: true,
145 ttl_secs: default_cache_ttl_secs(),
146 }
147 }
148}
149
150fn default_tafc_complexity_threshold() -> f64 {
151 0.6
152}
153
154#[derive(Debug, Clone, Deserialize, Serialize)]
156pub struct TafcConfig {
157 #[serde(default)]
159 pub enabled: bool,
160 #[serde(default = "default_tafc_complexity_threshold")]
163 pub complexity_threshold: f64,
164}
165
166impl Default for TafcConfig {
167 fn default() -> Self {
168 Self {
169 enabled: false,
170 complexity_threshold: default_tafc_complexity_threshold(),
171 }
172 }
173}
174
175impl TafcConfig {
176 #[must_use]
178 pub fn validated(mut self) -> Self {
179 if self.complexity_threshold.is_finite() {
180 self.complexity_threshold = self.complexity_threshold.clamp(0.0, 1.0);
181 } else {
182 self.complexity_threshold = 0.6;
183 }
184 self
185 }
186}
187
188fn default_utility_exempt_tools() -> Vec<String> {
189 vec!["invoke_skill".to_string(), "load_skill".to_string()]
190}
191
192fn default_utility_threshold() -> f32 {
193 0.1
194}
195
196fn default_utility_gain_weight() -> f32 {
197 1.0
198}
199
200fn default_utility_cost_weight() -> f32 {
201 0.5
202}
203
204fn default_utility_redundancy_weight() -> f32 {
205 0.3
206}
207
208fn default_utility_uncertainty_bonus() -> f32 {
209 0.2
210}
211
212#[derive(Debug, Clone, Deserialize, Serialize)]
218#[serde(default)]
219pub struct UtilityScoringConfig {
220 pub enabled: bool,
222 #[serde(default = "default_utility_threshold")]
224 pub threshold: f32,
225 #[serde(default = "default_utility_gain_weight")]
227 pub gain_weight: f32,
228 #[serde(default = "default_utility_cost_weight")]
230 pub cost_weight: f32,
231 #[serde(default = "default_utility_redundancy_weight")]
233 pub redundancy_weight: f32,
234 #[serde(default = "default_utility_uncertainty_bonus")]
236 pub uncertainty_bonus: f32,
237 #[serde(default = "default_utility_exempt_tools")]
241 pub exempt_tools: Vec<String>,
242}
243
244impl Default for UtilityScoringConfig {
245 fn default() -> Self {
246 Self {
247 enabled: false,
248 threshold: default_utility_threshold(),
249 gain_weight: default_utility_gain_weight(),
250 cost_weight: default_utility_cost_weight(),
251 redundancy_weight: default_utility_redundancy_weight(),
252 uncertainty_bonus: default_utility_uncertainty_bonus(),
253 exempt_tools: default_utility_exempt_tools(),
254 }
255 }
256}
257
258impl UtilityScoringConfig {
259 pub fn validate(&self) -> Result<(), String> {
265 let fields = [
266 ("threshold", self.threshold),
267 ("gain_weight", self.gain_weight),
268 ("cost_weight", self.cost_weight),
269 ("redundancy_weight", self.redundancy_weight),
270 ("uncertainty_bonus", self.uncertainty_bonus),
271 ];
272 for (name, val) in fields {
273 if !val.is_finite() {
274 return Err(format!("[tools.utility] {name} must be finite, got {val}"));
275 }
276 if val < 0.0 {
277 return Err(format!("[tools.utility] {name} must be >= 0, got {val}"));
278 }
279 }
280 Ok(())
281 }
282}
283
284fn default_boost_per_dep() -> f32 {
285 0.15
286}
287
288fn default_max_total_boost() -> f32 {
289 0.2
290}
291
292#[derive(Debug, Clone, Default, Deserialize, Serialize)]
294pub struct ToolDependency {
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 pub requires: Vec<String>,
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
300 pub prefers: Vec<String>,
301}
302
303#[derive(Debug, Clone, Deserialize, Serialize)]
305pub struct DependencyConfig {
306 #[serde(default)]
308 pub enabled: bool,
309 #[serde(default = "default_boost_per_dep")]
311 pub boost_per_dep: f32,
312 #[serde(default = "default_max_total_boost")]
314 pub max_total_boost: f32,
315 #[serde(default)]
317 pub rules: std::collections::HashMap<String, ToolDependency>,
318}
319
320impl Default for DependencyConfig {
321 fn default() -> Self {
322 Self {
323 enabled: false,
324 boost_per_dep: default_boost_per_dep(),
325 max_total_boost: default_max_total_boost(),
326 rules: std::collections::HashMap::new(),
327 }
328 }
329}
330
331fn default_retry_max_attempts() -> usize {
332 2
333}
334
335fn default_retry_base_ms() -> u64 {
336 500
337}
338
339fn default_retry_max_ms() -> u64 {
340 5_000
341}
342
343fn default_retry_budget_secs() -> u64 {
344 30
345}
346
347#[derive(Debug, Clone, Deserialize, Serialize)]
349pub struct RetryConfig {
350 #[serde(default = "default_retry_max_attempts")]
352 pub max_attempts: usize,
353 #[serde(default = "default_retry_base_ms")]
355 pub base_ms: u64,
356 #[serde(default = "default_retry_max_ms")]
358 pub max_ms: u64,
359 #[serde(default = "default_retry_budget_secs")]
361 pub budget_secs: u64,
362 #[serde(default)]
365 pub parameter_reformat_provider: String,
366}
367
368impl Default for RetryConfig {
369 fn default() -> Self {
370 Self {
371 max_attempts: default_retry_max_attempts(),
372 base_ms: default_retry_base_ms(),
373 max_ms: default_retry_max_ms(),
374 budget_secs: default_retry_budget_secs(),
375 parameter_reformat_provider: String::new(),
376 }
377 }
378}
379
380#[derive(Debug, Clone, Deserialize, Serialize)]
382pub struct AdversarialPolicyConfig {
383 #[serde(default)]
385 pub enabled: bool,
386 #[serde(default)]
390 pub policy_provider: String,
391 pub policy_file: Option<String>,
393 #[serde(default)]
399 pub fail_open: bool,
400 #[serde(default = "default_adversarial_timeout_ms")]
402 pub timeout_ms: u64,
403 #[serde(default = "AdversarialPolicyConfig::default_exempt_tools")]
407 pub exempt_tools: Vec<String>,
408}
409impl Default for AdversarialPolicyConfig {
410 fn default() -> Self {
411 Self {
412 enabled: false,
413 policy_provider: String::new(),
414 policy_file: None,
415 fail_open: false,
416 timeout_ms: default_adversarial_timeout_ms(),
417 exempt_tools: Self::default_exempt_tools(),
418 }
419 }
420}
421impl AdversarialPolicyConfig {
422 fn default_exempt_tools() -> Vec<String> {
423 vec![
424 "memory_save".into(),
425 "memory_search".into(),
426 "read_overflow".into(),
427 "load_skill".into(),
428 "invoke_skill".into(),
429 "schedule_deferred".into(),
430 ]
431 }
432}
433
434#[derive(Debug, Clone, Default, Deserialize, Serialize)]
441pub struct FileConfig {
442 #[serde(default)]
444 pub deny_read: Vec<String>,
445 #[serde(default)]
447 pub allow_read: Vec<String>,
448}
449
450#[derive(Debug, Deserialize, Serialize)]
452pub struct ToolsConfig {
453 #[serde(default = "default_true")]
454 pub enabled: bool,
455 #[serde(default = "default_true")]
456 pub summarize_output: bool,
457 #[serde(default)]
458 pub shell: ShellConfig,
459 #[serde(default)]
460 pub scrape: ScrapeConfig,
461 #[serde(default)]
462 pub audit: AuditConfig,
463 #[serde(default)]
464 pub permissions: Option<PermissionsConfig>,
465 #[serde(default)]
466 pub filters: crate::filter::FilterConfig,
467 #[serde(default)]
468 pub overflow: OverflowConfig,
469 #[serde(default)]
470 pub anomaly: AnomalyConfig,
471 #[serde(default)]
472 pub result_cache: ResultCacheConfig,
473 #[serde(default)]
474 pub tafc: TafcConfig,
475 #[serde(default)]
476 pub dependencies: DependencyConfig,
477 #[serde(default)]
478 pub retry: RetryConfig,
479 #[serde(default)]
481 pub policy: PolicyConfig,
482 #[serde(default)]
484 pub adversarial_policy: AdversarialPolicyConfig,
485 #[serde(default)]
487 pub utility: UtilityScoringConfig,
488 #[serde(default)]
490 pub file: FileConfig,
491 #[serde(default)]
496 pub authorization: AuthorizationConfig,
497 #[serde(default)]
500 pub max_tool_calls_per_session: Option<u32>,
501 #[serde(default)]
505 pub speculative: SpeculativeConfig,
506 #[serde(default)]
511 pub sandbox: SandboxConfig,
512 #[serde(default)]
514 pub egress: EgressConfig,
515}
516
517impl ToolsConfig {
518 #[must_use]
520 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
521 let policy = if let Some(ref perms) = self.permissions {
522 PermissionPolicy::from(perms.clone())
523 } else {
524 PermissionPolicy::from_legacy(
525 &self.shell.blocked_commands,
526 &self.shell.confirm_patterns,
527 )
528 };
529 policy.with_autonomy(autonomy_level)
530 }
531}
532
533#[derive(Debug, Deserialize, Serialize)]
535#[allow(clippy::struct_excessive_bools)]
536pub struct ShellConfig {
537 #[serde(default = "default_timeout")]
538 pub timeout: u64,
539 #[serde(default)]
540 pub blocked_commands: Vec<String>,
541 #[serde(default)]
542 pub allowed_commands: Vec<String>,
543 #[serde(default)]
544 pub allowed_paths: Vec<String>,
545 #[serde(default = "default_true")]
546 pub allow_network: bool,
547 #[serde(default = "default_confirm_patterns")]
548 pub confirm_patterns: Vec<String>,
549 #[serde(default = "ShellConfig::default_env_blocklist")]
553 pub env_blocklist: Vec<String>,
554 #[serde(default)]
556 pub transactional: bool,
557 #[serde(default)]
561 pub transaction_scope: Vec<String>,
562 #[serde(default)]
566 pub auto_rollback: bool,
567 #[serde(default)]
570 pub auto_rollback_exit_codes: Vec<i32>,
571 #[serde(default)]
574 pub snapshot_required: bool,
575 #[serde(default)]
577 pub max_snapshot_bytes: u64,
578 #[serde(default = "default_max_background_runs")]
583 pub max_background_runs: usize,
584 #[serde(default = "default_background_timeout_secs")]
589 pub background_timeout_secs: u64,
590}
591
592impl Default for ShellConfig {
593 fn default() -> Self {
594 Self {
595 timeout: default_timeout(),
596 blocked_commands: Vec::new(),
597 allowed_commands: Vec::new(),
598 allowed_paths: Vec::new(),
599 allow_network: true,
600 confirm_patterns: default_confirm_patterns(),
601 env_blocklist: Self::default_env_blocklist(),
602 transactional: false,
603 transaction_scope: Vec::new(),
604 auto_rollback: false,
605 auto_rollback_exit_codes: Vec::new(),
606 snapshot_required: false,
607 max_snapshot_bytes: 0,
608 max_background_runs: default_max_background_runs(),
609 background_timeout_secs: default_background_timeout_secs(),
610 }
611 }
612}
613
614impl ShellConfig {
615 #[must_use]
616 pub fn default_env_blocklist() -> Vec<String> {
617 vec![
618 "ZEPH_".into(),
619 "AWS_".into(),
620 "AZURE_".into(),
621 "GCP_".into(),
622 "GOOGLE_".into(),
623 "OPENAI_".into(),
624 "ANTHROPIC_".into(),
625 "HF_".into(),
626 "HUGGING".into(),
627 ]
628 }
629}
630
631#[derive(Debug, Deserialize, Serialize)]
633pub struct AuditConfig {
634 #[serde(default = "default_true")]
635 pub enabled: bool,
636 #[serde(default = "default_audit_destination")]
637 pub destination: String,
638 #[serde(default)]
643 pub tool_risk_summary: bool,
644}
645
646impl Default for ToolsConfig {
647 fn default() -> Self {
648 Self {
649 enabled: true,
650 summarize_output: true,
651 shell: ShellConfig::default(),
652 scrape: ScrapeConfig::default(),
653 audit: AuditConfig::default(),
654 permissions: None,
655 filters: crate::filter::FilterConfig::default(),
656 overflow: OverflowConfig::default(),
657 anomaly: AnomalyConfig::default(),
658 result_cache: ResultCacheConfig::default(),
659 tafc: TafcConfig::default(),
660 dependencies: DependencyConfig::default(),
661 retry: RetryConfig::default(),
662 policy: PolicyConfig::default(),
663 adversarial_policy: AdversarialPolicyConfig::default(),
664 utility: UtilityScoringConfig::default(),
665 file: FileConfig::default(),
666 authorization: AuthorizationConfig::default(),
667 max_tool_calls_per_session: None,
668 speculative: SpeculativeConfig::default(),
669 sandbox: SandboxConfig::default(),
670 egress: EgressConfig::default(),
671 }
672 }
673}
674
675fn default_max_in_flight() -> usize {
676 4
677}
678
679fn default_confidence_threshold() -> f32 {
680 0.55
681}
682
683fn default_max_wasted_per_minute() -> u64 {
684 100
685}
686
687fn default_ttl_seconds() -> u64 {
688 30
689}
690
691fn default_min_observations() -> u32 {
692 5
693}
694
695fn default_half_life_days() -> f64 {
696 14.0
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
704#[serde(rename_all = "kebab-case")]
705pub enum SpeculationMode {
706 #[default]
708 Off,
709 Decoding,
711 Pattern,
713 Both,
715}
716
717#[derive(Debug, Clone, Deserialize, Serialize)]
722pub struct SpeculativePatternConfig {
723 #[serde(default)]
725 pub enabled: bool,
726 #[serde(default = "default_min_observations")]
728 pub min_observations: u32,
729 #[serde(default = "default_half_life_days")]
731 pub half_life_days: f64,
732 #[serde(default)]
735 pub rerank_provider: String,
736}
737
738impl Default for SpeculativePatternConfig {
739 fn default() -> Self {
740 Self {
741 enabled: false,
742 min_observations: default_min_observations(),
743 half_life_days: default_half_life_days(),
744 rerank_provider: String::new(),
745 }
746 }
747}
748
749#[derive(Debug, Clone, Default, Deserialize, Serialize)]
754pub struct SpeculativeAllowlistConfig {
755 #[serde(default)]
757 pub shell: Vec<String>,
758}
759
760#[derive(Debug, Clone, Deserialize, Serialize)]
777pub struct SpeculativeConfig {
778 #[serde(default)]
780 pub mode: SpeculationMode,
781 #[serde(default = "default_max_in_flight")]
783 pub max_in_flight: usize,
784 #[serde(default = "default_confidence_threshold")]
786 pub confidence_threshold: f32,
787 #[serde(default = "default_max_wasted_per_minute")]
789 pub max_wasted_per_minute: u64,
790 #[serde(default = "default_ttl_seconds")]
792 pub ttl_seconds: u64,
793 #[serde(default = "default_true")]
795 pub audit: bool,
796 #[serde(default)]
798 pub pattern: SpeculativePatternConfig,
799 #[serde(default)]
801 pub allowlist: SpeculativeAllowlistConfig,
802}
803
804impl Default for SpeculativeConfig {
805 fn default() -> Self {
806 Self {
807 mode: SpeculationMode::Off,
808 max_in_flight: default_max_in_flight(),
809 confidence_threshold: default_confidence_threshold(),
810 max_wasted_per_minute: default_max_wasted_per_minute(),
811 ttl_seconds: default_ttl_seconds(),
812 audit: true,
813 pattern: SpeculativePatternConfig::default(),
814 allowlist: SpeculativeAllowlistConfig::default(),
815 }
816 }
817}
818
819impl Default for AuditConfig {
820 fn default() -> Self {
821 Self {
822 enabled: true,
823 destination: default_audit_destination(),
824 tool_risk_summary: false,
825 }
826 }
827}
828
829#[derive(Debug, Clone, Default, Deserialize, Serialize)]
835pub struct AuthorizationConfig {
836 #[serde(default)]
838 pub enabled: bool,
839 #[serde(default)]
841 pub rules: Vec<PolicyRuleConfig>,
842}
843
844#[derive(Debug, Clone, Deserialize, Serialize)]
850#[serde(default)]
851#[allow(clippy::struct_excessive_bools)]
852pub struct EgressConfig {
853 pub enabled: bool,
855 pub log_blocked: bool,
858 pub log_response_bytes: bool,
860 pub log_hosts_to_tui: bool,
863}
864
865impl Default for EgressConfig {
866 fn default() -> Self {
867 Self {
868 enabled: true,
869 log_blocked: true,
870 log_response_bytes: true,
871 log_hosts_to_tui: true,
872 }
873 }
874}
875
876fn default_scrape_timeout() -> u64 {
877 15
878}
879
880fn default_max_body_bytes() -> usize {
881 4_194_304
882}
883
884#[derive(Debug, Deserialize, Serialize)]
886pub struct ScrapeConfig {
887 #[serde(default = "default_scrape_timeout")]
888 pub timeout: u64,
889 #[serde(default = "default_max_body_bytes")]
890 pub max_body_bytes: usize,
891 #[serde(default)]
900 pub allowed_domains: Vec<String>,
901 #[serde(default)]
904 pub denied_domains: Vec<String>,
905}
906
907impl Default for ScrapeConfig {
908 fn default() -> Self {
909 Self {
910 timeout: default_scrape_timeout(),
911 max_body_bytes: default_max_body_bytes(),
912 allowed_domains: Vec::new(),
913 denied_domains: Vec::new(),
914 }
915 }
916}
917
918fn default_sandbox_profile() -> crate::sandbox::SandboxProfile {
919 crate::sandbox::SandboxProfile::Workspace
920}
921
922fn default_sandbox_backend() -> String {
923 "auto".into()
924}
925
926#[derive(Debug, Clone, Deserialize, Serialize)]
947pub struct SandboxConfig {
948 #[serde(default)]
954 pub enabled: bool,
955
956 #[serde(default = "default_sandbox_profile")]
958 pub profile: crate::sandbox::SandboxProfile,
959
960 #[serde(default)]
962 pub allow_read: Vec<std::path::PathBuf>,
963
964 #[serde(default)]
966 pub allow_write: Vec<std::path::PathBuf>,
967
968 #[serde(default = "default_true")]
970 pub strict: bool,
971
972 #[serde(default = "default_sandbox_backend")]
976 pub backend: String,
977
978 #[serde(default)]
993 pub denied_domains: Vec<String>,
994
995 #[serde(default)]
1002 pub fail_if_unavailable: bool,
1003}
1004
1005impl Default for SandboxConfig {
1006 fn default() -> Self {
1007 Self {
1008 enabled: false,
1009 profile: default_sandbox_profile(),
1010 allow_read: Vec::new(),
1011 allow_write: Vec::new(),
1012 strict: true,
1013 backend: default_sandbox_backend(),
1014 denied_domains: Vec::new(),
1015 fail_if_unavailable: false,
1016 }
1017 }
1018}
1019
1020impl SandboxConfig {
1021 pub fn validate_denied_domains(&self) -> Result<(), String> {
1031 crate::domain_match::validate_domain_patterns(&self.denied_domains)
1032 }
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037 use super::*;
1038
1039 #[test]
1040 fn deserialize_default_config() {
1041 let toml_str = r#"
1042 enabled = true
1043
1044 [shell]
1045 timeout = 60
1046 blocked_commands = ["rm -rf /", "sudo"]
1047 "#;
1048
1049 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1050 assert!(config.enabled);
1051 assert_eq!(config.shell.timeout, 60);
1052 assert_eq!(config.shell.blocked_commands.len(), 2);
1053 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
1054 assert_eq!(config.shell.blocked_commands[1], "sudo");
1055 }
1056
1057 #[test]
1058 fn empty_blocked_commands() {
1059 let toml_str = r"
1060 [shell]
1061 timeout = 30
1062 ";
1063
1064 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1065 assert!(config.enabled);
1066 assert_eq!(config.shell.timeout, 30);
1067 assert!(config.shell.blocked_commands.is_empty());
1068 }
1069
1070 #[test]
1071 fn default_tools_config() {
1072 let config = ToolsConfig::default();
1073 assert!(config.enabled);
1074 assert!(config.summarize_output);
1075 assert_eq!(config.shell.timeout, 30);
1076 assert!(config.shell.blocked_commands.is_empty());
1077 assert!(config.audit.enabled);
1078 }
1079
1080 #[test]
1081 fn tools_summarize_output_default_true() {
1082 let config = ToolsConfig::default();
1083 assert!(config.summarize_output);
1084 }
1085
1086 #[test]
1087 fn tools_summarize_output_parsing() {
1088 let toml_str = r"
1089 summarize_output = true
1090 ";
1091 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1092 assert!(config.summarize_output);
1093 }
1094
1095 #[test]
1096 fn default_shell_config() {
1097 let config = ShellConfig::default();
1098 assert_eq!(config.timeout, 30);
1099 assert!(config.blocked_commands.is_empty());
1100 assert!(config.allowed_paths.is_empty());
1101 assert!(config.allow_network);
1102 assert!(!config.confirm_patterns.is_empty());
1103 }
1104
1105 #[test]
1106 fn deserialize_omitted_fields_use_defaults() {
1107 let toml_str = "";
1108 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1109 assert!(config.enabled);
1110 assert_eq!(config.shell.timeout, 30);
1111 assert!(config.shell.blocked_commands.is_empty());
1112 assert!(config.shell.allow_network);
1113 assert!(!config.shell.confirm_patterns.is_empty());
1114 assert_eq!(config.scrape.timeout, 15);
1115 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
1116 assert!(config.audit.enabled);
1117 assert_eq!(config.audit.destination, "stdout");
1118 assert!(config.summarize_output);
1119 }
1120
1121 #[test]
1122 fn default_scrape_config() {
1123 let config = ScrapeConfig::default();
1124 assert_eq!(config.timeout, 15);
1125 assert_eq!(config.max_body_bytes, 4_194_304);
1126 }
1127
1128 #[test]
1129 fn deserialize_scrape_config() {
1130 let toml_str = r"
1131 [scrape]
1132 timeout = 30
1133 max_body_bytes = 2097152
1134 ";
1135
1136 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1137 assert_eq!(config.scrape.timeout, 30);
1138 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
1139 }
1140
1141 #[test]
1142 fn tools_config_default_includes_scrape() {
1143 let config = ToolsConfig::default();
1144 assert_eq!(config.scrape.timeout, 15);
1145 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
1146 }
1147
1148 #[test]
1149 fn deserialize_allowed_commands() {
1150 let toml_str = r#"
1151 [shell]
1152 timeout = 30
1153 allowed_commands = ["curl", "wget"]
1154 "#;
1155
1156 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1157 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
1158 }
1159
1160 #[test]
1161 fn default_allowed_commands_empty() {
1162 let config = ShellConfig::default();
1163 assert!(config.allowed_commands.is_empty());
1164 }
1165
1166 #[test]
1167 fn deserialize_shell_security_fields() {
1168 let toml_str = r#"
1169 [shell]
1170 allowed_paths = ["/tmp", "/home/user"]
1171 allow_network = false
1172 confirm_patterns = ["rm ", "drop table"]
1173 "#;
1174
1175 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1176 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
1177 assert!(!config.shell.allow_network);
1178 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
1179 }
1180
1181 #[test]
1182 fn deserialize_audit_config() {
1183 let toml_str = r#"
1184 [audit]
1185 enabled = true
1186 destination = "/var/log/zeph-audit.log"
1187 "#;
1188
1189 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1190 assert!(config.audit.enabled);
1191 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
1192 }
1193
1194 #[test]
1195 fn default_audit_config() {
1196 let config = AuditConfig::default();
1197 assert!(config.enabled);
1198 assert_eq!(config.destination, "stdout");
1199 }
1200
1201 #[test]
1202 fn permission_policy_from_legacy_fields() {
1203 let config = ToolsConfig {
1204 shell: ShellConfig {
1205 blocked_commands: vec!["sudo".to_owned()],
1206 confirm_patterns: vec!["rm ".to_owned()],
1207 ..ShellConfig::default()
1208 },
1209 ..ToolsConfig::default()
1210 };
1211 let policy = config.permission_policy(AutonomyLevel::Supervised);
1212 assert_eq!(
1213 policy.check("bash", "sudo apt"),
1214 crate::permissions::PermissionAction::Deny
1215 );
1216 assert_eq!(
1217 policy.check("bash", "rm file"),
1218 crate::permissions::PermissionAction::Ask
1219 );
1220 }
1221
1222 #[test]
1223 fn permission_policy_from_explicit_config() {
1224 let toml_str = r#"
1225 [permissions]
1226 [[permissions.bash]]
1227 pattern = "*sudo*"
1228 action = "deny"
1229 "#;
1230 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1231 let policy = config.permission_policy(AutonomyLevel::Supervised);
1232 assert_eq!(
1233 policy.check("bash", "sudo rm"),
1234 crate::permissions::PermissionAction::Deny
1235 );
1236 }
1237
1238 #[test]
1239 fn permission_policy_default_uses_legacy() {
1240 let config = ToolsConfig::default();
1241 assert!(config.permissions.is_none());
1242 let policy = config.permission_policy(AutonomyLevel::Supervised);
1243 assert!(!config.shell.confirm_patterns.is_empty());
1245 assert!(policy.rules().contains_key("bash"));
1246 }
1247
1248 #[test]
1249 fn deserialize_overflow_config_full() {
1250 let toml_str = r"
1251 [overflow]
1252 threshold = 100000
1253 retention_days = 14
1254 max_overflow_bytes = 5242880
1255 ";
1256 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1257 assert_eq!(config.overflow.threshold, 100_000);
1258 assert_eq!(config.overflow.retention_days, 14);
1259 assert_eq!(config.overflow.max_overflow_bytes, 5_242_880);
1260 }
1261
1262 #[test]
1263 fn deserialize_overflow_config_unknown_dir_field_is_ignored() {
1264 let toml_str = r#"
1266 [overflow]
1267 threshold = 75000
1268 dir = "/tmp/overflow"
1269 "#;
1270 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1271 assert_eq!(config.overflow.threshold, 75_000);
1272 }
1273
1274 #[test]
1275 fn deserialize_overflow_config_partial_uses_defaults() {
1276 let toml_str = r"
1277 [overflow]
1278 threshold = 75000
1279 ";
1280 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1281 assert_eq!(config.overflow.threshold, 75_000);
1282 assert_eq!(config.overflow.retention_days, 7);
1283 }
1284
1285 #[test]
1286 fn deserialize_overflow_config_omitted_uses_defaults() {
1287 let config: ToolsConfig = toml::from_str("").unwrap();
1288 assert_eq!(config.overflow.threshold, 50_000);
1289 assert_eq!(config.overflow.retention_days, 7);
1290 assert_eq!(config.overflow.max_overflow_bytes, 10 * 1024 * 1024);
1291 }
1292
1293 #[test]
1294 fn result_cache_config_defaults() {
1295 let config = ResultCacheConfig::default();
1296 assert!(config.enabled);
1297 assert_eq!(config.ttl_secs, 300);
1298 }
1299
1300 #[test]
1301 fn deserialize_result_cache_config() {
1302 let toml_str = r"
1303 [result_cache]
1304 enabled = false
1305 ttl_secs = 60
1306 ";
1307 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1308 assert!(!config.result_cache.enabled);
1309 assert_eq!(config.result_cache.ttl_secs, 60);
1310 }
1311
1312 #[test]
1313 fn result_cache_omitted_uses_defaults() {
1314 let config: ToolsConfig = toml::from_str("").unwrap();
1315 assert!(config.result_cache.enabled);
1316 assert_eq!(config.result_cache.ttl_secs, 300);
1317 }
1318
1319 #[test]
1320 fn result_cache_ttl_zero_is_valid() {
1321 let toml_str = r"
1322 [result_cache]
1323 ttl_secs = 0
1324 ";
1325 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1326 assert_eq!(config.result_cache.ttl_secs, 0);
1327 }
1328
1329 #[test]
1330 fn adversarial_policy_default_exempt_tools_contains_skill_ops() {
1331 let exempt = AdversarialPolicyConfig::default_exempt_tools();
1332 assert!(
1333 exempt.contains(&"load_skill".to_string()),
1334 "default exempt_tools must contain load_skill"
1335 );
1336 assert!(
1337 exempt.contains(&"invoke_skill".to_string()),
1338 "default exempt_tools must contain invoke_skill"
1339 );
1340 }
1341
1342 #[test]
1343 fn utility_scoring_default_exempt_tools_contains_skill_ops() {
1344 let cfg = UtilityScoringConfig::default();
1345 assert!(
1346 cfg.exempt_tools.contains(&"invoke_skill".to_string()),
1347 "UtilityScoringConfig default exempt_tools must contain invoke_skill"
1348 );
1349 assert!(
1350 cfg.exempt_tools.contains(&"load_skill".to_string()),
1351 "UtilityScoringConfig default exempt_tools must contain load_skill"
1352 );
1353 }
1354
1355 #[test]
1356 fn utility_partial_toml_exempt_tools_uses_default_not_empty_vec() {
1357 let toml_str = r"
1360 [utility]
1361 enabled = true
1362 threshold = 0.1
1363 ";
1364 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1365 assert!(
1366 config
1367 .utility
1368 .exempt_tools
1369 .contains(&"invoke_skill".to_string()),
1370 "partial [tools.utility] TOML must populate exempt_tools with invoke_skill"
1371 );
1372 assert!(
1373 config
1374 .utility
1375 .exempt_tools
1376 .contains(&"load_skill".to_string()),
1377 "partial [tools.utility] TOML must populate exempt_tools with load_skill"
1378 );
1379 }
1380}