From 9df520d50768c3c865b612819322a59da50ee61e Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 19 Aug 2026 10:58:20 +0000 Subject: [PATCH] fix(cli): deduplicate enabled builtins (#11340) --- crates/goose-cli/src/session/builder.rs | 53 ++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 94b4c7fff..36b44e2bb 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -86,6 +86,28 @@ fn disambiguate_stdio_extension_names( Ok(()) } +fn is_builtin_or_platform_extension(config: &ExtensionConfig) -> bool { + matches!( + config, + ExtensionConfig::Builtin { .. } | ExtensionConfig::Platform { .. } + ) +} + +fn deduplicate_cli_builtins( + existing: &[(String, ExtensionConfig)], + cli_extensions: &mut Vec<(String, ExtensionConfig, bool)>, +) { + let mut seen_builtin_names = existing + .iter() + .filter(|(_, config)| is_builtin_or_platform_extension(config)) + .map(|(_, config)| config.key()) + .collect::>(); + + cli_extensions.retain(|(_, config, _)| { + !is_builtin_or_platform_extension(config) || seen_builtin_names.insert(config.key()) + }); +} + fn parse_cli_flag_extensions( extensions: &[String], streamable_http_extensions: &[StreamableHttpOptions], @@ -602,7 +624,7 @@ async fn collect_extension_configs( resolve_extensions_for_new_session(recipe_extensions, None) }; - let cli_flag_extensions = parse_cli_flag_extensions( + let mut cli_flag_extensions = parse_cli_flag_extensions( &session_config.extensions, &session_config.streamable_http_extensions, &session_config.builtins, @@ -621,6 +643,8 @@ async fn collect_extension_configs( ); } + deduplicate_cli_builtins(&all, &mut cli_flag_extensions); + let cli_start = all.len(); let renameable = cli_flag_extensions .iter() @@ -1035,6 +1059,33 @@ mod tests { .contains("extension name 'memory' is already in use")); } + #[test] + fn test_cli_builtin_reuses_configured_registered_extension() { + let configured = CliSession::parse_builtin_extensions("developer") + .into_iter() + .next() + .unwrap(); + let extensions = vec![("configured".to_string(), configured)]; + let mut cli = parse_cli_flag_extensions(&[], &[], &["developer".to_string()]); + + deduplicate_cli_builtins(&extensions, &mut cli); + + assert!(cli.is_empty()); + } + + #[test] + fn test_cli_builtin_does_not_reuse_different_extension_with_same_name() { + let extensions = vec![( + "configured".to_string(), + CliSession::parse_stdio_extension("developer:npx custom-developer").unwrap(), + )]; + let mut cli = parse_cli_flag_extensions(&[], &[], &["developer".to_string()]); + + deduplicate_cli_builtins(&extensions, &mut cli); + + assert_eq!(cli.len(), 1); + } + #[test] fn test_identical_commands_still_get_distinct_names() { assert_eq!(