Persist dynamic extension config so we can resume recipe sessions w/ extensions (#4331)

This commit is contained in:
Will Pfleger
2025-10-04 17:17:12 -04:00
committed by GitHub
parent 51651550e0
commit a59fbab69c
14 changed files with 357 additions and 305 deletions
+123 -116
View File
@@ -1,7 +1,6 @@
use super::base::Config;
use crate::agents::extension::PLATFORM_EXTENSIONS;
use crate::agents::ExtensionConfig;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
@@ -28,132 +27,140 @@ pub fn name_to_key(name: &str) -> String {
.to_lowercase()
}
pub struct ExtensionConfigManager;
fn get_extensions_map() -> HashMap<String, ExtensionEntry> {
let raw: Value = Config::global()
.get_param::<Value>(EXTENSIONS_CONFIG_KEY)
.unwrap_or_else(|err| {
warn!(
"Failed to load {}: {err}. Falling back to empty object.",
EXTENSIONS_CONFIG_KEY
);
Value::Object(serde_json::Map::new())
});
impl ExtensionConfigManager {
fn get_extensions_map() -> Result<HashMap<String, ExtensionEntry>> {
let raw: Value = Config::global()
.get_param::<Value>(EXTENSIONS_CONFIG_KEY)
.unwrap_or_else(|err| {
warn!(
"Failed to load {}: {err}. Falling back to empty object.",
EXTENSIONS_CONFIG_KEY
);
Value::Object(serde_json::Map::new())
});
let mut extensions_map: HashMap<String, ExtensionEntry> = match raw {
Value::Object(obj) => {
let mut m = HashMap::with_capacity(obj.len());
for (k, mut v) in obj {
if let Value::Object(ref mut inner) = v {
match inner.get("description") {
Some(Value::Null) | None => {
inner.insert(
"description".to_string(),
Value::String(String::new()),
);
}
_ => {}
}
}
match serde_json::from_value::<ExtensionEntry>(v.clone()) {
Ok(entry) => {
m.insert(k, entry);
}
Err(err) => {
let bad_json = serde_json::to_string(&v).unwrap_or_else(|e| {
format!("<failed to serialize malformed value: {e}>")
});
warn!(
extension = %k,
error = %err,
bad_json = %bad_json,
"Skipping malformed extension"
);
let mut extensions_map: HashMap<String, ExtensionEntry> = match raw {
Value::Object(obj) => {
let mut m = HashMap::with_capacity(obj.len());
for (k, mut v) in obj {
if let Value::Object(ref mut inner) = v {
match inner.get("description") {
Some(Value::Null) | None => {
inner.insert("description".to_string(), Value::String(String::new()));
}
_ => {}
}
}
match serde_json::from_value::<ExtensionEntry>(v.clone()) {
Ok(entry) => {
m.insert(k, entry);
}
Err(err) => {
let bad_json = serde_json::to_string(&v).unwrap_or_else(|e| {
format!("<failed to serialize malformed value: {e}>")
});
warn!(
extension = %k,
error = %err,
bad_json = %bad_json,
"Skipping malformed extension"
);
}
}
m
}
other => {
warn!(
"Expected object for {}, got {}. Using empty map.",
EXTENSIONS_CONFIG_KEY, other
);
HashMap::new()
}
};
m
}
other => {
warn!(
"Expected object for {}, got {}. Using empty map.",
EXTENSIONS_CONFIG_KEY, other
);
HashMap::new()
}
};
if !extensions_map.is_empty() {
for (name, def) in PLATFORM_EXTENSIONS.iter() {
if !extensions_map.contains_key(*name) {
extensions_map.insert(
name.to_string(),
ExtensionEntry {
config: ExtensionConfig::Platform {
name: def.name.to_string(),
description: def.description.to_string(),
bundled: Some(true),
available_tools: Vec::new(),
},
enabled: true,
if !extensions_map.is_empty() {
for (name, def) in PLATFORM_EXTENSIONS.iter() {
if !extensions_map.contains_key(*name) {
extensions_map.insert(
name.to_string(),
ExtensionEntry {
config: ExtensionConfig::Platform {
name: def.name.to_string(),
description: def.description.to_string(),
bundled: Some(true),
available_tools: Vec::new(),
},
);
}
enabled: true,
},
);
}
}
Ok(extensions_map)
}
extensions_map
}
fn save_extensions_map(extensions: HashMap<String, ExtensionEntry>) -> Result<()> {
let config = Config::global();
config.set_param(EXTENSIONS_CONFIG_KEY, serde_json::to_value(extensions)?)?;
Ok(())
}
pub fn get_config_by_name(name: &str) -> Result<Option<ExtensionConfig>> {
let extensions = Self::get_extensions_map()?;
Ok(extensions
.values()
.find(|entry| entry.config.name() == name)
.map(|entry| entry.config.clone()))
}
pub fn set(entry: ExtensionEntry) -> Result<()> {
let mut extensions = Self::get_extensions_map()?;
let key = entry.config.key();
extensions.insert(key, entry);
Self::save_extensions_map(extensions)
}
pub fn remove(key: &str) -> Result<()> {
let mut extensions = Self::get_extensions_map()?;
extensions.remove(key);
Self::save_extensions_map(extensions)
}
pub fn set_enabled(key: &str, enabled: bool) -> Result<()> {
let mut extensions = Self::get_extensions_map()?;
if let Some(entry) = extensions.get_mut(key) {
entry.enabled = enabled;
Self::save_extensions_map(extensions)?;
fn save_extensions_map(extensions: HashMap<String, ExtensionEntry>) {
let config = Config::global();
match serde_json::to_value(extensions) {
Ok(value) => {
if let Err(e) = config.set_param(EXTENSIONS_CONFIG_KEY, value) {
tracing::debug!("Failed to save extensions config: {}", e);
}
}
Err(e) => {
tracing::debug!("Failed to serialize extensions: {}", e);
}
Ok(())
}
pub fn get_all() -> Result<Vec<ExtensionEntry>> {
let extensions = Self::get_extensions_map()?;
Ok(extensions.into_values().collect())
}
pub fn get_all_names() -> Result<Vec<String>> {
let extensions = Self::get_extensions_map()?;
Ok(extensions.keys().cloned().collect())
}
pub fn is_enabled(key: &str) -> Result<bool> {
let extensions = Self::get_extensions_map()?;
Ok(extensions.get(key).map(|e| e.enabled).unwrap_or(false))
}
}
pub fn get_extension_by_name(name: &str) -> Option<ExtensionConfig> {
let extensions = get_extensions_map();
extensions
.values()
.find(|entry| entry.config.name() == name)
.map(|entry| entry.config.clone())
}
pub fn set_extension(entry: ExtensionEntry) {
let mut extensions = get_extensions_map();
let key = entry.config.key();
extensions.insert(key, entry);
save_extensions_map(extensions);
}
pub fn remove_extension(key: &str) {
let mut extensions = get_extensions_map();
extensions.remove(key);
save_extensions_map(extensions);
}
pub fn set_extension_enabled(key: &str, enabled: bool) {
let mut extensions = get_extensions_map();
if let Some(entry) = extensions.get_mut(key) {
entry.enabled = enabled;
save_extensions_map(extensions);
}
}
pub fn get_all_extensions() -> Vec<ExtensionEntry> {
let extensions = get_extensions_map();
extensions.into_values().collect()
}
pub fn get_all_extension_names() -> Vec<String> {
let extensions = get_extensions_map();
extensions.keys().cloned().collect()
}
pub fn is_extension_enabled(key: &str) -> bool {
let extensions = get_extensions_map();
extensions.get(key).map(|e| e.enabled).unwrap_or(false)
}
pub fn get_enabled_extensions() -> Vec<ExtensionConfig> {
get_all_extensions()
.into_iter()
.filter(|ext| ext.enabled)
.map(|ext| ext.config)
.collect()
}
+4 -1
View File
@@ -10,7 +10,10 @@ pub use crate::agents::ExtensionConfig;
pub use base::{get_config_dir, Config, ConfigError, APP_STRATEGY};
pub use custom_providers::CustomProviderConfig;
pub use experiments::ExperimentManager;
pub use extensions::{ExtensionConfigManager, ExtensionEntry};
pub use extensions::{
get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name,
is_extension_enabled, remove_extension, set_extension, set_extension_enabled, ExtensionEntry,
};
pub use permission::PermissionManager;
pub use signup_openrouter::configure_openrouter;
pub use signup_tetrate::configure_tetrate;