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(),
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<dyn Error>> {
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<dyn Error>> {
envs: Envs::new(envs),
description,
timeout: Some(timeout),
bundled: None,
},
})?;
@@ -686,6 +689,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
envs: Envs::new(envs),
description,
timeout: Some(timeout),
bundled: None,
},
})?;

View File

@@ -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)

View File

@@ -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,
},
};

View File

@@ -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 {

View File

@@ -130,6 +130,9 @@ pub enum ExtensionConfig {
// NOTE: set timeout to be optional for compatibility.
// However, new configurations should include this field.
timeout: Option<u64>,
/// Whether this extension is bundled with Goose
#[serde(default)]
bundled: Option<bool>,
},
/// Standard I/O client with command and arguments
#[serde(rename = "stdio")]
@@ -142,6 +145,9 @@ pub enum ExtensionConfig {
envs: Envs,
timeout: Option<u64>,
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
#[serde(rename = "builtin")]
@@ -150,6 +156,9 @@ pub enum ExtensionConfig {
name: String,
display_name: Option<String>, // needed for the UI
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
#[serde(rename = "frontend")]
@@ -160,6 +169,9 @@ pub enum ExtensionConfig {
tools: Vec<Tool>,
/// Instructions for how to use these tools
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(),
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,
}

View File

@@ -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()

View File

@@ -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),
},
},
)]);

View File

@@ -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",

View File

@@ -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<string>;
/**
* 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
*/

View File

@@ -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
}
]

View File

@@ -33,68 +33,58 @@ export async function syncBundledExtensions(
addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>
): Promise<void> {
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;
}
}

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 (
<div
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%]"
onClick={(e) => e.stopPropagation()}
>
{/* Only show config button for non-builtin extensions */}
{extension.type !== 'builtin' && (
{editable && (
<button
className="text-textSubtle hover:text-textStandard"
onClick={() => onConfigure(extension)}