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