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 #[serde(default = "default_fail_strategy")]
265 pub fail_strategy: GuardrailFailStrategy,
266}
267
268fn default_quarantine_sources() -> Vec<String> {
269 vec!["web_scrape".to_owned(), "a2a_message".to_owned()]
270}
271
272fn default_quarantine_model() -> String {
273 "claude".to_owned()
274}
275
276fn default_quarantine_timeout_ms() -> u64 {
277 30_000
278}
279
280impl Default for QuarantineConfig {
281 fn default() -> Self {
282 Self {
283 enabled: false,
284 sources: default_quarantine_sources(),
285 model: default_quarantine_model(),
286 timeout_ms: default_quarantine_timeout_ms(),
287 fail_strategy: default_fail_strategy(),
288 }
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
299pub struct ExfiltrationGuardConfig {
300 #[serde(default = "default_true")]
302 pub block_markdown_images: bool,
303
304 #[serde(default = "default_true")]
306 pub validate_tool_urls: bool,
307
308 #[serde(default = "default_true")]
310 pub guard_memory_writes: bool,
311}
312
313impl Default for ExfiltrationGuardConfig {
314 fn default() -> Self {
315 Self {
316 block_markdown_images: true,
317 validate_tool_urls: true,
318 guard_memory_writes: true,
319 }
320 }
321}
322
323fn default_max_content_bytes() -> usize {
328 4096
329}
330
331fn default_max_entity_name_bytes() -> usize {
332 256
333}
334
335fn default_min_entity_name_bytes() -> usize {
336 3
337}
338
339fn default_max_fact_bytes() -> usize {
340 1024
341}
342
343fn default_max_entities() -> usize {
344 50
345}
346
347fn default_max_edges() -> usize {
348 100
349}
350
351#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
355pub struct MemoryWriteValidationConfig {
356 #[serde(default = "default_true")]
358 pub enabled: bool,
359 #[serde(default = "default_max_content_bytes")]
361 pub max_content_bytes: usize,
362 #[serde(default = "default_min_entity_name_bytes")]
364 pub min_entity_name_bytes: usize,
365 #[serde(default = "default_max_entity_name_bytes")]
367 pub max_entity_name_bytes: usize,
368 #[serde(default = "default_max_fact_bytes")]
370 pub max_fact_bytes: usize,
371 #[serde(default = "default_max_entities")]
373 pub max_entities_per_extraction: usize,
374 #[serde(default = "default_max_edges")]
376 pub max_edges_per_extraction: usize,
377 #[serde(default)]
379 pub forbidden_content_patterns: Vec<String>,
380}
381
382impl Default for MemoryWriteValidationConfig {
383 fn default() -> Self {
384 Self {
385 enabled: true,
386 max_content_bytes: default_max_content_bytes(),
387 min_entity_name_bytes: default_min_entity_name_bytes(),
388 max_entity_name_bytes: default_max_entity_name_bytes(),
389 max_fact_bytes: default_max_fact_bytes(),
390 max_entities_per_extraction: default_max_entities(),
391 max_edges_per_extraction: default_max_edges(),
392 forbidden_content_patterns: Vec::new(),
393 }
394 }
395}
396
397#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
403pub struct CustomPiiPattern {
404 pub name: String,
406 pub pattern: String,
408 #[serde(default = "default_custom_replacement")]
410 pub replacement: String,
411}
412
413fn default_custom_replacement() -> String {
414 "[PII:custom]".to_owned()
415}
416
417#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
422#[allow(clippy::struct_excessive_bools)] pub struct PiiFilterConfig {
424 #[serde(default = "default_true")]
426 pub enabled: bool,
427 #[serde(default = "default_true")]
429 pub filter_email: bool,
430 #[serde(default = "default_true")]
432 pub filter_phone: bool,
433 #[serde(default = "default_true")]
435 pub filter_ssn: bool,
436 #[serde(default = "default_true")]
438 pub filter_credit_card: bool,
439 #[serde(default)]
448 pub filter_names: bool,
449 #[serde(default)]
451 pub custom_patterns: Vec<CustomPiiPattern>,
452}
453
454impl Default for PiiFilterConfig {
455 fn default() -> Self {
456 Self {
457 enabled: true,
458 filter_email: true,
459 filter_phone: true,
460 filter_ssn: true,
461 filter_credit_card: true,
462 filter_names: false,
463 custom_patterns: Vec::new(),
464 }
465 }
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
474#[serde(rename_all = "lowercase")]
475#[non_exhaustive]
476pub enum GuardrailAction {
477 #[default]
479 Block,
480 Warn,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
486#[serde(rename_all = "lowercase")]
487#[non_exhaustive]
488pub enum GuardrailFailStrategy {
489 #[default]
491 Closed,
492 Open,
494}
495
496#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
498pub struct GuardrailConfig {
499 #[serde(default)]
501 pub enabled: bool,
502 #[serde(default)]
504 pub provider: Option<String>,
505 #[serde(default)]
507 pub model: Option<String>,
508 #[serde(default = "default_guardrail_timeout_ms")]
510 pub timeout_ms: u64,
511 #[serde(default)]
513 pub action: GuardrailAction,
514 #[serde(default = "default_fail_strategy")]
516 pub fail_strategy: GuardrailFailStrategy,
517 #[serde(default)]
519 pub scan_tool_output: bool,
520 #[serde(default = "default_max_input_chars")]
522 pub max_input_chars: usize,
523}
524fn default_guardrail_timeout_ms() -> u64 {
525 500
526}
527fn default_max_input_chars() -> usize {
528 4096
529}
530fn default_fail_strategy() -> GuardrailFailStrategy {
531 GuardrailFailStrategy::Closed
532}
533impl Default for GuardrailConfig {
534 fn default() -> Self {
535 Self {
536 enabled: false,
537 provider: None,
538 model: None,
539 timeout_ms: default_guardrail_timeout_ms(),
540 action: GuardrailAction::default(),
541 fail_strategy: default_fail_strategy(),
542 scan_tool_output: false,
543 max_input_chars: default_max_input_chars(),
544 }
545 }
546}
547
548#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
558pub struct ResponseVerificationConfig {
559 #[serde(default = "default_true")]
561 pub enabled: bool,
562 #[serde(default)]
568 pub block_on_detection: bool,
569 #[serde(default)]
576 pub verifier_provider: ProviderName,
577}
578
579impl Default for ResponseVerificationConfig {
580 fn default() -> Self {
581 Self {
582 enabled: true,
583 block_on_detection: false,
584 verifier_provider: ProviderName::default(),
585 }
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
594 fn content_isolation_default_mcp_to_acp_boundary_true() {
595 let cfg = ContentIsolationConfig::default();
596 assert!(cfg.mcp_to_acp_boundary);
597 }
598
599 #[test]
600 fn content_isolation_deserialize_mcp_to_acp_boundary_false() {
601 let toml = r"
602 mcp_to_acp_boundary = false
603 ";
604 let cfg: ContentIsolationConfig = toml::from_str(toml).unwrap();
605 assert!(!cfg.mcp_to_acp_boundary);
606 }
607
608 #[test]
609 fn content_isolation_deserialize_absent_defaults_true() {
610 let cfg: ContentIsolationConfig = toml::from_str("").unwrap();
611 assert!(cfg.mcp_to_acp_boundary);
612 }
613
614 #[test]
617 fn pii_filter_default_is_enabled() {
618 assert!(PiiFilterConfig::default().enabled);
619 }
620
621 #[test]
622 fn pii_filter_deserialize_absent_defaults_enabled_true() {
623 let cfg: PiiFilterConfig = toml::from_str("").unwrap();
625 assert!(cfg.enabled);
626 }
627
628 #[test]
629 fn pii_filter_deserialize_section_present_without_enabled_key_defaults_true() {
630 let cfg: PiiFilterConfig = toml::from_str("filter_email = false").unwrap();
633 assert!(cfg.enabled);
634 assert!(!cfg.filter_email);
635 }
636
637 #[test]
638 fn pii_filter_deserialize_explicit_false_is_respected() {
639 let cfg: PiiFilterConfig = toml::from_str("enabled = false").unwrap();
640 assert!(!cfg.enabled);
641 }
642
643 #[test]
644 fn secret_masking_default_is_enabled() {
645 assert!(SecretMaskingConfig::default().enabled);
646 }
647
648 #[test]
649 fn secret_masking_deserialize_absent_defaults_enabled_true() {
650 let cfg: SecretMaskingConfig = toml::from_str("").unwrap();
651 assert!(cfg.enabled);
652 }
653
654 #[test]
655 fn secret_masking_deserialize_section_present_without_enabled_key_defaults_true() {
656 let cfg: SecretMaskingConfig = toml::from_str("min_secret_len = 12").unwrap();
657 assert!(cfg.enabled);
658 assert_eq!(cfg.min_secret_len, 12);
659 }
660
661 #[test]
662 fn secret_masking_deserialize_explicit_false_is_respected() {
663 let cfg: SecretMaskingConfig = toml::from_str("enabled = false").unwrap();
664 assert!(!cfg.enabled);
665 }
666
667 fn de_guard(toml: &str) -> Result<EmbeddingGuardConfig, toml::de::Error> {
668 toml::from_str(toml)
669 }
670
671 #[test]
672 fn threshold_valid() {
673 let cfg = de_guard("threshold = 0.35\nmin_samples = 5").unwrap();
674 assert!((cfg.threshold - 0.35).abs() < f64::EPSILON);
675 }
676
677 #[test]
678 fn threshold_one_valid() {
679 let cfg = de_guard("threshold = 1.0\nmin_samples = 1").unwrap();
680 assert!((cfg.threshold - 1.0).abs() < f64::EPSILON);
681 }
682
683 #[test]
684 fn threshold_zero_rejected() {
685 assert!(de_guard("threshold = 0.0\nmin_samples = 1").is_err());
686 }
687
688 #[test]
689 fn threshold_above_one_rejected() {
690 assert!(de_guard("threshold = 1.5\nmin_samples = 1").is_err());
691 }
692
693 #[test]
694 fn threshold_negative_rejected() {
695 assert!(de_guard("threshold = -0.1\nmin_samples = 1").is_err());
696 }
697
698 #[test]
699 fn min_samples_zero_rejected() {
700 assert!(de_guard("threshold = 0.35\nmin_samples = 0").is_err());
701 }
702
703 #[test]
704 fn min_samples_one_valid() {
705 let cfg = de_guard("threshold = 0.35\nmin_samples = 1").unwrap();
706 assert_eq!(cfg.min_samples, 1);
707 }
708}
709
710fn default_causal_threshold() -> f32 {
715 0.7
716}
717
718fn default_probe_max_tokens() -> u32 {
719 100
720}
721
722fn default_probe_timeout_ms() -> u64 {
723 3000
724}
725
726#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
734pub struct CausalIpiConfig {
735 #[serde(default)]
737 pub enabled: bool,
738
739 #[serde(
744 default = "default_causal_threshold",
745 deserialize_with = "crate::de_helpers::de_unit_open"
746 )]
747 pub threshold: f32,
748
749 #[serde(default)]
754 pub provider: Option<String>,
755
756 #[serde(default = "default_probe_max_tokens")]
760 pub probe_max_tokens: u32,
761
762 #[serde(default = "default_probe_timeout_ms")]
766 pub probe_timeout_ms: u64,
767
768 #[serde(default)]
770 pub shadow_memory: ShadowMemoryConfig,
771}
772
773impl Default for CausalIpiConfig {
774 fn default() -> Self {
775 Self {
776 enabled: false,
777 threshold: default_causal_threshold(),
778 provider: None,
779 probe_max_tokens: default_probe_max_tokens(),
780 probe_timeout_ms: default_probe_timeout_ms(),
781 shadow_memory: ShadowMemoryConfig::default(),
782 }
783 }
784}
785
786fn default_shadow_window() -> usize {
791 8
792}
793
794fn default_shadow_max_events() -> usize {
795 64
796}
797
798fn default_shadow_drift_threshold() -> f32 {
799 0.6
800}
801
802fn validate_shadow_window<'de, D>(deserializer: D) -> Result<usize, D::Error>
803where
804 D: serde::Deserializer<'de>,
805{
806 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
807 if value == 0 {
808 return Err(serde::de::Error::custom(
809 "shadow_memory.window_size must be >= 1",
810 ));
811 }
812 Ok(value)
813}
814
815fn validate_shadow_max_events<'de, D>(deserializer: D) -> Result<usize, D::Error>
816where
817 D: serde::Deserializer<'de>,
818{
819 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
820 if value == 0 {
821 return Err(serde::de::Error::custom(
822 "shadow_memory.max_events must be >= 1",
823 ));
824 }
825 Ok(value)
826}
827
828#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
845pub struct ShadowMemoryConfig {
846 #[serde(default)]
848 pub enabled: bool,
849
850 #[serde(
852 default = "default_shadow_window",
853 deserialize_with = "validate_shadow_window"
854 )]
855 pub window_size: usize,
856
857 #[serde(
859 default = "default_shadow_max_events",
860 deserialize_with = "validate_shadow_max_events"
861 )]
862 pub max_events: usize,
863
864 #[serde(
866 default = "default_shadow_drift_threshold",
867 deserialize_with = "crate::de_helpers::de_unit_open"
868 )]
869 pub drift_threshold: f32,
870}
871
872impl Default for ShadowMemoryConfig {
873 fn default() -> Self {
874 Self {
875 enabled: false,
876 window_size: default_shadow_window(),
877 max_events: default_shadow_max_events(),
878 drift_threshold: default_shadow_drift_threshold(),
879 }
880 }
881}
882
883#[cfg(test)]
884mod causal_ipi_tests {
885 use super::*;
886
887 #[test]
888 fn causal_ipi_defaults() {
889 let cfg = CausalIpiConfig::default();
890 assert!(!cfg.enabled);
891 assert!((cfg.threshold - 0.7).abs() < 1e-6);
892 assert!(cfg.provider.is_none());
893 assert_eq!(cfg.probe_max_tokens, 100);
894 assert_eq!(cfg.probe_timeout_ms, 3000);
895 }
896
897 #[test]
898 fn causal_ipi_deserialize_enabled() {
899 let toml = r#"
900 enabled = true
901 threshold = 0.8
902 provider = "fast"
903 probe_max_tokens = 150
904 probe_timeout_ms = 5000
905 "#;
906 let cfg: CausalIpiConfig = toml::from_str(toml).unwrap();
907 assert!(cfg.enabled);
908 assert!((cfg.threshold - 0.8).abs() < 1e-6);
909 assert_eq!(cfg.provider.as_deref(), Some("fast"));
910 assert_eq!(cfg.probe_max_tokens, 150);
911 assert_eq!(cfg.probe_timeout_ms, 5000);
912 }
913
914 #[test]
915 fn causal_ipi_threshold_zero_rejected() {
916 let result: Result<CausalIpiConfig, _> = toml::from_str("threshold = 0.0");
917 assert!(result.is_err());
918 }
919
920 #[test]
921 fn causal_ipi_threshold_above_one_rejected() {
922 let result: Result<CausalIpiConfig, _> = toml::from_str("threshold = 1.1");
923 assert!(result.is_err());
924 }
925
926 #[test]
927 fn causal_ipi_threshold_exactly_one_accepted() {
928 let cfg: CausalIpiConfig = toml::from_str("threshold = 1.0").unwrap();
929 assert!((cfg.threshold - 1.0).abs() < 1e-6);
930 }
931}
932
933#[cfg(test)]
934mod shadow_memory_config_tests {
935 use super::*;
936
937 #[test]
938 fn shadow_memory_defaults() {
939 let cfg = ShadowMemoryConfig::default();
940 assert!(!cfg.enabled);
941 assert_eq!(cfg.window_size, 8);
942 assert_eq!(cfg.max_events, 64);
943 assert!((cfg.drift_threshold - 0.6).abs() < 1e-6);
944 }
945
946 #[test]
947 fn shadow_memory_window_zero_rejected() {
948 let result: Result<ShadowMemoryConfig, _> = toml::from_str("window_size = 0");
949 assert!(result.is_err());
950 }
951
952 #[test]
953 fn shadow_memory_max_events_zero_rejected() {
954 let result: Result<ShadowMemoryConfig, _> = toml::from_str("max_events = 0");
955 assert!(result.is_err());
956 }
957
958 #[test]
959 fn shadow_memory_drift_threshold_zero_rejected() {
960 let result: Result<ShadowMemoryConfig, _> = toml::from_str("drift_threshold = 0.0");
961 assert!(result.is_err());
962 }
963
964 #[test]
965 fn shadow_memory_drift_threshold_above_one_rejected() {
966 let result: Result<ShadowMemoryConfig, _> = toml::from_str("drift_threshold = 1.1");
967 assert!(result.is_err());
968 }
969
970 #[test]
971 fn shadow_memory_drift_threshold_exactly_one_accepted() {
972 let cfg: ShadowMemoryConfig = toml::from_str("drift_threshold = 1.0").unwrap();
973 assert!((cfg.drift_threshold - 1.0).abs() < 1e-6);
974 }
975
976 #[test]
977 fn shadow_memory_full_deserialization() {
978 let toml = r"
979 enabled = true
980 window_size = 4
981 max_events = 32
982 drift_threshold = 0.8
983 ";
984 let cfg: ShadowMemoryConfig = toml::from_str(toml).unwrap();
985 assert!(cfg.enabled);
986 assert_eq!(cfg.window_size, 4);
987 assert_eq!(cfg.max_events, 32);
988 assert!((cfg.drift_threshold - 0.8).abs() < 1e-6);
989 }
990}