From 258ba436f1f784aa058625aec6b6653df83fe9c6 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 30 Mar 2026 10:48:49 -0700 Subject: [PATCH] codex-tools: extract discoverable tool models (#16254) ## Why `#16193` moved the pure `tool_search` and `tool_suggest` spec builders into `codex-tools`, but `codex-core` still owned the shared discoverable-tool model that those builders and the `tool_suggest` runtime both depend on. This change continues the migration by moving that reusable model boundary out of `codex-core` as well, so the discovery/suggestion stack uses one shared set of types and `core/src/tools` no longer needs its own `discoverable.rs` module. ## What changed - Moved `DiscoverableTool`, `DiscoverablePluginInfo`, and `filter_tool_suggest_discoverable_tools_for_client()` into `codex-rs/tools/src/tool_discovery.rs` alongside the extracted discovery/suggestion spec builders. - Added `codex-app-server-protocol` as a `codex-tools` dependency so the shared discoverable-tool model can own the connector-side `AppInfo` variant directly. - Updated `core/src/tools/handlers/tool_suggest.rs`, `core/src/tools/spec.rs`, `core/src/tools/router.rs`, `core/src/connectors.rs`, and `core/src/codex.rs` to consume the shared `codex-tools` model instead of the old core-local declarations. - Changed `core/src/plugins/discoverable.rs` to return `DiscoverablePluginInfo` directly, moved the pure client-filter coverage into `tool_discovery_tests.rs`, and deleted the old `core/src/tools/discoverable.rs` module. - Updated `codex-rs/tools/README.md` so the crate boundary documents that `codex-tools` now owns the discoverable-tool models in addition to the discovery/suggestion spec builders. ## Test plan - `cargo test -p codex-tools` - `CARGO_TARGET_DIR=/tmp/codex-core-discoverable-model cargo test -p codex-core --lib tools::handlers::tool_suggest::` - `CARGO_TARGET_DIR=/tmp/codex-core-discoverable-model cargo test -p codex-core --lib tools::spec::` - `CARGO_TARGET_DIR=/tmp/codex-core-discoverable-model cargo test -p codex-core --lib plugins::discoverable::` - `just bazel-lock-check` - `just argument-comment-lint` ## References - #16193 - #16154 - #15923 - #15928 - #15944 - #15953 - #16031 - #16047 - #16129 - #16132 - #16138 - #16141 --- codex-rs/Cargo.lock | 1 + codex-rs/core/src/codex.rs | 3 +- codex-rs/core/src/connectors.rs | 4 +- codex-rs/core/src/plugins/discoverable.rs | 27 ++++-- .../core/src/plugins/discoverable_tests.rs | 32 ++----- codex-rs/core/src/tools/discoverable.rs | 94 ------------------- .../core/src/tools/handlers/tool_suggest.rs | 4 +- .../src/tools/handlers/tool_suggest_tests.rs | 52 +--------- codex-rs/core/src/tools/mod.rs | 1 - codex-rs/core/src/tools/router.rs | 2 +- codex-rs/core/src/tools/spec.rs | 2 +- codex-rs/core/src/tools/spec_tests.rs | 3 +- codex-rs/tools/Cargo.toml | 1 + codex-rs/tools/README.md | 2 +- codex-rs/tools/src/lib.rs | 3 + codex-rs/tools/src/tool_discovery.rs | 75 +++++++++++++++ codex-rs/tools/src/tool_discovery_tests.rs | 49 ++++++++++ 17 files changed, 168 insertions(+), 187 deletions(-) delete mode 100644 codex-rs/core/src/tools/discoverable.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 3dd569c6b0..f8d1de44fc 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2613,6 +2613,7 @@ dependencies = [ name = "codex-tools" version = "0.0.0" dependencies = [ + "codex-app-server-protocol", "codex-code-mode", "codex-protocol", "pretty_assertions", diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9a8cc9cea7..2c70aad7a7 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -121,6 +121,7 @@ use codex_protocol::request_user_input::RequestUserInputResponse; use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_terminal_detection::user_agent; +use codex_tools::filter_tool_suggest_discoverable_tools_for_client; use codex_utils_output_truncation::TruncationPolicy; use codex_utils_stream_parser::AssistantTextChunk; use codex_utils_stream_parser::AssistantTextStreamParser; @@ -6546,7 +6547,7 @@ pub(crate) async fn built_tools( ) .await .map(|discoverable_tools| { - crate::tools::discoverable::filter_tool_suggest_discoverable_tools_for_client( + filter_tool_suggest_discoverable_tools_for_client( discoverable_tools, turn_context.app_server_client_name.as_deref(), ) diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index e4999b9b41..a15138f6fb 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -18,6 +18,7 @@ use codex_connectors::AllConnectorsCacheKey; use codex_connectors::DirectoryListResponse; use codex_login::token_data::TokenData; use codex_protocol::protocol::SandboxPolicy; +use codex_tools::DiscoverableTool; use rmcp::model::ToolAnnotations; use serde::Deserialize; use serde::de::DeserializeOwned; @@ -44,8 +45,6 @@ use crate::mcp_connection_manager::codex_apps_tools_cache_key; use crate::plugins::AppConnectorId; use crate::plugins::PluginsManager; use crate::plugins::list_tool_suggest_discoverable_plugins; -use crate::tools::discoverable::DiscoverablePluginInfo; -use crate::tools::discoverable::DiscoverableTool; use codex_features::Feature; pub use codex_connectors::CONNECTORS_CACHE_TTL; @@ -133,7 +132,6 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( .map(DiscoverableTool::from); let discoverable_plugins = list_tool_suggest_discoverable_plugins(config)? .into_iter() - .map(DiscoverablePluginInfo::from) .map(DiscoverableTool::from); Ok(discoverable_connectors .chain(discoverable_plugins) diff --git a/codex-rs/core/src/plugins/discoverable.rs b/codex-rs/core/src/plugins/discoverable.rs index 427706235f..f7006185bf 100644 --- a/codex-rs/core/src/plugins/discoverable.rs +++ b/codex-rs/core/src/plugins/discoverable.rs @@ -9,6 +9,7 @@ use super::PluginsManager; use crate::config::Config; use crate::config::types::ToolSuggestDiscoverableType; use codex_features::Feature; +use codex_tools::DiscoverablePluginInfo; const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[ "github@openai-curated", @@ -23,7 +24,7 @@ const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[ pub(crate) fn list_tool_suggest_discoverable_plugins( config: &Config, -) -> anyhow::Result> { +) -> anyhow::Result> { if !config.features.enabled(Feature::Plugins) { return Ok(Vec::new()); } @@ -47,7 +48,7 @@ pub(crate) fn list_tool_suggest_discoverable_plugins( return Ok(Vec::new()); }; - let mut discoverable_plugins = Vec::::new(); + let mut discoverable_plugins = Vec::::new(); for plugin in curated_marketplace.plugins { if plugin.installed || (!TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str()) @@ -66,14 +67,28 @@ pub(crate) fn list_tool_suggest_discoverable_plugins( marketplace_path: curated_marketplace.path.clone(), }, ) { - Ok(plugin) => discoverable_plugins.push(plugin.plugin.into()), + Ok(plugin) => { + let plugin: PluginCapabilitySummary = plugin.plugin.into(); + discoverable_plugins.push(DiscoverablePluginInfo { + id: plugin.config_name, + name: plugin.display_name, + description: plugin.description, + has_skills: plugin.has_skills, + mcp_server_names: plugin.mcp_server_names, + app_connector_ids: plugin + .app_connector_ids + .into_iter() + .map(|connector_id| connector_id.0) + .collect(), + }); + } Err(err) => warn!("failed to load discoverable plugin suggestion {plugin_id}: {err:#}"), } } discoverable_plugins.sort_by(|left, right| { - left.display_name - .cmp(&right.display_name) - .then_with(|| left.config_name.cmp(&right.config_name)) + left.name + .cmp(&right.name) + .then_with(|| left.id.cmp(&right.id)) }); Ok(discoverable_plugins) } diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index fd9df9ddf6..70ac887cb6 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -5,7 +5,7 @@ use crate::plugins::test_support::write_curated_plugin_sha; use crate::plugins::test_support::write_file; use crate::plugins::test_support::write_openai_curated_marketplace; use crate::plugins::test_support::write_plugins_feature_config; -use crate::tools::discoverable::DiscoverablePluginInfo; +use codex_tools::DiscoverablePluginInfo; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::tempdir; @@ -18,11 +18,7 @@ async fn list_tool_suggest_discoverable_plugins_returns_uninstalled_curated_plug write_plugins_feature_config(codex_home.path()); let config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) - .unwrap() - .into_iter() - .map(DiscoverablePluginInfo::from) - .collect::>(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); assert_eq!( discoverable_plugins, @@ -52,11 +48,7 @@ plugins = false ); let config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) - .unwrap() - .into_iter() - .map(DiscoverablePluginInfo::from) - .collect::>(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); assert_eq!(discoverable_plugins, Vec::::new()); } @@ -76,11 +68,7 @@ async fn list_tool_suggest_discoverable_plugins_normalizes_description() { ); let config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) - .unwrap() - .into_iter() - .map(DiscoverablePluginInfo::from) - .collect::>(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); assert_eq!( discoverable_plugins, @@ -115,11 +103,7 @@ async fn list_tool_suggest_discoverable_plugins_omits_installed_curated_plugins( .expect("plugin should install"); let refreshed_config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&refreshed_config) - .unwrap() - .into_iter() - .map(DiscoverablePluginInfo::from) - .collect::>(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&refreshed_config).unwrap(); assert_eq!(discoverable_plugins, Vec::::new()); } @@ -140,11 +124,7 @@ discoverables = [{ type = "plugin", id = "sample@openai-curated" }] ); let config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) - .unwrap() - .into_iter() - .map(DiscoverablePluginInfo::from) - .collect::>(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); assert_eq!( discoverable_plugins, diff --git a/codex-rs/core/src/tools/discoverable.rs b/codex-rs/core/src/tools/discoverable.rs deleted file mode 100644 index 78d8935aa7..0000000000 --- a/codex-rs/core/src/tools/discoverable.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::plugins::PluginCapabilitySummary; -use codex_app_server_protocol::AppInfo; -use codex_tools::DiscoverableToolType; - -const TUI_CLIENT_NAME: &str = "codex-tui"; - -#[derive(Clone, Debug, PartialEq)] -pub(crate) enum DiscoverableTool { - Connector(Box), - Plugin(Box), -} - -impl DiscoverableTool { - pub(crate) fn tool_type(&self) -> DiscoverableToolType { - match self { - Self::Connector(_) => DiscoverableToolType::Connector, - Self::Plugin(_) => DiscoverableToolType::Plugin, - } - } - - pub(crate) fn id(&self) -> &str { - match self { - Self::Connector(connector) => connector.id.as_str(), - Self::Plugin(plugin) => plugin.id.as_str(), - } - } - - pub(crate) fn name(&self) -> &str { - match self { - Self::Connector(connector) => connector.name.as_str(), - Self::Plugin(plugin) => plugin.name.as_str(), - } - } - - pub(crate) fn install_url(&self) -> Option<&str> { - match self { - Self::Connector(connector) => connector.install_url.as_deref(), - Self::Plugin(_) => None, - } - } -} - -impl From for DiscoverableTool { - fn from(value: AppInfo) -> Self { - Self::Connector(Box::new(value)) - } -} - -impl From for DiscoverableTool { - fn from(value: DiscoverablePluginInfo) -> Self { - Self::Plugin(Box::new(value)) - } -} - -pub(crate) fn filter_tool_suggest_discoverable_tools_for_client( - discoverable_tools: Vec, - app_server_client_name: Option<&str>, -) -> Vec { - if app_server_client_name != Some(TUI_CLIENT_NAME) { - return discoverable_tools; - } - - discoverable_tools - .into_iter() - .filter(|tool| !matches!(tool, DiscoverableTool::Plugin(_))) - .collect() -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct DiscoverablePluginInfo { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) description: Option, - pub(crate) has_skills: bool, - pub(crate) mcp_server_names: Vec, - pub(crate) app_connector_ids: Vec, -} - -impl From for DiscoverablePluginInfo { - fn from(value: PluginCapabilitySummary) -> Self { - Self { - id: value.config_name, - name: value.display_name, - description: value.description, - has_skills: value.has_skills, - mcp_server_names: value.mcp_server_names, - app_connector_ids: value - .app_connector_ids - .into_iter() - .map(|connector_id| connector_id.0) - .collect(), - } - } -} diff --git a/codex-rs/core/src/tools/handlers/tool_suggest.rs b/codex-rs/core/src/tools/handlers/tool_suggest.rs index 478bd2c837..c68ac8589c 100644 --- a/codex-rs/core/src/tools/handlers/tool_suggest.rs +++ b/codex-rs/core/src/tools/handlers/tool_suggest.rs @@ -8,8 +8,10 @@ use codex_app_server_protocol::McpElicitationSchema; use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; use codex_rmcp_client::ElicitationAction; +use codex_tools::DiscoverableTool; use codex_tools::DiscoverableToolAction; use codex_tools::DiscoverableToolType; +use codex_tools::filter_tool_suggest_discoverable_tools_for_client; use rmcp::model::RequestId; use serde::Deserialize; use serde::Serialize; @@ -22,8 +24,6 @@ use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; -use crate::tools::discoverable::DiscoverableTool; -use crate::tools::discoverable::filter_tool_suggest_discoverable_tools_for_client; use crate::tools::handlers::parse_arguments; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; diff --git a/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs b/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs index c932e03145..56089a1aa9 100644 --- a/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs +++ b/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs @@ -5,9 +5,9 @@ use crate::plugins::test_support::load_plugins_config; use crate::plugins::test_support::write_curated_plugin_sha; use crate::plugins::test_support::write_openai_curated_marketplace; use crate::plugins::test_support::write_plugins_feature_config; -use crate::tools::discoverable::DiscoverablePluginInfo; -use crate::tools::discoverable::filter_tool_suggest_discoverable_tools_for_client; use codex_app_server_protocol::AppInfo; +use codex_tools::DiscoverablePluginInfo; +use codex_tools::DiscoverableTool; use codex_tools::DiscoverableToolAction; use codex_tools::DiscoverableToolType; use codex_utils_absolute_path::AbsolutePathBuf; @@ -161,54 +161,6 @@ fn build_tool_suggestion_meta_uses_expected_shape() { ); } -#[test] -fn filter_tool_suggest_discoverable_tools_for_codex_tui_omits_plugins() { - let discoverable_tools = vec![ - DiscoverableTool::Connector(Box::new(AppInfo { - id: "connector_google_calendar".to_string(), - name: "Google Calendar".to_string(), - description: Some("Plan events and schedules.".to_string()), - logo_url: None, - logo_url_dark: None, - distribution_channel: None, - branding: None, - app_metadata: None, - labels: None, - install_url: Some("https://example.test/google-calendar".to_string()), - is_accessible: false, - is_enabled: true, - plugin_display_names: Vec::new(), - })), - DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { - id: "slack@openai-curated".to_string(), - name: "Slack".to_string(), - description: Some("Search Slack messages".to_string()), - has_skills: true, - mcp_server_names: vec!["slack".to_string()], - app_connector_ids: vec!["connector_slack".to_string()], - })), - ]; - - assert_eq!( - filter_tool_suggest_discoverable_tools_for_client(discoverable_tools, Some("codex-tui"),), - vec![DiscoverableTool::Connector(Box::new(AppInfo { - id: "connector_google_calendar".to_string(), - name: "Google Calendar".to_string(), - description: Some("Plan events and schedules.".to_string()), - logo_url: None, - logo_url_dark: None, - distribution_channel: None, - branding: None, - app_metadata: None, - labels: None, - install_url: Some("https://example.test/google-calendar".to_string()), - is_accessible: false, - is_enabled: true, - plugin_display_names: Vec::new(), - }))] - ); -} - #[test] fn verified_connector_suggestion_completed_requires_accessible_connector() { let accessible_connectors = vec![AppInfo { diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index 57859bba42..e866d1f5f7 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -1,6 +1,5 @@ pub mod code_mode; pub mod context; -pub(crate) mod discoverable; pub mod events; pub(crate) mod handlers; pub mod js_repl; diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index 1145e3a8f2..646c1d1387 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -7,7 +7,6 @@ use crate::sandboxing::SandboxPermissions; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; -use crate::tools::discoverable::DiscoverableTool; use crate::tools::registry::AnyToolResult; use crate::tools::registry::ToolRegistry; use crate::tools::spec::ToolsConfig; @@ -18,6 +17,7 @@ use codex_protocol::models::ResponseItem; use codex_protocol::models::SearchToolCallParams; use codex_protocol::models::ShellToolCallParams; use codex_tools::ConfiguredToolSpec; +use codex_tools::DiscoverableTool; use rmcp::model::Tool; use std::collections::HashMap; use std::sync::Arc; diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 6852671576..d0614fc35b 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -7,7 +7,6 @@ use crate::shell::Shell; use crate::shell::ShellType; use crate::tools::code_mode::PUBLIC_TOOL_NAME; use crate::tools::code_mode::WAIT_TOOL_NAME; -use crate::tools::discoverable::DiscoverableTool; use crate::tools::handlers::PLAN_TOOL; use crate::tools::handlers::TOOL_SEARCH_DEFAULT_LIMIT; use crate::tools::handlers::TOOL_SEARCH_TOOL_NAME; @@ -38,6 +37,7 @@ use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_tools::CommandToolOptions; +use codex_tools::DiscoverableTool; use codex_tools::DiscoverableToolType; use codex_tools::ShellToolOptions; use codex_tools::SpawnAgentToolOptions; diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 283bcd7161..16f87ca50e 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -4,7 +4,6 @@ use crate::models_manager::model_info::with_config_overrides; use crate::shell::Shell; use crate::shell::ShellType; use crate::tools::ToolRouter; -use crate::tools::discoverable::DiscoverablePluginInfo; use crate::tools::router::ToolRouterParams; use codex_app_server_protocol::AppInfo; use codex_protocol::models::VIEW_IMAGE_TOOL_NAME; @@ -14,6 +13,8 @@ use codex_protocol::openai_models::ModelsResponse; use codex_tools::AdditionalProperties; use codex_tools::CommandToolOptions; use codex_tools::ConfiguredToolSpec; +use codex_tools::DiscoverablePluginInfo; +use codex_tools::DiscoverableTool; use codex_tools::FreeformTool; use codex_tools::ResponsesApiTool; use codex_tools::ResponsesApiWebSearchFilters; diff --git a/codex-rs/tools/Cargo.toml b/codex-rs/tools/Cargo.toml index 9e2d23fb58..3d61dab7d8 100644 --- a/codex-rs/tools/Cargo.toml +++ b/codex-rs/tools/Cargo.toml @@ -8,6 +8,7 @@ version.workspace = true workspace = true [dependencies] +codex-app-server-protocol = { workspace = true } codex-code-mode = { workspace = true } codex-protocol = { workspace = true } rmcp = { workspace = true, default-features = false, features = [ diff --git a/codex-rs/tools/README.md b/codex-rs/tools/README.md index 59533f459d..cafb4b89ac 100644 --- a/codex-rs/tools/README.md +++ b/codex-rs/tools/README.md @@ -27,7 +27,7 @@ schema and Responses API tool primitives that no longer need to live in - local host tool spec builders for shell/exec/request-permissions/view-image - collaboration and agent-job `ToolSpec` builders for spawn/send/wait/close, `request_user_input`, and CSV fanout/reporting -- tool discovery and suggestion models / `ToolSpec` builders for +- discoverable-tool models, client filtering, and `ToolSpec` builders for `tool_search` and `tool_suggest` - `parse_tool_input_schema()` - `parse_dynamic_tool()` diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 9dd629f916..9ae6031ccd 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -67,12 +67,15 @@ pub use responses_api::mcp_tool_to_deferred_responses_api_tool; pub use responses_api::mcp_tool_to_responses_api_tool; pub use responses_api::tool_definition_to_responses_api_tool; pub use tool_definition::ToolDefinition; +pub use tool_discovery::DiscoverablePluginInfo; +pub use tool_discovery::DiscoverableTool; pub use tool_discovery::DiscoverableToolAction; pub use tool_discovery::DiscoverableToolType; pub use tool_discovery::ToolSearchAppInfo; pub use tool_discovery::ToolSuggestEntry; pub use tool_discovery::create_tool_search_tool; pub use tool_discovery::create_tool_suggest_tool; +pub use tool_discovery::filter_tool_suggest_discoverable_tools_for_client; pub use tool_spec::ConfiguredToolSpec; pub use tool_spec::ResponsesApiWebSearchFilters; pub use tool_spec::ResponsesApiWebSearchUserLocation; diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index f2b9b4f934..0216cc1703 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -1,10 +1,13 @@ use crate::JsonSchema; use crate::ResponsesApiTool; use crate::ToolSpec; +use codex_app_server_protocol::AppInfo; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; +const TUI_CLIENT_NAME: &str = "codex-tui"; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ToolSearchAppInfo { pub name: String, @@ -34,6 +37,78 @@ pub enum DiscoverableToolAction { Enable, } +#[derive(Clone, Debug, PartialEq)] +pub enum DiscoverableTool { + Connector(Box), + Plugin(Box), +} + +impl DiscoverableTool { + pub fn tool_type(&self) -> DiscoverableToolType { + match self { + Self::Connector(_) => DiscoverableToolType::Connector, + Self::Plugin(_) => DiscoverableToolType::Plugin, + } + } + + pub fn id(&self) -> &str { + match self { + Self::Connector(connector) => connector.id.as_str(), + Self::Plugin(plugin) => plugin.id.as_str(), + } + } + + pub fn name(&self) -> &str { + match self { + Self::Connector(connector) => connector.name.as_str(), + Self::Plugin(plugin) => plugin.name.as_str(), + } + } + + pub fn install_url(&self) -> Option<&str> { + match self { + Self::Connector(connector) => connector.install_url.as_deref(), + Self::Plugin(_) => None, + } + } +} + +impl From for DiscoverableTool { + fn from(value: AppInfo) -> Self { + Self::Connector(Box::new(value)) + } +} + +impl From for DiscoverableTool { + fn from(value: DiscoverablePluginInfo) -> Self { + Self::Plugin(Box::new(value)) + } +} + +pub fn filter_tool_suggest_discoverable_tools_for_client( + discoverable_tools: Vec, + app_server_client_name: Option<&str>, +) -> Vec { + if app_server_client_name != Some(TUI_CLIENT_NAME) { + return discoverable_tools; + } + + discoverable_tools + .into_iter() + .filter(|tool| !matches!(tool, DiscoverableTool::Plugin(_))) + .collect() +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DiscoverablePluginInfo { + pub id: String, + pub name: String, + pub description: Option, + pub has_skills: bool, + pub mcp_server_names: Vec, + pub app_connector_ids: Vec, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ToolSuggestEntry { pub id: String, diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index 7be6bedd2c..1b241c5844 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -1,4 +1,5 @@ use super::*; +use codex_app_server_protocol::AppInfo; use pretty_assertions::assert_eq; use serde_json::json; use std::collections::BTreeMap; @@ -147,3 +148,51 @@ fn discoverable_tool_enums_use_expected_wire_names() { }) ); } + +#[test] +fn filter_tool_suggest_discoverable_tools_for_codex_tui_omits_plugins() { + let discoverable_tools = vec![ + DiscoverableTool::Connector(Box::new(AppInfo { + id: "connector_google_calendar".to_string(), + name: "Google Calendar".to_string(), + description: Some("Plan events and schedules.".to_string()), + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: Some("https://example.test/google-calendar".to_string()), + is_accessible: false, + is_enabled: true, + plugin_display_names: Vec::new(), + })), + DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { + id: "slack@openai-curated".to_string(), + name: "Slack".to_string(), + description: Some("Search Slack messages".to_string()), + has_skills: true, + mcp_server_names: vec!["slack".to_string()], + app_connector_ids: vec!["connector_slack".to_string()], + })), + ]; + + assert_eq!( + filter_tool_suggest_discoverable_tools_for_client(discoverable_tools, Some("codex-tui"),), + vec![DiscoverableTool::Connector(Box::new(AppInfo { + id: "connector_google_calendar".to_string(), + name: "Google Calendar".to_string(), + description: Some("Plan events and schedules.".to_string()), + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: Some("https://example.test/google-calendar".to_string()), + is_accessible: false, + is_enabled: true, + plugin_display_names: Vec::new(), + }))] + ); +}