1use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7use zeph_common::SkillTrustLevel;
8
9use crate::providers::ProviderName;
10use crate::tools::{AutonomyLevel, PreExecutionVerifierConfig};
11
12use crate::defaults::default_true;
13use crate::vigil::VigilConfig;
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
19pub struct ScannerConfig {
20 #[serde(default = "default_true")]
26 pub injection_patterns: bool,
27 #[serde(default)]
32 pub capability_escalation_check: bool,
33}
34
35impl Default for ScannerConfig {
36 fn default() -> Self {
37 Self {
38 injection_patterns: true,
39 capability_escalation_check: false,
40 }
41 }
42}
43use crate::rate_limit::RateLimitConfig;
44use crate::sanitizer::GuardrailConfig;
45use crate::sanitizer::{
46 CausalIpiConfig, ContentIsolationConfig, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
47 PiiFilterConfig, ResponseVerificationConfig,
48};
49
50fn default_trust_default_level() -> SkillTrustLevel {
51 SkillTrustLevel::Quarantined
52}
53
54fn default_trust_local_level() -> SkillTrustLevel {
55 SkillTrustLevel::Trusted
56}
57
58fn default_trust_hash_mismatch_level() -> SkillTrustLevel {
59 SkillTrustLevel::Quarantined
60}
61
62fn default_trust_bundled_level() -> SkillTrustLevel {
63 SkillTrustLevel::Trusted
64}
65
66fn default_llm_timeout() -> u64 {
67 120
68}
69
70fn default_embedding_timeout() -> u64 {
71 30
72}
73
74fn default_a2a_timeout() -> u64 {
75 30
76}
77
78fn default_max_parallel_tools() -> usize {
79 8
80}
81
82fn default_llm_request_timeout() -> u64 {
83 600
84}
85
86fn default_context_prep_timeout() -> u64 {
87 30
88}
89
90fn default_no_providers_backoff_secs() -> u64 {
91 2
92}
93
94#[derive(Debug, Clone, Deserialize, Serialize)]
108pub struct TrustConfig {
109 #[serde(default = "default_trust_default_level")]
111 pub default_level: SkillTrustLevel,
112 #[serde(default = "default_trust_local_level")]
114 pub local_level: SkillTrustLevel,
115 #[serde(default = "default_trust_hash_mismatch_level")]
118 pub hash_mismatch_level: SkillTrustLevel,
119 #[serde(default = "default_trust_bundled_level")]
121 pub bundled_level: SkillTrustLevel,
122 #[serde(default = "default_true")]
130 pub scan_on_load: bool,
131 #[serde(default = "default_true")]
140 pub require_integrity_check_on_promote: bool,
141 #[serde(default)]
143 pub scanner: ScannerConfig,
144}
145
146impl Default for TrustConfig {
147 fn default() -> Self {
148 Self {
149 default_level: default_trust_default_level(),
150 local_level: default_trust_local_level(),
151 hash_mismatch_level: default_trust_hash_mismatch_level(),
152 bundled_level: default_trust_bundled_level(),
153 scan_on_load: true,
154 require_integrity_check_on_promote: true,
155 scanner: ScannerConfig::default(),
156 }
157 }
158}
159
160fn default_decay_per_turn() -> f32 {
163 0.85
164}
165fn default_window_turns() -> u32 {
166 8
167}
168fn default_elevated_at() -> f32 {
169 2.0
170}
171fn default_high_at() -> f32 {
172 4.0
173}
174fn default_critical_at() -> f32 {
175 8.0
176}
177fn default_alert_threshold() -> f32 {
178 4.0
179}
180fn default_auto_recover_after_turns() -> u32 {
181 16
182}
183fn default_subagent_inheritance_factor() -> f32 {
184 0.5
185}
186fn default_high_call_rate_threshold() -> u32 {
187 12
188}
189fn default_unusual_read_threshold() -> u32 {
190 24
191}
192fn default_auto_recover_floor() -> u32 {
193 4
194}
195
196#[derive(Debug, Clone, Deserialize, Serialize)]
213pub struct TrajectorySentinelConfig {
214 #[serde(default = "default_decay_per_turn")]
218 pub decay_per_turn: f32,
219 #[serde(default = "default_window_turns")]
223 pub window_turns: u32,
224 #[serde(default = "default_elevated_at")]
226 pub elevated_at: f32,
227 #[serde(default = "default_high_at")]
229 pub high_at: f32,
230 #[serde(default = "default_critical_at")]
232 pub critical_at: f32,
233 #[serde(default = "default_alert_threshold")]
237 pub alert_threshold: f32,
238 #[serde(default = "default_auto_recover_after_turns")]
240 pub auto_recover_after_turns: u32,
241 #[serde(default = "default_subagent_inheritance_factor")]
246 pub subagent_inheritance_factor: f32,
247 #[serde(default = "default_high_call_rate_threshold")]
249 pub high_call_rate_threshold: u32,
250 #[serde(default = "default_unusual_read_threshold")]
252 pub unusual_read_threshold: u32,
253}
254
255impl Default for TrajectorySentinelConfig {
256 fn default() -> Self {
257 Self {
258 decay_per_turn: default_decay_per_turn(),
259 window_turns: default_window_turns(),
260 elevated_at: default_elevated_at(),
261 high_at: default_high_at(),
262 critical_at: default_critical_at(),
263 alert_threshold: default_alert_threshold(),
264 auto_recover_after_turns: default_auto_recover_after_turns(),
265 subagent_inheritance_factor: default_subagent_inheritance_factor(),
266 high_call_rate_threshold: default_high_call_rate_threshold(),
267 unusual_read_threshold: default_unusual_read_threshold(),
268 }
269 }
270}
271
272impl TrajectorySentinelConfig {
273 #[must_use = "validation result must be checked"]
279 pub fn validate(&self) -> Result<(), String> {
280 if self.decay_per_turn <= 0.0 || self.decay_per_turn > 1.0 {
281 return Err(format!(
282 "trajectory.decay_per_turn must be in (0.0, 1.0]; got {}",
283 self.decay_per_turn
284 ));
285 }
286 if self.elevated_at >= self.high_at {
287 return Err(format!(
288 "trajectory: elevated_at ({}) must be < high_at ({})",
289 self.elevated_at, self.high_at
290 ));
291 }
292 if self.high_at >= self.critical_at {
293 return Err(format!(
294 "trajectory: high_at ({}) must be < critical_at ({})",
295 self.high_at, self.critical_at
296 ));
297 }
298 if self.auto_recover_after_turns < default_auto_recover_floor() {
299 return Err(format!(
300 "trajectory.auto_recover_after_turns must be >= {}; got {}",
301 default_auto_recover_floor(),
302 self.auto_recover_after_turns
303 ));
304 }
305 if self.decay_per_turn < 1.0 {
307 let ideal = self
308 .decay_per_turn
309 .powf(0.5_f32.ln() / self.decay_per_turn.ln());
310 if (self.subagent_inheritance_factor - ideal).abs() > 0.1 {
311 tracing::warn!(
313 configured = self.subagent_inheritance_factor,
314 ideal = ideal,
315 decay = self.decay_per_turn,
316 "trajectory.subagent_inheritance_factor deviates from calibrated value by more than 0.1"
317 );
318 }
319 }
320 Ok(())
321 }
322}
323
324fn default_shadow_max_context_events() -> usize {
327 50
328}
329fn default_shadow_probe_timeout_ms() -> u64 {
330 2000
331}
332fn default_shadow_max_probes_per_turn() -> usize {
333 3
334}
335fn default_shadow_probe_patterns() -> Vec<String> {
336 vec![
337 "builtin:shell".to_owned(),
338 "builtin:write".to_owned(),
339 "builtin:edit".to_owned(),
340 "*write*".to_owned(),
346 "*edit*".to_owned(),
347 "*delete*".to_owned(),
348 "*exec*".to_owned(),
349 ]
350}
351
352#[derive(Debug, Clone, Deserialize, Serialize)]
368pub struct ShadowSentinelConfig {
369 #[serde(default)]
371 pub enabled: bool,
372 #[serde(default)]
377 pub probe_provider: ProviderName,
378 #[serde(default = "default_shadow_max_context_events")]
380 pub max_context_events: usize,
381 #[serde(default = "default_shadow_probe_timeout_ms")]
383 pub probe_timeout_ms: u64,
384 #[serde(default = "default_shadow_max_probes_per_turn")]
386 pub max_probes_per_turn: usize,
387 #[serde(default = "default_shadow_probe_patterns")]
393 pub probe_patterns: Vec<String>,
394 #[serde(default)]
402 pub deny_on_timeout: bool,
403}
404
405impl Default for ShadowSentinelConfig {
406 fn default() -> Self {
407 Self {
408 enabled: false,
409 probe_provider: ProviderName::default(),
410 max_context_events: default_shadow_max_context_events(),
411 probe_timeout_ms: default_shadow_probe_timeout_ms(),
412 max_probes_per_turn: default_shadow_max_probes_per_turn(),
413 probe_patterns: default_shadow_probe_patterns(),
414 deny_on_timeout: false,
415 }
416 }
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
425#[serde(rename_all = "snake_case")]
426#[non_exhaustive]
427pub enum PatternStrictness {
428 Strict,
430 Permissive,
432 #[default]
436 ProvisionalForDynamicNamespaces,
437}
438
439#[derive(Debug, Clone, Deserialize, Serialize)]
449pub struct ScopeConfig {
450 #[serde(default)]
454 pub patterns: Vec<String>,
455}
456
457#[derive(Debug, Clone, Deserialize, Serialize, Default)]
476pub struct CapabilityScopesConfig {
477 #[serde(default = "default_scope_name")]
482 pub default_scope: String,
483 #[serde(default)]
486 pub strict: bool,
487 #[serde(default)]
489 pub pattern_strictness: PatternStrictness,
490 #[serde(default, flatten)]
492 pub scopes: HashMap<String, ScopeConfig>,
493}
494
495fn default_scope_name() -> String {
496 "general".to_owned()
497}
498
499#[derive(Debug, Clone, Deserialize, Serialize)]
519pub struct SecurityConfig {
520 #[serde(default = "default_true")]
523 pub redact_secrets: bool,
524 #[serde(default)]
526 pub autonomy_level: AutonomyLevel,
527 #[serde(default)]
528 pub content_isolation: ContentIsolationConfig,
529 #[serde(default)]
530 pub exfiltration_guard: ExfiltrationGuardConfig,
531 #[serde(default)]
533 pub memory_validation: MemoryWriteValidationConfig,
534 #[serde(default)]
536 pub pii_filter: PiiFilterConfig,
537 #[serde(default)]
539 pub rate_limit: RateLimitConfig,
540 #[serde(default)]
542 pub pre_execution_verify: PreExecutionVerifierConfig,
543 #[serde(default)]
545 pub guardrail: GuardrailConfig,
546 #[serde(default)]
548 pub response_verification: ResponseVerificationConfig,
549 #[serde(default)]
551 pub causal_ipi: CausalIpiConfig,
552 #[serde(default)]
557 pub vigil: VigilConfig,
558 #[serde(default)]
563 pub trajectory: TrajectorySentinelConfig,
564 #[serde(default)]
569 pub capability_scopes: CapabilityScopesConfig,
570 #[serde(default)]
576 pub shadow_sentinel: ShadowSentinelConfig,
577}
578
579impl Default for SecurityConfig {
580 fn default() -> Self {
581 Self {
582 redact_secrets: true,
583 autonomy_level: AutonomyLevel::default(),
584 content_isolation: ContentIsolationConfig::default(),
585 exfiltration_guard: ExfiltrationGuardConfig::default(),
586 memory_validation: MemoryWriteValidationConfig::default(),
587 pii_filter: PiiFilterConfig::default(),
588 rate_limit: RateLimitConfig::default(),
589 pre_execution_verify: PreExecutionVerifierConfig::default(),
590 guardrail: GuardrailConfig::default(),
591 response_verification: ResponseVerificationConfig::default(),
592 causal_ipi: CausalIpiConfig::default(),
593 vigil: VigilConfig::default(),
594 trajectory: TrajectorySentinelConfig::default(),
595 capability_scopes: CapabilityScopesConfig::default(),
596 shadow_sentinel: ShadowSentinelConfig::default(),
597 }
598 }
599}
600
601#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
615pub struct TimeoutConfig {
616 #[serde(default = "default_llm_timeout")]
618 pub llm_seconds: u64,
619 #[serde(default = "default_llm_request_timeout")]
622 pub llm_request_timeout_secs: u64,
623 #[serde(default = "default_embedding_timeout")]
625 pub embedding_seconds: u64,
626 #[serde(default = "default_a2a_timeout")]
628 pub a2a_seconds: u64,
629 #[serde(default = "default_max_parallel_tools")]
632 pub max_parallel_tools: usize,
633 #[serde(default = "default_context_prep_timeout")]
640 pub context_prep_timeout_secs: u64,
641 #[serde(default = "default_no_providers_backoff_secs")]
645 pub no_providers_backoff_secs: u64,
646}
647
648impl Default for TimeoutConfig {
649 fn default() -> Self {
650 Self {
651 llm_seconds: default_llm_timeout(),
652 llm_request_timeout_secs: default_llm_request_timeout(),
653 embedding_seconds: default_embedding_timeout(),
654 a2a_seconds: default_a2a_timeout(),
655 max_parallel_tools: default_max_parallel_tools(),
656 context_prep_timeout_secs: default_context_prep_timeout(),
657 no_providers_backoff_secs: default_no_providers_backoff_secs(),
658 }
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn trust_config_default_has_scan_on_load_true() {
668 let config = TrustConfig::default();
669 assert!(config.scan_on_load);
670 }
671
672 #[test]
673 fn trust_config_serde_roundtrip_with_scan_on_load() {
674 let config = TrustConfig {
675 default_level: SkillTrustLevel::Quarantined,
676 local_level: SkillTrustLevel::Trusted,
677 hash_mismatch_level: SkillTrustLevel::Quarantined,
678 bundled_level: SkillTrustLevel::Trusted,
679 scan_on_load: false,
680 require_integrity_check_on_promote: false,
681 scanner: ScannerConfig::default(),
682 };
683 let toml = toml::to_string(&config).expect("serialize");
684 let deserialized: TrustConfig = toml::from_str(&toml).expect("deserialize");
685 assert!(!deserialized.scan_on_load);
686 assert_eq!(deserialized.bundled_level, SkillTrustLevel::Trusted);
687 assert!(!deserialized.require_integrity_check_on_promote);
688 }
689
690 #[test]
691 fn trust_config_missing_scan_on_load_defaults_to_true() {
692 let toml = r#"
693default_level = "quarantined"
694local_level = "trusted"
695hash_mismatch_level = "quarantined"
696"#;
697 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
698 assert!(
699 config.scan_on_load,
700 "missing scan_on_load must default to true"
701 );
702 }
703
704 #[test]
705 fn trust_config_default_has_bundled_level_trusted() {
706 let config = TrustConfig::default();
707 assert_eq!(config.bundled_level, SkillTrustLevel::Trusted);
708 }
709
710 #[test]
711 fn trust_config_default_has_require_integrity_check_on_promote_true() {
712 let config = TrustConfig::default();
713 assert!(config.require_integrity_check_on_promote);
714 }
715
716 #[test]
717 fn trust_config_missing_require_integrity_check_on_promote_defaults_to_true() {
718 let toml = r#"
719default_level = "quarantined"
720local_level = "trusted"
721hash_mismatch_level = "quarantined"
722"#;
723 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
724 assert!(
725 config.require_integrity_check_on_promote,
726 "missing require_integrity_check_on_promote must default to true"
727 );
728 }
729
730 #[test]
731 fn trust_config_missing_bundled_level_defaults_to_trusted() {
732 let toml = r#"
733default_level = "quarantined"
734local_level = "trusted"
735hash_mismatch_level = "quarantined"
736"#;
737 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
738 assert_eq!(
739 config.bundled_level,
740 SkillTrustLevel::Trusted,
741 "missing bundled_level must default to trusted"
742 );
743 }
744
745 #[test]
746 fn scanner_config_defaults() {
747 let cfg = ScannerConfig::default();
748 assert!(cfg.injection_patterns);
749 assert!(!cfg.capability_escalation_check);
750 }
751
752 #[test]
753 fn scanner_config_serde_roundtrip() {
754 let cfg = ScannerConfig {
755 injection_patterns: false,
756 capability_escalation_check: true,
757 };
758 let toml = toml::to_string(&cfg).expect("serialize");
759 let back: ScannerConfig = toml::from_str(&toml).expect("deserialize");
760 assert!(!back.injection_patterns);
761 assert!(back.capability_escalation_check);
762 }
763
764 #[test]
765 fn trust_config_scanner_defaults_when_missing() {
766 let toml = r#"
767default_level = "quarantined"
768local_level = "trusted"
769hash_mismatch_level = "quarantined"
770"#;
771 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
772 assert!(config.scanner.injection_patterns);
773 assert!(!config.scanner.capability_escalation_check);
774 }
775
776 #[test]
781 fn timeout_config_context_prep_timeout_default() {
782 let cfg = TimeoutConfig::default();
783 assert_eq!(
784 cfg.context_prep_timeout_secs, 30,
785 "context_prep_timeout_secs default must be 30s (#3357)"
786 );
787 }
788
789 #[test]
790 fn timeout_config_no_providers_backoff_default() {
791 let cfg = TimeoutConfig::default();
792 assert_eq!(
793 cfg.no_providers_backoff_secs, 2,
794 "no_providers_backoff_secs default must be 2s (#3357)"
795 );
796 }
797
798 #[test]
799 fn timeout_config_new_fields_deserialize_from_toml() {
800 let toml = r"
801context_prep_timeout_secs = 60
802no_providers_backoff_secs = 10
803";
804 let cfg: TimeoutConfig = toml::from_str(toml).expect("deserialize");
805 assert_eq!(cfg.context_prep_timeout_secs, 60);
806 assert_eq!(cfg.no_providers_backoff_secs, 10);
807 }
808
809 #[test]
810 fn timeout_config_new_fields_default_when_missing_from_toml() {
811 let cfg: TimeoutConfig = toml::from_str("").expect("deserialize empty");
813 assert_eq!(cfg.context_prep_timeout_secs, 30);
814 assert_eq!(cfg.no_providers_backoff_secs, 2);
815 }
816
817 #[test]
822 fn security_config_absent_rate_limit_section_is_enabled() {
823 let cfg: SecurityConfig = toml::from_str("").expect("deserialize empty");
824 assert!(cfg.rate_limit.enabled);
825 }
826
827 #[test]
831 fn security_config_partial_rate_limit_section_is_enabled() {
832 let toml = r"
833[rate_limit]
834shell_calls_per_minute = 50
835";
836 let cfg: SecurityConfig = toml::from_str(toml).expect("deserialize");
837 assert!(
838 cfg.rate_limit.enabled,
839 "partial [security.rate_limit] section must default enabled = true"
840 );
841 assert_eq!(cfg.rate_limit.shell_calls_per_minute, 50);
842 }
843
844 #[test]
845 fn security_config_explicit_rate_limit_disabled_is_preserved() {
846 let toml = r"
847[rate_limit]
848enabled = false
849";
850 let cfg: SecurityConfig = toml::from_str(toml).expect("deserialize");
851 assert!(!cfg.rate_limit.enabled);
852 }
853}