From 1d74f538efeaf3410c7c518f45de65ee881887f7 Mon Sep 17 00:00:00 2001 From: Alex Hancock Date: Wed, 9 Apr 2025 17:27:40 -0400 Subject: [PATCH] feat: non-editable bundled extensions (#2114) --- crates/goose-cli/src/commands/configure.rs | 4 + crates/goose-cli/src/session/mod.rs | 3 + crates/goose-server/src/routes/extension.rs | 4 + crates/goose/src/agents/agent.rs | 1 + crates/goose/src/agents/extension.rs | 17 +++ crates/goose/src/agents/extension_manager.rs | 1 + crates/goose/src/config/extensions.rs | 1 + ui/desktop/openapi.json | 20 ++++ ui/desktop/src/api/types.gen.ts | 16 +++ .../extensions/bundled-extensions.json | 15 ++- .../extensions/bundled-extensions.ts | 100 ++++++++---------- .../subcomponents/ExtensionItem.tsx | 7 +- 12 files changed, 127 insertions(+), 62 deletions(-) diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index e3bd6822..a8214acc 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -70,6 +70,7 @@ pub async fn handle_configure() -> Result<(), Box> { name: "developer".to_string(), display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()), timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), + bundled: Some(true), }, })?; } @@ -509,6 +510,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { name: extension.clone(), display_name: Some(display_name), timeout: Some(timeout), + bundled: Some(true), }, })?; @@ -600,6 +602,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { envs: Envs::new(envs), description, timeout: Some(timeout), + bundled: None, }, })?; @@ -686,6 +689,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { envs: Envs::new(envs), description, timeout: Some(timeout), + bundled: None, }, })?; diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 1d96ccee..4e08bb15 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -159,6 +159,7 @@ impl Session { description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()), // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), + bundled: None, }; self.agent @@ -190,6 +191,7 @@ impl Session { description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()), // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), + bundled: None, }; self.agent @@ -214,6 +216,7 @@ impl Session { display_name: None, // TODO: should set a timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), + bundled: None, }; self.agent .add_extension(config) diff --git a/crates/goose-server/src/routes/extension.rs b/crates/goose-server/src/routes/extension.rs index 056af4f0..e5c5906c 100644 --- a/crates/goose-server/src/routes/extension.rs +++ b/crates/goose-server/src/routes/extension.rs @@ -204,6 +204,7 @@ async fn add_extension( envs: Envs::new(env_map), description: None, timeout, + bundled: None, } } ExtensionConfigRequest::Stdio { @@ -254,6 +255,7 @@ async fn add_extension( description: None, envs: Envs::new(env_map), timeout, + bundled: None, } } ExtensionConfigRequest::Builtin { @@ -264,6 +266,7 @@ async fn add_extension( name, display_name, timeout, + bundled: None, }, ExtensionConfigRequest::Frontend { name, @@ -273,6 +276,7 @@ async fn add_extension( name, tools, instructions, + bundled: None, }, }; diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 5852ab8b..703a45ed 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -243,6 +243,7 @@ impl Agent { name: _, tools, instructions, + bundled: _, } => { // For frontend tools, just store them in the frontend_tools map for tool in tools { diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 7db2ddcd..8e5748bd 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -130,6 +130,9 @@ pub enum ExtensionConfig { // NOTE: set timeout to be optional for compatibility. // However, new configurations should include this field. timeout: Option, + /// Whether this extension is bundled with Goose + #[serde(default)] + bundled: Option, }, /// Standard I/O client with command and arguments #[serde(rename = "stdio")] @@ -142,6 +145,9 @@ pub enum ExtensionConfig { envs: Envs, timeout: Option, description: Option, + /// Whether this extension is bundled with Goose + #[serde(default)] + bundled: Option, }, /// Built-in extension that is part of the goose binary #[serde(rename = "builtin")] @@ -150,6 +156,9 @@ pub enum ExtensionConfig { name: String, display_name: Option, // needed for the UI timeout: Option, + /// Whether this extension is bundled with Goose + #[serde(default)] + bundled: Option, }, /// Frontend-provided tools that will be called through the frontend #[serde(rename = "frontend")] @@ -160,6 +169,9 @@ pub enum ExtensionConfig { tools: Vec, /// Instructions for how to use these tools instructions: Option, + /// Whether this extension is bundled with Goose + #[serde(default)] + bundled: Option, }, } @@ -169,6 +181,7 @@ impl Default for ExtensionConfig { name: config::DEFAULT_EXTENSION.to_string(), display_name: Some(config::DEFAULT_DISPLAY_NAME.to_string()), timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT), + bundled: Some(true), } } } @@ -181,6 +194,7 @@ impl ExtensionConfig { envs: Envs::default(), description: Some(description.into()), timeout: Some(timeout.into()), + bundled: None, } } @@ -197,6 +211,7 @@ impl ExtensionConfig { envs: Envs::default(), description: Some(description.into()), timeout: Some(timeout.into()), + bundled: None, } } @@ -212,6 +227,7 @@ impl ExtensionConfig { envs, timeout, description, + bundled, .. } => Self::Stdio { name, @@ -220,6 +236,7 @@ impl ExtensionConfig { args: args.into_iter().map(Into::into).collect(), description, timeout, + bundled, }, other => other, } diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 48f72157..d5eb6365 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -146,6 +146,7 @@ impl ExtensionManager { name, display_name: _, timeout, + bundled: _, } => { // For builtin extensions, we run the current executable with mcp and extension name let cmd = std::env::current_exe() diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index f7775894..a5a688d7 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -45,6 +45,7 @@ impl ExtensionConfigManager { name: DEFAULT_EXTENSION.to_string(), display_name: Some(DEFAULT_DISPLAY_NAME.to_string()), timeout: Some(DEFAULT_EXTENSION_TIMEOUT), + bundled: Some(true), }, }, )]); diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index fb07b085..8a2d6c88 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -390,6 +390,11 @@ "type" ], "properties": { + "bundled": { + "type": "boolean", + "description": "Whether this extension is bundled with Goose", + "nullable": true + }, "description": { "type": "string", "nullable": true @@ -434,6 +439,11 @@ "type": "string" } }, + "bundled": { + "type": "boolean", + "description": "Whether this extension is bundled with Goose", + "nullable": true + }, "cmd": { "type": "string" }, @@ -470,6 +480,11 @@ "type" ], "properties": { + "bundled": { + "type": "boolean", + "description": "Whether this extension is bundled with Goose", + "nullable": true + }, "display_name": { "type": "string", "nullable": true @@ -501,6 +516,11 @@ "type" ], "properties": { + "bundled": { + "type": "boolean", + "description": "Whether this extension is bundled with Goose", + "nullable": true + }, "instructions": { "type": "string", "description": "Instructions for how to use these tools", diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 75e2e016..32c21c87 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -24,6 +24,10 @@ export type Envs = { * Represents the different types of MCP extensions that can be added to the manager */ export type ExtensionConfig = { + /** + * Whether this extension is bundled with Goose + */ + bundled?: boolean | null; description?: string | null; envs?: Envs; /** @@ -35,6 +39,10 @@ export type ExtensionConfig = { uri: string; } | { args: Array; + /** + * Whether this extension is bundled with Goose + */ + bundled?: boolean | null; cmd: string; description?: string | null; envs?: Envs; @@ -45,6 +53,10 @@ export type ExtensionConfig = { timeout?: number | null; type: 'stdio'; } | { + /** + * Whether this extension is bundled with Goose + */ + bundled?: boolean | null; display_name?: string | null; /** * The name used to identify this extension @@ -53,6 +65,10 @@ export type ExtensionConfig = { timeout?: number | null; type: 'builtin'; } | { + /** + * Whether this extension is bundled with Goose + */ + bundled?: boolean | null; /** * Instructions for how to use these tools */ diff --git a/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.json b/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.json index 828210e7..42a05459 100644 --- a/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.json +++ b/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.json @@ -7,7 +7,8 @@ "enabled": true, "type": "builtin", "env_keys": [], - "timeout": 300 + "timeout": 300, + "bundled": true }, { "id": "computercontroller", @@ -17,7 +18,8 @@ "enabled": false, "type": "builtin", "env_keys": [], - "timeout": 300 + "timeout": 300, + "bundled": true }, { "id": "memory", @@ -27,7 +29,8 @@ "enabled": false, "type": "builtin", "env_keys": [], - "timeout": 300 + "timeout": 300, + "bundled": true }, { "id": "jetbrains", @@ -37,7 +40,8 @@ "enabled": false, "type": "builtin", "env_keys": [], - "timeout": 300 + "timeout": 300, + "bundled": true }, { "id": "tutorial", @@ -46,6 +50,7 @@ "description": "Access interactive tutorials and guides", "enabled": false, "type": "builtin", - "env_keys": [] + "env_keys": [], + "bundled": true } ] diff --git a/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.ts b/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.ts index 682be4e1..65eadccf 100644 --- a/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.ts +++ b/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.ts @@ -33,68 +33,58 @@ export async function syncBundledExtensions( addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise ): Promise { try { - // Create a set of existing extension IDs for quick lookup - const existingExtensionKeys = new Set(existingExtensions.map((ext) => nameToKey(ext.name))); - // Cast the imported JSON data to the expected type const bundledExtensions = bundledExtensionsData as BundledExtension[]; - // Track how many extensions were added - let addedCount = 0; - - // Check each built-in extension + // Process each bundled extension for (const bundledExt of bundledExtensions) { - // Only add if the extension doesn't already exist -- use the id - if (!existingExtensionKeys.has(bundledExt.id)) { - console.log(`Adding built-in extension: ${bundledExt.id}`); - let extConfig: ExtensionConfig; - switch (bundledExt.type) { - case 'builtin': - extConfig = { - name: bundledExt.name, - display_name: bundledExt.display_name, - type: bundledExt.type, - timeout: bundledExt.timeout ?? 300, - }; - break; - case 'stdio': - extConfig = { - name: bundledExt.name, - description: bundledExt.description, - type: bundledExt.type, - timeout: bundledExt.timeout, - cmd: bundledExt.cmd, - args: bundledExt.args, - envs: bundledExt.envs, - }; - break; - case 'sse': - extConfig = { - name: bundledExt.name, - description: bundledExt.description, - type: bundledExt.type, - timeout: bundledExt.timeout, - uri: bundledExt.uri, - }; - } - // Add the extension with its default enabled state - try { - await addExtensionFn(bundledExt.name, extConfig, bundledExt.enabled); - addedCount++; - } catch (error) { - console.error(`Failed to add built-in extension ${bundledExt.name}:`, error); - // Continue with other extensions even if one fails - } - } - } + // Find if this extension already exists + const existingExt = existingExtensions.find((ext) => nameToKey(ext.name) === bundledExt.id); - if (addedCount > 0) { - console.log(`Added ${addedCount} built-in extensions.`); - } else { - console.log('All built-in extensions already present.'); + // Skip if extension exists and is already marked as bundled + if (existingExt?.bundled) continue; + + // Create the config for this extension + let extConfig: ExtensionConfig; + switch (bundledExt.type) { + case 'builtin': + extConfig = { + name: bundledExt.name, + display_name: bundledExt.display_name, + type: bundledExt.type, + timeout: bundledExt.timeout ?? 300, + bundled: true, + }; + break; + case 'stdio': + extConfig = { + name: bundledExt.name, + description: bundledExt.description, + type: bundledExt.type, + timeout: bundledExt.timeout, + cmd: bundledExt.cmd, + args: bundledExt.args, + envs: bundledExt.envs, + bundled: true, + }; + break; + case 'sse': + extConfig = { + name: bundledExt.name, + description: bundledExt.description, + type: bundledExt.type, + timeout: bundledExt.timeout, + uri: bundledExt.uri, + bundled: true, + }; + } + + // Add or update the extension, preserving enabled state if it exists + const enabled = existingExt ? existingExt.enabled : bundledExt.enabled; + await addExtensionFn(bundledExt.name, extConfig, enabled); } } catch (error) { - console.error('Failed to add built-in extensions:', error); + console.error('Failed to sync built-in extensions:', error); throw error; } } diff --git a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx index 9a6e0681..c025f751 100644 --- a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx @@ -56,6 +56,10 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte )); }; + // Bundled extensions and builtins are not editable + // Over time we can take the first part of the conditional away as people have bundled: true in their config.yaml entries + const editable = !(extension.type === 'builtin' || extension.bundled); + return (
e.stopPropagation()} > - {/* Only show config button for non-builtin extensions */} - {extension.type !== 'builtin' && ( + {editable && (