1use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7use zeph_common::SkillTrustLevel;
8
9use crate::tools::{AutonomyLevel, PreExecutionVerifierConfig};
10
11use crate::defaults::default_true;
12use crate::vigil::VigilConfig;
13
14#[derive(Debug, Clone, Deserialize, Serialize)]
18pub struct ScannerConfig {
19 #[serde(default = "default_true")]
25 pub injection_patterns: bool,
26 #[serde(default)]
31 pub capability_escalation_check: bool,
32}
33
34impl Default for ScannerConfig {
35 fn default() -> Self {
36 Self {
37 injection_patterns: true,
38 capability_escalation_check: false,
39 }
40 }
41}
42use crate::rate_limit::RateLimitConfig;
43use crate::sanitizer::GuardrailConfig;
44use crate::sanitizer::{
45 CausalIpiConfig, ContentIsolationConfig, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
46 PiiFilterConfig, ResponseVerificationConfig,
47};
48
49fn default_trust_default_level() -> SkillTrustLevel {
50 SkillTrustLevel::Quarantined
51}
52
53fn default_trust_local_level() -> SkillTrustLevel {
54 SkillTrustLevel::Trusted
55}
56
57fn default_trust_hash_mismatch_level() -> SkillTrustLevel {
58 SkillTrustLevel::Quarantined
59}
60
61fn default_trust_bundled_level() -> SkillTrustLevel {
62 SkillTrustLevel::Trusted
63}
64
65fn default_llm_timeout() -> u64 {
66 120
67}
68
69fn default_embedding_timeout() -> u64 {
70 30
71}
72
73fn default_a2a_timeout() -> u64 {
74 30
75}
76
77fn default_max_parallel_tools() -> usize {
78 8
79}
80
81fn default_llm_request_timeout() -> u64 {
82 600
83}
84
85fn default_context_prep_timeout() -> u64 {
86 30
87}
88
89fn default_no_providers_backoff_secs() -> u64 {
90 2
91}
92
93#[derive(Debug, Clone, Deserialize, Serialize)]
107pub struct TrustConfig {
108 #[serde(default = "default_trust_default_level")]
110 pub default_level: SkillTrustLevel,
111 #[serde(default = "default_trust_local_level")]
113 pub local_level: SkillTrustLevel,
114 #[serde(default = "default_trust_hash_mismatch_level")]
117 pub hash_mismatch_level: SkillTrustLevel,
118 #[serde(default = "default_trust_bundled_level")]
120 pub bundled_level: SkillTrustLevel,
121 #[serde(default = "default_true")]
129 pub scan_on_load: bool,
130 #[serde(default)]
132 pub scanner: ScannerConfig,
133}
134
135impl Default for TrustConfig {
136 fn default() -> Self {
137 Self {
138 default_level: default_trust_default_level(),
139 local_level: default_trust_local_level(),
140 hash_mismatch_level: default_trust_hash_mismatch_level(),
141 bundled_level: default_trust_bundled_level(),
142 scan_on_load: true,
143 scanner: ScannerConfig::default(),
144 }
145 }
146}
147
148fn default_decay_per_turn() -> f32 {
151 0.85
152}
153fn default_window_turns() -> u32 {
154 8
155}
156fn default_elevated_at() -> f32 {
157 2.0
158}
159fn default_high_at() -> f32 {
160 4.0
161}
162fn default_critical_at() -> f32 {
163 8.0
164}
165fn default_alert_threshold() -> f32 {
166 4.0
167}
168fn default_auto_recover_after_turns() -> u32 {
169 16
170}
171fn default_subagent_inheritance_factor() -> f32 {
172 0.5
173}
174fn default_high_call_rate_threshold() -> u32 {
175 12
176}
177fn default_unusual_read_threshold() -> u32 {
178 24
179}
180fn default_auto_recover_floor() -> u32 {
181 4
182}
183
184#[derive(Debug, Clone, Deserialize, Serialize)]
201pub struct TrajectorySentinelConfig {
202 #[serde(default = "default_decay_per_turn")]
206 pub decay_per_turn: f32,
207 #[serde(default = "default_window_turns")]
211 pub window_turns: u32,
212 #[serde(default = "default_elevated_at")]
214 pub elevated_at: f32,
215 #[serde(default = "default_high_at")]
217 pub high_at: f32,
218 #[serde(default = "default_critical_at")]
220 pub critical_at: f32,
221 #[serde(default = "default_alert_threshold")]
225 pub alert_threshold: f32,
226 #[serde(default = "default_auto_recover_after_turns")]
228 pub auto_recover_after_turns: u32,
229 #[serde(default = "default_subagent_inheritance_factor")]
234 pub subagent_inheritance_factor: f32,
235 #[serde(default = "default_high_call_rate_threshold")]
237 pub high_call_rate_threshold: u32,
238 #[serde(default = "default_unusual_read_threshold")]
240 pub unusual_read_threshold: u32,
241}
242
243impl Default for TrajectorySentinelConfig {
244 fn default() -> Self {
245 Self {
246 decay_per_turn: default_decay_per_turn(),
247 window_turns: default_window_turns(),
248 elevated_at: default_elevated_at(),
249 high_at: default_high_at(),
250 critical_at: default_critical_at(),
251 alert_threshold: default_alert_threshold(),
252 auto_recover_after_turns: default_auto_recover_after_turns(),
253 subagent_inheritance_factor: default_subagent_inheritance_factor(),
254 high_call_rate_threshold: default_high_call_rate_threshold(),
255 unusual_read_threshold: default_unusual_read_threshold(),
256 }
257 }
258}
259
260impl TrajectorySentinelConfig {
261 pub fn validate(&self) -> Result<(), String> {
267 if self.decay_per_turn <= 0.0 || self.decay_per_turn > 1.0 {
268 return Err(format!(
269 "trajectory.decay_per_turn must be in (0.0, 1.0]; got {}",
270 self.decay_per_turn
271 ));
272 }
273 if self.elevated_at >= self.high_at {
274 return Err(format!(
275 "trajectory: elevated_at ({}) must be < high_at ({})",
276 self.elevated_at, self.high_at
277 ));
278 }
279 if self.high_at >= self.critical_at {
280 return Err(format!(
281 "trajectory: high_at ({}) must be < critical_at ({})",
282 self.high_at, self.critical_at
283 ));
284 }
285 if self.auto_recover_after_turns < default_auto_recover_floor() {
286 return Err(format!(
287 "trajectory.auto_recover_after_turns must be >= {}; got {}",
288 default_auto_recover_floor(),
289 self.auto_recover_after_turns
290 ));
291 }
292 if self.decay_per_turn < 1.0 {
294 let ideal = self
295 .decay_per_turn
296 .powf(0.5_f32.ln() / self.decay_per_turn.ln());
297 if (self.subagent_inheritance_factor - ideal).abs() > 0.1 {
298 tracing::warn!(
300 configured = self.subagent_inheritance_factor,
301 ideal = ideal,
302 decay = self.decay_per_turn,
303 "trajectory.subagent_inheritance_factor deviates from calibrated value by more than 0.1"
304 );
305 }
306 }
307 Ok(())
308 }
309}
310
311fn default_shadow_max_context_events() -> usize {
314 50
315}
316fn default_shadow_probe_timeout_ms() -> u64 {
317 2000
318}
319fn default_shadow_max_probes_per_turn() -> usize {
320 3
321}
322fn default_shadow_probe_patterns() -> Vec<String> {
323 vec![
324 "builtin:shell".to_owned(),
325 "builtin:write".to_owned(),
326 "builtin:edit".to_owned(),
327 "mcp:*/file_*".to_owned(),
328 "mcp:*/exec_*".to_owned(),
329 ]
330}
331
332#[derive(Debug, Clone, Deserialize, Serialize)]
348pub struct ShadowSentinelConfig {
349 #[serde(default)]
351 pub enabled: bool,
352 #[serde(default)]
357 pub probe_provider: String,
358 #[serde(default = "default_shadow_max_context_events")]
360 pub max_context_events: usize,
361 #[serde(default = "default_shadow_probe_timeout_ms")]
363 pub probe_timeout_ms: u64,
364 #[serde(default = "default_shadow_max_probes_per_turn")]
366 pub max_probes_per_turn: usize,
367 #[serde(default = "default_shadow_probe_patterns")]
371 pub probe_patterns: Vec<String>,
372 #[serde(default)]
380 pub deny_on_timeout: bool,
381}
382
383impl Default for ShadowSentinelConfig {
384 fn default() -> Self {
385 Self {
386 enabled: false,
387 probe_provider: String::new(),
388 max_context_events: default_shadow_max_context_events(),
389 probe_timeout_ms: default_shadow_probe_timeout_ms(),
390 max_probes_per_turn: default_shadow_max_probes_per_turn(),
391 probe_patterns: default_shadow_probe_patterns(),
392 deny_on_timeout: false,
393 }
394 }
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
403#[serde(rename_all = "snake_case")]
404pub enum PatternStrictness {
405 Strict,
407 Permissive,
409 #[default]
413 ProvisionalForDynamicNamespaces,
414}
415
416#[derive(Debug, Clone, Deserialize, Serialize)]
426pub struct ScopeConfig {
427 #[serde(default)]
431 pub patterns: Vec<String>,
432}
433
434#[derive(Debug, Clone, Deserialize, Serialize, Default)]
453pub struct CapabilityScopesConfig {
454 #[serde(default = "default_scope_name")]
459 pub default_scope: String,
460 #[serde(default)]
463 pub strict: bool,
464 #[serde(default)]
466 pub pattern_strictness: PatternStrictness,
467 #[serde(default, flatten)]
469 pub scopes: HashMap<String, ScopeConfig>,
470}
471
472fn default_scope_name() -> String {
473 "general".to_owned()
474}
475
476#[derive(Debug, Clone, Deserialize, Serialize)]
496pub struct SecurityConfig {
497 #[serde(default = "default_true")]
500 pub redact_secrets: bool,
501 #[serde(default)]
503 pub autonomy_level: AutonomyLevel,
504 #[serde(default)]
505 pub content_isolation: ContentIsolationConfig,
506 #[serde(default)]
507 pub exfiltration_guard: ExfiltrationGuardConfig,
508 #[serde(default)]
510 pub memory_validation: MemoryWriteValidationConfig,
511 #[serde(default)]
513 pub pii_filter: PiiFilterConfig,
514 #[serde(default)]
516 pub rate_limit: RateLimitConfig,
517 #[serde(default)]
519 pub pre_execution_verify: PreExecutionVerifierConfig,
520 #[serde(default)]
522 pub guardrail: GuardrailConfig,
523 #[serde(default)]
525 pub response_verification: ResponseVerificationConfig,
526 #[serde(default)]
528 pub causal_ipi: CausalIpiConfig,
529 #[serde(default)]
534 pub vigil: VigilConfig,
535 #[serde(default)]
540 pub trajectory: TrajectorySentinelConfig,
541 #[serde(default)]
546 pub capability_scopes: CapabilityScopesConfig,
547 #[serde(default)]
553 pub shadow_sentinel: ShadowSentinelConfig,
554}
555
556impl Default for SecurityConfig {
557 fn default() -> Self {
558 Self {
559 redact_secrets: true,
560 autonomy_level: AutonomyLevel::default(),
561 content_isolation: ContentIsolationConfig::default(),
562 exfiltration_guard: ExfiltrationGuardConfig::default(),
563 memory_validation: MemoryWriteValidationConfig::default(),
564 pii_filter: PiiFilterConfig::default(),
565 rate_limit: RateLimitConfig::default(),
566 pre_execution_verify: PreExecutionVerifierConfig::default(),
567 guardrail: GuardrailConfig::default(),
568 response_verification: ResponseVerificationConfig::default(),
569 causal_ipi: CausalIpiConfig::default(),
570 vigil: VigilConfig::default(),
571 trajectory: TrajectorySentinelConfig::default(),
572 capability_scopes: CapabilityScopesConfig::default(),
573 shadow_sentinel: ShadowSentinelConfig::default(),
574 }
575 }
576}
577
578#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
592pub struct TimeoutConfig {
593 #[serde(default = "default_llm_timeout")]
595 pub llm_seconds: u64,
596 #[serde(default = "default_llm_request_timeout")]
599 pub llm_request_timeout_secs: u64,
600 #[serde(default = "default_embedding_timeout")]
602 pub embedding_seconds: u64,
603 #[serde(default = "default_a2a_timeout")]
605 pub a2a_seconds: u64,
606 #[serde(default = "default_max_parallel_tools")]
609 pub max_parallel_tools: usize,
610 #[serde(default = "default_context_prep_timeout")]
617 pub context_prep_timeout_secs: u64,
618 #[serde(default = "default_no_providers_backoff_secs")]
622 pub no_providers_backoff_secs: u64,
623}
624
625impl Default for TimeoutConfig {
626 fn default() -> Self {
627 Self {
628 llm_seconds: default_llm_timeout(),
629 llm_request_timeout_secs: default_llm_request_timeout(),
630 embedding_seconds: default_embedding_timeout(),
631 a2a_seconds: default_a2a_timeout(),
632 max_parallel_tools: default_max_parallel_tools(),
633 context_prep_timeout_secs: default_context_prep_timeout(),
634 no_providers_backoff_secs: default_no_providers_backoff_secs(),
635 }
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[test]
644 fn trust_config_default_has_scan_on_load_true() {
645 let config = TrustConfig::default();
646 assert!(config.scan_on_load);
647 }
648
649 #[test]
650 fn trust_config_serde_roundtrip_with_scan_on_load() {
651 let config = TrustConfig {
652 default_level: SkillTrustLevel::Quarantined,
653 local_level: SkillTrustLevel::Trusted,
654 hash_mismatch_level: SkillTrustLevel::Quarantined,
655 bundled_level: SkillTrustLevel::Trusted,
656 scan_on_load: false,
657 scanner: ScannerConfig::default(),
658 };
659 let toml = toml::to_string(&config).expect("serialize");
660 let deserialized: TrustConfig = toml::from_str(&toml).expect("deserialize");
661 assert!(!deserialized.scan_on_load);
662 assert_eq!(deserialized.bundled_level, SkillTrustLevel::Trusted);
663 }
664
665 #[test]
666 fn trust_config_missing_scan_on_load_defaults_to_true() {
667 let toml = r#"
668default_level = "quarantined"
669local_level = "trusted"
670hash_mismatch_level = "quarantined"
671"#;
672 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
673 assert!(
674 config.scan_on_load,
675 "missing scan_on_load must default to true"
676 );
677 }
678
679 #[test]
680 fn trust_config_default_has_bundled_level_trusted() {
681 let config = TrustConfig::default();
682 assert_eq!(config.bundled_level, SkillTrustLevel::Trusted);
683 }
684
685 #[test]
686 fn trust_config_missing_bundled_level_defaults_to_trusted() {
687 let toml = r#"
688default_level = "quarantined"
689local_level = "trusted"
690hash_mismatch_level = "quarantined"
691"#;
692 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
693 assert_eq!(
694 config.bundled_level,
695 SkillTrustLevel::Trusted,
696 "missing bundled_level must default to trusted"
697 );
698 }
699
700 #[test]
701 fn scanner_config_defaults() {
702 let cfg = ScannerConfig::default();
703 assert!(cfg.injection_patterns);
704 assert!(!cfg.capability_escalation_check);
705 }
706
707 #[test]
708 fn scanner_config_serde_roundtrip() {
709 let cfg = ScannerConfig {
710 injection_patterns: false,
711 capability_escalation_check: true,
712 };
713 let toml = toml::to_string(&cfg).expect("serialize");
714 let back: ScannerConfig = toml::from_str(&toml).expect("deserialize");
715 assert!(!back.injection_patterns);
716 assert!(back.capability_escalation_check);
717 }
718
719 #[test]
720 fn trust_config_scanner_defaults_when_missing() {
721 let toml = r#"
722default_level = "quarantined"
723local_level = "trusted"
724hash_mismatch_level = "quarantined"
725"#;
726 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
727 assert!(config.scanner.injection_patterns);
728 assert!(!config.scanner.capability_escalation_check);
729 }
730
731 #[test]
736 fn timeout_config_context_prep_timeout_default() {
737 let cfg = TimeoutConfig::default();
738 assert_eq!(
739 cfg.context_prep_timeout_secs, 30,
740 "context_prep_timeout_secs default must be 30s (#3357)"
741 );
742 }
743
744 #[test]
745 fn timeout_config_no_providers_backoff_default() {
746 let cfg = TimeoutConfig::default();
747 assert_eq!(
748 cfg.no_providers_backoff_secs, 2,
749 "no_providers_backoff_secs default must be 2s (#3357)"
750 );
751 }
752
753 #[test]
754 fn timeout_config_new_fields_deserialize_from_toml() {
755 let toml = r"
756context_prep_timeout_secs = 60
757no_providers_backoff_secs = 10
758";
759 let cfg: TimeoutConfig = toml::from_str(toml).expect("deserialize");
760 assert_eq!(cfg.context_prep_timeout_secs, 60);
761 assert_eq!(cfg.no_providers_backoff_secs, 10);
762 }
763
764 #[test]
765 fn timeout_config_new_fields_default_when_missing_from_toml() {
766 let cfg: TimeoutConfig = toml::from_str("").expect("deserialize empty");
768 assert_eq!(cfg.context_prep_timeout_secs, 30);
769 assert_eq!(cfg.no_providers_backoff_secs, 2);
770 }
771}