1use crate::providers::ProviderName;
5use serde::{Deserialize, Serialize};
6
7use crate::defaults::default_true;
8
9fn default_max_content_size() -> usize {
14 65_536
15}
16
17#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
20pub struct EmbeddingGuardConfig {
21 #[serde(default)]
23 pub enabled: bool,
24 #[serde(
26 default = "default_embedding_threshold",
27 deserialize_with = "crate::de_helpers::de_unit_open"
28 )]
29 pub threshold: f64,
30 #[serde(
33 default = "default_embedding_min_samples",
34 deserialize_with = "validate_min_samples"
35 )]
36 pub min_samples: usize,
37 #[serde(default = "default_ema_floor")]
44 pub ema_floor: f32,
45}
46
47fn validate_min_samples<'de, D>(deserializer: D) -> Result<usize, D::Error>
48where
49 D: serde::Deserializer<'de>,
50{
51 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
52 if value == 0 {
53 return Err(serde::de::Error::custom(
54 "embedding_guard.min_samples must be >= 1",
55 ));
56 }
57 Ok(value)
58}
59
60fn default_embedding_threshold() -> f64 {
61 0.35
62}
63
64fn default_embedding_min_samples() -> usize {
65 10
66}
67
68fn default_ema_floor() -> f32 {
69 0.01
70}
71
72impl Default for EmbeddingGuardConfig {
73 fn default() -> Self {
74 Self {
75 enabled: false,
76 threshold: default_embedding_threshold(),
77 min_samples: default_embedding_min_samples(),
78 ema_floor: default_ema_floor(),
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
86#[allow(clippy::struct_excessive_bools)] pub struct ContentIsolationConfig {
88 #[serde(default = "default_true")]
90 pub enabled: bool,
91
92 #[serde(default = "default_max_content_size")]
94 pub max_content_size: usize,
95
96 #[serde(default = "default_true")]
99 pub flag_injection_patterns: bool,
100
101 #[serde(default = "default_true")]
104 pub spotlight_untrusted: bool,
105
106 #[serde(default)]
108 pub quarantine: QuarantineConfig,
109
110 #[serde(default)]
112 pub embedding_guard: EmbeddingGuardConfig,
113
114 #[serde(default = "default_true")]
119 pub mcp_to_acp_boundary: bool,
120
121 #[serde(default)]
123 pub nli: NliConfig,
124
125 #[serde(default)]
127 pub secret_masking: SecretMaskingConfig,
128}
129
130impl Default for ContentIsolationConfig {
131 fn default() -> Self {
132 Self {
133 enabled: true,
134 max_content_size: default_max_content_size(),
135 flag_injection_patterns: true,
136 spotlight_untrusted: true,
137 quarantine: QuarantineConfig::default(),
138 embedding_guard: EmbeddingGuardConfig::default(),
139 mcp_to_acp_boundary: true,
140 nli: NliConfig::default(),
141 secret_masking: SecretMaskingConfig::default(),
142 }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
151pub struct NliConfig {
152 #[serde(default)]
154 pub enabled: bool,
155
156 #[serde(default)]
160 pub provider: ProviderName,
161
162 #[serde(default = "default_nli_threshold")]
164 pub threshold: f32,
165
166 #[serde(default = "default_nli_timeout_ms")]
168 pub timeout_ms: u64,
169
170 #[serde(default = "default_nli_max_content_len")]
172 pub max_content_len: usize,
173}
174
175fn default_nli_threshold() -> f32 {
176 0.75
177}
178
179fn default_nli_timeout_ms() -> u64 {
180 5000
181}
182
183fn default_nli_max_content_len() -> usize {
184 2048
185}
186
187impl Default for NliConfig {
188 fn default() -> Self {
189 Self {
190 enabled: false,
191 provider: ProviderName::default(),
192 threshold: default_nli_threshold(),
193 timeout_ms: default_nli_timeout_ms(),
194 max_content_len: default_nli_max_content_len(),
195 }
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
204pub struct SecretMaskingConfig {
205 #[serde(default)]
207 pub enabled: bool,
208
209 #[serde(default = "default_min_secret_len")]
214 pub min_secret_len: usize,
215}
216
217fn default_min_secret_len() -> usize {
218 8
219}
220
221impl Default for SecretMaskingConfig {
222 fn default() -> Self {
223 Self {
224 enabled: false,
225 min_secret_len: default_min_secret_len(),
226 }
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
233pub struct QuarantineConfig {
234 #[serde(default)]
236 pub enabled: bool,
237
238 #[serde(default = "default_quarantine_sources")]
240 pub sources: Vec<String>,
241
242 #[serde(default = "default_quarantine_model")]
244 pub model: String,
245
246 #[serde(default = "default_quarantine_timeout_ms")]
252 pub timeout_ms: u64,
253}
254
255fn default_quarantine_sources() -> Vec<String> {
256 vec!["web_scrape".to_owned(), "a2a_message".to_owned()]
257}
258
259fn default_quarantine_model() -> String {
260 "claude".to_owned()
261}
262
263fn default_quarantine_timeout_ms() -> u64 {
264 30_000
265}
266
267impl Default for QuarantineConfig {
268 fn default() -> Self {
269 Self {
270 enabled: false,
271 sources: default_quarantine_sources(),
272 model: default_quarantine_model(),
273 timeout_ms: default_quarantine_timeout_ms(),
274 }
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
285pub struct ExfiltrationGuardConfig {
286 #[serde(default = "default_true")]
288 pub block_markdown_images: bool,
289
290 #[serde(default = "default_true")]
292 pub validate_tool_urls: bool,
293
294 #[serde(default = "default_true")]
296 pub guard_memory_writes: bool,
297}
298
299impl Default for ExfiltrationGuardConfig {
300 fn default() -> Self {
301 Self {
302 block_markdown_images: true,
303 validate_tool_urls: true,
304 guard_memory_writes: true,
305 }
306 }
307}
308
309fn default_max_content_bytes() -> usize {
314 4096
315}
316
317fn default_max_entity_name_bytes() -> usize {
318 256
319}
320
321fn default_min_entity_name_bytes() -> usize {
322 3
323}
324
325fn default_max_fact_bytes() -> usize {
326 1024
327}
328
329fn default_max_entities() -> usize {
330 50
331}
332
333fn default_max_edges() -> usize {
334 100
335}
336
337#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
341pub struct MemoryWriteValidationConfig {
342 #[serde(default = "default_true")]
344 pub enabled: bool,
345 #[serde(default = "default_max_content_bytes")]
347 pub max_content_bytes: usize,
348 #[serde(default = "default_min_entity_name_bytes")]
350 pub min_entity_name_bytes: usize,
351 #[serde(default = "default_max_entity_name_bytes")]
353 pub max_entity_name_bytes: usize,
354 #[serde(default = "default_max_fact_bytes")]
356 pub max_fact_bytes: usize,
357 #[serde(default = "default_max_entities")]
359 pub max_entities_per_extraction: usize,
360 #[serde(default = "default_max_edges")]
362 pub max_edges_per_extraction: usize,
363 #[serde(default)]
365 pub forbidden_content_patterns: Vec<String>,
366}
367
368impl Default for MemoryWriteValidationConfig {
369 fn default() -> Self {
370 Self {
371 enabled: true,
372 max_content_bytes: default_max_content_bytes(),
373 min_entity_name_bytes: default_min_entity_name_bytes(),
374 max_entity_name_bytes: default_max_entity_name_bytes(),
375 max_fact_bytes: default_max_fact_bytes(),
376 max_entities_per_extraction: default_max_entities(),
377 max_edges_per_extraction: default_max_edges(),
378 forbidden_content_patterns: Vec::new(),
379 }
380 }
381}
382
383#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
389pub struct CustomPiiPattern {
390 pub name: String,
392 pub pattern: String,
394 #[serde(default = "default_custom_replacement")]
396 pub replacement: String,
397}
398
399fn default_custom_replacement() -> String {
400 "[PII:custom]".to_owned()
401}
402
403#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
407#[allow(clippy::struct_excessive_bools)] pub struct PiiFilterConfig {
409 #[serde(default)]
411 pub enabled: bool,
412 #[serde(default = "default_true")]
414 pub filter_email: bool,
415 #[serde(default = "default_true")]
417 pub filter_phone: bool,
418 #[serde(default = "default_true")]
420 pub filter_ssn: bool,
421 #[serde(default = "default_true")]
423 pub filter_credit_card: bool,
424 #[serde(default)]
433 pub filter_names: bool,
434 #[serde(default)]
436 pub custom_patterns: Vec<CustomPiiPattern>,
437}
438
439impl Default for PiiFilterConfig {
440 fn default() -> Self {
441 Self {
442 enabled: false,
443 filter_email: true,
444 filter_phone: true,
445 filter_ssn: true,
446 filter_credit_card: true,
447 filter_names: false,
448 custom_patterns: Vec::new(),
449 }
450 }
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
459#[serde(rename_all = "lowercase")]
460#[non_exhaustive]
461pub enum GuardrailAction {
462 #[default]
464 Block,
465 Warn,
467}
468
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
471#[serde(rename_all = "lowercase")]
472#[non_exhaustive]
473pub enum GuardrailFailStrategy {
474 #[default]
476 Closed,
477 Open,
479}
480
481#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
483pub struct GuardrailConfig {
484 #[serde(default)]
486 pub enabled: bool,
487 #[serde(default)]
489 pub provider: Option<String>,
490 #[serde(default)]
492 pub model: Option<String>,
493 #[serde(default = "default_guardrail_timeout_ms")]
495 pub timeout_ms: u64,
496 #[serde(default)]
498 pub action: GuardrailAction,
499 #[serde(default = "default_fail_strategy")]
501 pub fail_strategy: GuardrailFailStrategy,
502 #[serde(default)]
504 pub scan_tool_output: bool,
505 #[serde(default = "default_max_input_chars")]
507 pub max_input_chars: usize,
508}
509fn default_guardrail_timeout_ms() -> u64 {
510 500
511}
512fn default_max_input_chars() -> usize {
513 4096
514}
515fn default_fail_strategy() -> GuardrailFailStrategy {
516 GuardrailFailStrategy::Closed
517}
518impl Default for GuardrailConfig {
519 fn default() -> Self {
520 Self {
521 enabled: false,
522 provider: None,
523 model: None,
524 timeout_ms: default_guardrail_timeout_ms(),
525 action: GuardrailAction::default(),
526 fail_strategy: default_fail_strategy(),
527 scan_tool_output: false,
528 max_input_chars: default_max_input_chars(),
529 }
530 }
531}
532
533#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
543pub struct ResponseVerificationConfig {
544 #[serde(default = "default_true")]
546 pub enabled: bool,
547 #[serde(default)]
553 pub block_on_detection: bool,
554 #[serde(default)]
561 pub verifier_provider: ProviderName,
562}
563
564impl Default for ResponseVerificationConfig {
565 fn default() -> Self {
566 Self {
567 enabled: true,
568 block_on_detection: false,
569 verifier_provider: ProviderName::default(),
570 }
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 #[test]
579 fn content_isolation_default_mcp_to_acp_boundary_true() {
580 let cfg = ContentIsolationConfig::default();
581 assert!(cfg.mcp_to_acp_boundary);
582 }
583
584 #[test]
585 fn content_isolation_deserialize_mcp_to_acp_boundary_false() {
586 let toml = r"
587 mcp_to_acp_boundary = false
588 ";
589 let cfg: ContentIsolationConfig = toml::from_str(toml).unwrap();
590 assert!(!cfg.mcp_to_acp_boundary);
591 }
592
593 #[test]
594 fn content_isolation_deserialize_absent_defaults_true() {
595 let cfg: ContentIsolationConfig = toml::from_str("").unwrap();
596 assert!(cfg.mcp_to_acp_boundary);
597 }
598
599 fn de_guard(toml: &str) -> Result<EmbeddingGuardConfig, toml::de::Error> {
600 toml::from_str(toml)
601 }
602
603 #[test]
604 fn threshold_valid() {
605 let cfg = de_guard("threshold = 0.35\nmin_samples = 5").unwrap();
606 assert!((cfg.threshold - 0.35).abs() < f64::EPSILON);
607 }
608
609 #[test]
610 fn threshold_one_valid() {
611 let cfg = de_guard("threshold = 1.0\nmin_samples = 1").unwrap();
612 assert!((cfg.threshold - 1.0).abs() < f64::EPSILON);
613 }
614
615 #[test]
616 fn threshold_zero_rejected() {
617 assert!(de_guard("threshold = 0.0\nmin_samples = 1").is_err());
618 }
619
620 #[test]
621 fn threshold_above_one_rejected() {
622 assert!(de_guard("threshold = 1.5\nmin_samples = 1").is_err());
623 }
624
625 #[test]
626 fn threshold_negative_rejected() {
627 assert!(de_guard("threshold = -0.1\nmin_samples = 1").is_err());
628 }
629
630 #[test]
631 fn min_samples_zero_rejected() {
632 assert!(de_guard("threshold = 0.35\nmin_samples = 0").is_err());
633 }
634
635 #[test]
636 fn min_samples_one_valid() {
637 let cfg = de_guard("threshold = 0.35\nmin_samples = 1").unwrap();
638 assert_eq!(cfg.min_samples, 1);
639 }
640}
641
642fn default_causal_threshold() -> f32 {
647 0.7
648}
649
650fn default_probe_max_tokens() -> u32 {
651 100
652}
653
654fn default_probe_timeout_ms() -> u64 {
655 3000
656}
657
658#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
666pub struct CausalIpiConfig {
667 #[serde(default)]
669 pub enabled: bool,
670
671 #[serde(
676 default = "default_causal_threshold",
677 deserialize_with = "crate::de_helpers::de_unit_open"
678 )]
679 pub threshold: f32,
680
681 #[serde(default)]
686 pub provider: Option<String>,
687
688 #[serde(default = "default_probe_max_tokens")]
692 pub probe_max_tokens: u32,
693
694 #[serde(default = "default_probe_timeout_ms")]
698 pub probe_timeout_ms: u64,
699
700 #[serde(default)]
702 pub shadow_memory: ShadowMemoryConfig,
703}
704
705impl Default for CausalIpiConfig {
706 fn default() -> Self {
707 Self {
708 enabled: false,
709 threshold: default_causal_threshold(),
710 provider: None,
711 probe_max_tokens: default_probe_max_tokens(),
712 probe_timeout_ms: default_probe_timeout_ms(),
713 shadow_memory: ShadowMemoryConfig::default(),
714 }
715 }
716}
717
718fn default_shadow_window() -> usize {
723 8
724}
725
726fn default_shadow_max_events() -> usize {
727 64
728}
729
730fn default_shadow_drift_threshold() -> f32 {
731 0.6
732}
733
734fn validate_shadow_window<'de, D>(deserializer: D) -> Result<usize, D::Error>
735where
736 D: serde::Deserializer<'de>,
737{
738 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
739 if value == 0 {
740 return Err(serde::de::Error::custom(
741 "shadow_memory.window_size must be >= 1",
742 ));
743 }
744 Ok(value)
745}
746
747fn validate_shadow_max_events<'de, D>(deserializer: D) -> Result<usize, D::Error>
748where
749 D: serde::Deserializer<'de>,
750{
751 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
752 if value == 0 {
753 return Err(serde::de::Error::custom(
754 "shadow_memory.max_events must be >= 1",
755 ));
756 }
757 Ok(value)
758}
759
760#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
777pub struct ShadowMemoryConfig {
778 #[serde(default)]
780 pub enabled: bool,
781
782 #[serde(
784 default = "default_shadow_window",
785 deserialize_with = "validate_shadow_window"
786 )]
787 pub window_size: usize,
788
789 #[serde(
791 default = "default_shadow_max_events",
792 deserialize_with = "validate_shadow_max_events"
793 )]
794 pub max_events: usize,
795
796 #[serde(
798 default = "default_shadow_drift_threshold",
799 deserialize_with = "crate::de_helpers::de_unit_open"
800 )]
801 pub drift_threshold: f32,
802}
803
804impl Default for ShadowMemoryConfig {
805 fn default() -> Self {
806 Self {
807 enabled: false,
808 window_size: default_shadow_window(),
809 max_events: default_shadow_max_events(),
810 drift_threshold: default_shadow_drift_threshold(),
811 }
812 }
813}
814
815#[cfg(test)]
816mod causal_ipi_tests {
817 use super::*;
818
819 #[test]
820 fn causal_ipi_defaults() {
821 let cfg = CausalIpiConfig::default();
822 assert!(!cfg.enabled);
823 assert!((cfg.threshold - 0.7).abs() < 1e-6);
824 assert!(cfg.provider.is_none());
825 assert_eq!(cfg.probe_max_tokens, 100);
826 assert_eq!(cfg.probe_timeout_ms, 3000);
827 }
828
829 #[test]
830 fn causal_ipi_deserialize_enabled() {
831 let toml = r#"
832 enabled = true
833 threshold = 0.8
834 provider = "fast"
835 probe_max_tokens = 150
836 probe_timeout_ms = 5000
837 "#;
838 let cfg: CausalIpiConfig = toml::from_str(toml).unwrap();
839 assert!(cfg.enabled);
840 assert!((cfg.threshold - 0.8).abs() < 1e-6);
841 assert_eq!(cfg.provider.as_deref(), Some("fast"));
842 assert_eq!(cfg.probe_max_tokens, 150);
843 assert_eq!(cfg.probe_timeout_ms, 5000);
844 }
845
846 #[test]
847 fn causal_ipi_threshold_zero_rejected() {
848 let result: Result<CausalIpiConfig, _> = toml::from_str("threshold = 0.0");
849 assert!(result.is_err());
850 }
851
852 #[test]
853 fn causal_ipi_threshold_above_one_rejected() {
854 let result: Result<CausalIpiConfig, _> = toml::from_str("threshold = 1.1");
855 assert!(result.is_err());
856 }
857
858 #[test]
859 fn causal_ipi_threshold_exactly_one_accepted() {
860 let cfg: CausalIpiConfig = toml::from_str("threshold = 1.0").unwrap();
861 assert!((cfg.threshold - 1.0).abs() < 1e-6);
862 }
863}
864
865#[cfg(test)]
866mod shadow_memory_config_tests {
867 use super::*;
868
869 #[test]
870 fn shadow_memory_defaults() {
871 let cfg = ShadowMemoryConfig::default();
872 assert!(!cfg.enabled);
873 assert_eq!(cfg.window_size, 8);
874 assert_eq!(cfg.max_events, 64);
875 assert!((cfg.drift_threshold - 0.6).abs() < 1e-6);
876 }
877
878 #[test]
879 fn shadow_memory_window_zero_rejected() {
880 let result: Result<ShadowMemoryConfig, _> = toml::from_str("window_size = 0");
881 assert!(result.is_err());
882 }
883
884 #[test]
885 fn shadow_memory_max_events_zero_rejected() {
886 let result: Result<ShadowMemoryConfig, _> = toml::from_str("max_events = 0");
887 assert!(result.is_err());
888 }
889
890 #[test]
891 fn shadow_memory_drift_threshold_zero_rejected() {
892 let result: Result<ShadowMemoryConfig, _> = toml::from_str("drift_threshold = 0.0");
893 assert!(result.is_err());
894 }
895
896 #[test]
897 fn shadow_memory_drift_threshold_above_one_rejected() {
898 let result: Result<ShadowMemoryConfig, _> = toml::from_str("drift_threshold = 1.1");
899 assert!(result.is_err());
900 }
901
902 #[test]
903 fn shadow_memory_drift_threshold_exactly_one_accepted() {
904 let cfg: ShadowMemoryConfig = toml::from_str("drift_threshold = 1.0").unwrap();
905 assert!((cfg.drift_threshold - 1.0).abs() < 1e-6);
906 }
907
908 #[test]
909 fn shadow_memory_full_deserialization() {
910 let toml = r"
911 enabled = true
912 window_size = 4
913 max_events = 32
914 drift_threshold = 0.8
915 ";
916 let cfg: ShadowMemoryConfig = toml::from_str(toml).unwrap();
917 assert!(cfg.enabled);
918 assert_eq!(cfg.window_size, 4);
919 assert_eq!(cfg.max_events, 32);
920 assert!((cfg.drift_threshold - 0.8).abs() < 1e-6);
921 }
922}