feat: platform extension migrator + code mode rename (#6611)

This commit is contained in:
Alex Hancock
2026-01-29 17:12:51 -05:00
committed by GitHub
parent fb4ac05016
commit edd0109997
14 changed files with 207 additions and 52 deletions
+2 -1
View File
@@ -255,8 +255,9 @@ async fn add_builtins(agent: &Agent, builtins: Vec<String>) {
let config = if PLATFORM_EXTENSIONS.contains_key(builtin.as_str()) { let config = if PLATFORM_EXTENSIONS.contains_key(builtin.as_str()) {
ExtensionConfig::Platform { ExtensionConfig::Platform {
name: builtin.clone(), name: builtin.clone(),
bundled: None,
description: builtin.clone(), description: builtin.clone(),
display_name: None,
bundled: None,
available_tools: Vec::new(), available_tools: Vec::new(),
} }
} else { } else {
+2 -1
View File
@@ -314,8 +314,9 @@ impl CliSession {
if PLATFORM_EXTENSIONS.contains_key(extension_name) { if PLATFORM_EXTENSIONS.contains_key(extension_name) {
ExtensionConfig::Platform { ExtensionConfig::Platform {
name: extension_name.to_string(), name: extension_name.to_string(),
bundled: None,
description: extension_name.to_string(), description: extension_name.to_string(),
display_name: None,
bundled: None,
available_tools: Vec::new(), available_tools: Vec::new(),
} }
} else { } else {
@@ -434,7 +434,7 @@ impl CodeExecutionClient {
}, },
server_info: Implementation { server_info: Implementation {
name: EXTENSION_NAME.to_string(), name: EXTENSION_NAME.to_string(),
title: Some("Code Execution".to_string()), title: Some("Code Mode".to_string()),
version: "1.0.0".to_string(), version: "1.0.0".to_string(),
icons: None, icons: None,
website_url: None, website_url: None,
+10 -1
View File
@@ -48,6 +48,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
todo_extension::EXTENSION_NAME, todo_extension::EXTENSION_NAME,
PlatformExtensionDef { PlatformExtensionDef {
name: todo_extension::EXTENSION_NAME, name: todo_extension::EXTENSION_NAME,
display_name: "Todo",
description: description:
"Enable a todo list for goose so it can keep track of what it is doing", "Enable a todo list for goose so it can keep track of what it is doing",
default_enabled: true, default_enabled: true,
@@ -59,6 +60,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
apps_extension::EXTENSION_NAME, apps_extension::EXTENSION_NAME,
PlatformExtensionDef { PlatformExtensionDef {
name: apps_extension::EXTENSION_NAME, name: apps_extension::EXTENSION_NAME,
display_name: "Apps",
description: description:
"Create and manage custom Goose apps through chat. Apps are HTML/CSS/JavaScript and run in sandboxed windows.", "Create and manage custom Goose apps through chat. Apps are HTML/CSS/JavaScript and run in sandboxed windows.",
default_enabled: true, default_enabled: true,
@@ -70,6 +72,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
chatrecall_extension::EXTENSION_NAME, chatrecall_extension::EXTENSION_NAME,
PlatformExtensionDef { PlatformExtensionDef {
name: chatrecall_extension::EXTENSION_NAME, name: chatrecall_extension::EXTENSION_NAME,
display_name: "Chat Recall",
description: description:
"Search past conversations and load session summaries for contextual memory", "Search past conversations and load session summaries for contextual memory",
default_enabled: false, default_enabled: false,
@@ -83,6 +86,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
"extensionmanager", "extensionmanager",
PlatformExtensionDef { PlatformExtensionDef {
name: extension_manager_extension::EXTENSION_NAME, name: extension_manager_extension::EXTENSION_NAME,
display_name: "Extension Manager",
description: description:
"Enable extension management tools for discovering, enabling, and disabling extensions", "Enable extension management tools for discovering, enabling, and disabling extensions",
default_enabled: true, default_enabled: true,
@@ -94,6 +98,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
skills_extension::EXTENSION_NAME, skills_extension::EXTENSION_NAME,
PlatformExtensionDef { PlatformExtensionDef {
name: skills_extension::EXTENSION_NAME, name: skills_extension::EXTENSION_NAME,
display_name: "Skills",
description: "Load and use skills from relevant directories", description: "Load and use skills from relevant directories",
default_enabled: true, default_enabled: true,
client_factory: |ctx| Box::new(skills_extension::SkillsClient::new(ctx).unwrap()), client_factory: |ctx| Box::new(skills_extension::SkillsClient::new(ctx).unwrap()),
@@ -104,7 +109,9 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
code_execution_extension::EXTENSION_NAME, code_execution_extension::EXTENSION_NAME,
PlatformExtensionDef { PlatformExtensionDef {
name: code_execution_extension::EXTENSION_NAME, name: code_execution_extension::EXTENSION_NAME,
description: "Execute JavaScript code in a sandboxed environment", display_name: "Code Mode",
description:
"Goose will make extension calls through code execution, saving tokens",
default_enabled: false, default_enabled: false,
client_factory: |ctx| { client_factory: |ctx| {
Box::new(code_execution_extension::CodeExecutionClient::new(ctx).unwrap()) Box::new(code_execution_extension::CodeExecutionClient::new(ctx).unwrap())
@@ -158,6 +165,7 @@ impl PlatformExtensionContext {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PlatformExtensionDef { pub struct PlatformExtensionDef {
pub name: &'static str, pub name: &'static str,
pub display_name: &'static str,
pub description: &'static str, pub description: &'static str,
pub default_enabled: bool, pub default_enabled: bool,
pub client_factory: fn(PlatformExtensionContext) -> Box<dyn McpClientTrait>, pub client_factory: fn(PlatformExtensionContext) -> Box<dyn McpClientTrait>,
@@ -335,6 +343,7 @@ pub enum ExtensionConfig {
#[serde(deserialize_with = "deserialize_null_with_default")] #[serde(deserialize_with = "deserialize_null_with_default")]
#[schema(required)] #[schema(required)]
description: String, description: String,
display_name: Option<String>,
#[serde(default)] #[serde(default)]
bundled: Option<bool>, bundled: Option<bool>,
#[serde(default)] #[serde(default)]
+35 -26
View File
@@ -273,25 +273,34 @@ impl Config {
} }
fn load(&self) -> Result<Mapping, ConfigError> { fn load(&self) -> Result<Mapping, ConfigError> {
if self.config_path.exists() { let mut values = if self.config_path.exists() {
self.load_values_with_recovery() self.load_values_with_recovery()?
} else { } else {
// Config file doesn't exist, try to recover from backup first // Config file doesn't exist, try to recover from backup first
tracing::info!("Config file doesn't exist, attempting recovery from backup"); tracing::info!("Config file doesn't exist, attempting recovery from backup");
if let Ok(backup_values) = self.try_restore_from_backup() { if let Ok(backup_values) = self.try_restore_from_backup() {
tracing::info!("Successfully restored config from backup"); tracing::info!("Successfully restored config from backup");
return Ok(backup_values); backup_values
} else {
// No backup available, create a default config
tracing::info!("No backup found, creating default configuration");
// Try to load from init-config.yaml if it exists, otherwise use empty config
let default_config = self.load_init_config_if_exists().unwrap_or_default();
self.create_and_save_default_config(default_config)?
} }
};
// No backup available, create a default config // Run migrations on the loaded config
tracing::info!("No backup found, creating default configuration"); if crate::config::migrations::run_migrations(&mut values) {
if let Err(e) = self.save_values(values.clone()) {
// Try to load from init-config.yaml if it exists, otherwise use empty config tracing::warn!("Failed to save migrated config: {}", e);
let default_config = self.load_init_config_if_exists().unwrap_or_default(); }
self.create_and_save_default_config(default_config)
} }
Ok(values)
} }
pub fn all_values(&self) -> Result<HashMap<String, Value>, ConfigError> { pub fn all_values(&self) -> Result<HashMap<String, Value>, ConfigError> {
@@ -1203,13 +1212,7 @@ mod tests {
// Print the final values for debugging // Print the final values for debugging
println!("Final values: {:?}", final_values); println!("Final values: {:?}", final_values);
assert_eq!( // Check that our 3 keys are present (migrations may add additional keys like "extensions")
final_values.len(),
3,
"Expected 3 values, got {}",
final_values.len()
);
for i in 0..3 { for i in 0..3 {
let key = format!("key{}", i); let key = format!("key{}", i);
let value = format!("value{}", i); let value = format!("value{}", i);
@@ -1285,19 +1288,22 @@ mod tests {
// Try to load values - should create a fresh default config // Try to load values - should create a fresh default config
let recovered_values = config.all_values()?; let recovered_values = config.all_values()?;
// Should return empty config // Note: migrations may add keys like "extensions", so we just verify
assert_eq!(recovered_values.len(), 0); // that no user-defined keys exist (the config was reset)
assert!(
!recovered_values.contains_key("key1"),
"Should not have user keys after recovery"
);
// Verify that a clean config file was written to disk // Verify that a clean config file was written to disk
let file_content = std::fs::read_to_string(config_file.path())?; let file_content = std::fs::read_to_string(config_file.path())?;
// Should be valid YAML (empty object) // Should be valid YAML
let parsed: serde_yaml::Value = serde_yaml::from_str(&file_content)?; let parsed: serde_yaml::Value = serde_yaml::from_str(&file_content)?;
assert!(parsed.is_mapping()); assert!(parsed.is_mapping());
// Should be able to load it again without issues // Should be able to load it again without issues
let reloaded_values = config.all_values()?; let _reloaded_values = config.all_values()?;
assert_eq!(reloaded_values.len(), 0);
Ok(()) Ok(())
} }
@@ -1316,8 +1322,12 @@ mod tests {
// Try to load values - should create a fresh default config file // Try to load values - should create a fresh default config file
let values = config.all_values()?; let values = config.all_values()?;
// Should return empty config // Note: migrations may add keys like "extensions", so we just verify
assert_eq!(values.len(), 0); // that no user-defined keys exist (the config was freshly created)
assert!(
!values.contains_key("key1"),
"Should not have user keys in fresh config"
);
// Verify that the config file was created // Verify that the config file was created
assert!(config_path.exists()); assert!(config_path.exists());
@@ -1328,8 +1338,7 @@ mod tests {
assert!(parsed.is_mapping()); assert!(parsed.is_mapping());
// Should be able to load it again without issues // Should be able to load it again without issues
let reloaded_values = config.all_values()?; let _reloaded_values = config.all_values()?;
assert_eq!(reloaded_values.len(), 0);
Ok(()) Ok(())
} }
-19
View File
@@ -1,5 +1,4 @@
use super::base::Config; use super::base::Config;
use crate::agents::extension::PLATFORM_EXTENSIONS;
use crate::agents::ExtensionConfig; use crate::agents::ExtensionConfig;
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -54,24 +53,6 @@ fn get_extensions_map() -> IndexMap<String, ExtensionEntry> {
} }
} }
// Always inject platform extensions (code_execution, todo, skills, etc.)
// These are internal agent extensions that should always be available
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: def.default_enabled,
},
);
}
}
extensions_map extensions_map
} }
+141
View File
@@ -0,0 +1,141 @@
use crate::agents::extension::PLATFORM_EXTENSIONS;
use crate::agents::ExtensionConfig;
use crate::config::extensions::ExtensionEntry;
use serde_yaml::Mapping;
const EXTENSIONS_CONFIG_KEY: &str = "extensions";
pub fn run_migrations(config: &mut Mapping) -> bool {
let mut changed = false;
changed |= migrate_platform_extensions(config);
changed
}
fn migrate_platform_extensions(config: &mut Mapping) -> bool {
let extensions_key = serde_yaml::Value::String(EXTENSIONS_CONFIG_KEY.to_string());
let extensions_value = config
.get(&extensions_key)
.cloned()
.unwrap_or(serde_yaml::Value::Mapping(Mapping::new()));
let mut extensions_map: Mapping = match extensions_value {
serde_yaml::Value::Mapping(m) => m,
_ => Mapping::new(),
};
let mut needs_save = false;
for (name, def) in PLATFORM_EXTENSIONS.iter() {
let ext_key = serde_yaml::Value::String(name.to_string());
let existing = extensions_map.get(&ext_key);
let needs_migration = match existing {
None => true,
Some(value) => match serde_yaml::from_value::<ExtensionEntry>(value.clone()) {
Ok(entry) => {
if let ExtensionConfig::Platform {
description,
display_name,
..
} = &entry.config
{
description != def.description
|| display_name.as_deref() != Some(def.display_name)
} else {
true
}
}
Err(_) => true,
},
};
if needs_migration {
let enabled = existing
.and_then(|v| serde_yaml::from_value::<ExtensionEntry>(v.clone()).ok())
.map(|e| e.enabled)
.unwrap_or(def.default_enabled);
let new_entry = ExtensionEntry {
config: ExtensionConfig::Platform {
name: def.name.to_string(),
description: def.description.to_string(),
display_name: Some(def.display_name.to_string()),
bundled: Some(true),
available_tools: Vec::new(),
},
enabled,
};
if let Ok(value) = serde_yaml::to_value(&new_entry) {
extensions_map.insert(ext_key, value);
needs_save = true;
}
}
}
if needs_save {
config.insert(extensions_key, serde_yaml::Value::Mapping(extensions_map));
}
needs_save
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_migrate_platform_extensions_empty_config() {
let mut config = Mapping::new();
let changed = run_migrations(&mut config);
assert!(changed);
let extensions_key = serde_yaml::Value::String(EXTENSIONS_CONFIG_KEY.to_string());
assert!(config.contains_key(&extensions_key));
}
#[test]
fn test_migrate_platform_extensions_preserves_enabled_state() {
let mut config = Mapping::new();
let mut extensions = Mapping::new();
let todo_entry = ExtensionEntry {
config: ExtensionConfig::Platform {
name: "todo".to_string(),
description: "old description".to_string(),
display_name: Some("Old Name".to_string()),
bundled: Some(true),
available_tools: Vec::new(),
},
enabled: false,
};
extensions.insert(
serde_yaml::Value::String("todo".to_string()),
serde_yaml::to_value(&todo_entry).unwrap(),
);
config.insert(
serde_yaml::Value::String(EXTENSIONS_CONFIG_KEY.to_string()),
serde_yaml::Value::Mapping(extensions),
);
let changed = run_migrations(&mut config);
assert!(changed);
let extensions_key = serde_yaml::Value::String(EXTENSIONS_CONFIG_KEY.to_string());
let extensions = config.get(&extensions_key).unwrap().as_mapping().unwrap();
let todo_key = serde_yaml::Value::String("todo".to_string());
let todo_value = extensions.get(&todo_key).unwrap();
let todo_entry: ExtensionEntry = serde_yaml::from_value(todo_value.clone()).unwrap();
assert!(!todo_entry.enabled);
}
#[test]
fn test_migrate_platform_extensions_idempotent() {
let mut config = Mapping::new();
run_migrations(&mut config);
let changed = run_migrations(&mut config);
assert!(!changed);
}
}
+1
View File
@@ -3,6 +3,7 @@ pub mod declarative_providers;
mod experiments; mod experiments;
pub mod extensions; pub mod extensions;
pub mod goose_mode; pub mod goose_mode;
mod migrations;
pub mod paths; pub mod paths;
pub mod permission; pub mod permission;
pub mod search_path; pub mod search_path;
@@ -42,6 +42,8 @@ enum RecipeExtensionConfigInternal {
#[serde(default)] #[serde(default)]
description: Option<String>, description: Option<String>,
#[serde(default)] #[serde(default)]
display_name: Option<String>,
#[serde(default)]
bundled: Option<bool>, bundled: Option<bool>,
#[serde(default)] #[serde(default)]
available_tools: Vec<String>, available_tools: Vec<String>,
@@ -128,6 +130,7 @@ impl From<RecipeExtensionConfigInternal> for ExtensionConfig {
available_tools available_tools
}, },
Platform { Platform {
display_name,
bundled, bundled,
available_tools available_tools
}, },
+2
View File
@@ -512,6 +512,7 @@ mod tests {
description: description:
"Enable a todo list for goose so it can keep track of what it is doing" "Enable a todo list for goose so it can keep track of what it is doing"
.to_string(), .to_string(),
display_name: Some("Todo".to_string()),
bundled: Some(true), bundled: Some(true),
available_tools: vec![], available_tools: vec![],
}, },
@@ -535,6 +536,7 @@ mod tests {
let ext_config = ExtensionConfig::Platform { let ext_config = ExtensionConfig::Platform {
name: "extensionmanager".to_string(), name: "extensionmanager".to_string(),
description: "Extension Manager".to_string(), description: "Extension Manager".to_string(),
display_name: Some("Extension Manager".to_string()),
bundled: Some(true), bundled: Some(true),
available_tools: vec![], available_tools: vec![],
}; };
+4
View File
@@ -3813,6 +3813,10 @@
"description": { "description": {
"type": "string" "type": "string"
}, },
"display_name": {
"type": "string",
"nullable": true
},
"name": { "name": {
"type": "string", "type": "string",
"description": "The name used to identify this extension" "description": "The name used to identify this extension"
+1
View File
@@ -253,6 +253,7 @@ export type ExtensionConfig = {
available_tools?: Array<string>; available_tools?: Array<string>;
bundled?: boolean | null; bundled?: boolean | null;
description: string; description: string;
display_name?: string | null;
/** /**
* The name used to identify this extension * The name used to identify this extension
*/ */
@@ -30,7 +30,7 @@ type SourceType = 'file' | 'deeplink';
interface CleanExtension { interface CleanExtension {
name: string; name: string;
type: 'stdio' | 'sse' | 'builtin' | 'frontend' | 'streamable_http'; type: 'stdio' | 'sse' | 'builtin' | 'frontend' | 'streamable_http' | 'platform';
cmd?: string; cmd?: string;
args?: string[]; args?: string[];
uri?: string; uri?: string;
@@ -120,7 +120,7 @@ function recipeToYaml(recipe: Recipe): string {
if (extAny.args) { if (extAny.args) {
cleanExt.args = extAny.args as string[]; cleanExt.args = extAny.args as string[];
} }
} else if (ext.type === 'builtin' && extAny.display_name) { } else if ((ext.type === 'builtin' || ext.type === 'platform') && extAny.display_name) {
cleanExt.display_name = extAny.display_name as string; cleanExt.display_name = extAny.display_name as string;
} }
@@ -104,7 +104,9 @@ export function formatExtensionName(name: string): string {
} }
export function getFriendlyTitle(extension: FixedExtensionEntry): string { export function getFriendlyTitle(extension: FixedExtensionEntry): string {
const name = (extension.type === 'builtin' && extension.display_name) || extension.name; const name =
((extension.type === 'builtin' || extension.type === 'platform') && extension.display_name) ||
extension.name;
return formatExtensionName(name); return formatExtensionName(name);
} }