diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index cbeb4fd1b7..a1c9f9d4f4 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -87,6 +87,7 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option> Some(filter_disallowed_connectors( connectors, originator().value.as_str(), + /*allow_openai_connector_ids*/ false, )) } @@ -123,6 +124,7 @@ pub async fn list_all_connectors_with_options( Ok(filter_disallowed_connectors( connectors, originator().value.as_str(), + /*allow_openai_connector_ids*/ false, )) } @@ -158,10 +160,14 @@ pub fn connectors_for_plugin_apps( .iter() .map(|connector_id| connector_id.0.clone()), ); - filter_disallowed_connectors(connectors, originator().value.as_str()) - .into_iter() - .filter(|connector| plugin_app_ids.contains(connector.id.as_str())) - .collect() + filter_disallowed_connectors( + connectors, + originator().value.as_str(), + /*allow_openai_connector_ids*/ false, + ) + .into_iter() + .filter(|connector| plugin_app_ids.contains(connector.id.as_str())) + .collect() } pub fn merge_connectors_with_accessible( @@ -182,7 +188,11 @@ pub fn merge_connectors_with_accessible( accessible_connectors }; let merged = merge_connectors(connectors, accessible_connectors); - filter_disallowed_connectors(merged, originator().value.as_str()) + filter_disallowed_connectors( + merged, + originator().value.as_str(), + /*allow_openai_connector_ids*/ false, + ) } #[cfg(test)] diff --git a/codex-rs/codex-mcp/src/codex_apps.rs b/codex-rs/codex-mcp/src/codex_apps.rs index 81643e6665..c521593cf5 100644 --- a/codex-rs/codex-mcp/src/codex_apps.rs +++ b/codex-rs/codex-mcp/src/codex_apps.rs @@ -41,6 +41,7 @@ pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCac pub(crate) struct CodexAppsToolsCacheContext { pub(crate) codex_home: PathBuf, pub(crate) user_key: CodexAppsToolsCacheKey, + pub(crate) allow_openai_connector_ids: bool, } impl CodexAppsToolsCacheContext { @@ -197,7 +198,10 @@ pub(crate) fn load_cached_codex_apps_tools( if cache.schema_version != CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION { return CachedCodexAppsToolsLoad::Invalid; } - CachedCodexAppsToolsLoad::Hit(filter_disallowed_codex_apps_tools(cache.tools)) + CachedCodexAppsToolsLoad::Hit(filter_disallowed_codex_apps_tools( + cache.tools, + cache_context.allow_openai_connector_ids, + )) } pub(crate) fn write_cached_codex_apps_tools( @@ -210,7 +214,10 @@ pub(crate) fn write_cached_codex_apps_tools( { return; } - let tools = filter_disallowed_codex_apps_tools(tools.to_vec()); + let tools = filter_disallowed_codex_apps_tools( + tools.to_vec(), + cache_context.allow_openai_connector_ids, + ); let Ok(bytes) = serde_json::to_vec_pretty(&CodexAppsToolsDiskCache { schema_version: CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION, tools, @@ -220,13 +227,16 @@ pub(crate) fn write_cached_codex_apps_tools( let _ = std::fs::write(cache_path, bytes); } -pub(crate) fn filter_disallowed_codex_apps_tools(tools: Vec) -> Vec { +pub(crate) fn filter_disallowed_codex_apps_tools( + tools: Vec, + allow_openai_connector_ids: bool, +) -> Vec { tools .into_iter() .filter(|tool| { - tool.connector_id - .as_deref() - .is_none_or(is_connector_id_allowed) + tool.connector_id.as_deref().is_none_or(|connector_id| { + is_connector_id_allowed(connector_id, allow_openai_connector_ids) + }) }) .collect() } diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index e02b6094b3..ad4ce284ef 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -177,6 +177,7 @@ impl McpConnectionManager { runtime_environment: McpRuntimeEnvironment, codex_home: PathBuf, codex_apps_tools_cache_key: CodexAppsToolsCacheKey, + allow_openai_connector_ids: bool, host_owned_codex_apps_enabled: bool, tool_plugin_provenance: ToolPluginProvenance, auth: Option<&CodexAuth>, @@ -216,6 +217,7 @@ impl McpConnectionManager { Some(CodexAppsToolsCacheContext { codex_home: codex_home.clone(), user_key: codex_apps_tools_cache_key.clone(), + allow_openai_connector_ids, }) } else { None @@ -397,6 +399,10 @@ impl McpConnectionManager { &managed_client.client, managed_client.tool_timeout, managed_client.server_instructions.as_deref(), + managed_client + .codex_apps_tools_cache_context + .as_ref() + .is_some_and(|context| context.allow_openai_connector_ids), ) .await .with_context(|| { diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 4835bc5705..517ddc62c0 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::codex_apps::CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION; use crate::codex_apps::CodexAppsToolsCacheContext; +use crate::codex_apps::filter_disallowed_codex_apps_tools; use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot; use crate::codex_apps::read_cached_codex_apps_tools; use crate::codex_apps::write_cached_codex_apps_tools; @@ -83,6 +84,7 @@ fn create_codex_apps_tools_cache_context( chatgpt_user_id: chatgpt_user_id.map(ToOwned::to_owned), is_workspace_account: false, }, + allow_openai_connector_ids: false, } } @@ -611,6 +613,34 @@ fn codex_apps_tools_cache_filters_disallowed_connectors() { assert_eq!(cached[0].connector_id.as_deref(), Some("calendar")); } +#[test] +fn codex_apps_tool_filter_can_allow_openai_prefixed_connectors() { + let tools = vec![ + create_test_tool_with_connector( + CODEX_APPS_MCP_SERVER_NAME, + "openai_tool", + "connector_openai_hidden", + Some("Hidden"), + ), + create_test_tool_with_connector( + CODEX_APPS_MCP_SERVER_NAME, + "blocked_tool", + "asdk_app_6938a94a61d881918ef32cb999ff937c", + Some("Blocked"), + ), + ]; + + let filtered = + filter_disallowed_codex_apps_tools(tools, /*allow_openai_connector_ids*/ true); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].callable_name, "openai_tool"); + assert_eq!( + filtered[0].connector_id.as_deref(), + Some("connector_openai_hidden") + ); +} + #[test] fn codex_apps_tools_cache_is_ignored_when_schema_version_mismatches() { let codex_home = tempdir().expect("tempdir"); diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 71f79b9b4a..5868fa3e41 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -108,6 +108,9 @@ pub struct McpConfig { pub chatgpt_base_url: String, /// Optional path override for the built-in apps MCP server. pub apps_mcp_path_override: Option, + /// Whether Codex should allow app connector IDs with the `connector_openai_` + /// prefix when filtering the built-in apps MCP server. + pub apps_allow_openai_connector_ids: bool, /// Codex home directory used for MCP OAuth state and app-tool cache files. pub codex_home: PathBuf, /// Preferred credential store for MCP OAuth tokens. @@ -280,6 +283,7 @@ pub async fn read_mcp_resource( runtime_environment, config.codex_home.clone(), codex_apps_tools_cache_key(auth), + config.apps_allow_openai_connector_ids, host_owned_codex_apps_enabled, tool_plugin_provenance(config), auth, @@ -348,6 +352,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( runtime_environment, config.codex_home.clone(), codex_apps_tools_cache_key(auth), + config.apps_allow_openai_connector_ids, host_owned_codex_apps_enabled, tool_plugin_provenance, auth, diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 491341c3e9..1efa6cc6bb 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -19,6 +19,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig { McpConfig { chatgpt_base_url: "https://chatgpt.com".to_string(), apps_mcp_path_override: None, + apps_allow_openai_connector_ids: false, codex_home, mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode::default(), mcp_oauth_callback_port: None, diff --git a/codex-rs/codex-mcp/src/rmcp_client.rs b/codex-rs/codex-mcp/src/rmcp_client.rs index c9a8ca8c33..63c676a841 100644 --- a/codex-rs/codex-mcp/src/rmcp_client.rs +++ b/codex-rs/codex-mcp/src/rmcp_client.rs @@ -344,6 +344,7 @@ pub(crate) async fn list_tools_for_client_uncached( client: &Arc, timeout: Option, server_instructions: Option<&str>, + allow_openai_connector_ids: bool, ) -> Result> { let resp = client .list_tools_with_connector_ids(/*params*/ None, timeout) @@ -397,7 +398,10 @@ pub(crate) async fn list_tools_for_client_uncached( }) .collect(); if server_name == CODEX_APPS_MCP_SERVER_NAME { - return Ok(filter_disallowed_codex_apps_tools(tools)); + return Ok(filter_disallowed_codex_apps_tools( + tools, + allow_openai_connector_ids, + )); } Ok(tools) } @@ -478,6 +482,9 @@ async fn start_server_task( elicitation_requests, codex_apps_tools_cache_context, } = params; + let allow_openai_connector_ids = codex_apps_tools_cache_context + .as_ref() + .is_some_and(|context| context.allow_openai_connector_ids); let elicitation = elicitation_capability_for_server(&server_name); let params = InitializeRequestParams { meta: None, @@ -520,6 +527,7 @@ async fn start_server_task( &client, startup_timeout, initialize_result.instructions.as_deref(), + allow_openai_connector_ids, ) .await .map_err(StartupOutcomeError::from)?; diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 39fd0a442f..1b894240d1 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -443,6 +443,10 @@ pub struct AppConfig { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct AppsConfigToml { + /// Allows app connector IDs with the internal `connector_openai_` prefix. + #[serde(default)] + pub allow_openai_connector_ids: bool, + /// Default settings for all apps. #[serde(default, rename = "_default", skip_serializing_if = "Option::is_none")] pub default: Option, diff --git a/codex-rs/connectors/src/filter.rs b/codex-rs/connectors/src/filter.rs index 82c334f82d..c92fa87f1a 100644 --- a/codex-rs/connectors/src/filter.rs +++ b/codex-rs/connectors/src/filter.rs @@ -7,6 +7,7 @@ pub fn filter_tool_suggest_discoverable_connectors( accessible_connectors: &[AppInfo], discoverable_connector_ids: &HashSet, originator_value: &str, + allow_openai_connector_ids: bool, ) -> Vec { let accessible_connector_ids: HashSet<&str> = accessible_connectors .iter() @@ -14,11 +15,15 @@ pub fn filter_tool_suggest_discoverable_connectors( .map(|connector| connector.id.as_str()) .collect(); - let mut connectors = filter_disallowed_connectors(directory_connectors, originator_value) - .into_iter() - .filter(|connector| !accessible_connector_ids.contains(connector.id.as_str())) - .filter(|connector| discoverable_connector_ids.contains(connector.id.as_str())) - .collect::>(); + let mut connectors = filter_disallowed_connectors( + directory_connectors, + originator_value, + allow_openai_connector_ids, + ) + .into_iter() + .filter(|connector| !accessible_connector_ids.contains(connector.id.as_str())) + .filter(|connector| discoverable_connector_ids.contains(connector.id.as_str())) + .collect::>(); connectors.sort_by(|left, right| { left.name .cmp(&right.name) @@ -42,12 +47,17 @@ const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_"; pub fn filter_disallowed_connectors( connectors: Vec, originator_value: &str, + allow_openai_connector_ids: bool, ) -> Vec { let first_party_chat_originator = is_first_party_chat_originator(originator_value); connectors .into_iter() .filter(|connector| { - is_connector_id_allowed(connector.id.as_str(), first_party_chat_originator) + is_connector_id_allowed( + connector.id.as_str(), + first_party_chat_originator, + allow_openai_connector_ids, + ) }) .collect() } @@ -56,13 +66,17 @@ fn is_first_party_chat_originator(originator_value: &str) -> bool { originator_value == "codex_atlas" || originator_value == "codex_chatgpt_desktop" } -fn is_connector_id_allowed(connector_id: &str, first_party_chat_originator: bool) -> bool { +fn is_connector_id_allowed( + connector_id: &str, + first_party_chat_originator: bool, + allow_openai_connector_ids: bool, +) -> bool { let disallowed_connector_ids = if first_party_chat_originator { FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS } else { DISALLOWED_CONNECTOR_IDS }; - !connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX) + (allow_openai_connector_ids || !connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX)) && !disallowed_connector_ids.contains(&connector_id) } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index edbf45694f..90e331ec64 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -194,6 +194,11 @@ } ], "description": "Default settings for all apps." + }, + "allow_openai_connector_ids": { + "default": false, + "description": "Allows app connector IDs with the internal `connector_openai_` prefix.", + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 9bda54a9a7..c7392476aa 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4237,20 +4237,24 @@ async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<( let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); config.apps_mcp_path_override = Some("/custom/mcp".to_string()); + config.apps_allow_openai_connector_ids = true; let mcp_config = config.to_mcp_config(&plugins_manager).await; assert!(mcp_config.apps_enabled); assert_eq!( mcp_config.apps_mcp_path_override.as_deref(), Some("/custom/mcp") ); + assert!(mcp_config.apps_allow_openai_connector_ids); let _ = config.features.disable(Feature::Apps); let mcp_config = config.to_mcp_config(&plugins_manager).await; assert!(!mcp_config.apps_enabled); + assert!(mcp_config.apps_allow_openai_connector_ids); let _ = config.features.enable(Feature::Apps); let mcp_config = config.to_mcp_config(&plugins_manager).await; assert!(mcp_config.apps_enabled); + assert!(mcp_config.apps_allow_openai_connector_ids); Ok(()) } @@ -7032,6 +7036,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), apps_mcp_path_override: None, + apps_allow_openai_connector_ids: false, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -7452,6 +7457,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), apps_mcp_path_override: None, + apps_allow_openai_connector_ids: false, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -7610,6 +7616,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), apps_mcp_path_override: None, + apps_allow_openai_connector_ids: false, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -7753,6 +7760,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), apps_mcp_path_override: None, + apps_allow_openai_connector_ids: false, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -8422,6 +8430,29 @@ path = "/custom/mcp" Ok(()) } +#[tokio::test] +async fn config_loads_apps_allow_openai_connector_ids_from_toml() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let toml = r#" +model = "gpt-5.4" + +[apps] +allow_openai_connector_ids = true +"#; + let cfg: ConfigToml = + toml::from_str(toml).expect("TOML deserialization should succeed for apps config"); + + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + + assert!(config.apps_allow_openai_connector_ids); + Ok(()) +} + #[tokio::test] async fn config_loads_mcp_oauth_callback_url_from_toml() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index b1277eda48..cff5768205 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -710,6 +710,9 @@ pub struct Config { /// Optional path override for the built-in apps MCP server. pub apps_mcp_path_override: Option, + /// Whether app connector IDs with the internal `connector_openai_` prefix are allowed. + pub apps_allow_openai_connector_ids: bool, + /// Machine-local realtime audio device preferences used by realtime voice. pub realtime_audio: RealtimeAudioConfig, @@ -1122,6 +1125,7 @@ impl Config { McpConfig { chatgpt_base_url: self.chatgpt_base_url.clone(), apps_mcp_path_override: self.apps_mcp_path_override.clone(), + apps_allow_openai_connector_ids: self.apps_allow_openai_connector_ids, codex_home: self.codex_home.to_path_buf(), mcp_oauth_credentials_store_mode: self.mcp_oauth_credentials_store_mode, mcp_oauth_callback_port: self.mcp_oauth_callback_port, @@ -2188,6 +2192,10 @@ impl Config { None => ConfigProfile::default(), }; let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack); + let apps_allow_openai_connector_ids = cfg + .apps + .as_ref() + .is_some_and(|apps| apps.allow_openai_connector_ids); let feature_overrides = FeatureOverrides { include_apply_patch_tool: include_apply_patch_tool_override, web_search_request: override_tools_web_search_request, @@ -3095,6 +3103,7 @@ impl Config { .or(cfg.chatgpt_base_url) .unwrap_or("https://chatgpt.com/backend-api/".to_string()), apps_mcp_path_override, + apps_allow_openai_connector_ids, realtime_audio: cfg .audio .map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig { diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 4da588edb6..52a3ba2d52 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -129,6 +129,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( accessible_connectors, &connector_ids, originator().value.as_str(), + config.apps_allow_openai_connector_ids, ) .into_iter() .map(DiscoverableTool::from); @@ -154,12 +155,8 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools( return Some(Vec::new()); } let cache_key = accessible_connectors_cache_key(config, auth.as_ref()); - read_cached_accessible_connectors(&cache_key).map(|connectors| { - codex_connectors::filter::filter_disallowed_connectors( - connectors, - originator().value.as_str(), - ) - }) + read_cached_accessible_connectors(&cache_key) + .map(|connectors| filter_disallowed_connectors_for_config(connectors, config)) } pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools( @@ -172,9 +169,9 @@ pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools( } let cache_key = accessible_connectors_cache_key(config, auth); - let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors( + let accessible_connectors = filter_disallowed_connectors_for_config( accessible_connectors_from_mcp_tools(mcp_tools), - originator().value.as_str(), + config, ); write_cached_accessible_connectors(cache_key, &accessible_connectors); } @@ -234,10 +231,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config).await; if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key) { - let cached_connectors = codex_connectors::filter::filter_disallowed_connectors( - cached_connectors, - originator().value.as_str(), - ); + let cached_connectors = filter_disallowed_connectors_for_config(cached_connectors, config); let cached_connectors = with_app_plugin_sources(cached_connectors, &tool_plugin_provenance); return Ok(AccessibleConnectorsStatus { connectors: cached_connectors, @@ -280,6 +274,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()), config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), + config.apps_allow_openai_connector_ids, host_owned_codex_apps_enabled, ToolPluginProvenance::default(), auth.as_ref(), @@ -342,9 +337,9 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( cancel_token.cancel(); } - let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors( + let accessible_connectors = filter_disallowed_connectors_for_config( accessible_connectors_from_mcp_tools(&tools), - originator().value.as_str(), + config, ); if codex_apps_ready || !accessible_connectors.is_empty() { write_cached_accessible_connectors(cache_key, &accessible_connectors); @@ -535,6 +530,17 @@ pub(crate) fn accessible_connectors_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Ve codex_connectors::accessible::collect_accessible_connectors(tools) } +fn filter_disallowed_connectors_for_config( + connectors: Vec, + config: &Config, +) -> Vec { + codex_connectors::filter::filter_disallowed_connectors( + connectors, + originator().value.as_str(), + config.apps_allow_openai_connector_ids, + ) +} + pub fn with_app_enabled_state(mut connectors: Vec, config: &Config) -> Vec { let user_apps_config = read_user_apps_config(config); let requirements_apps_config = config.config_layer_stack.requirements_toml().apps.as_ref(); diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 014ab1cad8..4ec10800bc 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -344,6 +344,7 @@ fn accessible_connectors_from_mcp_tools_preserves_description() { #[test] fn app_tool_policy_uses_global_defaults_for_destructive_hints() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: Some(AppsDefaultConfig { enabled: true, destructive_enabled: false, @@ -372,6 +373,7 @@ fn app_tool_policy_uses_global_defaults_for_destructive_hints() { #[test] fn app_tool_policy_defaults_missing_destructive_hint_to_true() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: Some(AppsDefaultConfig { enabled: true, destructive_enabled: false, @@ -400,6 +402,7 @@ fn app_tool_policy_defaults_missing_destructive_hint_to_true() { #[test] fn app_tool_policy_defaults_missing_open_world_hint_to_true() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: Some(AppsDefaultConfig { enabled: true, destructive_enabled: true, @@ -428,6 +431,7 @@ fn app_tool_policy_defaults_missing_open_world_hint_to_true() { #[test] fn app_is_enabled_uses_default_for_unconfigured_apps() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: Some(AppsDefaultConfig { enabled: false, destructive_enabled: true, @@ -443,6 +447,7 @@ fn app_is_enabled_uses_default_for_unconfigured_apps() { #[test] fn app_is_enabled_prefers_per_app_override_over_default() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: Some(AppsDefaultConfig { enabled: false, destructive_enabled: true, @@ -468,6 +473,7 @@ fn app_is_enabled_prefers_per_app_override_over_default() { #[test] fn requirements_disabled_connector_overrides_enabled_connector() { let mut effective_apps = AppsConfigToml { + allow_openai_connector_ids: false, default: None, apps: HashMap::from([( "connector_123123".to_string(), @@ -500,6 +506,7 @@ fn requirements_disabled_connector_overrides_enabled_connector() { #[test] fn requirements_enabled_does_not_override_disabled_connector() { let mut effective_apps = AppsConfigToml { + allow_openai_connector_ids: false, default: None, apps: HashMap::from([( "connector_123123".to_string(), @@ -756,6 +763,7 @@ async fn with_app_enabled_state_preserves_unrelated_disabled_connector() { #[test] fn app_tool_policy_honors_default_app_enabled_false() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: Some(AppsDefaultConfig { enabled: false, destructive_enabled: true, @@ -786,6 +794,7 @@ fn app_tool_policy_honors_default_app_enabled_false() { #[test] fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: Some(AppsDefaultConfig { enabled: false, destructive_enabled: true, @@ -826,6 +835,7 @@ fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() { #[test] fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: None, apps: HashMap::from([( "calendar".to_string(), @@ -868,6 +878,7 @@ fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() { #[test] fn app_tool_policy_default_tools_enabled_true_overrides_app_level_tool_hints() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: None, apps: HashMap::from([( "calendar".to_string(), @@ -902,6 +913,7 @@ fn app_tool_policy_default_tools_enabled_true_overrides_app_level_tool_hints() { #[test] fn app_tool_policy_default_tools_enabled_false_overrides_app_level_tool_hints() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: None, apps: HashMap::from([( "calendar".to_string(), @@ -938,6 +950,7 @@ fn app_tool_policy_default_tools_enabled_false_overrides_app_level_tool_hints() #[test] fn app_tool_policy_uses_default_tools_approval_mode() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: None, apps: HashMap::from([( "calendar".to_string(), @@ -976,6 +989,7 @@ fn app_tool_policy_uses_default_tools_approval_mode() { #[test] fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() { let apps_config = AppsConfigToml { + allow_openai_connector_ids: false, default: None, apps: HashMap::from([( "calendar".to_string(), @@ -1017,8 +1031,11 @@ fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() { #[test] fn filter_disallowed_connectors_allows_non_disallowed_connectors() { - let filtered = - filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")], "codex_cli"); + let filtered = filter_disallowed_connectors( + vec![app("asdk_app_hidden"), app("alpha")], + "codex_cli", + /*allow_openai_connector_ids*/ false, + ); assert_eq!(filtered, vec![app("asdk_app_hidden"), app("alpha")]); } @@ -1031,10 +1048,25 @@ fn filter_disallowed_connectors_filters_openai_prefix() { app("gamma"), ], "codex_cli", + /*allow_openai_connector_ids*/ false, ); assert_eq!(filtered, vec![app("gamma")]); } +#[test] +fn filter_disallowed_connectors_can_allow_openai_prefix() { + let filtered = filter_disallowed_connectors( + vec![ + app("connector_openai_foo"), + app("asdk_app_6938a94a61d881918ef32cb999ff937c"), + app("gamma"), + ], + "codex_cli", + /*allow_openai_connector_ids*/ true, + ); + assert_eq!(filtered, vec![app("connector_openai_foo"), app("gamma")]); +} + #[test] fn filter_disallowed_connectors_filters_disallowed_connector_ids() { let filtered = filter_disallowed_connectors( @@ -1044,6 +1076,7 @@ fn filter_disallowed_connectors_filters_disallowed_connector_ids() { app("delta"), ], "codex_cli", + /*allow_openai_connector_ids*/ false, ); assert_eq!(filtered, vec![app("delta")]); } @@ -1057,6 +1090,7 @@ fn first_party_chat_originator_filters_target_and_openai_prefixed_connectors() { app("connector_0f9c9d4592e54d0a9a12b3f44a1e2010"), ], "codex_atlas", + /*allow_openai_connector_ids*/ false, ); assert_eq!( filtered, @@ -1143,6 +1177,7 @@ fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstal "connector_68df038e0ba48191908c8434991bbac2".to_string(), ]), "codex_cli", + /*allow_openai_connector_ids*/ false, ); assert_eq!( @@ -1183,6 +1218,7 @@ fn filter_tool_suggest_discoverable_connectors_excludes_accessible_apps_even_whe "connector_68df038e0ba48191908c8434991bbac2".to_string(), ]), "codex_cli", + /*allow_openai_connector_ids*/ false, ); assert_eq!(filtered, Vec::::new()); diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index a556e228ac..f6f459f8c1 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1121,6 +1121,7 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: codex_mcp::McpRuntimeEnvironment::new(environment, turn_context.cwd.to_path_buf()), turn_context.config.codex_home.to_path_buf(), codex_mcp::codex_apps_tools_cache_key(auth.as_ref()), + turn_context.config.apps_allow_openai_connector_ids, /*host_owned_codex_apps_enabled*/ true, codex_mcp::ToolPluginProvenance::default(), auth.as_ref(), @@ -1778,6 +1779,7 @@ async fn persist_codex_app_tool_approval_writes_tool_override() { assert_eq!( parsed.apps, Some(AppsConfigToml { + allow_openai_connector_ids: false, default: None, apps: HashMap::from([( "calendar".to_string(), diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index a7d7a965a2..5ccbb67dfd 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -328,6 +328,7 @@ impl Session { mcp_runtime_environment, config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), + config.apps_allow_openai_connector_ids, host_owned_codex_apps_enabled, tool_plugin_provenance, auth.as_ref(), diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index f72a173c80..94bc300d27 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -988,6 +988,7 @@ impl Session { mcp_runtime_environment, config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth), + config.apps_allow_openai_connector_ids, host_owned_codex_apps_enabled, tool_plugin_provenance, auth, diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 6817f677e6..69ab3c738d 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -244,6 +244,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R model_verbosity: None, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), apps_mcp_path_override: None, + apps_allow_openai_connector_ids: false, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_ws_base_url: None, experimental_realtime_ws_model: None, diff --git a/codex-rs/utils/plugins/src/mcp_connector.rs b/codex-rs/utils/plugins/src/mcp_connector.rs index 40fb0d4bbf..f439af134b 100644 --- a/codex-rs/utils/plugins/src/mcp_connector.rs +++ b/codex-rs/utils/plugins/src/mcp_connector.rs @@ -13,18 +13,26 @@ const FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS: &[&str] = &["connector_0f9c9d4592e54d0a9a12b3f44a1e2010"]; const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_"; -pub fn is_connector_id_allowed(connector_id: &str) -> bool { - is_connector_id_allowed_for_originator(connector_id, originator().value.as_str()) +pub fn is_connector_id_allowed(connector_id: &str, allow_openai_connector_ids: bool) -> bool { + is_connector_id_allowed_for_originator( + connector_id, + originator().value.as_str(), + allow_openai_connector_ids, + ) } -fn is_connector_id_allowed_for_originator(connector_id: &str, originator_value: &str) -> bool { +fn is_connector_id_allowed_for_originator( + connector_id: &str, + originator_value: &str, + allow_openai_connector_ids: bool, +) -> bool { let disallowed_connector_ids = if is_first_party_chat_originator(originator_value) { FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS } else { DISALLOWED_CONNECTOR_IDS }; - !connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX) + (allow_openai_connector_ids || !connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX)) && !disallowed_connector_ids.contains(&connector_id) }