From b0292f4d5a82f3f7172b3d94723dd74f76910253 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Mon, 23 Mar 2026 15:32:57 -0700 Subject: [PATCH 1/2] update --- codex-rs/core/src/apps/render.rs | 66 ++++++++++++++++++++++++++- codex-rs/core/src/codex.rs | 13 +++++- codex-rs/core/src/tools/spec.rs | 5 +- codex-rs/core/src/tools/spec_tests.rs | 48 +++++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/apps/render.rs b/codex-rs/core/src/apps/render.rs index 7cc07c0747a..3657a23255a 100644 --- a/codex-rs/core/src/apps/render.rs +++ b/codex-rs/core/src/apps/render.rs @@ -1,10 +1,72 @@ use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_app_server_protocol::AppInfo; use codex_protocol::protocol::APPS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG; -pub(crate) fn render_apps_section() -> String { +pub(crate) fn render_apps_section(connectors: &[AppInfo]) -> Option { + if !connectors + .iter() + .any(|connector| connector.is_accessible && connector.is_enabled) + { + return None; + } + let body = format!( "## Apps (Connectors)\nApps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{{connector_id}})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps, the available apps will be listed by the `tool_search` tool.\nAn app is equivalent to a set of MCP tools within the `{CODEX_APPS_MCP_SERVER_NAME}` MCP.\nAn installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool.\nDo not additionally call list_mcp_resources or list_mcp_resource_templates for apps." ); - format!("{APPS_INSTRUCTIONS_OPEN_TAG}\n{body}\n{APPS_INSTRUCTIONS_CLOSE_TAG}") + Some(format!( + "{APPS_INSTRUCTIONS_OPEN_TAG}\n{body}\n{APPS_INSTRUCTIONS_CLOSE_TAG}" + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn connector(id: &str, is_accessible: bool, is_enabled: bool) -> AppInfo { + AppInfo { + id: id.to_string(), + name: id.to_string(), + description: None, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: None, + is_accessible, + is_enabled, + plugin_display_names: Vec::new(), + } + } + + #[test] + fn omits_apps_section_without_accessible_and_enabled_apps() { + assert_eq!(render_apps_section(&[]), None); + assert_eq!( + render_apps_section(&[connector( + "calendar", /*is_accessible*/ true, /*is_enabled*/ false + )]), + None + ); + assert_eq!( + render_apps_section(&[connector( + "calendar", /*is_accessible*/ false, /*is_enabled*/ true + )]), + None + ); + } + + #[test] + fn renders_apps_section_with_an_accessible_and_enabled_app() { + let rendered = render_apps_section(&[connector( + "calendar", /*is_accessible*/ true, /*is_enabled*/ true, + )]) + .expect("expected apps section"); + + assert!(rendered.starts_with(APPS_INSTRUCTIONS_OPEN_TAG)); + assert!(rendered.contains("## Apps (Connectors)")); + assert!(rendered.ends_with(APPS_INSTRUCTIONS_CLOSE_TAG)); + } } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9b810255268..cd6d087e46d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3537,7 +3537,18 @@ impl Session { } } if turn_context.apps_enabled() { - developer_sections.push(render_apps_section()); + let accessible_connectors = { + let mcp_connection_manager = self.services.mcp_connection_manager.read().await; + connectors::with_app_enabled_state( + connectors::accessible_connectors_from_mcp_tools( + &mcp_connection_manager.list_all_tools().await, + ), + &turn_context.config, + ) + }; + if let Some(apps_section) = render_apps_section(&accessible_connectors) { + developer_sections.push(apps_section); + } } let implicit_skills = turn_context .turn_skills diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index d8419e1f580..0a61d4330aa 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -338,7 +338,10 @@ impl ToolsConfig { let include_default_mode_request_user_input = include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput); let include_search_tool = model_info.supports_search_tool; - let include_tool_suggest = include_search_tool && features.enabled(Feature::ToolSuggest); + let include_tool_suggest = include_search_tool + && features.enabled(Feature::ToolSuggest) + && features.enabled(Feature::Apps) + && features.enabled(Feature::Plugins); let include_original_image_detail = can_request_original_image_detail(features, model_info); let include_artifact_tools = features.enabled(Feature::Artifact) && codex_artifacts::can_manage_artifact_runtime(); diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 1cb6bb16645..78cbb3b2732 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -1964,6 +1964,7 @@ fn tool_suggest_is_not_registered_without_feature_flag() { let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); + features.enable(Feature::Plugins); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -1994,6 +1995,52 @@ fn tool_suggest_is_not_registered_without_feature_flag() { ); } +#[test] +fn tool_suggest_requires_apps_and_plugins_features() { + let model_info = search_capable_model_info(); + let discoverable_tools = Some(vec![discoverable_connector( + "connector_2128aebfecb84f64a069897515042a44", + "Google Calendar", + "Plan events and schedules.", + )]); + let available_models = Vec::new(); + + for disabled_feature in [Feature::Apps, Feature::Plugins] { + let mut features = Features::with_defaults(); + features.enable(Feature::ToolSuggest); + for feature in [Feature::Apps, Feature::Plugins] { + if feature != disabled_feature { + features.enable(feature); + } + } + + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs_with_discoverable_tools( + &tools_config, + None, + None, + discoverable_tools.clone(), + &[], + ) + .build(); + + assert!( + !tools + .iter() + .any(|tool| tool_name(&tool.spec) == TOOL_SUGGEST_TOOL_NAME), + "tool_suggest should be absent when {disabled_feature:?} is disabled" + ); + } +} + #[test] fn search_tool_description_handles_no_enabled_apps() { let model_info = search_capable_model_info(); @@ -2138,6 +2185,7 @@ fn tool_suggest_description_lists_discoverable_tools() { let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); + features.enable(Feature::Plugins); features.enable(Feature::ToolSuggest); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { From f9de6792c48420f633c00ee7b7e72a70b99fee5c Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Mon, 23 Mar 2026 15:43:24 -0700 Subject: [PATCH 2/2] update --- codex-rs/core/src/codex.rs | 14 ++++++-------- codex-rs/core/src/connectors.rs | 13 +++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index cd6d087e46d..d7480944468 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3537,16 +3537,14 @@ impl Session { } } if turn_context.apps_enabled() { - let accessible_connectors = { - let mcp_connection_manager = self.services.mcp_connection_manager.read().await; - connectors::with_app_enabled_state( - connectors::accessible_connectors_from_mcp_tools( - &mcp_connection_manager.list_all_tools().await, - ), + let mcp_connection_manager = self.services.mcp_connection_manager.read().await; + let accessible_and_enabled_connectors = + connectors::list_accessible_and_enabled_connectors_from_manager( + &mcp_connection_manager, &turn_context.config, ) - }; - if let Some(apps_section) = render_apps_section(&accessible_connectors) { + .await; + if let Some(apps_section) = render_apps_section(&accessible_and_enabled_connectors) { developer_sections.push(apps_section); } } diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 600ba9c6f9b..fdb0f432229 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -103,6 +103,19 @@ pub async fn list_accessible_connectors_from_mcp_tools( ) } +pub(crate) async fn list_accessible_and_enabled_connectors_from_manager( + mcp_connection_manager: &McpConnectionManager, + config: &Config, +) -> Vec { + with_app_enabled_state( + accessible_connectors_from_mcp_tools(&mcp_connection_manager.list_all_tools().await), + config, + ) + .into_iter() + .filter(|connector| connector.is_accessible && connector.is_enabled) + .collect() +} + pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( config: &Config, auth: Option<&CodexAuth>,