Compare commits

...

1 Commits

Author SHA1 Message Date
Eric Ning
d6b4edb2a2 Allow OpenAI-prefixed app connectors via config 2026-05-08 11:58:33 -07:00
19 changed files with 228 additions and 40 deletions

View File

@@ -87,6 +87,7 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option<Vec<AppInfo>>
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)]

View File

@@ -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<ToolInfo>) -> Vec<ToolInfo> {
pub(crate) fn filter_disallowed_codex_apps_tools(
tools: Vec<ToolInfo>,
allow_openai_connector_ids: bool,
) -> Vec<ToolInfo> {
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()
}

View File

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

View File

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

View File

@@ -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<String>,
/// 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,

View File

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

View File

@@ -344,6 +344,7 @@ pub(crate) async fn list_tools_for_client_uncached(
client: &Arc<RmcpClient>,
timeout: Option<Duration>,
server_instructions: Option<&str>,
allow_openai_connector_ids: bool,
) -> Result<Vec<ToolInfo>> {
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)?;

View File

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

View File

@@ -7,6 +7,7 @@ pub fn filter_tool_suggest_discoverable_connectors(
accessible_connectors: &[AppInfo],
discoverable_connector_ids: &HashSet<String>,
originator_value: &str,
allow_openai_connector_ids: bool,
) -> Vec<AppInfo> {
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::<Vec<_>>();
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::<Vec<_>>();
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<AppInfo>,
originator_value: &str,
allow_openai_connector_ids: bool,
) -> Vec<AppInfo> {
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)
}

View File

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

View File

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

View File

@@ -710,6 +710,9 @@ pub struct Config {
/// Optional path override for the built-in apps MCP server.
pub apps_mcp_path_override: Option<String>,
/// 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 {

View File

@@ -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<AppInfo>,
config: &Config,
) -> Vec<AppInfo> {
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<AppInfo>, config: &Config) -> Vec<AppInfo> {
let user_apps_config = read_user_apps_config(config);
let requirements_apps_config = config.config_layer_stack.requirements_toml().apps.as_ref();

View File

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

View File

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

View File

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

View File

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

View File

@@ -244,6 +244,7 @@ fn new_config(model: Option<String>, 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,

View File

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