mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
[codex] Preserve plugin apps in connector listings (#27602)
## Context This is PR4 in the plugin auth-routing stack. The earlier PRs make plugin surface projection auth-aware and narrow App/MCP conflicts by App declaration name. This PR keeps connector listing paths aligned with that projected plugin App set. This means ChatGPT/SIWC users will still see plugin-provided Apps in connector listing surfaces like the Apps/connector picker, while API-key users will not see Apps they cannot use. ## Stack - PR1: #27652 seed plugin manager auth at construction. - PR2: #27459 route plugin surfaces by auth mode. - PR3: #27607 dedupe plugin MCP servers by App declaration name. - PR4: #27602 preserve plugin Apps in connector listings. - PR5: #27461 skip install-time plugin MCP OAuth for matching App routes. ## Summary - Have app-server compute effective plugin Apps from the existing PluginsManager and pass them into connector listing. - Keep plugin Apps visible in Apps/connector listing for ChatGPT/SIWC users. - Keep API-key-style auth from surfacing plugin Apps in connector listings. ## Validation ```bash cargo test -p codex-chatgpt connectors::tests cargo test -p codex-app-server list_apps_includes_plugin_apps_for_chatgpt_auth git diff --check ```
This commit is contained in:
1 parent
127224cacc
commit
cededa26c5
7 files changed
+153
-53
No files matched your search
Generated
-1
@@ -2257,7 +2257,6 @@ dependencies = [
|
|||||||
"codex-app-server-protocol",
|
"codex-app-server-protocol",
|
||||||
"codex-connectors",
|
"codex-connectors",
|
||||||
"codex-core",
|
"codex-core",
|
||||||
"codex-core-plugins",
|
|
||||||
"codex-git-utils",
|
"codex-git-utils",
|
||||||
"codex-login",
|
"codex-login",
|
||||||
"codex-model-provider",
|
"codex-model-provider",
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ use codex_core_plugins::PluginInstallError as CorePluginInstallError;
|
|||||||
use codex_core_plugins::PluginInstallRequest;
|
use codex_core_plugins::PluginInstallRequest;
|
||||||
use codex_core_plugins::PluginReadRequest;
|
use codex_core_plugins::PluginReadRequest;
|
||||||
use codex_core_plugins::PluginUninstallError as CorePluginUninstallError;
|
use codex_core_plugins::PluginUninstallError as CorePluginUninstallError;
|
||||||
|
use codex_core_plugins::PluginsManager;
|
||||||
use codex_core_plugins::loader::load_plugin_apps;
|
use codex_core_plugins::loader::load_plugin_apps;
|
||||||
use codex_core_plugins::loader::load_plugin_mcp_servers;
|
use codex_core_plugins::loader::load_plugin_mcp_servers;
|
||||||
use codex_core_plugins::loader::plugin_telemetry_metadata_from_root;
|
use codex_core_plugins::loader::plugin_telemetry_metadata_from_root;
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ impl AppsRequestProcessor {
|
|||||||
let outgoing = Arc::clone(&self.outgoing);
|
let outgoing = Arc::clone(&self.outgoing);
|
||||||
let environment_manager = self.thread_manager.environment_manager();
|
let environment_manager = self.thread_manager.environment_manager();
|
||||||
let mcp_manager = self.thread_manager.mcp_manager();
|
let mcp_manager = self.thread_manager.mcp_manager();
|
||||||
|
let plugins_manager = self.thread_manager.plugins_manager();
|
||||||
let shutdown_token = self.shutdown_token.child_token();
|
let shutdown_token = self.shutdown_token.child_token();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -100,6 +101,7 @@ impl AppsRequestProcessor {
|
|||||||
config,
|
config,
|
||||||
environment_manager,
|
environment_manager,
|
||||||
mcp_manager,
|
mcp_manager,
|
||||||
|
plugins_manager,
|
||||||
) => {}
|
) => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -117,14 +119,22 @@ impl AppsRequestProcessor {
|
|||||||
config: Config,
|
config: Config,
|
||||||
environment_manager: Arc<EnvironmentManager>,
|
environment_manager: Arc<EnvironmentManager>,
|
||||||
mcp_manager: Arc<McpManager>,
|
mcp_manager: Arc<McpManager>,
|
||||||
|
plugins_manager: Arc<PluginsManager>,
|
||||||
) {
|
) {
|
||||||
let retry_params = params.clone();
|
let retry_params = params.clone();
|
||||||
let retry_config = config.clone();
|
let retry_config = config.clone();
|
||||||
let retry_environment_manager = Arc::clone(&environment_manager);
|
let retry_environment_manager = Arc::clone(&environment_manager);
|
||||||
let retry_mcp_manager = Arc::clone(&mcp_manager);
|
let retry_mcp_manager = Arc::clone(&mcp_manager);
|
||||||
let result =
|
let retry_plugins_manager = Arc::clone(&plugins_manager);
|
||||||
Self::apps_list_response(&outgoing, params, config, environment_manager, mcp_manager)
|
let result = Self::apps_list_response(
|
||||||
.await;
|
&outgoing,
|
||||||
|
params,
|
||||||
|
config,
|
||||||
|
environment_manager,
|
||||||
|
mcp_manager,
|
||||||
|
plugins_manager,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
let should_retry = result
|
let should_retry = result
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
|
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
|
||||||
@@ -141,6 +151,7 @@ impl AppsRequestProcessor {
|
|||||||
retry_config,
|
retry_config,
|
||||||
retry_environment_manager,
|
retry_environment_manager,
|
||||||
retry_mcp_manager,
|
retry_mcp_manager,
|
||||||
|
retry_plugins_manager,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -155,6 +166,7 @@ impl AppsRequestProcessor {
|
|||||||
config: Config,
|
config: Config,
|
||||||
environment_manager: Arc<EnvironmentManager>,
|
environment_manager: Arc<EnvironmentManager>,
|
||||||
mcp_manager: Arc<McpManager>,
|
mcp_manager: Arc<McpManager>,
|
||||||
|
plugins_manager: Arc<PluginsManager>,
|
||||||
) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {
|
) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {
|
||||||
let AppsListParams {
|
let AppsListParams {
|
||||||
cursor,
|
cursor,
|
||||||
@@ -170,9 +182,13 @@ impl AppsRequestProcessor {
|
|||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let plugin_apps = plugins_manager
|
||||||
|
.plugins_for_config(&config.plugins_config_input())
|
||||||
|
.await
|
||||||
|
.effective_apps();
|
||||||
let (mut accessible_connectors, mut all_connectors) = tokio::join!(
|
let (mut accessible_connectors, mut all_connectors) = tokio::join!(
|
||||||
connectors::list_cached_accessible_connectors_from_mcp_tools(&config),
|
connectors::list_cached_accessible_connectors_from_mcp_tools(&config),
|
||||||
connectors::list_cached_all_connectors(&config)
|
connectors::list_cached_all_connectors(&config, &plugin_apps)
|
||||||
);
|
);
|
||||||
let cached_all_connectors = all_connectors.clone();
|
let cached_all_connectors = all_connectors.clone();
|
||||||
|
|
||||||
@@ -193,10 +209,15 @@ impl AppsRequestProcessor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let all_config = config.clone();
|
let all_config = config.clone();
|
||||||
|
let all_plugin_apps = plugin_apps.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let result = connectors::list_all_connectors_with_options(&all_config, force_refetch)
|
let result = connectors::list_all_connectors_with_options(
|
||||||
.await
|
&all_config,
|
||||||
.map_err(|err| format!("failed to list apps: {err}"));
|
force_refetch,
|
||||||
|
&all_plugin_apps,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("failed to list apps: {err}"));
|
||||||
let _ = tx.send(AppListLoadResult::Directory(result));
|
let _ = tx.send(AppListLoadResult::Directory(result));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1578,7 +1578,7 @@ impl PluginRequestProcessor {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(plugin_app_category_by_id_from_value)
|
.map(plugin_app_category_by_id_from_value)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let all_connectors = connectors::list_cached_all_connectors(&config)
|
let all_connectors = connectors::list_cached_all_connectors(&config, &[])
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps)
|
connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps)
|
||||||
@@ -1630,7 +1630,7 @@ impl PluginRequestProcessor {
|
|||||||
|
|
||||||
let environment_manager = self.thread_manager.environment_manager();
|
let environment_manager = self.thread_manager.environment_manager();
|
||||||
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
|
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
|
||||||
connectors::list_all_connectors_with_options(config, /*force_refetch*/ false),
|
connectors::list_all_connectors_with_options(config, /*force_refetch*/ false, &[]),
|
||||||
connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||||
config,
|
config,
|
||||||
/*force_refetch*/ true,
|
/*force_refetch*/ true,
|
||||||
@@ -1646,7 +1646,7 @@ impl PluginRequestProcessor {
|
|||||||
plugin = plugin_id,
|
plugin = plugin_id,
|
||||||
"failed to load app metadata after plugin install: {err:#}"
|
"failed to load app metadata after plugin install: {err:#}"
|
||||||
);
|
);
|
||||||
connectors::list_cached_all_connectors(config)
|
connectors::list_cached_all_connectors(config, &[])
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -1910,16 +1910,21 @@ async fn load_plugin_app_summaries(
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let connectors =
|
let connectors = match connectors::list_all_connectors_with_options(
|
||||||
match connectors::list_all_connectors_with_options(config, /*force_refetch*/ false).await {
|
config,
|
||||||
Ok(connectors) => connectors,
|
/*force_refetch*/ false,
|
||||||
Err(err) => {
|
&[],
|
||||||
warn!("failed to load app metadata for plugin/read: {err:#}");
|
)
|
||||||
connectors::list_cached_all_connectors(config)
|
.await
|
||||||
.await
|
{
|
||||||
.unwrap_or_default()
|
Ok(connectors) => connectors,
|
||||||
}
|
Err(err) => {
|
||||||
};
|
warn!("failed to load app metadata for plugin/read: {err:#}");
|
||||||
|
connectors::list_cached_all_connectors(config, &[])
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);
|
let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex as StdMutex;
|
use std::sync::Mutex as StdMutex;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -215,6 +216,50 @@ async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Resu
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_apps_includes_plugin_apps_for_chatgpt_auth() -> Result<()> {
|
||||||
|
let (server_url, server_handle) =
|
||||||
|
start_apps_server_with_delays(Vec::new(), Vec::new(), Duration::ZERO, Duration::ZERO)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
write_connectors_and_plugins_config(codex_home.path(), &server_url)?;
|
||||||
|
write_plugin_app_fixture(codex_home.path(), "sample", "connector_sample")?;
|
||||||
|
write_chatgpt_auth(
|
||||||
|
codex_home.path(),
|
||||||
|
ChatGptAuthFixture::new("chatgpt-token")
|
||||||
|
.account_id("account-123")
|
||||||
|
.chatgpt_user_id("user-plugin-apps")
|
||||||
|
.chatgpt_account_id("account-123"),
|
||||||
|
AuthCredentialsStoreMode::File,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||||
|
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
|
let request_id = mcp
|
||||||
|
.send_apps_list_request(AppsListParams {
|
||||||
|
limit: None,
|
||||||
|
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.iter().any(|app| app.id == "connector_sample"));
|
||||||
|
assert!(next_cursor.is_none());
|
||||||
|
|
||||||
|
server_handle.abort();
|
||||||
|
let _ = server_handle.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Result<()> {
|
async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Result<()> {
|
||||||
let connectors = vec![AppInfo {
|
let connectors = vec![AppInfo {
|
||||||
@@ -1623,3 +1668,45 @@ connectors = true
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_connectors_and_plugins_config(codex_home: &Path, base_url: &str) -> std::io::Result<()> {
|
||||||
|
let config_toml = codex_home.join("config.toml");
|
||||||
|
std::fs::write(
|
||||||
|
config_toml,
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
chatgpt_base_url = "{base_url}"
|
||||||
|
mcp_oauth_credentials_store = "file"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
connectors = true
|
||||||
|
plugins = true
|
||||||
|
|
||||||
|
[plugins."sample@test"]
|
||||||
|
enabled = true
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_plugin_app_fixture(codex_home: &Path, plugin_name: &str, app_id: &str) -> Result<()> {
|
||||||
|
let plugin_root = codex_home
|
||||||
|
.join("plugins/cache")
|
||||||
|
.join("test")
|
||||||
|
.join(plugin_name)
|
||||||
|
.join("local");
|
||||||
|
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||||
|
std::fs::write(
|
||||||
|
plugin_root.join(".codex-plugin/plugin.json"),
|
||||||
|
format!(r#"{{"name":"{plugin_name}"}}"#),
|
||||||
|
)?;
|
||||||
|
std::fs::write(
|
||||||
|
plugin_root.join(".app.json"),
|
||||||
|
serde_json::to_vec_pretty(&json!({
|
||||||
|
"apps": {
|
||||||
|
plugin_name: { "id": app_id }
|
||||||
|
}
|
||||||
|
}))?,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ clap = { workspace = true, features = ["derive"] }
|
|||||||
codex-app-server-protocol = { workspace = true }
|
codex-app-server-protocol = { workspace = true }
|
||||||
codex-connectors = { workspace = true }
|
codex-connectors = { workspace = true }
|
||||||
codex-core = { workspace = true }
|
codex-core = { workspace = true }
|
||||||
codex-core-plugins = { workspace = true }
|
|
||||||
codex-git-utils = { workspace = true }
|
codex-git-utils = { workspace = true }
|
||||||
codex-login = { workspace = true }
|
codex-login = { workspace = true }
|
||||||
codex-model-provider = { workspace = true }
|
codex-model-provider = { workspace = true }
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_o
|
|||||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status;
|
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status;
|
||||||
pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools;
|
pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools;
|
||||||
pub use codex_core::connectors::with_app_enabled_state;
|
pub use codex_core::connectors::with_app_enabled_state;
|
||||||
use codex_core_plugins::PluginsManager;
|
|
||||||
use codex_login::AuthManager;
|
use codex_login::AuthManager;
|
||||||
use codex_login::CodexAuth;
|
use codex_login::CodexAuth;
|
||||||
use codex_login::default_client::originator;
|
use codex_login::default_client::originator;
|
||||||
@@ -69,10 +68,13 @@ pub async fn list_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_all_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>> {
|
pub async fn list_all_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>> {
|
||||||
list_all_connectors_with_options(config, /*force_refetch*/ false).await
|
list_all_connectors_with_options(config, /*force_refetch*/ false, &[]).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_cached_all_connectors(config: &Config) -> Option<Vec<AppInfo>> {
|
pub async fn list_cached_all_connectors(
|
||||||
|
config: &Config,
|
||||||
|
plugin_apps: &[AppConnectorId],
|
||||||
|
) -> Option<Vec<AppInfo>> {
|
||||||
if !apps_enabled(config).await {
|
if !apps_enabled(config).await {
|
||||||
return Some(Vec::new());
|
return Some(Vec::new());
|
||||||
}
|
}
|
||||||
@@ -80,22 +82,13 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option<Vec<AppInfo>>
|
|||||||
let auth = connector_auth(config).await.ok()?;
|
let auth = connector_auth(config).await.ok()?;
|
||||||
let cache_context = connector_directory_cache_context(config, &auth);
|
let cache_context = connector_directory_cache_context(config, &auth);
|
||||||
let connectors = codex_connectors::cached_directory_connectors(&cache_context)?;
|
let connectors = codex_connectors::cached_directory_connectors(&cache_context)?;
|
||||||
let connectors = merge_plugin_connectors(
|
Some(merge_and_filter_plugin_connectors(connectors, plugin_apps))
|
||||||
connectors,
|
|
||||||
plugin_apps_for_config(config)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.map(|connector_id| connector_id.0),
|
|
||||||
);
|
|
||||||
Some(filter_disallowed_connectors(
|
|
||||||
connectors,
|
|
||||||
originator().value.as_str(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_all_connectors_with_options(
|
pub async fn list_all_connectors_with_options(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
force_refetch: bool,
|
force_refetch: bool,
|
||||||
|
plugin_apps: &[AppConnectorId],
|
||||||
) -> anyhow::Result<Vec<AppInfo>> {
|
) -> anyhow::Result<Vec<AppInfo>> {
|
||||||
if !apps_enabled(config).await {
|
if !apps_enabled(config).await {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
@@ -116,17 +109,7 @@ pub async fn list_all_connectors_with_options(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let connectors = merge_plugin_connectors(
|
Ok(merge_and_filter_plugin_connectors(connectors, plugin_apps))
|
||||||
connectors,
|
|
||||||
plugin_apps_for_config(config)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.map(|connector_id| connector_id.0),
|
|
||||||
);
|
|
||||||
Ok(filter_disallowed_connectors(
|
|
||||||
connectors,
|
|
||||||
originator().value.as_str(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connector_directory_cache_context(
|
fn connector_directory_cache_context(
|
||||||
@@ -144,12 +127,17 @@ fn connector_directory_cache_context(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn plugin_apps_for_config(config: &Config) -> Vec<AppConnectorId> {
|
fn merge_and_filter_plugin_connectors(
|
||||||
let plugins_input = config.plugins_config_input();
|
connectors: Vec<AppInfo>,
|
||||||
PluginsManager::new(config.codex_home.to_path_buf())
|
plugin_apps: &[AppConnectorId],
|
||||||
.plugins_for_config(&plugins_input)
|
) -> Vec<AppInfo> {
|
||||||
.await
|
let connectors = merge_plugin_connectors(
|
||||||
.effective_apps()
|
connectors,
|
||||||
|
plugin_apps
|
||||||
|
.iter()
|
||||||
|
.map(|connector_id| connector_id.0.clone()),
|
||||||
|
);
|
||||||
|
filter_disallowed_connectors(connectors, originator().value.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connectors_for_plugin_apps(
|
pub fn connectors_for_plugin_apps(
|
||||||
|
|||||||
Reference in new issue
Block a user