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)]
206pub struct SecretMaskingConfig {
207 #[serde(default = "default_true")]
209 pub enabled: bool,
210
211 #[serde(default = "default_min_secret_len")]
216 pub min_secret_len: usize,
217}
218
219fn default_min_secret_len() -> usize {
220 8
221}
222
223impl Default for SecretMaskingConfig {
224 fn default() -> Self {
225 Self {
226 enabled: true,
227 min_secret_len: default_min_secret_len(),
228 }
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
235pub struct QuarantineConfig {
236 #[serde(default)]
238 pub enabled: bool,
239
240 #[serde(default = "default_quarantine_sources")]
242 pub sources: Vec<String>,
243
244 #[serde(default = "default_quarantine_model")]
246 pub model: String,
247
248 #[serde(default = "default_quarantine_timeout_ms")]
254 pub timeout_ms: u64,
255}
256
257fn default_quarantine_sources() -> Vec<String> {
258 vec!["web_scrape".to_owned(), "a2a_message".to_owned()]
259}
260
261fn default_quarantine_model() -> String {
262 "claude".to_owned()
263}
264
265fn default_quarantine_timeout_ms() -> u64 {
266 30_000
267}
268
269impl Default for QuarantineConfig {
270 fn default() -> Self {
271 Self {
272 enabled: false,
273 sources: default_quarantine_sources(),
274 model: default_quarantine_model(),
275 timeout_ms: default_quarantine_timeout_ms(),
276 }
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
287pub struct ExfiltrationGuardConfig {
288 #[serde(default = "default_true")]
290 pub block_markdown_images: bool,
291
292 #[serde(default = "default_true")]
294 pub validate_tool_urls: bool,
295
296 #[serde(default = "default_true")]
298 pub guard_memory_writes: bool,
299}
300
301impl Default for ExfiltrationGuardConfig {
302 fn default() -> Self {
303 Self {
304 block_markdown_images: true,
305 validate_tool_urls: true,
306 guard_memory_writes: true,
307 }
308 }
309}
310
311fn default_max_content_bytes() -> usize {
316 4096
317}
318
319fn default_max_entity_name_bytes() -> usize {
320 256
321}
322
323fn default_min_entity_name_bytes() -> usize {
324 3
325}
326
327fn default_max_fact_bytes() -> usize {
328 1024
329}
330
331fn default_max_entities() -> usize {
332 50
333}
334
335fn default_max_edges() -> usize {
336 100
337}
338
339#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
343pub struct MemoryWriteValidationConfig {
344 #[serde(default = "default_true")]
346 pub enabled: bool,
347 #[serde(default = "default_max_content_bytes")]
349 pub max_content_bytes: usize,
350 #[serde(default = "default_min_entity_name_bytes")]
352 pub min_entity_name_bytes: usize,
353 #[serde(default = "default_max_entity_name_bytes")]
355 pub max_entity_name_bytes: usize,
356 #[serde(default = "default_max_fact_bytes")]
358 pub max_fact_bytes: usize,
359 #[serde(default = "default_max_entities")]
361 pub max_entities_per_extraction: usize,
362 #[serde(default = "default_max_edges")]
364 pub max_edges_per_extraction: usize,
365 #[serde(default)]
367 pub forbidden_content_patterns: Vec<String>,
368}
369
370impl Default for MemoryWriteValidationConfig {
371 fn default() -> Self {
372 Self {
373 enabled: true,
374 max_content_bytes: default_max_content_bytes(),
375 min_entity_name_bytes: default_min_entity_name_bytes(),
376 max_entity_name_bytes: default_max_entity_name_bytes(),
377 max_fact_bytes: default_max_fact_bytes(),
378 max_entities_per_extraction: default_max_entities(),
379 max_edges_per_extraction: default_max_edges(),
380 forbidden_content_patterns: Vec::new(),
381 }
382 }
383}
384
385#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
391pub struct CustomPiiPattern {
392 pub name: String,
394 pub pattern: String,
396 #[serde(default = "default_custom_replacement")]
398 pub replacement: String,
399}
400
401fn default_custom_replacement() -> String {
402 "[PII:custom]".to_owned()
403}
404
405#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
410#[allow(clippy::struct_excessive_bools)] pub struct PiiFilterConfig {
412 #[serde(default = "default_true")]
414 pub enabled: bool,
415 #[serde(default = "default_true")]
417 pub filter_email: bool,
418 #[serde(default = "default_true")]
420 pub filter_phone: bool,
421 #[serde(default = "default_true")]
423 pub filter_ssn: bool,
424 #[serde(default = "default_true")]
426 pub filter_credit_card: bool,
427 #[serde(default)]
436 pub filter_names: bool,
437 #[serde(default)]
439 pub custom_patterns: Vec<CustomPiiPattern>,
440}
441
442impl Default for PiiFilterConfig {
443 fn default() -> Self {
444 Self {
445 enabled: true,
446 filter_email: true,
447 filter_phone: true,
448 filter_ssn: true,
449 filter_credit_card: true,
450 filter_names: false,
451 custom_patterns: Vec::new(),
452 }
453 }
454}
455
456#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
462#[serde(rename_all = "lowercase")]
463#[non_exhaustive]
464pub enum GuardrailAction {
465 #[default]
467 Block,
468 Warn,
470}
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
474#[serde(rename_all = "lowercase")]
475#[non_exhaustive]
476pub enum GuardrailFailStrategy {
477 #[default]
479 Closed,
480 Open,
482}
483
484#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
486pub struct GuardrailConfig {
487 #[serde(default)]
489 pub enabled: bool,
490 #[serde(default)]
492 pub provider: Option<String>,
493 #[serde(default)]
495 pub model: Option<String>,
496 #[serde(default = "default_guardrail_timeout_ms")]
498 pub timeout_ms: u64,
499 #[serde(default)]
501 pub action: GuardrailAction,
502 #[serde(default = "default_fail_strategy")]
504 pub fail_strategy: GuardrailFailStrategy,
505 #[serde(default)]
507 pub scan_tool_output: bool,
508 #[serde(default = "default_max_input_chars")]
510 pub max_input_chars: usize,
511}
512fn default_guardrail_timeout_ms() -> u64 {
513 500
514}
515fn default_max_input_chars() -> usize {
516 4096
517}
518fn default_fail_strategy() -> GuardrailFailStrategy {
519 GuardrailFailStrategy::Closed
520}
521impl Default for GuardrailConfig {
522 fn default() -> Self {
523 Self {
524 enabled: false,
525 provider: None,
526 model: None,
527 timeout_ms: default_guardrail_timeout_ms(),
528 action: GuardrailAction::default(),
529 fail_strategy: default_fail_strategy(),
530 scan_tool_output: false,
531 max_input_chars: default_max_input_chars(),
532 }
533 }
534}
535
536#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
546pub struct ResponseVerificationConfig {
547 #[serde(default = "default_true")]
549 pub enabled: bool,
550 #[serde(default)]
556 pub block_on_detection: bool,
557 #[serde(default)]
564 pub verifier_provider: ProviderName,
565}
566
567impl Default for ResponseVerificationConfig {
568 fn default() -> Self {
569 Self {
570 enabled: true,
571 block_on_detection: false,
572 verifier_provider: ProviderName::default(),
573 }
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 #[test]
582 fn content_isolation_default_mcp_to_acp_boundary_true() {
583 let cfg = ContentIsolationConfig::default();
584 assert!(cfg.mcp_to_acp_boundary);
585 }
586
587 #[test]
588 fn content_isolation_deserialize_mcp_to_acp_boundary_false() {
589 let toml = r"
590 mcp_to_acp_boundary = false
591 ";
592 let cfg: ContentIsolationConfig = toml::from_str(toml).unwrap();
593 assert!(!cfg.mcp_to_acp_boundary);
594 }
595
596 #[test]
597 fn content_isolation_deserialize_absent_defaults_true() {
598 let cfg: ContentIsolationConfig = toml::from_str("").unwrap();
599 assert!(cfg.mcp_to_acp_boundary);
600 }
601
602 #[test]
605 fn pii_filter_default_is_enabled() {
606 assert!(PiiFilterConfig::default().enabled);
607 }
608
609 #[test]
610 fn pii_filter_deserialize_absent_defaults_enabled_true() {
611 let cfg: PiiFilterConfig = toml::from_str("").unwrap();
613 assert!(cfg.enabled);
614 }
615
616 #[test]
617 fn pii_filter_deserialize_section_present_without_enabled_key_defaults_true() {
618 let cfg: PiiFilterConfig = toml::from_str("filter_email = false").unwrap();
621 assert!(cfg.enabled);
622 assert!(!cfg.filter_email);
623 }
624
625 #[test]
626 fn pii_filter_deserialize_explicit_false_is_respected() {
627 let cfg: PiiFilterConfig = toml::from_str("enabled = false").unwrap();
628 assert!(!cfg.enabled);
629 }
630
631 #[test]
632 fn secret_masking_default_is_enabled() {
633 assert!(SecretMaskingConfig::default().enabled);
634 }
635
636 #[test]
637 fn secret_masking_deserialize_absent_defaults_enabled_true() {
638 let cfg: SecretMaskingConfig = toml::from_str("").unwrap();
639 assert!(cfg.enabled);
640 }
641
642 #[test]
643 fn secret_masking_deserialize_section_present_without_enabled_key_defaults_true() {
644 let cfg: SecretMaskingConfig = toml::from_str("min_secret_len = 12").unwrap();
645 assert!(cfg.enabled);
646 assert_eq!(cfg.min_secret_len, 12);
647 }
648
649 #[test]
650 fn secret_masking_deserialize_explicit_false_is_respected() {
651 let cfg: SecretMaskingConfig = toml::from_str("enabled = false").unwrap();
652 assert!(!cfg.enabled);
653 }
654
655 fn de_guard(toml: &str) -> Result<EmbeddingGuardConfig, toml::de::Error> {
656 toml::from_str(toml)
657 }
658
659 #[test]
660 fn threshold_valid() {
661 let cfg = de_guard("threshold = 0.35\nmin_samples = 5").unwrap();
662 assert!((cfg.threshold - 0.35).abs() < f64::EPSILON);
663 }
664
665 #[test]
666 fn threshold_one_valid() {
667 let cfg = de_guard("threshold = 1.0\nmin_samples = 1").unwrap();
668 assert!((cfg.threshold - 1.0).abs() < f64::EPSILON);
669 }
670
671 #[test]
672 fn threshold_zero_rejected() {
673 assert!(de_guard("threshold = 0.0\nmin_samples = 1").is_err());
674 }
675
676 #[test]
677 fn threshold_above_one_rejected() {
678 assert!(de_guard("threshold = 1.5\nmin_samples = 1").is_err());
679 }
680
681 #[test]
682 fn threshold_negative_rejected() {
683 assert!(de_guard("threshold = -0.1\nmin_samples = 1").is_err());
684 }
685
686 #[test]
687 fn min_samples_zero_rejected() {
688 assert!(de_guard("threshold = 0.35\nmin_samples = 0").is_err());
689 }
690
691 #[test]
692 fn min_samples_one_valid() {
693 let cfg = de_guard("threshold = 0.35\nmin_samples = 1").unwrap();
694 assert_eq!(cfg.min_samples, 1);
695 }
696}
697
698fn default_causal_threshold() -> f32 {
703 0.7
704}
705
706fn default_probe_max_tokens() -> u32 {
707 100
708}
709
710fn default_probe_timeout_ms() -> u64 {
711 3000
712}
713
714#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
722pub struct CausalIpiConfig {
723 #[serde(default)]
725 pub enabled: bool,
726
727 #[serde(
732 default = "default_causal_threshold",
733 deserialize_with = "crate::de_helpers::de_unit_open"
734 )]
735 pub threshold: f32,
736
737 #[serde(default)]
742 pub provider: Option<String>,
743
744 #[serde(default = "default_probe_max_tokens")]
748 pub probe_max_tokens: u32,
749
750 #[serde(default = "default_probe_timeout_ms")]
754 pub probe_timeout_ms: u64,
755
756 #[serde(default)]
758 pub shadow_memory: ShadowMemoryConfig,
759}
760
761impl Default for CausalIpiConfig {
762 fn default() -> Self {
763 Self {
764 enabled: false,
765 threshold: default_causal_threshold(),
766 provider: None,
767 probe_max_tokens: default_probe_max_tokens(),
768 probe_timeout_ms: default_probe_timeout_ms(),
769 shadow_memory: ShadowMemoryConfig::default(),
770 }
771 }
772}
773
774fn default_shadow_window() -> usize {
779 8
780}
781
782fn default_shadow_max_events() -> usize {
783 64
784}
785
786fn default_shadow_drift_threshold() -> f32 {
787 0.6
788}
789
790fn validate_shadow_window<'de, D>(deserializer: D) -> Result<usize, D::Error>
791where
792 D: serde::Deserializer<'de>,
793{
794 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
795 if value == 0 {
796 return Err(serde::de::Error::custom(
797 "shadow_memory.window_size must be >= 1",
798 ));
799 }
800 Ok(value)
801}
802
803fn validate_shadow_max_events<'de, D>(deserializer: D) -> Result<usize, D::Error>
804where
805 D: serde::Deserializer<'de>,
806{
807 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
808 if value == 0 {
809 return Err(serde::de::Error::custom(
810 "shadow_memory.max_events must be >= 1",
811 ));
812 }
813 Ok(value)
814}
815
816#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
833pub struct ShadowMemoryConfig {
834 #[serde(default)]
836 pub enabled: bool,
837
838 #[serde(
840 default = "default_shadow_window",
841 deserialize_with = "validate_shadow_window"
842 )]
843 pub window_size: usize,
844
845 #[serde(
847 default = "default_shadow_max_events",
848 deserialize_with = "validate_shadow_max_events"
849 )]
850 pub max_events: usize,
851
852 #[serde(
854 default = "default_shadow_drift_threshold",
855 deserialize_with = "crate::de_helpers::de_unit_open"
856 )]
857 pub drift_threshold: f32,
858}
859
860impl Default for ShadowMemoryConfig {
861 fn default() -> Self {
862 Self {
863 enabled: false,
864 window_size: default_shadow_window(),
865 max_events: default_shadow_max_events(),
866 drift_threshold: default_shadow_drift_threshold(),
867 }
868 }
869}
870
871#[cfg(test)]
872mod causal_ipi_tests {
873 use super::*;
874
875 #[test]
876 fn causal_ipi_defaults() {
877 let cfg = CausalIpiConfig::default();
878 assert!(!cfg.enabled);
879 assert!((cfg.threshold - 0.7).abs() < 1e-6);
880 assert!(cfg.provider.is_none());
881 assert_eq!(cfg.probe_max_tokens, 100);
882 assert_eq!(cfg.probe_timeout_ms, 3000);
883 }
884
885 #[test]
886 fn causal_ipi_deserialize_enabled() {
887 let toml = r#"
888 enabled = true
889 threshold = 0.8
890 provider = "fast"
891 probe_max_tokens = 150
892 probe_timeout_ms = 5000
893 "#;
894 let cfg: CausalIpiConfig = toml::from_str(toml).unwrap();
895 assert!(cfg.enabled);
896 assert!((cfg.threshold - 0.8).abs() < 1e-6);
897 assert_eq!(cfg.provider.as_deref(), Some("fast"));
898 assert_eq!(cfg.probe_max_tokens, 150);
899 assert_eq!(cfg.probe_timeout_ms, 5000);
900 }
901
902 #[test]
903 fn causal_ipi_threshold_zero_rejected() {
904 let result: Result<CausalIpiConfig, _> = toml::from_str("threshold = 0.0");
905 assert!(result.is_err());
906 }
907
908 #[test]
909 fn causal_ipi_threshold_above_one_rejected() {
910 let result: Result<CausalIpiConfig, _> = toml::from_str("threshold = 1.1");
911 assert!(result.is_err());
912 }
913
914 #[test]
915 fn causal_ipi_threshold_exactly_one_accepted() {
916 let cfg: CausalIpiConfig = toml::from_str("threshold = 1.0").unwrap();
917 assert!((cfg.threshold - 1.0).abs() < 1e-6);
918 }
919}
920
921#[cfg(test)]
922mod shadow_memory_config_tests {
923 use super::*;
924
925 #[test]
926 fn shadow_memory_defaults() {
927 let cfg = ShadowMemoryConfig::default();
928 assert!(!cfg.enabled);
929 assert_eq!(cfg.window_size, 8);
930 assert_eq!(cfg.max_events, 64);
931 assert!((cfg.drift_threshold - 0.6).abs() < 1e-6);
932 }
933
934 #[test]
935 fn shadow_memory_window_zero_rejected() {
936 let result: Result<ShadowMemoryConfig, _> = toml::from_str("window_size = 0");
937 assert!(result.is_err());
938 }
939
940 #[test]
941 fn shadow_memory_max_events_zero_rejected() {
942 let result: Result<ShadowMemoryConfig, _> = toml::from_str("max_events = 0");
943 assert!(result.is_err());
944 }
945
946 #[test]
947 fn shadow_memory_drift_threshold_zero_rejected() {
948 let result: Result<ShadowMemoryConfig, _> = toml::from_str("drift_threshold = 0.0");
949 assert!(result.is_err());
950 }
951
952 #[test]
953 fn shadow_memory_drift_threshold_above_one_rejected() {
954 let result: Result<ShadowMemoryConfig, _> = toml::from_str("drift_threshold = 1.1");
955 assert!(result.is_err());
956 }
957
958 #[test]
959 fn shadow_memory_drift_threshold_exactly_one_accepted() {
960 let cfg: ShadowMemoryConfig = toml::from_str("drift_threshold = 1.0").unwrap();
961 assert!((cfg.drift_threshold - 1.0).abs() < 1e-6);
962 }
963
964 #[test]
965 fn shadow_memory_full_deserialization() {
966 let toml = r"
967 enabled = true
968 window_size = 4
969 max_events = 32
970 drift_threshold = 0.8
971 ";
972 let cfg: ShadowMemoryConfig = toml::from_str(toml).unwrap();
973 assert!(cfg.enabled);
974 assert_eq!(cfg.window_size, 4);
975 assert_eq!(cfg.max_events, 32);
976 assert!((cfg.drift_threshold - 0.8).abs() < 1e-6);
977 }
978}