From 7bc6eb44863b0fd6ea2d6c1ed68762a77b2cf464 Mon Sep 17 00:00:00 2001 From: xli-oai Date: Mon, 13 Apr 2026 22:20:11 -0700 Subject: [PATCH 1/8] [codex] Port marketplace source support to shared core flow --- .../tests/suite/v2/marketplace_add.rs | 44 ++- codex-rs/cli/src/marketplace_cmd.rs | 3 +- codex-rs/cli/tests/marketplace_add.rs | 91 +++++- codex-rs/config/src/types.rs | 2 + codex-rs/core/config.schema.json | 4 +- codex-rs/core/src/plugins/marketplace_add.rs | 34 +- .../src/plugins/marketplace_add/metadata.rs | 20 ++ .../src/plugins/marketplace_add/source.rs | 307 +++++++++++++++++- 8 files changed, 475 insertions(+), 30 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_add.rs b/codex-rs/app-server/tests/suite/v2/marketplace_add.rs index 8e81d6865497..58e252c19f4a 100644 --- a/codex-rs/app-server/tests/suite/v2/marketplace_add.rs +++ b/codex-rs/app-server/tests/suite/v2/marketplace_add.rs @@ -1,7 +1,12 @@ use anyhow::Result; use app_test_support::McpProcess; +use app_test_support::to_response; +use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::MarketplaceAddParams; +use codex_app_server_protocol::MarketplaceAddResponse; use codex_app_server_protocol::RequestId; +use codex_core::plugins::marketplace_install_root; +use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::Duration; use tokio::time::timeout; @@ -9,8 +14,20 @@ use tokio::time::timeout; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); #[tokio::test] -async fn marketplace_add_rejects_local_directory_source() -> Result<()> { +async fn marketplace_add_supports_local_directory_source() -> Result<()> { let codex_home = TempDir::new()?; + let source = codex_home.path().join("marketplace"); + std::fs::create_dir_all(source.join(".agents/plugins"))?; + std::fs::create_dir_all(source.join("plugins/sample/.codex-plugin"))?; + std::fs::write( + source.join(".agents/plugins/marketplace.json"), + r#"{"name":"debug","plugins":[]}"#, + )?; + std::fs::write( + source.join("plugins/sample/.codex-plugin/plugin.json"), + r#"{"name":"sample"}"#, + )?; + std::fs::write(source.join("plugins/sample/marker.txt"), "local ref")?; let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; @@ -22,19 +39,26 @@ async fn marketplace_add_rejects_local_directory_source() -> Result<()> { }) .await?; - let err = timeout( + let response: JSONRPCResponse = timeout( DEFAULT_TIMEOUT, - mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), ) .await??; + let MarketplaceAddResponse { + marketplace_name, + installed_root, + already_added, + } = to_response(response)?; + let expected_root = marketplace_install_root(codex_home.path()) + .join("debug") + .canonicalize()?; - assert_eq!(err.error.code, -32600); - assert!( - err.error.message.contains( - "local marketplace sources are not supported yet; use an HTTP(S) Git URL, SSH Git URL, or GitHub owner/repo" - ), - "unexpected error: {}", - err.error.message + assert_eq!(marketplace_name, "debug"); + assert_eq!(installed_root.as_path(), expected_root.as_path()); + assert!(!already_added); + assert_eq!( + std::fs::read_to_string(installed_root.as_path().join("plugins/sample/marker.txt"))?, + "local ref" ); Ok(()) } diff --git a/codex-rs/cli/src/marketplace_cmd.rs b/codex-rs/cli/src/marketplace_cmd.rs index b21e25a3934c..95d76b91fbf9 100644 --- a/codex-rs/cli/src/marketplace_cmd.rs +++ b/codex-rs/cli/src/marketplace_cmd.rs @@ -23,7 +23,8 @@ enum MarketplaceSubcommand { #[derive(Debug, Parser)] struct AddMarketplaceArgs { - /// Marketplace source. Supports owner/repo[@ref], HTTP(S) Git URLs, or SSH URLs. + /// Marketplace source. Supports owner/repo[@ref], HTTP(S) Git URLs, SSH URLs, + /// local filesystem paths, or direct marketplace.json URLs. source: String, /// Git ref to check out. Overrides any @ref or #ref suffix in SOURCE. diff --git a/codex-rs/cli/tests/marketplace_add.rs b/codex-rs/cli/tests/marketplace_add.rs index 9cc5c65a5cdd..e79e6c06161f 100644 --- a/codex-rs/cli/tests/marketplace_add.rs +++ b/codex-rs/cli/tests/marketplace_add.rs @@ -1,7 +1,12 @@ use anyhow::Result; +use codex_config::CONFIG_TOML_FILE; use codex_core::plugins::marketplace_install_root; -use predicates::str::contains; +use pretty_assertions::assert_eq; +use std::io::Read; +use std::io::Write; +use std::net::TcpListener; use std::path::Path; +use std::thread; use tempfile::TempDir; fn codex_command(codex_home: &Path) -> Result { @@ -37,7 +42,7 @@ fn write_marketplace_source(source: &Path, marker: &str) -> Result<()> { } #[tokio::test] -async fn marketplace_add_rejects_local_directory_source() -> Result<()> { +async fn marketplace_add_supports_local_directory_source() -> Result<()> { let codex_home = TempDir::new()?; let source = TempDir::new()?; write_marketplace_source(source.path(), "local ref")?; @@ -48,16 +53,84 @@ async fn marketplace_add_rejects_local_directory_source() -> Result<()> { .current_dir(source_parent) .args(["marketplace", "add", source_arg.as_str()]) .assert() - .failure() - .stderr(contains( - "local marketplace sources are not supported yet; use an HTTP(S) Git URL, SSH Git URL, or GitHub owner/repo", - )); + .success(); + let installed_root = marketplace_install_root(codex_home.path()).join("debug"); + assert_eq!( + std::fs::read_to_string(installed_root.join("plugins/sample/marker.txt"))?, + "local ref" + ); + + let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?; + let config: toml::Value = toml::from_str(&config)?; + let expected_source = source.path().canonicalize()?.display().to_string(); + assert_eq!( + config["marketplaces"]["debug"]["source_type"].as_str(), + Some("path") + ); + assert_eq!( + config["marketplaces"]["debug"]["source"].as_str(), + Some(expected_source.as_str()) + ); + + Ok(()) +} + +fn spawn_manifest_server(body: String) -> Result<(u16, thread::JoinHandle>)> { + let listener = TcpListener::bind("127.0.0.1:0")?; + let port = listener.local_addr()?.port(); + Ok(( + port, + thread::spawn(move || { + let (mut stream, _addr) = listener.accept()?; + let mut request = [0_u8; 2048]; + let _ = stream.read(&mut request)?; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + stream.write_all(response.as_bytes())?; + Ok(()) + }), + )) +} + +#[tokio::test] +async fn marketplace_add_supports_manifest_url_source() -> Result<()> { + let codex_home = TempDir::new()?; + let source = TempDir::new()?; + std::fs::create_dir_all(source.path().join(".agents/plugins"))?; + std::fs::write( + source.path().join(".agents/plugins/marketplace.json"), + r#"{"name":"debug-url","plugins":[]}"#, + )?; + let (port, server) = spawn_manifest_server(r#"{"name":"debug-url","plugins":[]}"#.to_string())?; + let url = format!("http://127.0.0.1:{port}/.agents/plugins/marketplace.json"); + + codex_command(codex_home.path())? + .args(["marketplace", "add", &url]) + .assert() + .success(); + + let installed_root = marketplace_install_root(codex_home.path()).join("debug-url"); assert!( - !marketplace_install_root(codex_home.path()) - .join("debug") - .exists() + installed_root + .join(".agents/plugins/marketplace.json") + .is_file() + ); + + let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?; + let config: toml::Value = toml::from_str(&config)?; + assert_eq!( + config["marketplaces"]["debug-url"]["source_type"].as_str(), + Some("manifest_url") + ); + assert_eq!( + config["marketplaces"]["debug-url"]["source"].as_str(), + Some(url.as_str()) ); + server.join().unwrap()?; Ok(()) } diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 64cfe85d085c..ff21ceb6f228 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -632,6 +632,8 @@ pub struct MarketplaceConfig { #[serde(rename_all = "snake_case")] pub enum MarketplaceSourceType { Git, + Path, + ManifestUrl, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 7c3f6dd99a74..e50a64aee727 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -810,7 +810,9 @@ }, "MarketplaceSourceType": { "enum": [ - "git" + "git", + "path", + "manifest_url" ], "type": "string" }, diff --git a/codex-rs/core/src/plugins/marketplace_add.rs b/codex-rs/core/src/plugins/marketplace_add.rs index 55c50099a7a2..fdf70f826ef6 100644 --- a/codex-rs/core/src/plugins/marketplace_add.rs +++ b/codex-rs/core/src/plugins/marketplace_add.rs @@ -20,6 +20,7 @@ use metadata::installed_marketplace_root_for_source; use metadata::record_added_marketplace_entry; use source::MarketplaceSource; use source::parse_marketplace_source; +use source::stage_marketplace_source; use source::validate_marketplace_source_root; #[derive(Debug, Clone, PartialEq, Eq)] @@ -120,8 +121,7 @@ where })?; let staged_root = staged_root.keep(); - let MarketplaceSource::Git { url, ref_name } = &source; - clone_source(url, ref_name.as_deref(), &sparse_paths, &staged_root)?; + stage_marketplace_source(&source, &sparse_paths, &staged_root, clone_source)?; let marketplace_name = validate_marketplace_source_root(&staged_root)?; if marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME { @@ -214,6 +214,36 @@ mod tests { Ok(()) } + #[test] + fn add_marketplace_sync_installs_local_directory_source_and_updates_config() -> Result<()> { + let codex_home = TempDir::new()?; + let source_root = TempDir::new()?; + write_marketplace_source(source_root.path(), "local copy")?; + + let result = add_marketplace_sync_with_cloner( + codex_home.path(), + MarketplaceAddRequest { + source: source_root.path().display().to_string(), + ref_name: None, + sparse_paths: Vec::new(), + }, + |_url, _ref_name, _sparse_paths, _destination| { + panic!("git cloner should not be called for local marketplace sources") + }, + )?; + + let expected_source = source_root.path().canonicalize()?.display().to_string(); + assert_eq!(result.marketplace_name, "debug"); + assert_eq!(result.source_display, expected_source); + assert!(!result.already_added); + + let config = fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE))?; + assert!(config.contains("[marketplaces.debug]")); + assert!(config.contains("source_type = \"path\"")); + assert!(config.contains(&format!("source = \"{expected_source}\""))); + Ok(()) + } + fn write_marketplace_source(source: &Path, marker: &str) -> std::io::Result<()> { fs::create_dir_all(source.join(".agents/plugins"))?; fs::create_dir_all(source.join("plugins/sample/.codex-plugin"))?; diff --git a/codex-rs/core/src/plugins/marketplace_add/metadata.rs b/codex-rs/core/src/plugins/marketplace_add/metadata.rs index 9ee27a9df57c..2fd8dfa76c69 100644 --- a/codex-rs/core/src/plugins/marketplace_add/metadata.rs +++ b/codex-rs/core/src/plugins/marketplace_add/metadata.rs @@ -23,6 +23,12 @@ enum InstalledMarketplaceSource { ref_name: Option, sparse_paths: Vec, }, + Path { + path: String, + }, + ManifestUrl { + url: String, + }, } pub(super) fn record_added_marketplace_entry( @@ -94,6 +100,12 @@ impl MarketplaceInstallMetadata { ref_name: ref_name.clone(), sparse_paths: sparse_paths.to_vec(), }, + MarketplaceSource::Path { path } => InstalledMarketplaceSource::Path { + path: path.display().to_string(), + }, + MarketplaceSource::ManifestUrl { url } => { + InstalledMarketplaceSource::ManifestUrl { url: url.clone() } + } }; Self { source } } @@ -101,24 +113,32 @@ impl MarketplaceInstallMetadata { fn config_source_type(&self) -> &'static str { match &self.source { InstalledMarketplaceSource::Git { .. } => "git", + InstalledMarketplaceSource::Path { .. } => "path", + InstalledMarketplaceSource::ManifestUrl { .. } => "manifest_url", } } fn config_source(&self) -> String { match &self.source { InstalledMarketplaceSource::Git { url, .. } => url.clone(), + InstalledMarketplaceSource::Path { path } => path.clone(), + InstalledMarketplaceSource::ManifestUrl { url } => url.clone(), } } fn ref_name(&self) -> Option<&str> { match &self.source { InstalledMarketplaceSource::Git { ref_name, .. } => ref_name.as_deref(), + InstalledMarketplaceSource::Path { .. } + | InstalledMarketplaceSource::ManifestUrl { .. } => None, } } fn sparse_paths(&self) -> &[String] { match &self.source { InstalledMarketplaceSource::Git { sparse_paths, .. } => sparse_paths, + InstalledMarketplaceSource::Path { .. } + | InstalledMarketplaceSource::ManifestUrl { .. } => &[], } } diff --git a/codex-rs/core/src/plugins/marketplace_add/source.rs b/codex-rs/core/src/plugins/marketplace_add/source.rs index bb3d746a01d3..15dc841de952 100644 --- a/codex-rs/core/src/plugins/marketplace_add/source.rs +++ b/codex-rs/core/src/plugins/marketplace_add/source.rs @@ -1,7 +1,9 @@ use super::MarketplaceAddError; use crate::plugins::validate_marketplace_root; use crate::plugins::validate_plugin_segment; +use std::fs; use std::path::Path; +use std::path::PathBuf; #[derive(Debug, Clone, PartialEq, Eq)] pub(super) enum MarketplaceSource { @@ -9,6 +11,12 @@ pub(super) enum MarketplaceSource { url: String, ref_name: Option, }, + Path { + path: PathBuf, + }, + ManifestUrl { + url: String, + }, } pub(super) fn parse_marketplace_source( @@ -26,9 +34,23 @@ pub(super) fn parse_marketplace_source( let ref_name = explicit_ref.or(parsed_ref); if looks_like_local_path(&base_source) { - return Err(MarketplaceAddError::InvalidRequest( - "local marketplace sources are not supported yet; use an HTTP(S) Git URL, SSH Git URL, or GitHub owner/repo".to_string(), - )); + if ref_name.is_some() { + return Err(MarketplaceAddError::InvalidRequest( + "--ref is only supported for git marketplace sources".to_string(), + )); + } + return Ok(MarketplaceSource::Path { + path: resolve_local_source_path(&base_source)?, + }); + } + + if looks_like_manifest_url(&base_source) { + if ref_name.is_some() { + return Err(MarketplaceAddError::InvalidRequest( + "--ref is only supported for git marketplace sources".to_string(), + )); + } + return Ok(MarketplaceSource::ManifestUrl { url: base_source }); } if is_ssh_git_url(&base_source) || is_git_url(&base_source) { @@ -50,6 +72,30 @@ pub(super) fn parse_marketplace_source( ))) } +pub(super) fn stage_marketplace_source( + source: &MarketplaceSource, + sparse_paths: &[String], + staged_root: &Path, + clone_source: F, +) -> Result<(), MarketplaceAddError> +where + F: Fn(&str, Option<&str>, &[String], &Path) -> Result<(), MarketplaceAddError>, +{ + if !sparse_paths.is_empty() && !matches!(source, MarketplaceSource::Git { .. }) { + return Err(MarketplaceAddError::InvalidRequest( + "--sparse is only supported for git marketplace sources".to_string(), + )); + } + + match source { + MarketplaceSource::Git { url, ref_name } => { + clone_source(url, ref_name.as_deref(), sparse_paths, staged_root) + } + MarketplaceSource::Path { path } => stage_local_source(path, staged_root), + MarketplaceSource::ManifestUrl { url } => download_manifest_url_source(url, staged_root), + } +} + pub(super) fn validate_marketplace_source_root(root: &Path) -> Result { let marketplace_name = validate_marketplace_root(root) .map_err(|err| MarketplaceAddError::InvalidRequest(err.to_string()))?; @@ -94,6 +140,208 @@ fn looks_like_local_path(source: &str) -> bool { || source == ".." } +fn looks_like_manifest_url(source: &str) -> bool { + if !is_git_url(source) { + return false; + } + + let without_query = source.split('?').next().unwrap_or(source); + without_query + .trim_end_matches('/') + .ends_with("marketplace.json") +} + +fn resolve_local_source_path(source: &str) -> Result { + let path = expand_tilde_path(source); + let path = if path.is_absolute() { + path + } else { + std::env::current_dir() + .map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to read current working directory for local marketplace source: {err}" + )) + })? + .join(path) + }; + + path.canonicalize().map_err(|err| { + MarketplaceAddError::InvalidRequest(format!( + "failed to resolve local marketplace source {}: {err}", + path.display() + )) + }) +} + +fn expand_tilde_path(source: &str) -> PathBuf { + let Some(rest) = source.strip_prefix("~/") else { + return PathBuf::from(source); + }; + let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) else { + return PathBuf::from(source); + }; + PathBuf::from(home).join(rest) +} + +fn stage_local_source(source_path: &Path, staged_root: &Path) -> Result<(), MarketplaceAddError> { + let metadata = fs::metadata(source_path).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to read local marketplace source metadata {}: {err}", + source_path.display() + )) + })?; + + if metadata.is_dir() { + copy_dir_recursive(source_path, staged_root)?; + return Ok(()); + } + + if !metadata.is_file() { + return Err(MarketplaceAddError::InvalidRequest(format!( + "local marketplace source must be a file or directory: {}", + source_path.display() + ))); + } + + if let Some(marketplace_root) = marketplace_root_for_manifest_path(source_path) { + copy_dir_recursive(marketplace_root, staged_root)?; + return Ok(()); + } + + let staged_manifest_path = staged_root.join(".agents/plugins/marketplace.json"); + if let Some(parent) = staged_manifest_path.parent() { + fs::create_dir_all(parent).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to create marketplace staging directory {}: {err}", + parent.display() + )) + })?; + } + fs::copy(source_path, &staged_manifest_path).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to copy local marketplace manifest {} to {}: {err}", + source_path.display(), + staged_manifest_path.display() + )) + })?; + + Ok(()) +} + +fn marketplace_root_for_manifest_path(path: &Path) -> Option<&Path> { + let plugins_dir = path.parent()?; + let dot_agents_dir = plugins_dir.parent()?; + let marketplace_root = dot_agents_dir.parent()?; + + (path.file_name().and_then(|name| name.to_str()) == Some("marketplace.json") + && plugins_dir.file_name().and_then(|name| name.to_str()) == Some("plugins") + && dot_agents_dir.file_name().and_then(|name| name.to_str()) == Some(".agents")) + .then_some(marketplace_root) +} + +fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), MarketplaceAddError> { + fs::create_dir_all(target).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to create local marketplace target directory {}: {err}", + target.display() + )) + })?; + + for entry in fs::read_dir(source).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to read local marketplace directory {}: {err}", + source.display() + )) + })? { + let entry = entry.map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to read local marketplace entry in {}: {err}", + source.display() + )) + })?; + let source_path = entry.path(); + let target_path = target.join(entry.file_name()); + let file_type = entry.file_type().map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to read file type for local marketplace entry {}: {err}", + source_path.display() + )) + })?; + + if file_type.is_dir() { + copy_dir_recursive(&source_path, &target_path)?; + } else if file_type.is_file() { + if let Some(parent) = target_path.parent() { + fs::create_dir_all(parent).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to create local marketplace target directory {}: {err}", + parent.display() + )) + })?; + } + fs::copy(&source_path, &target_path).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to copy local marketplace file {} to {}: {err}", + source_path.display(), + target_path.display() + )) + })?; + } + } + + Ok(()) +} + +fn download_manifest_url_source(url: &str, staged_root: &Path) -> Result<(), MarketplaceAddError> { + let contents = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to create runtime for marketplace manifest download: {err}" + )) + })? + .block_on(async { + let response = reqwest::Client::new() + .get(url) + .send() + .await + .map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to download marketplace manifest from {url}: {err}" + )) + })?; + let response = response.error_for_status().map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to download marketplace manifest from {url}: {err}" + )) + })?; + response.bytes().await.map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to read downloaded marketplace manifest from {url}: {err}" + )) + }) + })?; + + let staged_manifest_path = staged_root.join(".agents/plugins/marketplace.json"); + if let Some(parent) = staged_manifest_path.parent() { + fs::create_dir_all(parent).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to create marketplace staging directory {}: {err}", + parent.display() + )) + })?; + } + fs::write(&staged_manifest_path, contents).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to write downloaded marketplace manifest to {}: {err}", + staged_manifest_path.display() + )) + })?; + + Ok(()) +} + fn is_ssh_git_url(source: &str) -> bool { source.starts_with("ssh://") || source.starts_with("git@") && source.contains(':') } @@ -126,6 +374,8 @@ impl MarketplaceSource { Some(ref_name) => format!("{url}#{ref_name}"), None => url.clone(), }, + Self::Path { path } => path.display().to_string(), + Self::ManifestUrl { url } => url.clone(), } } } @@ -229,12 +479,55 @@ mod tests { } #[test] - fn parse_marketplace_source_rejects_local_directory_source() { - let err = parse_marketplace_source("./marketplace", /*explicit_ref*/ None).unwrap_err(); + fn local_path_source_parses() { + let source = parse_marketplace_source(".", /*explicit_ref*/ None).unwrap(); + let MarketplaceSource::Path { path } = source else { + panic!("expected local path source"); + }; + assert!(path.is_absolute()); + } + + #[test] + fn manifest_url_source_parses() { assert_eq!( - err.to_string(), - "local marketplace sources are not supported yet; use an HTTP(S) Git URL, SSH Git URL, or GitHub owner/repo" + parse_marketplace_source( + "https://example.com/.agents/plugins/marketplace.json", + /*explicit_ref*/ None, + ) + .unwrap(), + MarketplaceSource::ManifestUrl { + url: "https://example.com/.agents/plugins/marketplace.json".to_string(), + } + ); + } + + #[test] + fn non_git_sources_reject_ref_override() { + let err = parse_marketplace_source("./marketplace", Some("main".to_string())).unwrap_err(); + + assert!( + err.to_string() + .contains("--ref is only supported for git marketplace sources"), + "unexpected error: {err}" + ); + } + + #[test] + fn non_git_sources_reject_sparse_checkout() { + let path = std::env::current_dir().unwrap(); + let err = stage_marketplace_source( + &MarketplaceSource::Path { path }, + &["plugins/foo".to_string()], + Path::new("/tmp"), + |_url, _ref_name, _sparse_paths, _staged_root| Ok(()), + ) + .unwrap_err(); + + assert!( + err.to_string() + .contains("--sparse is only supported for git marketplace sources"), + "unexpected error: {err}" ); } From 8a10bd960ec9c3c5fcbb74b20983e63726c1f13f Mon Sep 17 00:00:00 2001 From: xli-oai Date: Tue, 14 Apr 2026 11:05:06 -0700 Subject: [PATCH 2/8] Remove manifest-based marketplace add sources --- codex-rs/cli/src/marketplace_cmd.rs | 2 +- codex-rs/cli/tests/marketplace_add.rs | 62 ++------ codex-rs/config/src/types.rs | 1 - codex-rs/core/config.schema.json | 3 +- .../src/plugins/marketplace_add/metadata.rs | 14 +- .../src/plugins/marketplace_add/source.rs | 134 +----------------- 6 files changed, 17 insertions(+), 199 deletions(-) diff --git a/codex-rs/cli/src/marketplace_cmd.rs b/codex-rs/cli/src/marketplace_cmd.rs index 95d76b91fbf9..cde50a2bf4ae 100644 --- a/codex-rs/cli/src/marketplace_cmd.rs +++ b/codex-rs/cli/src/marketplace_cmd.rs @@ -24,7 +24,7 @@ enum MarketplaceSubcommand { #[derive(Debug, Parser)] struct AddMarketplaceArgs { /// Marketplace source. Supports owner/repo[@ref], HTTP(S) Git URLs, SSH URLs, - /// local filesystem paths, or direct marketplace.json URLs. + /// or local marketplace root directories. source: String, /// Git ref to check out. Overrides any @ref or #ref suffix in SOURCE. diff --git a/codex-rs/cli/tests/marketplace_add.rs b/codex-rs/cli/tests/marketplace_add.rs index e79e6c06161f..cc5ac0e487a5 100644 --- a/codex-rs/cli/tests/marketplace_add.rs +++ b/codex-rs/cli/tests/marketplace_add.rs @@ -1,12 +1,9 @@ use anyhow::Result; use codex_config::CONFIG_TOML_FILE; use codex_core::plugins::marketplace_install_root; +use predicates::str::contains; use pretty_assertions::assert_eq; -use std::io::Read; -use std::io::Write; -use std::net::TcpListener; use std::path::Path; -use std::thread; use tempfile::TempDir; fn codex_command(codex_home: &Path) -> Result { @@ -76,61 +73,20 @@ async fn marketplace_add_supports_local_directory_source() -> Result<()> { Ok(()) } -fn spawn_manifest_server(body: String) -> Result<(u16, thread::JoinHandle>)> { - let listener = TcpListener::bind("127.0.0.1:0")?; - let port = listener.local_addr()?.port(); - Ok(( - port, - thread::spawn(move || { - let (mut stream, _addr) = listener.accept()?; - let mut request = [0_u8; 2048]; - let _ = stream.read(&mut request)?; - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), - body - ); - stream.write_all(response.as_bytes())?; - Ok(()) - }), - )) -} - #[tokio::test] -async fn marketplace_add_supports_manifest_url_source() -> Result<()> { +async fn marketplace_add_rejects_local_manifest_file_source() -> Result<()> { let codex_home = TempDir::new()?; let source = TempDir::new()?; - std::fs::create_dir_all(source.path().join(".agents/plugins"))?; - std::fs::write( - source.path().join(".agents/plugins/marketplace.json"), - r#"{"name":"debug-url","plugins":[]}"#, - )?; - let (port, server) = spawn_manifest_server(r#"{"name":"debug-url","plugins":[]}"#.to_string())?; - let url = format!("http://127.0.0.1:{port}/.agents/plugins/marketplace.json"); + write_marketplace_source(source.path(), "local ref")?; + let manifest_path = source.path().join(".agents/plugins/marketplace.json"); codex_command(codex_home.path())? - .args(["marketplace", "add", &url]) + .args(["marketplace", "add", manifest_path.to_str().unwrap()]) .assert() - .success(); - - let installed_root = marketplace_install_root(codex_home.path()).join("debug-url"); - assert!( - installed_root - .join(".agents/plugins/marketplace.json") - .is_file() - ); - - let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?; - let config: toml::Value = toml::from_str(&config)?; - assert_eq!( - config["marketplaces"]["debug-url"]["source_type"].as_str(), - Some("manifest_url") - ); - assert_eq!( - config["marketplaces"]["debug-url"]["source"].as_str(), - Some(url.as_str()) - ); - server.join().unwrap()?; + .failure() + .stderr(contains( + "local marketplace source must be a directory, not a file", + )); Ok(()) } diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index ff21ceb6f228..3e0e6783e611 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -633,7 +633,6 @@ pub struct MarketplaceConfig { pub enum MarketplaceSourceType { Git, Path, - ManifestUrl, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index e50a64aee727..3a5e0cda3d43 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -811,8 +811,7 @@ "MarketplaceSourceType": { "enum": [ "git", - "path", - "manifest_url" + "path" ], "type": "string" }, diff --git a/codex-rs/core/src/plugins/marketplace_add/metadata.rs b/codex-rs/core/src/plugins/marketplace_add/metadata.rs index 2fd8dfa76c69..e7c1f6446c3d 100644 --- a/codex-rs/core/src/plugins/marketplace_add/metadata.rs +++ b/codex-rs/core/src/plugins/marketplace_add/metadata.rs @@ -26,9 +26,6 @@ enum InstalledMarketplaceSource { Path { path: String, }, - ManifestUrl { - url: String, - }, } pub(super) fn record_added_marketplace_entry( @@ -103,9 +100,6 @@ impl MarketplaceInstallMetadata { MarketplaceSource::Path { path } => InstalledMarketplaceSource::Path { path: path.display().to_string(), }, - MarketplaceSource::ManifestUrl { url } => { - InstalledMarketplaceSource::ManifestUrl { url: url.clone() } - } }; Self { source } } @@ -114,7 +108,6 @@ impl MarketplaceInstallMetadata { match &self.source { InstalledMarketplaceSource::Git { .. } => "git", InstalledMarketplaceSource::Path { .. } => "path", - InstalledMarketplaceSource::ManifestUrl { .. } => "manifest_url", } } @@ -122,23 +115,20 @@ impl MarketplaceInstallMetadata { match &self.source { InstalledMarketplaceSource::Git { url, .. } => url.clone(), InstalledMarketplaceSource::Path { path } => path.clone(), - InstalledMarketplaceSource::ManifestUrl { url } => url.clone(), } } fn ref_name(&self) -> Option<&str> { match &self.source { InstalledMarketplaceSource::Git { ref_name, .. } => ref_name.as_deref(), - InstalledMarketplaceSource::Path { .. } - | InstalledMarketplaceSource::ManifestUrl { .. } => None, + InstalledMarketplaceSource::Path { .. } => None, } } fn sparse_paths(&self) -> &[String] { match &self.source { InstalledMarketplaceSource::Git { sparse_paths, .. } => sparse_paths, - InstalledMarketplaceSource::Path { .. } - | InstalledMarketplaceSource::ManifestUrl { .. } => &[], + InstalledMarketplaceSource::Path { .. } => &[], } } diff --git a/codex-rs/core/src/plugins/marketplace_add/source.rs b/codex-rs/core/src/plugins/marketplace_add/source.rs index 15dc841de952..c31f78f93d1c 100644 --- a/codex-rs/core/src/plugins/marketplace_add/source.rs +++ b/codex-rs/core/src/plugins/marketplace_add/source.rs @@ -14,9 +14,6 @@ pub(super) enum MarketplaceSource { Path { path: PathBuf, }, - ManifestUrl { - url: String, - }, } pub(super) fn parse_marketplace_source( @@ -44,15 +41,6 @@ pub(super) fn parse_marketplace_source( }); } - if looks_like_manifest_url(&base_source) { - if ref_name.is_some() { - return Err(MarketplaceAddError::InvalidRequest( - "--ref is only supported for git marketplace sources".to_string(), - )); - } - return Ok(MarketplaceSource::ManifestUrl { url: base_source }); - } - if is_ssh_git_url(&base_source) || is_git_url(&base_source) { return Ok(MarketplaceSource::Git { url: normalize_git_url(&base_source), @@ -92,7 +80,6 @@ where clone_source(url, ref_name.as_deref(), sparse_paths, staged_root) } MarketplaceSource::Path { path } => stage_local_source(path, staged_root), - MarketplaceSource::ManifestUrl { url } => download_manifest_url_source(url, staged_root), } } @@ -140,17 +127,6 @@ fn looks_like_local_path(source: &str) -> bool { || source == ".." } -fn looks_like_manifest_url(source: &str) -> bool { - if !is_git_url(source) { - return false; - } - - let without_query = source.split('?').next().unwrap_or(source); - without_query - .trim_end_matches('/') - .ends_with("marketplace.json") -} - fn resolve_local_source_path(source: &str) -> Result { let path = expand_tilde_path(source); let path = if path.is_absolute() { @@ -196,47 +172,10 @@ fn stage_local_source(source_path: &Path, staged_root: &Path) -> Result<(), Mark return Ok(()); } - if !metadata.is_file() { - return Err(MarketplaceAddError::InvalidRequest(format!( - "local marketplace source must be a file or directory: {}", - source_path.display() - ))); - } - - if let Some(marketplace_root) = marketplace_root_for_manifest_path(source_path) { - copy_dir_recursive(marketplace_root, staged_root)?; - return Ok(()); - } - - let staged_manifest_path = staged_root.join(".agents/plugins/marketplace.json"); - if let Some(parent) = staged_manifest_path.parent() { - fs::create_dir_all(parent).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to create marketplace staging directory {}: {err}", - parent.display() - )) - })?; - } - fs::copy(source_path, &staged_manifest_path).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to copy local marketplace manifest {} to {}: {err}", - source_path.display(), - staged_manifest_path.display() - )) - })?; - - Ok(()) -} - -fn marketplace_root_for_manifest_path(path: &Path) -> Option<&Path> { - let plugins_dir = path.parent()?; - let dot_agents_dir = plugins_dir.parent()?; - let marketplace_root = dot_agents_dir.parent()?; - - (path.file_name().and_then(|name| name.to_str()) == Some("marketplace.json") - && plugins_dir.file_name().and_then(|name| name.to_str()) == Some("plugins") - && dot_agents_dir.file_name().and_then(|name| name.to_str()) == Some(".agents")) - .then_some(marketplace_root) + Err(MarketplaceAddError::InvalidRequest(format!( + "local marketplace source must be a directory, not a file: {}", + source_path.display() + ))) } fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), MarketplaceAddError> { @@ -292,56 +231,6 @@ fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), MarketplaceAdd Ok(()) } -fn download_manifest_url_source(url: &str, staged_root: &Path) -> Result<(), MarketplaceAddError> { - let contents = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to create runtime for marketplace manifest download: {err}" - )) - })? - .block_on(async { - let response = reqwest::Client::new() - .get(url) - .send() - .await - .map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to download marketplace manifest from {url}: {err}" - )) - })?; - let response = response.error_for_status().map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to download marketplace manifest from {url}: {err}" - )) - })?; - response.bytes().await.map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to read downloaded marketplace manifest from {url}: {err}" - )) - }) - })?; - - let staged_manifest_path = staged_root.join(".agents/plugins/marketplace.json"); - if let Some(parent) = staged_manifest_path.parent() { - fs::create_dir_all(parent).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to create marketplace staging directory {}: {err}", - parent.display() - )) - })?; - } - fs::write(&staged_manifest_path, contents).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to write downloaded marketplace manifest to {}: {err}", - staged_manifest_path.display() - )) - })?; - - Ok(()) -} - fn is_ssh_git_url(source: &str) -> bool { source.starts_with("ssh://") || source.starts_with("git@") && source.contains(':') } @@ -375,7 +264,6 @@ impl MarketplaceSource { None => url.clone(), }, Self::Path { path } => path.display().to_string(), - Self::ManifestUrl { url } => url.clone(), } } } @@ -488,20 +376,6 @@ mod tests { assert!(path.is_absolute()); } - #[test] - fn manifest_url_source_parses() { - assert_eq!( - parse_marketplace_source( - "https://example.com/.agents/plugins/marketplace.json", - /*explicit_ref*/ None, - ) - .unwrap(), - MarketplaceSource::ManifestUrl { - url: "https://example.com/.agents/plugins/marketplace.json".to_string(), - } - ); - } - #[test] fn non_git_sources_reject_ref_override() { let err = parse_marketplace_source("./marketplace", Some("main".to_string())).unwrap_err(); From 6d76aa00c33ced983ca5569ae606bc9b34f189e4 Mon Sep 17 00:00:00 2001 From: xli-oai Date: Tue, 14 Apr 2026 13:49:16 -0700 Subject: [PATCH 3/8] Use local marketplace roots without copying --- codex-rs/cli/tests/marketplace_add.rs | 5 +- .../src/plugins/installed_marketplaces.rs | 18 ++- codex-rs/core/src/plugins/marketplace_add.rs | 69 ++++++++++++ .../src/plugins/marketplace_add/metadata.rs | 96 +++++++++++++++- .../src/plugins/marketplace_add/source.rs | 104 +++++------------- 5 files changed, 209 insertions(+), 83 deletions(-) diff --git a/codex-rs/cli/tests/marketplace_add.rs b/codex-rs/cli/tests/marketplace_add.rs index cc5ac0e487a5..93c908c08faa 100644 --- a/codex-rs/cli/tests/marketplace_add.rs +++ b/codex-rs/cli/tests/marketplace_add.rs @@ -53,10 +53,7 @@ async fn marketplace_add_supports_local_directory_source() -> Result<()> { .success(); let installed_root = marketplace_install_root(codex_home.path()).join("debug"); - assert_eq!( - std::fs::read_to_string(installed_root.join("plugins/sample/marker.txt"))?, - "local ref" - ); + assert!(!installed_root.exists()); let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?; let config: toml::Value = toml::from_str(&config)?; diff --git a/codex-rs/core/src/plugins/installed_marketplaces.rs b/codex-rs/core/src/plugins/installed_marketplaces.rs index edfc7d895aa9..8bc244fdf50a 100644 --- a/codex-rs/core/src/plugins/installed_marketplaces.rs +++ b/codex-rs/core/src/plugins/installed_marketplaces.rs @@ -45,7 +45,8 @@ pub(crate) fn installed_marketplace_roots_from_config( ); return None; } - let path = default_install_root.join(marketplace_name); + let path = + configured_marketplace_root(marketplace_name, marketplace, &default_install_root)?; path.join(".agents/plugins/marketplace.json") .is_file() .then_some(path) @@ -55,3 +56,18 @@ pub(crate) fn installed_marketplace_roots_from_config( roots.sort_unstable_by(|left, right| left.as_path().cmp(right.as_path())); roots } + +fn configured_marketplace_root( + marketplace_name: &str, + marketplace: &toml::Value, + default_install_root: &Path, +) -> Option { + match marketplace.get("source_type").and_then(toml::Value::as_str) { + Some("path") => marketplace + .get("source") + .and_then(toml::Value::as_str) + .filter(|source| !source.is_empty()) + .map(PathBuf::from), + _ => Some(default_install_root.join(marketplace_name)), + } +} diff --git a/codex-rs/core/src/plugins/marketplace_add.rs b/codex-rs/core/src/plugins/marketplace_add.rs index fdf70f826ef6..f77d8313ba1f 100644 --- a/codex-rs/core/src/plugins/marketplace_add.rs +++ b/codex-rs/core/src/plugins/marketplace_add.rs @@ -16,6 +16,7 @@ use install::marketplace_staging_root; use install::replace_marketplace_root; use install::safe_marketplace_dir_name; use metadata::MarketplaceInstallMetadata; +use metadata::installed_marketplace_root_for_name; use metadata::installed_marketplace_root_for_source; use metadata::record_added_marketplace_entry; use source::MarketplaceSource; @@ -103,6 +104,35 @@ where }); } + if let MarketplaceSource::Path { path } = &source { + let marketplace_name = validate_marketplace_source_root(path)?; + if marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME { + return Err(MarketplaceAddError::InvalidRequest(format!( + "marketplace '{OPENAI_CURATED_MARKETPLACE_NAME}' is reserved and cannot be added from {}", + source.display() + ))); + } + if installed_marketplace_root_for_name(codex_home, &install_root, &marketplace_name)? + .is_some() + { + return Err(MarketplaceAddError::InvalidRequest(format!( + "marketplace '{marketplace_name}' is already added from a different source; remove it before adding {}", + source.display() + ))); + } + record_added_marketplace_entry(codex_home, &marketplace_name, &install_metadata)?; + return Ok(MarketplaceAddOutcome { + marketplace_name, + source_display: source.display(), + installed_root: AbsolutePathBuf::try_from(path.clone()).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to resolve installed marketplace root: {err}" + )) + })?, + already_added: false, + }); + } + let staging_root = marketplace_staging_root(&install_root); fs::create_dir_all(&staging_root).map_err(|err| { MarketplaceAddError::Internal(format!( @@ -235,7 +265,16 @@ mod tests { let expected_source = source_root.path().canonicalize()?.display().to_string(); assert_eq!(result.marketplace_name, "debug"); assert_eq!(result.source_display, expected_source); + assert_eq!( + result.installed_root.as_path(), + source_root.path().canonicalize()? + ); assert!(!result.already_added); + assert!( + !marketplace_install_root(codex_home.path()) + .join("debug") + .exists() + ); let config = fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE))?; assert!(config.contains("[marketplaces.debug]")); @@ -244,6 +283,36 @@ mod tests { Ok(()) } + #[test] + fn add_marketplace_sync_treats_existing_local_directory_source_as_already_added() -> Result<()> + { + let codex_home = TempDir::new()?; + let source_root = TempDir::new()?; + write_marketplace_source(source_root.path(), "local copy")?; + + let request = MarketplaceAddRequest { + source: source_root.path().display().to_string(), + ref_name: None, + sparse_paths: Vec::new(), + }; + let first_result = add_marketplace_sync_with_cloner(codex_home.path(), request.clone(), { + |_url, _ref_name, _sparse_paths, _destination| { + panic!("git cloner should not be called for local marketplace sources") + } + })?; + let second_result = add_marketplace_sync_with_cloner(codex_home.path(), request, { + |_url, _ref_name, _sparse_paths, _destination| { + panic!("git cloner should not be called for local marketplace sources") + } + })?; + + assert!(!first_result.already_added); + assert!(second_result.already_added); + assert_eq!(second_result.installed_root, first_result.installed_root); + + Ok(()) + } + fn write_marketplace_source(source: &Path, marker: &str) -> std::io::Result<()> { fs::create_dir_all(source.join(".agents/plugins"))?; fs::create_dir_all(source.join("plugins/sample/.codex-plugin"))?; diff --git a/codex-rs/core/src/plugins/marketplace_add/metadata.rs b/codex-rs/core/src/plugins/marketplace_add/metadata.rs index e7c1f6446c3d..7e57ed1905a5 100644 --- a/codex-rs/core/src/plugins/marketplace_add/metadata.rs +++ b/codex-rs/core/src/plugins/marketplace_add/metadata.rs @@ -80,7 +80,10 @@ pub(super) fn installed_marketplace_root_for_source( if !install_metadata.matches_config(marketplace) { continue; } - let root = install_root.join(marketplace_name); + let Some(root) = configured_marketplace_root(marketplace_name, marketplace, install_root) + else { + continue; + }; if validate_marketplace_root(&root).is_ok() { return Ok(Some(root)); } @@ -89,6 +92,47 @@ pub(super) fn installed_marketplace_root_for_source( Ok(None) } +pub(super) fn installed_marketplace_root_for_name( + codex_home: &Path, + install_root: &Path, + marketplace_name: &str, +) -> Result, MarketplaceAddError> { + let config_path = codex_home.join(CONFIG_TOML_FILE); + let config = match fs::read_to_string(&config_path) { + Ok(config) => config, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), + Err(err) => { + return Err(MarketplaceAddError::Internal(format!( + "failed to read user config {}: {err}", + config_path.display() + ))); + } + }; + let config: toml::Value = toml::from_str(&config).map_err(|err| { + MarketplaceAddError::Internal(format!( + "failed to parse user config {}: {err}", + config_path.display() + )) + })?; + let Some(marketplace) = config + .get("marketplaces") + .and_then(toml::Value::as_table) + .and_then(|marketplaces| marketplaces.get(marketplace_name)) + else { + return Ok(None); + }; + + let Some(root) = configured_marketplace_root(marketplace_name, marketplace, install_root) + else { + return Ok(None); + }; + if validate_marketplace_root(&root).is_ok() { + Ok(Some(root)) + } else { + Ok(None) + } +} + impl MarketplaceInstallMetadata { pub(super) fn from_source(source: &MarketplaceSource, sparse_paths: &[String]) -> Self { let source = match source { @@ -156,6 +200,21 @@ fn config_sparse_paths(marketplace: &toml::Value) -> Vec { .unwrap_or_default() } +fn configured_marketplace_root( + marketplace_name: &str, + marketplace: &toml::Value, + install_root: &Path, +) -> Option { + match marketplace.get("source_type").and_then(toml::Value::as_str) { + Some("path") => marketplace + .get("source") + .and_then(toml::Value::as_str) + .filter(|source| !source.is_empty()) + .map(PathBuf::from), + _ => Some(install_root.join(marketplace_name)), + } +} + fn utc_timestamp_now() -> Result { let duration = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -237,4 +296,39 @@ mod tests { "unexpected error: {err}" ); } + + #[test] + fn installed_marketplace_root_for_source_uses_path_source_root() { + let codex_home = TempDir::new().unwrap(); + let install_root = codex_home.path().join("marketplaces"); + let source_root = codex_home.path().join("source"); + fs::create_dir_all(source_root.join(".agents/plugins")).unwrap(); + fs::write( + source_root.join(".agents/plugins/marketplace.json"), + r#"{"name":"debug","plugins":[]}"#, + ) + .unwrap(); + fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + format!( + "[marketplaces.debug]\nsource_type = \"path\"\nsource = \"{}\"\n", + source_root.display() + ), + ) + .unwrap(); + + let source = MarketplaceSource::Path { + path: source_root.clone(), + }; + let install_metadata = MarketplaceInstallMetadata::from_source(&source, &[]); + + let root = installed_marketplace_root_for_source( + codex_home.path(), + &install_root, + &install_metadata, + ) + .unwrap(); + + assert_eq!(root, Some(source_root)); + } } diff --git a/codex-rs/core/src/plugins/marketplace_add/source.rs b/codex-rs/core/src/plugins/marketplace_add/source.rs index c31f78f93d1c..9e2d55e1f2e5 100644 --- a/codex-rs/core/src/plugins/marketplace_add/source.rs +++ b/codex-rs/core/src/plugins/marketplace_add/source.rs @@ -1,7 +1,6 @@ use super::MarketplaceAddError; use crate::plugins::validate_marketplace_root; use crate::plugins::validate_plugin_segment; -use std::fs; use std::path::Path; use std::path::PathBuf; @@ -36,9 +35,13 @@ pub(super) fn parse_marketplace_source( "--ref is only supported for git marketplace sources".to_string(), )); } - return Ok(MarketplaceSource::Path { - path: resolve_local_source_path(&base_source)?, - }); + let path = resolve_local_source_path(&base_source)?; + if path.is_file() { + return Err(MarketplaceAddError::InvalidRequest( + "local marketplace source must be a directory, not a file".to_string(), + )); + } + return Ok(MarketplaceSource::Path { path }); } if is_ssh_git_url(&base_source) || is_git_url(&base_source) { @@ -79,7 +82,9 @@ where MarketplaceSource::Git { url, ref_name } => { clone_source(url, ref_name.as_deref(), sparse_paths, staged_root) } - MarketplaceSource::Path { path } => stage_local_source(path, staged_root), + MarketplaceSource::Path { .. } => unreachable!( + "local marketplace sources are added without staging a copied install root" + ), } } @@ -159,78 +164,6 @@ fn expand_tilde_path(source: &str) -> PathBuf { PathBuf::from(home).join(rest) } -fn stage_local_source(source_path: &Path, staged_root: &Path) -> Result<(), MarketplaceAddError> { - let metadata = fs::metadata(source_path).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to read local marketplace source metadata {}: {err}", - source_path.display() - )) - })?; - - if metadata.is_dir() { - copy_dir_recursive(source_path, staged_root)?; - return Ok(()); - } - - Err(MarketplaceAddError::InvalidRequest(format!( - "local marketplace source must be a directory, not a file: {}", - source_path.display() - ))) -} - -fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), MarketplaceAddError> { - fs::create_dir_all(target).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to create local marketplace target directory {}: {err}", - target.display() - )) - })?; - - for entry in fs::read_dir(source).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to read local marketplace directory {}: {err}", - source.display() - )) - })? { - let entry = entry.map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to read local marketplace entry in {}: {err}", - source.display() - )) - })?; - let source_path = entry.path(); - let target_path = target.join(entry.file_name()); - let file_type = entry.file_type().map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to read file type for local marketplace entry {}: {err}", - source_path.display() - )) - })?; - - if file_type.is_dir() { - copy_dir_recursive(&source_path, &target_path)?; - } else if file_type.is_file() { - if let Some(parent) = target_path.parent() { - fs::create_dir_all(parent).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to create local marketplace target directory {}: {err}", - parent.display() - )) - })?; - } - fs::copy(&source_path, &target_path).map_err(|err| { - MarketplaceAddError::Internal(format!( - "failed to copy local marketplace file {} to {}: {err}", - source_path.display(), - target_path.display() - )) - })?; - } - } - - Ok(()) -} - fn is_ssh_git_url(source: &str) -> bool { source.starts_with("ssh://") || source.starts_with("git@") && source.contains(':') } @@ -272,6 +205,7 @@ impl MarketplaceSource { mod tests { use super::*; use pretty_assertions::assert_eq; + use tempfile::TempDir; #[test] fn github_shorthand_parses_ref_suffix() { @@ -376,6 +310,22 @@ mod tests { assert!(path.is_absolute()); } + #[test] + fn local_file_source_is_rejected() { + let tempdir = TempDir::new().unwrap(); + let file = tempdir.path().join("marketplace.json"); + std::fs::write(&file, "{}").unwrap(); + + let err = + parse_marketplace_source(file.to_str().unwrap(), /*explicit_ref*/ None).unwrap_err(); + + assert!( + err.to_string() + .contains("local marketplace source must be a directory, not a file"), + "unexpected error: {err}" + ); + } + #[test] fn non_git_sources_reject_ref_override() { let err = parse_marketplace_source("./marketplace", Some("main".to_string())).unwrap_err(); From d71cff4766405e479934e17d44c81b75c753010a Mon Sep 17 00:00:00 2001 From: xli-oai Date: Tue, 14 Apr 2026 14:02:56 -0700 Subject: [PATCH 4/8] Rename marketplace source_type path to local --- codex-rs/cli/tests/marketplace_add.rs | 2 +- codex-rs/config/src/types.rs | 3 ++- codex-rs/config/src/types_tests.rs | 14 ++++++++++++++ codex-rs/core/config.schema.json | 2 +- .../core/src/plugins/installed_marketplaces.rs | 2 +- codex-rs/core/src/plugins/marketplace_add.rs | 2 +- .../core/src/plugins/marketplace_add/metadata.rs | 6 +++--- 7 files changed, 23 insertions(+), 8 deletions(-) diff --git a/codex-rs/cli/tests/marketplace_add.rs b/codex-rs/cli/tests/marketplace_add.rs index 93c908c08faa..3b325da4f690 100644 --- a/codex-rs/cli/tests/marketplace_add.rs +++ b/codex-rs/cli/tests/marketplace_add.rs @@ -60,7 +60,7 @@ async fn marketplace_add_supports_local_directory_source() -> Result<()> { let expected_source = source.path().canonicalize()?.display().to_string(); assert_eq!( config["marketplaces"]["debug"]["source_type"].as_str(), - Some("path") + Some("local") ); assert_eq!( config["marketplaces"]["debug"]["source"].as_str(), diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 3e0e6783e611..559b383eab8e 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -632,7 +632,8 @@ pub struct MarketplaceConfig { #[serde(rename_all = "snake_case")] pub enum MarketplaceSourceType { Git, - Path, + #[serde(alias = "path")] + Local, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] diff --git a/codex-rs/config/src/types_tests.rs b/codex-rs/config/src/types_tests.rs index fbcd2bdc6411..9579bfc4504e 100644 --- a/codex-rs/config/src/types_tests.rs +++ b/codex-rs/config/src/types_tests.rs @@ -1,6 +1,20 @@ use super::*; use pretty_assertions::assert_eq; +#[test] +fn deserialize_marketplace_source_type_with_legacy_path_alias() { + let cfg: MarketplaceConfig = toml::from_str( + r#" + source_type = "path" + source = "/tmp/debug" + "#, + ) + .expect("should deserialize marketplace config with legacy path source type"); + + assert_eq!(cfg.source_type, Some(MarketplaceSourceType::Local)); + assert_eq!(cfg.source.as_deref(), Some("/tmp/debug")); +} + #[test] fn deserialize_skill_config_with_name_selector() { let cfg: SkillConfig = toml::from_str( diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 3a5e0cda3d43..d5d832d6cb16 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -811,7 +811,7 @@ "MarketplaceSourceType": { "enum": [ "git", - "path" + "local" ], "type": "string" }, diff --git a/codex-rs/core/src/plugins/installed_marketplaces.rs b/codex-rs/core/src/plugins/installed_marketplaces.rs index 8bc244fdf50a..c2c4e0bf3969 100644 --- a/codex-rs/core/src/plugins/installed_marketplaces.rs +++ b/codex-rs/core/src/plugins/installed_marketplaces.rs @@ -63,7 +63,7 @@ fn configured_marketplace_root( default_install_root: &Path, ) -> Option { match marketplace.get("source_type").and_then(toml::Value::as_str) { - Some("path") => marketplace + Some("local" | "path") => marketplace .get("source") .and_then(toml::Value::as_str) .filter(|source| !source.is_empty()) diff --git a/codex-rs/core/src/plugins/marketplace_add.rs b/codex-rs/core/src/plugins/marketplace_add.rs index f77d8313ba1f..ea41aa9cd454 100644 --- a/codex-rs/core/src/plugins/marketplace_add.rs +++ b/codex-rs/core/src/plugins/marketplace_add.rs @@ -278,7 +278,7 @@ mod tests { let config = fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE))?; assert!(config.contains("[marketplaces.debug]")); - assert!(config.contains("source_type = \"path\"")); + assert!(config.contains("source_type = \"local\"")); assert!(config.contains(&format!("source = \"{expected_source}\""))); Ok(()) } diff --git a/codex-rs/core/src/plugins/marketplace_add/metadata.rs b/codex-rs/core/src/plugins/marketplace_add/metadata.rs index 7e57ed1905a5..72de2e76c8c1 100644 --- a/codex-rs/core/src/plugins/marketplace_add/metadata.rs +++ b/codex-rs/core/src/plugins/marketplace_add/metadata.rs @@ -151,7 +151,7 @@ impl MarketplaceInstallMetadata { fn config_source_type(&self) -> &'static str { match &self.source { InstalledMarketplaceSource::Git { .. } => "git", - InstalledMarketplaceSource::Path { .. } => "path", + InstalledMarketplaceSource::Path { .. } => "local", } } @@ -206,7 +206,7 @@ fn configured_marketplace_root( install_root: &Path, ) -> Option { match marketplace.get("source_type").and_then(toml::Value::as_str) { - Some("path") => marketplace + Some("local" | "path") => marketplace .get("source") .and_then(toml::Value::as_str) .filter(|source| !source.is_empty()) @@ -311,7 +311,7 @@ mod tests { fs::write( codex_home.path().join(CONFIG_TOML_FILE), format!( - "[marketplaces.debug]\nsource_type = \"path\"\nsource = \"{}\"\n", + "[marketplaces.debug]\nsource_type = \"local\"\nsource = \"{}\"\n", source_root.display() ), ) From 5ffe6abf5358b8dfcd4f9e97430f7fdc94c1788b Mon Sep 17 00:00:00 2001 From: xli-oai Date: Tue, 14 Apr 2026 14:10:00 -0700 Subject: [PATCH 5/8] Drop marketplace source_type path alias --- codex-rs/config/src/types.rs | 1 - codex-rs/config/src/types_tests.rs | 14 -------------- .../core/src/plugins/installed_marketplaces.rs | 2 +- codex-rs/core/src/plugins/marketplace_add.rs | 2 +- .../src/plugins/marketplace_add/metadata.rs | 18 +++++++++--------- .../core/src/plugins/marketplace_add/source.rs | 12 ++++++------ 6 files changed, 17 insertions(+), 32 deletions(-) diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 559b383eab8e..73a5763fe1e2 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -632,7 +632,6 @@ pub struct MarketplaceConfig { #[serde(rename_all = "snake_case")] pub enum MarketplaceSourceType { Git, - #[serde(alias = "path")] Local, } diff --git a/codex-rs/config/src/types_tests.rs b/codex-rs/config/src/types_tests.rs index 9579bfc4504e..fbcd2bdc6411 100644 --- a/codex-rs/config/src/types_tests.rs +++ b/codex-rs/config/src/types_tests.rs @@ -1,20 +1,6 @@ use super::*; use pretty_assertions::assert_eq; -#[test] -fn deserialize_marketplace_source_type_with_legacy_path_alias() { - let cfg: MarketplaceConfig = toml::from_str( - r#" - source_type = "path" - source = "/tmp/debug" - "#, - ) - .expect("should deserialize marketplace config with legacy path source type"); - - assert_eq!(cfg.source_type, Some(MarketplaceSourceType::Local)); - assert_eq!(cfg.source.as_deref(), Some("/tmp/debug")); -} - #[test] fn deserialize_skill_config_with_name_selector() { let cfg: SkillConfig = toml::from_str( diff --git a/codex-rs/core/src/plugins/installed_marketplaces.rs b/codex-rs/core/src/plugins/installed_marketplaces.rs index c2c4e0bf3969..bd8506aae715 100644 --- a/codex-rs/core/src/plugins/installed_marketplaces.rs +++ b/codex-rs/core/src/plugins/installed_marketplaces.rs @@ -63,7 +63,7 @@ fn configured_marketplace_root( default_install_root: &Path, ) -> Option { match marketplace.get("source_type").and_then(toml::Value::as_str) { - Some("local" | "path") => marketplace + Some("local") => marketplace .get("source") .and_then(toml::Value::as_str) .filter(|source| !source.is_empty()) diff --git a/codex-rs/core/src/plugins/marketplace_add.rs b/codex-rs/core/src/plugins/marketplace_add.rs index ea41aa9cd454..5c6bb7f1e2ad 100644 --- a/codex-rs/core/src/plugins/marketplace_add.rs +++ b/codex-rs/core/src/plugins/marketplace_add.rs @@ -104,7 +104,7 @@ where }); } - if let MarketplaceSource::Path { path } = &source { + if let MarketplaceSource::Local { path } = &source { let marketplace_name = validate_marketplace_source_root(path)?; if marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME { return Err(MarketplaceAddError::InvalidRequest(format!( diff --git a/codex-rs/core/src/plugins/marketplace_add/metadata.rs b/codex-rs/core/src/plugins/marketplace_add/metadata.rs index 72de2e76c8c1..2ece007a0063 100644 --- a/codex-rs/core/src/plugins/marketplace_add/metadata.rs +++ b/codex-rs/core/src/plugins/marketplace_add/metadata.rs @@ -23,7 +23,7 @@ enum InstalledMarketplaceSource { ref_name: Option, sparse_paths: Vec, }, - Path { + Local { path: String, }, } @@ -141,7 +141,7 @@ impl MarketplaceInstallMetadata { ref_name: ref_name.clone(), sparse_paths: sparse_paths.to_vec(), }, - MarketplaceSource::Path { path } => InstalledMarketplaceSource::Path { + MarketplaceSource::Local { path } => InstalledMarketplaceSource::Local { path: path.display().to_string(), }, }; @@ -151,28 +151,28 @@ impl MarketplaceInstallMetadata { fn config_source_type(&self) -> &'static str { match &self.source { InstalledMarketplaceSource::Git { .. } => "git", - InstalledMarketplaceSource::Path { .. } => "local", + InstalledMarketplaceSource::Local { .. } => "local", } } fn config_source(&self) -> String { match &self.source { InstalledMarketplaceSource::Git { url, .. } => url.clone(), - InstalledMarketplaceSource::Path { path } => path.clone(), + InstalledMarketplaceSource::Local { path } => path.clone(), } } fn ref_name(&self) -> Option<&str> { match &self.source { InstalledMarketplaceSource::Git { ref_name, .. } => ref_name.as_deref(), - InstalledMarketplaceSource::Path { .. } => None, + InstalledMarketplaceSource::Local { .. } => None, } } fn sparse_paths(&self) -> &[String] { match &self.source { InstalledMarketplaceSource::Git { sparse_paths, .. } => sparse_paths, - InstalledMarketplaceSource::Path { .. } => &[], + InstalledMarketplaceSource::Local { .. } => &[], } } @@ -206,7 +206,7 @@ fn configured_marketplace_root( install_root: &Path, ) -> Option { match marketplace.get("source_type").and_then(toml::Value::as_str) { - Some("local" | "path") => marketplace + Some("local") => marketplace .get("source") .and_then(toml::Value::as_str) .filter(|source| !source.is_empty()) @@ -298,7 +298,7 @@ mod tests { } #[test] - fn installed_marketplace_root_for_source_uses_path_source_root() { + fn installed_marketplace_root_for_source_uses_local_source_root() { let codex_home = TempDir::new().unwrap(); let install_root = codex_home.path().join("marketplaces"); let source_root = codex_home.path().join("source"); @@ -317,7 +317,7 @@ mod tests { ) .unwrap(); - let source = MarketplaceSource::Path { + let source = MarketplaceSource::Local { path: source_root.clone(), }; let install_metadata = MarketplaceInstallMetadata::from_source(&source, &[]); diff --git a/codex-rs/core/src/plugins/marketplace_add/source.rs b/codex-rs/core/src/plugins/marketplace_add/source.rs index 9e2d55e1f2e5..19fec57ade4c 100644 --- a/codex-rs/core/src/plugins/marketplace_add/source.rs +++ b/codex-rs/core/src/plugins/marketplace_add/source.rs @@ -10,7 +10,7 @@ pub(super) enum MarketplaceSource { url: String, ref_name: Option, }, - Path { + Local { path: PathBuf, }, } @@ -41,7 +41,7 @@ pub(super) fn parse_marketplace_source( "local marketplace source must be a directory, not a file".to_string(), )); } - return Ok(MarketplaceSource::Path { path }); + return Ok(MarketplaceSource::Local { path }); } if is_ssh_git_url(&base_source) || is_git_url(&base_source) { @@ -82,7 +82,7 @@ where MarketplaceSource::Git { url, ref_name } => { clone_source(url, ref_name.as_deref(), sparse_paths, staged_root) } - MarketplaceSource::Path { .. } => unreachable!( + MarketplaceSource::Local { .. } => unreachable!( "local marketplace sources are added without staging a copied install root" ), } @@ -196,7 +196,7 @@ impl MarketplaceSource { Some(ref_name) => format!("{url}#{ref_name}"), None => url.clone(), }, - Self::Path { path } => path.display().to_string(), + Self::Local { path } => path.display().to_string(), } } } @@ -304,7 +304,7 @@ mod tests { fn local_path_source_parses() { let source = parse_marketplace_source(".", /*explicit_ref*/ None).unwrap(); - let MarketplaceSource::Path { path } = source else { + let MarketplaceSource::Local { path } = source else { panic!("expected local path source"); }; assert!(path.is_absolute()); @@ -341,7 +341,7 @@ mod tests { fn non_git_sources_reject_sparse_checkout() { let path = std::env::current_dir().unwrap(); let err = stage_marketplace_source( - &MarketplaceSource::Path { path }, + &MarketplaceSource::Local { path }, &["plugins/foo".to_string()], Path::new("/tmp"), |_url, _ref_name, _sparse_paths, _staged_root| Ok(()), From 0100de263dc123b94857c8c2fb8705d266620393 Mon Sep 17 00:00:00 2001 From: xli-oai Date: Tue, 14 Apr 2026 14:53:34 -0700 Subject: [PATCH 6/8] Update app-server marketplace add test for local roots --- codex-rs/app-server/tests/suite/v2/marketplace_add.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_add.rs b/codex-rs/app-server/tests/suite/v2/marketplace_add.rs index 58e252c19f4a..64aff0d9aed0 100644 --- a/codex-rs/app-server/tests/suite/v2/marketplace_add.rs +++ b/codex-rs/app-server/tests/suite/v2/marketplace_add.rs @@ -5,7 +5,6 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::MarketplaceAddParams; use codex_app_server_protocol::MarketplaceAddResponse; use codex_app_server_protocol::RequestId; -use codex_core::plugins::marketplace_install_root; use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::Duration; @@ -49,9 +48,7 @@ async fn marketplace_add_supports_local_directory_source() -> Result<()> { installed_root, already_added, } = to_response(response)?; - let expected_root = marketplace_install_root(codex_home.path()) - .join("debug") - .canonicalize()?; + let expected_root = source.canonicalize()?; assert_eq!(marketplace_name, "debug"); assert_eq!(installed_root.as_path(), expected_root.as_path()); From 9852fab85282ba703725381c68b76a33bccdea98 Mon Sep 17 00:00:00 2001 From: xli-oai Date: Tue, 14 Apr 2026 15:20:27 -0700 Subject: [PATCH 7/8] Consolidate marketplace root config resolution --- .../src/plugins/installed_marketplaces.rs | 9 ++++--- codex-rs/core/src/plugins/marketplace_add.rs | 6 ++--- .../src/plugins/marketplace_add/metadata.rs | 24 +++++-------------- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/codex-rs/core/src/plugins/installed_marketplaces.rs b/codex-rs/core/src/plugins/installed_marketplaces.rs index bd8506aae715..df69111a2736 100644 --- a/codex-rs/core/src/plugins/installed_marketplaces.rs +++ b/codex-rs/core/src/plugins/installed_marketplaces.rs @@ -45,8 +45,11 @@ pub(crate) fn installed_marketplace_roots_from_config( ); return None; } - let path = - configured_marketplace_root(marketplace_name, marketplace, &default_install_root)?; + let path = resolve_configured_marketplace_root( + marketplace_name, + marketplace, + &default_install_root, + )?; path.join(".agents/plugins/marketplace.json") .is_file() .then_some(path) @@ -57,7 +60,7 @@ pub(crate) fn installed_marketplace_roots_from_config( roots } -fn configured_marketplace_root( +pub(crate) fn resolve_configured_marketplace_root( marketplace_name: &str, marketplace: &toml::Value, default_install_root: &Path, diff --git a/codex-rs/core/src/plugins/marketplace_add.rs b/codex-rs/core/src/plugins/marketplace_add.rs index 5c6bb7f1e2ad..49c1c2e565be 100644 --- a/codex-rs/core/src/plugins/marketplace_add.rs +++ b/codex-rs/core/src/plugins/marketplace_add.rs @@ -16,7 +16,7 @@ use install::marketplace_staging_root; use install::replace_marketplace_root; use install::safe_marketplace_dir_name; use metadata::MarketplaceInstallMetadata; -use metadata::installed_marketplace_root_for_name; +use metadata::find_marketplace_root_by_name; use metadata::installed_marketplace_root_for_source; use metadata::record_added_marketplace_entry; use source::MarketplaceSource; @@ -112,9 +112,7 @@ where source.display() ))); } - if installed_marketplace_root_for_name(codex_home, &install_root, &marketplace_name)? - .is_some() - { + if find_marketplace_root_by_name(codex_home, &install_root, &marketplace_name)?.is_some() { return Err(MarketplaceAddError::InvalidRequest(format!( "marketplace '{marketplace_name}' is already added from a different source; remove it before adding {}", source.display() diff --git a/codex-rs/core/src/plugins/marketplace_add/metadata.rs b/codex-rs/core/src/plugins/marketplace_add/metadata.rs index 2ece007a0063..ce271b116d80 100644 --- a/codex-rs/core/src/plugins/marketplace_add/metadata.rs +++ b/codex-rs/core/src/plugins/marketplace_add/metadata.rs @@ -1,5 +1,6 @@ use super::MarketplaceAddError; use super::MarketplaceSource; +use crate::plugins::installed_marketplaces::resolve_configured_marketplace_root; use crate::plugins::validate_marketplace_root; use codex_config::CONFIG_TOML_FILE; use codex_config::MarketplaceConfigUpdate; @@ -80,7 +81,8 @@ pub(super) fn installed_marketplace_root_for_source( if !install_metadata.matches_config(marketplace) { continue; } - let Some(root) = configured_marketplace_root(marketplace_name, marketplace, install_root) + let Some(root) = + resolve_configured_marketplace_root(marketplace_name, marketplace, install_root) else { continue; }; @@ -92,7 +94,7 @@ pub(super) fn installed_marketplace_root_for_source( Ok(None) } -pub(super) fn installed_marketplace_root_for_name( +pub(super) fn find_marketplace_root_by_name( codex_home: &Path, install_root: &Path, marketplace_name: &str, @@ -122,7 +124,8 @@ pub(super) fn installed_marketplace_root_for_name( return Ok(None); }; - let Some(root) = configured_marketplace_root(marketplace_name, marketplace, install_root) + let Some(root) = + resolve_configured_marketplace_root(marketplace_name, marketplace, install_root) else { return Ok(None); }; @@ -200,21 +203,6 @@ fn config_sparse_paths(marketplace: &toml::Value) -> Vec { .unwrap_or_default() } -fn configured_marketplace_root( - marketplace_name: &str, - marketplace: &toml::Value, - install_root: &Path, -) -> Option { - match marketplace.get("source_type").and_then(toml::Value::as_str) { - Some("local") => marketplace - .get("source") - .and_then(toml::Value::as_str) - .filter(|source| !source.is_empty()) - .map(PathBuf::from), - _ => Some(install_root.join(marketplace_name)), - } -} - fn utc_timestamp_now() -> Result { let duration = SystemTime::now() .duration_since(UNIX_EPOCH) From 681dba9c3ad032d8b1f6be74efeb139a149d97dd Mon Sep 17 00:00:00 2001 From: xli-oai Date: Tue, 14 Apr 2026 15:28:00 -0700 Subject: [PATCH 8/8] Rename local marketplace add tests --- codex-rs/app-server/tests/suite/v2/marketplace_add.rs | 2 +- codex-rs/cli/tests/marketplace_add.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_add.rs b/codex-rs/app-server/tests/suite/v2/marketplace_add.rs index 64aff0d9aed0..cf3c57360ffd 100644 --- a/codex-rs/app-server/tests/suite/v2/marketplace_add.rs +++ b/codex-rs/app-server/tests/suite/v2/marketplace_add.rs @@ -13,7 +13,7 @@ use tokio::time::timeout; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); #[tokio::test] -async fn marketplace_add_supports_local_directory_source() -> Result<()> { +async fn marketplace_add_local_directory_source() -> Result<()> { let codex_home = TempDir::new()?; let source = codex_home.path().join("marketplace"); std::fs::create_dir_all(source.join(".agents/plugins"))?; diff --git a/codex-rs/cli/tests/marketplace_add.rs b/codex-rs/cli/tests/marketplace_add.rs index 3b325da4f690..e04cfb579168 100644 --- a/codex-rs/cli/tests/marketplace_add.rs +++ b/codex-rs/cli/tests/marketplace_add.rs @@ -39,7 +39,7 @@ fn write_marketplace_source(source: &Path, marker: &str) -> Result<()> { } #[tokio::test] -async fn marketplace_add_supports_local_directory_source() -> Result<()> { +async fn marketplace_add_local_directory_source() -> Result<()> { let codex_home = TempDir::new()?; let source = TempDir::new()?; write_marketplace_source(source.path(), "local ref")?;