feat: Add vertical remote plugin collection support (#23584)

- Adds an explicit vertical marketplace kind for plugin/list that
fail-open fetches collection=vertical only when full remote plugins are
disabled.

- Renames the global remote marketplace/cache identity to
openai-curated-remote and materializes remote installs with backend
release versions and app manifests.
This commit is contained in:
xl-openai
2026-05-19 22:03:08 -07:00
committed by GitHub
parent 9dda71dbae
commit dc255b0d8a
18 changed files with 806 additions and 221 deletions
+175 -7
View File
@@ -1,3 +1,4 @@
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
use crate::store::PluginInstallResult;
use crate::store::PluginStore;
use crate::store::PluginStoreError;
@@ -10,6 +11,7 @@ use codex_utils_plugins::find_plugin_manifest_path;
use flate2::read::GzDecoder;
use reqwest::Response;
use reqwest::StatusCode;
use serde_json::Value as JsonValue;
use std::fs;
use std::io;
use std::io::Read;
@@ -33,6 +35,7 @@ const TEST_ALLOW_LOOPBACK_HTTP_REMOTE_PLUGIN_BUNDLES_ENV: &str =
pub struct ValidatedRemotePluginBundle {
pub plugin_id: PluginId,
pub plugin_version: String,
app_manifest: Option<JsonValue>,
bundle_download_url: String,
}
@@ -137,6 +140,7 @@ pub fn validate_remote_plugin_bundle(
plugin_name: &str,
release_version: Option<&str>,
bundle_download_url: Option<&str>,
app_manifest: Option<JsonValue>,
) -> Result<ValidatedRemotePluginBundle, RemotePluginBundleInstallError> {
let plugin_id = PluginId::new(plugin_name.to_string(), remote_marketplace_name.to_string())
.map_err(|source| RemotePluginBundleInstallError::InvalidPluginId {
@@ -187,6 +191,7 @@ pub fn validate_remote_plugin_bundle(
Ok(ValidatedRemotePluginBundle {
plugin_id,
plugin_version,
app_manifest,
bundle_download_url,
})
}
@@ -367,6 +372,7 @@ fn install_remote_plugin_bundle(
extract_plugin_bundle_tar_gz(&bundle_bytes, extract_dir.path())?;
let plugin_root = find_extracted_plugin_root(extract_dir.path())?;
prepare_extracted_remote_plugin_root(&plugin_root, &bundle)?;
let plugin_root = AbsolutePathBuf::try_from(plugin_root).map_err(|err| {
RemotePluginBundleInstallError::InvalidBundle(format!(
"failed to resolve extracted remote plugin bundle root: {err}"
@@ -436,6 +442,90 @@ fn extract_remote_plugin_bundle_to_path(
Ok(destination)
}
fn prepare_extracted_remote_plugin_root(
plugin_root: &Path,
bundle: &ValidatedRemotePluginBundle,
) -> Result<(), RemotePluginBundleInstallError> {
if bundle.plugin_id.marketplace_name != REMOTE_GLOBAL_MARKETPLACE_NAME {
return Ok(());
}
overwrite_plugin_manifest_version(plugin_root, &bundle.plugin_version)?;
if let Some(app_manifest) = &bundle.app_manifest {
overwrite_plugin_app_manifest(plugin_root, app_manifest)?;
}
Ok(())
}
fn overwrite_plugin_manifest_version(
plugin_root: &Path,
plugin_version: &str,
) -> Result<(), RemotePluginBundleInstallError> {
let manifest_path = find_plugin_manifest_path(plugin_root).ok_or_else(|| {
RemotePluginBundleInstallError::InvalidBundle(
"remote plugin bundle did not contain a valid plugin.json".to_string(),
)
})?;
let contents = fs::read_to_string(&manifest_path).map_err(|source| {
RemotePluginBundleInstallError::io("failed to read remote plugin manifest", source)
})?;
let mut manifest: JsonValue = serde_json::from_str(&contents).map_err(|err| {
RemotePluginBundleInstallError::InvalidBundle(format!(
"failed to parse remote plugin manifest: {err}"
))
})?;
let Some(manifest_object) = manifest.as_object_mut() else {
return Err(RemotePluginBundleInstallError::InvalidBundle(
"remote plugin manifest must be a JSON object".to_string(),
));
};
manifest_object.insert(
"version".to_string(),
JsonValue::String(plugin_version.to_string()),
);
write_json_file(
&manifest_path,
&manifest,
"failed to write remote plugin manifest",
)
}
fn overwrite_plugin_app_manifest(
plugin_root: &Path,
app_manifest: &JsonValue,
) -> Result<(), RemotePluginBundleInstallError> {
let app_manifest_path = crate::manifest::load_plugin_manifest(plugin_root)
.and_then(|manifest| manifest.paths.apps.map(|path| path.to_path_buf()))
.unwrap_or_else(|| plugin_root.join(".app.json"));
write_json_file(
&app_manifest_path,
app_manifest,
"failed to write remote plugin app manifest",
)
}
fn write_json_file(
path: &Path,
value: &JsonValue,
context: &'static str,
) -> Result<(), RemotePluginBundleInstallError> {
let parent = path.parent().ok_or_else(|| {
RemotePluginBundleInstallError::InvalidBundle(format!(
"remote plugin output path has no parent: {}",
path.display()
))
})?;
fs::create_dir_all(parent)
.map_err(|source| RemotePluginBundleInstallError::io(context, source))?;
let mut contents = serde_json::to_vec_pretty(value).map_err(|err| {
RemotePluginBundleInstallError::InvalidBundle(format!(
"failed to serialize remote plugin JSON override: {err}"
))
})?;
contents.push(b'\n');
fs::write(path, contents).map_err(|source| RemotePluginBundleInstallError::io(context, source))
}
fn extract_plugin_bundle_tar_gz(
bytes: &[u8],
destination: &Path,
@@ -624,15 +714,16 @@ mod tests {
fn validate_remote_plugin_bundle_uses_detail_name_for_local_plugin_id() {
let bundle = validate_remote_plugin_bundle(
REMOTE_PLUGIN_ID,
"chatgpt-global",
"openai-curated-remote",
"linear",
Some("1.2.3"),
Some("https://example.com/linear.tar.gz"),
/*app_manifest*/ None,
)
.expect("valid install plan");
assert_eq!(bundle.plugin_id.plugin_name, "linear");
assert_eq!(bundle.plugin_id.marketplace_name, "chatgpt-global");
assert_eq!(bundle.plugin_id.marketplace_name, "openai-curated-remote");
assert_eq!(bundle.plugin_version, "1.2.3");
assert_eq!(
bundle.bundle_download_url.as_str(),
@@ -644,10 +735,11 @@ mod tests {
fn validate_remote_plugin_bundle_rejects_missing_release_version() {
let err = validate_remote_plugin_bundle(
REMOTE_PLUGIN_ID,
"chatgpt-global",
"openai-curated-remote",
"linear",
/*release_version*/ None,
Some("https://example.com/linear.tar.gz"),
/*app_manifest*/ None,
)
.expect_err("missing release version should be rejected");
@@ -661,10 +753,11 @@ mod tests {
fn validate_remote_plugin_bundle_rejects_invalid_release_version() {
let err = validate_remote_plugin_bundle(
REMOTE_PLUGIN_ID,
"chatgpt-global",
"openai-curated-remote",
"linear",
Some("../1.2.3"),
Some("https://example.com/linear.tar.gz"),
/*app_manifest*/ None,
)
.expect_err("invalid release version should be rejected");
@@ -678,10 +771,11 @@ mod tests {
fn validate_remote_plugin_bundle_rejects_missing_download_url() {
let err = validate_remote_plugin_bundle(
REMOTE_PLUGIN_ID,
"chatgpt-global",
"openai-curated-remote",
"linear",
Some("1.2.3"),
/*bundle_download_url*/ None,
/*app_manifest*/ None,
)
.expect_err("missing bundle download URL should be rejected");
@@ -695,10 +789,11 @@ mod tests {
fn validate_remote_plugin_bundle_rejects_unsupported_download_url_scheme() {
let err = validate_remote_plugin_bundle(
REMOTE_PLUGIN_ID,
"chatgpt-global",
"openai-curated-remote",
"linear",
Some("1.2.3"),
Some("http://example.com/linear.tar.gz"),
/*app_manifest*/ None,
)
.expect_err("plain HTTP URLs should be rejected before cloud install");
@@ -755,6 +850,78 @@ mod tests {
);
}
#[test]
fn install_preserves_non_global_bundle_manifest_metadata() {
let codex_home = tempdir().expect("tempdir");
let bundle = validate_remote_plugin_bundle(
REMOTE_PLUGIN_ID,
"workspace-shared-with-me",
"linear",
Some("backend-version"),
Some("https://example.com/linear.tar.gz"),
Some(serde_json::json!({
"apps": {
"remote": {
"id": "remote-app"
}
}
})),
)
.expect("valid install plan");
let result = install_remote_plugin_bundle(
codex_home.path().to_path_buf(),
bundle,
tar_gz_bytes(&[
(
".codex-plugin/plugin.json",
br#"{"name":"linear","version":"bundle-version"}"#,
/*mode*/ 0o644,
),
(
".app.json",
br#"{"apps":{"bundled":{"id":"bundled-app"}}}"#,
/*mode*/ 0o644,
),
]),
)
.expect("install bundle");
assert_eq!(result.plugin_version, "backend-version");
let installed_manifest: JsonValue = serde_json::from_str(
&std::fs::read_to_string(
result
.installed_path
.join(".codex-plugin/plugin.json")
.as_path(),
)
.expect("read installed plugin manifest"),
)
.expect("parse installed plugin manifest");
assert_eq!(
installed_manifest,
serde_json::json!({
"name": "linear",
"version": "bundle-version",
})
);
let installed_app_manifest: JsonValue = serde_json::from_str(
&std::fs::read_to_string(result.installed_path.join(".app.json").as_path())
.expect("read installed app manifest"),
)
.expect("parse installed app manifest");
assert_eq!(
installed_app_manifest,
serde_json::json!({
"apps": {
"bundled": {
"id": "bundled-app",
},
},
})
);
}
#[test]
fn find_extracted_plugin_root_uses_local_manifest_discovery() {
let extraction_root = tempdir().expect("tempdir");
@@ -866,10 +1033,11 @@ mod tests {
fn valid_remote_plugin_bundle() -> ValidatedRemotePluginBundle {
validate_remote_plugin_bundle(
REMOTE_PLUGIN_ID,
"chatgpt-global",
"openai-curated-remote",
"linear",
Some("1.2.3"),
Some("https://example.com/linear.tar.gz"),
/*app_manifest*/ None,
)
.expect("valid install plan")
}