Skip to main content

zeph_config/
security.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use serde::{Deserialize, Serialize};
5use zeph_tools::AutonomyLevel;
6use zeph_tools::PreExecutionVerifierConfig;
7use zeph_tools::SkillTrustLevel;
8
9use crate::defaults::default_true;
10
11/// Fine-grained controls for the skill body scanner.
12///
13/// Nested under `[skills.trust.scanner]` in TOML.
14#[derive(Debug, Clone, Deserialize, Serialize)]
15pub struct ScannerConfig {
16    /// Scan skill body content for injection patterns at load time.
17    ///
18    /// More specific than `scan_on_load` (which controls whether `scan_loaded()` is called at
19    /// all). When `scan_on_load = true` and `injection_patterns = false`, the scan loop still
20    /// runs but skips the injection pattern check.
21    #[serde(default = "default_true")]
22    pub injection_patterns: bool,
23    /// Check whether a skill's `allowed_tools` exceed its trust level's permissions.
24    ///
25    /// When enabled, the bootstrap calls `check_escalations()` on the registry and logs
26    /// warnings for any tool declarations that violate the trust boundary.
27    #[serde(default)]
28    pub capability_escalation_check: bool,
29}
30
31impl Default for ScannerConfig {
32    fn default() -> Self {
33        Self {
34            injection_patterns: true,
35            capability_escalation_check: false,
36        }
37    }
38}
39use crate::rate_limit::RateLimitConfig;
40use crate::sanitizer::GuardrailConfig;
41use crate::sanitizer::{
42    CausalIpiConfig, ContentIsolationConfig, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
43    PiiFilterConfig, ResponseVerificationConfig,
44};
45
46fn default_trust_default_level() -> SkillTrustLevel {
47    SkillTrustLevel::Quarantined
48}
49
50fn default_trust_local_level() -> SkillTrustLevel {
51    SkillTrustLevel::Trusted
52}
53
54fn default_trust_hash_mismatch_level() -> SkillTrustLevel {
55    SkillTrustLevel::Quarantined
56}
57
58fn default_llm_timeout() -> u64 {
59    120
60}
61
62fn default_embedding_timeout() -> u64 {
63    30
64}
65
66fn default_a2a_timeout() -> u64 {
67    30
68}
69
70fn default_max_parallel_tools() -> usize {
71    8
72}
73
74fn default_llm_request_timeout() -> u64 {
75    600
76}
77
78#[derive(Debug, Clone, Deserialize, Serialize)]
79pub struct TrustConfig {
80    #[serde(default = "default_trust_default_level")]
81    pub default_level: SkillTrustLevel,
82    #[serde(default = "default_trust_local_level")]
83    pub local_level: SkillTrustLevel,
84    #[serde(default = "default_trust_hash_mismatch_level")]
85    pub hash_mismatch_level: SkillTrustLevel,
86    /// Scan skill body content for injection patterns at load time.
87    ///
88    /// When `true`, `SkillRegistry::scan_loaded()` is called at agent startup.
89    /// This is **advisory only** — scan results are logged as warnings and do not
90    /// automatically change trust levels or block tool calls.
91    ///
92    /// Defaults to `true` (secure by default).
93    #[serde(default = "default_true")]
94    pub scan_on_load: bool,
95    /// Fine-grained scanner controls (injection patterns, capability escalation).
96    #[serde(default)]
97    pub scanner: ScannerConfig,
98}
99
100impl Default for TrustConfig {
101    fn default() -> Self {
102        Self {
103            default_level: default_trust_default_level(),
104            local_level: default_trust_local_level(),
105            hash_mismatch_level: default_trust_hash_mismatch_level(),
106            scan_on_load: true,
107            scanner: ScannerConfig::default(),
108        }
109    }
110}
111
112#[derive(Debug, Clone, Deserialize, Serialize)]
113pub struct SecurityConfig {
114    #[serde(default = "default_true")]
115    pub redact_secrets: bool,
116    #[serde(default)]
117    pub autonomy_level: AutonomyLevel,
118    #[serde(default)]
119    pub content_isolation: ContentIsolationConfig,
120    #[serde(default)]
121    pub exfiltration_guard: ExfiltrationGuardConfig,
122    /// Memory write validation (enabled by default).
123    #[serde(default)]
124    pub memory_validation: MemoryWriteValidationConfig,
125    /// PII filter for tool outputs and debug dumps (opt-in, disabled by default).
126    #[serde(default)]
127    pub pii_filter: PiiFilterConfig,
128    /// Tool action rate limiter (opt-in, disabled by default).
129    #[serde(default)]
130    pub rate_limit: RateLimitConfig,
131    /// Pre-execution verifiers (enabled by default).
132    #[serde(default)]
133    pub pre_execution_verify: PreExecutionVerifierConfig,
134    /// LLM-based prompt injection pre-screener (opt-in, disabled by default).
135    #[serde(default)]
136    pub guardrail: GuardrailConfig,
137    /// Post-LLM response verification layer (enabled by default).
138    #[serde(default)]
139    pub response_verification: ResponseVerificationConfig,
140    /// Temporal causal IPI analysis at tool-return boundaries (opt-in, disabled by default).
141    #[serde(default)]
142    pub causal_ipi: CausalIpiConfig,
143}
144
145impl Default for SecurityConfig {
146    fn default() -> Self {
147        Self {
148            redact_secrets: true,
149            autonomy_level: AutonomyLevel::default(),
150            content_isolation: ContentIsolationConfig::default(),
151            exfiltration_guard: ExfiltrationGuardConfig::default(),
152            memory_validation: MemoryWriteValidationConfig::default(),
153            pii_filter: PiiFilterConfig::default(),
154            rate_limit: RateLimitConfig::default(),
155            pre_execution_verify: PreExecutionVerifierConfig::default(),
156            guardrail: GuardrailConfig::default(),
157            response_verification: ResponseVerificationConfig::default(),
158            causal_ipi: CausalIpiConfig::default(),
159        }
160    }
161}
162
163#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
164pub struct TimeoutConfig {
165    #[serde(default = "default_llm_timeout")]
166    pub llm_seconds: u64,
167    #[serde(default = "default_llm_request_timeout")]
168    pub llm_request_timeout_secs: u64,
169    #[serde(default = "default_embedding_timeout")]
170    pub embedding_seconds: u64,
171    #[serde(default = "default_a2a_timeout")]
172    pub a2a_seconds: u64,
173    #[serde(default = "default_max_parallel_tools")]
174    pub max_parallel_tools: usize,
175}
176
177impl Default for TimeoutConfig {
178    fn default() -> Self {
179        Self {
180            llm_seconds: default_llm_timeout(),
181            llm_request_timeout_secs: default_llm_request_timeout(),
182            embedding_seconds: default_embedding_timeout(),
183            a2a_seconds: default_a2a_timeout(),
184            max_parallel_tools: default_max_parallel_tools(),
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn trust_config_default_has_scan_on_load_true() {
195        let config = TrustConfig::default();
196        assert!(config.scan_on_load);
197    }
198
199    #[test]
200    fn trust_config_serde_roundtrip_with_scan_on_load() {
201        let config = TrustConfig {
202            default_level: SkillTrustLevel::Quarantined,
203            local_level: SkillTrustLevel::Trusted,
204            hash_mismatch_level: SkillTrustLevel::Quarantined,
205            scan_on_load: false,
206            scanner: ScannerConfig::default(),
207        };
208        let toml = toml::to_string(&config).expect("serialize");
209        let deserialized: TrustConfig = toml::from_str(&toml).expect("deserialize");
210        assert!(!deserialized.scan_on_load);
211    }
212
213    #[test]
214    fn trust_config_missing_scan_on_load_defaults_to_true() {
215        let toml = r#"
216default_level = "quarantined"
217local_level = "trusted"
218hash_mismatch_level = "quarantined"
219"#;
220        let config: TrustConfig = toml::from_str(toml).expect("deserialize");
221        assert!(
222            config.scan_on_load,
223            "missing scan_on_load must default to true"
224        );
225    }
226
227    #[test]
228    fn scanner_config_defaults() {
229        let cfg = ScannerConfig::default();
230        assert!(cfg.injection_patterns);
231        assert!(!cfg.capability_escalation_check);
232    }
233
234    #[test]
235    fn scanner_config_serde_roundtrip() {
236        let cfg = ScannerConfig {
237            injection_patterns: false,
238            capability_escalation_check: true,
239        };
240        let toml = toml::to_string(&cfg).expect("serialize");
241        let back: ScannerConfig = toml::from_str(&toml).expect("deserialize");
242        assert!(!back.injection_patterns);
243        assert!(back.capability_escalation_check);
244    }
245
246    #[test]
247    fn trust_config_scanner_defaults_when_missing() {
248        let toml = r#"
249default_level = "quarantined"
250local_level = "trusted"
251hash_mismatch_level = "quarantined"
252"#;
253        let config: TrustConfig = toml::from_str(toml).expect("deserialize");
254        assert!(config.scanner.injection_patterns);
255        assert!(!config.scanner.capability_escalation_check);
256    }
257}