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
432fn default_max_per_call_override() -> usize {
433 131_072
434}
435
436#[derive(Debug, Clone, Deserialize, Serialize)]
438pub struct OverflowConfig {
439 #[serde(default = "default_overflow_threshold")]
441 pub threshold: usize,
442 #[serde(default = "default_retention_days")]
444 pub retention_days: u64,
445 #[serde(default = "default_max_overflow_bytes")]
447 pub max_overflow_bytes: usize,
448 #[serde(default = "default_max_per_call_override")]
467 pub max_per_call_override: usize,
468}
469
470impl Default for OverflowConfig {
471 fn default() -> Self {
472 Self {
473 threshold: default_overflow_threshold(),
474 retention_days: default_retention_days(),
475 max_overflow_bytes: default_max_overflow_bytes(),
476 max_per_call_override: default_max_per_call_override(),
477 }
478 }
479}
480
481fn default_anomaly_window() -> usize {
482 10
483}
484
485fn default_anomaly_error_threshold() -> f64 {
486 0.5
487}
488
489fn default_anomaly_critical_threshold() -> f64 {
490 0.8
491}
492
493#[derive(Debug, Clone, Deserialize, Serialize)]
495pub struct AnomalyConfig {
496 #[serde(default = "default_true")]
498 pub enabled: bool,
499 #[serde(default = "default_anomaly_window")]
501 pub window_size: usize,
502 #[serde(default = "default_anomaly_error_threshold")]
504 pub error_threshold: f64,
505 #[serde(default = "default_anomaly_critical_threshold")]
507 pub critical_threshold: f64,
508 #[serde(default = "default_true")]
510 pub reasoning_model_warning: bool,
511}
512
513impl Default for AnomalyConfig {
514 fn default() -> Self {
515 Self {
516 enabled: true,
517 window_size: default_anomaly_window(),
518 error_threshold: default_anomaly_error_threshold(),
519 critical_threshold: default_anomaly_critical_threshold(),
520 reasoning_model_warning: true,
521 }
522 }
523}
524
525fn default_cache_ttl_secs() -> u64 {
526 300
527}
528
529#[derive(Debug, Clone, Deserialize, Serialize)]
531pub struct ResultCacheConfig {
532 #[serde(default = "default_true")]
534 pub enabled: bool,
535 #[serde(default = "default_cache_ttl_secs")]
537 pub ttl_secs: u64,
538}
539
540impl Default for ResultCacheConfig {
541 fn default() -> Self {
542 Self {
543 enabled: true,
544 ttl_secs: default_cache_ttl_secs(),
545 }
546 }
547}
548
549fn default_tafc_complexity_threshold() -> f64 {
550 0.6
551}
552
553#[derive(Debug, Clone, Deserialize, Serialize)]
555pub struct TafcConfig {
556 #[serde(default)]
558 pub enabled: bool,
559 #[serde(default = "default_tafc_complexity_threshold")]
561 pub complexity_threshold: f64,
562}
563
564impl Default for TafcConfig {
565 fn default() -> Self {
566 Self {
567 enabled: false,
568 complexity_threshold: default_tafc_complexity_threshold(),
569 }
570 }
571}
572
573impl TafcConfig {
574 #[must_use]
576 pub fn validated(mut self) -> Self {
577 if self.complexity_threshold.is_finite() {
578 self.complexity_threshold = self.complexity_threshold.clamp(0.0, 1.0);
579 } else {
580 self.complexity_threshold = 0.6;
581 }
582 self
583 }
584}
585
586fn default_utility_exempt_tools() -> Vec<String> {
587 vec!["invoke_skill".to_string(), "load_skill".to_string()]
588}
589
590fn default_utility_threshold() -> f32 {
591 0.1
592}
593
594fn default_utility_gain_weight() -> f32 {
595 1.0
596}
597
598fn default_utility_cost_weight() -> f32 {
599 0.5
600}
601
602fn default_utility_redundancy_weight() -> f32 {
603 0.3
604}
605
606fn default_utility_uncertainty_bonus() -> f32 {
607 0.2
608}
609
610#[derive(Debug, Clone, Deserialize, Serialize)]
612#[serde(default)]
613pub struct UtilityScoringConfig {
614 pub enabled: bool,
616 #[serde(default = "default_utility_threshold")]
618 pub threshold: f32,
619 #[serde(default = "default_utility_gain_weight")]
621 pub gain_weight: f32,
622 #[serde(default = "default_utility_cost_weight")]
624 pub cost_weight: f32,
625 #[serde(default = "default_utility_redundancy_weight")]
627 pub redundancy_weight: f32,
628 #[serde(default = "default_utility_uncertainty_bonus")]
630 pub uncertainty_bonus: f32,
631 #[serde(default = "default_utility_exempt_tools")]
633 pub exempt_tools: Vec<String>,
634 #[serde(default)]
639 pub utility_window: usize,
640 #[serde(default)]
650 pub high_gain_tools: Vec<String>,
651}
652
653impl Default for UtilityScoringConfig {
654 fn default() -> Self {
655 Self {
656 enabled: false,
657 threshold: default_utility_threshold(),
658 gain_weight: default_utility_gain_weight(),
659 cost_weight: default_utility_cost_weight(),
660 redundancy_weight: default_utility_redundancy_weight(),
661 uncertainty_bonus: default_utility_uncertainty_bonus(),
662 exempt_tools: default_utility_exempt_tools(),
663 utility_window: 0,
664 high_gain_tools: Vec::new(),
665 }
666 }
667}
668
669impl UtilityScoringConfig {
670 #[must_use = "validation result must be checked"]
676 pub fn validate(&self) -> Result<(), String> {
677 let fields = [
678 ("threshold", self.threshold),
679 ("gain_weight", self.gain_weight),
680 ("cost_weight", self.cost_weight),
681 ("redundancy_weight", self.redundancy_weight),
682 ("uncertainty_bonus", self.uncertainty_bonus),
683 ];
684 for (name, val) in fields {
685 if !val.is_finite() {
686 return Err(format!("[tools.utility] {name} must be finite, got {val}"));
687 }
688 if val < 0.0 {
689 return Err(format!("[tools.utility] {name} must be >= 0, got {val}"));
690 }
691 }
692 Ok(())
693 }
694}
695
696#[derive(Debug, Clone, Default, Deserialize, Serialize)]
698pub struct ToolDependency {
699 #[serde(default, skip_serializing_if = "Vec::is_empty")]
701 pub requires: Vec<String>,
702 #[serde(default, skip_serializing_if = "Vec::is_empty")]
704 pub prefers: Vec<String>,
705}
706
707fn default_boost_per_dep() -> f32 {
708 0.15
709}
710
711fn default_max_total_boost() -> f32 {
712 0.2
713}
714
715#[derive(Debug, Clone, Deserialize, Serialize)]
717pub struct DependencyConfig {
718 #[serde(default)]
720 pub enabled: bool,
721 #[serde(default = "default_boost_per_dep")]
723 pub boost_per_dep: f32,
724 #[serde(default = "default_max_total_boost")]
726 pub max_total_boost: f32,
727 #[serde(default)]
729 pub rules: HashMap<String, ToolDependency>,
730}
731
732impl Default for DependencyConfig {
733 fn default() -> Self {
734 Self {
735 enabled: false,
736 boost_per_dep: default_boost_per_dep(),
737 max_total_boost: default_max_total_boost(),
738 rules: HashMap::new(),
739 }
740 }
741}
742
743fn default_retry_max_attempts() -> usize {
744 2
745}
746
747fn default_retry_base_ms() -> u64 {
748 500
749}
750
751fn default_retry_max_ms() -> u64 {
752 5_000
753}
754
755fn default_retry_budget_secs() -> u64 {
756 30
757}
758
759#[derive(Debug, Clone, Deserialize, Serialize)]
761pub struct RetryConfig {
762 #[serde(default = "default_retry_max_attempts")]
764 pub max_attempts: usize,
765 #[serde(default = "default_retry_base_ms")]
767 pub base_ms: u64,
768 #[serde(default = "default_retry_max_ms")]
770 pub max_ms: u64,
771 #[serde(default = "default_retry_budget_secs")]
773 pub budget_secs: u64,
774 #[serde(default)]
777 pub parameter_reformat_provider: ProviderName,
778}
779
780impl Default for RetryConfig {
781 fn default() -> Self {
782 Self {
783 max_attempts: default_retry_max_attempts(),
784 base_ms: default_retry_base_ms(),
785 max_ms: default_retry_max_ms(),
786 budget_secs: default_retry_budget_secs(),
787 parameter_reformat_provider: ProviderName::default(),
788 }
789 }
790}
791
792fn default_adversarial_timeout_ms() -> u64 {
795 3_000
796}
797
798const LOCAL_PROVIDER_ADVERSARIAL_TIMEOUT_MS: u64 = 45_000;
805
806#[must_use]
835pub fn adversarial_timeout_for_provider_kind(provider_kind: &str) -> u64 {
836 if matches!(provider_kind, "ollama" | "candle" | "local") {
837 LOCAL_PROVIDER_ADVERSARIAL_TIMEOUT_MS
838 } else {
839 default_adversarial_timeout_ms()
840 }
841}
842
843#[derive(Debug, Clone, Deserialize, Serialize)]
845pub struct AdversarialPolicyConfig {
846 #[serde(default)]
848 pub enabled: bool,
849 #[serde(default)]
851 pub policy_provider: ProviderName,
852 pub policy_file: Option<String>,
854 #[serde(default)]
856 pub fail_open: bool,
857 #[serde(default)]
864 pub timeout_ms: Option<u64>,
865 #[serde(default = "AdversarialPolicyConfig::default_exempt_tools")]
867 pub exempt_tools: Vec<String>,
868}
869
870impl Default for AdversarialPolicyConfig {
871 fn default() -> Self {
872 Self {
873 enabled: false,
874 policy_provider: ProviderName::default(),
875 policy_file: None,
876 fail_open: false,
877 timeout_ms: None,
878 exempt_tools: Self::default_exempt_tools(),
879 }
880 }
881}
882
883impl AdversarialPolicyConfig {
884 #[must_use]
885 pub fn default_exempt_tools() -> Vec<String> {
886 vec![
887 "memory_save".into(),
888 "memory_search".into(),
889 "read_overflow".into(),
890 "load_skill".into(),
891 "invoke_skill".into(),
892 "schedule_deferred".into(),
893 "list_tasks".into(),
896 ]
897 }
898}
899
900#[derive(Debug, Clone, Default, Deserialize, Serialize)]
905pub struct FileConfig {
906 #[serde(default)]
908 pub deny_read: Vec<String>,
909 #[serde(default)]
911 pub allow_read: Vec<String>,
912}
913
914#[derive(Debug, Clone, Default, Deserialize, Serialize)]
916pub struct AuthorizationConfig {
917 #[serde(default)]
919 pub enabled: bool,
920 #[serde(default)]
922 pub rules: Vec<PolicyRuleConfig>,
923}
924
925#[derive(Debug, Clone, PartialEq, Eq, Default)]
929#[non_exhaustive]
930pub enum AuditDestination {
931 #[default]
933 Stdout,
934 Stderr,
936 File(std::path::PathBuf),
938}
939
940impl AuditDestination {
941 #[must_use]
943 pub fn is_stdout(&self) -> bool {
944 matches!(self, Self::Stdout)
945 }
946}
947
948impl serde::Serialize for AuditDestination {
949 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
950 match self {
951 Self::Stdout => s.serialize_str("stdout"),
952 Self::Stderr => s.serialize_str("stderr"),
953 Self::File(p) => s.serialize_str(&p.display().to_string()),
954 }
955 }
956}
957
958impl<'de> serde::Deserialize<'de> for AuditDestination {
959 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
960 let s = String::deserialize(d)?;
961 Ok(match s.as_str() {
962 "stdout" => Self::Stdout,
963 "stderr" => Self::Stderr,
964 path => Self::File(std::path::PathBuf::from(path)),
965 })
966 }
967}
968
969#[derive(Debug, Deserialize, Serialize)]
971pub struct AuditConfig {
972 #[serde(default = "default_true")]
974 pub enabled: bool,
975 #[serde(default)]
977 pub destination: AuditDestination,
978 #[serde(default)]
980 pub tool_risk_summary: bool,
981}
982
983impl Default for AuditConfig {
984 fn default() -> Self {
985 Self {
986 enabled: true,
987 destination: AuditDestination::default(),
988 tool_risk_summary: false,
989 }
990 }
991}
992
993fn default_timeout() -> u64 {
994 30
995}
996
997fn default_confirm_patterns() -> Vec<String> {
998 vec![
999 "rm ".into(),
1000 "git push -f".into(),
1001 "git push --force".into(),
1002 "drop table".into(),
1003 "drop database".into(),
1004 "truncate ".into(),
1005 "$(".into(),
1006 "`".into(),
1007 "<(".into(),
1008 ">(".into(),
1009 "<<<".into(),
1010 "eval ".into(),
1011 ]
1012}
1013
1014fn default_max_background_runs() -> usize {
1015 8
1016}
1017
1018fn default_background_timeout_secs() -> u64 {
1019 1800
1020}
1021
1022fn default_max_checkpoints() -> usize {
1023 20
1024}
1025
1026#[derive(Debug, Clone, Deserialize, Serialize)]
1028#[allow(clippy::struct_excessive_bools)]
1029pub struct ShellConfig {
1030 #[serde(default = "default_timeout")]
1032 pub timeout: u64,
1033 #[serde(default)]
1035 pub blocked_commands: Vec<String>,
1036 #[serde(default)]
1038 pub allowed_commands: Vec<String>,
1039 #[serde(default)]
1041 pub allowed_paths: Vec<String>,
1042 #[serde(default = "default_true")]
1044 pub allow_network: bool,
1045 #[serde(default = "default_confirm_patterns")]
1047 pub confirm_patterns: Vec<String>,
1048 #[serde(default = "ShellConfig::default_env_blocklist")]
1050 pub env_blocklist: Vec<String>,
1051 #[serde(default)]
1053 pub transactional: bool,
1054 #[serde(default)]
1056 pub transaction_scope: Vec<String>,
1057 #[serde(default)]
1059 pub auto_rollback: bool,
1060 #[serde(default)]
1062 pub auto_rollback_exit_codes: Vec<i32>,
1063 #[serde(default)]
1065 pub snapshot_required: bool,
1066 #[serde(default)]
1068 pub max_snapshot_bytes: u64,
1069 #[serde(default = "default_max_background_runs")]
1071 pub max_background_runs: usize,
1072 #[serde(default = "default_background_timeout_secs")]
1074 pub background_timeout_secs: u64,
1075 #[serde(default)]
1080 pub risk_chain_threshold: Option<f32>,
1081 #[serde(default)]
1087 pub checkpoints_enabled: bool,
1088 #[serde(default = "default_max_checkpoints")]
1093 pub max_checkpoints: usize,
1094}
1095
1096impl Default for ShellConfig {
1097 fn default() -> Self {
1098 Self {
1099 timeout: default_timeout(),
1100 blocked_commands: Vec::new(),
1101 allowed_commands: Vec::new(),
1102 allowed_paths: Vec::new(),
1103 allow_network: true,
1104 confirm_patterns: default_confirm_patterns(),
1105 env_blocklist: Self::default_env_blocklist(),
1106 transactional: false,
1107 transaction_scope: Vec::new(),
1108 auto_rollback: false,
1109 auto_rollback_exit_codes: Vec::new(),
1110 snapshot_required: false,
1111 max_snapshot_bytes: 0,
1112 max_background_runs: default_max_background_runs(),
1113 background_timeout_secs: default_background_timeout_secs(),
1114 risk_chain_threshold: None,
1115 checkpoints_enabled: false,
1116 max_checkpoints: default_max_checkpoints(),
1117 }
1118 }
1119}
1120
1121impl ShellConfig {
1122 #[must_use]
1124 pub fn default_env_blocklist() -> Vec<String> {
1125 vec![
1126 "ZEPH_".into(),
1127 "AWS_".into(),
1128 "AZURE_".into(),
1129 "GCP_".into(),
1130 "GOOGLE_".into(),
1131 "OPENAI_".into(),
1132 "ANTHROPIC_".into(),
1133 "HF_".into(),
1134 "HUGGING".into(),
1135 ]
1136 }
1137}
1138
1139fn default_scrape_timeout() -> u64 {
1140 15
1141}
1142
1143fn default_max_body_bytes() -> usize {
1144 4_194_304
1145}
1146
1147fn default_ipi_filter_threshold() -> f32 {
1148 0.6
1149}
1150
1151#[derive(Debug, Deserialize, Serialize)]
1153pub struct ScrapeConfig {
1154 #[serde(default = "default_scrape_timeout")]
1156 pub timeout: u64,
1157 #[serde(default = "default_max_body_bytes")]
1159 pub max_body_bytes: usize,
1160 #[serde(default)]
1162 pub allowed_domains: Vec<String>,
1163 #[serde(default)]
1165 pub denied_domains: Vec<String>,
1166 #[serde(default = "default_ipi_filter_threshold")]
1169 pub ipi_filter_threshold: f32,
1170}
1171
1172impl Default for ScrapeConfig {
1173 fn default() -> Self {
1174 Self {
1175 timeout: default_scrape_timeout(),
1176 max_body_bytes: default_max_body_bytes(),
1177 allowed_domains: Vec::new(),
1178 denied_domains: Vec::new(),
1179 ipi_filter_threshold: default_ipi_filter_threshold(),
1180 }
1181 }
1182}
1183
1184fn default_search_backend() -> String {
1185 "brave".to_owned()
1186}
1187
1188fn default_search_vault_key() -> String {
1189 "ZEPH_WEB_SEARCH_API_KEY".to_owned()
1190}
1191
1192fn default_search_endpoint() -> String {
1193 "https://api.search.brave.com/res/v1/web/search".to_owned()
1194}
1195
1196fn default_search_max_results() -> usize {
1197 10
1198}
1199
1200fn default_search_timeout() -> u64 {
1201 15
1202}
1203
1204#[derive(Debug, Clone, Deserialize, Serialize)]
1213pub struct SearchConfig {
1214 #[serde(default)]
1216 pub enabled: bool,
1217 #[serde(default = "default_search_backend")]
1219 pub backend: String,
1220 #[serde(default = "default_search_vault_key")]
1223 pub api_key_vault_key: String,
1224 #[serde(default = "default_search_endpoint")]
1227 pub endpoint: String,
1228 #[serde(default = "default_search_max_results")]
1230 pub max_results: usize,
1231 #[serde(default = "default_search_timeout")]
1233 pub timeout: u64,
1234}
1235
1236impl Default for SearchConfig {
1237 fn default() -> Self {
1238 Self {
1239 enabled: false,
1240 backend: default_search_backend(),
1241 api_key_vault_key: default_search_vault_key(),
1242 endpoint: default_search_endpoint(),
1243 max_results: default_search_max_results(),
1244 timeout: default_search_timeout(),
1245 }
1246 }
1247}
1248
1249#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
1251#[serde(rename_all = "kebab-case")]
1252#[non_exhaustive]
1253pub enum SpeculationMode {
1254 #[default]
1256 Off,
1257 Decoding,
1259 Pattern,
1261 Both,
1263}
1264
1265#[derive(Debug, Clone, Deserialize, Serialize)]
1267pub struct SpeculativePatternConfig {
1268 #[serde(default)]
1270 pub enabled: bool,
1271 #[serde(default = "default_min_observations")]
1273 pub min_observations: u32,
1274 #[serde(default = "default_half_life_days")]
1276 pub half_life_days: f64,
1277 #[serde(default)]
1279 pub rerank_provider: ProviderName,
1280}
1281
1282fn default_min_observations() -> u32 {
1283 5
1284}
1285
1286fn default_half_life_days() -> f64 {
1287 14.0
1288}
1289
1290impl Default for SpeculativePatternConfig {
1291 fn default() -> Self {
1292 Self {
1293 enabled: false,
1294 min_observations: default_min_observations(),
1295 half_life_days: default_half_life_days(),
1296 rerank_provider: ProviderName::default(),
1297 }
1298 }
1299}
1300
1301#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1303pub struct SpeculativeAllowlistConfig {
1304 #[serde(default)]
1306 pub shell: Vec<String>,
1307}
1308
1309fn default_max_in_flight() -> usize {
1310 4
1311}
1312
1313fn default_confidence_threshold() -> f32 {
1314 0.55
1315}
1316
1317fn default_max_wasted_per_minute() -> u64 {
1318 100
1319}
1320
1321fn default_ttl_seconds() -> u64 {
1322 30
1323}
1324
1325#[derive(Debug, Clone, Deserialize, Serialize)]
1327pub struct SpeculativeConfig {
1328 #[serde(default)]
1330 pub mode: SpeculationMode,
1331 #[serde(default = "default_max_in_flight")]
1333 pub max_in_flight: usize,
1334 #[serde(default = "default_confidence_threshold")]
1336 pub confidence_threshold: f32,
1337 #[serde(default = "default_max_wasted_per_minute")]
1339 pub max_wasted_per_minute: u64,
1340 #[serde(default = "default_ttl_seconds")]
1342 pub ttl_seconds: u64,
1343 #[serde(default = "default_true")]
1345 pub audit: bool,
1346 #[serde(default)]
1348 pub pattern: SpeculativePatternConfig,
1349 #[serde(default)]
1351 pub allowlist: SpeculativeAllowlistConfig,
1352}
1353
1354impl Default for SpeculativeConfig {
1355 fn default() -> Self {
1356 Self {
1357 mode: SpeculationMode::Off,
1358 max_in_flight: default_max_in_flight(),
1359 confidence_threshold: default_confidence_threshold(),
1360 max_wasted_per_minute: default_max_wasted_per_minute(),
1361 ttl_seconds: default_ttl_seconds(),
1362 audit: true,
1363 pattern: SpeculativePatternConfig::default(),
1364 allowlist: SpeculativeAllowlistConfig::default(),
1365 }
1366 }
1367}
1368
1369#[derive(Debug, Clone, Deserialize, Serialize)]
1371#[serde(default)]
1372#[allow(clippy::struct_excessive_bools)]
1373pub struct EgressConfig {
1374 pub enabled: bool,
1376 pub log_blocked: bool,
1378 pub log_response_bytes: bool,
1380 pub log_hosts_to_tui: bool,
1382}
1383
1384impl Default for EgressConfig {
1385 fn default() -> Self {
1386 Self {
1387 enabled: true,
1388 log_blocked: true,
1389 log_response_bytes: true,
1390 log_hosts_to_tui: true,
1391 }
1392 }
1393}
1394
1395fn default_compression_min_lines() -> usize {
1398 10
1399}
1400
1401fn default_compression_max_rules() -> u32 {
1402 200
1403}
1404
1405fn default_regex_compile_timeout_ms() -> u64 {
1406 500
1407}
1408
1409fn default_evolution_min_interval_secs() -> u64 {
1410 3600
1411}
1412
1413#[derive(Debug, Clone, Deserialize, Serialize)]
1428#[serde(default)]
1429pub struct ToolCompressionConfig {
1430 pub enabled: bool,
1432 #[serde(default = "default_compression_min_lines")]
1434 pub min_lines_to_compress: usize,
1435 #[serde(default)]
1437 pub evolution_provider: ProviderName,
1438 #[serde(default = "default_evolution_min_interval_secs")]
1440 pub evolution_min_interval_secs: u64,
1441 #[serde(default = "default_compression_max_rules")]
1443 pub max_rules: u32,
1444 #[serde(default = "default_regex_compile_timeout_ms")]
1446 pub regex_compile_timeout_ms: u64,
1447}
1448
1449impl Default for ToolCompressionConfig {
1450 fn default() -> Self {
1451 Self {
1452 enabled: false,
1453 min_lines_to_compress: default_compression_min_lines(),
1454 evolution_provider: ProviderName::default(),
1455 evolution_min_interval_secs: default_evolution_min_interval_secs(),
1456 max_rules: default_compression_max_rules(),
1457 regex_compile_timeout_ms: default_regex_compile_timeout_ms(),
1458 }
1459 }
1460}
1461
1462#[derive(Debug, Deserialize, Serialize)]
1470pub struct ToolsConfig {
1471 #[serde(default = "default_true")]
1474 pub enabled: bool,
1475 #[serde(default = "default_true")]
1477 pub summarize_output: bool,
1478 #[serde(default)]
1480 pub shell: ShellConfig,
1481 #[serde(default)]
1483 pub scrape: ScrapeConfig,
1484 #[serde(default)]
1486 pub search: SearchConfig,
1487 #[serde(default)]
1489 pub audit: AuditConfig,
1490 #[serde(default)]
1492 pub permissions: Option<PermissionsConfig>,
1493 #[serde(default)]
1495 pub filters: FilterConfig,
1496 #[serde(default)]
1498 pub overflow: OverflowConfig,
1499 #[serde(default)]
1501 pub anomaly: AnomalyConfig,
1502 #[serde(default)]
1504 pub result_cache: ResultCacheConfig,
1505 #[serde(default)]
1507 pub tafc: TafcConfig,
1508 #[serde(default)]
1510 pub dependencies: DependencyConfig,
1511 #[serde(default)]
1513 pub retry: RetryConfig,
1514 #[serde(default)]
1516 pub policy: PolicyConfig,
1517 #[serde(default)]
1519 pub adversarial_policy: AdversarialPolicyConfig,
1520 #[serde(default)]
1522 pub utility: UtilityScoringConfig,
1523 #[serde(default)]
1525 pub file: FileConfig,
1526 #[serde(default)]
1528 pub authorization: AuthorizationConfig,
1529 #[serde(default)]
1531 pub max_tool_calls_per_session: Option<u32>,
1532 #[serde(default)]
1534 pub speculative: SpeculativeConfig,
1535 #[serde(default)]
1537 pub sandbox: SandboxConfig,
1538 #[serde(default)]
1540 pub egress: EgressConfig,
1541 #[serde(default)]
1543 pub compression: ToolCompressionConfig,
1544}
1545
1546impl Default for ToolsConfig {
1547 fn default() -> Self {
1548 Self {
1549 enabled: true,
1550 summarize_output: true,
1551 shell: ShellConfig::default(),
1552 scrape: ScrapeConfig::default(),
1553 search: SearchConfig::default(),
1554 audit: AuditConfig::default(),
1555 permissions: None,
1556 filters: FilterConfig::default(),
1557 overflow: OverflowConfig::default(),
1558 anomaly: AnomalyConfig::default(),
1559 result_cache: ResultCacheConfig::default(),
1560 tafc: TafcConfig::default(),
1561 dependencies: DependencyConfig::default(),
1562 retry: RetryConfig::default(),
1563 policy: PolicyConfig::default(),
1564 adversarial_policy: AdversarialPolicyConfig::default(),
1565 utility: UtilityScoringConfig::default(),
1566 file: FileConfig::default(),
1567 authorization: AuthorizationConfig::default(),
1568 max_tool_calls_per_session: None,
1569 speculative: SpeculativeConfig::default(),
1570 sandbox: SandboxConfig::default(),
1571 egress: EgressConfig::default(),
1572 compression: ToolCompressionConfig::default(),
1573 }
1574 }
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579 use super::*;
1580
1581 #[test]
1582 fn deserialize_default_config() {
1583 let toml_str = r#"
1584 enabled = true
1585
1586 [shell]
1587 timeout = 60
1588 blocked_commands = ["rm -rf /", "sudo"]
1589 "#;
1590
1591 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1592 assert!(config.enabled);
1593 assert_eq!(config.shell.timeout, 60);
1594 assert_eq!(config.shell.blocked_commands.len(), 2);
1595 }
1596
1597 #[test]
1598 fn empty_blocked_commands() {
1599 let config: ToolsConfig = toml::from_str(r"[shell]\ntimeout = 30\n").unwrap_or_default();
1600 assert!(config.enabled);
1601 }
1602
1603 #[test]
1604 fn default_tools_config() {
1605 let config = ToolsConfig::default();
1606 assert!(config.enabled);
1607 assert!(config.summarize_output);
1608 assert_eq!(config.shell.timeout, 30);
1609 assert!(config.shell.blocked_commands.is_empty());
1610 assert!(config.audit.enabled);
1611 }
1612
1613 #[test]
1614 fn audit_destination_serde_roundtrip() {
1615 let cases = [
1616 ("\"stdout\"", AuditDestination::Stdout),
1617 ("\"stderr\"", AuditDestination::Stderr),
1618 (
1619 "\"/var/log/audit.log\"",
1620 AuditDestination::File("/var/log/audit.log".into()),
1621 ),
1622 ];
1623 for (json_str, expected) in cases {
1624 let got: AuditDestination = serde_json::from_str(json_str).unwrap();
1625 assert_eq!(got, expected);
1626 let serialized = serde_json::to_string(&got).unwrap();
1627 let roundtrip: AuditDestination = serde_json::from_str(&serialized).unwrap();
1628 assert_eq!(roundtrip, expected);
1629 }
1630 }
1631
1632 #[test]
1633 fn audit_destination_toml_in_config() {
1634 let cases = [
1635 (
1636 r#"[audit]
1637destination = "stdout""#,
1638 AuditDestination::Stdout,
1639 ),
1640 (
1641 r#"[audit]
1642destination = "stderr""#,
1643 AuditDestination::Stderr,
1644 ),
1645 (
1646 r#"[audit]
1647destination = "/var/log/zeph-audit.log""#,
1648 AuditDestination::File("/var/log/zeph-audit.log".into()),
1649 ),
1650 ];
1651 for (toml_str, expected) in cases {
1652 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1653 assert_eq!(config.audit.destination, expected);
1654 }
1655 }
1656
1657 #[test]
1658 fn policy_provider_serde_roundtrip() {
1659 let toml_str = r#"
1660 [policy]
1661 enabled = true
1662 policy_provider = "my-llm"
1663 "#;
1664 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1665 assert_eq!(config.policy.policy_provider.as_str(), "my-llm");
1666
1667 let json = serde_json::to_string(&config.policy).unwrap();
1668 let back: PolicyConfig = serde_json::from_str(&json).unwrap();
1669 assert_eq!(back.policy_provider.as_str(), "my-llm");
1670 }
1671
1672 #[test]
1673 fn policy_provider_default_is_empty() {
1674 let config = PolicyConfig::default();
1675 assert!(config.policy_provider.is_empty());
1676 }
1677
1678 #[test]
1679 fn utility_window_default_is_zero() {
1680 let config = UtilityScoringConfig::default();
1681 assert_eq!(config.utility_window, 0);
1682 }
1683
1684 #[test]
1685 fn utility_window_serde_roundtrip() {
1686 #[derive(serde::Deserialize)]
1687 struct Wrapper {
1688 utility_scoring: UtilityScoringConfig,
1689 }
1690
1691 let toml_str = "[utility_scoring]\nenabled = true\nutility_window = 3\n";
1692 let w: Wrapper = toml::from_str(toml_str).unwrap();
1693 assert_eq!(w.utility_scoring.utility_window, 3);
1694
1695 let json = serde_json::to_string(&w.utility_scoring).unwrap();
1696 let back: UtilityScoringConfig = serde_json::from_str(&json).unwrap();
1697 assert_eq!(back.utility_window, 3);
1698 }
1699
1700 #[test]
1701 fn high_gain_tools_default_is_empty() {
1702 let config = UtilityScoringConfig::default();
1703 assert!(config.high_gain_tools.is_empty());
1704 }
1705
1706 #[test]
1707 fn high_gain_tools_serde_roundtrip() {
1708 #[derive(serde::Deserialize)]
1709 struct Wrapper {
1710 utility_scoring: UtilityScoringConfig,
1711 }
1712
1713 let toml_str =
1714 "[utility_scoring]\nenabled = true\nhigh_gain_tools = [\"github_create_issue\"]\n";
1715 let w: Wrapper = toml::from_str(toml_str).unwrap();
1716 assert_eq!(
1717 w.utility_scoring.high_gain_tools,
1718 vec!["github_create_issue"]
1719 );
1720
1721 let json = serde_json::to_string(&w.utility_scoring).unwrap();
1722 let back: UtilityScoringConfig = serde_json::from_str(&json).unwrap();
1723 assert_eq!(back.high_gain_tools, vec!["github_create_issue"]);
1724 }
1725
1726 #[test]
1727 fn adversarial_policy_provider_default_is_empty() {
1728 let config = AdversarialPolicyConfig::default();
1729 assert!(config.policy_provider.is_empty());
1730 }
1731
1732 #[test]
1733 fn adversarial_policy_provider_serde_roundtrip() {
1734 #[derive(serde::Deserialize)]
1735 struct Wrapper {
1736 adversarial_policy: AdversarialPolicyConfig,
1737 }
1738 let toml_str = r#"
1739 [adversarial_policy]
1740 enabled = true
1741 policy_provider = "fast-llm"
1742 "#;
1743 let w: Wrapper = toml::from_str(toml_str).unwrap();
1744 assert_eq!(w.adversarial_policy.policy_provider.as_str(), "fast-llm");
1745
1746 let json = serde_json::to_string(&w.adversarial_policy).unwrap();
1747 let back: AdversarialPolicyConfig = serde_json::from_str(&json).unwrap();
1748 assert_eq!(back.policy_provider.as_str(), "fast-llm");
1749 }
1750
1751 #[test]
1752 fn adversarial_policy_provider_empty_when_omitted() {
1753 #[derive(serde::Deserialize)]
1754 struct Wrapper {
1755 adversarial_policy: AdversarialPolicyConfig,
1756 }
1757 let toml_str = r"
1758 [adversarial_policy]
1759 enabled = true
1760 ";
1761 let w: Wrapper = toml::from_str(toml_str).unwrap();
1762 assert!(
1763 w.adversarial_policy.policy_provider.is_empty(),
1764 "omitted policy_provider must default to empty (→ primary provider used)"
1765 );
1766 }
1767
1768 #[test]
1769 fn adversarial_policy_timeout_ms_defaults_to_none() {
1770 let config = AdversarialPolicyConfig::default();
1772 assert_eq!(config.timeout_ms, None);
1773 }
1774
1775 #[test]
1776 fn adversarial_policy_timeout_ms_explicit_override_roundtrips() {
1777 #[derive(serde::Deserialize)]
1778 struct Wrapper {
1779 adversarial_policy: AdversarialPolicyConfig,
1780 }
1781 let toml_str = r"
1782 [adversarial_policy]
1783 enabled = true
1784 timeout_ms = 90000
1785 ";
1786 let w: Wrapper = toml::from_str(toml_str).unwrap();
1787 assert_eq!(w.adversarial_policy.timeout_ms, Some(90_000));
1788
1789 let json = serde_json::to_string(&w.adversarial_policy).unwrap();
1790 let back: AdversarialPolicyConfig = serde_json::from_str(&json).unwrap();
1791 assert_eq!(back.timeout_ms, Some(90_000));
1792 }
1793
1794 #[test]
1795 fn adversarial_timeout_for_provider_kind_scales_local_vs_cloud() {
1796 assert_eq!(adversarial_timeout_for_provider_kind("ollama"), 45_000);
1797 assert_eq!(adversarial_timeout_for_provider_kind("candle"), 45_000);
1798 assert_eq!(adversarial_timeout_for_provider_kind("local"), 45_000);
1799 assert_eq!(adversarial_timeout_for_provider_kind("cloud"), 3_000);
1800 assert_eq!(adversarial_timeout_for_provider_kind("unknown"), 3_000);
1801 }
1802}