Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/goose-acp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ workspace = true

[dependencies]
goose = { path = "../goose" }
goose-mcp = { path = "../goose-mcp" }
rmcp = { workspace = true }
sacp = "10.1.0"
anyhow = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/goose-acp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::Result;
use fs_err as fs;
use goose::agents::extension::{Envs, PLATFORM_EXTENSIONS};
use goose::agents::{Agent, AgentConfig, ExtensionConfig, SessionConfig};
use goose::builtin_extension::register_builtin_extensions;
use goose::config::base::CONFIG_YAML_NAME;
use goose::config::extensions::get_enabled_extensions_with_config;
use goose::config::paths::Paths;
Expand Down Expand Up @@ -1035,6 +1036,7 @@ where
}

pub async fn run(builtins: Vec<String>) -> Result<()> {
register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone());
info!("listening on stdio");

let outgoing = tokio::io::stdout().compat_write();
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-acp/tests/fixtures/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use assert_json_diff::{assert_json_matches_no_panic, CompareMode, Config};
use async_trait::async_trait;
use fs_err as fs;
use goose::builtin_extension::register_builtin_extensions;
use goose::config::{GooseMode, PermissionManager};
use goose::model::ModelConfig;
use goose::providers::api_client::{ApiClient, AuthMethod};
Expand Down Expand Up @@ -445,6 +446,8 @@ pub fn run_test<F>(fut: F)
where
F: Future<Output = ()> + Send + 'static,
{
register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone());

let handle = std::thread::Builder::new()
.name("acp-test".to_string())
.stack_size(8 * 1024 * 1024)
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
use clap::{Args, CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell as ClapShell};
use goose::builtin_extension::register_builtin_extensions;
use goose::config::Config;
use goose::posthog::get_telemetry_choice;
use goose::recipe::Recipe;
Expand Down Expand Up @@ -1513,6 +1514,8 @@ async fn handle_default_session() -> Result<()> {
}

pub async fn cli() -> anyhow::Result<()> {
register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone());

let cli = Cli::parse();

if let Err(e) = crate::project_tracker::update_project_tracker(None, None) {
Expand Down
2 changes: 1 addition & 1 deletion crates/goose-cli/src/scenario_tests/scenario_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ where
let temp_dir = TempDir::new()?;
let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf()));
let permission_manager = Arc::new(PermissionManager::new(temp_dir.path().to_path_buf()));
let agent_config = AgentConfig::new(session_manager, permission_manager, None, GooseMode::Auto); // no scheduler needed for scenario tests
let agent_config = AgentConfig::new(session_manager, permission_manager, None, GooseMode::Auto);
let agent = Agent::with_config(agent_config);
agent
.extension_manager
Expand Down
16 changes: 3 additions & 13 deletions crates/goose-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@ pub use developer::rmcp_developer::DeveloperServer;
pub use memory::MemoryServer;
pub use tutorial::TutorialServer;

/// Type definition for a function that spawns and serves a builtin extension server
pub type SpawnServerFn = fn(tokio::io::DuplexStream, tokio::io::DuplexStream);

pub struct BuiltinDef {
pub name: &'static str,
pub spawn_server: SpawnServerFn,
}

fn spawn_and_serve<S>(
name: &'static str,
server: S,
Expand All @@ -51,17 +47,11 @@ macro_rules! builtin {
fn spawn(r: tokio::io::DuplexStream, w: tokio::io::DuplexStream) {
spawn_and_serve(stringify!($name), <$server_ty>::new(), (r, w));
}
(
stringify!($name),
BuiltinDef {
name: stringify!($name),
spawn_server: spawn,
},
)
(stringify!($name), spawn as SpawnServerFn)
}};
}

pub static BUILTIN_EXTENSIONS: Lazy<HashMap<&'static str, BuiltinDef>> = Lazy::new(|| {
pub static BUILTIN_EXTENSIONS: Lazy<HashMap<&'static str, SpawnServerFn>> = Lazy::new(|| {
HashMap::from([
builtin!(developer, DeveloperServer),
builtin!(autovisualiser, AutoVisualiserRouter),
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-server/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use axum::http::StatusCode;
use goose::builtin_extension::register_builtin_extensions;
use goose::execution::manager::AgentManager;
use goose::scheduler_trait::SchedulerTrait;
use goose::session::SessionManager;
Expand Down Expand Up @@ -26,6 +27,8 @@ pub struct AppState {

impl AppState {
pub async fn new() -> anyhow::Result<Arc<AppState>> {
register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone());

let agent_manager = AgentManager::instance().await?;
let tunnel_manager = Arc::new(TunnelManager::new());

Expand Down
1 change: 0 additions & 1 deletion crates/goose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ dashmap = "6.1"
ahash = "0.8"
tokio-util = { version = "0.7.15", features = ["compat"] }
unicode-normalization = "0.1"
goose-mcp = { path = "../goose-mcp" }
zip = "0.6"
sys-info = "0.9"

Expand Down
19 changes: 6 additions & 13 deletions crates/goose/src/agents/extension_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use super::types::SharedProvider;
use crate::agents::extension::{Envs, ProcessExit};
use crate::agents::extension_malware_check;
use crate::agents::mcp_client::{McpClient, McpClientTrait};
use crate::builtin_extension::get_builtin_extension;
use crate::config::extensions::name_to_key;
use crate::config::search_path::SearchPaths;
use crate::config::{get_all_extensions, Config};
Expand Down Expand Up @@ -564,14 +565,9 @@ impl ExtensionManager {
}
ExtensionConfig::Builtin { name, timeout, .. } => {
let timeout_duration = Duration::from_secs(timeout.unwrap_or(300));
let normalized_name = name_to_key(name);

if !goose_mcp::BUILTIN_EXTENSIONS.contains_key(normalized_name.as_str()) {
return Err(ExtensionError::ConfigError(format!(
"Unknown builtin extension: {}",
name
)));
}
let extension_fn = get_builtin_extension(name.as_str()).ok_or_else(|| {
ExtensionError::ConfigError(format!("Unknown builtin extension: {}", name))
})?;
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExtensionConfig::Builtin lookup now uses the raw name, but builtin keys are normalized elsewhere (and previously used name_to_key), so configs like "Developer"/"developer" may regress; lookup should use the normalized key (or ensure registration uses the same normalization).

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The builtin extension lookup no longer normalizes the configured name (unlike the previous name_to_key lookup), so configs like "Developer" or names containing spaces/hyphens that previously worked may now fail with "Unknown builtin extension"; compute normalized_name = name_to_key(name) once and use it for both registry lookup and the Docker goose mcp invocation.

Copilot uses AI. Check for mistakes.

if let Some(container) = container {
let container_id = container.id();
Expand All @@ -580,6 +576,7 @@ impl ExtensionManager {
builtin = %name,
"Starting builtin extension inside Docker container"
);
let normalized_name = name_to_key(name);
let command = Command::new("docker").configure(|command| {
command
.arg("exec")
Expand All @@ -600,10 +597,6 @@ impl ExtensionManager {
.await?;
Box::new(client)
} else {
let def = goose_mcp::BUILTIN_EXTENSIONS
.get(normalized_name.as_str())
.unwrap();

// Set GOOSE_WORKING_DIR in the current process for builtin extensions
// since they run in-process and read from std::env::var
if effective_working_dir.exists() && effective_working_dir.is_dir() {
Expand All @@ -616,7 +609,7 @@ impl ExtensionManager {

let (server_read, client_write) = tokio::io::duplex(65536);
let (client_read, server_write) = tokio::io::duplex(65536);
(def.spawn_server)(server_read, server_write);
extension_fn(server_read, server_write);
Box::new(
McpClient::connect(
(client_read, client_write),
Expand Down
24 changes: 24 additions & 0 deletions crates/goose/src/builtin_extension.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::RwLock;

pub type SpawnServerFn = fn(tokio::io::DuplexStream, tokio::io::DuplexStream);

static BUILTIN_REGISTRY: Lazy<RwLock<HashMap<&'static str, SpawnServerFn>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
Comment on lines +7 to +8
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module uses std::sync::RwLock but it’s accessed from async code paths (e.g., extension loading on Tokio); consider switching to tokio::sync::RwLock (or another async-friendly container) to avoid blocking executor threads.

Copilot uses AI. Check for mistakes.

/// Register a builtin extension into the global registry
pub fn register_builtin_extension(name: &'static str, spawn_fn: SpawnServerFn) {
BUILTIN_REGISTRY.write().unwrap().insert(name, spawn_fn);
}

/// Register multiple builtin extensions from a HashMap
pub fn register_builtin_extensions(extensions: HashMap<&'static str, SpawnServerFn>) {
let mut registry = BUILTIN_REGISTRY.write().unwrap();
registry.extend(extensions);
}

/// Get a copy of all registered builtin extensions
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says this returns “a copy of all registered builtin extensions”, but the function returns a single extension by name; update the comment to match the behavior.

Suggested change
/// Get a copy of all registered builtin extensions
/// Get a registered builtin extension by name

Copilot uses AI. Check for mistakes.
pub fn get_builtin_extension(name: &str) -> Option<SpawnServerFn> {
BUILTIN_REGISTRY.read().unwrap().get(name).cloned()
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_builtin_extensions() clones the entire registry, which forces an allocation even when callers only need a lookup; consider adding has_builtin_extension(name: &str) / get_builtin_extension(name: &str) helpers that do the lookup under the read lock to avoid cloning the full HashMap.

Suggested change
}
}
/// Check if a builtin extension with the given name is registered
pub fn has_builtin_extension(name: &str) -> bool {
BUILTIN_REGISTRY.read().unwrap().contains_key(name)
}
/// Get the builtin extension spawn function for the given name, if registered
pub fn get_builtin_extension(name: &str) -> Option<SpawnServerFn> {
BUILTIN_REGISTRY.read().unwrap().get(name).copied()
}

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions crates/goose/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod action_required_manager;
pub mod agents;
pub mod builtin_extension;
pub mod config;
pub mod context_mgmt;
pub mod conversation;
Expand Down
Loading