1use std::collections::HashMap;
11use std::path::PathBuf;
12
13use serde::{Deserialize, Serialize};
14
15use crate::providers::ProviderName;
16use zeph_common::SkillTrustLevel;
17
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
22#[serde(rename_all = "lowercase")]
23#[non_exhaustive]
24pub enum AutonomyLevel {
25 ReadOnly,
27 #[default]
29 Supervised,
30 Full,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
36#[serde(rename_all = "lowercase")]
37#[non_exhaustive]
38pub enum PermissionAction {
39 Allow,
41 Ask,
43 Deny,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct PermissionRule {
50 pub pattern: String,
52 pub action: PermissionAction,
54}
55
56#[derive(Debug, Clone, Deserialize, Serialize, Default)]
58pub struct PermissionsConfig {
59 #[serde(flatten)]
61 pub tools: HashMap<String, Vec<PermissionRule>>,
62}
63
64fn default_true() -> bool {
67 true
68}
69
70fn default_shell_tools() -> Vec<String> {
71 vec![
72 "bash".to_string(),
73 "shell".to_string(),
74 "terminal".to_string(),
75 ]
76}
77
78fn default_guarded_tools() -> Vec<String> {
79 vec!["fetch".to_string(), "web_scrape".to_string()]
80}
81
82#[derive(Debug, Clone, Deserialize, Serialize)]
84pub struct DestructiveVerifierConfig {
85 #[serde(default = "default_true")]
87 pub enabled: bool,
88 #[serde(default)]
90 pub allowed_paths: Vec<String>,
91 #[serde(default)]
93 pub extra_patterns: Vec<String>,
94 #[serde(default = "default_shell_tools")]
96 pub shell_tools: Vec<String>,
97}
98
99impl Default for DestructiveVerifierConfig {
100 fn default() -> Self {
101 Self {
102 enabled: true,
103 allowed_paths: Vec::new(),
104 extra_patterns: Vec::new(),
105 shell_tools: default_shell_tools(),
106 }
107 }
108}
109
110#[derive(Debug, Clone, Deserialize, Serialize)]
112pub struct InjectionVerifierConfig {
113 #[serde(default = "default_true")]
115 pub enabled: bool,
116 #[serde(default)]
118 pub extra_patterns: Vec<String>,
119 #[serde(default)]
121 pub allowlisted_urls: Vec<String>,
122}
123
124impl Default for InjectionVerifierConfig {
125 fn default() -> Self {
126 Self {
127 enabled: true,
128 extra_patterns: Vec::new(),
129 allowlisted_urls: Vec::new(),
130 }
131 }
132}
133
134#[derive(Debug, Clone, Deserialize, Serialize)]
136pub struct UrlGroundingVerifierConfig {
137 #[serde(default = "default_true")]
139 pub enabled: bool,
140 #[serde(default = "default_guarded_tools")]
142 pub guarded_tools: Vec<String>,
143}
144
145impl Default for UrlGroundingVerifierConfig {
146 fn default() -> Self {
147 Self {
148 enabled: true,
149 guarded_tools: default_guarded_tools(),
150 }
151 }
152}
153
154#[derive(Debug, Clone, Deserialize, Serialize)]
156pub struct FirewallVerifierConfig {
157 #[serde(default = "default_true")]
159 pub enabled: bool,
160 #[serde(default)]
162 pub blocked_paths: Vec<String>,
163 #[serde(default)]
165 pub blocked_env_vars: Vec<String>,
166 #[serde(default)]
168 pub exempt_tools: Vec<String>,
169}
170
171impl Default for FirewallVerifierConfig {
172 fn default() -> Self {
173 Self {
174 enabled: true,
175 blocked_paths: Vec::new(),
176 blocked_env_vars: Vec::new(),
177 exempt_tools: Vec::new(),
178 }
179 }
180}
181
182#[derive(Debug, Clone, Deserialize, Serialize)]
184pub struct PreExecutionVerifierConfig {
185 #[serde(default = "default_true")]
187 pub enabled: bool,
188 #[serde(default)]
190 pub destructive_commands: DestructiveVerifierConfig,
191 #[serde(default)]
193 pub injection_patterns: InjectionVerifierConfig,
194 #[serde(default)]
196 pub url_grounding: UrlGroundingVerifierConfig,
197 #[serde(default)]
199 pub firewall: FirewallVerifierConfig,
200}
201
202impl Default for PreExecutionVerifierConfig {
203 fn default() -> Self {
204 Self {
205 enabled: true,
206 destructive_commands: DestructiveVerifierConfig::default(),
207 injection_patterns: InjectionVerifierConfig::default(),
208 url_grounding: UrlGroundingVerifierConfig::default(),
209 firewall: FirewallVerifierConfig::default(),
210 }
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
218#[serde(rename_all = "snake_case")]
219#[non_exhaustive]
220pub enum PolicyEffect {
221 Allow,
223 Deny,
225}
226
227#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
229#[serde(rename_all = "lowercase")]
230#[non_exhaustive]
231pub enum DefaultEffect {
232 Allow,
234 #[default]
236 Deny,
237}
238
239fn default_deny() -> DefaultEffect {
240 DefaultEffect::Deny
241}
242
243#[derive(Debug, Clone, Deserialize, Serialize, Default)]
245pub struct PolicyConfig {
246 #[serde(default)]
248 pub enabled: bool,
249 #[serde(default = "default_deny")]
251 pub default_effect: DefaultEffect,
252 #[serde(default)]
254 pub rules: Vec<PolicyRuleConfig>,
255 pub policy_file: Option<String>,
257 #[serde(default)]
259 pub policy_provider: ProviderName,
260}
261
262#[derive(Debug, Clone, Deserialize, Serialize)]
264pub struct PolicyRuleConfig {
265 pub effect: PolicyEffect,
267 pub tool: String,
269 #[serde(default)]
271 pub paths: Vec<String>,
272 #[serde(default)]
274 pub env: Vec<String>,
275 pub trust_level: Option<SkillTrustLevel>,
277 pub args_match: Option<String>,
279 #[serde(default)]
281 pub capabilities: Vec<String>,
282}
283
284#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
288#[serde(rename_all = "kebab-case")]
289#[non_exhaustive]
290pub enum SandboxProfile {
291 ReadOnly,
293 #[default]
295 Workspace,
296 #[serde(rename = "network-allow-all", alias = "network")]
298 NetworkAllowAll,
299 Off,
301}
302
303fn default_sandbox_profile() -> SandboxProfile {
304 SandboxProfile::Workspace
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
312#[serde(rename_all = "kebab-case")]
313#[non_exhaustive]
314pub enum SandboxBackend {
315 #[default]
317 Auto,
318 Seatbelt,
320 LandlockBwrap,
322 Noop,
324}
325
326#[derive(Debug, Clone, Deserialize, Serialize)]
328pub struct SandboxConfig {
329 #[serde(default)]
331 pub enabled: bool,
332 #[serde(default = "default_sandbox_profile")]
334 pub profile: SandboxProfile,
335 #[serde(default)]
337 pub allow_read: Vec<PathBuf>,
338 #[serde(default)]
340 pub allow_write: Vec<PathBuf>,
341 #[serde(default = "default_true")]
343 pub strict: bool,
344 #[serde(default)]
348 pub backend: SandboxBackend,
349 #[serde(default)]
351 pub denied_domains: Vec<String>,
352 #[serde(default)]
354 pub fail_if_unavailable: bool,
355}
356
357impl Default for SandboxConfig {
358 fn default() -> Self {
359 Self {
360 enabled: false,
361 profile: default_sandbox_profile(),
362 allow_read: Vec::new(),
363 allow_write: Vec::new(),
364 strict: true,
365 backend: SandboxBackend::Auto,
366 denied_domains: Vec::new(),
367 fail_if_unavailable: false,
368 }
369 }
370}
371
372#[derive(Debug, Clone, Deserialize, Serialize)]
376pub struct SecurityFilterConfig {
377 #[serde(default = "default_true")]
379 pub enabled: bool,
380 #[serde(default)]
382 pub extra_patterns: Vec<String>,
383}
384
385impl Default for SecurityFilterConfig {
386 fn default() -> Self {
387 Self {
388 enabled: true,
389 extra_patterns: Vec::new(),
390 }
391 }
392}
393
394#[derive(Debug, Clone, Deserialize, Serialize)]
396pub struct FilterConfig {
397 #[serde(default = "default_true")]
399 pub enabled: bool,
400 #[serde(default)]
402 pub security: SecurityFilterConfig,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub filters_path: Option<PathBuf>,
406}
407
408impl Default for FilterConfig {
409 fn default() -> Self {
410 Self {
411 enabled: true,
412 security: SecurityFilterConfig::default(),
413 filters_path: None,
414 }
415 }
416}
417
418fn default_overflow_threshold() -> usize {
421 50_000
422}
423
424fn default_retention_days() -> u64 {
425 7
426}
427
428fn default_max_overflow_bytes() -> usize {
429 10 * 1024 * 1024
430}
431
432#[derive(Debug, Clone, Deserialize, Serialize)]
434pub struct OverflowConfig {
435 #[serde(default = "default_overflow_threshold")]
437 pub threshold: usize,
438 #[serde(default = "default_retention_days")]
440 pub retention_days: u64,
441 #[serde(default = "default_max_overflow_bytes")]
443 pub max_overflow_bytes: usize,
444}
445
446impl Default for OverflowConfig {
447 fn default() -> Self {
448 Self {
449 threshold: default_overflow_threshold(),
450 retention_days: default_retention_days(),
451 max_overflow_bytes: default_max_overflow_bytes(),
452 }
453 }
454}
455
456fn default_anomaly_window() -> usize {
457 10
458}
459
460fn default_anomaly_error_threshold() -> f64 {
461 0.5
462}
463
464fn default_anomaly_critical_threshold() -> f64 {
465 0.8
466}
467
468#[derive(Debug, Clone, Deserialize, Serialize)]
470pub struct AnomalyConfig {
471 #[serde(default = "default_true")]
473 pub enabled: bool,
474 #[serde(default = "default_anomaly_window")]
476 pub window_size: usize,
477 #[serde(default = "default_anomaly_error_threshold")]
479 pub error_threshold: f64,
480 #[serde(default = "default_anomaly_critical_threshold")]
482 pub critical_threshold: f64,
483 #[serde(default = "default_true")]
485 pub reasoning_model_warning: bool,
486}
487
488impl Default for AnomalyConfig {
489 fn default() -> Self {
490 Self {
491 enabled: true,
492 window_size: default_anomaly_window(),
493 error_threshold: default_anomaly_error_threshold(),
494 critical_threshold: default_anomaly_critical_threshold(),
495 reasoning_model_warning: true,
496 }
497 }
498}
499
500fn default_cache_ttl_secs() -> u64 {
501 300
502}
503
504#[derive(Debug, Clone, Deserialize, Serialize)]
506pub struct ResultCacheConfig {
507 #[serde(default = "default_true")]
509 pub enabled: bool,
510 #[serde(default = "default_cache_ttl_secs")]
512 pub ttl_secs: u64,
513}
514
515impl Default for ResultCacheConfig {
516 fn default() -> Self {
517 Self {
518 enabled: true,
519 ttl_secs: default_cache_ttl_secs(),
520 }
521 }
522}
523
524fn default_tafc_complexity_threshold() -> f64 {
525 0.6
526}
527
528#[derive(Debug, Clone, Deserialize, Serialize)]
530pub struct TafcConfig {
531 #[serde(default)]
533 pub enabled: bool,
534 #[serde(default = "default_tafc_complexity_threshold")]
536 pub complexity_threshold: f64,
537}
538
539impl Default for TafcConfig {
540 fn default() -> Self {
541 Self {
542 enabled: false,
543 complexity_threshold: default_tafc_complexity_threshold(),
544 }
545 }
546}
547
548impl TafcConfig {
549 #[must_use]
551 pub fn validated(mut self) -> Self {
552 if self.complexity_threshold.is_finite() {
553 self.complexity_threshold = self.complexity_threshold.clamp(0.0, 1.0);
554 } else {
555 self.complexity_threshold = 0.6;
556 }
557 self
558 }
559}
560
561fn default_utility_exempt_tools() -> Vec<String> {
562 vec!["invoke_skill".to_string(), "load_skill".to_string()]
563}
564
565fn default_utility_threshold() -> f32 {
566 0.1
567}
568
569fn default_utility_gain_weight() -> f32 {
570 1.0
571}
572
573fn default_utility_cost_weight() -> f32 {
574 0.5
575}
576
577fn default_utility_redundancy_weight() -> f32 {
578 0.3
579}
580
581fn default_utility_uncertainty_bonus() -> f32 {
582 0.2
583}
584
585#[derive(Debug, Clone, Deserialize, Serialize)]
587#[serde(default)]
588pub struct UtilityScoringConfig {
589 pub enabled: bool,
591 #[serde(default = "default_utility_threshold")]
593 pub threshold: f32,
594 #[serde(default = "default_utility_gain_weight")]
596 pub gain_weight: f32,
597 #[serde(default = "default_utility_cost_weight")]
599 pub cost_weight: f32,
600 #[serde(default = "default_utility_redundancy_weight")]
602 pub redundancy_weight: f32,
603 #[serde(default = "default_utility_uncertainty_bonus")]
605 pub uncertainty_bonus: f32,
606 #[serde(default = "default_utility_exempt_tools")]
608 pub exempt_tools: Vec<String>,
609 #[serde(default)]
614 pub utility_window: usize,
615 #[serde(default)]
625 pub high_gain_tools: Vec<String>,
626}
627
628impl Default for UtilityScoringConfig {
629 fn default() -> Self {
630 Self {
631 enabled: false,
632 threshold: default_utility_threshold(),
633 gain_weight: default_utility_gain_weight(),
634 cost_weight: default_utility_cost_weight(),
635 redundancy_weight: default_utility_redundancy_weight(),
636 uncertainty_bonus: default_utility_uncertainty_bonus(),
637 exempt_tools: default_utility_exempt_tools(),
638 utility_window: 0,
639 high_gain_tools: Vec::new(),
640 }
641 }
642}
643
644impl UtilityScoringConfig {
645 #[must_use = "validation result must be checked"]
651 pub fn validate(&self) -> Result<(), String> {
652 let fields = [
653 ("threshold", self.threshold),
654 ("gain_weight", self.gain_weight),
655 ("cost_weight", self.cost_weight),
656 ("redundancy_weight", self.redundancy_weight),
657 ("uncertainty_bonus", self.uncertainty_bonus),
658 ];
659 for (name, val) in fields {
660 if !val.is_finite() {
661 return Err(format!("[tools.utility] {name} must be finite, got {val}"));
662 }
663 if val < 0.0 {
664 return Err(format!("[tools.utility] {name} must be >= 0, got {val}"));
665 }
666 }
667 Ok(())
668 }
669}
670
671#[derive(Debug, Clone, Default, Deserialize, Serialize)]
673pub struct ToolDependency {
674 #[serde(default, skip_serializing_if = "Vec::is_empty")]
676 pub requires: Vec<String>,
677 #[serde(default, skip_serializing_if = "Vec::is_empty")]
679 pub prefers: Vec<String>,
680}
681
682fn default_boost_per_dep() -> f32 {
683 0.15
684}
685
686fn default_max_total_boost() -> f32 {
687 0.2
688}
689
690#[derive(Debug, Clone, Deserialize, Serialize)]
692pub struct DependencyConfig {
693 #[serde(default)]
695 pub enabled: bool,
696 #[serde(default = "default_boost_per_dep")]
698 pub boost_per_dep: f32,
699 #[serde(default = "default_max_total_boost")]
701 pub max_total_boost: f32,
702 #[serde(default)]
704 pub rules: HashMap<String, ToolDependency>,
705}
706
707impl Default for DependencyConfig {
708 fn default() -> Self {
709 Self {
710 enabled: false,
711 boost_per_dep: default_boost_per_dep(),
712 max_total_boost: default_max_total_boost(),
713 rules: HashMap::new(),
714 }
715 }
716}
717
718fn default_retry_max_attempts() -> usize {
719 2
720}
721
722fn default_retry_base_ms() -> u64 {
723 500
724}
725
726fn default_retry_max_ms() -> u64 {
727 5_000
728}
729
730fn default_retry_budget_secs() -> u64 {
731 30
732}
733
734#[derive(Debug, Clone, Deserialize, Serialize)]
736pub struct RetryConfig {
737 #[serde(default = "default_retry_max_attempts")]
739 pub max_attempts: usize,
740 #[serde(default = "default_retry_base_ms")]
742 pub base_ms: u64,
743 #[serde(default = "default_retry_max_ms")]
745 pub max_ms: u64,
746 #[serde(default = "default_retry_budget_secs")]
748 pub budget_secs: u64,
749 #[serde(default)]
752 pub parameter_reformat_provider: ProviderName,
753}
754
755impl Default for RetryConfig {
756 fn default() -> Self {
757 Self {
758 max_attempts: default_retry_max_attempts(),
759 base_ms: default_retry_base_ms(),
760 max_ms: default_retry_max_ms(),
761 budget_secs: default_retry_budget_secs(),
762 parameter_reformat_provider: ProviderName::default(),
763 }
764 }
765}
766
767fn default_adversarial_timeout_ms() -> u64 {
770 3_000
771}
772
773const LOCAL_PROVIDER_ADVERSARIAL_TIMEOUT_MS: u64 = 45_000;
780
781#[must_use]
810pub fn adversarial_timeout_for_provider_kind(provider_kind: &str) -> u64 {
811 if matches!(provider_kind, "ollama" | "candle" | "local") {
812 LOCAL_PROVIDER_ADVERSARIAL_TIMEOUT_MS
813 } else {
814 default_adversarial_timeout_ms()
815 }
816}
817
818#[derive(Debug, Clone, Deserialize, Serialize)]
820pub struct AdversarialPolicyConfig {
821 #[serde(default)]
823 pub enabled: bool,
824 #[serde(default)]
826 pub policy_provider: ProviderName,
827 pub policy_file: Option<String>,
829 #[serde(default)]
831 pub fail_open: bool,
832 #[serde(default)]
839 pub timeout_ms: Option<u64>,
840 #[serde(default = "AdversarialPolicyConfig::default_exempt_tools")]
842 pub exempt_tools: Vec<String>,
843}
844
845impl Default for AdversarialPolicyConfig {
846 fn default() -> Self {
847 Self {
848 enabled: false,
849 policy_provider: ProviderName::default(),
850 policy_file: None,
851 fail_open: false,
852 timeout_ms: None,
853 exempt_tools: Self::default_exempt_tools(),
854 }
855 }
856}
857
858impl AdversarialPolicyConfig {
859 #[must_use]
860 pub fn default_exempt_tools() -> Vec<String> {
861 vec![
862 "memory_save".into(),
863 "memory_search".into(),
864 "read_overflow".into(),
865 "load_skill".into(),
866 "invoke_skill".into(),
867 "schedule_deferred".into(),
868 "list_tasks".into(),
871 ]
872 }
873}
874
875#[derive(Debug, Clone, Default, Deserialize, Serialize)]
880pub struct FileConfig {
881 #[serde(default)]
883 pub deny_read: Vec<String>,
884 #[serde(default)]
886 pub allow_read: Vec<String>,
887}
888
889#[derive(Debug, Clone, Default, Deserialize, Serialize)]
891pub struct AuthorizationConfig {
892 #[serde(default)]
894 pub enabled: bool,
895 #[serde(default)]
897 pub rules: Vec<PolicyRuleConfig>,
898}
899
900#[derive(Debug, Clone, PartialEq, Eq, Default)]
904#[non_exhaustive]
905pub enum AuditDestination {
906 #[default]
908 Stdout,
909 Stderr,
911 File(std::path::PathBuf),
913}
914
915impl AuditDestination {
916 #[must_use]
918 pub fn is_stdout(&self) -> bool {
919 matches!(self, Self::Stdout)
920 }
921}
922
923impl serde::Serialize for AuditDestination {
924 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
925 match self {
926 Self::Stdout => s.serialize_str("stdout"),
927 Self::Stderr => s.serialize_str("stderr"),
928 Self::File(p) => s.serialize_str(&p.display().to_string()),
929 }
930 }
931}
932
933impl<'de> serde::Deserialize<'de> for AuditDestination {
934 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
935 let s = String::deserialize(d)?;
936 Ok(match s.as_str() {
937 "stdout" => Self::Stdout,
938 "stderr" => Self::Stderr,
939 path => Self::File(std::path::PathBuf::from(path)),
940 })
941 }
942}
943
944#[derive(Debug, Deserialize, Serialize)]
946pub struct AuditConfig {
947 #[serde(default = "default_true")]
949 pub enabled: bool,
950 #[serde(default)]
952 pub destination: AuditDestination,
953 #[serde(default)]
955 pub tool_risk_summary: bool,
956}
957
958impl Default for AuditConfig {
959 fn default() -> Self {
960 Self {
961 enabled: true,
962 destination: AuditDestination::default(),
963 tool_risk_summary: false,
964 }
965 }
966}
967
968fn default_timeout() -> u64 {
969 30
970}
971
972fn default_confirm_patterns() -> Vec<String> {
973 vec![
974 "rm ".into(),
975 "git push -f".into(),
976 "git push --force".into(),
977 "drop table".into(),
978 "drop database".into(),
979 "truncate ".into(),
980 "$(".into(),
981 "`".into(),
982 "<(".into(),
983 ">(".into(),
984 "<<<".into(),
985 "eval ".into(),
986 ]
987}
988
989fn default_max_background_runs() -> usize {
990 8
991}
992
993fn default_background_timeout_secs() -> u64 {
994 1800
995}
996
997fn default_max_checkpoints() -> usize {
998 20
999}
1000
1001#[derive(Debug, Deserialize, Serialize)]
1003#[allow(clippy::struct_excessive_bools)]
1004pub struct ShellConfig {
1005 #[serde(default = "default_timeout")]
1007 pub timeout: u64,
1008 #[serde(default)]
1010 pub blocked_commands: Vec<String>,
1011 #[serde(default)]
1013 pub allowed_commands: Vec<String>,
1014 #[serde(default)]
1016 pub allowed_paths: Vec<String>,
1017 #[serde(default = "default_true")]
1019 pub allow_network: bool,
1020 #[serde(default = "default_confirm_patterns")]
1022 pub confirm_patterns: Vec<String>,
1023 #[serde(default = "ShellConfig::default_env_blocklist")]
1025 pub env_blocklist: Vec<String>,
1026 #[serde(default)]
1028 pub transactional: bool,
1029 #[serde(default)]
1031 pub transaction_scope: Vec<String>,
1032 #[serde(default)]
1034 pub auto_rollback: bool,
1035 #[serde(default)]
1037 pub auto_rollback_exit_codes: Vec<i32>,
1038 #[serde(default)]
1040 pub snapshot_required: bool,
1041 #[serde(default)]
1043 pub max_snapshot_bytes: u64,
1044 #[serde(default = "default_max_background_runs")]
1046 pub max_background_runs: usize,
1047 #[serde(default = "default_background_timeout_secs")]
1049 pub background_timeout_secs: u64,
1050 #[serde(default)]
1055 pub risk_chain_threshold: Option<f32>,
1056 #[serde(default)]
1062 pub checkpoints_enabled: bool,
1063 #[serde(default = "default_max_checkpoints")]
1068 pub max_checkpoints: usize,
1069}
1070
1071impl Default for ShellConfig {
1072 fn default() -> Self {
1073 Self {
1074 timeout: default_timeout(),
1075 blocked_commands: Vec::new(),
1076 allowed_commands: Vec::new(),
1077 allowed_paths: Vec::new(),
1078 allow_network: true,
1079 confirm_patterns: default_confirm_patterns(),
1080 env_blocklist: Self::default_env_blocklist(),
1081 transactional: false,
1082 transaction_scope: Vec::new(),
1083 auto_rollback: false,
1084 auto_rollback_exit_codes: Vec::new(),
1085 snapshot_required: false,
1086 max_snapshot_bytes: 0,
1087 max_background_runs: default_max_background_runs(),
1088 background_timeout_secs: default_background_timeout_secs(),
1089 risk_chain_threshold: None,
1090 checkpoints_enabled: false,
1091 max_checkpoints: default_max_checkpoints(),
1092 }
1093 }
1094}
1095
1096impl ShellConfig {
1097 #[must_use]
1099 pub fn default_env_blocklist() -> Vec<String> {
1100 vec![
1101 "ZEPH_".into(),
1102 "AWS_".into(),
1103 "AZURE_".into(),
1104 "GCP_".into(),
1105 "GOOGLE_".into(),
1106 "OPENAI_".into(),
1107 "ANTHROPIC_".into(),
1108 "HF_".into(),
1109 "HUGGING".into(),
1110 ]
1111 }
1112}
1113
1114fn default_scrape_timeout() -> u64 {
1115 15
1116}
1117
1118fn default_max_body_bytes() -> usize {
1119 4_194_304
1120}
1121
1122fn default_ipi_filter_threshold() -> f32 {
1123 0.6
1124}
1125
1126#[derive(Debug, Deserialize, Serialize)]
1128pub struct ScrapeConfig {
1129 #[serde(default = "default_scrape_timeout")]
1131 pub timeout: u64,
1132 #[serde(default = "default_max_body_bytes")]
1134 pub max_body_bytes: usize,
1135 #[serde(default)]
1137 pub allowed_domains: Vec<String>,
1138 #[serde(default)]
1140 pub denied_domains: Vec<String>,
1141 #[serde(default = "default_ipi_filter_threshold")]
1144 pub ipi_filter_threshold: f32,
1145}
1146
1147impl Default for ScrapeConfig {
1148 fn default() -> Self {
1149 Self {
1150 timeout: default_scrape_timeout(),
1151 max_body_bytes: default_max_body_bytes(),
1152 allowed_domains: Vec::new(),
1153 denied_domains: Vec::new(),
1154 ipi_filter_threshold: default_ipi_filter_threshold(),
1155 }
1156 }
1157}
1158
1159#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
1161#[serde(rename_all = "kebab-case")]
1162#[non_exhaustive]
1163pub enum SpeculationMode {
1164 #[default]
1166 Off,
1167 Decoding,
1169 Pattern,
1171 Both,
1173}
1174
1175#[derive(Debug, Clone, Deserialize, Serialize)]
1177pub struct SpeculativePatternConfig {
1178 #[serde(default)]
1180 pub enabled: bool,
1181 #[serde(default = "default_min_observations")]
1183 pub min_observations: u32,
1184 #[serde(default = "default_half_life_days")]
1186 pub half_life_days: f64,
1187 #[serde(default)]
1189 pub rerank_provider: ProviderName,
1190}
1191
1192fn default_min_observations() -> u32 {
1193 5
1194}
1195
1196fn default_half_life_days() -> f64 {
1197 14.0
1198}
1199
1200impl Default for SpeculativePatternConfig {
1201 fn default() -> Self {
1202 Self {
1203 enabled: false,
1204 min_observations: default_min_observations(),
1205 half_life_days: default_half_life_days(),
1206 rerank_provider: ProviderName::default(),
1207 }
1208 }
1209}
1210
1211#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1213pub struct SpeculativeAllowlistConfig {
1214 #[serde(default)]
1216 pub shell: Vec<String>,
1217}
1218
1219fn default_max_in_flight() -> usize {
1220 4
1221}
1222
1223fn default_confidence_threshold() -> f32 {
1224 0.55
1225}
1226
1227fn default_max_wasted_per_minute() -> u64 {
1228 100
1229}
1230
1231fn default_ttl_seconds() -> u64 {
1232 30
1233}
1234
1235#[derive(Debug, Clone, Deserialize, Serialize)]
1237pub struct SpeculativeConfig {
1238 #[serde(default)]
1240 pub mode: SpeculationMode,
1241 #[serde(default = "default_max_in_flight")]
1243 pub max_in_flight: usize,
1244 #[serde(default = "default_confidence_threshold")]
1246 pub confidence_threshold: f32,
1247 #[serde(default = "default_max_wasted_per_minute")]
1249 pub max_wasted_per_minute: u64,
1250 #[serde(default = "default_ttl_seconds")]
1252 pub ttl_seconds: u64,
1253 #[serde(default = "default_true")]
1255 pub audit: bool,
1256 #[serde(default)]
1258 pub pattern: SpeculativePatternConfig,
1259 #[serde(default)]
1261 pub allowlist: SpeculativeAllowlistConfig,
1262}
1263
1264impl Default for SpeculativeConfig {
1265 fn default() -> Self {
1266 Self {
1267 mode: SpeculationMode::Off,
1268 max_in_flight: default_max_in_flight(),
1269 confidence_threshold: default_confidence_threshold(),
1270 max_wasted_per_minute: default_max_wasted_per_minute(),
1271 ttl_seconds: default_ttl_seconds(),
1272 audit: true,
1273 pattern: SpeculativePatternConfig::default(),
1274 allowlist: SpeculativeAllowlistConfig::default(),
1275 }
1276 }
1277}
1278
1279#[derive(Debug, Clone, Deserialize, Serialize)]
1281#[serde(default)]
1282#[allow(clippy::struct_excessive_bools)]
1283pub struct EgressConfig {
1284 pub enabled: bool,
1286 pub log_blocked: bool,
1288 pub log_response_bytes: bool,
1290 pub log_hosts_to_tui: bool,
1292}
1293
1294impl Default for EgressConfig {
1295 fn default() -> Self {
1296 Self {
1297 enabled: true,
1298 log_blocked: true,
1299 log_response_bytes: true,
1300 log_hosts_to_tui: true,
1301 }
1302 }
1303}
1304
1305fn default_compression_min_lines() -> usize {
1308 10
1309}
1310
1311fn default_compression_max_rules() -> u32 {
1312 200
1313}
1314
1315fn default_regex_compile_timeout_ms() -> u64 {
1316 500
1317}
1318
1319fn default_evolution_min_interval_secs() -> u64 {
1320 3600
1321}
1322
1323#[derive(Debug, Clone, Deserialize, Serialize)]
1338#[serde(default)]
1339pub struct ToolCompressionConfig {
1340 pub enabled: bool,
1342 #[serde(default = "default_compression_min_lines")]
1344 pub min_lines_to_compress: usize,
1345 #[serde(default)]
1347 pub evolution_provider: ProviderName,
1348 #[serde(default = "default_evolution_min_interval_secs")]
1350 pub evolution_min_interval_secs: u64,
1351 #[serde(default = "default_compression_max_rules")]
1353 pub max_rules: u32,
1354 #[serde(default = "default_regex_compile_timeout_ms")]
1356 pub regex_compile_timeout_ms: u64,
1357}
1358
1359impl Default for ToolCompressionConfig {
1360 fn default() -> Self {
1361 Self {
1362 enabled: false,
1363 min_lines_to_compress: default_compression_min_lines(),
1364 evolution_provider: ProviderName::default(),
1365 evolution_min_interval_secs: default_evolution_min_interval_secs(),
1366 max_rules: default_compression_max_rules(),
1367 regex_compile_timeout_ms: default_regex_compile_timeout_ms(),
1368 }
1369 }
1370}
1371
1372#[derive(Debug, Deserialize, Serialize)]
1380pub struct ToolsConfig {
1381 #[serde(default = "default_true")]
1383 pub enabled: bool,
1384 #[serde(default = "default_true")]
1386 pub summarize_output: bool,
1387 #[serde(default)]
1389 pub shell: ShellConfig,
1390 #[serde(default)]
1392 pub scrape: ScrapeConfig,
1393 #[serde(default)]
1395 pub audit: AuditConfig,
1396 #[serde(default)]
1398 pub permissions: Option<PermissionsConfig>,
1399 #[serde(default)]
1401 pub filters: FilterConfig,
1402 #[serde(default)]
1404 pub overflow: OverflowConfig,
1405 #[serde(default)]
1407 pub anomaly: AnomalyConfig,
1408 #[serde(default)]
1410 pub result_cache: ResultCacheConfig,
1411 #[serde(default)]
1413 pub tafc: TafcConfig,
1414 #[serde(default)]
1416 pub dependencies: DependencyConfig,
1417 #[serde(default)]
1419 pub retry: RetryConfig,
1420 #[serde(default)]
1422 pub policy: PolicyConfig,
1423 #[serde(default)]
1425 pub adversarial_policy: AdversarialPolicyConfig,
1426 #[serde(default)]
1428 pub utility: UtilityScoringConfig,
1429 #[serde(default)]
1431 pub file: FileConfig,
1432 #[serde(default)]
1434 pub authorization: AuthorizationConfig,
1435 #[serde(default)]
1437 pub max_tool_calls_per_session: Option<u32>,
1438 #[serde(default)]
1440 pub speculative: SpeculativeConfig,
1441 #[serde(default)]
1443 pub sandbox: SandboxConfig,
1444 #[serde(default)]
1446 pub egress: EgressConfig,
1447 #[serde(default)]
1449 pub compression: ToolCompressionConfig,
1450}
1451
1452impl Default for ToolsConfig {
1453 fn default() -> Self {
1454 Self {
1455 enabled: true,
1456 summarize_output: true,
1457 shell: ShellConfig::default(),
1458 scrape: ScrapeConfig::default(),
1459 audit: AuditConfig::default(),
1460 permissions: None,
1461 filters: FilterConfig::default(),
1462 overflow: OverflowConfig::default(),
1463 anomaly: AnomalyConfig::default(),
1464 result_cache: ResultCacheConfig::default(),
1465 tafc: TafcConfig::default(),
1466 dependencies: DependencyConfig::default(),
1467 retry: RetryConfig::default(),
1468 policy: PolicyConfig::default(),
1469 adversarial_policy: AdversarialPolicyConfig::default(),
1470 utility: UtilityScoringConfig::default(),
1471 file: FileConfig::default(),
1472 authorization: AuthorizationConfig::default(),
1473 max_tool_calls_per_session: None,
1474 speculative: SpeculativeConfig::default(),
1475 sandbox: SandboxConfig::default(),
1476 egress: EgressConfig::default(),
1477 compression: ToolCompressionConfig::default(),
1478 }
1479 }
1480}
1481
1482#[cfg(test)]
1483mod tests {
1484 use super::*;
1485
1486 #[test]
1487 fn deserialize_default_config() {
1488 let toml_str = r#"
1489 enabled = true
1490
1491 [shell]
1492 timeout = 60
1493 blocked_commands = ["rm -rf /", "sudo"]
1494 "#;
1495
1496 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1497 assert!(config.enabled);
1498 assert_eq!(config.shell.timeout, 60);
1499 assert_eq!(config.shell.blocked_commands.len(), 2);
1500 }
1501
1502 #[test]
1503 fn empty_blocked_commands() {
1504 let config: ToolsConfig = toml::from_str(r"[shell]\ntimeout = 30\n").unwrap_or_default();
1505 assert!(config.enabled);
1506 }
1507
1508 #[test]
1509 fn default_tools_config() {
1510 let config = ToolsConfig::default();
1511 assert!(config.enabled);
1512 assert!(config.summarize_output);
1513 assert_eq!(config.shell.timeout, 30);
1514 assert!(config.shell.blocked_commands.is_empty());
1515 assert!(config.audit.enabled);
1516 }
1517
1518 #[test]
1519 fn audit_destination_serde_roundtrip() {
1520 let cases = [
1521 ("\"stdout\"", AuditDestination::Stdout),
1522 ("\"stderr\"", AuditDestination::Stderr),
1523 (
1524 "\"/var/log/audit.log\"",
1525 AuditDestination::File("/var/log/audit.log".into()),
1526 ),
1527 ];
1528 for (json_str, expected) in cases {
1529 let got: AuditDestination = serde_json::from_str(json_str).unwrap();
1530 assert_eq!(got, expected);
1531 let serialized = serde_json::to_string(&got).unwrap();
1532 let roundtrip: AuditDestination = serde_json::from_str(&serialized).unwrap();
1533 assert_eq!(roundtrip, expected);
1534 }
1535 }
1536
1537 #[test]
1538 fn audit_destination_toml_in_config() {
1539 let cases = [
1540 (
1541 r#"[audit]
1542destination = "stdout""#,
1543 AuditDestination::Stdout,
1544 ),
1545 (
1546 r#"[audit]
1547destination = "stderr""#,
1548 AuditDestination::Stderr,
1549 ),
1550 (
1551 r#"[audit]
1552destination = "/var/log/zeph-audit.log""#,
1553 AuditDestination::File("/var/log/zeph-audit.log".into()),
1554 ),
1555 ];
1556 for (toml_str, expected) in cases {
1557 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1558 assert_eq!(config.audit.destination, expected);
1559 }
1560 }
1561
1562 #[test]
1563 fn policy_provider_serde_roundtrip() {
1564 let toml_str = r#"
1565 [policy]
1566 enabled = true
1567 policy_provider = "my-llm"
1568 "#;
1569 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1570 assert_eq!(config.policy.policy_provider.as_str(), "my-llm");
1571
1572 let json = serde_json::to_string(&config.policy).unwrap();
1573 let back: PolicyConfig = serde_json::from_str(&json).unwrap();
1574 assert_eq!(back.policy_provider.as_str(), "my-llm");
1575 }
1576
1577 #[test]
1578 fn policy_provider_default_is_empty() {
1579 let config = PolicyConfig::default();
1580 assert!(config.policy_provider.is_empty());
1581 }
1582
1583 #[test]
1584 fn utility_window_default_is_zero() {
1585 let config = UtilityScoringConfig::default();
1586 assert_eq!(config.utility_window, 0);
1587 }
1588
1589 #[test]
1590 fn utility_window_serde_roundtrip() {
1591 #[derive(serde::Deserialize)]
1592 struct Wrapper {
1593 utility_scoring: UtilityScoringConfig,
1594 }
1595
1596 let toml_str = "[utility_scoring]\nenabled = true\nutility_window = 3\n";
1597 let w: Wrapper = toml::from_str(toml_str).unwrap();
1598 assert_eq!(w.utility_scoring.utility_window, 3);
1599
1600 let json = serde_json::to_string(&w.utility_scoring).unwrap();
1601 let back: UtilityScoringConfig = serde_json::from_str(&json).unwrap();
1602 assert_eq!(back.utility_window, 3);
1603 }
1604
1605 #[test]
1606 fn high_gain_tools_default_is_empty() {
1607 let config = UtilityScoringConfig::default();
1608 assert!(config.high_gain_tools.is_empty());
1609 }
1610
1611 #[test]
1612 fn high_gain_tools_serde_roundtrip() {
1613 #[derive(serde::Deserialize)]
1614 struct Wrapper {
1615 utility_scoring: UtilityScoringConfig,
1616 }
1617
1618 let toml_str =
1619 "[utility_scoring]\nenabled = true\nhigh_gain_tools = [\"github_create_issue\"]\n";
1620 let w: Wrapper = toml::from_str(toml_str).unwrap();
1621 assert_eq!(
1622 w.utility_scoring.high_gain_tools,
1623 vec!["github_create_issue"]
1624 );
1625
1626 let json = serde_json::to_string(&w.utility_scoring).unwrap();
1627 let back: UtilityScoringConfig = serde_json::from_str(&json).unwrap();
1628 assert_eq!(back.high_gain_tools, vec!["github_create_issue"]);
1629 }
1630
1631 #[test]
1632 fn adversarial_policy_provider_default_is_empty() {
1633 let config = AdversarialPolicyConfig::default();
1634 assert!(config.policy_provider.is_empty());
1635 }
1636
1637 #[test]
1638 fn adversarial_policy_provider_serde_roundtrip() {
1639 #[derive(serde::Deserialize)]
1640 struct Wrapper {
1641 adversarial_policy: AdversarialPolicyConfig,
1642 }
1643 let toml_str = r#"
1644 [adversarial_policy]
1645 enabled = true
1646 policy_provider = "fast-llm"
1647 "#;
1648 let w: Wrapper = toml::from_str(toml_str).unwrap();
1649 assert_eq!(w.adversarial_policy.policy_provider.as_str(), "fast-llm");
1650
1651 let json = serde_json::to_string(&w.adversarial_policy).unwrap();
1652 let back: AdversarialPolicyConfig = serde_json::from_str(&json).unwrap();
1653 assert_eq!(back.policy_provider.as_str(), "fast-llm");
1654 }
1655
1656 #[test]
1657 fn adversarial_policy_provider_empty_when_omitted() {
1658 #[derive(serde::Deserialize)]
1659 struct Wrapper {
1660 adversarial_policy: AdversarialPolicyConfig,
1661 }
1662 let toml_str = r"
1663 [adversarial_policy]
1664 enabled = true
1665 ";
1666 let w: Wrapper = toml::from_str(toml_str).unwrap();
1667 assert!(
1668 w.adversarial_policy.policy_provider.is_empty(),
1669 "omitted policy_provider must default to empty (→ primary provider used)"
1670 );
1671 }
1672
1673 #[test]
1674 fn adversarial_policy_timeout_ms_defaults_to_none() {
1675 let config = AdversarialPolicyConfig::default();
1677 assert_eq!(config.timeout_ms, None);
1678 }
1679
1680 #[test]
1681 fn adversarial_policy_timeout_ms_explicit_override_roundtrips() {
1682 #[derive(serde::Deserialize)]
1683 struct Wrapper {
1684 adversarial_policy: AdversarialPolicyConfig,
1685 }
1686 let toml_str = r"
1687 [adversarial_policy]
1688 enabled = true
1689 timeout_ms = 90000
1690 ";
1691 let w: Wrapper = toml::from_str(toml_str).unwrap();
1692 assert_eq!(w.adversarial_policy.timeout_ms, Some(90_000));
1693
1694 let json = serde_json::to_string(&w.adversarial_policy).unwrap();
1695 let back: AdversarialPolicyConfig = serde_json::from_str(&json).unwrap();
1696 assert_eq!(back.timeout_ms, Some(90_000));
1697 }
1698
1699 #[test]
1700 fn adversarial_timeout_for_provider_kind_scales_local_vs_cloud() {
1701 assert_eq!(adversarial_timeout_for_provider_kind("ollama"), 45_000);
1702 assert_eq!(adversarial_timeout_for_provider_kind("candle"), 45_000);
1703 assert_eq!(adversarial_timeout_for_provider_kind("local"), 45_000);
1704 assert_eq!(adversarial_timeout_for_provider_kind("cloud"), 3_000);
1705 assert_eq!(adversarial_timeout_for_provider_kind("unknown"), 3_000);
1706 }
1707}