From 74400aa412c2b4ac625ddc10bedf3721a724078d Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Mon, 6 Apr 2026 21:05:55 -0700 Subject: [PATCH] feat: configurable extension timeouts via ACP _meta and global default (#8295) Signed-off-by: Bradley Axen Signed-off-by: Douwe Osinga Co-authored-by: Douwe Osinga --- crates/goose-acp/src/server.rs | 66 ++++++++++++-------- crates/goose/src/agents/extension_manager.rs | 15 +++-- crates/goose/src/config/base.rs | 1 + 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 8c1e0197..4a12d204 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -31,7 +31,7 @@ use sacp::schema::{ ConfigOptionUpdate, Content, ContentBlock, ContentChunk, CurrentModeUpdate, EmbeddedResource, EmbeddedResourceResource, FileSystemCapabilities, ImageContent, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, - LoadSessionResponse, McpCapabilities, McpServer, ModelId, ModelInfo, NewSessionRequest, + LoadSessionResponse, McpCapabilities, McpServer, Meta, ModelId, ModelInfo, NewSessionRequest, NewSessionResponse, PermissionOption, PermissionOptionKind, PromptCapabilities, PromptRequest, PromptResponse, RequestPermissionOutcome, RequestPermissionRequest, ResourceLink, SessionCapabilities, SessionCloseCapabilities, SessionConfigOption, @@ -90,34 +90,46 @@ pub struct GooseAcpAgent { disable_session_naming: bool, } +fn extract_timeout_from_meta(meta: &Option) -> Option { + meta.as_ref() + .and_then(|m| m.get("timeout")) + .and_then(|v| v.as_u64()) +} + fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result { match mcp_server { - McpServer::Stdio(stdio) => Ok(ExtensionConfig::Stdio { - name: stdio.name, - description: String::new(), - cmd: stdio.command.to_string_lossy().to_string(), - args: stdio.args, - envs: Envs::new(stdio.env.into_iter().map(|e| (e.name, e.value)).collect()), - env_keys: vec![], - timeout: None, - bundled: Some(false), - available_tools: vec![], - }), - McpServer::Http(http) => Ok(ExtensionConfig::StreamableHttp { - name: http.name, - description: String::new(), - uri: http.url, - envs: Envs::default(), - env_keys: vec![], - headers: http - .headers - .into_iter() - .map(|h| (h.name, h.value)) - .collect(), - timeout: None, - bundled: Some(false), - available_tools: vec![], - }), + McpServer::Stdio(stdio) => { + let timeout = extract_timeout_from_meta(&stdio.meta); + Ok(ExtensionConfig::Stdio { + name: stdio.name, + description: String::new(), + cmd: stdio.command.to_string_lossy().to_string(), + args: stdio.args, + envs: Envs::new(stdio.env.into_iter().map(|e| (e.name, e.value)).collect()), + env_keys: vec![], + timeout, + bundled: Some(false), + available_tools: vec![], + }) + } + McpServer::Http(http) => { + let timeout = extract_timeout_from_meta(&http.meta); + Ok(ExtensionConfig::StreamableHttp { + name: http.name, + description: String::new(), + uri: http.url, + envs: Envs::default(), + env_keys: vec![], + headers: http + .headers + .into_iter() + .map(|h| (h.name, h.value)) + .collect(), + timeout, + bundled: Some(false), + available_tools: vec![], + }) + } McpServer::Sse(_) => Err("SSE is unsupported, migrate to streamable_http".to_string()), _ => Err("Unknown MCP server type".to_string()), } diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index bee36018..6f80e976 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -58,6 +58,14 @@ static RE_ENV_BRACES: Lazy = static RE_ENV_SIMPLE: Lazy = Lazy::new(|| regex::Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").expect("valid regex")); +fn resolve_timeout(timeout: Option) -> u64 { + timeout.unwrap_or_else(|| { + Config::global() + .get_goose_default_extension_timeout() + .unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT) + }) +} + struct Extension { pub config: ExtensionConfig, /// Resolved config snapshot (with secrets from keyring substituted) @@ -275,7 +283,7 @@ async fn child_process_client( let client_result = McpClient::connect_with_container( transport, - Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)), + Duration::from_secs(resolve_timeout(*timeout)), provider, docker_container, client_name, @@ -441,8 +449,7 @@ async fn create_streamable_http_client( }, ); - let timeout_duration = - Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)); + let timeout_duration = Duration::from_secs(resolve_timeout(timeout)); let client_res = McpClient::connect( transport, @@ -636,7 +643,7 @@ impl ExtensionManager { (def.client_factory)(context) } else { // Builtin MCP server extension - let timeout_secs = timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT); + let timeout_secs = resolve_timeout(timeout); let extension_fn = get_builtin_extension(normalized_name.as_str()).ok_or_else(|| { ExtensionError::ConfigError(format!("Unknown extension: {}", name)) diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index d2dd2ce0..b6698b5a 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -1064,6 +1064,7 @@ config_value!(GEMINI3_THINKING_LEVEL, String); config_value!(CLAUDE_THINKING_TYPE, String); config_value!(CLAUDE_THINKING_EFFORT, String); config_value!(CLAUDE_THINKING_BUDGET, i32); +config_value!(GOOSE_DEFAULT_EXTENSION_TIMEOUT, u64); fn find_workspace_or_exe_root() -> Option { let exe = std::env::current_exe().ok()?;