mirror of
https://github.com/openai/codex.git
synced 2026-09-06 15:29:32 +00:00
Add connector skills feature toggle
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(()));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user