From faaff79b410d2a175883ee103b1cc022e343c1c0 Mon Sep 17 00:00:00 2001 From: xli-oai Date: Wed, 8 Apr 2026 10:32:47 -0700 Subject: [PATCH] Record added marketplaces in local registry --- codex-rs/cli/src/marketplace_cmd.rs | 3 + codex-rs/cli/tests/marketplace_add.rs | 3 + codex-rs/core/src/plugins/manager.rs | 114 ++++++++++++++++++++- codex-rs/core/src/plugins/manager_tests.rs | 2 + codex-rs/core/src/plugins/mod.rs | 1 + 5 files changed, 122 insertions(+), 1 deletion(-) diff --git a/codex-rs/cli/src/marketplace_cmd.rs b/codex-rs/cli/src/marketplace_cmd.rs index ff78b8f95c..442ee66879 100644 --- a/codex-rs/cli/src/marketplace_cmd.rs +++ b/codex-rs/cli/src/marketplace_cmd.rs @@ -5,6 +5,7 @@ use clap::Parser; use codex_core::config::find_codex_home; use codex_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME; use codex_core::plugins::marketplace_install_root; +use codex_core::plugins::record_installed_marketplace_root; use codex_core::plugins::validate_marketplace_root; use codex_utils_cli::CliConfigOverrides; use std::fs; @@ -162,6 +163,8 @@ async fn run_add(args: AddMarketplaceArgs) -> Result<()> { ensure_marketplace_destination_is_inside_install_root(&install_root, &destination)?; replace_marketplace_root(&staged_root, &destination) .with_context(|| format!("failed to install marketplace at {}", destination.display()))?; + record_installed_marketplace_root(&codex_home, &marketplace_name, &destination) + .with_context(|| format!("failed to record marketplace `{marketplace_name}`"))?; println!( "Added marketplace `{marketplace_name}` from {}.", diff --git a/codex-rs/cli/tests/marketplace_add.rs b/codex-rs/cli/tests/marketplace_add.rs index 5d79570586..52c3507ffd 100644 --- a/codex-rs/cli/tests/marketplace_add.rs +++ b/codex-rs/cli/tests/marketplace_add.rs @@ -53,6 +53,9 @@ async fn marketplace_add_local_directory_installs_valid_marketplace_root() -> Re let installed_root = marketplace_install_root(codex_home.path()).join("debug"); assert_eq!(validate_marketplace_root(&installed_root)?, "debug"); + let registry = std::fs::read_to_string(codex_home.path().join(".tmp/known_marketplaces.json"))?; + assert!(registry.contains(r#""name": "debug""#)); + assert!(registry.contains(r#""installLocation""#)); assert!( installed_root .join("plugins/sample/.codex-plugin/plugin.json") diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index c6970c3773..db48f105df 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -58,6 +58,7 @@ use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; +use serde::Serialize; use serde_json::Map as JsonMap; use serde_json::Value as JsonValue; use serde_json::json; @@ -79,7 +80,8 @@ use tracing::warn; const DEFAULT_SKILLS_DIR_NAME: &str = "skills"; const DEFAULT_MCP_CONFIG_FILE: &str = ".mcp.json"; const DEFAULT_APP_CONFIG_FILE: &str = ".app.json"; -pub const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/plugins/marketplaces"; +pub const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces"; +const KNOWN_MARKETPLACES_FILE: &str = "known_marketplaces.json"; pub const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated"; pub const OPENAI_CURATED_MARKETPLACE_DISPLAY_NAME: &str = "OpenAI Curated"; static CURATED_REPO_SYNC_STARTED: AtomicBool = AtomicBool::new(false); @@ -1240,7 +1242,37 @@ pub fn marketplace_install_root(codex_home: &Path) -> PathBuf { codex_home.join(INSTALLED_MARKETPLACES_DIR) } +pub fn record_installed_marketplace_root( + codex_home: &Path, + marketplace_name: &str, + install_location: &Path, +) -> std::io::Result<()> { + let registry_path = marketplace_registry_path(codex_home); + let mut registry = if registry_path.is_file() { + read_marketplace_registry(®istry_path)? + } else { + KnownMarketplacesRegistry::default() + }; + + registry + .marketplaces + .retain(|marketplace| marketplace.name != marketplace_name); + registry.marketplaces.push(KnownMarketplaceRegistryEntry { + name: marketplace_name.to_string(), + install_location: install_location.to_path_buf(), + }); + registry + .marketplaces + .sort_unstable_by(|left, right| left.name.cmp(&right.name)); + write_marketplace_registry(®istry_path, ®istry) +} + fn installed_marketplace_roots(codex_home: &Path) -> Vec { + let registry_path = marketplace_registry_path(codex_home); + if registry_path.is_file() { + return installed_marketplace_roots_from_registry(®istry_path); + } + let install_root = marketplace_install_root(codex_home); let Ok(entries) = fs::read_dir(&install_root) else { return Vec::new(); @@ -1260,6 +1292,86 @@ fn installed_marketplace_roots(codex_home: &Path) -> Vec { roots } +fn marketplace_registry_path(codex_home: &Path) -> PathBuf { + codex_home.join(".tmp").join(KNOWN_MARKETPLACES_FILE) +} + +fn installed_marketplace_roots_from_registry(registry_path: &Path) -> Vec { + let registry = match read_marketplace_registry(registry_path) { + Ok(registry) => registry, + Err(err) => { + warn!( + path = %registry_path.display(), + error = %err, + "failed to read installed marketplace registry" + ); + return Vec::new(); + } + }; + + let mut roots = registry + .marketplaces + .into_iter() + .filter_map(|marketplace| { + let path = marketplace.install_location; + if path.join(".agents/plugins/marketplace.json").is_file() { + AbsolutePathBuf::try_from(path).ok() + } else { + None + } + }) + .collect::>(); + roots.sort_unstable_by(|left, right| left.as_path().cmp(right.as_path())); + roots +} + +fn read_marketplace_registry(path: &Path) -> std::io::Result { + let contents = fs::read_to_string(path)?; + serde_json::from_str(&contents).map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "failed to parse marketplace registry {}: {err}", + path.display() + ), + ) + }) +} + +fn write_marketplace_registry( + path: &Path, + registry: &KnownMarketplacesRegistry, +) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let contents = serde_json::to_vec_pretty(registry).map_err(std::io::Error::other)?; + fs::write(path, contents) +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct KnownMarketplacesRegistry { + version: u32, + marketplaces: Vec, +} + +impl Default for KnownMarketplacesRegistry { + fn default() -> Self { + Self { + version: 1, + marketplaces: Vec::new(), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct KnownMarketplaceRegistryEntry { + name: String, + install_location: PathBuf, +} + #[derive(Debug, thiserror::Error)] pub enum PluginInstallError { #[error("{0}")] diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core/src/plugins/manager_tests.rs index fbe104371f..2a27e41ca7 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core/src/plugins/manager_tests.rs @@ -1539,6 +1539,8 @@ plugins = true r#"{"name":"sample"}"#, ) .unwrap(); + record_installed_marketplace_root(tmp.path(), "debug", &marketplace_root) + .expect("record installed marketplace"); let config = load_config(tmp.path(), tmp.path()).await; let marketplaces = PluginsManager::new(tmp.path().to_path_buf()) diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 4ca09d0d10..29c6674ca2 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -46,6 +46,7 @@ pub use manager::load_plugin_apps; pub use manager::load_plugin_mcp_servers; pub use manager::marketplace_install_root; pub use manager::plugin_telemetry_metadata_from_root; +pub use manager::record_installed_marketplace_root; pub use manifest::PluginManifestInterface; pub(crate) use manifest::PluginManifestPaths; pub(crate) use manifest::load_plugin_manifest;