Platform extensions sketch (#4868)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Douwe Osinga
2025-10-03 13:45:37 -04:00
committed by GitHub
parent 5b8efb5b9a
commit 96ded37e15
36 changed files with 742 additions and 847 deletions
+52 -93
View File
@@ -28,27 +28,6 @@ use std::error::Error;
// cursor-selected and cursor-unselected items.
const MULTISELECT_VISIBILITY_HINT: &str = "<";
fn get_display_name(extension_id: &str) -> String {
match extension_id {
"developer" => "Developer Tools".to_string(),
"computercontroller" => "Computer Controller".to_string(),
"autovisualiser" => "Auto Visualiser".to_string(),
"memory" => "Memory".to_string(),
"tutorial" => "Tutorial".to_string(),
"jetbrains" => "JetBrains".to_string(),
// Add other extensions as needed
_ => {
extension_id
.chars()
.next()
.unwrap_or_default()
.to_uppercase()
.collect::<String>()
+ &extension_id[1..]
}
}
}
pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
let config = Config::global();
@@ -128,14 +107,7 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
// This operation is best-effort and errors are ignored
ExtensionConfigManager::set(ExtensionEntry {
enabled: true,
config: ExtensionConfig::Builtin {
name: "developer".to_string(),
display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()),
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: Some(true),
description: None,
available_tools: Vec::new(),
},
config: ExtensionConfig::default(),
})?;
}
Ok(false) => {
@@ -747,35 +719,40 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
match extension_type {
// TODO we'll want a place to collect all these options, maybe just an enum in goose-mcp
"built-in" => {
let extension = cliclack::select("Which built-in extension would you like to enable?")
.item(
let extensions = vec![
(
"autovisualiser",
"Auto Visualiser",
"Data visualisation and UI generation tools",
)
.item(
),
(
"computercontroller",
"Computer Controller",
"controls for webscraping, file caching, and automations",
)
.item(
),
(
"developer",
"Developer Tools",
"Code editing and shell access",
)
.item("jetbrains", "JetBrains", "Connect to jetbrains IDEs")
.item(
),
("jetbrains", "JetBrains", "Connect to jetbrains IDEs"),
(
"memory",
"Memory",
"Tools to save and retrieve durable memories",
)
.item(
),
(
"tutorial",
"Tutorial",
"Access interactive tutorials and guides",
)
.interact()?
.to_string();
),
];
let mut select = cliclack::select("Which built-in extension would you like to enable?");
for (id, name, desc) in &extensions {
select = select.item(id, name, desc);
}
let extension = select.interact()?.to_string();
let timeout: u64 = cliclack::input("Please set the timeout for this tool (in secs):")
.placeholder(&goose::config::DEFAULT_EXTENSION_TIMEOUT.to_string())
@@ -785,7 +762,11 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
})
.interact()?;
let display_name = get_display_name(&extension);
let (display_name, description) = extensions
.iter()
.find(|(id, _, _)| id == &extension)
.map(|(_, name, desc)| (name.to_string(), desc.to_string()))
.unwrap_or_else(|| (extension.clone(), extension.clone()));
ExtensionConfigManager::set(ExtensionEntry {
enabled: true,
@@ -794,7 +775,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
display_name: Some(display_name),
timeout: Some(timeout),
bundled: Some(true),
description: None,
description,
available_tools: Vec::new(),
},
})?;
@@ -841,20 +822,13 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
let cmd = parts.next().unwrap_or("").to_string();
let args: Vec<String> = parts.map(String::from).collect();
let add_desc = cliclack::confirm("Would you like to add a description?").interact()?;
let description = if add_desc {
let desc = cliclack::input("Enter a description for this extension:")
.placeholder("Description")
.validate(|input: &String| match input.parse::<String>() {
Ok(_) => Ok(()),
Err(_) => Err("Please enter a valid description"),
})
.interact()?;
Some(desc)
} else {
None
};
let description = cliclack::input("Enter a description for this extension:")
.placeholder("Description")
.validate(|input: &String| match input.parse::<String>() {
Ok(_) => Ok(()),
Err(_) => Err("Please enter a valid description"),
})
.interact()?;
let add_env =
cliclack::confirm("Would you like to add environment variables?").interact()?;
@@ -945,21 +919,13 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
})
.interact()?;
let add_desc = cliclack::confirm("Would you like to add a description?").interact()?;
let description = if add_desc {
let desc = cliclack::input("Enter a description for this extension:")
.placeholder("Description")
.validate(|input: &String| match input.parse::<String>() {
Ok(_) => Ok(()),
Err(_) => Err("Please enter a valid description"),
})
.interact()?;
Some(desc)
} else {
None
};
let description = cliclack::input("Enter a description for this extension:")
.placeholder("Description")
.validate(|input: &String| match input.parse::<String>() {
Ok(_) => Ok(()),
Err(_) => Err("Please enter a valid description"),
})
.interact()?;
let add_env =
cliclack::confirm("Would you like to add environment variables?").interact()?;
@@ -1048,23 +1014,16 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
})
.interact()?;
let add_desc = cliclack::confirm("Would you like to add a description?").interact()?;
let description = if add_desc {
let desc = cliclack::input("Enter a description for this extension:")
.placeholder("Description")
.validate(|input: &String| {
if input.trim().is_empty() {
Err("Please enter a valid description")
} else {
Ok(())
}
})
.interact()?;
Some(desc)
} else {
None
};
let description = cliclack::input("Enter a description for this extension:")
.placeholder("Description")
.validate(|input: &String| {
if input.trim().is_empty() {
Err("Please enter a valid description")
} else {
Ok(())
}
})
.interact()?;
let add_headers =
cliclack::confirm("Would you like to add custom headers?").interact()?;
@@ -1762,7 +1721,7 @@ pub async fn handle_openrouter_auth() -> Result<(), Box<dyn Error>> {
),
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: Some(true),
description: None,
description: "Developer extension".to_string(),
available_tools: Vec::new(),
},
}) {
@@ -1865,7 +1824,7 @@ pub async fn handle_tetrate_auth() -> Result<(), Box<dyn Error>> {
),
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: Some(true),
description: None,
description: "Developer extension".to_string(),
available_tools: Vec::new(),
},
}) {
@@ -55,6 +55,7 @@ fn extract_secrets_from_extensions(
ExtensionConfig::Stdio { name, env_keys, .. } => (name, env_keys),
ExtensionConfig::StreamableHttp { name, env_keys, .. } => (name, env_keys),
ExtensionConfig::Builtin { name, .. } => (name, &Vec::new()),
ExtensionConfig::Platform { name, .. } => (name, &Vec::new()),
ExtensionConfig::Frontend { name, .. } => (name, &Vec::new()),
ExtensionConfig::InlinePython { name, .. } => (name, &Vec::new()),
};
@@ -140,7 +141,7 @@ mod tests {
uri: "sse://example.com".to_string(),
envs: Envs::new(HashMap::new()),
env_keys: vec!["GITHUB_TOKEN".to_string(), "GITHUB_API_URL".to_string()],
description: None,
description: "github-mcp".to_string(),
timeout: None,
bundled: None,
available_tools: Vec::new(),
@@ -152,14 +153,14 @@ mod tests {
envs: Envs::new(HashMap::new()),
env_keys: vec!["SLACK_TOKEN".to_string()],
timeout: None,
description: None,
description: "slack-mcp".to_string(),
bundled: None,
available_tools: Vec::new(),
},
ExtensionConfig::Builtin {
name: "builtin-ext".to_string(),
display_name: None,
description: None,
description: "builtin-ext".to_string(),
timeout: None,
bundled: None,
available_tools: Vec::new(),
@@ -237,7 +238,7 @@ mod tests {
uri: "sse://example.com".to_string(),
envs: Envs::new(HashMap::new()),
env_keys: vec!["API_KEY".to_string()],
description: None,
description: "service-a".to_string(),
timeout: None,
bundled: None,
available_tools: Vec::new(),
@@ -249,7 +250,7 @@ mod tests {
envs: Envs::new(HashMap::new()),
env_keys: vec!["API_KEY".to_string()], // Same original key, different extension
timeout: None,
description: None,
description: "service-b".to_string(),
bundled: None,
available_tools: Vec::new(),
},
@@ -296,7 +297,7 @@ mod tests {
uri: "sse://parent.com".to_string(),
envs: Envs::new(HashMap::new()),
env_keys: vec!["PARENT_TOKEN".to_string()],
description: None,
description: "parent-ext".to_string(),
timeout: None,
bundled: None,
available_tools: Vec::new(),
@@ -203,7 +203,7 @@ where
ExtensionConfig::Builtin {
name: "".to_string(),
display_name: None,
description: None,
description: "".to_string(),
timeout: None,
bundled: None,
available_tools: vec![],
+8
View File
@@ -7,6 +7,7 @@ use goose::config::{Config, ExtensionConfig, ExtensionConfigManager};
use goose::providers::create;
use goose::recipe::{Response, SubRecipe};
use goose::agents::extension::PlatformExtensionContext;
use goose::session::SessionManager;
use rustyline::EditMode;
use std::collections::HashSet;
@@ -281,6 +282,13 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
Some(session.id)
};
agent
.extension_manager
.set_context(PlatformExtensionContext {
session_id: session_id.clone(),
})
.await;
if session_config.resume {
if let Some(session_id) = session_id.as_ref() {
// Read the session metadata from database
+4 -4
View File
@@ -207,7 +207,7 @@ impl CliSession {
args: parts.iter().map(|s| s.to_string()).collect(),
envs: Envs::new(envs),
env_keys: Vec::new(),
description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()),
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
// TODO: should set timeout
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: None,
@@ -241,7 +241,7 @@ impl CliSession {
uri: extension_url,
envs: Envs::new(HashMap::new()),
env_keys: Vec::new(),
description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()),
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
// TODO: should set timeout
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: None,
@@ -276,7 +276,7 @@ impl CliSession {
envs: Envs::new(HashMap::new()),
env_keys: Vec::new(),
headers: HashMap::new(),
description: Some(goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string()),
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
// TODO: should set timeout
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: None,
@@ -306,7 +306,7 @@ impl CliSession {
// TODO: should set a timeout
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
bundled: None,
description: None,
description: name.trim().to_string(),
available_tools: Vec::new(),
};
self.agent