Skip to content
Draft
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
65 changes: 37 additions & 28 deletions codex-rs/app-server/src/request_processors/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,47 +565,56 @@ impl PluginRequestProcessor {
(Vec::new(), Vec::new())
};

// TODO(remote plugins): Remove this once remote plugins are ready and vertical plugins are
// served directly from the normal remote catalog.
if include_vertical && !config.features.enabled(Feature::RemotePlugin) {
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
match codex_core_plugins::remote::fetch_openai_curated_remote_collection_marketplace(
&remote_plugin_service_config,
auth.as_ref(),
)
.await
let mut remote_sources = Vec::new();
if !explicit_marketplace_kinds && config.features.enabled(Feature::RemotePlugin) {
remote_sources.push(RemoteMarketplaceSource::Global);
}
if marketplace_kinds.contains(&PluginListMarketplaceKind::WorkspaceDirectory) {
remote_sources.push(RemoteMarketplaceSource::WorkspaceDirectory);
}
if marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe)
&& config.features.enabled(Feature::PluginSharing)
{
remote_sources.push(RemoteMarketplaceSource::SharedWithMe);
}
// Tool suggestions stay limited to the server-selected vertical collection, even when
// the full remote marketplace is available in the plugin browser. Cache that collection
// while plugin/list is loading remote catalog data so turn construction is network-free.
Copy link
Copy Markdown
Collaborator

@xl-openai xl-openai May 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if remote plugin is on should we cache the entire remote plugins? not just the vertical?

let remote_plugin_enabled = config.features.enabled(Feature::RemotePlugin);
let should_cache_remote_tool_suggest_marketplace = (include_vertical
&& !remote_plugin_enabled)
|| (!explicit_marketplace_kinds && remote_plugin_enabled);
let remote_tool_suggest_marketplace = if should_cache_remote_tool_suggest_marketplace {
match plugins_manager
.remote_tool_suggest_marketplace_for_config(&plugins_input, auth.as_ref())
.await
{
Ok(Some(remote_marketplace)) => {
data.push(remote_marketplace_to_info(remote_marketplace));
}
Ok(None) => {}
Ok(remote_marketplace) => remote_marketplace,
Err(
RemotePluginCatalogError::AuthRequired
| RemotePluginCatalogError::UnsupportedAuthMode,
) => {}
) => None,
Err(err) => {
warn!(
error = %err,
"plugin/list openai-curated-remote collection fetch failed; returning local marketplaces only"
"plugin/list remote plugin suggestion catalog fetch failed; returning other marketplaces only"
);
None
}
}
}
} else {
None
};

let mut remote_sources = Vec::new();
if !explicit_marketplace_kinds && config.features.enabled(Feature::RemotePlugin) {
remote_sources.push(RemoteMarketplaceSource::Global);
}
if marketplace_kinds.contains(&PluginListMarketplaceKind::WorkspaceDirectory) {
remote_sources.push(RemoteMarketplaceSource::WorkspaceDirectory);
}
if marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe)
&& config.features.enabled(Feature::PluginSharing)
// TODO(remote plugins): Remove this compatibility response once clients no longer request
// the vertical marketplace explicitly. The same result remains cached for suggestions.
if include_vertical
&& !remote_plugin_enabled
&& let Some(remote_marketplace) = remote_tool_suggest_marketplace
{
remote_sources.push(RemoteMarketplaceSource::SharedWithMe);
data.push(remote_marketplace_to_info(remote_marketplace));
}

if !remote_sources.is_empty() {
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/app-server/tests/suite/v2/plugin_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1712,6 +1712,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
.respond_with(ResponseTemplate::new(200).set_body_string(global_directory_body))
.mount(&server)
.await;
mount_openai_curated_remote_collection_plugin_list(&server, empty_page_body).await;
Mock::given(method("GET"))
.and(path("/backend-api/ps/plugins/list"))
.and(query_param("scope", "WORKSPACE"))
Expand Down Expand Up @@ -1801,7 +1802,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
);
assert_eq!(response.featured_plugin_ids, Vec::<String>::new());
assert!(
!server
server
.received_requests()
.await
.expect("wiremock should record requests")
Expand Down
48 changes: 48 additions & 0 deletions codex-rs/core-plugins/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome;
use crate::marketplace_upgrade::configured_git_marketplace_names;
use crate::marketplace_upgrade::upgrade_configured_git_marketplaces;
use crate::remote::RemoteInstalledPlugin;
use crate::remote::RemoteMarketplace;
use crate::remote::RemotePluginCatalogError;
use crate::remote::RemotePluginScope;
use crate::remote::RemotePluginServiceConfig;
use crate::remote::fetch_openai_curated_remote_collection_marketplace;
use crate::remote_legacy::RemotePluginFetchError;
use crate::remote_legacy::RemotePluginMutationError;
use crate::startup_sync::curated_plugins_repo_path;
Expand Down Expand Up @@ -124,6 +126,12 @@ struct CachedFeaturedPluginIds {
featured_plugin_ids: Vec<String>,
}

#[derive(Clone)]
struct CachedRemoteToolSuggestMarketplace {
key: FeaturedPluginIdsCacheKey,
marketplace: Option<RemoteMarketplace>,
}

struct RemoteInstalledPluginsCacheRefreshRequest {
service_config: RemotePluginServiceConfig,
auth: Option<CodexAuth>,
Expand Down Expand Up @@ -402,6 +410,7 @@ pub struct PluginsManager {
configured_marketplace_upgrade_state: RwLock<ConfiguredMarketplaceUpgradeState>,
non_curated_cache_refresh_state: RwLock<NonCuratedCacheRefreshState>,
cached_enabled_outcome: RwLock<Option<CachedPluginLoadOutcome>>,
remote_tool_suggest_marketplace_cache: RwLock<Option<CachedRemoteToolSuggestMarketplace>>,
remote_installed_plugins_cache: RwLock<Option<Vec<RemoteInstalledPlugin>>>,
remote_installed_plugins_cache_refresh_state: RwLock<RemoteInstalledPluginsCacheRefreshState>,
remote_sync_lock: Semaphore,
Expand Down Expand Up @@ -440,6 +449,7 @@ impl PluginsManager {
),
non_curated_cache_refresh_state: RwLock::new(NonCuratedCacheRefreshState::default()),
cached_enabled_outcome: RwLock::new(None),
remote_tool_suggest_marketplace_cache: RwLock::new(None),
remote_installed_plugins_cache: RwLock::new(None),
remote_installed_plugins_cache_refresh_state: RwLock::new(
RemoteInstalledPluginsCacheRefreshState::default(),
Expand Down Expand Up @@ -513,6 +523,12 @@ impl PluginsManager {
Err(err) => err.into_inner(),
};
*featured_plugin_ids_cache = None;
let mut remote_tool_suggest_marketplace_cache =
match self.remote_tool_suggest_marketplace_cache.write() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
};
*remote_tool_suggest_marketplace_cache = None;
}

fn clear_enabled_outcome_cache(&self) {
Expand Down Expand Up @@ -590,6 +606,38 @@ impl PluginsManager {
Some(crate::remote::group_remote_installed_plugins_by_marketplaces(plugins, visible_scopes))
}

pub async fn remote_tool_suggest_marketplace_for_config(
&self,
config: &PluginsConfigInput,
auth: Option<&CodexAuth>,
) -> Result<Option<RemoteMarketplace>, RemotePluginCatalogError> {
let key = featured_plugin_ids_cache_key(config, auth);
{
let cache = match self.remote_tool_suggest_marketplace_cache.read() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
};
if let Some(cached) = cache.as_ref().filter(|cached| cached.key == key) {
return Ok(cached.marketplace.clone());
}
}

let marketplace = fetch_openai_curated_remote_collection_marketplace(
&remote_plugin_service_config(config),
auth,
)
.await?;
let mut cache = match self.remote_tool_suggest_marketplace_cache.write() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
};
*cache = Some(CachedRemoteToolSuggestMarketplace {
key,
marketplace: marketplace.clone(),
});
Ok(marketplace)
}

pub async fn build_and_cache_remote_installed_plugin_marketplaces(
&self,
config: &PluginsConfigInput,
Expand Down
66 changes: 66 additions & 0 deletions codex-rs/core-plugins/src/manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,72 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad
);
}

#[tokio::test]
async fn remote_tool_suggest_marketplace_for_config_caches_vertical_collection() {
let codex_home = TempDir::new().unwrap();
write_file(
&codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
"#,
);
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/backend-api/ps/plugins/list"))
.and(query_param("scope", "GLOBAL"))
.and(query_param("limit", "200"))
.and(query_param("collection", "vertical"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r#"{
"plugins": [{
"id": "plugins~Plugin_sample_remote",
"name": "sample-remote",
"scope": "GLOBAL",
"installation_policy": "AVAILABLE",
"authentication_policy": "ON_USE",
"status": "ENABLED",
"release": {
"display_name": "Sample Remote",
"description": "Sample remote plugin",
"app_ids": ["asdk_app_sample_source"],
"interface": {},
"skills": []
}
}],
"pagination": { "next_page_token": null }
}"#,
))
.expect(1)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/backend-api/ps/plugins/installed"))
.and(query_param("scope", "GLOBAL"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(r#"{"plugins": [], "pagination": {"next_page_token": null}}"#),
)
.expect(1)
.mount(&server)
.await;

let mut config = load_config(codex_home.path(), codex_home.path()).await;
config.chatgpt_base_url = format!("{}/backend-api", server.uri());
let manager = PluginsManager::new(codex_home.path().to_path_buf());
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();

let first = manager
.remote_tool_suggest_marketplace_for_config(&config, Some(&auth))
.await
.expect("first suggestion marketplace fetch should succeed");
let second = manager
.remote_tool_suggest_marketplace_for_config(&config, Some(&auth))
.await
.expect("cached suggestion marketplace fetch should succeed");

assert_eq!(second, first);
}

#[tokio::test]
async fn load_plugins_resolves_disabled_skill_names_against_loaded_plugin_skills() {
let codex_home = TempDir::new().unwrap();
Expand Down
9 changes: 9 additions & 0 deletions codex-rs/core-plugins/src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ pub struct RemotePluginSummary {
pub id: String,
pub remote_plugin_id: String,
pub name: String,
pub description: Option<String>,
pub has_skills: bool,
pub app_ids: Vec<String>,
pub share_context: Option<RemotePluginShareContext>,
pub installed: bool,
pub enabled: bool,
Expand Down Expand Up @@ -723,6 +726,9 @@ pub fn group_remote_installed_plugins_by_marketplaces(
id: plugin_id.as_key(),
remote_plugin_id: plugin.id.clone(),
name: plugin.name.clone(),
description: None,
has_skills: false,
app_ids: Vec::new(),
share_context: None,
installed: true,
enabled: plugin.enabled,
Expand Down Expand Up @@ -1042,6 +1048,9 @@ fn build_remote_plugin_summary(
id: plugin_id.as_key(),
remote_plugin_id: plugin.id.clone(),
name: plugin.name.clone(),
description: non_empty_string(Some(&plugin.release.description)),
has_skills: !plugin.release.skills.is_empty(),
app_ids: plugin.release.app_ids.clone(),
share_context: remote_plugin_share_context(plugin)?,
installed: installed_plugin.is_some(),
enabled: installed_plugin.is_some_and(|plugin| plugin.enabled),
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/core-plugins/src/remote/share/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,9 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
id: "demo-plugin@workspace-shared-with-me".to_string(),
remote_plugin_id: "plugins_123".to_string(),
name: "demo-plugin".to_string(),
description: Some("Demo plugin description".to_string()),
has_skills: false,
app_ids: Vec::new(),
share_context: Some(RemotePluginShareContext {
remote_plugin_id: "plugins_123".to_string(),
remote_version: Some("0.1.0".to_string()),
Expand Down Expand Up @@ -656,6 +659,9 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
id: "demo-plugin@workspace-shared-with-me".to_string(),
remote_plugin_id: "plugins_456".to_string(),
name: "demo-plugin".to_string(),
description: Some("Demo plugin description".to_string()),
has_skills: false,
app_ids: Vec::new(),
share_context: Some(RemotePluginShareContext {
remote_plugin_id: "plugins_456".to_string(),
remote_version: Some("0.1.0".to_string()),
Expand Down
15 changes: 10 additions & 5 deletions codex-rs/core/src/connectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
auth: Option<&CodexAuth>,
accessible_connectors: &[AppInfo],
loaded_plugin_app_connector_ids: &[String],
plugins_manager: &PluginsManager,
) -> anyhow::Result<Vec<DiscoverableTool>> {
let connector_ids = tool_suggest_connector_ids(config, loaded_plugin_app_connector_ids);
let directory_connectors = codex_connectors::merge::merge_plugin_connectors(
Expand All @@ -129,11 +130,15 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
)
.into_iter()
.map(DiscoverableTool::from);
let discoverable_plugins =
list_tool_suggest_discoverable_plugins(config, loaded_plugin_app_connector_ids)
.await?
.into_iter()
.map(DiscoverableTool::from);
let discoverable_plugins = list_tool_suggest_discoverable_plugins(
config,
auth,
plugins_manager,
loaded_plugin_app_connector_ids,
)
.await?
.into_iter()
.map(DiscoverableTool::from);
Ok(discoverable_connectors
.chain(discoverable_plugins)
.collect())
Expand Down
16 changes: 12 additions & 4 deletions codex-rs/core/src/connectors_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1237,10 +1237,16 @@ discoverables = [
.expect("config should load");
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();

let discoverable_tools =
list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[], &[])
.await
.expect("discoverable tools should load");
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(
&config,
Some(&auth),
&[],
&[],
&plugins_manager,
)
.await
.expect("discoverable tools should load");

assert_eq!(
discoverable_tools,
Expand Down Expand Up @@ -1268,12 +1274,14 @@ apps = true
.expect("config should load");
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let loaded_plugin_app_connector_ids = vec!["asdk_app_databricks_workspace".to_string()];
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());

let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(
&config,
Some(&auth),
&[],
&loaded_plugin_app_connector_ids,
&plugins_manager,
)
.await
.expect("discoverable tools should load");
Expand Down
Loading
Loading