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;
10use crate::vigil::VigilConfig;
11
12/// Fine-grained controls for the skill body scanner.
13///
14/// Nested under `[skills.trust.scanner]` in TOML.
15#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct ScannerConfig {
17    /// Scan skill body content for injection patterns at load time.
18    ///
19    /// More specific than `scan_on_load` (which controls whether `scan_loaded()` is called at
20    /// all). When `scan_on_load = true` and `injection_patterns = false`, the scan loop still
21    /// runs but skips the injection pattern check.
22    #[serde(default = "default_true")]
23    pub injection_patterns: bool,
24    /// Check whether a skill's `allowed_tools` exceed its trust level's permissions.
25    ///
26    /// When enabled, the bootstrap calls `check_escalations()` on the registry and logs
27    /// warnings for any tool declarations that violate the trust boundary.
28    #[serde(default)]
29    pub capability_escalation_check: bool,
30}
31
32impl Default for ScannerConfig {
33    fn default() -> Self {
34        Self {
35            injection_patterns: true,
36            capability_escalation_check: false,
37        }
38    }
39}
40use crate::rate_limit::RateLimitConfig;
41use crate::sanitizer::GuardrailConfig;
42use crate::sanitizer::{
43    CausalIpiConfig, ContentIsolationConfig, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
44    PiiFilterConfig, ResponseVerificationConfig,
45};
46
47fn default_trust_default_level() -> SkillTrustLevel {
48    SkillTrustLevel::Quarantined
49}
50
51fn default_trust_local_level() -> SkillTrustLevel {
52    SkillTrustLevel::Trusted
53}
54
55fn default_trust_hash_mismatch_level() -> SkillTrustLevel {
56    SkillTrustLevel::Quarantined
57}
58
59fn default_trust_bundled_level() -> SkillTrustLevel {
60    SkillTrustLevel::Trusted
61}
62
63fn default_llm_timeout() -> u64 {
64    120
65}
66
67fn default_embedding_timeout() -> u64 {
68    30
69}
70
71fn default_a2a_timeout() -> u64 {
72    30
73}
74
75fn default_max_parallel_tools() -> usize {
76    8
77}
78
79fn default_llm_request_timeout() -> u64 {
80    600
81}
82
83/// Skill trust policy configuration, nested under `[skills.trust]` in TOML.
84///
85/// Controls how trust levels are assigned to skills at load time based on their
86/// origin (local filesystem vs network) and integrity (hash verification result).
87///
88/// # Example (TOML)
89///
90/// ```toml
91/// [skills.trust]
92/// default_level = "quarantined"
93/// local_level = "trusted"
94/// scan_on_load = true
95/// ```
96#[derive(Debug, Clone, Deserialize, Serialize)]
97pub struct TrustConfig {
98    /// Trust level assigned to skills from unknown or remote origins. Default: `quarantined`.
99    #[serde(default = "default_trust_default_level")]
100    pub default_level: SkillTrustLevel,
101    /// Trust level assigned to skills found on the local filesystem. Default: `trusted`.
102    #[serde(default = "default_trust_local_level")]
103    pub local_level: SkillTrustLevel,
104    /// Trust level assigned when a skill's content hash does not match the stored hash.
105    /// Default: `quarantined`.
106    #[serde(default = "default_trust_hash_mismatch_level")]
107    pub hash_mismatch_level: SkillTrustLevel,
108    /// Trust level assigned to bundled (built-in) skills shipped with the binary. Default: `trusted`.
109    #[serde(default = "default_trust_bundled_level")]
110    pub bundled_level: SkillTrustLevel,
111    /// Scan skill body content for injection patterns at load time.
112    ///
113    /// When `true`, `SkillRegistry::scan_loaded()` is called at agent startup.
114    /// This is **advisory only** — scan results are logged as warnings and do not
115    /// automatically change trust levels or block tool calls.
116    ///
117    /// Defaults to `true` (secure by default).
118    #[serde(default = "default_true")]
119    pub scan_on_load: bool,
120    /// Fine-grained scanner controls (injection patterns, capability escalation).
121    #[serde(default)]
122    pub scanner: ScannerConfig,
123}
124
125impl Default for TrustConfig {
126    fn default() -> Self {
127        Self {
128            default_level: default_trust_default_level(),
129            local_level: default_trust_local_level(),
130            hash_mismatch_level: default_trust_hash_mismatch_level(),
131            bundled_level: default_trust_bundled_level(),
132            scan_on_load: true,
133            scanner: ScannerConfig::default(),
134        }
135    }
136}
137
138/// Agent security configuration, nested under `[security]` in TOML.
139///
140/// Aggregates all security-related subsystems: content isolation, exfiltration guards,
141/// memory write validation, PII filtering, rate limiting, prompt injection screening,
142/// and response verification.
143///
144/// # Example (TOML)
145///
146/// ```toml
147/// [security]
148/// redact_secrets = true
149/// autonomy_level = "moderate"
150///
151/// [security.rate_limit]
152/// enabled = true
153/// shell_calls_per_minute = 20
154/// ```
155#[derive(Debug, Clone, Deserialize, Serialize)]
156pub struct SecurityConfig {
157    /// Automatically redact detected secrets from tool outputs before they reach the LLM.
158    /// Default: `true`.
159    #[serde(default = "default_true")]
160    pub redact_secrets: bool,
161    /// Autonomy level controlling which tool actions require explicit user confirmation.
162    #[serde(default)]
163    pub autonomy_level: AutonomyLevel,
164    #[serde(default)]
165    pub content_isolation: ContentIsolationConfig,
166    #[serde(default)]
167    pub exfiltration_guard: ExfiltrationGuardConfig,
168    /// Memory write validation (enabled by default).
169    #[serde(default)]
170    pub memory_validation: MemoryWriteValidationConfig,
171    /// PII filter for tool outputs and debug dumps (opt-in, disabled by default).
172    #[serde(default)]
173    pub pii_filter: PiiFilterConfig,
174    /// Tool action rate limiter (opt-in, disabled by default).
175    #[serde(default)]
176    pub rate_limit: RateLimitConfig,
177    /// Pre-execution verifiers (enabled by default).
178    #[serde(default)]
179    pub pre_execution_verify: PreExecutionVerifierConfig,
180    /// LLM-based prompt injection pre-screener (opt-in, disabled by default).
181    #[serde(default)]
182    pub guardrail: GuardrailConfig,
183    /// Post-LLM response verification layer (enabled by default).
184    #[serde(default)]
185    pub response_verification: ResponseVerificationConfig,
186    /// Temporal causal IPI analysis at tool-return boundaries (opt-in, disabled by default).
187    #[serde(default)]
188    pub causal_ipi: CausalIpiConfig,
189    /// VIGIL verify-before-commit intent anchoring gate (enabled by default).
190    ///
191    /// Runs a regex tripwire before `sanitize_tool_output` to intercept low-effort injection
192    /// patterns. See `[[security.vigil]]` in TOML and spec `010-6-vigil-intent-anchoring`.
193    #[serde(default)]
194    pub vigil: VigilConfig,
195}
196
197impl Default for SecurityConfig {
198    fn default() -> Self {
199        Self {
200            redact_secrets: true,
201            autonomy_level: AutonomyLevel::default(),
202            content_isolation: ContentIsolationConfig::default(),
203            exfiltration_guard: ExfiltrationGuardConfig::default(),
204            memory_validation: MemoryWriteValidationConfig::default(),
205            pii_filter: PiiFilterConfig::default(),
206            rate_limit: RateLimitConfig::default(),
207            pre_execution_verify: PreExecutionVerifierConfig::default(),
208            guardrail: GuardrailConfig::default(),
209            response_verification: ResponseVerificationConfig::default(),
210            causal_ipi: CausalIpiConfig::default(),
211            vigil: VigilConfig::default(),
212        }
213    }
214}
215
216/// Timeout configuration for external operations, nested under `[timeouts]` in TOML.
217///
218/// All timeouts are in seconds. Exceeding a timeout returns an error to the agent
219/// loop rather than blocking indefinitely.
220///
221/// # Example (TOML)
222///
223/// ```toml
224/// [timeouts]
225/// llm_seconds = 60
226/// embedding_seconds = 15
227/// max_parallel_tools = 4
228/// ```
229#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
230pub struct TimeoutConfig {
231    /// Timeout for streaming LLM first-token responses, in seconds. Default: `120`.
232    #[serde(default = "default_llm_timeout")]
233    pub llm_seconds: u64,
234    /// Total wall-clock timeout for a complete LLM request (all tokens), in seconds.
235    /// Default: `600`.
236    #[serde(default = "default_llm_request_timeout")]
237    pub llm_request_timeout_secs: u64,
238    /// Timeout for embedding API calls, in seconds. Default: `30`.
239    #[serde(default = "default_embedding_timeout")]
240    pub embedding_seconds: u64,
241    /// Timeout for A2A agent-to-agent calls, in seconds. Default: `30`.
242    #[serde(default = "default_a2a_timeout")]
243    pub a2a_seconds: u64,
244    /// Maximum number of tool calls that may execute concurrently in a single turn.
245    /// Default: `8`.
246    #[serde(default = "default_max_parallel_tools")]
247    pub max_parallel_tools: usize,
248}
249
250impl Default for TimeoutConfig {
251    fn default() -> Self {
252        Self {
253            llm_seconds: default_llm_timeout(),
254            llm_request_timeout_secs: default_llm_request_timeout(),
255            embedding_seconds: default_embedding_timeout(),
256            a2a_seconds: default_a2a_timeout(),
257            max_parallel_tools: default_max_parallel_tools(),
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn trust_config_default_has_scan_on_load_true() {
268        let config = TrustConfig::default();
269        assert!(config.scan_on_load);
270    }
271
272    #[test]
273    fn trust_config_serde_roundtrip_with_scan_on_load() {
274        let config = TrustConfig {
275            default_level: SkillTrustLevel::Quarantined,
276            local_level: SkillTrustLevel::Trusted,
277            hash_mismatch_level: SkillTrustLevel::Quarantined,
278            bundled_level: SkillTrustLevel::Trusted,
279            scan_on_load: false,
280            scanner: ScannerConfig::default(),
281        };
282        let toml = toml::to_string(&config).expect("serialize");
283        let deserialized: TrustConfig = toml::from_str(&toml).expect("deserialize");
284        assert!(!deserialized.scan_on_load);
285        assert_eq!(deserialized.bundled_level, SkillTrustLevel::Trusted);
286    }
287
288    #[test]
289    fn trust_config_missing_scan_on_load_defaults_to_true() {
290        let toml = r#"
291default_level = "quarantined"
292local_level = "trusted"
293hash_mismatch_level = "quarantined"
294"#;
295        let config: TrustConfig = toml::from_str(toml).expect("deserialize");
296        assert!(
297            config.scan_on_load,
298            "missing scan_on_load must default to true"
299        );
300    }
301
302    #[test]
303    fn trust_config_default_has_bundled_level_trusted() {
304        let config = TrustConfig::default();
305        assert_eq!(config.bundled_level, SkillTrustLevel::Trusted);
306    }
307
308    #[test]
309    fn trust_config_missing_bundled_level_defaults_to_trusted() {
310        let toml = r#"
311default_level = "quarantined"
312local_level = "trusted"
313hash_mismatch_level = "quarantined"
314"#;
315        let config: TrustConfig = toml::from_str(toml).expect("deserialize");
316        assert_eq!(
317            config.bundled_level,
318            SkillTrustLevel::Trusted,
319            "missing bundled_level must default to trusted"
320        );
321    }
322
323    #[test]
324    fn scanner_config_defaults() {
325        let cfg = ScannerConfig::default();
326        assert!(cfg.injection_patterns);
327        assert!(!cfg.capability_escalation_check);
328    }
329
330    #[test]
331    fn scanner_config_serde_roundtrip() {
332        let cfg = ScannerConfig {
333            injection_patterns: false,
334            capability_escalation_check: true,
335        };
336        let toml = toml::to_string(&cfg).expect("serialize");
337        let back: ScannerConfig = toml::from_str(&toml).expect("deserialize");
338        assert!(!back.injection_patterns);
339        assert!(back.capability_escalation_check);
340    }
341
342    #[test]
343    fn trust_config_scanner_defaults_when_missing() {
344        let toml = r#"
345default_level = "quarantined"
346local_level = "trusted"
347hash_mismatch_level = "quarantined"
348"#;
349        let config: TrustConfig = toml::from_str(toml).expect("deserialize");
350        assert!(config.scanner.injection_patterns);
351        assert!(!config.scanner.capability_escalation_check);
352    }
353}