use super::*;
impl AppState {
pub async fn reload_provider(&self) -> Result<(), bamboo_llm::LLMError> {
let config = self.config.read().await.clone();
self.provider_registry
.reload_from_config(&config, self.app_data_dir.clone())
.await?;
let default_provider_name = self.provider_registry.default_provider_name();
tracing::info!(
default_provider = %default_provider_name,
legacy_provider = %config.provider,
has_provider_instances = config.has_provider_instances(),
"Reloading provider runtime from current config"
);
let new_provider = self.provider_registry.get_default().unwrap_or_else(|| {
let message = if config.has_provider_instances() {
format!(
"Default provider instance '{}' is not available or failed to initialize",
default_provider_name
)
} else {
format!(
"Provider '{}' is not available or failed to initialize",
config.provider
)
};
Arc::new(UnconfiguredProvider { message }) as Arc<dyn LLMProvider>
});
let mut provider = self.provider.write().await;
*provider = new_provider;
tracing::info!(
default_provider = %default_provider_name,
"Provider reloaded successfully"
);
Ok(())
}
pub async fn reload_config(&self) -> Config {
let _io = self.config_io_lock.lock().await;
let mut config = self.config.write().await;
let new_config = Config::from_data_dir(Some(self.app_data_dir.clone()));
*config = new_config.clone();
new_config
}
async fn persist_config_snapshot(&self, config: Config) -> anyhow::Result<()> {
let data_dir = self.app_data_dir.clone();
tokio::task::spawn_blocking(move || config.save_to_dir(data_dir))
.await
.map_err(|e| anyhow::anyhow!("Config save task failed: {e}"))??;
Ok(())
}
pub async fn update_config<F>(
&self,
update: F,
effects: ConfigUpdateEffects,
) -> Result<Config, AppError>
where
F: FnOnce(&mut Config) -> Result<(), AppError>,
{
let snapshot = {
let _io = self.config_io_lock.lock().await;
let (snapshot, enforcement_newly_off) = {
let mut cfg = self.config.write().await;
reject_if_recovery_pending(&cfg)?;
let was_off = cfg.plugin_trust.enforcement_is_off();
update(&mut cfg)?;
cfg.publish_env_vars();
let newly_off = !was_off && cfg.plugin_trust.enforcement_is_off();
(cfg.clone(), newly_off)
};
if enforcement_newly_off {
warn_plugin_trust_enforcement_off();
}
self.persist_config_snapshot(snapshot.clone())
.await
.map_err(|e| {
AppError::InternalError(anyhow::anyhow!("Failed to save config: {e}"))
})?;
snapshot
};
self.apply_config_effects(snapshot.clone(), effects).await?;
Ok(snapshot)
}
pub async fn replace_config(
&self,
new_config: Config,
effects: ConfigUpdateEffects,
) -> Result<Config, AppError> {
{
let _io = self.config_io_lock.lock().await;
let enforcement_newly_off = {
let mut cfg = self.config.write().await;
reject_if_recovery_pending(&cfg)?;
let was_off = cfg.plugin_trust.enforcement_is_off();
*cfg = new_config.clone();
cfg.publish_env_vars();
!was_off && cfg.plugin_trust.enforcement_is_off()
};
if enforcement_newly_off {
warn_plugin_trust_enforcement_off();
}
self.persist_config_snapshot(new_config.clone())
.await
.map_err(|e| {
AppError::InternalError(anyhow::anyhow!("Failed to save config: {e}"))
})?;
}
self.apply_config_effects(new_config.clone(), effects)
.await?;
Ok(new_config)
}
async fn apply_config_effects(
&self,
new_config: Config,
effects: ConfigUpdateEffects,
) -> Result<(), AppError> {
if effects.reload_provider {
self.reload_provider().await.map_err(|e| {
AppError::InternalError(anyhow::anyhow!(
"Failed to reload provider after updating config: {e}"
))
})?;
}
if effects.reconcile_mcp {
self.mcp_manager
.reconcile_from_config(&new_config.mcp)
.await;
}
Ok(())
}
pub async fn confirm_config_recovery(&self, accept: bool) -> Result<Config, AppError> {
let _io = self.config_io_lock.lock().await;
if !accept {
let cfg = self.config.read().await;
return match cfg.recovery_status() {
Some(_) => Ok(cfg.clone()),
None => Err(AppError::BadRequest(
"No pending config-corruption recovery to resolve".to_string(),
)),
};
}
let mut candidate = {
let cfg = self.config.read().await;
match cfg.recovery_status() {
Some(_) => cfg.clone(),
None => {
return Err(AppError::BadRequest(
"No pending config-corruption recovery to resolve".to_string(),
))
}
}
};
let data_dir = self.app_data_dir.clone();
candidate = tokio::task::spawn_blocking(move || {
candidate
.confirm_recovery_and_save_to_dir(data_dir)
.map(|_| candidate)
})
.await
.map_err(|e| {
AppError::InternalError(anyhow::anyhow!("Config recovery-confirm task failed: {e}"))
})?
.map_err(|e| {
AppError::InternalError(anyhow::anyhow!("Failed to save recovered config: {e}"))
})?;
{
let mut cfg = self.config.write().await;
*cfg = candidate.clone();
cfg.publish_env_vars();
}
Ok(candidate)
}
}
fn reject_if_recovery_pending(cfg: &Config) -> Result<(), AppError> {
if let Some(status) = cfg.recovery_status() {
if !status.confirmed {
return Err(AppError::ConfigRecoveryPending(format!(
"config.json was recovered from corruption ({:?}) and is awaiting \
confirmation; confirm or reject the recovery (see /bamboo/config/recovery-status \
and /bamboo/config/recovery/confirm) before changing settings",
status.source
)));
}
}
Ok(())
}
pub(crate) fn warn_plugin_trust_enforcement_off() {
tracing::warn!(
"plugin_trust.enforcement is OFF — plugin installs from ANY URL are accepted \
without host/signature/checksum verification (config.json plugin_trust.enforcement)"
);
}