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)]
133 pub scanner: ScannerConfig,
134}
135
136impl Default for TrustConfig {
137 fn default() -> Self {
138 Self {
139 default_level: default_trust_default_level(),
140 local_level: default_trust_local_level(),
141 hash_mismatch_level: default_trust_hash_mismatch_level(),
142 bundled_level: default_trust_bundled_level(),
143 scan_on_load: true,
144 scanner: ScannerConfig::default(),
145 }
146 }
147}
148
149fn default_decay_per_turn() -> f32 {
152 0.85
153}
154fn default_window_turns() -> u32 {
155 8
156}
157fn default_elevated_at() -> f32 {
158 2.0
159}
160fn default_high_at() -> f32 {
161 4.0
162}
163fn default_critical_at() -> f32 {
164 8.0
165}
166fn default_alert_threshold() -> f32 {
167 4.0
168}
169fn default_auto_recover_after_turns() -> u32 {
170 16
171}
172fn default_subagent_inheritance_factor() -> f32 {
173 0.5
174}
175fn default_high_call_rate_threshold() -> u32 {
176 12
177}
178fn default_unusual_read_threshold() -> u32 {
179 24
180}
181fn default_auto_recover_floor() -> u32 {
182 4
183}
184
185#[derive(Debug, Clone, Deserialize, Serialize)]
202pub struct TrajectorySentinelConfig {
203 #[serde(default = "default_decay_per_turn")]
207 pub decay_per_turn: f32,
208 #[serde(default = "default_window_turns")]
212 pub window_turns: u32,
213 #[serde(default = "default_elevated_at")]
215 pub elevated_at: f32,
216 #[serde(default = "default_high_at")]
218 pub high_at: f32,
219 #[serde(default = "default_critical_at")]
221 pub critical_at: f32,
222 #[serde(default = "default_alert_threshold")]
226 pub alert_threshold: f32,
227 #[serde(default = "default_auto_recover_after_turns")]
229 pub auto_recover_after_turns: u32,
230 #[serde(default = "default_subagent_inheritance_factor")]
235 pub subagent_inheritance_factor: f32,
236 #[serde(default = "default_high_call_rate_threshold")]
238 pub high_call_rate_threshold: u32,
239 #[serde(default = "default_unusual_read_threshold")]
241 pub unusual_read_threshold: u32,
242}
243
244impl Default for TrajectorySentinelConfig {
245 fn default() -> Self {
246 Self {
247 decay_per_turn: default_decay_per_turn(),
248 window_turns: default_window_turns(),
249 elevated_at: default_elevated_at(),
250 high_at: default_high_at(),
251 critical_at: default_critical_at(),
252 alert_threshold: default_alert_threshold(),
253 auto_recover_after_turns: default_auto_recover_after_turns(),
254 subagent_inheritance_factor: default_subagent_inheritance_factor(),
255 high_call_rate_threshold: default_high_call_rate_threshold(),
256 unusual_read_threshold: default_unusual_read_threshold(),
257 }
258 }
259}
260
261impl TrajectorySentinelConfig {
262 pub fn validate(&self) -> Result<(), String> {
268 if self.decay_per_turn <= 0.0 || self.decay_per_turn > 1.0 {
269 return Err(format!(
270 "trajectory.decay_per_turn must be in (0.0, 1.0]; got {}",
271 self.decay_per_turn
272 ));
273 }
274 if self.elevated_at >= self.high_at {
275 return Err(format!(
276 "trajectory: elevated_at ({}) must be < high_at ({})",
277 self.elevated_at, self.high_at
278 ));
279 }
280 if self.high_at >= self.critical_at {
281 return Err(format!(
282 "trajectory: high_at ({}) must be < critical_at ({})",
283 self.high_at, self.critical_at
284 ));
285 }
286 if self.auto_recover_after_turns < default_auto_recover_floor() {
287 return Err(format!(
288 "trajectory.auto_recover_after_turns must be >= {}; got {}",
289 default_auto_recover_floor(),
290 self.auto_recover_after_turns
291 ));
292 }
293 if self.decay_per_turn < 1.0 {
295 let ideal = self
296 .decay_per_turn
297 .powf(0.5_f32.ln() / self.decay_per_turn.ln());
298 if (self.subagent_inheritance_factor - ideal).abs() > 0.1 {
299 tracing::warn!(
301 configured = self.subagent_inheritance_factor,
302 ideal = ideal,
303 decay = self.decay_per_turn,
304 "trajectory.subagent_inheritance_factor deviates from calibrated value by more than 0.1"
305 );
306 }
307 }
308 Ok(())
309 }
310}
311
312fn default_shadow_max_context_events() -> usize {
315 50
316}
317fn default_shadow_probe_timeout_ms() -> u64 {
318 2000
319}
320fn default_shadow_max_probes_per_turn() -> usize {
321 3
322}
323fn default_shadow_probe_patterns() -> Vec<String> {
324 vec![
325 "builtin:shell".to_owned(),
326 "builtin:write".to_owned(),
327 "builtin:edit".to_owned(),
328 "mcp:*/file_*".to_owned(),
329 "mcp:*/exec_*".to_owned(),
330 ]
331}
332
333#[derive(Debug, Clone, Deserialize, Serialize)]
349pub struct ShadowSentinelConfig {
350 #[serde(default)]
352 pub enabled: bool,
353 #[serde(default)]
358 pub probe_provider: ProviderName,
359 #[serde(default = "default_shadow_max_context_events")]
361 pub max_context_events: usize,
362 #[serde(default = "default_shadow_probe_timeout_ms")]
364 pub probe_timeout_ms: u64,
365 #[serde(default = "default_shadow_max_probes_per_turn")]
367 pub max_probes_per_turn: usize,
368 #[serde(default = "default_shadow_probe_patterns")]
372 pub probe_patterns: Vec<String>,
373 #[serde(default)]
381 pub deny_on_timeout: bool,
382}
383
384impl Default for ShadowSentinelConfig {
385 fn default() -> Self {
386 Self {
387 enabled: false,
388 probe_provider: ProviderName::default(),
389 max_context_events: default_shadow_max_context_events(),
390 probe_timeout_ms: default_shadow_probe_timeout_ms(),
391 max_probes_per_turn: default_shadow_max_probes_per_turn(),
392 probe_patterns: default_shadow_probe_patterns(),
393 deny_on_timeout: false,
394 }
395 }
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
404#[serde(rename_all = "snake_case")]
405#[non_exhaustive]
406pub enum PatternStrictness {
407 Strict,
409 Permissive,
411 #[default]
415 ProvisionalForDynamicNamespaces,
416}
417
418#[derive(Debug, Clone, Deserialize, Serialize)]
428pub struct ScopeConfig {
429 #[serde(default)]
433 pub patterns: Vec<String>,
434}
435
436#[derive(Debug, Clone, Deserialize, Serialize, Default)]
455pub struct CapabilityScopesConfig {
456 #[serde(default = "default_scope_name")]
461 pub default_scope: String,
462 #[serde(default)]
465 pub strict: bool,
466 #[serde(default)]
468 pub pattern_strictness: PatternStrictness,
469 #[serde(default, flatten)]
471 pub scopes: HashMap<String, ScopeConfig>,
472}
473
474fn default_scope_name() -> String {
475 "general".to_owned()
476}
477
478#[derive(Debug, Clone, Deserialize, Serialize)]
498pub struct SecurityConfig {
499 #[serde(default = "default_true")]
502 pub redact_secrets: bool,
503 #[serde(default)]
505 pub autonomy_level: AutonomyLevel,
506 #[serde(default)]
507 pub content_isolation: ContentIsolationConfig,
508 #[serde(default)]
509 pub exfiltration_guard: ExfiltrationGuardConfig,
510 #[serde(default)]
512 pub memory_validation: MemoryWriteValidationConfig,
513 #[serde(default)]
515 pub pii_filter: PiiFilterConfig,
516 #[serde(default)]
518 pub rate_limit: RateLimitConfig,
519 #[serde(default)]
521 pub pre_execution_verify: PreExecutionVerifierConfig,
522 #[serde(default)]
524 pub guardrail: GuardrailConfig,
525 #[serde(default)]
527 pub response_verification: ResponseVerificationConfig,
528 #[serde(default)]
530 pub causal_ipi: CausalIpiConfig,
531 #[serde(default)]
536 pub vigil: VigilConfig,
537 #[serde(default)]
542 pub trajectory: TrajectorySentinelConfig,
543 #[serde(default)]
548 pub capability_scopes: CapabilityScopesConfig,
549 #[serde(default)]
555 pub shadow_sentinel: ShadowSentinelConfig,
556}
557
558impl Default for SecurityConfig {
559 fn default() -> Self {
560 Self {
561 redact_secrets: true,
562 autonomy_level: AutonomyLevel::default(),
563 content_isolation: ContentIsolationConfig::default(),
564 exfiltration_guard: ExfiltrationGuardConfig::default(),
565 memory_validation: MemoryWriteValidationConfig::default(),
566 pii_filter: PiiFilterConfig::default(),
567 rate_limit: RateLimitConfig::default(),
568 pre_execution_verify: PreExecutionVerifierConfig::default(),
569 guardrail: GuardrailConfig::default(),
570 response_verification: ResponseVerificationConfig::default(),
571 causal_ipi: CausalIpiConfig::default(),
572 vigil: VigilConfig::default(),
573 trajectory: TrajectorySentinelConfig::default(),
574 capability_scopes: CapabilityScopesConfig::default(),
575 shadow_sentinel: ShadowSentinelConfig::default(),
576 }
577 }
578}
579
580#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
594pub struct TimeoutConfig {
595 #[serde(default = "default_llm_timeout")]
597 pub llm_seconds: u64,
598 #[serde(default = "default_llm_request_timeout")]
601 pub llm_request_timeout_secs: u64,
602 #[serde(default = "default_embedding_timeout")]
604 pub embedding_seconds: u64,
605 #[serde(default = "default_a2a_timeout")]
607 pub a2a_seconds: u64,
608 #[serde(default = "default_max_parallel_tools")]
611 pub max_parallel_tools: usize,
612 #[serde(default = "default_context_prep_timeout")]
619 pub context_prep_timeout_secs: u64,
620 #[serde(default = "default_no_providers_backoff_secs")]
624 pub no_providers_backoff_secs: u64,
625}
626
627impl Default for TimeoutConfig {
628 fn default() -> Self {
629 Self {
630 llm_seconds: default_llm_timeout(),
631 llm_request_timeout_secs: default_llm_request_timeout(),
632 embedding_seconds: default_embedding_timeout(),
633 a2a_seconds: default_a2a_timeout(),
634 max_parallel_tools: default_max_parallel_tools(),
635 context_prep_timeout_secs: default_context_prep_timeout(),
636 no_providers_backoff_secs: default_no_providers_backoff_secs(),
637 }
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
646 fn trust_config_default_has_scan_on_load_true() {
647 let config = TrustConfig::default();
648 assert!(config.scan_on_load);
649 }
650
651 #[test]
652 fn trust_config_serde_roundtrip_with_scan_on_load() {
653 let config = TrustConfig {
654 default_level: SkillTrustLevel::Quarantined,
655 local_level: SkillTrustLevel::Trusted,
656 hash_mismatch_level: SkillTrustLevel::Quarantined,
657 bundled_level: SkillTrustLevel::Trusted,
658 scan_on_load: false,
659 scanner: ScannerConfig::default(),
660 };
661 let toml = toml::to_string(&config).expect("serialize");
662 let deserialized: TrustConfig = toml::from_str(&toml).expect("deserialize");
663 assert!(!deserialized.scan_on_load);
664 assert_eq!(deserialized.bundled_level, SkillTrustLevel::Trusted);
665 }
666
667 #[test]
668 fn trust_config_missing_scan_on_load_defaults_to_true() {
669 let toml = r#"
670default_level = "quarantined"
671local_level = "trusted"
672hash_mismatch_level = "quarantined"
673"#;
674 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
675 assert!(
676 config.scan_on_load,
677 "missing scan_on_load must default to true"
678 );
679 }
680
681 #[test]
682 fn trust_config_default_has_bundled_level_trusted() {
683 let config = TrustConfig::default();
684 assert_eq!(config.bundled_level, SkillTrustLevel::Trusted);
685 }
686
687 #[test]
688 fn trust_config_missing_bundled_level_defaults_to_trusted() {
689 let toml = r#"
690default_level = "quarantined"
691local_level = "trusted"
692hash_mismatch_level = "quarantined"
693"#;
694 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
695 assert_eq!(
696 config.bundled_level,
697 SkillTrustLevel::Trusted,
698 "missing bundled_level must default to trusted"
699 );
700 }
701
702 #[test]
703 fn scanner_config_defaults() {
704 let cfg = ScannerConfig::default();
705 assert!(cfg.injection_patterns);
706 assert!(!cfg.capability_escalation_check);
707 }
708
709 #[test]
710 fn scanner_config_serde_roundtrip() {
711 let cfg = ScannerConfig {
712 injection_patterns: false,
713 capability_escalation_check: true,
714 };
715 let toml = toml::to_string(&cfg).expect("serialize");
716 let back: ScannerConfig = toml::from_str(&toml).expect("deserialize");
717 assert!(!back.injection_patterns);
718 assert!(back.capability_escalation_check);
719 }
720
721 #[test]
722 fn trust_config_scanner_defaults_when_missing() {
723 let toml = r#"
724default_level = "quarantined"
725local_level = "trusted"
726hash_mismatch_level = "quarantined"
727"#;
728 let config: TrustConfig = toml::from_str(toml).expect("deserialize");
729 assert!(config.scanner.injection_patterns);
730 assert!(!config.scanner.capability_escalation_check);
731 }
732
733 #[test]
738 fn timeout_config_context_prep_timeout_default() {
739 let cfg = TimeoutConfig::default();
740 assert_eq!(
741 cfg.context_prep_timeout_secs, 30,
742 "context_prep_timeout_secs default must be 30s (#3357)"
743 );
744 }
745
746 #[test]
747 fn timeout_config_no_providers_backoff_default() {
748 let cfg = TimeoutConfig::default();
749 assert_eq!(
750 cfg.no_providers_backoff_secs, 2,
751 "no_providers_backoff_secs default must be 2s (#3357)"
752 );
753 }
754
755 #[test]
756 fn timeout_config_new_fields_deserialize_from_toml() {
757 let toml = r"
758context_prep_timeout_secs = 60
759no_providers_backoff_secs = 10
760";
761 let cfg: TimeoutConfig = toml::from_str(toml).expect("deserialize");
762 assert_eq!(cfg.context_prep_timeout_secs, 60);
763 assert_eq!(cfg.no_providers_backoff_secs, 10);
764 }
765
766 #[test]
767 fn timeout_config_new_fields_default_when_missing_from_toml() {
768 let cfg: TimeoutConfig = toml::from_str("").expect("deserialize empty");
770 assert_eq!(cfg.context_prep_timeout_secs, 30);
771 assert_eq!(cfg.no_providers_backoff_secs, 2);
772 }
773}