bamboo_server/
config_manager.rs1use serde_json::{Map, Value};
21
22use crate::error::AppError;
23use bamboo_infrastructure::patch::ProviderApiKeyIntents;
24use bamboo_infrastructure::Config;
25
26pub use bamboo_infrastructure::patch::{
29 deep_merge_json, domains_for_root_patch, effects_for_root_patch, is_masked_api_key,
30 preserve_masked_provider_api_keys, provider_api_key_intents, sanitize_root_patch,
31 DomainChanges, PatchEffects, ReloadMode,
32};
33
34pub fn sync_provider_api_keys_encrypted_for_patch(
35 config: &mut Config,
36 intents: &ProviderApiKeyIntents,
37) -> Result<(), AppError> {
38 for name in intents.providers.iter() {
39 match name.as_str() {
40 "openai" => {
41 if let Some(openai) = config.providers.openai.as_mut() {
42 let api_key = openai.api_key.trim();
43 openai.api_key_encrypted = if api_key.is_empty() {
44 None
45 } else {
46 Some(
47 bamboo_infrastructure::encryption::encrypt(api_key).map_err(|e| {
48 AppError::InternalError(anyhow::anyhow!(
49 "Failed to encrypt OpenAI api_key: {e}"
50 ))
51 })?,
52 )
53 };
54 }
55 }
56 "anthropic" => {
57 if let Some(anthropic) = config.providers.anthropic.as_mut() {
58 let api_key = anthropic.api_key.trim();
59 anthropic.api_key_encrypted = if api_key.is_empty() {
60 None
61 } else {
62 Some(
63 bamboo_infrastructure::encryption::encrypt(api_key).map_err(|e| {
64 AppError::InternalError(anyhow::anyhow!(
65 "Failed to encrypt Anthropic api_key: {e}"
66 ))
67 })?,
68 )
69 };
70 }
71 }
72 "gemini" => {
73 if let Some(gemini) = config.providers.gemini.as_mut() {
74 let api_key = gemini.api_key.trim();
75 gemini.api_key_encrypted = if api_key.is_empty() {
76 None
77 } else {
78 Some(
79 bamboo_infrastructure::encryption::encrypt(api_key).map_err(|e| {
80 AppError::InternalError(anyhow::anyhow!(
81 "Failed to encrypt Gemini api_key: {e}"
82 ))
83 })?,
84 )
85 };
86 }
87 }
88 "bodhi" => {
89 if let Some(bodhi) = config.providers.bodhi.as_mut() {
90 let api_key = bodhi.api_key.trim();
91 bodhi.api_key_encrypted = if api_key.is_empty() {
92 None
93 } else {
94 Some(
95 bamboo_infrastructure::encryption::encrypt(api_key).map_err(|e| {
96 AppError::InternalError(anyhow::anyhow!(
97 "Failed to encrypt Bodhi api_key: {e}"
98 ))
99 })?,
100 )
101 };
102 }
103 }
104 _ => {}
105 }
106 }
107
108 for instance_id in intents.provider_instances.iter() {
109 if let Some(instance) = config.provider_instances.get_mut(instance_id) {
110 let api_key = instance.api_key.trim();
111 instance.api_key_encrypted = if api_key.is_empty() {
112 None
113 } else {
114 Some(
115 bamboo_infrastructure::encryption::encrypt(api_key).map_err(|e| {
116 AppError::InternalError(anyhow::anyhow!(
117 "Failed to encrypt provider instance api_key for '{instance_id}': {e}"
118 ))
119 })?,
120 )
121 };
122 }
123 }
124
125 Ok(())
126}
127
128pub fn assert_json_object(value: Value) -> Result<Map<String, Value>, AppError> {
129 match value {
130 Value::Object(map) => Ok(map),
131 _ => Err(AppError::BadRequest(
132 "config.json must be a JSON object".to_string(),
133 )),
134 }
135}
136
137pub fn build_merged_config(
138 current: &Config,
139 patch_obj: Map<String, Value>,
140) -> Result<Config, AppError> {
141 let mut merged = serde_json::to_value(current)
142 .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to serialize config: {e}")))?;
143
144 deep_merge_json(&mut merged, Value::Object(patch_obj));
145
146 let mut new_config: Config = serde_json::from_value(merged)
147 .map_err(|e| AppError::BadRequest(format!("Invalid configuration JSON: {e}")))?;
148 new_config.hydrate_proxy_auth_from_encrypted();
149 new_config.hydrate_provider_api_keys_from_encrypted();
150 new_config.hydrate_provider_instance_api_keys_from_encrypted();
151 new_config.hydrate_mcp_secrets_from_encrypted();
152 new_config.hydrate_env_vars_from_encrypted();
153 new_config.normalize_tool_settings();
154 new_config.normalize_skill_settings();
155
156 Ok(new_config)
157}