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)]
1086 pub risk_chain_window_turns: Option<u64>,
1087 #[serde(default)]
1093 pub checkpoints_enabled: bool,
1094 #[serde(default = "default_max_checkpoints")]
1099 pub max_checkpoints: usize,
1100}
1101
1102impl Default for ShellConfig {
1103 fn default() -> Self {
1104 Self {
1105 timeout: default_timeout(),
1106 blocked_commands: Vec::new(),
1107 allowed_commands: Vec::new(),
1108 allowed_paths: Vec::new(),
1109 allow_network: true,
1110 confirm_patterns: default_confirm_patterns(),
1111 env_blocklist: Self::default_env_blocklist(),
1112 transactional: false,
1113 transaction_scope: Vec::new(),
1114 auto_rollback: false,
1115 auto_rollback_exit_codes: Vec::new(),
1116 snapshot_required: false,
1117 max_snapshot_bytes: 0,
1118 max_background_runs: default_max_background_runs(),
1119 background_timeout_secs: default_background_timeout_secs(),
1120 risk_chain_threshold: None,
1121 risk_chain_window_turns: None,
1122 checkpoints_enabled: false,
1123 max_checkpoints: default_max_checkpoints(),
1124 }
1125 }
1126}
1127
1128impl ShellConfig {
1129 #[must_use]
1131 pub fn default_env_blocklist() -> Vec<String> {
1132 vec![
1133 "ZEPH_".into(),
1134 "AWS_".into(),
1135 "AZURE_".into(),
1136 "GCP_".into(),
1137 "GOOGLE_".into(),
1138 "OPENAI_".into(),
1139 "ANTHROPIC_".into(),
1140 "HF_".into(),
1141 "HUGGING".into(),
1142 ]
1143 }
1144}
1145
1146fn default_scrape_timeout() -> u64 {
1147 15
1148}
1149
1150fn default_max_body_bytes() -> usize {
1151 4_194_304
1152}
1153
1154fn default_ipi_filter_threshold() -> f32 {
1155 0.6
1156}
1157
1158#[derive(Debug, Deserialize, Serialize)]
1160pub struct ScrapeConfig {
1161 #[serde(default = "default_scrape_timeout")]
1163 pub timeout: u64,
1164 #[serde(default = "default_max_body_bytes")]
1166 pub max_body_bytes: usize,
1167 #[serde(default)]
1169 pub allowed_domains: Vec<String>,
1170 #[serde(default)]
1172 pub denied_domains: Vec<String>,
1173 #[serde(default = "default_ipi_filter_threshold")]
1176 pub ipi_filter_threshold: f32,
1177}
1178
1179impl Default for ScrapeConfig {
1180 fn default() -> Self {
1181 Self {
1182 timeout: default_scrape_timeout(),
1183 max_body_bytes: default_max_body_bytes(),
1184 allowed_domains: Vec::new(),
1185 denied_domains: Vec::new(),
1186 ipi_filter_threshold: default_ipi_filter_threshold(),
1187 }
1188 }
1189}
1190
1191fn default_search_backend() -> String {
1192 "brave".to_owned()
1193}
1194
1195fn default_search_vault_key() -> String {
1196 "ZEPH_WEB_SEARCH_API_KEY".to_owned()
1197}
1198
1199fn default_search_endpoint() -> String {
1200 "https://api.search.brave.com/res/v1/web/search".to_owned()
1201}
1202
1203fn default_search_max_results() -> usize {
1204 10
1205}
1206
1207fn default_search_timeout() -> u64 {
1208 15
1209}
1210
1211#[derive(Debug, Clone, Deserialize, Serialize)]
1220pub struct SearchConfig {
1221 #[serde(default)]
1223 pub enabled: bool,
1224 #[serde(default = "default_search_backend")]
1226 pub backend: String,
1227 #[serde(default = "default_search_vault_key")]
1230 pub api_key_vault_key: String,
1231 #[serde(default = "default_search_endpoint")]
1234 pub endpoint: String,
1235 #[serde(default = "default_search_max_results")]
1237 pub max_results: usize,
1238 #[serde(default = "default_search_timeout")]
1240 pub timeout: u64,
1241}
1242
1243impl Default for SearchConfig {
1244 fn default() -> Self {
1245 Self {
1246 enabled: false,
1247 backend: default_search_backend(),
1248 api_key_vault_key: default_search_vault_key(),
1249 endpoint: default_search_endpoint(),
1250 max_results: default_search_max_results(),
1251 timeout: default_search_timeout(),
1252 }
1253 }
1254}
1255
1256#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
1258#[serde(rename_all = "kebab-case")]
1259#[non_exhaustive]
1260pub enum SpeculationMode {
1261 #[default]
1263 Off,
1264 Decoding,
1266 Pattern,
1268 Both,
1270}
1271
1272#[derive(Debug, Clone, Deserialize, Serialize)]
1274pub struct SpeculativePatternConfig {
1275 #[serde(default)]
1277 pub enabled: bool,
1278 #[serde(default = "default_min_observations")]
1280 pub min_observations: u32,
1281 #[serde(default = "default_half_life_days")]
1283 pub half_life_days: f64,
1284 #[serde(default)]
1286 pub rerank_provider: ProviderName,
1287}
1288
1289fn default_min_observations() -> u32 {
1290 5
1291}
1292
1293fn default_half_life_days() -> f64 {
1294 14.0
1295}
1296
1297impl Default for SpeculativePatternConfig {
1298 fn default() -> Self {
1299 Self {
1300 enabled: false,
1301 min_observations: default_min_observations(),
1302 half_life_days: default_half_life_days(),
1303 rerank_provider: ProviderName::default(),
1304 }
1305 }
1306}
1307
1308#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1310pub struct SpeculativeAllowlistConfig {
1311 #[serde(default)]
1313 pub shell: Vec<String>,
1314}
1315
1316fn default_max_in_flight() -> usize {
1317 4
1318}
1319
1320fn default_confidence_threshold() -> f32 {
1321 0.55
1322}
1323
1324fn default_max_wasted_per_minute() -> u64 {
1325 100
1326}
1327
1328fn default_ttl_seconds() -> u64 {
1329 30
1330}
1331
1332#[derive(Debug, Clone, Deserialize, Serialize)]
1334pub struct SpeculativeConfig {
1335 #[serde(default)]
1337 pub mode: SpeculationMode,
1338 #[serde(default = "default_max_in_flight")]
1340 pub max_in_flight: usize,
1341 #[serde(default = "default_confidence_threshold")]
1343 pub confidence_threshold: f32,
1344 #[serde(default = "default_max_wasted_per_minute")]
1346 pub max_wasted_per_minute: u64,
1347 #[serde(default = "default_ttl_seconds")]
1349 pub ttl_seconds: u64,
1350 #[serde(default = "default_true")]
1352 pub audit: bool,
1353 #[serde(default)]
1355 pub pattern: SpeculativePatternConfig,
1356 #[serde(default)]
1358 pub allowlist: SpeculativeAllowlistConfig,
1359}
1360
1361impl Default for SpeculativeConfig {
1362 fn default() -> Self {
1363 Self {
1364 mode: SpeculationMode::Off,
1365 max_in_flight: default_max_in_flight(),
1366 confidence_threshold: default_confidence_threshold(),
1367 max_wasted_per_minute: default_max_wasted_per_minute(),
1368 ttl_seconds: default_ttl_seconds(),
1369 audit: true,
1370 pattern: SpeculativePatternConfig::default(),
1371 allowlist: SpeculativeAllowlistConfig::default(),
1372 }
1373 }
1374}
1375
1376#[derive(Debug, Clone, Deserialize, Serialize)]
1378#[serde(default)]
1379#[allow(clippy::struct_excessive_bools)]
1380pub struct EgressConfig {
1381 pub enabled: bool,
1383 pub log_blocked: bool,
1385 pub log_response_bytes: bool,
1387 pub log_hosts_to_tui: bool,
1389}
1390
1391impl Default for EgressConfig {
1392 fn default() -> Self {
1393 Self {
1394 enabled: true,
1395 log_blocked: true,
1396 log_response_bytes: true,
1397 log_hosts_to_tui: true,
1398 }
1399 }
1400}
1401
1402fn default_compression_min_lines() -> usize {
1405 10
1406}
1407
1408fn default_compression_max_rules() -> u32 {
1409 200
1410}
1411
1412fn default_regex_compile_timeout_ms() -> u64 {
1413 500
1414}
1415
1416fn default_evolution_min_interval_secs() -> u64 {
1417 3600
1418}
1419
1420#[derive(Debug, Clone, Deserialize, Serialize)]
1435#[serde(default)]
1436pub struct ToolCompressionConfig {
1437 pub enabled: bool,
1439 #[serde(default = "default_compression_min_lines")]
1441 pub min_lines_to_compress: usize,
1442 #[serde(default)]
1444 pub evolution_provider: ProviderName,
1445 #[serde(default = "default_evolution_min_interval_secs")]
1447 pub evolution_min_interval_secs: u64,
1448 #[serde(default = "default_compression_max_rules")]
1450 pub max_rules: u32,
1451 #[serde(default = "default_regex_compile_timeout_ms")]
1453 pub regex_compile_timeout_ms: u64,
1454}
1455
1456impl Default for ToolCompressionConfig {
1457 fn default() -> Self {
1458 Self {
1459 enabled: false,
1460 min_lines_to_compress: default_compression_min_lines(),
1461 evolution_provider: ProviderName::default(),
1462 evolution_min_interval_secs: default_evolution_min_interval_secs(),
1463 max_rules: default_compression_max_rules(),
1464 regex_compile_timeout_ms: default_regex_compile_timeout_ms(),
1465 }
1466 }
1467}
1468
1469#[derive(Debug, Deserialize, Serialize)]
1477pub struct ToolsConfig {
1478 #[serde(default = "default_true")]
1481 pub enabled: bool,
1482 #[serde(default = "default_true")]
1484 pub summarize_output: bool,
1485 #[serde(default)]
1487 pub shell: ShellConfig,
1488 #[serde(default)]
1490 pub scrape: ScrapeConfig,
1491 #[serde(default)]
1493 pub search: SearchConfig,
1494 #[serde(default)]
1496 pub audit: AuditConfig,
1497 #[serde(default)]
1499 pub permissions: Option<PermissionsConfig>,
1500 #[serde(default)]
1502 pub filters: FilterConfig,
1503 #[serde(default)]
1505 pub overflow: OverflowConfig,
1506 #[serde(default)]
1508 pub anomaly: AnomalyConfig,
1509 #[serde(default)]
1511 pub result_cache: ResultCacheConfig,
1512 #[serde(default)]
1514 pub tafc: TafcConfig,
1515 #[serde(default)]
1517 pub dependencies: DependencyConfig,
1518 #[serde(default)]
1520 pub retry: RetryConfig,
1521 #[serde(default)]
1523 pub policy: PolicyConfig,
1524 #[serde(default)]
1526 pub adversarial_policy: AdversarialPolicyConfig,
1527 #[serde(default)]
1529 pub utility: UtilityScoringConfig,
1530 #[serde(default)]
1532 pub file: FileConfig,
1533 #[serde(default)]
1535 pub authorization: AuthorizationConfig,
1536 #[serde(default)]
1538 pub max_tool_calls_per_session: Option<u32>,
1539 #[serde(default)]
1541 pub speculative: SpeculativeConfig,
1542 #[serde(default)]
1544 pub sandbox: SandboxConfig,
1545 #[serde(default)]
1547 pub egress: EgressConfig,
1548 #[serde(default)]
1550 pub compression: ToolCompressionConfig,
1551}
1552
1553impl Default for ToolsConfig {
1554 fn default() -> Self {
1555 Self {
1556 enabled: true,
1557 summarize_output: true,
1558 shell: ShellConfig::default(),
1559 scrape: ScrapeConfig::default(),
1560 search: SearchConfig::default(),
1561 audit: AuditConfig::default(),
1562 permissions: None,
1563 filters: FilterConfig::default(),
1564 overflow: OverflowConfig::default(),
1565 anomaly: AnomalyConfig::default(),
1566 result_cache: ResultCacheConfig::default(),
1567 tafc: TafcConfig::default(),
1568 dependencies: DependencyConfig::default(),
1569 retry: RetryConfig::default(),
1570 policy: PolicyConfig::default(),
1571 adversarial_policy: AdversarialPolicyConfig::default(),
1572 utility: UtilityScoringConfig::default(),
1573 file: FileConfig::default(),
1574 authorization: AuthorizationConfig::default(),
1575 max_tool_calls_per_session: None,
1576 speculative: SpeculativeConfig::default(),
1577 sandbox: SandboxConfig::default(),
1578 egress: EgressConfig::default(),
1579 compression: ToolCompressionConfig::default(),
1580 }
1581 }
1582}
1583
1584#[cfg(test)]
1585mod tests {
1586 use super::*;
1587
1588 #[test]
1589 fn deserialize_default_config() {
1590 let toml_str = r#"
1591 enabled = true
1592
1593 [shell]
1594 timeout = 60
1595 blocked_commands = ["rm -rf /", "sudo"]
1596 "#;
1597
1598 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1599 assert!(config.enabled);
1600 assert_eq!(config.shell.timeout, 60);
1601 assert_eq!(config.shell.blocked_commands.len(), 2);
1602 }
1603
1604 #[test]
1605 fn empty_blocked_commands() {
1606 let config: ToolsConfig = toml::from_str(r"[shell]\ntimeout = 30\n").unwrap_or_default();
1607 assert!(config.enabled);
1608 }
1609
1610 #[test]
1611 fn default_tools_config() {
1612 let config = ToolsConfig::default();
1613 assert!(config.enabled);
1614 assert!(config.summarize_output);
1615 assert_eq!(config.shell.timeout, 30);
1616 assert!(config.shell.blocked_commands.is_empty());
1617 assert!(config.audit.enabled);
1618 }
1619
1620 #[test]
1621 fn audit_destination_serde_roundtrip() {
1622 let cases = [
1623 ("\"stdout\"", AuditDestination::Stdout),
1624 ("\"stderr\"", AuditDestination::Stderr),
1625 (
1626 "\"/var/log/audit.log\"",
1627 AuditDestination::File("/var/log/audit.log".into()),
1628 ),
1629 ];
1630 for (json_str, expected) in cases {
1631 let got: AuditDestination = serde_json::from_str(json_str).unwrap();
1632 assert_eq!(got, expected);
1633 let serialized = serde_json::to_string(&got).unwrap();
1634 let roundtrip: AuditDestination = serde_json::from_str(&serialized).unwrap();
1635 assert_eq!(roundtrip, expected);
1636 }
1637 }
1638
1639 #[test]
1640 fn audit_destination_toml_in_config() {
1641 let cases = [
1642 (
1643 r#"[audit]
1644destination = "stdout""#,
1645 AuditDestination::Stdout,
1646 ),
1647 (
1648 r#"[audit]
1649destination = "stderr""#,
1650 AuditDestination::Stderr,
1651 ),
1652 (
1653 r#"[audit]
1654destination = "/var/log/zeph-audit.log""#,
1655 AuditDestination::File("/var/log/zeph-audit.log".into()),
1656 ),
1657 ];
1658 for (toml_str, expected) in cases {
1659 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1660 assert_eq!(config.audit.destination, expected);
1661 }
1662 }
1663
1664 #[test]
1665 fn policy_provider_serde_roundtrip() {
1666 let toml_str = r#"
1667 [policy]
1668 enabled = true
1669 policy_provider = "my-llm"
1670 "#;
1671 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1672 assert_eq!(config.policy.policy_provider.as_str(), "my-llm");
1673
1674 let json = serde_json::to_string(&config.policy).unwrap();
1675 let back: PolicyConfig = serde_json::from_str(&json).unwrap();
1676 assert_eq!(back.policy_provider.as_str(), "my-llm");
1677 }
1678
1679 #[test]
1680 fn policy_provider_default_is_empty() {
1681 let config = PolicyConfig::default();
1682 assert!(config.policy_provider.is_empty());
1683 }
1684
1685 #[test]
1686 fn utility_window_default_is_zero() {
1687 let config = UtilityScoringConfig::default();
1688 assert_eq!(config.utility_window, 0);
1689 }
1690
1691 #[test]
1692 fn utility_window_serde_roundtrip() {
1693 #[derive(serde::Deserialize)]
1694 struct Wrapper {
1695 utility_scoring: UtilityScoringConfig,
1696 }
1697
1698 let toml_str = "[utility_scoring]\nenabled = true\nutility_window = 3\n";
1699 let w: Wrapper = toml::from_str(toml_str).unwrap();
1700 assert_eq!(w.utility_scoring.utility_window, 3);
1701
1702 let json = serde_json::to_string(&w.utility_scoring).unwrap();
1703 let back: UtilityScoringConfig = serde_json::from_str(&json).unwrap();
1704 assert_eq!(back.utility_window, 3);
1705 }
1706
1707 #[test]
1708 fn high_gain_tools_default_is_empty() {
1709 let config = UtilityScoringConfig::default();
1710 assert!(config.high_gain_tools.is_empty());
1711 }
1712
1713 #[test]
1714 fn high_gain_tools_serde_roundtrip() {
1715 #[derive(serde::Deserialize)]
1716 struct Wrapper {
1717 utility_scoring: UtilityScoringConfig,
1718 }
1719
1720 let toml_str =
1721 "[utility_scoring]\nenabled = true\nhigh_gain_tools = [\"github_create_issue\"]\n";
1722 let w: Wrapper = toml::from_str(toml_str).unwrap();
1723 assert_eq!(
1724 w.utility_scoring.high_gain_tools,
1725 vec!["github_create_issue"]
1726 );
1727
1728 let json = serde_json::to_string(&w.utility_scoring).unwrap();
1729 let back: UtilityScoringConfig = serde_json::from_str(&json).unwrap();
1730 assert_eq!(back.high_gain_tools, vec!["github_create_issue"]);
1731 }
1732
1733 #[test]
1734 fn adversarial_policy_provider_default_is_empty() {
1735 let config = AdversarialPolicyConfig::default();
1736 assert!(config.policy_provider.is_empty());
1737 }
1738
1739 #[test]
1740 fn adversarial_policy_provider_serde_roundtrip() {
1741 #[derive(serde::Deserialize)]
1742 struct Wrapper {
1743 adversarial_policy: AdversarialPolicyConfig,
1744 }
1745 let toml_str = r#"
1746 [adversarial_policy]
1747 enabled = true
1748 policy_provider = "fast-llm"
1749 "#;
1750 let w: Wrapper = toml::from_str(toml_str).unwrap();
1751 assert_eq!(w.adversarial_policy.policy_provider.as_str(), "fast-llm");
1752
1753 let json = serde_json::to_string(&w.adversarial_policy).unwrap();
1754 let back: AdversarialPolicyConfig = serde_json::from_str(&json).unwrap();
1755 assert_eq!(back.policy_provider.as_str(), "fast-llm");
1756 }
1757
1758 #[test]
1759 fn adversarial_policy_provider_empty_when_omitted() {
1760 #[derive(serde::Deserialize)]
1761 struct Wrapper {
1762 adversarial_policy: AdversarialPolicyConfig,
1763 }
1764 let toml_str = r"
1765 [adversarial_policy]
1766 enabled = true
1767 ";
1768 let w: Wrapper = toml::from_str(toml_str).unwrap();
1769 assert!(
1770 w.adversarial_policy.policy_provider.is_empty(),
1771 "omitted policy_provider must default to empty (→ primary provider used)"
1772 );
1773 }
1774
1775 #[test]
1776 fn adversarial_policy_timeout_ms_defaults_to_none() {
1777 let config = AdversarialPolicyConfig::default();
1779 assert_eq!(config.timeout_ms, None);
1780 }
1781
1782 #[test]
1783 fn adversarial_policy_timeout_ms_explicit_override_roundtrips() {
1784 #[derive(serde::Deserialize)]
1785 struct Wrapper {
1786 adversarial_policy: AdversarialPolicyConfig,
1787 }
1788 let toml_str = r"
1789 [adversarial_policy]
1790 enabled = true
1791 timeout_ms = 90000
1792 ";
1793 let w: Wrapper = toml::from_str(toml_str).unwrap();
1794 assert_eq!(w.adversarial_policy.timeout_ms, Some(90_000));
1795
1796 let json = serde_json::to_string(&w.adversarial_policy).unwrap();
1797 let back: AdversarialPolicyConfig = serde_json::from_str(&json).unwrap();
1798 assert_eq!(back.timeout_ms, Some(90_000));
1799 }
1800
1801 #[test]
1802 fn adversarial_timeout_for_provider_kind_scales_local_vs_cloud() {
1803 assert_eq!(adversarial_timeout_for_provider_kind("ollama"), 45_000);
1804 assert_eq!(adversarial_timeout_for_provider_kind("candle"), 45_000);
1805 assert_eq!(adversarial_timeout_for_provider_kind("local"), 45_000);
1806 assert_eq!(adversarial_timeout_for_provider_kind("cloud"), 3_000);
1807 assert_eq!(adversarial_timeout_for_provider_kind("unknown"), 3_000);
1808 }
1809}