From a9bd86f0c8a718ab8c5eba71e9af38e20718271e Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 19 Jun 2026 11:01:45 +0000 Subject: [PATCH] Add connector skills feature toggle --- codex-rs/core/config.schema.json | 6 ++ codex-rs/core/src/plugins/mod.rs | 15 +++++ codex-rs/core/src/session/mod.rs | 6 +- codex-rs/core/src/session/session.rs | 2 + codex-rs/core/src/session/turn.rs | 4 ++ codex-rs/core/src/session/turn_context.rs | 2 + codex-rs/core/tests/suite/plugins.rs | 64 +++++++++++++++++++++ codex-rs/features/src/lib.rs | 8 +++ codex-rs/plugin/src/load_outcome.rs | 68 +++++++++++++++++++++++ 9 files changed, 174 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 7d21c90690..5342099523 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -470,6 +470,9 @@ "computer_use": { "type": "boolean" }, + "connector_skills": { + "type": "boolean" + }, "connectors": { "type": "boolean" }, @@ -4709,6 +4712,9 @@ "computer_use": { "type": "boolean" }, + "connector_skills": { + "type": "boolean" + }, "connectors": { "type": "boolean" }, diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 11e2c338bc..452efebbce 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -16,3 +16,18 @@ pub(crate) use mentions::build_skill_name_counts; pub(crate) use mentions::collect_explicit_app_ids; pub(crate) use mentions::collect_explicit_plugin_mentions; pub(crate) use mentions::collect_tool_mentions_from_messages; + +use crate::config::Config; +use codex_core_plugins::PluginLoadOutcome; +use codex_features::Feature; + +pub(crate) fn apply_connector_skills_feature( + config: &Config, + loaded_plugins: PluginLoadOutcome, +) -> PluginLoadOutcome { + if config.features.enabled(Feature::ConnectorSkills) { + loaded_plugins + } else { + loaded_plugins.without_app_backed_skills() + } +} diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 3c1f06f6b6..6c8d08705f 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -3127,6 +3127,10 @@ impl Session { .plugins_manager .plugins_for_config(&turn_context.config.plugins_config_input()) .await; + let model_visible_plugins = crate::plugins::apply_connector_skills_feature( + turn_context.config.as_ref(), + loaded_plugins.clone(), + ); let recommended_plugin_candidates = if crate::tools::spec_plan::tool_suggest_enabled(turn_context) { let auth = self.services.auth_manager.auth().await; @@ -3151,7 +3155,7 @@ impl Session { contextual_user_sections.push(recommended_plugins.render()); } if let Some(plugin_instructions) = - AvailablePluginsInstructions::from_plugins(loaded_plugins.capability_summaries()) + AvailablePluginsInstructions::from_plugins(model_visible_plugins.capability_summaries()) { developer_sections.push(plugin_instructions.render()); } diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 0c07f5e951..c60d2634fd 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -450,6 +450,8 @@ async fn warm_plugins_and_skills_for_session_init( let fs = turn_environments.primary_filesystem(); let plugins_input = config.plugins_config_input(); let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; + let plugin_outcome = + crate::plugins::apply_connector_skills_feature(config.as_ref(), plugin_outcome); let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); let plugin_skill_snapshots = plugins_manager.plugin_skill_snapshots_for_config(&plugins_input); let skills_input = skills_load_input_from_config(config.as_ref(), effective_skill_roots) diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 42fab7a506..945f8883d8 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -524,6 +524,10 @@ async fn build_skills_and_plugins( .plugins_manager .plugins_for_config(&turn_context.config.plugins_config_input()) .await; + let loaded_plugins = crate::plugins::apply_connector_skills_feature( + turn_context.config.as_ref(), + loaded_plugins, + ); // Structured plugin:// mentions are resolved from the current session's // enabled plugins, then converted into turn-scoped guidance below. let mentioned_plugins = diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 30d7d28e56..cd4c2b8e01 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -738,6 +738,8 @@ impl Session { .plugins_manager .plugins_for_config(&plugins_input) .await; + let plugin_outcome = + crate::plugins::apply_connector_skills_feature(&per_turn_config, plugin_outcome); let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); let plugin_skill_snapshots = self .services diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 198db86d88..9683d1dd64 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -232,6 +232,70 @@ async fn capability_sections_render_in_developer_message_in_order() -> Result<() Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn disabled_connector_skills_preserve_connector_apps() -> Result<()> { + skip_if_no_network!(Ok(())); + let server = start_mock_server().await; + let apps_server = AppsTestServer::mount_with_connector_name(&server, "Google Calendar").await?; + let mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + + let codex_home = Arc::new(TempDir::new()?); + write_plugin_skill_plugin(codex_home.as_ref()); + write_plugin_app_plugin(codex_home.as_ref()); + let chatgpt_base_url = apps_server.chatgpt_base_url; + let mut builder = test_codex() + .with_home(codex_home) + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(move |config| { + config + .features + .enable(Feature::Apps) + .expect("test config should allow feature update"); + config + .features + .disable(Feature::ConnectorSkills) + .expect("test config should allow feature update"); + config.chatgpt_base_url = chatgpt_base_url; + }); + let test_codex = builder + .build(&server) + .await + .expect("create new conversation"); + let codex = Arc::clone(&test_codex.codex); + wait_for_mcp_server(&codex, CODEX_APPS_MCP_SERVER_NAME).await?; + + codex + .submit(Op::UserInput { + items: vec![codex_protocol::user_input::UserInput::Mention { + name: "sample".into(), + path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: Default::default(), + }) + .await?; + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + let request = mock.single_request(); + let developer_text = request.message_input_texts("developer").join("\n\n"); + assert!(!developer_text.contains("sample:sample-search")); + assert!(!developer_text.contains("Skills from this plugin")); + assert!(developer_text.contains("Apps from this plugin")); + assert!( + request + .tool_by_name("mcp__codex_apps__google_calendar", "_create_event") + .is_some() + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 6dc8827fe0..31f0185d68 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -164,6 +164,8 @@ pub enum Feature { ToolSuggest, /// Enable plugins. Plugins, + /// Make skills bundled by app-backed connector plugins available to the model. + ConnectorSkills, /// Removed compatibility flag for plugin-bundled lifecycle hooks. PluginHooks, /// Allow the in-app browser pane in desktop apps. @@ -1086,6 +1088,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::ConnectorSkills, + key: "connector_skills", + stage: Stage::UnderDevelopment, + default_enabled: true, + }, FeatureSpec { id: Feature::PluginHooks, key: "plugin_hooks", diff --git a/codex-rs/plugin/src/load_outcome.rs b/codex-rs/plugin/src/load_outcome.rs index ad83655463..212bc10c1e 100644 --- a/codex-rs/plugin/src/load_outcome.rs +++ b/codex-rs/plugin/src/load_outcome.rs @@ -146,6 +146,18 @@ impl PluginLoadOutcome { skill_roots } + /// Returns a runtime view that preserves connector apps, MCP servers, and hooks while hiding + /// skills supplied by plugins with app declarations. + pub fn without_app_backed_skills(mut self) -> Self { + for plugin in &mut self.plugins { + if !plugin.apps.is_empty() { + plugin.skill_roots.clear(); + plugin.has_enabled_skills = false; + } + } + Self::from_plugins(self.plugins) + } + pub fn effective_mcp_servers(&self) -> HashMap { let mut mcp_servers = HashMap::new(); for plugin in self.plugins.iter().filter(|plugin| plugin.is_active()) { @@ -261,4 +273,60 @@ mod tests { }] ); } + + #[test] + fn without_app_backed_skills_preserves_other_plugin_capabilities() { + let connector_skill_root = test_path("connector-skills"); + let standalone_skill_root = test_path("standalone-skills"); + let mut connector = loaded_plugin("connector@test", vec![connector_skill_root]); + connector.apps = vec![AppDeclaration { + name: "calendar".to_string(), + connector_id: AppConnectorId("connector-calendar".to_string()), + category: None, + }]; + connector.mcp_servers.insert("calendar".to_string(), ()); + let standalone = loaded_plugin("standalone@test", vec![standalone_skill_root.clone()]); + + let outcome = PluginLoadOutcome::from_plugins(vec![connector, standalone]) + .without_app_backed_skills(); + + assert_eq!( + outcome.effective_plugin_skill_roots(), + vec![PluginSkillRoot { + path: standalone_skill_root, + plugin_id: "standalone@test".to_string(), + plugin_namespace: "standalone".to_string(), + plugin_root: test_path("standalone@test"), + }] + ); + assert_eq!( + outcome.effective_apps(), + vec![AppConnectorId("connector-calendar".to_string())] + ); + assert_eq!( + outcome.effective_mcp_servers(), + HashMap::from([("calendar".to_string(), ())]) + ); + assert_eq!( + outcome.capability_summaries(), + &[ + PluginCapabilitySummary { + config_name: "connector@test".to_string(), + display_name: "connector@test".to_string(), + description: None, + has_skills: false, + mcp_server_names: vec!["calendar".to_string()], + app_connector_ids: vec![AppConnectorId("connector-calendar".to_string())], + }, + PluginCapabilitySummary { + config_name: "standalone@test".to_string(), + display_name: "standalone@test".to_string(), + description: None, + has_skills: true, + mcp_server_names: Vec::new(), + app_connector_ids: Vec::new(), + }, + ] + ); + } }