Skip to main content

zeph_config/
sanitizer.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use serde::{Deserialize, Serialize};
5
6use crate::defaults::default_true;
7
8// ---------------------------------------------------------------------------
9// ContentIsolationConfig
10// ---------------------------------------------------------------------------
11
12fn default_max_content_size() -> usize {
13    65_536
14}
15
16/// Configuration for the content isolation pipeline, nested under
17/// `[security.content_isolation]` in the agent config file.
18#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
19pub struct ContentIsolationConfig {
20    /// When `false`, the sanitizer is a no-op: content passes through unchanged.
21    #[serde(default = "default_true")]
22    pub enabled: bool,
23
24    /// Maximum byte length of untrusted content before truncation.
25    #[serde(default = "default_max_content_size")]
26    pub max_content_size: usize,
27
28    /// When `true`, injection patterns detected in content are recorded as
29    /// flags and a warning is prepended to the spotlighting wrapper.
30    #[serde(default = "default_true")]
31    pub flag_injection_patterns: bool,
32
33    /// When `true`, untrusted content is wrapped in spotlighting XML delimiters
34    /// that instruct the LLM to treat the enclosed text as data, not instructions.
35    #[serde(default = "default_true")]
36    pub spotlight_untrusted: bool,
37
38    /// Quarantine summarizer configuration.
39    #[serde(default)]
40    pub quarantine: QuarantineConfig,
41}
42
43impl Default for ContentIsolationConfig {
44    fn default() -> Self {
45        Self {
46            enabled: true,
47            max_content_size: default_max_content_size(),
48            flag_injection_patterns: true,
49            spotlight_untrusted: true,
50            quarantine: QuarantineConfig::default(),
51        }
52    }
53}
54
55/// Configuration for the quarantine summarizer, nested under
56/// `[security.content_isolation.quarantine]` in the agent config file.
57#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
58pub struct QuarantineConfig {
59    /// When `false`, quarantine summarization is disabled entirely.
60    #[serde(default)]
61    pub enabled: bool,
62
63    /// Source kinds to route through the quarantine LLM.
64    #[serde(default = "default_quarantine_sources")]
65    pub sources: Vec<String>,
66
67    /// Provider name passed to `create_named_provider`.
68    #[serde(default = "default_quarantine_model")]
69    pub model: String,
70}
71
72fn default_quarantine_sources() -> Vec<String> {
73    vec!["web_scrape".to_owned(), "a2a_message".to_owned()]
74}
75
76fn default_quarantine_model() -> String {
77    "claude".to_owned()
78}
79
80impl Default for QuarantineConfig {
81    fn default() -> Self {
82        Self {
83            enabled: false,
84            sources: default_quarantine_sources(),
85            model: default_quarantine_model(),
86        }
87    }
88}
89
90// ---------------------------------------------------------------------------
91// ExfiltrationGuardConfig
92// ---------------------------------------------------------------------------
93
94/// Configuration for exfiltration guards, nested under
95/// `[security.exfiltration_guard]` in the agent config file.
96#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
97pub struct ExfiltrationGuardConfig {
98    /// Strip external markdown images from LLM output to prevent pixel-tracking exfiltration.
99    #[serde(default = "default_true")]
100    pub block_markdown_images: bool,
101
102    /// Cross-reference tool call arguments against URLs seen in flagged untrusted content.
103    #[serde(default = "default_true")]
104    pub validate_tool_urls: bool,
105
106    /// Skip Qdrant embedding for messages that contained injection-flagged content.
107    #[serde(default = "default_true")]
108    pub guard_memory_writes: bool,
109}
110
111impl Default for ExfiltrationGuardConfig {
112    fn default() -> Self {
113        Self {
114            block_markdown_images: true,
115            validate_tool_urls: true,
116            guard_memory_writes: true,
117        }
118    }
119}
120
121// ---------------------------------------------------------------------------
122// MemoryWriteValidationConfig
123// ---------------------------------------------------------------------------
124
125fn default_max_content_bytes() -> usize {
126    4096
127}
128
129fn default_max_entity_name_bytes() -> usize {
130    256
131}
132
133fn default_min_entity_name_bytes() -> usize {
134    3
135}
136
137fn default_max_fact_bytes() -> usize {
138    1024
139}
140
141fn default_max_entities() -> usize {
142    50
143}
144
145fn default_max_edges() -> usize {
146    100
147}
148
149/// Configuration for memory write validation, nested under `[security.memory_validation]`.
150///
151/// Enabled by default with conservative limits.
152#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
153pub struct MemoryWriteValidationConfig {
154    /// Master switch. When `false`, validation is a no-op.
155    #[serde(default = "default_true")]
156    pub enabled: bool,
157    /// Maximum byte length of content passed to `memory_save`.
158    #[serde(default = "default_max_content_bytes")]
159    pub max_content_bytes: usize,
160    /// Minimum byte length of an entity name in graph extraction.
161    #[serde(default = "default_min_entity_name_bytes")]
162    pub min_entity_name_bytes: usize,
163    /// Maximum byte length of a single entity name in graph extraction.
164    #[serde(default = "default_max_entity_name_bytes")]
165    pub max_entity_name_bytes: usize,
166    /// Maximum byte length of an edge fact string in graph extraction.
167    #[serde(default = "default_max_fact_bytes")]
168    pub max_fact_bytes: usize,
169    /// Maximum number of entities allowed per graph extraction result.
170    #[serde(default = "default_max_entities")]
171    pub max_entities_per_extraction: usize,
172    /// Maximum number of edges allowed per graph extraction result.
173    #[serde(default = "default_max_edges")]
174    pub max_edges_per_extraction: usize,
175    /// Forbidden substring patterns.
176    #[serde(default)]
177    pub forbidden_content_patterns: Vec<String>,
178}
179
180impl Default for MemoryWriteValidationConfig {
181    fn default() -> Self {
182        Self {
183            enabled: true,
184            max_content_bytes: default_max_content_bytes(),
185            min_entity_name_bytes: default_min_entity_name_bytes(),
186            max_entity_name_bytes: default_max_entity_name_bytes(),
187            max_fact_bytes: default_max_fact_bytes(),
188            max_entities_per_extraction: default_max_entities(),
189            max_edges_per_extraction: default_max_edges(),
190            forbidden_content_patterns: Vec::new(),
191        }
192    }
193}
194
195// ---------------------------------------------------------------------------
196// PiiFilterConfig
197// ---------------------------------------------------------------------------
198
199/// A single user-defined PII pattern.
200#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
201pub struct CustomPiiPattern {
202    /// Human-readable name used in the replacement label.
203    pub name: String,
204    /// Regular expression pattern.
205    pub pattern: String,
206    /// Replacement text. Defaults to `[PII:custom]`.
207    #[serde(default = "default_custom_replacement")]
208    pub replacement: String,
209}
210
211fn default_custom_replacement() -> String {
212    "[PII:custom]".to_owned()
213}
214
215/// Configuration for the PII filter, nested under `[security.pii_filter]` in the config file.
216///
217/// Disabled by default — opt-in to avoid unexpected data loss.
218#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
219#[allow(clippy::struct_excessive_bools)]
220pub struct PiiFilterConfig {
221    /// Master switch. When `false`, the filter is a no-op.
222    #[serde(default)]
223    pub enabled: bool,
224    /// Scrub email addresses.
225    #[serde(default = "default_true")]
226    pub filter_email: bool,
227    /// Scrub US phone numbers.
228    #[serde(default = "default_true")]
229    pub filter_phone: bool,
230    /// Scrub US Social Security Numbers.
231    #[serde(default = "default_true")]
232    pub filter_ssn: bool,
233    /// Scrub credit card numbers (16-digit patterns).
234    #[serde(default = "default_true")]
235    pub filter_credit_card: bool,
236    /// Custom regex patterns to add on top of the built-ins.
237    #[serde(default)]
238    pub custom_patterns: Vec<CustomPiiPattern>,
239}
240
241impl Default for PiiFilterConfig {
242    fn default() -> Self {
243        Self {
244            enabled: false,
245            filter_email: true,
246            filter_phone: true,
247            filter_ssn: true,
248            filter_credit_card: true,
249            custom_patterns: Vec::new(),
250        }
251    }
252}
253
254// ---------------------------------------------------------------------------
255// GuardrailConfig
256// ---------------------------------------------------------------------------
257
258/// What happens when the guardrail flags input.
259#[cfg(feature = "guardrail")]
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
261#[serde(rename_all = "lowercase")]
262pub enum GuardrailAction {
263    /// Block the input and return an error message to the user.
264    #[default]
265    Block,
266    /// Allow the input but emit a warning message.
267    Warn,
268}
269
270/// Behavior on timeout or LLM error.
271#[cfg(feature = "guardrail")]
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
273#[serde(rename_all = "lowercase")]
274pub enum GuardrailFailStrategy {
275    /// Block input on timeout/error (safe default for security-sensitive deployments).
276    #[default]
277    Closed,
278    /// Allow input on timeout/error (for availability-sensitive deployments).
279    Open,
280}
281
282/// Configuration for the LLM-based guardrail, nested under `[security.guardrail]`.
283#[cfg(feature = "guardrail")]
284#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
285pub struct GuardrailConfig {
286    /// Enable the guardrail (default: false).
287    #[serde(default)]
288    pub enabled: bool,
289    /// Provider to use for guardrail classification (e.g. `"ollama"`, `"claude"`).
290    #[serde(default)]
291    pub provider: Option<String>,
292    /// Model to use (e.g. `"llama-guard-3:1b"`).
293    #[serde(default)]
294    pub model: Option<String>,
295    /// Timeout for each guardrail LLM call in milliseconds (default: 500).
296    #[serde(default = "default_guardrail_timeout_ms")]
297    pub timeout_ms: u64,
298    /// Action to take when a message is flagged (default: block).
299    #[serde(default)]
300    pub action: GuardrailAction,
301    /// What to do on timeout or LLM error (default: closed — block).
302    #[serde(default = "default_fail_strategy")]
303    pub fail_strategy: GuardrailFailStrategy,
304    /// When `true`, also scan tool outputs before they enter message history (default: false).
305    #[serde(default)]
306    pub scan_tool_output: bool,
307    /// Maximum number of characters to send to the guard model (default: 4096).
308    #[serde(default = "default_max_input_chars")]
309    pub max_input_chars: usize,
310}
311
312#[cfg(feature = "guardrail")]
313fn default_guardrail_timeout_ms() -> u64 {
314    500
315}
316
317#[cfg(feature = "guardrail")]
318fn default_max_input_chars() -> usize {
319    4096
320}
321
322#[cfg(feature = "guardrail")]
323fn default_fail_strategy() -> GuardrailFailStrategy {
324    GuardrailFailStrategy::Closed
325}
326
327#[cfg(feature = "guardrail")]
328impl Default for GuardrailConfig {
329    fn default() -> Self {
330        Self {
331            enabled: false,
332            provider: None,
333            model: None,
334            timeout_ms: default_guardrail_timeout_ms(),
335            action: GuardrailAction::default(),
336            fail_strategy: default_fail_strategy(),
337            scan_tool_output: false,
338            max_input_chars: default_max_input_chars(),
339        }
340    }
341}
342
343// ---------------------------------------------------------------------------
344// ResponseVerificationConfig
345// ---------------------------------------------------------------------------
346
347/// Configuration for post-LLM response verification, nested under
348/// `[security.response_verification]` in the agent config file.
349///
350/// Scans LLM responses for injected instruction patterns before tool dispatch.
351/// This is defense-in-depth layer 3 (after input sanitization and pre-execution verification).
352#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
353pub struct ResponseVerificationConfig {
354    /// Enable post-LLM response verification (default: true).
355    #[serde(default = "default_true")]
356    pub enabled: bool,
357    /// Block tool dispatch when injection patterns are detected (default: false).
358    ///
359    /// When `false`, flagged responses are logged and shown in the TUI SEC panel
360    /// but still delivered. When `true`, the response is suppressed and the user
361    /// is notified.
362    #[serde(default)]
363    pub block_on_detection: bool,
364}
365
366impl Default for ResponseVerificationConfig {
367    fn default() -> Self {
368        Self {
369            enabled: true,
370            block_on_detection: false,
371        }
372    }
373}