Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4855,7 +4855,7 @@ impl CodexMessageProcessor {
.set_enabled(Feature::Apps, thread.enabled(Feature::Apps));
}

if !config.features.enabled(Feature::Apps) {
if !config.features.apps_enabled(Some(&self.auth_manager)).await {
self.outgoing
.send_response(
request_id,
Expand Down Expand Up @@ -5418,7 +5418,7 @@ impl CodexMessageProcessor {
};
let plugin_apps = load_plugin_apps(result.installed_path.as_path());
let apps_needing_auth = if plugin_apps.is_empty()
|| !config.features.enabled(Feature::Apps)
|| !config.features.apps_enabled(Some(&self.auth_manager)).await
{
Vec::new()
} else {
Expand Down
64 changes: 64 additions & 0 deletions codex-rs/app-server/tests/suite/v2/app_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ use codex_app_server_protocol::AppReview;
use codex_app_server_protocol::AppScreenshot;
use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::AppsListResponse;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::AuthDotJson;
use codex_core::auth::save_auth;
use pretty_assertions::assert_eq;
use rmcp::handler::server::ServerHandler;
use rmcp::model::JsonObject;
Expand Down Expand Up @@ -82,6 +85,67 @@ async fn list_apps_returns_empty_when_connectors_disabled() -> Result<()> {
Ok(())
}

#[tokio::test]
async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
let connectors = vec![AppInfo {
id: "beta".to_string(),
name: "Beta".to_string(),
description: Some("Beta connector".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
}];
let tools = vec![connector_tool("beta", "Beta App")?];
let (server_url, server_handle) =
start_apps_server_with_delays(connectors, tools, Duration::ZERO, Duration::ZERO).await?;

let codex_home = TempDir::new()?;
write_connectors_config(codex_home.path(), &server_url)?;
save_auth(
codex_home.path(),
&AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some("test-api-key".to_string()),
tokens: None,
last_refresh: None,
},
AuthCredentialsStoreMode::File,
)?;

let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

let request_id = mcp
.send_apps_list_request(AppsListParams {
limit: Some(50),
cursor: None,
thread_id: None,
force_refetch: false,
})
.await?;

let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;

let AppsListResponse { data, next_cursor } = to_response(response)?;
assert!(data.is_empty());
assert!(next_cursor.is_none());

server_handle.abort();
let _ = server_handle.await;
Ok(())
}

#[tokio::test]
async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Result<()> {
let connectors = vec![AppInfo {
Expand Down
17 changes: 13 additions & 4 deletions codex-rs/chatgpt/src/connectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::collections::HashSet;
use std::sync::LazyLock;
use std::sync::Mutex as StdMutex;

use codex_core::AuthManager;
use codex_core::config::Config;
use codex_core::features::Feature;
use codex_core::token_data::TokenData;
use serde::Deserialize;
use std::time::Duration;
Expand Down Expand Up @@ -75,8 +75,17 @@ struct CachedAllConnectors {
static ALL_CONNECTORS_CACHE: LazyLock<StdMutex<Option<CachedAllConnectors>>> =
LazyLock::new(|| StdMutex::new(None));

async fn apps_enabled(config: &Config) -> bool {
let auth_manager = AuthManager::shared(
config.codex_home.clone(),
false,
config.cli_auth_credentials_store_mode,
);
config.features.apps_enabled(Some(&auth_manager)).await
}

pub async fn list_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>> {
if !config.features.enabled(Feature::Apps) {
if !apps_enabled(config).await {
return Ok(Vec::new());
}
let (connectors_result, accessible_result) = tokio::join!(
Expand All @@ -96,7 +105,7 @@ pub async fn list_all_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>
}

pub async fn list_cached_all_connectors(config: &Config) -> Option<Vec<AppInfo>> {
if !config.features.enabled(Feature::Apps) {
if !apps_enabled(config).await {
return Some(Vec::new());
}

Expand All @@ -118,7 +127,7 @@ pub async fn list_all_connectors_with_options(
config: &Config,
force_refetch: bool,
) -> anyhow::Result<Vec<AppInfo>> {
if !config.features.enabled(Feature::Apps) {
if !apps_enabled(config).await {
return Ok(Vec::new());
}
init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
54 changes: 29 additions & 25 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,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());
Expand Down Expand Up @@ -3370,7 +3375,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)
Expand Down Expand Up @@ -3842,7 +3847,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(),
);
Expand Down Expand Up @@ -5305,28 +5310,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),
Expand Down Expand Up @@ -6077,7 +6081,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),
Expand Down
14 changes: 6 additions & 8 deletions codex-rs/core/src/connectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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);
Expand Down
38 changes: 38 additions & 0 deletions codex-rs/core/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -253,6 +255,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
Expand Down Expand Up @@ -957,4 +980,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)));
}
}
2 changes: 1 addition & 1 deletion codex-rs/core/src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Loading
Loading