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