feat: non-editable bundled extensions (#2114)

This commit is contained in:
Alex Hancock
2025-04-09 17:27:40 -04:00
committed by GitHub
parent 9610406065
commit 1d74f538ef
12 changed files with 127 additions and 62 deletions

View File

@@ -70,6 +70,7 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
name: "developer".to_string(), name: "developer".to_string(),
display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()), display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()),
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: Some(true),
}, },
})?; })?;
} }
@@ -509,6 +510,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
name: extension.clone(), name: extension.clone(),
display_name: Some(display_name), display_name: Some(display_name),
timeout: Some(timeout), timeout: Some(timeout),
bundled: Some(true),
}, },
})?; })?;
@@ -600,6 +602,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
envs: Envs::new(envs), envs: Envs::new(envs),
description, description,
timeout: Some(timeout), timeout: Some(timeout),
bundled: None,
}, },
})?; })?;
@@ -686,6 +689,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
envs: Envs::new(envs), envs: Envs::new(envs),
description, description,
timeout: Some(timeout), timeout: Some(timeout),
bundled: None,
}, },
})?; })?;

View File

@@ -159,6 +159,7 @@ impl Session {
description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()), description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()),
// TODO: should set timeout // TODO: should set timeout
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: None,
}; };
self.agent self.agent
@@ -190,6 +191,7 @@ impl Session {
description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()), description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()),
// TODO: should set timeout // TODO: should set timeout
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: None,
}; };
self.agent self.agent
@@ -214,6 +216,7 @@ impl Session {
display_name: None, display_name: None,
// TODO: should set a timeout // TODO: should set a timeout
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: None,
}; };
self.agent self.agent
.add_extension(config) .add_extension(config)

View File

@@ -204,6 +204,7 @@ async fn add_extension(
envs: Envs::new(env_map), envs: Envs::new(env_map),
description: None, description: None,
timeout, timeout,
bundled: None,
} }
} }
ExtensionConfigRequest::Stdio { ExtensionConfigRequest::Stdio {
@@ -254,6 +255,7 @@ async fn add_extension(
description: None, description: None,
envs: Envs::new(env_map), envs: Envs::new(env_map),
timeout, timeout,
bundled: None,
} }
} }
ExtensionConfigRequest::Builtin { ExtensionConfigRequest::Builtin {
@@ -264,6 +266,7 @@ async fn add_extension(
name, name,
display_name, display_name,
timeout, timeout,
bundled: None,
}, },
ExtensionConfigRequest::Frontend { ExtensionConfigRequest::Frontend {
name, name,
@@ -273,6 +276,7 @@ async fn add_extension(
name, name,
tools, tools,
instructions, instructions,
bundled: None,
}, },
}; };

View File

@@ -243,6 +243,7 @@ impl Agent {
name: _, name: _,
tools, tools,
instructions, instructions,
bundled: _,
} => { } => {
// For frontend tools, just store them in the frontend_tools map // For frontend tools, just store them in the frontend_tools map
for tool in tools { for tool in tools {

View File

@@ -130,6 +130,9 @@ pub enum ExtensionConfig {
// NOTE: set timeout to be optional for compatibility. // NOTE: set timeout to be optional for compatibility.
// However, new configurations should include this field. // However, new configurations should include this field.
timeout: Option<u64>, timeout: Option<u64>,
/// Whether this extension is bundled with Goose
#[serde(default)]
bundled: Option<bool>,
}, },
/// Standard I/O client with command and arguments /// Standard I/O client with command and arguments
#[serde(rename = "stdio")] #[serde(rename = "stdio")]
@@ -142,6 +145,9 @@ pub enum ExtensionConfig {
envs: Envs, envs: Envs,
timeout: Option<u64>, timeout: Option<u64>,
description: Option<String>, description: Option<String>,
/// Whether this extension is bundled with Goose
#[serde(default)]
bundled: Option<bool>,
}, },
/// Built-in extension that is part of the goose binary /// Built-in extension that is part of the goose binary
#[serde(rename = "builtin")] #[serde(rename = "builtin")]
@@ -150,6 +156,9 @@ pub enum ExtensionConfig {
name: String, name: String,
display_name: Option<String>, // needed for the UI display_name: Option<String>, // needed for the UI
timeout: Option<u64>, timeout: Option<u64>,
/// Whether this extension is bundled with Goose
#[serde(default)]
bundled: Option<bool>,
}, },
/// Frontend-provided tools that will be called through the frontend /// Frontend-provided tools that will be called through the frontend
#[serde(rename = "frontend")] #[serde(rename = "frontend")]
@@ -160,6 +169,9 @@ pub enum ExtensionConfig {
tools: Vec<Tool>, tools: Vec<Tool>,
/// Instructions for how to use these tools /// Instructions for how to use these tools
instructions: Option<String>, instructions: Option<String>,
/// Whether this extension is bundled with Goose
#[serde(default)]
bundled: Option<bool>,
}, },
} }
@@ -169,6 +181,7 @@ impl Default for ExtensionConfig {
name: config::DEFAULT_EXTENSION.to_string(), name: config::DEFAULT_EXTENSION.to_string(),
display_name: Some(config::DEFAULT_DISPLAY_NAME.to_string()), display_name: Some(config::DEFAULT_DISPLAY_NAME.to_string()),
timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT), timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT),
bundled: Some(true),
} }
} }
} }
@@ -181,6 +194,7 @@ impl ExtensionConfig {
envs: Envs::default(), envs: Envs::default(),
description: Some(description.into()), description: Some(description.into()),
timeout: Some(timeout.into()), timeout: Some(timeout.into()),
bundled: None,
} }
} }
@@ -197,6 +211,7 @@ impl ExtensionConfig {
envs: Envs::default(), envs: Envs::default(),
description: Some(description.into()), description: Some(description.into()),
timeout: Some(timeout.into()), timeout: Some(timeout.into()),
bundled: None,
} }
} }
@@ -212,6 +227,7 @@ impl ExtensionConfig {
envs, envs,
timeout, timeout,
description, description,
bundled,
.. ..
} => Self::Stdio { } => Self::Stdio {
name, name,
@@ -220,6 +236,7 @@ impl ExtensionConfig {
args: args.into_iter().map(Into::into).collect(), args: args.into_iter().map(Into::into).collect(),
description, description,
timeout, timeout,
bundled,
}, },
other => other, other => other,
} }

View File

@@ -146,6 +146,7 @@ impl ExtensionManager {
name, name,
display_name: _, display_name: _,
timeout, timeout,
bundled: _,
} => { } => {
// For builtin extensions, we run the current executable with mcp and extension name // For builtin extensions, we run the current executable with mcp and extension name
let cmd = std::env::current_exe() let cmd = std::env::current_exe()

View File

@@ -45,6 +45,7 @@ impl ExtensionConfigManager {
name: DEFAULT_EXTENSION.to_string(), name: DEFAULT_EXTENSION.to_string(),
display_name: Some(DEFAULT_DISPLAY_NAME.to_string()), display_name: Some(DEFAULT_DISPLAY_NAME.to_string()),
timeout: Some(DEFAULT_EXTENSION_TIMEOUT), timeout: Some(DEFAULT_EXTENSION_TIMEOUT),
bundled: Some(true),
}, },
}, },
)]); )]);

View File

@@ -390,6 +390,11 @@
"type" "type"
], ],
"properties": { "properties": {
"bundled": {
"type": "boolean",
"description": "Whether this extension is bundled with Goose",
"nullable": true
},
"description": { "description": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -434,6 +439,11 @@
"type": "string" "type": "string"
} }
}, },
"bundled": {
"type": "boolean",
"description": "Whether this extension is bundled with Goose",
"nullable": true
},
"cmd": { "cmd": {
"type": "string" "type": "string"
}, },
@@ -470,6 +480,11 @@
"type" "type"
], ],
"properties": { "properties": {
"bundled": {
"type": "boolean",
"description": "Whether this extension is bundled with Goose",
"nullable": true
},
"display_name": { "display_name": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -501,6 +516,11 @@
"type" "type"
], ],
"properties": { "properties": {
"bundled": {
"type": "boolean",
"description": "Whether this extension is bundled with Goose",
"nullable": true
},
"instructions": { "instructions": {
"type": "string", "type": "string",
"description": "Instructions for how to use these tools", "description": "Instructions for how to use these tools",

View File

@@ -24,6 +24,10 @@ export type Envs = {
* Represents the different types of MCP extensions that can be added to the manager * Represents the different types of MCP extensions that can be added to the manager
*/ */
export type ExtensionConfig = { export type ExtensionConfig = {
/**
* Whether this extension is bundled with Goose
*/
bundled?: boolean | null;
description?: string | null; description?: string | null;
envs?: Envs; envs?: Envs;
/** /**
@@ -35,6 +39,10 @@ export type ExtensionConfig = {
uri: string; uri: string;
} | { } | {
args: Array<string>; args: Array<string>;
/**
* Whether this extension is bundled with Goose
*/
bundled?: boolean | null;
cmd: string; cmd: string;
description?: string | null; description?: string | null;
envs?: Envs; envs?: Envs;
@@ -45,6 +53,10 @@ export type ExtensionConfig = {
timeout?: number | null; timeout?: number | null;
type: 'stdio'; type: 'stdio';
} | { } | {
/**
* Whether this extension is bundled with Goose
*/
bundled?: boolean | null;
display_name?: string | null; display_name?: string | null;
/** /**
* The name used to identify this extension * The name used to identify this extension
@@ -53,6 +65,10 @@ export type ExtensionConfig = {
timeout?: number | null; timeout?: number | null;
type: 'builtin'; type: 'builtin';
} | { } | {
/**
* Whether this extension is bundled with Goose
*/
bundled?: boolean | null;
/** /**
* Instructions for how to use these tools * Instructions for how to use these tools
*/ */

View File

@@ -7,7 +7,8 @@
"enabled": true, "enabled": true,
"type": "builtin", "type": "builtin",
"env_keys": [], "env_keys": [],
"timeout": 300 "timeout": 300,
"bundled": true
}, },
{ {
"id": "computercontroller", "id": "computercontroller",
@@ -17,7 +18,8 @@
"enabled": false, "enabled": false,
"type": "builtin", "type": "builtin",
"env_keys": [], "env_keys": [],
"timeout": 300 "timeout": 300,
"bundled": true
}, },
{ {
"id": "memory", "id": "memory",
@@ -27,7 +29,8 @@
"enabled": false, "enabled": false,
"type": "builtin", "type": "builtin",
"env_keys": [], "env_keys": [],
"timeout": 300 "timeout": 300,
"bundled": true
}, },
{ {
"id": "jetbrains", "id": "jetbrains",
@@ -37,7 +40,8 @@
"enabled": false, "enabled": false,
"type": "builtin", "type": "builtin",
"env_keys": [], "env_keys": [],
"timeout": 300 "timeout": 300,
"bundled": true
}, },
{ {
"id": "tutorial", "id": "tutorial",
@@ -46,6 +50,7 @@
"description": "Access interactive tutorials and guides", "description": "Access interactive tutorials and guides",
"enabled": false, "enabled": false,
"type": "builtin", "type": "builtin",
"env_keys": [] "env_keys": [],
"bundled": true
} }
] ]

View File

@@ -33,68 +33,58 @@ export async function syncBundledExtensions(
addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void> addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>
): Promise<void> { ): Promise<void> {
try { 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 // Cast the imported JSON data to the expected type
const bundledExtensions = bundledExtensionsData as BundledExtension[]; const bundledExtensions = bundledExtensionsData as BundledExtension[];
// Track how many extensions were added // Process each bundled extension
let addedCount = 0;
// Check each built-in extension
for (const bundledExt of bundledExtensions) { for (const bundledExt of bundledExtensions) {
// Only add if the extension doesn't already exist -- use the id // Find if this extension already exists
if (!existingExtensionKeys.has(bundledExt.id)) { const existingExt = existingExtensions.find((ext) => nameToKey(ext.name) === 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
}
}
}
if (addedCount > 0) { // Skip if extension exists and is already marked as bundled
console.log(`Added ${addedCount} built-in extensions.`); if (existingExt?.bundled) continue;
} else {
console.log('All built-in extensions already present.'); // 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) { } catch (error) {
console.error('Failed to add built-in extensions:', error); console.error('Failed to sync built-in extensions:', error);
throw error; throw error;
} }
} }

View File

@@ -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 ( return (
<div <div
className="flex justify-between rounded-lg transition-colors border border-borderSubtle p-4 pt-3 hover:border-borderProminent hover:cursor-pointer" className="flex justify-between rounded-lg transition-colors border border-borderSubtle p-4 pt-3 hover:border-borderProminent hover:cursor-pointer"
@@ -70,8 +74,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
className="flex items-center justify-end gap-2 w-max-[10%]" className="flex items-center justify-end gap-2 w-max-[10%]"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Only show config button for non-builtin extensions */} {editable && (
{extension.type !== 'builtin' && (
<button <button
className="text-textSubtle hover:text-textStandard" className="text-textSubtle hover:text-textStandard"
onClick={() => onConfigure(extension)} onClick={() => onConfigure(extension)}