Skip to main content

bamboo_server/
config_manager.rs

1//! Configuration patching helpers.
2//!
3//! The server has multiple endpoints that update different "sections" of the unified `config.json`
4//! (provider, proxy, setup, mcp, etc). These helpers keep patch application consistent and safe:
5//! - sanitize incoming patches (never accept encrypted secret material from clients)
6//! - preserve masked API keys (UI sends placeholders)
7//! - compute which runtime side-effects should run (reload provider / reconcile MCP)
8//!
9//! Pure domain logic (domain types, sanitization, merge) lives in
10//! `bamboo_infrastructure::patch`. This module keeps the infrastructure-coupled
11//! orchestration functions.
12//!
13//! Important design note:
14//! - `/v1/bamboo/config` is a *permissive* config management endpoint used by setup/UX flows.
15//!   It should allow persisting partial config even when the currently-selected provider is
16//!   not fully configured yet.
17//! - Strict provider validation belongs in provider-specific endpoints like
18//!   `/v1/bamboo/settings/provider` (and explicit reload/apply actions).
19
20use serde_json::{Map, Value};
21
22use crate::error::AppError;
23use bamboo_infrastructure::Config;
24
25// Re-export pure domain logic from the config crate so server consumers
26// can import through `config_manager`.
27pub use bamboo_infrastructure::patch::{
28    deep_merge_json, domains_for_root_patch, effects_for_root_patch, is_masked_api_key,
29    preserve_masked_provider_api_keys, provider_api_key_intents, sanitize_root_patch,
30    DomainChanges, PatchEffects, ReloadMode,
31};
32
33pub fn sync_provider_api_keys_encrypted_for_patch(
34    config: &mut Config,
35    providers: &std::collections::BTreeSet<String>,
36) -> Result<(), AppError> {
37    for name in providers.iter() {
38        match name.as_str() {
39            "openai" => {
40                if let Some(openai) = config.providers.openai.as_mut() {
41                    let api_key = openai.api_key.trim();
42                    openai.api_key_encrypted = if api_key.is_empty() {
43                        None
44                    } else {
45                        Some(
46                            bamboo_infrastructure::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            }
55            "anthropic" => {
56                if let Some(anthropic) = config.providers.anthropic.as_mut() {
57                    let api_key = anthropic.api_key.trim();
58                    anthropic.api_key_encrypted = if api_key.is_empty() {
59                        None
60                    } else {
61                        Some(
62                            bamboo_infrastructure::encryption::encrypt(api_key).map_err(|e| {
63                                AppError::InternalError(anyhow::anyhow!(
64                                    "Failed to encrypt Anthropic api_key: {e}"
65                                ))
66                            })?,
67                        )
68                    };
69                }
70            }
71            "gemini" => {
72                if let Some(gemini) = config.providers.gemini.as_mut() {
73                    let api_key = gemini.api_key.trim();
74                    gemini.api_key_encrypted = if api_key.is_empty() {
75                        None
76                    } else {
77                        Some(
78                            bamboo_infrastructure::encryption::encrypt(api_key).map_err(|e| {
79                                AppError::InternalError(anyhow::anyhow!(
80                                    "Failed to encrypt Gemini api_key: {e}"
81                                ))
82                            })?,
83                        )
84                    };
85                }
86            }
87            _ => {}
88        }
89    }
90
91    Ok(())
92}
93
94pub fn assert_json_object(value: Value) -> Result<Map<String, Value>, AppError> {
95    match value {
96        Value::Object(map) => Ok(map),
97        _ => Err(AppError::BadRequest(
98            "config.json must be a JSON object".to_string(),
99        )),
100    }
101}
102
103pub fn build_merged_config(
104    current: &Config,
105    patch_obj: Map<String, Value>,
106) -> Result<Config, AppError> {
107    let mut merged = serde_json::to_value(current)
108        .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to serialize config: {e}")))?;
109
110    deep_merge_json(&mut merged, Value::Object(patch_obj));
111
112    let mut new_config: Config = serde_json::from_value(merged)
113        .map_err(|e| AppError::BadRequest(format!("Invalid configuration JSON: {e}")))?;
114    new_config.hydrate_proxy_auth_from_encrypted();
115    new_config.hydrate_provider_api_keys_from_encrypted();
116    new_config.hydrate_mcp_secrets_from_encrypted();
117    new_config.hydrate_env_vars_from_encrypted();
118    new_config.normalize_tool_settings();
119    new_config.normalize_skill_settings();
120
121    Ok(new_config)
122}