1use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9
10use super::{Config, ProxyAuth};
11use crate::patch::ProviderApiKeyIntents;
12
13trait CredentialResolver {
14 fn resolve(
15 &self,
16 credential_ref: &crate::CredentialRef,
17 ) -> crate::ConfigStoreResult<Option<crate::SecretValue>>;
18}
19
20impl CredentialResolver for crate::CredentialStore {
21 fn resolve(
22 &self,
23 credential_ref: &crate::CredentialRef,
24 ) -> crate::ConfigStoreResult<Option<crate::SecretValue>> {
25 crate::CredentialStore::resolve(self, credential_ref)
26 }
27}
28
29impl CredentialResolver for crate::credential_store::CredentialDocumentLkg {
30 fn resolve(
31 &self,
32 credential_ref: &crate::CredentialRef,
33 ) -> crate::ConfigStoreResult<Option<crate::SecretValue>> {
34 self.resolve(credential_ref)
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub(crate) struct AccessVerifierRecord {
40 pub hash: String,
41 pub salt: String,
42}
43
44pub fn access_password_credential_ref() -> crate::ConfigStoreResult<crate::CredentialRef> {
45 crate::credential_ref("access", "root", "password_verifier")
46}
47
48pub fn access_device_credential_ref(
49 device_id: &str,
50) -> crate::ConfigStoreResult<crate::CredentialRef> {
51 crate::credential_ref("access", device_id, "device_token_verifier")
52}
53
54pub(crate) fn encode_access_verifier(hash: &str, salt: &str) -> crate::ConfigStoreResult<String> {
55 validate_access_verifier(hash, salt)?;
56 serde_json::to_string(&AccessVerifierRecord {
57 hash: hash.to_string(),
58 salt: salt.to_string(),
59 })
60 .map_err(Into::into)
61}
62
63fn validate_access_verifier(hash: &str, salt: &str) -> crate::ConfigStoreResult<()> {
64 if hash.len() != 64
65 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit())
66 || salt.is_empty()
67 || !salt.len().is_multiple_of(2)
68 || !salt.bytes().all(|byte| byte.is_ascii_hexdigit())
69 {
70 return Err(crate::ConfigStoreError::Validation(
71 "access-control verifier is invalid".to_string(),
72 ));
73 }
74 Ok(())
75}
76
77pub(crate) fn decode_access_verifier(
78 secret: &str,
79) -> crate::ConfigStoreResult<AccessVerifierRecord> {
80 let record: AccessVerifierRecord = serde_json::from_str(secret).map_err(|_| {
81 crate::ConfigStoreError::Validation(
82 "access-control verifier credential is invalid".to_string(),
83 )
84 })?;
85 validate_access_verifier(&record.hash, &record.salt)?;
86 Ok(record)
87}
88
89fn hydrate_header_credentials(
90 resolver: &impl CredentialResolver,
91 headers: &mut [bamboo_domain::mcp_config::HeaderConfig],
92) -> crate::ConfigStoreResult<()> {
93 for header in headers {
94 if !header.value.is_empty() {
95 continue;
96 }
97 let Some(raw_reference) = header.credential_ref.as_ref() else {
98 continue;
99 };
100 let reference = crate::CredentialRef::parse(raw_reference.clone())?;
101 header.value = resolver
102 .resolve(&reference)?
103 .ok_or_else(|| {
104 crate::ConfigStoreError::Validation(
105 "referenced MCP credential is unavailable".to_string(),
106 )
107 })?
108 .expose()
109 .to_string();
110 }
111 Ok(())
112}
113
114impl crate::BrokerClientConfig {
115 pub fn hydrate_credential_from_store(
119 &mut self,
120 data_dir: &std::path::Path,
121 ) -> crate::ConfigStoreResult<()> {
122 self.token_encrypted = None;
123 let Some(reference) = self.credential_ref.as_ref() else {
124 if self.configured {
125 return Err(crate::ConfigStoreError::Validation(
126 "configured broker credential reference is missing".to_string(),
127 ));
128 }
129 self.token.clear();
130 return Ok(());
131 };
132 match crate::CredentialStore::open(data_dir).resolve(reference)? {
133 Some(secret) => {
134 self.token = secret.expose().to_string();
135 self.configured = true;
136 Ok(())
137 }
138 None => Err(crate::ConfigStoreError::Validation(
139 "referenced broker credential is unavailable".to_string(),
140 )),
141 }
142 }
143}
144
145impl Config {
146 pub fn hydrate_proxy_auth_from_encrypted(&mut self) {
154 if self.proxy_auth_credential_ref.is_some() {
155 self.proxy_auth_encrypted = None;
156 return;
157 }
158 if self.proxy_auth.is_some() {
159 return;
160 }
161
162 if self
169 .proxy_auth_encrypted
170 .as_deref()
171 .map(|s| s.trim().is_empty())
172 .unwrap_or(true)
173 {
174 let legacy = self
175 .extra
176 .get("https_proxy_auth_encrypted")
177 .and_then(|v| v.as_str())
178 .or_else(|| {
179 self.extra
180 .get("http_proxy_auth_encrypted")
181 .and_then(|v| v.as_str())
182 })
183 .map(|s| s.trim())
184 .filter(|s| !s.is_empty())
185 .map(|s| s.to_string());
186
187 if let Some(legacy) = legacy {
188 self.proxy_auth_encrypted = Some(legacy);
189 }
190 }
191
192 let Some(encrypted) = self.proxy_auth_encrypted.as_deref() else {
193 return;
194 };
195
196 match crate::encryption::decrypt(encrypted) {
197 Ok(decrypted) => match serde_json::from_str::<ProxyAuth>(&decrypted) {
198 Ok(auth) => {
199 self.proxy_auth = Some(auth);
200 self.extra.remove("http_proxy_auth_encrypted");
203 self.extra.remove("https_proxy_auth_encrypted");
204 }
205 Err(e) => tracing::warn!("Failed to parse decrypted proxy auth JSON: {}", e),
206 },
207 Err(e) => tracing::warn!("Failed to decrypt proxy auth: {}", e),
208 }
209 }
210
211 pub fn refresh_proxy_auth_encrypted(&mut self) -> Result<()> {
216 if self.proxy_auth_credential_ref.is_some() {
217 self.proxy_auth_encrypted = None;
218 return Ok(());
219 }
220 let Some(auth) = self.proxy_auth.as_ref() else {
224 self.proxy_auth_encrypted = None;
225 return Ok(());
226 };
227
228 let auth_str = serde_json::to_string(auth).context("Failed to serialize proxy auth")?;
229 let encrypted =
230 crate::encryption::encrypt(&auth_str).context("Failed to encrypt proxy auth")?;
231 self.proxy_auth_encrypted = Some(encrypted);
232 Ok(())
233 }
234
235 pub fn hydrate_proxy_auth_from_store(&mut self, data_dir: &std::path::Path) -> Result<()> {
238 let store = crate::CredentialStore::open(data_dir);
239 self.hydrate_proxy_auth_from_resolver(&store)
240 .map_err(anyhow::Error::from)
241 }
242
243 pub(crate) fn hydrate_proxy_auth_from_snapshot(
244 &mut self,
245 snapshot: &crate::credential_store::CredentialDocumentLkg,
246 ) -> crate::ConfigStoreResult<()> {
247 self.hydrate_proxy_auth_from_resolver(snapshot)
248 }
249
250 fn hydrate_proxy_auth_from_resolver(
251 &mut self,
252 resolver: &impl CredentialResolver,
253 ) -> crate::ConfigStoreResult<()> {
254 let Some(reference) = self.proxy_auth_credential_ref.as_ref() else {
255 return Ok(());
256 };
257 let value = resolver.resolve(reference)?;
258 let Some(value) = value else {
259 self.proxy_auth = None;
260 return Ok(());
261 };
262 self.proxy_auth = Some(serde_json::from_str(value.expose()).map_err(|_| {
263 crate::ConfigStoreError::Validation("proxy auth credential is invalid".to_string())
264 })?);
265 self.proxy_auth_encrypted = None;
266 Ok(())
267 }
268
269 pub fn hydrate_provider_api_keys_from_encrypted(&mut self) {
272 if let Some(openai) = self.providers.openai.as_mut() {
273 if openai.api_key.trim().is_empty() {
274 if let Some(encrypted) = openai.api_key_encrypted.as_deref() {
275 match crate::encryption::decrypt(encrypted) {
276 Ok(value) => openai.api_key = value,
277 Err(e) => tracing::warn!("Failed to decrypt OpenAI api_key: {}", e),
278 }
279 }
280 }
281 }
282
283 if let Some(anthropic) = self.providers.anthropic.as_mut() {
284 if anthropic.api_key.trim().is_empty() {
285 if let Some(encrypted) = anthropic.api_key_encrypted.as_deref() {
286 match crate::encryption::decrypt(encrypted) {
287 Ok(value) => anthropic.api_key = value,
288 Err(e) => tracing::warn!("Failed to decrypt Anthropic api_key: {}", e),
289 }
290 }
291 }
292 }
293
294 if let Some(gemini) = self.providers.gemini.as_mut() {
295 if gemini.api_key.trim().is_empty() {
296 if let Some(encrypted) = gemini.api_key_encrypted.as_deref() {
297 match crate::encryption::decrypt(encrypted) {
298 Ok(value) => gemini.api_key = value,
299 Err(e) => tracing::warn!("Failed to decrypt Gemini api_key: {}", e),
300 }
301 }
302 }
303 }
304
305 if let Some(bodhi) = self.providers.bodhi.as_mut() {
306 if bodhi.api_key.trim().is_empty() {
307 if let Some(encrypted) = bodhi.api_key_encrypted.as_deref() {
308 match crate::encryption::decrypt(encrypted) {
309 Ok(value) => bodhi.api_key = value,
310 Err(e) => tracing::warn!("Failed to decrypt Bodhi api_key: {}", e),
311 }
312 }
313 }
314 }
315 }
316
317 pub fn hydrate_provider_credentials_from_store(
321 &mut self,
322 data_dir: &std::path::Path,
323 ) -> crate::ConfigStoreResult<()> {
324 let store = crate::CredentialStore::open(data_dir);
325 self.hydrate_provider_credentials_from_resolver(&store)
326 }
327
328 pub(crate) fn hydrate_provider_credentials_from_snapshot(
329 &mut self,
330 snapshot: &crate::credential_store::CredentialDocumentLkg,
331 ) -> crate::ConfigStoreResult<()> {
332 self.hydrate_provider_credentials_from_resolver(snapshot)
333 }
334
335 fn hydrate_provider_credentials_from_resolver(
336 &mut self,
337 resolver: &impl CredentialResolver,
338 ) -> crate::ConfigStoreResult<()> {
339 macro_rules! hydrate {
340 ($provider:expr) => {
341 if let Some(provider) = $provider {
342 if provider.api_key.trim().is_empty() {
343 if let Some(reference) = provider.credential_ref.as_ref() {
344 provider.api_key = resolver
345 .resolve(reference)?
346 .ok_or_else(|| {
347 crate::ConfigStoreError::Validation(
348 "referenced provider credential is unavailable".to_string(),
349 )
350 })?
351 .expose()
352 .to_string();
353 }
354 }
355 }
356 };
357 }
358 hydrate!(self.providers.openai.as_mut());
359 hydrate!(self.providers.anthropic.as_mut());
360 hydrate!(self.providers.gemini.as_mut());
361 hydrate!(self.providers.bodhi.as_mut());
362 for instance in self.provider_instances.values_mut() {
363 if instance.api_key.trim().is_empty() {
364 if let Some(reference) = instance.credential_ref.as_ref() {
365 instance.api_key = resolver
366 .resolve(reference)?
367 .ok_or_else(|| {
368 crate::ConfigStoreError::Validation(
369 "referenced provider credential is unavailable".to_string(),
370 )
371 })?
372 .expose()
373 .to_string();
374 }
375 }
376 }
377 Ok(())
378 }
379
380 pub fn refresh_provider_api_keys_encrypted(&mut self) -> Result<()> {
381 if let Some(openai) = self.providers.openai.as_mut() {
386 if openai.credential_ref.is_some() {
387 openai.api_key_encrypted = None;
388 } else if !openai.api_key_from_env {
389 let api_key = openai.api_key.trim();
390 if !api_key.is_empty() {
397 openai.api_key_encrypted = Some(
398 crate::encryption::encrypt(api_key)
399 .context("Failed to encrypt OpenAI api_key")?,
400 );
401 }
402 }
403 }
404
405 if let Some(anthropic) = self.providers.anthropic.as_mut() {
406 if anthropic.credential_ref.is_some() {
407 anthropic.api_key_encrypted = None;
408 } else if !anthropic.api_key_from_env {
409 let api_key = anthropic.api_key.trim();
410 if !api_key.is_empty() {
412 anthropic.api_key_encrypted = Some(
413 crate::encryption::encrypt(api_key)
414 .context("Failed to encrypt Anthropic api_key")?,
415 );
416 }
417 }
418 }
419
420 if let Some(gemini) = self.providers.gemini.as_mut() {
421 if gemini.credential_ref.is_some() {
422 gemini.api_key_encrypted = None;
423 } else if !gemini.api_key_from_env {
424 let api_key = gemini.api_key.trim();
425 if !api_key.is_empty() {
427 gemini.api_key_encrypted = Some(
428 crate::encryption::encrypt(api_key)
429 .context("Failed to encrypt Gemini api_key")?,
430 );
431 }
432 }
433 }
434
435 if let Some(bodhi) = self.providers.bodhi.as_mut() {
436 if bodhi.credential_ref.is_some() {
437 bodhi.api_key_encrypted = None;
438 return Ok(());
439 }
440 let api_key = bodhi.api_key.trim();
441 if !api_key.is_empty() {
443 bodhi.api_key_encrypted = Some(
444 crate::encryption::encrypt(api_key)
445 .context("Failed to encrypt Bodhi api_key")?,
446 );
447 }
448 }
449
450 Ok(())
451 }
452
453 pub fn hydrate_provider_instance_api_keys_from_encrypted(&mut self) {
458 for (id, instance) in self.provider_instances.iter_mut() {
459 if instance.api_key.trim().is_empty() {
460 if let Some(encrypted) = instance.api_key_encrypted.as_deref() {
461 match crate::encryption::decrypt(encrypted) {
462 Ok(value) => instance.api_key = value,
463 Err(e) => {
464 tracing::warn!(instance_id = id, "Failed to decrypt api_key: {}", e)
465 }
466 }
467 }
468 }
469 }
470 }
471
472 pub fn refresh_provider_instance_api_keys_encrypted(&mut self) -> Result<()> {
475 for (id, instance) in self.provider_instances.iter_mut() {
476 if instance.credential_ref.is_some() {
477 instance.api_key_encrypted = None;
478 continue;
479 }
480 let api_key = instance.api_key.trim();
481 if !api_key.is_empty() {
484 instance.api_key_encrypted = Some(crate::encryption::encrypt(api_key).context(
485 format!("Failed to encrypt api_key for provider instance '{}'", id),
486 )?);
487 }
488 }
489 Ok(())
490 }
491
492 pub fn ensure_provider_instance_credentials_isolated(&mut self) -> Result<()> {
496 for (id, instance) in &mut self.provider_instances {
497 if instance.credential_ref.is_some() {
498 instance.api_key_encrypted = None;
499 continue;
500 }
501 if !instance.api_key.trim().is_empty() || instance.api_key_encrypted.is_some() {
502 anyhow::bail!(
503 "provider instance '{id}' secret requires credential transaction before persistence"
504 );
505 }
506 }
507 Ok(())
508 }
509
510 pub fn hydrate_mcp_secrets_from_encrypted(&mut self) {
513 for server in self.mcp.servers.iter_mut() {
514 match &mut server.transport {
515 bamboo_domain::mcp_config::TransportConfig::Stdio(stdio) => {
516 if stdio.env_encrypted.is_empty() {
517 continue;
518 }
519
520 for (key, encrypted) in stdio.env_encrypted.clone() {
522 let should_hydrate = stdio
523 .env
524 .get(&key)
525 .map(|v| v.trim().is_empty())
526 .unwrap_or(true);
527 if !should_hydrate {
528 continue;
529 }
530
531 match crate::encryption::decrypt(&encrypted) {
532 Ok(value) => {
533 stdio.env.insert(key, value);
534 }
535 Err(e) => tracing::warn!("Failed to decrypt MCP stdio env var: {}", e),
536 }
537 }
538 }
539 bamboo_domain::mcp_config::TransportConfig::Sse(sse) => {
540 for header in sse.headers.iter_mut() {
541 if !header.value.trim().is_empty() {
542 continue;
543 }
544 let Some(encrypted) = header.value_encrypted.as_deref() else {
545 continue;
546 };
547 match crate::encryption::decrypt(encrypted) {
548 Ok(value) => header.value = value,
549 Err(e) => {
550 tracing::warn!("Failed to decrypt MCP SSE header value: {}", e)
551 }
552 }
553 }
554 }
555 bamboo_domain::mcp_config::TransportConfig::StreamableHttp(sh) => {
556 for header in sh.headers.iter_mut() {
557 if !header.value.trim().is_empty() {
558 continue;
559 }
560 let Some(encrypted) = header.value_encrypted.as_deref() else {
561 continue;
562 };
563 match crate::encryption::decrypt(encrypted) {
564 Ok(value) => header.value = value,
565 Err(e) => {
566 tracing::warn!(
567 "Failed to decrypt MCP StreamableHTTP header value: {}",
568 e
569 )
570 }
571 }
572 }
573 }
574 }
575 }
576 }
577
578 pub fn hydrate_mcp_credentials_from_store(
581 &mut self,
582 data_dir: &std::path::Path,
583 ) -> crate::ConfigStoreResult<()> {
584 let store = crate::CredentialStore::open(data_dir);
585 self.hydrate_mcp_credentials_from_resolver(&store)
586 }
587
588 pub(crate) fn hydrate_mcp_credentials_from_snapshot(
589 &mut self,
590 snapshot: &crate::credential_store::CredentialDocumentLkg,
591 ) -> crate::ConfigStoreResult<()> {
592 self.hydrate_mcp_credentials_from_resolver(snapshot)
593 }
594
595 fn hydrate_mcp_credentials_from_resolver(
596 &mut self,
597 resolver: &impl CredentialResolver,
598 ) -> crate::ConfigStoreResult<()> {
599 for server in &mut self.mcp.servers {
600 match &mut server.transport {
601 bamboo_domain::mcp_config::TransportConfig::Stdio(stdio) => {
602 for (name, raw_reference) in &stdio.env_credential_refs {
603 if stdio.env.get(name).is_some_and(|value| !value.is_empty()) {
604 continue;
605 }
606 let reference = crate::CredentialRef::parse(raw_reference.clone())?;
607 let secret = resolver.resolve(&reference)?.ok_or_else(|| {
608 crate::ConfigStoreError::Validation(
609 "referenced MCP credential is unavailable".to_string(),
610 )
611 })?;
612 stdio.env.insert(name.clone(), secret.expose().to_string());
613 }
614 }
615 bamboo_domain::mcp_config::TransportConfig::Sse(config) => {
616 hydrate_header_credentials(resolver, &mut config.headers)?;
617 }
618 bamboo_domain::mcp_config::TransportConfig::StreamableHttp(config) => {
619 hydrate_header_credentials(resolver, &mut config.headers)?;
620 }
621 }
622 }
623 Ok(())
624 }
625
626 pub fn refresh_mcp_secrets_encrypted(&mut self) -> Result<()> {
627 for server in self.mcp.servers.iter_mut() {
628 match &mut server.transport {
629 bamboo_domain::mcp_config::TransportConfig::Stdio(stdio) => {
630 stdio.env_encrypted.clear();
631 for (key, value) in &stdio.env {
632 let encrypted = crate::encryption::encrypt(value).with_context(|| {
633 format!("Failed to encrypt MCP stdio env var '{key}'")
634 })?;
635 stdio.env_encrypted.insert(key.clone(), encrypted);
636 }
637 }
638 bamboo_domain::mcp_config::TransportConfig::Sse(sse) => {
639 for header in sse.headers.iter_mut() {
640 let configured = !header.value.trim().is_empty();
641 header.value_encrypted = if !configured {
642 None
643 } else {
644 Some(crate::encryption::encrypt(&header.value).with_context(|| {
645 format!("Failed to encrypt MCP SSE header '{}'", header.name)
646 })?)
647 };
648 }
649 }
650 bamboo_domain::mcp_config::TransportConfig::StreamableHttp(sh) => {
651 for header in sh.headers.iter_mut() {
652 let configured = !header.value.trim().is_empty();
653 header.value_encrypted = if !configured {
654 None
655 } else {
656 Some(crate::encryption::encrypt(&header.value).with_context(|| {
657 format!(
658 "Failed to encrypt MCP StreamableHTTP header '{}'",
659 header.name
660 )
661 })?)
662 };
663 }
664 }
665 }
666 }
667
668 Ok(())
669 }
670
671 pub fn sanitize_mcp_credential_refs_for_disk(&mut self) {
676 for server in &mut self.mcp.servers {
677 match &mut server.transport {
678 bamboo_domain::mcp_config::TransportConfig::Stdio(stdio) => {
679 for name in stdio
680 .env_credential_refs
681 .keys()
682 .cloned()
683 .collect::<Vec<_>>()
684 {
685 stdio.env.remove(&name);
686 stdio.env_encrypted.remove(&name);
687 }
688 }
689 bamboo_domain::mcp_config::TransportConfig::Sse(config) => {
690 for header in &mut config.headers {
691 if header.credential_ref.is_some() {
692 header.value.clear();
693 header.value_encrypted = None;
694 }
695 }
696 }
697 bamboo_domain::mcp_config::TransportConfig::StreamableHttp(config) => {
698 for header in &mut config.headers {
699 if header.credential_ref.is_some() {
700 header.value.clear();
701 header.value_encrypted = None;
702 }
703 }
704 }
705 }
706 }
707 }
708
709 pub fn hydrate_env_vars_from_encrypted(&mut self) {
713 for entry in &mut self.env_vars {
714 if !entry.secret {
715 continue;
716 }
717 if !entry.value.trim().is_empty() {
718 continue;
720 }
721 let Some(encrypted) = &entry.value_encrypted else {
722 continue;
723 };
724 match crate::encryption::decrypt(encrypted) {
725 Ok(value) => entry.value = value,
726 Err(e) => tracing::warn!("Failed to decrypt env var '{}': {}", entry.name, e),
727 }
728 }
729 }
730
731 pub fn hydrate_env_var_credentials_from_store(
735 &mut self,
736 data_dir: &std::path::Path,
737 ) -> crate::ConfigStoreResult<()> {
738 let store = crate::CredentialStore::open(data_dir);
739 self.hydrate_env_var_credentials_from_resolver(&store)
740 }
741
742 pub(crate) fn hydrate_env_var_credentials_from_snapshot(
743 &mut self,
744 snapshot: &crate::credential_store::CredentialDocumentLkg,
745 ) -> crate::ConfigStoreResult<()> {
746 self.hydrate_env_var_credentials_from_resolver(snapshot)
747 }
748
749 fn hydrate_env_var_credentials_from_resolver(
750 &mut self,
751 resolver: &impl CredentialResolver,
752 ) -> crate::ConfigStoreResult<()> {
753 for entry in &mut self.env_vars {
754 if !entry.secret {
755 entry.credential_ref = None;
756 entry.configured = !entry.value.is_empty();
757 continue;
758 }
759 let Some(reference) = entry.credential_ref.as_ref() else {
760 entry.configured = false;
761 continue;
762 };
763 match resolver.resolve(reference)? {
764 Some(secret) => {
765 entry.value = secret.expose().to_string();
766 entry.configured = true;
767 }
768 None if entry.configured => {
769 return Err(crate::ConfigStoreError::Validation(
770 "referenced env credential is unavailable".to_string(),
771 ));
772 }
773 None => {
774 entry.value.clear();
775 }
776 }
777 }
778 Ok(())
779 }
780
781 pub fn refresh_env_vars_encrypted(&mut self) -> Result<()> {
783 for entry in &mut self.env_vars {
784 if entry.secret && entry.credential_ref.is_some() {
785 entry.value_encrypted = None;
786 } else if entry.secret && !entry.value.trim().is_empty() {
787 entry.value_encrypted = Some(
788 crate::encryption::encrypt(&entry.value)
789 .with_context(|| format!("Failed to encrypt env var '{}'", entry.name))?,
790 );
791 } else if !entry.secret {
792 entry.value_encrypted = None;
793 }
794 }
795 Ok(())
796 }
797
798 pub fn sanitize_env_vars_for_disk(&mut self) {
800 for entry in &mut self.env_vars {
801 if entry.secret {
802 entry.value = String::new();
803 entry.value_encrypted = None;
804 } else {
805 entry.credential_ref = None;
806 entry.configured = !entry.value.is_empty();
807 }
808 }
809 }
810
811 pub fn hydrate_broker_token_from_encrypted(&mut self) {
815 let Some(broker) = self.subagents.broker.as_mut() else {
816 return;
817 };
818 if !broker.token.trim().is_empty() {
819 return; }
821 if let Some(encrypted) = &broker.token_encrypted {
822 match crate::encryption::decrypt(encrypted) {
823 Ok(value) => broker.token = value,
824 Err(e) => tracing::warn!("Failed to decrypt broker token: {}", e),
825 }
826 }
827 }
828
829 pub fn refresh_broker_token_encrypted(&mut self) -> Result<()> {
831 let Some(broker) = self.subagents.broker.as_mut() else {
832 return Ok(());
833 };
834 if broker.token.trim().is_empty() {
835 return Ok(());
837 }
838 broker.token_encrypted = Some(
839 crate::encryption::encrypt(&broker.token).context("Failed to encrypt broker token")?,
840 );
841 Ok(())
842 }
843
844 pub fn sanitize_broker_token_for_disk(&mut self) {
846 if let Some(broker) = self.subagents.broker.as_mut() {
847 broker.token = String::new();
848 }
849 }
850
851 pub fn hydrate_notifications_from_encrypted(&mut self) {
857 let ntfy = &mut self.notifications.ntfy;
858 if ntfy
859 .token
860 .as_deref()
861 .map(str::trim)
862 .unwrap_or("")
863 .is_empty()
864 {
865 if let Some(encrypted) = ntfy.token_encrypted.as_deref() {
866 match crate::encryption::decrypt(encrypted) {
867 Ok(value) => ntfy.token = Some(value),
868 Err(e) => tracing::warn!("Failed to decrypt ntfy token: {}", e),
869 }
870 }
871 }
872
873 let bark = &mut self.notifications.bark;
874 if bark
875 .device_key
876 .as_deref()
877 .map(str::trim)
878 .unwrap_or("")
879 .is_empty()
880 {
881 if let Some(encrypted) = bark.device_key_encrypted.as_deref() {
882 match crate::encryption::decrypt(encrypted) {
883 Ok(value) => bark.device_key = Some(value),
884 Err(e) => tracing::warn!("Failed to decrypt Bark device key: {}", e),
885 }
886 }
887 }
888 }
889
890 pub fn hydrate_notification_credentials_from_store(
895 &mut self,
896 data_dir: &std::path::Path,
897 ) -> crate::ConfigStoreResult<()> {
898 let store = crate::CredentialStore::open(data_dir);
899 self.hydrate_notification_credentials_from_resolver(&store)
900 }
901
902 pub(crate) fn hydrate_notification_credentials_from_snapshot(
903 &mut self,
904 snapshot: &crate::credential_store::CredentialDocumentLkg,
905 ) -> crate::ConfigStoreResult<()> {
906 self.hydrate_notification_credentials_from_resolver(snapshot)
907 }
908
909 fn hydrate_notification_credentials_from_resolver(
910 &mut self,
911 resolver: &impl CredentialResolver,
912 ) -> crate::ConfigStoreResult<()> {
913 let reference_counts = crate::credential_store::config_credential_ref_counts(self)?;
914 for reference in [
915 self.notifications.ntfy.credential_ref.as_ref(),
916 self.notifications.bark.credential_ref.as_ref(),
917 ]
918 .into_iter()
919 .flatten()
920 {
921 if reference_counts.get(reference).copied() != Some(1) {
922 return Err(crate::ConfigStoreError::Validation(
923 "notification credential reference is shared by another config consumer"
924 .to_string(),
925 ));
926 }
927 }
928 let ntfy = &mut self.notifications.ntfy;
929 if let Some(reference) = ntfy.credential_ref.as_ref() {
930 match resolver.resolve(reference)? {
931 Some(secret) => {
932 ntfy.token = Some(secret.expose().to_string());
933 ntfy.configured = true;
934 }
935 None if ntfy.configured => {
936 return Err(crate::ConfigStoreError::Validation(
937 "referenced ntfy credential is unavailable".to_string(),
938 ));
939 }
940 None => ntfy.token = None,
941 }
942 } else if ntfy.configured {
943 return Err(crate::ConfigStoreError::Validation(
944 "configured ntfy credential reference is missing".to_string(),
945 ));
946 }
947
948 let bark = &mut self.notifications.bark;
949 if let Some(reference) = bark.credential_ref.as_ref() {
950 match resolver.resolve(reference)? {
951 Some(secret) => {
952 bark.device_key = Some(secret.expose().to_string());
953 bark.configured = true;
954 }
955 None if bark.configured => {
956 return Err(crate::ConfigStoreError::Validation(
957 "referenced Bark credential is unavailable".to_string(),
958 ));
959 }
960 None => bark.device_key = None,
961 }
962 } else if bark.configured {
963 return Err(crate::ConfigStoreError::Validation(
964 "configured Bark credential reference is missing".to_string(),
965 ));
966 }
967 Ok(())
968 }
969
970 pub fn refresh_notifications_encrypted(&mut self) -> Result<()> {
974 let ntfy = &mut self.notifications.ntfy;
975 if ntfy.credential_ref.is_some() {
976 ntfy.token_encrypted = None;
977 ntfy.configured = ntfy
978 .token
979 .as_deref()
980 .is_some_and(|value| !value.trim().is_empty())
981 || ntfy.configured;
982 } else {
983 let token = ntfy.token.as_deref().unwrap_or("").trim();
984 if !token.is_empty() {
985 ntfy.token_encrypted = Some(
986 crate::encryption::encrypt(token).context("Failed to encrypt ntfy token")?,
987 );
988 }
989 }
990
991 let bark = &mut self.notifications.bark;
992 if bark.credential_ref.is_some() {
993 bark.device_key_encrypted = None;
994 bark.configured = bark
995 .device_key
996 .as_deref()
997 .is_some_and(|value| !value.trim().is_empty())
998 || bark.configured;
999 } else {
1000 let device_key = bark.device_key.as_deref().unwrap_or("").trim();
1001 if !device_key.is_empty() {
1002 bark.device_key_encrypted = Some(
1003 crate::encryption::encrypt(device_key)
1004 .context("Failed to encrypt Bark device key")?,
1005 );
1006 }
1007 }
1008
1009 Ok(())
1010 }
1011
1012 pub fn sanitize_notifications_for_disk(&mut self) {
1016 self.notifications.ntfy.token = None;
1017 self.notifications.ntfy.token_encrypted = None;
1018 self.notifications.bark.device_key = None;
1019 self.notifications.bark.device_key_encrypted = None;
1020 }
1021
1022 pub fn hydrate_connect_platform_tokens_from_encrypted(&mut self) {
1030 for platform in &mut self.connect.platforms {
1031 let has_plaintext = platform
1032 .token
1033 .as_deref()
1034 .map(str::trim)
1035 .map(|value| !value.is_empty())
1036 .unwrap_or(false);
1037 if !has_plaintext {
1038 if let Some(encrypted) = platform.token_encrypted.as_deref() {
1039 match crate::encryption::decrypt(encrypted) {
1040 Ok(value) => platform.token = Some(value),
1041 Err(e) => tracing::warn!(
1042 "Failed to decrypt connect platform '{}' token: {}",
1043 platform.platform_type,
1044 e
1045 ),
1046 }
1047 }
1048 }
1049
1050 let has_app_secret_plaintext = platform
1051 .app_secret
1052 .as_deref()
1053 .map(str::trim)
1054 .map(|value| !value.is_empty())
1055 .unwrap_or(false);
1056 if !has_app_secret_plaintext {
1057 if let Some(encrypted) = platform.app_secret_encrypted.as_deref() {
1058 match crate::encryption::decrypt(encrypted) {
1059 Ok(value) => platform.app_secret = Some(value),
1060 Err(e) => tracing::warn!(
1061 "Failed to decrypt connect platform '{}' app_secret: {}",
1062 platform.platform_type,
1063 e
1064 ),
1065 }
1066 }
1067 }
1068 }
1069 }
1070
1071 pub fn hydrate_connect_credentials_from_store(
1076 &mut self,
1077 data_dir: &std::path::Path,
1078 ) -> crate::ConfigStoreResult<()> {
1079 let store = crate::CredentialStore::open(data_dir);
1080 let allow_legacy_runtime_value = !crate::section_layout_is_active(data_dir)?;
1081 self.hydrate_connect_credentials_from_resolver(&store, allow_legacy_runtime_value)
1082 }
1083
1084 pub(crate) fn hydrate_connect_credentials_from_snapshot(
1085 &mut self,
1086 snapshot: &crate::credential_store::CredentialDocumentLkg,
1087 ) -> crate::ConfigStoreResult<()> {
1088 self.hydrate_connect_credentials_from_resolver(snapshot, false)
1089 }
1090
1091 fn hydrate_connect_credentials_from_resolver(
1092 &mut self,
1093 resolver: &impl CredentialResolver,
1094 allow_legacy_runtime_value: bool,
1095 ) -> crate::ConfigStoreResult<()> {
1096 for platform in &mut self.connect.platforms {
1097 hydrate_optional_connect_secret(
1098 resolver,
1099 platform.token_credential_ref.as_ref(),
1100 platform.token_configured,
1101 &mut platform.token,
1102 allow_legacy_runtime_value,
1103 )?;
1104 hydrate_optional_connect_secret(
1105 resolver,
1106 platform.app_secret_credential_ref.as_ref(),
1107 platform.app_secret_configured,
1108 &mut platform.app_secret,
1109 allow_legacy_runtime_value,
1110 )?;
1111 platform.token_encrypted = None;
1112 platform.app_secret_encrypted = None;
1113 }
1114 Ok(())
1115 }
1116
1117 pub fn hydrate_access_control_credentials_from_store(
1121 &mut self,
1122 data_dir: &std::path::Path,
1123 ) -> crate::ConfigStoreResult<()> {
1124 let store = crate::CredentialStore::open(data_dir);
1125 self.hydrate_access_control_credentials_from_resolver(&store)
1126 }
1127
1128 pub(crate) fn hydrate_access_control_credentials_from_snapshot(
1129 &mut self,
1130 snapshot: &crate::credential_store::CredentialDocumentLkg,
1131 ) -> crate::ConfigStoreResult<()> {
1132 self.hydrate_access_control_credentials_from_resolver(snapshot)
1133 }
1134
1135 fn hydrate_access_control_credentials_from_resolver(
1136 &mut self,
1137 resolver: &impl CredentialResolver,
1138 ) -> crate::ConfigStoreResult<()> {
1139 let Some(access) = self.access_control.as_mut() else {
1140 return Ok(());
1141 };
1142 match access.password_credential_ref.as_ref() {
1143 Some(reference) => {
1144 if reference != &access_password_credential_ref()? || !access.password_configured {
1145 return Err(crate::ConfigStoreError::Validation(
1146 "access-control password credential metadata is invalid".to_string(),
1147 ));
1148 }
1149 let secret = resolver.resolve(reference)?.ok_or_else(|| {
1150 crate::ConfigStoreError::Validation(
1151 "access-control password verifier is unavailable".to_string(),
1152 )
1153 })?;
1154 let record = decode_access_verifier(secret.expose())?;
1155 access.password_hash = Some(record.hash);
1156 access.password_salt = Some(record.salt);
1157 }
1158 None if access.password_configured || access.password_enabled => {
1159 return Err(crate::ConfigStoreError::Validation(
1160 "access-control password verifier metadata is incomplete".to_string(),
1161 ));
1162 }
1163 None => {
1164 access.password_hash = None;
1165 access.password_salt = None;
1166 }
1167 }
1168 for device in &mut access.devices {
1169 let reference = device.token_credential_ref.as_ref().ok_or_else(|| {
1170 crate::ConfigStoreError::Validation(
1171 "access-control device verifier metadata is incomplete".to_string(),
1172 )
1173 })?;
1174 if reference != &access_device_credential_ref(&device.device_id)?
1175 || !device.token_configured
1176 {
1177 return Err(crate::ConfigStoreError::Validation(
1178 "access-control device credential metadata is invalid".to_string(),
1179 ));
1180 }
1181 let secret = resolver.resolve(reference)?.ok_or_else(|| {
1182 crate::ConfigStoreError::Validation(
1183 "access-control device verifier is unavailable".to_string(),
1184 )
1185 })?;
1186 let record = decode_access_verifier(secret.expose())?;
1187 device.token_hash = record.hash;
1188 device.token_salt = record.salt;
1189 }
1190 Ok(())
1191 }
1192
1193 pub fn clear_access_control_runtime_verifiers(&mut self) {
1194 if let Some(access) = self.access_control.as_mut() {
1195 access.password_hash = None;
1196 access.password_salt = None;
1197 for device in &mut access.devices {
1198 device.token_hash.clear();
1199 device.token_salt.clear();
1200 }
1201 }
1202 }
1203
1204 pub fn sanitize_access_control_for_disk(&mut self) {
1206 self.clear_access_control_runtime_verifiers();
1207 }
1208
1209 pub fn refresh_connect_platform_tokens_encrypted(&mut self) -> Result<()> {
1215 for platform in &mut self.connect.platforms {
1216 if platform.token_credential_ref.is_some() {
1217 platform.token_encrypted = None;
1218 }
1219 let token = platform.token.as_deref().unwrap_or("").trim();
1220 if platform.token_credential_ref.is_none() && !token.is_empty() {
1221 platform.token_encrypted =
1222 Some(crate::encryption::encrypt(token).with_context(|| {
1223 format!(
1224 "Failed to encrypt connect platform '{}' token",
1225 platform.platform_type
1226 )
1227 })?);
1228 }
1229
1230 if platform.app_secret_credential_ref.is_some() {
1231 platform.app_secret_encrypted = None;
1232 }
1233 let app_secret = platform.app_secret.as_deref().unwrap_or("").trim();
1234 if platform.app_secret_credential_ref.is_none() && !app_secret.is_empty() {
1235 platform.app_secret_encrypted =
1236 Some(crate::encryption::encrypt(app_secret).with_context(|| {
1237 format!(
1238 "Failed to encrypt connect platform '{}' app_secret",
1239 platform.platform_type
1240 )
1241 })?);
1242 }
1243 }
1244 Ok(())
1245 }
1246
1247 pub fn sanitize_connect_credentials_for_disk(&mut self) {
1250 for platform in &mut self.connect.platforms {
1251 platform.token = None;
1252 platform.token_encrypted = None;
1253 platform.app_secret = None;
1254 platform.app_secret_encrypted = None;
1255 }
1256 }
1257
1258 pub fn preserve_env_sourced_provider_keys(&mut self, previous: &Config) {
1274 macro_rules! restore_env_key {
1275 ($field:ident) => {
1276 if let (Some(current), Some(prev)) = (
1277 self.providers.$field.as_mut(),
1278 previous.providers.$field.as_ref(),
1279 ) {
1280 if prev.api_key_from_env && current.api_key.trim().is_empty() {
1281 current.api_key = prev.api_key.clone();
1282 current.api_key_from_env = true;
1283 }
1284 }
1285 };
1286 }
1287 restore_env_key!(openai);
1288 restore_env_key!(anthropic);
1289 restore_env_key!(gemini);
1290 }
1291
1292 pub fn preserve_provider_instance_plaintext_keys(
1302 &mut self,
1303 previous: &Config,
1304 intents: &ProviderApiKeyIntents,
1305 ) {
1306 for (id, instance) in self.provider_instances.iter_mut() {
1307 if intents.provider_instances.contains(id) {
1308 continue;
1309 }
1310 if !instance.api_key.trim().is_empty() || instance.api_key_encrypted.is_some() {
1311 continue;
1312 }
1313 if let Some(previous) = previous.provider_instances.get(id) {
1314 if !previous.api_key.trim().is_empty() || previous.api_key_encrypted.is_some() {
1315 instance.api_key = previous.api_key.clone();
1316 instance.api_key_encrypted = previous.api_key_encrypted.clone();
1317 }
1318 }
1319 }
1320 }
1321
1322 pub fn refresh_encrypted_secrets(&mut self) -> Result<()> {
1335 self.refresh_proxy_auth_encrypted()?;
1336 self.refresh_provider_api_keys_encrypted()?;
1337 self.refresh_provider_instance_api_keys_encrypted()?;
1338 self.refresh_env_vars_encrypted()?;
1339 self.refresh_cluster_fabric_encrypted()?;
1340 self.refresh_notifications_encrypted()?;
1341 self.refresh_connect_platform_tokens_encrypted()?;
1342 Ok(())
1343 }
1344}
1345
1346fn hydrate_optional_connect_secret(
1347 resolver: &impl CredentialResolver,
1348 reference: Option<&crate::CredentialRef>,
1349 configured: bool,
1350 target: &mut Option<String>,
1351 allow_legacy_runtime_value: bool,
1352) -> crate::ConfigStoreResult<()> {
1353 match reference {
1354 Some(reference) => match resolver.resolve(reference)? {
1355 Some(secret) => *target = Some(secret.expose().to_string()),
1356 None if configured => {
1357 return Err(crate::ConfigStoreError::Validation(
1358 "referenced connect credential is unavailable".to_string(),
1359 ));
1360 }
1361 None => *target = None,
1362 },
1363 None if configured => {
1364 return Err(crate::ConfigStoreError::Validation(
1365 "connect credential metadata is inconsistent".to_string(),
1366 ));
1367 }
1368 None if allow_legacy_runtime_value && target.is_some() => {}
1369 None => *target = None,
1370 }
1371 Ok(())
1372}