Skip to content

feat: Support remote plugin list/read.#18452

Merged
xl-openai merged 7 commits intomainfrom
xl/plugins3
Apr 22, 2026
Merged

feat: Support remote plugin list/read.#18452
xl-openai merged 7 commits intomainfrom
xl/plugins3

Conversation

@xl-openai
Copy link
Copy Markdown
Collaborator

Add a temporary internal remote_plugin feature flag that merges remote marketplaces into plugin/list and routes plugin/read through the remote APIs when needed, while keeping pure local marketplaces working as before.

@@ -0,0 +1,640 @@
use super::*;

impl CodexMessageProcessor {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Try move plugin API out from codex_message_processor.rs as the file is too large now.

@@ -0,0 +1,313 @@
use crate::remote::RemotePluginServiceConfig;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Git doesn’t detect it as a rename, but remote_legacy.rs is the old remote.rs moved over intact. It remains wired to the legacy plugin API and is currently only used for the initial sync path. The new implementation now lives in remote.rs.

let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_id = format!("{plugin_name}@{remote_marketplace_name}");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] plugin_name is request-controlled here, so concatenating it into "{plugin_name}@{remote_marketplace_name}" and then later interpolating that value into "{base_url}/ps/plugins/{plugin_id}" creates a path/query injection primitive against the authenticated ChatGPT plugin API. A crafted plugin name containing /, ?, or # can rewrite the request target before fetch_remote_plugin_detail() does the GET. We should either carry the raw plugin_name/marketplace separately and build the URL with path-segment escaping (for example via Url::path_segments_mut()), or percent-encode plugin_id before appending it to the path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Will add a safeguard but app-server is not a public facing service the caller should just be codex app/TUI.

@xl-openai xl-openai requested a review from xli-oai April 21, 2026 02:57
@xl-openai xl-openai force-pushed the xl/plugins3 branch 2 times, most recently from 578a2af to a10067c Compare April 21, 2026 03:38
@xl-openai xl-openai marked this pull request as ready for review April 21, 2026 04:44
@xl-openai xl-openai requested a review from a team as a code owner April 21, 2026 04:44
@xl-openai
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc88185633

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +643 to +645
| RemotePluginCatalogError::UnexpectedStatus { .. }
| RemotePluginCatalogError::Decode { .. } => JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Map missing remote plugins to invalid requests

Treating every UnexpectedStatus as INTERNAL_ERROR makes normal user mistakes (e.g. unknown plugin id returning HTTP 404 from /ps/plugins/{id}) look like server failures. plugin/read should classify not-found remote plugins as an invalid request, consistent with local plugin lookup behavior.

Useful? React with 👍 / 👎.

Comment thread codex-rs/core-plugins/src/remote.rs Outdated
Comment on lines +270 to +271
let directory_plugins = fetch_directory_plugins_for_scope(config, auth, scope).await?;
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Continue listing healthy scopes when one scope fails

fetch_remote_marketplaces aborts on the first scope error (?), so a WORKSPACE failure (e.g. permission or transient 5xx) drops GLOBAL results too. This causes plugin/list to return no remote marketplaces even when one scope is still available.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That's OK.

Comment on lines +364 to +365
let installed_plugin = fetch_installed_plugins_for_scope(config, auth, scope)
.await?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Fallback when installed-state fetch fails in plugin/read

fetch_remote_plugin_detail fetches plugin metadata first, but then hard-fails on any /ps/plugins/installed error via ?. A transient installed-list failure turns a readable plugin into an error response. This should degrade to installed=false/default skill states instead of failing the whole read.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That's OK.

Comment on lines +99 to +103
Ok(remote_marketplaces) => data.extend(
remote_marketplaces
.into_iter()
.map(remote_marketplace_to_info),
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Deduplicate name collisions when merging remote marketplaces

plugin_list blindly appends remote marketplaces to local ones. Because marketplace names are user-defined locally, a local chatgpt-global can collide with the remote chatgpt-global, yielding duplicate marketplace names and conflicting <plugin>@<marketplace> identities in client flows.

Useful? React with 👍 / 👎.

}
};

if config.features.enabled(Feature::RemotePlugin) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Enforce plugins gate before exposing remote catalogs

Remote branches in plugin/list and plugin/read are gated only by remote_plugin. If plugins = false but remote_plugin = true, remote marketplaces/details are still served, bypassing the main plugins feature-disable behavior used by local paths. The remote path should also require Feature::Plugins.

Useful? React with 👍 / 👎.

Comment thread codex-rs/core-plugins/src/remote.rs Outdated
Comment on lines +269 to +271
for scope in RemotePluginScope::all() {
let directory_plugins = fetch_directory_plugins_for_scope(config, auth, scope).await?;
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Fetch remote scopes concurrently to avoid long list stalls

Remote catalog loading is fully serial: for each scope it waits for directory then installed calls, each with a 30s timeout. In degraded networks this can add up to ~120s before returning plugin/list, causing avoidable latency spikes. Scope/page fetches should be parallelized or budgeted.

Useful? React with 👍 / 👎.

Comment on lines -1822 to -1824
interface: Some(MarketplaceInterface {
display_name: Some(OPENAI_CURATED_MARKETPLACE_DISPLAY_NAME.to_string()),
}),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

does this mean we no longer have the formatted OpenAI Curated in product surfaces and they see openai-curated instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don’t understand the original purpose of this code — our marketplace already defines its own marketplace display name.

Also, in the Codex App we actually override the value to “Built by OpenAI,” but the lookup uses the marketplace name rather than the display name.

Copy link
Copy Markdown
Collaborator

@sayan-oai sayan-oai left a comment

Choose a reason for hiding this comment

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

@xl-openai sorry to dump, but codex findings below.

1 and 2 can be deferred if we are not doing TUI changes in this PR like you said.
3 seems small, worth checking.
4 seems like it's worth checking, but if we are maintaining all the remote plugins we can probably be more lax.
5 is a nit.

Findings

  1. Remote plugin details are implemented in app-server, but the TUI never calls them.
    In plugins.rs (line 1098), the TUI decides details are viewable only when marketplace.path.is_some(). Remote marketplaces intentionally have path: None, so plugins.rs (line 1134) builds no Enter action.

  2. Installed remote plugins get a toggle that only writes local config, so it will not actually change remote state.

  3. Remote pagination can loop forever.
    remote.rs (line 519) and remote.rs (line 538) keep requesting pages until next_page_token is None, with no max page count and no repeated-token detection.

  4. Remote default prompts bypass the documented caps.
    The protocol says plugin default prompts are capped at 3 entries and 128 chars in v2.rs (line 3881). Local manifests enforce this in manifest.rs (line 244). Remote mapping just copies the prompt into a one-item vec in remote.rs (line 437).

5.: The app-server docs are stale for this API change.
README.md (line 189) still describes only local plugin list/read. It does not mention remote marketplaces, remoteMarketplaceName, or nullable marketplacePath / skill path. Example: a client following the README would still assume plugin/read requires marketplacePath.

Comment on lines +13 to +21
plugins_manager.maybe_start_non_curated_plugin_cache_refresh(&roots);

let config = match self.load_latest_config(/*fallback_cwd*/ None).await {
Ok(config) => config,
Err(err) => {
self.outgoing.send_error(request_id, err).await;
return;
}
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

saw that later in this function we check if Feature::Plugins is enabled; im surprised we dont already know that by that point. why not just swap the config load and cache refresh and early return if the feature is disabled?

@xl-openai xl-openai merged commit a978e41 into main Apr 22, 2026
35 of 36 checks passed
@xl-openai xl-openai deleted the xl/plugins3 branch April 22, 2026 01:39
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants