[apps] Fix apps enablement condition. (#14011)

- [x] Fix apps enablement condition to check both the feature flag and
that the user is not an API key user.
This commit is contained in:
Matthew Zeng
2026-03-09 22:25:43 -07:00
committed by GitHub
parent a9ae43621b
commit 566e4cee4b
18 changed files with 662 additions and 86 deletions
+4
View File
@@ -212,6 +212,10 @@ impl CodexAuth {
}
}
pub fn is_api_key_auth(&self) -> bool {
self.auth_mode() == AuthMode::ApiKey
}
pub fn is_chatgpt_auth(&self) -> bool {
self.auth_mode() == AuthMode::Chatgpt
}
+29 -25
View File
@@ -736,6 +736,11 @@ impl TurnContext {
})
}
pub(crate) fn apps_enabled(&self) -> bool {
self.features
.apps_enabled_cached(self.auth_manager.as_deref())
}
pub(crate) async fn with_model(&self, model: String, models_manager: &ModelsManager) -> Self {
let mut config = (*self.config).clone();
config.model = Some(model.clone());
@@ -3407,7 +3412,7 @@ impl Session {
);
}
}
if turn_context.features.enabled(Feature::Apps) {
if turn_context.apps_enabled() {
developer_sections.push(render_apps_section());
}
if turn_context.features.enabled(Feature::CodexGitCommit)
@@ -3894,7 +3899,7 @@ impl Session {
.tool_plugin_provenance(config.as_ref());
let mcp_servers = with_codex_apps_mcp(
mcp_servers,
self.features.enabled(Feature::Apps),
self.features.apps_enabled_for_auth(auth.as_ref()),
auth.as_ref(),
config.as_ref(),
);
@@ -5357,28 +5362,27 @@ pub(crate) async fn run_turn(
// enabled plugins, then converted into turn-scoped guidance below.
let mentioned_plugins =
collect_explicit_plugin_mentions(&input, loaded_plugins.capability_summaries());
let mcp_tools =
if turn_context.config.features.enabled(Feature::Apps) || !mentioned_plugins.is_empty() {
// Plugin mentions need raw MCP/app inventory even when app tools
// are normally hidden so we can describe the plugin's currently
// usable capabilities for this turn.
match sess
.services
.mcp_connection_manager
.read()
.await
.list_all_tools()
.or_cancel(&cancellation_token)
.await
{
Ok(mcp_tools) => mcp_tools,
Err(_) if turn_context.config.features.enabled(Feature::Apps) => return None,
Err(_) => HashMap::new(),
}
} else {
HashMap::new()
};
let available_connectors = if turn_context.config.features.enabled(Feature::Apps) {
let mcp_tools = if turn_context.apps_enabled() || !mentioned_plugins.is_empty() {
// Plugin mentions need raw MCP/app inventory even when app tools
// are normally hidden so we can describe the plugin's currently
// usable capabilities for this turn.
match sess
.services
.mcp_connection_manager
.read()
.await
.list_all_tools()
.or_cancel(&cancellation_token)
.await
{
Ok(mcp_tools) => mcp_tools,
Err(_) if turn_context.apps_enabled() => return None,
Err(_) => HashMap::new(),
}
} else {
HashMap::new()
};
let available_connectors = if turn_context.apps_enabled() {
let connectors = connectors::merge_plugin_apps_with_accessible(
loaded_plugins.effective_apps(),
connectors::accessible_connectors_from_mcp_tools(&mcp_tools),
@@ -6234,7 +6238,7 @@ async fn built_tools(
let mut effective_explicitly_enabled_connectors = explicitly_enabled_connectors.clone();
effective_explicitly_enabled_connectors.extend(sess.get_connector_selection().await);
let connectors = if turn_context.features.enabled(Feature::Apps) {
let connectors = if turn_context.apps_enabled() {
let connectors = connectors::merge_plugin_apps_with_accessible(
loaded_plugins.effective_apps(),
connectors::accessible_connectors_from_mcp_tools(&mcp_tools),
+6 -8
View File
@@ -93,12 +93,11 @@ pub async fn list_accessible_connectors_from_mcp_tools(
pub async fn list_cached_accessible_connectors_from_mcp_tools(
config: &Config,
) -> Option<Vec<AppInfo>> {
if !config.features.enabled(Feature::Apps) {
return Some(Vec::new());
}
let auth_manager = auth_manager_from_config(config);
let auth = auth_manager.auth().await;
if !config.features.apps_enabled_for_auth(auth.as_ref()) {
return Some(Vec::new());
}
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
read_cached_accessible_connectors(&cache_key).map(filter_disallowed_connectors)
}
@@ -118,15 +117,14 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
config: &Config,
force_refetch: bool,
) -> anyhow::Result<AccessibleConnectorsStatus> {
if !config.features.enabled(Feature::Apps) {
let auth_manager = auth_manager_from_config(config);
let auth = auth_manager.auth().await;
if !config.features.apps_enabled_for_auth(auth.as_ref()) {
return Ok(AccessibleConnectorsStatus {
connectors: Vec::new(),
codex_apps_ready: true,
});
}
let auth_manager = auth_manager_from_config(config);
let auth = auth_manager.auth().await;
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config);
+38
View File
@@ -5,6 +5,8 @@
//! booleans through multiple types, call sites consult a single `Features`
//! container attached to `Config`.
use crate::auth::AuthManager;
use crate::auth::CodexAuth;
use crate::config::Config;
use crate::config::ConfigToml;
use crate::config::profile::ConfigProfile;
@@ -257,6 +259,27 @@ impl Features {
self.enabled.contains(&f)
}
pub async fn apps_enabled(&self, auth_manager: Option<&AuthManager>) -> bool {
if !self.enabled(Feature::Apps) {
return false;
}
let auth = match auth_manager {
Some(auth_manager) => auth_manager.auth().await,
None => None,
};
self.apps_enabled_for_auth(auth.as_ref())
}
pub fn apps_enabled_cached(&self, auth_manager: Option<&AuthManager>) -> bool {
let auth = auth_manager.and_then(AuthManager::auth_cached);
self.apps_enabled_for_auth(auth.as_ref())
}
pub(crate) fn apps_enabled_for_auth(&self, auth: Option<&CodexAuth>) -> bool {
self.enabled(Feature::Apps) && auth.is_some_and(CodexAuth::is_chatgpt_auth)
}
pub fn enable(&mut self, f: Feature) -> &mut Self {
self.enabled.insert(f);
self
@@ -973,4 +996,19 @@ mod tests {
assert_eq!(feature_for_key("multi_agent"), Some(Feature::Collab));
assert_eq!(feature_for_key("collab"), Some(Feature::Collab));
}
#[test]
fn apps_require_feature_flag_and_chatgpt_auth() {
let mut features = Features::with_defaults();
assert!(!features.apps_enabled_for_auth(None));
features.enable(Feature::Apps);
assert!(!features.apps_enabled_for_auth(None));
let api_key_auth = CodexAuth::from_api_key("test-api-key");
assert!(!features.apps_enabled_for_auth(Some(&api_key_auth)));
let chatgpt_auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
assert!(features.apps_enabled_for_auth(Some(&chatgpt_auth)));
}
}
+1 -1
View File
@@ -268,7 +268,7 @@ fn effective_mcp_servers(
let servers = configured_mcp_servers(config, plugins_manager);
with_codex_apps_mcp(
servers,
config.features.enabled(Feature::Apps),
config.features.apps_enabled_for_auth(auth),
auth,
config,
)
+77 -5
View File
@@ -1294,8 +1294,13 @@ fn create_search_tool_bm25_tool(app_tools: &HashMap<String, ToolInfo>) -> ToolSp
app_names.dedup();
let app_names = app_names.join(", ");
let description =
SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE.replace("{{app_names}}", app_names.as_str());
let description = if app_names.is_empty() {
SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE
.replace("({{app_names}})", "(None currently enabled)")
.replace("{{app_names}}", "available apps")
} else {
SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE.replace("{{app_names}}", app_names.as_str())
};
ToolSpec::Function(ResponsesApiTool {
name: SEARCH_TOOL_BM25_TOOL_NAME.to_string(),
@@ -1996,9 +2001,8 @@ pub(crate) fn build_specs(
builder.register_handler("request_permissions", request_permissions_handler);
}
if config.search_tool
&& let Some(app_tools) = app_tools
{
if config.search_tool {
let app_tools = app_tools.unwrap_or_default();
builder.push_spec_with_parallel_support(create_search_tool_bm25_tool(&app_tools), true);
builder.register_handler(SEARCH_TOOL_BM25_TOOL_NAME, search_tool_handler);
}
@@ -3393,6 +3397,74 @@ mod tests {
assert!(!description.contains("mcp__rmcp__echo"));
}
#[test]
fn search_tool_requires_apps_feature_flag_only() {
let config = test_config();
let model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let app_tools = Some(HashMap::from([(
"mcp__codex_apps__calendar_create_event".to_string(),
ToolInfo {
server_name: crate::mcp::CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "calendar_create_event".to_string(),
tool: mcp_tool(
"calendar_create_event",
"Create calendar event",
serde_json::json!({"type": "object"}),
),
connector_id: Some("calendar".to_string()),
connector_name: Some("Calendar".to_string()),
plugin_display_names: Vec::new(),
},
)]));
let features = Features::with_defaults();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
});
let (tools, _) = build_specs(&tools_config, None, app_tools.clone(), &[]).build();
assert_lacks_tool_name(&tools, SEARCH_TOOL_BM25_TOOL_NAME);
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
});
let (tools, _) = build_specs(&tools_config, None, app_tools, &[]).build();
assert_contains_tool_names(&tools, &[SEARCH_TOOL_BM25_TOOL_NAME]);
}
#[test]
fn search_tool_description_handles_no_enabled_apps() {
let config = test_config();
let model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
});
let (tools, _) = build_specs(&tools_config, None, Some(HashMap::new()), &[]).build();
let search_tool = find_tool(&tools, SEARCH_TOOL_BM25_TOOL_NAME);
let ToolSpec::Function(ResponsesApiTool { description, .. }) = &search_tool.spec else {
panic!("expected function tool");
};
assert!(description.contains("(None currently enabled)"));
assert!(description.contains("available apps."));
assert!(!description.contains("{{app_names}}"));
}
#[test]
fn test_mcp_tool_property_missing_type_defaults_to_string() {
let config = test_config();