Add connector skills feature toggle

This commit is contained in:
William Yang
2026-06-19 11:01:45 +00:00
parent dac588f413
commit a9bd86f0c8
9 changed files with 174 additions and 1 deletions

View File

@@ -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"
},

View File

@@ -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()
}
}

View File

@@ -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());
}

View File

@@ -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)

View File

@@ -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 =

View File

@@ -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

View File

@@ -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(()));

View File

@@ -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",

View File

@@ -146,6 +146,18 @@ impl<M: Clone> PluginLoadOutcome<M> {
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<String, M> {
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(),
},
]
);
}
}