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/// Skill trust policy configuration, nested under `[skills.trust]` in TOML.
79///
80/// Controls how trust levels are assigned to skills at load time based on their
81/// origin (local filesystem vs network) and integrity (hash verification result).
82///
83/// # Example (TOML)
84///
85/// ```toml
86/// [skills.trust]
87/// default_level = "quarantined"
88/// local_level = "trusted"
89/// scan_on_load = true
90/// ```
91#[derive(Debug, Clone, Deserialize, Serialize)]
92pub struct TrustConfig {
93    /// Trust level assigned to skills from unknown or remote origins. Default: `quarantined`.
94    #[serde(default = "default_trust_default_level")]
95    pub default_level: SkillTrustLevel,
96    /// Trust level assigned to skills found on the local filesystem. Default: `trusted`.
97    #[serde(default = "default_trust_local_level")]
98    pub local_level: SkillTrustLevel,
99    /// Trust level assigned when a skill's content hash does not match the stored hash.
100    /// Default: `quarantined`.
101    #[serde(default = "default_trust_hash_mismatch_level")]
102    pub hash_mismatch_level: SkillTrustLevel,
103    /// Scan skill body content for injection patterns at load time.
104    ///
105    /// When `true`, `SkillRegistry::scan_loaded()` is called at agent startup.
106    /// This is **advisory only** — scan results are logged as warnings and do not
107    /// automatically change trust levels or block tool calls.
108    ///
109    /// Defaults to `true` (secure by default).
110    #[serde(default = "default_true")]
111    pub scan_on_load: bool,
112    /// Fine-grained scanner controls (injection patterns, capability escalation).
113    #[serde(default)]
114    pub scanner: ScannerConfig,
115}
116
117impl Default for TrustConfig {
118    fn default() -> Self {
119        Self {
120            default_level: default_trust_default_level(),
121            local_level: default_trust_local_level(),
122            hash_mismatch_level: default_trust_hash_mismatch_level(),
123            scan_on_load: true,
124            scanner: ScannerConfig::default(),
125        }
126    }
127}
128
129/// Agent security configuration, nested under `[security]` in TOML.
130///
131/// Aggregates all security-related subsystems: content isolation, exfiltration guards,
132/// memory write validation, PII filtering, rate limiting, prompt injection screening,
133/// and response verification.
134///
135/// # Example (TOML)
136///
137/// ```toml
138/// [security]
139/// redact_secrets = true
140/// autonomy_level = "moderate"
141///
142/// [security.rate_limit]
143/// enabled = true
144/// shell_calls_per_minute = 20
145/// ```
146#[derive(Debug, Clone, Deserialize, Serialize)]
147pub struct SecurityConfig {
148    /// Automatically redact detected secrets from tool outputs before they reach the LLM.
149    /// Default: `true`.
150    #[serde(default = "default_true")]
151    pub redact_secrets: bool,
152    /// Autonomy level controlling which tool actions require explicit user confirmation.
153    #[serde(default)]
154    pub autonomy_level: AutonomyLevel,
155    #[serde(default)]
156    pub content_isolation: ContentIsolationConfig,
157    #[serde(default)]
158    pub exfiltration_guard: ExfiltrationGuardConfig,
159    /// Memory write validation (enabled by default).
160    #[serde(default)]
161    pub memory_validation: MemoryWriteValidationConfig,
162    /// PII filter for tool outputs and debug dumps (opt-in, disabled by default).
163    #[serde(default)]
164    pub pii_filter: PiiFilterConfig,
165    /// Tool action rate limiter (opt-in, disabled by default).
166    #[serde(default)]
167    pub rate_limit: RateLimitConfig,
168    /// Pre-execution verifiers (enabled by default).
169    #[serde(default)]
170    pub pre_execution_verify: PreExecutionVerifierConfig,
171    /// LLM-based prompt injection pre-screener (opt-in, disabled by default).
172    #[serde(default)]
173    pub guardrail: GuardrailConfig,
174    /// Post-LLM response verification layer (enabled by default).
175    #[serde(default)]
176    pub response_verification: ResponseVerificationConfig,
177    /// Temporal causal IPI analysis at tool-return boundaries (opt-in, disabled by default).
178    #[serde(default)]
179    pub causal_ipi: CausalIpiConfig,
180}
181
182impl Default for SecurityConfig {
183    fn default() -> Self {
184        Self {
185            redact_secrets: true,
186            autonomy_level: AutonomyLevel::default(),
187            content_isolation: ContentIsolationConfig::default(),
188            exfiltration_guard: ExfiltrationGuardConfig::default(),
189            memory_validation: MemoryWriteValidationConfig::default(),
190            pii_filter: PiiFilterConfig::default(),
191            rate_limit: RateLimitConfig::default(),
192            pre_execution_verify: PreExecutionVerifierConfig::default(),
193            guardrail: GuardrailConfig::default(),
194            response_verification: ResponseVerificationConfig::default(),
195            causal_ipi: CausalIpiConfig::default(),
196        }
197    }
198}
199
200/// Timeout configuration for external operations, nested under `[timeouts]` in TOML.
201///
202/// All timeouts are in seconds. Exceeding a timeout returns an error to the agent
203/// loop rather than blocking indefinitely.
204///
205/// # Example (TOML)
206///
207/// ```toml
208/// [timeouts]
209/// llm_seconds = 60
210/// embedding_seconds = 15
211/// max_parallel_tools = 4
212/// ```
213#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
214pub struct TimeoutConfig {
215    /// Timeout for streaming LLM first-token responses, in seconds. Default: `120`.
216    #[serde(default = "default_llm_timeout")]
217    pub llm_seconds: u64,
218    /// Total wall-clock timeout for a complete LLM request (all tokens), in seconds.
219    /// Default: `600`.
220    #[serde(default = "default_llm_request_timeout")]
221    pub llm_request_timeout_secs: u64,
222    /// Timeout for embedding API calls, in seconds. Default: `30`.
223    #[serde(default = "default_embedding_timeout")]
224    pub embedding_seconds: u64,
225    /// Timeout for A2A agent-to-agent calls, in seconds. Default: `30`.
226    #[serde(default = "default_a2a_timeout")]
227    pub a2a_seconds: u64,
228    /// Maximum number of tool calls that may execute concurrently in a single turn.
229    /// Default: `8`.
230    #[serde(default = "default_max_parallel_tools")]
231    pub max_parallel_tools: usize,
232}
233
234impl Default for TimeoutConfig {
235    fn default() -> Self {
236        Self {
237            llm_seconds: default_llm_timeout(),
238            llm_request_timeout_secs: default_llm_request_timeout(),
239            embedding_seconds: default_embedding_timeout(),
240            a2a_seconds: default_a2a_timeout(),
241            max_parallel_tools: default_max_parallel_tools(),
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn trust_config_default_has_scan_on_load_true() {
252        let config = TrustConfig::default();
253        assert!(config.scan_on_load);
254    }
255
256    #[test]
257    fn trust_config_serde_roundtrip_with_scan_on_load() {
258        let config = TrustConfig {
259            default_level: SkillTrustLevel::Quarantined,
260            local_level: SkillTrustLevel::Trusted,
261            hash_mismatch_level: SkillTrustLevel::Quarantined,
262            scan_on_load: false,
263            scanner: ScannerConfig::default(),
264        };
265        let toml = toml::to_string(&config).expect("serialize");
266        let deserialized: TrustConfig = toml::from_str(&toml).expect("deserialize");
267        assert!(!deserialized.scan_on_load);
268    }
269
270    #[test]
271    fn trust_config_missing_scan_on_load_defaults_to_true() {
272        let toml = r#"
273default_level = "quarantined"
274local_level = "trusted"
275hash_mismatch_level = "quarantined"
276"#;
277        let config: TrustConfig = toml::from_str(toml).expect("deserialize");
278        assert!(
279            config.scan_on_load,
280            "missing scan_on_load must default to true"
281        );
282    }
283
284    #[test]
285    fn scanner_config_defaults() {
286        let cfg = ScannerConfig::default();
287        assert!(cfg.injection_patterns);
288        assert!(!cfg.capability_escalation_check);
289    }
290
291    #[test]
292    fn scanner_config_serde_roundtrip() {
293        let cfg = ScannerConfig {
294            injection_patterns: false,
295            capability_escalation_check: true,
296        };
297        let toml = toml::to_string(&cfg).expect("serialize");
298        let back: ScannerConfig = toml::from_str(&toml).expect("deserialize");
299        assert!(!back.injection_patterns);
300        assert!(back.capability_escalation_check);
301    }
302
303    #[test]
304    fn trust_config_scanner_defaults_when_missing() {
305        let toml = r#"
306default_level = "quarantined"
307local_level = "trusted"
308hash_mismatch_level = "quarantined"
309"#;
310        let config: TrustConfig = toml::from_str(toml).expect("deserialize");
311        assert!(config.scanner.injection_patterns);
312        assert!(!config.scanner.capability_escalation_check);
313    }
314}