1pub const UNSET_DISPLAY: &str = "<unset>";
5
6#[derive(Debug, Clone)]
8pub struct SecretFieldInfo {
9 pub name: &'static str,
11 pub category: &'static str,
13 pub is_set: bool,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum PropKind {
21 String,
22 Bool,
23 Integer,
24 Float,
25 Enum,
27 StringArray,
29 ObjectArray,
36 Object,
42}
43
44pub trait HasPropKind {
48 const PROP_KIND: PropKind;
49
50 fn display_secret_terminals() -> Vec<&'static str> {
55 Vec::new()
56 }
57}
58
59macro_rules! impl_prop_kind {
60 ($kind:expr, $($ty:ty),+) => {
61 $(impl HasPropKind for $ty { const PROP_KIND: PropKind = $kind; })+
62 };
63}
64
65impl_prop_kind!(PropKind::Bool, bool);
66impl_prop_kind!(PropKind::String, String);
67impl_prop_kind!(PropKind::Float, f64, f32);
68impl_prop_kind!(
69 PropKind::Integer,
70 u8,
71 u16,
72 u32,
73 u64,
74 usize,
75 i8,
76 i16,
77 i32,
78 i64,
79 isize
80);
81impl HasPropKind for Vec<String> {
82 const PROP_KIND: PropKind = PropKind::StringArray;
83}
84
85impl HasPropKind for crate::providers::ModelProviderRef {
89 const PROP_KIND: PropKind = PropKind::String;
90}
91impl HasPropKind for Vec<crate::providers::ModelProviderRef> {
92 const PROP_KIND: PropKind = PropKind::StringArray;
93}
94impl HasPropKind for crate::providers::TtsProviderRef {
95 const PROP_KIND: PropKind = PropKind::String;
96}
97impl HasPropKind for crate::providers::TranscriptionProviderRef {
98 const PROP_KIND: PropKind = PropKind::String;
99}
100impl HasPropKind for crate::providers::ChannelRef {
101 const PROP_KIND: PropKind = PropKind::String;
102}
103impl HasPropKind for Vec<crate::providers::ChannelRef> {
104 const PROP_KIND: PropKind = PropKind::StringArray;
105}
106
107impl HasPropKind for crate::multi_agent::AgentAlias {
111 const PROP_KIND: PropKind = PropKind::String;
112}
113impl HasPropKind for crate::multi_agent::PeerGroupName {
114 const PROP_KIND: PropKind = PropKind::String;
115}
116impl HasPropKind for crate::multi_agent::PeerUsername {
117 const PROP_KIND: PropKind = PropKind::String;
118}
119impl HasPropKind for crate::multi_agent::AccessMode {
120 const PROP_KIND: PropKind = PropKind::Enum;
121}
122impl HasPropKind for crate::multi_agent::MemoryBackendKind {
123 const PROP_KIND: PropKind = PropKind::Enum;
124}
125impl HasPropKind for crate::multi_agent::OutputModality {
126 const PROP_KIND: PropKind = PropKind::Enum;
127}
128impl HasPropKind for Vec<crate::multi_agent::AgentAlias> {
129 const PROP_KIND: PropKind = PropKind::StringArray;
130}
131impl HasPropKind for Vec<crate::multi_agent::PeerUsername> {
132 const PROP_KIND: PropKind = PropKind::StringArray;
133}
134impl HasPropKind
135 for std::collections::BTreeMap<crate::multi_agent::AgentAlias, crate::multi_agent::AccessMode>
136{
137 const PROP_KIND: PropKind = PropKind::Object;
139}
140
141impl HasPropKind for Vec<crate::schema::ClassificationRule> {
149 const PROP_KIND: PropKind = PropKind::ObjectArray;
150}
151impl HasPropKind for Vec<crate::schema::EmbeddingRouteConfig> {
152 const PROP_KIND: PropKind = PropKind::ObjectArray;
153}
154impl HasPropKind for Vec<crate::schema::GoogleWorkspaceAllowedOperation> {
155 const PROP_KIND: PropKind = PropKind::ObjectArray;
156}
157impl HasPropKind for Vec<crate::schema::McpServerConfig> {
158 const PROP_KIND: PropKind = PropKind::ObjectArray;
159
160 fn display_secret_terminals() -> Vec<&'static str> {
161 crate::schema::McpServerConfig::secret_field_terminals()
162 }
163}
164impl HasPropKind for Vec<crate::schema::ModelRouteConfig> {
165 const PROP_KIND: PropKind = PropKind::ObjectArray;
166}
167impl HasPropKind for Vec<crate::schema::NevisRoleMappingConfig> {
168 const PROP_KIND: PropKind = PropKind::ObjectArray;
169}
170impl HasPropKind for Vec<crate::schema::PeripheralBoardConfig> {
171 const PROP_KIND: PropKind = PropKind::ObjectArray;
172}
173impl HasPropKind for Vec<crate::schema::ToolFilterGroup> {
174 const PROP_KIND: PropKind = PropKind::ObjectArray;
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum CredentialSurfaceClass {
180 EncryptedSecret,
181 PathOnlyReference,
182 PublicValue,
183 ExternalAuthStore,
184 LegacyEnvPath,
185 RequiresFollowUp,
186}
187
188#[derive(
197 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
198)]
199pub enum ConfigTab {
200 #[default]
201 None,
203
204 Connection,
206 Advanced,
207
208 Model,
210
211 Behavior,
213
214 General,
216 Channels,
217 Providers,
218 Bundles,
219 Cron,
220 Tuning,
221 Workspace,
222 Memory,
223
224 PeerGroups,
226 Personality,
227
228 Settings,
230 Servers,
231
232 Limits,
234 Costs,
235
236 Skills,
238 Aliases,
239}
240
241impl ConfigTab {
242 pub fn label(self) -> &'static str {
244 match self {
245 Self::None => "",
246 Self::Connection => "Connection",
247 Self::Advanced => "Advanced",
248 Self::Model => "Model",
249 Self::Behavior => "Behavior",
250 Self::General => "General",
251 Self::Channels => "Channels",
252 Self::Providers => "Providers",
253 Self::Bundles => "Bundles",
254 Self::Cron => "Cron",
255 Self::Tuning => "Tuning",
256 Self::Workspace => "Workspace",
257 Self::Memory => "Memory",
258 Self::PeerGroups => "Peer Groups",
259 Self::Personality => "Personality",
260 Self::Settings => "Settings",
261 Self::Servers => "Servers",
262 Self::Limits => "Limits",
263 Self::Costs => "Costs",
264 Self::Skills => "Skills",
265 Self::Aliases => "Aliases",
266 }
267 }
268
269 pub fn is_none(&self) -> bool {
271 matches!(self, Self::None)
272 }
273}
274
275impl std::fmt::Display for ConfigTab {
276 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277 f.write_str(self.label())
278 }
279}
280
281#[derive(Clone)]
283pub struct PropFieldInfo {
284 pub name: String,
289 pub category: &'static str,
291 pub display_value: String,
293 pub type_hint: &'static str,
295 pub kind: PropKind,
297 pub is_secret: bool,
299 pub enum_variants: Option<fn() -> Vec<String>>,
301 pub description: &'static str,
305 pub derived_from_secret: bool,
309 pub credential_class: Option<CredentialSurfaceClass>,
311 pub tab: ConfigTab,
314}
315
316impl PropKind {
317 pub fn wire_name(self) -> &'static str {
321 match self {
322 Self::String => "string",
323 Self::Bool => "bool",
324 Self::Integer => "integer",
325 Self::Float => "float",
326 Self::Enum => "enum",
327 Self::StringArray => "string_array",
328 Self::ObjectArray => "object_array",
329 Self::Object => "object",
330 }
331 }
332}
333
334impl PropFieldInfo {
335 pub fn is_enum(&self) -> bool {
336 self.enum_variants.is_some()
337 }
338}
339
340impl std::fmt::Debug for PropFieldInfo {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 f.debug_struct("PropFieldInfo")
343 .field("name", &self.name)
344 .field("kind", &self.kind)
345 .field("is_secret", &self.is_secret)
346 .field("credential_class", &self.credential_class)
347 .field("tab", &self.tab)
348 .finish_non_exhaustive()
349 }
350}
351
352pub trait MaskSecrets {
359 fn mask_secrets(&mut self);
360 fn restore_secrets_from(&mut self, current: &Self);
361}
362
363impl<T: MaskSecrets> MaskSecrets for std::collections::HashMap<String, T> {
364 fn mask_secrets(&mut self) {
365 for v in self.values_mut() {
366 v.mask_secrets();
367 }
368 }
369 fn restore_secrets_from(&mut self, current: &Self) {
370 for (k, v) in self.iter_mut() {
371 if let Some(cur) = current.get(k) {
372 v.restore_secrets_from(cur);
373 }
374 }
375 }
376}
377
378impl<T: MaskSecrets> MaskSecrets for Vec<T> {
379 fn mask_secrets(&mut self) {
380 for v in self.iter_mut() {
381 v.mask_secrets();
382 }
383 }
384 fn restore_secrets_from(&mut self, current: &Self) {
385 for (v, cur) in self.iter_mut().zip(current.iter()) {
386 v.restore_secrets_from(cur);
387 }
388 }
389}
390
391pub const MASKED_SECRET: &str = "***MASKED***";
392
393pub fn is_masked_secret(value: &str) -> bool {
394 value == MASKED_SECRET
395}
396
397pub trait SecretField {
410 fn mask(&mut self);
412
413 fn restore_from(&mut self, current: &Self);
418
419 fn encrypt_in_place(
421 &mut self,
422 store: &crate::security::SecretStore,
423 field: &str,
424 ) -> anyhow::Result<()>;
425
426 fn decrypt_in_place(
428 &mut self,
429 store: &crate::security::SecretStore,
430 field: &str,
431 ) -> anyhow::Result<()>;
432
433 fn is_set(&self) -> bool;
436}
437
438impl SecretField for String {
439 fn mask(&mut self) {
440 if !self.is_empty() {
441 *self = MASKED_SECRET.to_string();
442 }
443 }
444
445 fn restore_from(&mut self, current: &Self) {
446 if is_masked_secret(self) {
447 self.clone_from(current);
448 }
449 }
450
451 fn encrypt_in_place(
452 &mut self,
453 store: &crate::security::SecretStore,
454 field: &str,
455 ) -> anyhow::Result<()> {
456 use anyhow::Context;
457 if !self.is_empty() && !crate::security::SecretStore::is_encrypted(self) {
458 *self = store
459 .encrypt(self)
460 .with_context(|| format!("Failed to encrypt {field}"))?;
461 }
462 Ok(())
463 }
464
465 fn decrypt_in_place(
466 &mut self,
467 store: &crate::security::SecretStore,
468 field: &str,
469 ) -> anyhow::Result<()> {
470 use anyhow::Context;
471 if crate::security::SecretStore::is_encrypted(self) {
472 *self = store
473 .decrypt(self)
474 .with_context(|| format!("Failed to decrypt {field}"))?;
475 }
476 Ok(())
477 }
478
479 fn is_set(&self) -> bool {
480 !self.is_empty()
481 }
482}
483
484impl SecretField for Option<String> {
485 fn mask(&mut self) {
486 if let Some(inner) = self {
487 inner.mask();
488 }
489 }
490
491 fn restore_from(&mut self, current: &Self) {
492 if let (Some(inner), Some(cur)) = (self.as_mut(), current.as_ref()) {
493 inner.restore_from(cur);
494 }
495 }
496
497 fn encrypt_in_place(
498 &mut self,
499 store: &crate::security::SecretStore,
500 field: &str,
501 ) -> anyhow::Result<()> {
502 match self {
503 Some(inner) => inner.encrypt_in_place(store, field),
504 None => Ok(()),
505 }
506 }
507
508 fn decrypt_in_place(
509 &mut self,
510 store: &crate::security::SecretStore,
511 field: &str,
512 ) -> anyhow::Result<()> {
513 match self {
514 Some(inner) => inner.decrypt_in_place(store, field),
515 None => Ok(()),
516 }
517 }
518
519 fn is_set(&self) -> bool {
520 self.as_ref().is_some_and(|v| !v.is_empty())
521 }
522}
523
524impl SecretField for Vec<String> {
525 fn mask(&mut self) {
526 for element in self.iter_mut() {
527 element.mask();
528 }
529 }
530
531 fn restore_from(&mut self, current: &Self) {
532 for (element, cur) in self.iter_mut().zip(current.iter()) {
533 element.restore_from(cur);
534 }
535 }
536
537 fn encrypt_in_place(
538 &mut self,
539 store: &crate::security::SecretStore,
540 field: &str,
541 ) -> anyhow::Result<()> {
542 for (idx, element) in self.iter_mut().enumerate() {
543 element.encrypt_in_place(store, &format!("{field}[{idx}]"))?;
544 }
545 Ok(())
546 }
547
548 fn decrypt_in_place(
549 &mut self,
550 store: &crate::security::SecretStore,
551 field: &str,
552 ) -> anyhow::Result<()> {
553 for (idx, element) in self.iter_mut().enumerate() {
554 element.decrypt_in_place(store, &format!("{field}[{idx}]"))?;
555 }
556 Ok(())
557 }
558
559 fn is_set(&self) -> bool {
560 !self.is_empty()
561 }
562}
563
564impl SecretField for std::collections::HashMap<String, String> {
565 fn mask(&mut self) {
566 for value in self.values_mut() {
567 value.mask();
568 }
569 }
570
571 fn restore_from(&mut self, current: &Self) {
572 for (key, value) in self.iter_mut() {
573 if let Some(cur) = current.get(key) {
574 value.restore_from(cur);
575 }
576 }
577 }
578
579 fn encrypt_in_place(
580 &mut self,
581 store: &crate::security::SecretStore,
582 field: &str,
583 ) -> anyhow::Result<()> {
584 for (key, value) in self.iter_mut() {
585 value.encrypt_in_place(store, &format!("{field}.{key}"))?;
586 }
587 Ok(())
588 }
589
590 fn decrypt_in_place(
591 &mut self,
592 store: &crate::security::SecretStore,
593 field: &str,
594 ) -> anyhow::Result<()> {
595 for (key, value) in self.iter_mut() {
596 value.decrypt_in_place(store, &format!("{field}.{key}"))?;
597 }
598 Ok(())
599 }
600
601 fn is_set(&self) -> bool {
602 self.values().any(|v| !v.is_empty())
603 }
604}
605
606impl SecretField for Option<std::collections::HashMap<String, String>> {
607 fn mask(&mut self) {
608 if let Some(inner) = self {
609 inner.mask();
610 }
611 }
612
613 fn restore_from(&mut self, current: &Self) {
614 if let (Some(inner), Some(cur)) = (self.as_mut(), current.as_ref()) {
615 inner.restore_from(cur);
616 }
617 }
618
619 fn encrypt_in_place(
620 &mut self,
621 store: &crate::security::SecretStore,
622 field: &str,
623 ) -> anyhow::Result<()> {
624 match self {
625 Some(inner) => inner.encrypt_in_place(store, field),
626 None => Ok(()),
627 }
628 }
629
630 fn decrypt_in_place(
631 &mut self,
632 store: &crate::security::SecretStore,
633 field: &str,
634 ) -> anyhow::Result<()> {
635 match self {
636 Some(inner) => inner.decrypt_in_place(store, field),
637 None => Ok(()),
638 }
639 }
640
641 fn is_set(&self) -> bool {
642 self.as_ref()
643 .is_some_and(|m| m.values().any(|v| !v.is_empty()))
644 }
645}
646
647#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
652#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
653#[serde(rename_all = "snake_case")]
654pub enum MapKeyKind {
655 Map,
657 List,
660}
661
662#[derive(Debug, Clone, Copy, serde::Serialize)]
663#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
664pub struct MapKeySection {
665 pub path: &'static str,
667 pub kind: MapKeyKind,
669 pub value_type: &'static str,
671 pub description: &'static str,
674}
675
676#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
682pub struct ConfigFieldEntry {
683 pub path: String,
684 pub category: String,
685 pub kind: PropKind,
686 pub type_hint: String,
687 #[serde(skip_serializing_if = "Option::is_none")]
688 pub value: Option<serde_json::Value>,
689 pub populated: bool,
690 pub is_secret: bool,
691 #[serde(default)]
692 pub is_env_overridden: bool,
693 #[serde(default, skip_serializing_if = "Vec::is_empty")]
694 pub enum_variants: Vec<String>,
695 pub description: String,
696 #[serde(skip_serializing_if = "Option::is_none")]
697 pub section: Option<String>,
698 #[serde(default, skip_serializing_if = "ConfigTab::is_none")]
700 pub tab: ConfigTab,
701}
702
703impl ConfigFieldEntry {
704 pub fn from_prop_field(info: PropFieldInfo, is_env_overridden: bool) -> Self {
708 let populated = info.display_value != crate::traits::UNSET_DISPLAY;
709 let is_sensitive = info.is_secret || info.derived_from_secret;
710 let value = if is_sensitive {
711 None
712 } else {
713 Some(serde_json::Value::String(info.display_value))
714 };
715 let enum_variants = info.enum_variants.map(|f| f()).unwrap_or_default();
716 let section = crate::sections::Section::from_key(info.name.split('.').next().unwrap_or(""))
717 .map(|s| s.as_str().to_string());
718
719 Self {
720 path: info.name,
721 category: info.category.to_string(),
722 kind: info.kind,
723 type_hint: info.type_hint.to_string(),
724 value,
725 populated,
726 is_secret: is_sensitive,
727 is_env_overridden,
728 enum_variants,
729 description: info.description.to_string(),
730 section,
731 tab: info.tab,
732 }
733 }
734}
735
736#[derive(Debug, Clone, Copy)]
742pub struct NestedOptionEntry {
743 pub field: &'static str,
746 pub present: bool,
748 pub display_name: &'static str,
752 pub description: &'static str,
755}
756
757#[derive(Debug, Clone, Copy)]
762pub struct IntegrationDescriptor {
763 pub display_name: &'static str,
764 pub description: &'static str,
765 pub category: &'static str,
770 pub active: bool,
773}
774
775#[derive(Debug, Clone)]
777pub struct ChannelInfo {
778 pub kind: &'static str,
783 pub name: &'static str,
784 pub desc: &'static str,
785 pub configured: bool,
786}
787
788pub trait ChannelConfig {
790 fn name() -> &'static str;
792 fn desc() -> &'static str;
794}
795
796#[cfg(test)]
797mod secret_field_tests {
798 use super::{MASKED_SECRET, SecretField};
799 use crate::security::SecretStore;
800 use std::collections::HashMap;
801 use tempfile::TempDir;
802
803 fn store() -> (TempDir, SecretStore) {
804 let tmp = TempDir::new().unwrap();
805 let store = SecretStore::new(tmp.path(), true);
806 (tmp, store)
807 }
808
809 #[test]
810 fn string_roundtrip_and_idempotent() {
811 let (_tmp, store) = store();
812 let mut s = String::from("sk-abc");
813 s.encrypt_in_place(&store, "test.s").unwrap();
814 assert!(SecretStore::is_encrypted(&s));
815 let enc1 = s.clone();
816 s.encrypt_in_place(&store, "test.s").unwrap();
818 assert_eq!(s, enc1);
819 s.decrypt_in_place(&store, "test.s").unwrap();
820 assert_eq!(s, "sk-abc");
821 }
822
823 #[test]
824 fn string_empty_stays_empty() {
825 let (_tmp, store) = store();
826 let mut s = String::new();
827 s.encrypt_in_place(&store, "test.s").unwrap();
828 assert_eq!(s, "");
829 assert!(!s.is_set());
830 }
831
832 #[test]
833 fn string_mask_and_restore() {
834 let mut s = String::from("Bearer xyz");
835 let cur = String::from("Bearer xyz");
836 s.mask();
837 assert_eq!(s, MASKED_SECRET);
838 s.restore_from(&cur);
839 assert_eq!(s, "Bearer xyz");
840 }
841
842 #[test]
843 fn option_string_none_is_noop() {
844 let (_tmp, store) = store();
845 let mut v: Option<String> = None;
846 v.encrypt_in_place(&store, "test.o").unwrap();
847 v.decrypt_in_place(&store, "test.o").unwrap();
848 v.mask();
849 assert_eq!(v, None);
850 assert!(!v.is_set());
851 }
852
853 #[test]
854 fn option_string_some_roundtrip() {
855 let (_tmp, store) = store();
856 let mut v: Option<String> = Some("Bearer xyz".into());
857 v.encrypt_in_place(&store, "test.o").unwrap();
858 assert!(SecretStore::is_encrypted(v.as_ref().unwrap()));
859 v.decrypt_in_place(&store, "test.o").unwrap();
860 assert_eq!(v.as_deref(), Some("Bearer xyz"));
861 assert!(v.is_set());
862 }
863
864 #[test]
865 fn vec_string_roundtrip_per_element() {
866 let (_tmp, store) = store();
867 let mut v: Vec<String> = vec!["one".into(), "".into(), "two".into()];
868 v.encrypt_in_place(&store, "test.v").unwrap();
869 assert!(SecretStore::is_encrypted(&v[0]));
870 assert_eq!(v[1], "", "empty element must stay empty");
871 assert!(SecretStore::is_encrypted(&v[2]));
872 v.decrypt_in_place(&store, "test.v").unwrap();
873 assert_eq!(v, vec!["one", "", "two"]);
874 }
875
876 #[test]
877 fn hashmap_string_string_roundtrip_per_value() {
878 let (_tmp, store) = store();
879 let mut h: HashMap<String, String> = HashMap::from([
880 ("Authorization".into(), "Bearer sk-abc".into()),
881 ("X-Trace".into(), "req-123".into()),
882 ]);
883 h.encrypt_in_place(&store, "mcp.servers.foo.headers")
884 .unwrap();
885 for v in h.values() {
886 assert!(SecretStore::is_encrypted(v));
887 }
888 h.decrypt_in_place(&store, "mcp.servers.foo.headers")
889 .unwrap();
890 assert_eq!(
891 h.get("Authorization").map(String::as_str),
892 Some("Bearer sk-abc")
893 );
894 assert_eq!(h.get("X-Trace").map(String::as_str), Some("req-123"));
895 assert!(h.is_set());
896 }
897
898 #[test]
899 fn hashmap_string_string_mask_and_restore() {
900 let mut h: HashMap<String, String> =
901 HashMap::from([("Authorization".into(), "Bearer xyz".into())]);
902 let cur = h.clone();
903 h.mask();
904 assert_eq!(
905 h.get("Authorization").map(String::as_str),
906 Some(MASKED_SECRET)
907 );
908 h.restore_from(&cur);
909 assert_eq!(
910 h.get("Authorization").map(String::as_str),
911 Some("Bearer xyz")
912 );
913 }
914
915 #[test]
916 fn option_hashmap_none_is_noop() {
917 let (_tmp, store) = store();
918 let mut v: Option<HashMap<String, String>> = None;
919 v.encrypt_in_place(&store, "test.oh").unwrap();
920 v.decrypt_in_place(&store, "test.oh").unwrap();
921 v.mask();
922 assert!(v.is_none());
923 assert!(!v.is_set());
924 }
925
926 #[test]
927 fn option_hashmap_some_roundtrip() {
928 let (_tmp, store) = store();
929 let mut v: Option<HashMap<String, String>> =
930 Some(HashMap::from([("k".into(), "secret".into())]));
931 v.encrypt_in_place(&store, "test.oh").unwrap();
932 assert!(SecretStore::is_encrypted(
933 v.as_ref().unwrap().get("k").unwrap()
934 ));
935 v.decrypt_in_place(&store, "test.oh").unwrap();
936 assert_eq!(
937 v.as_ref().unwrap().get("k").map(String::as_str),
938 Some("secret")
939 );
940 assert!(v.is_set());
941 }
942
943 #[test]
944 fn hashmap_empty_is_not_set() {
945 let h: HashMap<String, String> = HashMap::new();
946 assert!(!h.is_set());
947 let oh: Option<HashMap<String, String>> = Some(HashMap::new());
948 assert!(!oh.is_set());
949 }
950
951 #[test]
952 fn hashmap_with_only_empty_values_is_not_set() {
953 let h: HashMap<String, String> = HashMap::from([
959 ("Authorization".into(), String::new()),
960 ("X-Trace".into(), String::new()),
961 ]);
962 assert!(!h.is_set());
963
964 let oh: Option<HashMap<String, String>> =
965 Some(HashMap::from([("Authorization".into(), String::new())]));
966 assert!(!oh.is_set());
967
968 let mixed: HashMap<String, String> = HashMap::from([
969 ("Authorization".into(), "Bearer xyz".into()),
970 ("X-Trace".into(), String::new()),
971 ]);
972 assert!(mixed.is_set(), "any non-empty value makes the map set");
973 }
974
975 #[test]
976 fn encrypt_decrypt_failure_message_includes_field_path() {
977 let tmp = TempDir::new().unwrap();
978 let bad_store = SecretStore::new(tmp.path(), true);
979 let mut s = String::from("enc2:not-valid-hex");
981 let err = s
982 .decrypt_in_place(&bad_store, "mcp.servers.foo.headers.Authorization")
983 .expect_err("malformed ciphertext must fail");
984 let msg = format!("{err:#}");
985 assert!(
986 msg.contains("mcp.servers.foo.headers.Authorization"),
987 "error must include field path; got: {msg}"
988 );
989 }
990}