feat: configurable extension timeouts via ACP _meta and global default (#8295)

Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Bradley Axen
2026-04-06 21:05:55 -07:00
committed by GitHub
parent de730da900
commit 74400aa412
3 changed files with 51 additions and 31 deletions
+39 -27
View File
@@ -31,7 +31,7 @@ use sacp::schema::{
ConfigOptionUpdate, Content, ContentBlock, ContentChunk, CurrentModeUpdate, EmbeddedResource, ConfigOptionUpdate, Content, ContentBlock, ContentChunk, CurrentModeUpdate, EmbeddedResource,
EmbeddedResourceResource, FileSystemCapabilities, ImageContent, InitializeRequest, EmbeddedResourceResource, FileSystemCapabilities, ImageContent, InitializeRequest,
InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest,
LoadSessionResponse, McpCapabilities, McpServer, ModelId, ModelInfo, NewSessionRequest, LoadSessionResponse, McpCapabilities, McpServer, Meta, ModelId, ModelInfo, NewSessionRequest,
NewSessionResponse, PermissionOption, PermissionOptionKind, PromptCapabilities, PromptRequest, NewSessionResponse, PermissionOption, PermissionOptionKind, PromptCapabilities, PromptRequest,
PromptResponse, RequestPermissionOutcome, RequestPermissionRequest, ResourceLink, PromptResponse, RequestPermissionOutcome, RequestPermissionRequest, ResourceLink,
SessionCapabilities, SessionCloseCapabilities, SessionConfigOption, SessionCapabilities, SessionCloseCapabilities, SessionConfigOption,
@@ -90,34 +90,46 @@ pub struct GooseAcpAgent {
disable_session_naming: bool, disable_session_naming: bool,
} }
fn extract_timeout_from_meta(meta: &Option<Meta>) -> Option<u64> {
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<ExtensionConfig, String> { fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result<ExtensionConfig, String> {
match mcp_server { match mcp_server {
McpServer::Stdio(stdio) => Ok(ExtensionConfig::Stdio { McpServer::Stdio(stdio) => {
name: stdio.name, let timeout = extract_timeout_from_meta(&stdio.meta);
description: String::new(), Ok(ExtensionConfig::Stdio {
cmd: stdio.command.to_string_lossy().to_string(), name: stdio.name,
args: stdio.args, description: String::new(),
envs: Envs::new(stdio.env.into_iter().map(|e| (e.name, e.value)).collect()), cmd: stdio.command.to_string_lossy().to_string(),
env_keys: vec![], args: stdio.args,
timeout: None, envs: Envs::new(stdio.env.into_iter().map(|e| (e.name, e.value)).collect()),
bundled: Some(false), env_keys: vec![],
available_tools: vec![], timeout,
}), bundled: Some(false),
McpServer::Http(http) => Ok(ExtensionConfig::StreamableHttp { available_tools: vec![],
name: http.name, })
description: String::new(), }
uri: http.url, McpServer::Http(http) => {
envs: Envs::default(), let timeout = extract_timeout_from_meta(&http.meta);
env_keys: vec![], Ok(ExtensionConfig::StreamableHttp {
headers: http name: http.name,
.headers description: String::new(),
.into_iter() uri: http.url,
.map(|h| (h.name, h.value)) envs: Envs::default(),
.collect(), env_keys: vec![],
timeout: None, headers: http
bundled: Some(false), .headers
available_tools: vec![], .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()), McpServer::Sse(_) => Err("SSE is unsupported, migrate to streamable_http".to_string()),
_ => Err("Unknown MCP server type".to_string()), _ => Err("Unknown MCP server type".to_string()),
} }
+11 -4
View File
@@ -58,6 +58,14 @@ static RE_ENV_BRACES: Lazy<regex::Regex> =
static RE_ENV_SIMPLE: Lazy<regex::Regex> = static RE_ENV_SIMPLE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").expect("valid regex")); Lazy::new(|| regex::Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").expect("valid regex"));
fn resolve_timeout(timeout: Option<u64>) -> u64 {
timeout.unwrap_or_else(|| {
Config::global()
.get_goose_default_extension_timeout()
.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)
})
}
struct Extension { struct Extension {
pub config: ExtensionConfig, pub config: ExtensionConfig,
/// Resolved config snapshot (with secrets from keyring substituted) /// Resolved config snapshot (with secrets from keyring substituted)
@@ -275,7 +283,7 @@ async fn child_process_client(
let client_result = McpClient::connect_with_container( let client_result = McpClient::connect_with_container(
transport, transport,
Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)), Duration::from_secs(resolve_timeout(*timeout)),
provider, provider,
docker_container, docker_container,
client_name, client_name,
@@ -441,8 +449,7 @@ async fn create_streamable_http_client(
}, },
); );
let timeout_duration = let timeout_duration = Duration::from_secs(resolve_timeout(timeout));
Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT));
let client_res = McpClient::connect( let client_res = McpClient::connect(
transport, transport,
@@ -636,7 +643,7 @@ impl ExtensionManager {
(def.client_factory)(context) (def.client_factory)(context)
} else { } else {
// Builtin MCP server extension // Builtin MCP server extension
let timeout_secs = timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT); let timeout_secs = resolve_timeout(timeout);
let extension_fn = let extension_fn =
get_builtin_extension(normalized_name.as_str()).ok_or_else(|| { get_builtin_extension(normalized_name.as_str()).ok_or_else(|| {
ExtensionError::ConfigError(format!("Unknown extension: {}", name)) ExtensionError::ConfigError(format!("Unknown extension: {}", name))
+1
View File
@@ -1064,6 +1064,7 @@ config_value!(GEMINI3_THINKING_LEVEL, String);
config_value!(CLAUDE_THINKING_TYPE, String); config_value!(CLAUDE_THINKING_TYPE, String);
config_value!(CLAUDE_THINKING_EFFORT, String); config_value!(CLAUDE_THINKING_EFFORT, String);
config_value!(CLAUDE_THINKING_BUDGET, i32); config_value!(CLAUDE_THINKING_BUDGET, i32);
config_value!(GOOSE_DEFAULT_EXTENSION_TIMEOUT, u64);
fn find_workspace_or_exe_root() -> Option<PathBuf> { fn find_workspace_or_exe_root() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?; let exe = std::env::current_exe().ok()?;