From f23ef8445ce8826469e6cf2aeff0fedfd7cf68f3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 13 May 2026 16:16:31 +0100 Subject: [PATCH] Delete orphaned core plugins crate --- notes/fresh-workspace-handoff.md | 6 + state/ledgers.msgpack | Bin 100959 -> 101238 bytes state/map.yaml | 7 +- vendor/codex/codex-rs/Cargo.lock | 28 - vendor/codex/codex-rs/Cargo.toml | 2 - .../codex/codex-rs/core-plugins/BUILD.bazel | 15 - vendor/codex/codex-rs/core-plugins/Cargo.toml | 40 - vendor/codex/codex-rs/core-plugins/src/lib.rs | 8 - .../codex/codex-rs/core-plugins/src/loader.rs | 1130 ------------ .../codex-rs/core-plugins/src/manifest.rs | 545 ------ .../codex-rs/core-plugins/src/marketplace.rs | 778 --------- .../core-plugins/src/marketplace_tests.rs | 1530 ----------------- .../core-plugins/src/marketplace_upgrade.rs | 298 ---- .../src/marketplace_upgrade/activation.rs | 167 -- .../src/marketplace_upgrade/git.rs | 238 --- .../codex/codex-rs/core-plugins/src/remote.rs | 656 ------- .../core-plugins/src/remote_legacy.rs | 313 ---- .../codex/codex-rs/core-plugins/src/store.rs | 341 ---- .../codex-rs/core-plugins/src/store_tests.rs | 318 ---- .../codex-rs/core-plugins/src/toggles.rs | 100 -- 20 files changed, 11 insertions(+), 6509 deletions(-) delete mode 100644 vendor/codex/codex-rs/core-plugins/BUILD.bazel delete mode 100644 vendor/codex/codex-rs/core-plugins/Cargo.toml delete mode 100644 vendor/codex/codex-rs/core-plugins/src/lib.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/loader.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/manifest.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/marketplace.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/marketplace_tests.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade/activation.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade/git.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/remote.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/remote_legacy.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/store.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/store_tests.rs delete mode 100644 vendor/codex/codex-rs/core-plugins/src/toggles.rs diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 34d0fcb6..bd516ffa 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -889,6 +889,12 @@ auth surfaces that preserve the user's Codex subscription compatibility remain part of the keeper spine until the native OpenAI adapter can replace more of that transport. +The orphaned `codex-core-plugins` crate is deleted too. Nothing imported +`codex_core_plugins`; it was only a workspace member/dependency entry plus its +own marketplace/store/loader code. Remaining plugin-shaped imports are narrower +telemetry and skill-namespace compatibility residues, not the old marketplace +runtime. + The Phase 6 freshness slice is landed. It exposes read-only `thread/epiphany/freshness` from live retrieval summaries plus graph frontier/churn state and, for loaded threads, watcher-backed invalidation diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index bac9c6aeb66a5d202ed986f3cfa54edcca519f49..327e75c4f7a0bd2df341d088d9dbe39489398229 100644 GIT binary patch delta 221 zcmcaVh3(rkwh78i-8LIlr!g{eHt%8FzK4;qw_|!lA)_FhsgqfQo_oQj1a*ic)j)%TiOU74q^S zX2*lfj)$3@nOl%wR8kC5mYA70U9XE#YPx+Xqnud779#^AGhG8yT|?s#Lx`izq5!Zo BP09cO delta 49 zcmex1jqUyvwh78iU8);Zr!g{eH1A>DzK4;qw?nL9i;;nmnXZAUuAy;=p{bRjv6YcY F6acWS4>) { - for plugin in outcome - .plugins() - .iter() - .filter(|plugin| plugin.error.is_some()) - { - if let Some(error) = plugin.error.as_deref() { - warn!( - plugin = plugin.config_name, - path = %plugin.root.display(), - "failed to load plugin: {error}" - ); - } - } -} - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PluginMcpServersFile { - mcp_servers: HashMap, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum PluginMcpFile { - McpServersObject(PluginMcpServersFile), - ServerMap(HashMap), -} - -impl PluginMcpFile { - fn into_mcp_servers(self) -> HashMap { - match self { - Self::McpServersObject(file) => file.mcp_servers, - Self::ServerMap(mcp_servers) => mcp_servers, - } - } -} - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PluginAppFile { - #[serde(default)] - apps: HashMap, -} - -#[derive(Debug, Default, Deserialize)] -struct PluginAppConfig { - id: String, -} - -pub async fn load_plugins_from_layer_stack( - config_layer_stack: &ConfigLayerStack, - store: &PluginStore, - restriction_product: Option, -) -> PluginLoadOutcome { - let skill_config_rules = skill_config_rules_from_stack(config_layer_stack); - let mut configured_plugins: Vec<_> = configured_plugins_from_stack(config_layer_stack) - .into_iter() - .collect(); - configured_plugins.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); - - let mut plugins = Vec::with_capacity(configured_plugins.len()); - let mut seen_mcp_server_names = HashMap::::new(); - for (configured_name, plugin) in configured_plugins { - let loaded_plugin = load_plugin( - configured_name.clone(), - &plugin, - store, - restriction_product, - &skill_config_rules, - ) - .await; - for name in loaded_plugin.mcp_servers.keys() { - if let Some(previous_plugin) = - seen_mcp_server_names.insert(name.clone(), configured_name.clone()) - { - warn!( - plugin = configured_name, - previous_plugin, - server = name, - "skipping duplicate plugin MCP server name" - ); - } - } - plugins.push(loaded_plugin); - } - - PluginLoadOutcome::from_plugins(plugins) -} - -pub fn refresh_curated_plugin_cache( - codex_home: &Path, - plugin_version: &str, - configured_curated_plugin_ids: &[PluginId], -) -> Result { - let store = PluginStore::try_new(codex_home.to_path_buf()).map_err(|err| err.to_string())?; - let curated_marketplace_path = AbsolutePathBuf::try_from( - codex_home - .join(".tmp/plugins") - .join(".agents/plugins/marketplace.json"), - ) - .map_err(|_| "local curated marketplace is not available".to_string())?; - let curated_marketplace = load_marketplace(&curated_marketplace_path) - .map_err(|err| format!("failed to load curated marketplace for cache refresh: {err}"))?; - - let mut plugin_sources = HashMap::::new(); - for plugin in curated_marketplace.plugins { - let plugin_name = plugin.name; - if plugin_sources.contains_key(&plugin_name) { - warn!( - plugin = plugin_name, - marketplace = OPENAI_CURATED_MARKETPLACE_NAME, - "ignoring duplicate curated plugin entry during cache refresh" - ); - continue; - } - let source_path = match plugin.source { - MarketplacePluginSource::Local { path } => path, - MarketplacePluginSource::Git { .. } => { - warn!( - plugin = plugin_name, - marketplace = OPENAI_CURATED_MARKETPLACE_NAME, - "skipping remote curated plugin source during cache refresh" - ); - continue; - } - }; - plugin_sources.insert(plugin_name, source_path); - } - - let mut cache_refreshed = false; - for plugin_id in configured_curated_plugin_ids { - if store.active_plugin_version(plugin_id).as_deref() == Some(plugin_version) { - continue; - } - - let Some(source_path) = plugin_sources.get(&plugin_id.plugin_name).cloned() else { - warn!( - plugin = plugin_id.plugin_name, - marketplace = OPENAI_CURATED_MARKETPLACE_NAME, - "configured curated plugin no longer exists in curated marketplace during cache refresh" - ); - continue; - }; - - store - .install_with_version(source_path, plugin_id.clone(), plugin_version.to_string()) - .map_err(|err| { - format!( - "failed to refresh curated plugin cache for {}: {err}", - plugin_id.as_key() - ) - })?; - cache_refreshed = true; - } - - Ok(cache_refreshed) -} - -pub fn refresh_non_curated_plugin_cache( - codex_home: &Path, - additional_roots: &[AbsolutePathBuf], -) -> Result { - refresh_non_curated_plugin_cache_with_mode( - codex_home, - additional_roots, - NonCuratedCacheRefreshMode::IfVersionChanged, - ) -} - -pub fn refresh_non_curated_plugin_cache_force_reinstall( - codex_home: &Path, - additional_roots: &[AbsolutePathBuf], -) -> Result { - refresh_non_curated_plugin_cache_with_mode( - codex_home, - additional_roots, - NonCuratedCacheRefreshMode::ForceReinstall, - ) -} - -fn refresh_non_curated_plugin_cache_with_mode( - codex_home: &Path, - additional_roots: &[AbsolutePathBuf], - mode: NonCuratedCacheRefreshMode, -) -> Result { - let configured_non_curated_plugin_ids = - non_curated_plugin_ids_from_config_keys(configured_plugins_from_codex_home( - codex_home, - "failed to read user config while refreshing non-curated plugin cache", - "failed to parse user config while refreshing non-curated plugin cache", - )); - if configured_non_curated_plugin_ids.is_empty() { - return Ok(false); - } - let configured_non_curated_plugin_keys = configured_non_curated_plugin_ids - .iter() - .map(PluginId::as_key) - .collect::>(); - - let store = PluginStore::try_new(codex_home.to_path_buf()).map_err(|err| err.to_string())?; - let marketplace_outcome = list_marketplaces(additional_roots) - .map_err(|err| format!("failed to discover marketplaces for cache refresh: {err}"))?; - let mut plugin_sources = HashMap::::new(); - - for marketplace in marketplace_outcome.marketplaces { - if marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME { - continue; - } - - for plugin in marketplace.plugins { - let plugin_id = - PluginId::new(plugin.name.clone(), marketplace.name.clone()).map_err(|err| { - match err { - PluginIdError::Invalid(message) => { - format!("failed to prepare non-curated plugin cache refresh: {message}") - } - } - })?; - let plugin_key = plugin_id.as_key(); - if !configured_non_curated_plugin_keys.contains(&plugin_key) { - continue; - } - if plugin_sources.contains_key(&plugin_key) { - warn!( - plugin = plugin.name, - marketplace = marketplace.name, - "ignoring duplicate non-curated plugin entry during cache refresh" - ); - continue; - } - - plugin_sources.insert(plugin_key, plugin.source); - } - } - - let mut cache_refreshed = false; - for plugin_id in configured_non_curated_plugin_ids { - let plugin_key = plugin_id.as_key(); - let Some(source) = plugin_sources.get(&plugin_key).cloned() else { - warn!( - plugin = plugin_id.plugin_name, - marketplace = plugin_id.marketplace_name, - "configured non-curated plugin no longer exists in discovered marketplaces during cache refresh" - ); - continue; - }; - let materialized = - materialize_marketplace_plugin_source(codex_home, &source).map_err(|err| { - format!("failed to materialize plugin source for {plugin_key}: {err}") - })?; - let source_path = materialized.path.clone(); - let plugin_version = plugin_version_for_source(source_path.as_path()) - .map_err(|err| format!("failed to read plugin version for {plugin_key}: {err}"))?; - - if mode == NonCuratedCacheRefreshMode::IfVersionChanged - && store.active_plugin_version(&plugin_id).as_deref() == Some(plugin_version.as_str()) - { - continue; - } - - store - .install_with_version(source_path, plugin_id.clone(), plugin_version) - .map_err(|err| format!("failed to refresh plugin cache for {plugin_key}: {err}"))?; - cache_refreshed = true; - } - - Ok(cache_refreshed) -} - -fn configured_plugins_from_stack( - config_layer_stack: &ConfigLayerStack, -) -> HashMap { - let Some(user_layer) = config_layer_stack.get_user_layer() else { - return HashMap::new(); - }; - configured_plugins_from_user_config_value(&user_layer.config) -} - -fn configured_plugins_from_user_config_value( - user_config: &toml::Value, -) -> HashMap { - let Some(plugins_value) = user_config.get("plugins") else { - return HashMap::new(); - }; - match plugins_value.clone().try_into() { - Ok(plugins) => plugins, - Err(err) => { - warn!("invalid plugins config: {err}"); - HashMap::new() - } - } -} - -fn configured_plugins_from_codex_home( - codex_home: &Path, - read_error_message: &str, - parse_error_message: &str, -) -> HashMap { - let config_path = codex_home.join(CONFIG_TOML_FILE); - let user_config = match fs::read_to_string(&config_path) { - Ok(user_config) => user_config, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return HashMap::new(), - Err(err) => { - warn!( - path = %config_path.display(), - error = %err, - "{read_error_message}" - ); - return HashMap::new(); - } - }; - - let user_config = match toml::from_str::(&user_config) { - Ok(user_config) => user_config, - Err(err) => { - warn!( - path = %config_path.display(), - error = %err, - "{parse_error_message}" - ); - return HashMap::new(); - } - }; - - configured_plugins_from_user_config_value(&user_config) -} - -fn configured_plugin_ids( - configured_plugins: HashMap, - invalid_plugin_key_message: &str, -) -> Vec { - configured_plugins - .into_keys() - .filter_map(|plugin_key| match PluginId::parse(&plugin_key) { - Ok(plugin_id) => Some(plugin_id), - Err(err) => { - warn!( - plugin_key, - error = %err, - "{invalid_plugin_key_message}" - ); - None - } - }) - .collect() -} - -fn curated_plugin_ids_from_config_keys( - configured_plugins: HashMap, -) -> Vec { - let mut configured_curated_plugin_ids = configured_plugin_ids( - configured_plugins, - "ignoring invalid configured plugin key during curated sync setup", - ) - .into_iter() - .filter(|plugin_id| plugin_id.marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME) - .collect::>(); - configured_curated_plugin_ids.sort_unstable_by_key(PluginId::as_key); - configured_curated_plugin_ids -} - -fn non_curated_plugin_ids_from_config_keys( - configured_plugins: HashMap, -) -> Vec { - let mut configured_non_curated_plugin_ids = configured_plugin_ids( - configured_plugins, - "ignoring invalid plugin key during non-curated cache refresh setup", - ) - .into_iter() - .filter(|plugin_id| plugin_id.marketplace_name != OPENAI_CURATED_MARKETPLACE_NAME) - .collect::>(); - configured_non_curated_plugin_ids.sort_unstable_by_key(PluginId::as_key); - configured_non_curated_plugin_ids -} - -pub fn configured_curated_plugin_ids_from_codex_home(codex_home: &Path) -> Vec { - curated_plugin_ids_from_config_keys(configured_plugins_from_codex_home( - codex_home, - "failed to read user config while refreshing curated plugin cache", - "failed to parse user config while refreshing curated plugin cache", - )) -} - -async fn load_plugin( - config_name: String, - plugin: &PluginConfig, - store: &PluginStore, - restriction_product: Option, - skill_config_rules: &SkillConfigRules, -) -> LoadedPlugin { - let plugin_id = PluginId::parse(&config_name); - let active_plugin_root = plugin_id - .as_ref() - .ok() - .and_then(|plugin_id| store.active_plugin_root(plugin_id)); - let root = active_plugin_root - .clone() - .unwrap_or_else(|| match &plugin_id { - Ok(plugin_id) => store.plugin_base_root(plugin_id), - Err(_) => store.root().clone(), - }); - let mut loaded_plugin = LoadedPlugin { - config_name, - manifest_name: None, - manifest_description: None, - root, - enabled: plugin.enabled, - skill_roots: Vec::new(), - disabled_skill_paths: HashSet::new(), - has_enabled_skills: false, - mcp_servers: HashMap::new(), - apps: Vec::new(), - error: None, - }; - - if !plugin.enabled { - return loaded_plugin; - } - - let plugin_root = match plugin_id { - Ok(_) => match active_plugin_root { - Some(plugin_root) => plugin_root, - None => { - loaded_plugin.error = Some("plugin is not installed".to_string()); - return loaded_plugin; - } - }, - Err(err) => { - loaded_plugin.error = Some(err.to_string()); - return loaded_plugin; - } - }; - - if !plugin_root.as_path().is_dir() { - loaded_plugin.error = Some("path does not exist or is not a directory".to_string()); - return loaded_plugin; - } - - let Some(manifest) = load_plugin_manifest(plugin_root.as_path()) else { - loaded_plugin.error = Some("missing or invalid plugin.json".to_string()); - return loaded_plugin; - }; - - let manifest_paths = &manifest.paths; - loaded_plugin.manifest_name = manifest - .interface - .as_ref() - .and_then(|interface| interface.display_name.as_deref()) - .map(str::trim) - .filter(|display_name| !display_name.is_empty()) - .map(str::to_string) - .or_else(|| Some(manifest.name.clone())); - loaded_plugin.manifest_description = manifest.description.clone(); - loaded_plugin.skill_roots = plugin_skill_roots(&plugin_root, manifest_paths); - let resolved_skills = load_plugin_skills( - &plugin_root, - manifest_paths, - restriction_product, - skill_config_rules, - ) - .await; - let has_enabled_skills = resolved_skills.has_enabled_skills(); - loaded_plugin.disabled_skill_paths = resolved_skills.disabled_skill_paths; - loaded_plugin.has_enabled_skills = has_enabled_skills; - let mut mcp_servers = HashMap::new(); - for mcp_config_path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) { - let plugin_mcp = load_mcp_servers_from_file(plugin_root.as_path(), &mcp_config_path).await; - for (name, config) in plugin_mcp.mcp_servers { - if mcp_servers.insert(name.clone(), config).is_some() { - warn!( - plugin = %plugin_root.display(), - path = %mcp_config_path.display(), - server = name, - "plugin MCP file overwrote an earlier server definition" - ); - } - } - } - loaded_plugin.mcp_servers = mcp_servers; - loaded_plugin.apps = load_plugin_apps(plugin_root.as_path()).await; - loaded_plugin -} - -#[derive(Debug, Clone)] -pub struct ResolvedPluginSkills { - pub skills: Vec, - pub disabled_skill_paths: HashSet, - pub had_errors: bool, -} - -impl ResolvedPluginSkills { - pub fn has_enabled_skills(&self) -> bool { - self.had_errors - || self - .skills - .iter() - .any(|skill| !self.disabled_skill_paths.contains(&skill.path_to_skills_md)) - } -} - -pub async fn load_plugin_skills( - plugin_root: &AbsolutePathBuf, - manifest_paths: &PluginManifestPaths, - restriction_product: Option, - skill_config_rules: &SkillConfigRules, -) -> ResolvedPluginSkills { - let roots = plugin_skill_roots(plugin_root, manifest_paths) - .into_iter() - .map(|path| SkillRoot { - path, - scope: SkillScope::User, - file_system: Arc::clone(&LOCAL_FS), - }) - .collect::>(); - let outcome = load_skills_from_roots(roots).await; - let had_errors = !outcome.errors.is_empty(); - let skills = outcome - .skills - .into_iter() - .filter(|skill| skill.matches_product_restriction_for_product(restriction_product)) - .collect::>(); - let disabled_skill_paths = resolve_disabled_skill_paths(&skills, skill_config_rules); - - ResolvedPluginSkills { - skills, - disabled_skill_paths, - had_errors, - } -} - -fn plugin_skill_roots( - plugin_root: &AbsolutePathBuf, - manifest_paths: &PluginManifestPaths, -) -> Vec { - let mut paths = default_skill_roots(plugin_root); - if let Some(path) = &manifest_paths.skills { - paths.push(path.clone()); - } - paths.sort_unstable(); - paths.dedup(); - paths -} - -fn default_skill_roots(plugin_root: &AbsolutePathBuf) -> Vec { - let skills_dir = plugin_root.join(DEFAULT_SKILLS_DIR_NAME); - if skills_dir.is_dir() { - vec![skills_dir] - } else { - Vec::new() - } -} - -fn plugin_mcp_config_paths( - plugin_root: &Path, - manifest_paths: &PluginManifestPaths, -) -> Vec { - if let Some(path) = &manifest_paths.mcp_servers { - return vec![path.clone()]; - } - default_mcp_config_paths(plugin_root) -} - -fn default_mcp_config_paths(plugin_root: &Path) -> Vec { - let mut paths = Vec::new(); - let default_path = plugin_root.join(DEFAULT_MCP_CONFIG_FILE); - if default_path.is_file() - && let Ok(default_path) = AbsolutePathBuf::try_from(default_path) - { - paths.push(default_path); - } - paths.sort_unstable_by(|left, right| left.as_path().cmp(right.as_path())); - paths.dedup_by(|left, right| left.as_path() == right.as_path()); - paths -} - -pub async fn load_plugin_apps(plugin_root: &Path) -> Vec { - if let Some(manifest) = load_plugin_manifest(plugin_root) { - return load_apps_from_paths( - plugin_root, - plugin_app_config_paths(plugin_root, &manifest.paths), - ) - .await; - } - load_apps_from_paths(plugin_root, default_app_config_paths(plugin_root)).await -} - -fn plugin_app_config_paths( - plugin_root: &Path, - manifest_paths: &PluginManifestPaths, -) -> Vec { - if let Some(path) = &manifest_paths.apps { - return vec![path.clone()]; - } - default_app_config_paths(plugin_root) -} - -fn default_app_config_paths(plugin_root: &Path) -> Vec { - let mut paths = Vec::new(); - let default_path = plugin_root.join(DEFAULT_APP_CONFIG_FILE); - if default_path.is_file() - && let Ok(default_path) = AbsolutePathBuf::try_from(default_path) - { - paths.push(default_path); - } - paths.sort_unstable_by(|left, right| left.as_path().cmp(right.as_path())); - paths.dedup_by(|left, right| left.as_path() == right.as_path()); - paths -} - -async fn load_apps_from_paths( - plugin_root: &Path, - app_config_paths: Vec, -) -> Vec { - let mut connector_ids = Vec::new(); - for app_config_path in app_config_paths { - let Ok(contents) = tokio::fs::read_to_string(app_config_path.as_path()).await else { - continue; - }; - let parsed = match serde_json::from_str::(&contents) { - Ok(parsed) => parsed, - Err(err) => { - warn!( - path = %app_config_path.display(), - "failed to parse plugin app config: {err}" - ); - continue; - } - }; - - let mut apps: Vec = parsed.apps.into_values().collect(); - apps.sort_unstable_by(|left, right| left.id.cmp(&right.id)); - - connector_ids.extend(apps.into_iter().filter_map(|app| { - if app.id.trim().is_empty() { - warn!( - plugin = %plugin_root.display(), - "plugin app config is missing an app id" - ); - None - } else { - Some(AppConnectorId(app.id)) - } - })); - } - connector_ids.dedup(); - connector_ids -} - -pub async fn plugin_telemetry_metadata_from_root( - plugin_id: &PluginId, - plugin_root: &AbsolutePathBuf, -) -> PluginTelemetryMetadata { - let Some(manifest) = load_plugin_manifest(plugin_root.as_path()) else { - return PluginTelemetryMetadata::from_plugin_id(plugin_id); - }; - - let manifest_paths = &manifest.paths; - let has_skills = !plugin_skill_roots(plugin_root, manifest_paths).is_empty(); - let mut mcp_server_names = Vec::new(); - for path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) { - mcp_server_names.extend( - load_mcp_servers_from_file(plugin_root.as_path(), &path) - .await - .mcp_servers - .into_keys(), - ); - } - mcp_server_names.sort_unstable(); - mcp_server_names.dedup(); - - PluginTelemetryMetadata { - plugin_id: plugin_id.clone(), - capability_summary: Some(PluginCapabilitySummary { - config_name: plugin_id.as_key(), - display_name: plugin_id.plugin_name.clone(), - description: None, - has_skills, - mcp_server_names, - app_connector_ids: load_apps_from_paths( - plugin_root.as_path(), - plugin_app_config_paths(plugin_root.as_path(), manifest_paths), - ) - .await, - }), - } -} - -pub async fn load_plugin_mcp_servers(plugin_root: &Path) -> HashMap { - let Some(manifest) = load_plugin_manifest(plugin_root) else { - return HashMap::new(); - }; - - let mut mcp_servers = HashMap::new(); - for mcp_config_path in plugin_mcp_config_paths(plugin_root, &manifest.paths) { - let plugin_mcp = load_mcp_servers_from_file(plugin_root, &mcp_config_path).await; - for (name, config) in plugin_mcp.mcp_servers { - mcp_servers.entry(name).or_insert(config); - } - } - - mcp_servers -} - -pub async fn installed_plugin_telemetry_metadata( - codex_home: &Path, - plugin_id: &PluginId, -) -> PluginTelemetryMetadata { - let store = match PluginStore::try_new(codex_home.to_path_buf()) { - Ok(store) => store, - Err(err) => { - warn!("failed to resolve plugin cache root: {err}"); - return PluginTelemetryMetadata::from_plugin_id(plugin_id); - } - }; - let Some(plugin_root) = store.active_plugin_root(plugin_id) else { - return PluginTelemetryMetadata::from_plugin_id(plugin_id); - }; - - plugin_telemetry_metadata_from_root(plugin_id, &plugin_root).await -} - -async fn load_mcp_servers_from_file( - plugin_root: &Path, - mcp_config_path: &AbsolutePathBuf, -) -> PluginMcpDiscovery { - let Ok(contents) = tokio::fs::read_to_string(mcp_config_path.as_path()).await else { - return PluginMcpDiscovery::default(); - }; - let parsed = match serde_json::from_str::(&contents) { - Ok(parsed) => parsed, - Err(err) => { - warn!( - path = %mcp_config_path.display(), - "failed to parse plugin MCP config: {err}" - ); - return PluginMcpDiscovery::default(); - } - }; - normalize_plugin_mcp_servers( - plugin_root, - parsed.into_mcp_servers(), - mcp_config_path.to_string_lossy().as_ref(), - ) -} - -fn normalize_plugin_mcp_servers( - plugin_root: &Path, - plugin_mcp_servers: HashMap, - source: &str, -) -> PluginMcpDiscovery { - let mut mcp_servers = HashMap::new(); - - for (name, config_value) in plugin_mcp_servers { - let normalized = normalize_plugin_mcp_server_value(plugin_root, config_value); - match serde_json::from_value::(JsonValue::Object(normalized)) { - Ok(config) => { - mcp_servers.insert(name, config); - } - Err(err) => { - warn!( - plugin = %plugin_root.display(), - server = name, - "failed to parse plugin MCP server from {source}: {err}" - ); - } - } - } - - PluginMcpDiscovery { mcp_servers } -} - -fn normalize_plugin_mcp_server_value( - plugin_root: &Path, - value: JsonValue, -) -> JsonMap { - let mut object = match value { - JsonValue::Object(object) => object, - _ => return JsonMap::new(), - }; - - if let Some(JsonValue::String(transport_type)) = object.remove("type") { - match transport_type.as_str() { - "http" | "streamable_http" | "streamable-http" => {} - "stdio" => {} - other => { - warn!( - plugin = %plugin_root.display(), - transport = other, - "plugin MCP server uses an unknown transport type" - ); - } - } - } - - if let Some(JsonValue::Object(oauth)) = object.remove("oauth") - && oauth.contains_key("callbackPort") - { - warn!( - plugin = %plugin_root.display(), - "plugin MCP server OAuth callbackPort is ignored; Codex uses global MCP OAuth callback settings" - ); - } - - if let Some(JsonValue::String(cwd)) = object.get("cwd") - && !Path::new(cwd).is_absolute() - { - object.insert( - "cwd".to_string(), - JsonValue::String(plugin_root.join(cwd).display().to_string()), - ); - } - - object -} - -#[derive(Debug, Default)] -struct PluginMcpDiscovery { - mcp_servers: HashMap, -} - -#[derive(Debug)] -pub struct MaterializedMarketplacePluginSource { - pub path: AbsolutePathBuf, - _tempdir: Option, -} - -pub fn materialize_marketplace_plugin_source( - codex_home: &Path, - source: &MarketplacePluginSource, -) -> Result { - match source { - MarketplacePluginSource::Local { path } => Ok(MaterializedMarketplacePluginSource { - path: path.clone(), - _tempdir: None, - }), - MarketplacePluginSource::Git { - url, - path, - ref_name, - sha, - } => { - let staging_root = codex_home.join("plugins/.marketplace-plugin-source-staging"); - fs::create_dir_all(&staging_root).map_err(|err| { - format!( - "failed to create marketplace plugin source staging directory {}: {err}", - staging_root.display() - ) - })?; - let tempdir = tempfile::Builder::new() - .prefix("marketplace-plugin-source-") - .tempdir_in(&staging_root) - .map_err(|err| { - format!( - "failed to create marketplace plugin source staging directory in {}: {err}", - staging_root.display() - ) - })?; - clone_git_plugin_source( - url, - ref_name.as_deref(), - sha.as_deref(), - path.as_deref(), - tempdir.path(), - )?; - let path = if let Some(path) = path { - AbsolutePathBuf::try_from(tempdir.path().join(path)).map_err(|err| { - format!("failed to resolve materialized plugin source path: {err}") - })? - } else { - AbsolutePathBuf::try_from(tempdir.path().to_path_buf()).map_err(|err| { - format!("failed to resolve materialized plugin source path: {err}") - })? - }; - Ok(MaterializedMarketplacePluginSource { - path, - _tempdir: Some(tempdir), - }) - } - } -} - -fn clone_git_plugin_source( - url: &str, - ref_name: Option<&str>, - sha: Option<&str>, - sparse_checkout_path: Option<&str>, - destination: &Path, -) -> Result<(), String> { - if let Some(sparse_checkout_path) = sparse_checkout_path { - run_git( - &[ - "clone", - "--filter=blob:none", - "--sparse", - "--no-checkout", - url, - destination.to_string_lossy().as_ref(), - ], - /*cwd*/ None, - )?; - run_git( - &[ - "sparse-checkout", - "set", - "--no-cone", - "--", - sparse_checkout_path, - ], - Some(destination), - )?; - } else { - run_git( - &["clone", url, destination.to_string_lossy().as_ref()], - /*cwd*/ None, - )?; - } - if let Some(target) = sha.or(ref_name) { - run_git(&["checkout", target], Some(destination))?; - } else if sparse_checkout_path.is_some() { - run_git(&["checkout"], Some(destination))?; - } - Ok(()) -} - -fn run_git(args: &[&str], cwd: Option<&Path>) -> Result<(), String> { - let mut command = Command::new("git"); - command.args(args); - command.env("GIT_TERMINAL_PROMPT", "0"); - if let Some(cwd) = cwd { - command.current_dir(cwd); - } - - let output = command - .output() - .map_err(|err| format!("failed to run git {}: {err}", args.join(" ")))?; - if output.status.success() { - return Ok(()); - } - - Err(format!( - "git {} failed with status {}\nstdout:\n{}\nstderr:\n{}", - args.join(" "), - output.status, - String::from_utf8_lossy(&output.stdout).trim(), - String::from_utf8_lossy(&output.stderr).trim() - )) -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn plugin_mcp_file_supports_mcp_servers_object_format() { - let parsed = serde_json::from_str::( - r#"{ - "mcpServers": { - "sample": { - "command": "sample-mcp" - } - } -}"#, - ) - .expect("parse wrapped plugin mcp config") - .into_mcp_servers(); - - assert_eq!( - parsed, - HashMap::from([( - "sample".to_string(), - serde_json::json!({ - "command": "sample-mcp" - }), - )]) - ); - } - - #[test] - fn plugin_mcp_file_supports_mcp_servers_object_format_with_metadata() { - let parsed = serde_json::from_str::( - r#"{ - "$schema": "https://example.com/plugin-mcp.schema.json", - "mcpServers": { - "sample": { - "command": "sample-mcp" - } - } -}"#, - ) - .expect("parse plugin mcp config with metadata") - .into_mcp_servers(); - - assert_eq!( - parsed, - HashMap::from([( - "sample".to_string(), - serde_json::json!({ - "command": "sample-mcp" - }), - )]) - ); - } - - #[test] - fn plugin_mcp_file_supports_top_level_server_map_format() { - let parsed = serde_json::from_str::( - r#"{ - "linear": { - "type": "http", - "url": "https://mcp.linear.app/mcp" - } -}"#, - ) - .expect("parse flat plugin mcp config") - .into_mcp_servers(); - - assert_eq!( - parsed, - HashMap::from([( - "linear".to_string(), - serde_json::json!({ - "type": "http", - "url": "https://mcp.linear.app/mcp" - }), - )]) - ); - } - - #[test] - fn materialize_git_subdir_uses_sparse_checkout() { - let codex_home = tempfile::tempdir().expect("create codex home"); - let repo = tempfile::tempdir().expect("create git repo"); - let plugin_dir = repo.path().join("plugins/toolkit"); - fs::create_dir_all(&plugin_dir).expect("create plugin directory"); - fs::create_dir_all(repo.path().join("plugins/other")).expect("create other plugin"); - fs::write(plugin_dir.join("marker.txt"), "toolkit").expect("write plugin marker"); - fs::write(repo.path().join("plugins/other/marker.txt"), "other") - .expect("write other marker"); - fs::write(repo.path().join("root.txt"), "root").expect("write root marker"); - - run_git(&["init"], Some(repo.path())).expect("init git repo"); - run_git( - &["config", "user.email", "test@example.com"], - Some(repo.path()), - ) - .expect("configure git email"); - run_git(&["config", "user.name", "Test User"], Some(repo.path())) - .expect("configure git name"); - run_git(&["add", "."], Some(repo.path())).expect("stage git repo"); - run_git(&["commit", "-m", "init"], Some(repo.path())).expect("commit git repo"); - - let materialized = materialize_marketplace_plugin_source( - codex_home.path(), - &MarketplacePluginSource::Git { - url: repo.path().display().to_string(), - path: Some("plugins/toolkit".to_string()), - ref_name: None, - sha: None, - }, - ) - .expect("materialize git source"); - - assert_eq!( - plugin_dir.file_name(), - materialized.path.as_path().file_name() - ); - assert!(materialized.path.as_path().join("marker.txt").is_file()); - let checkout_root = materialized - .path - .as_path() - .parent() - .and_then(Path::parent) - .expect("materialized path should be nested under checkout root"); - assert!(!checkout_root.join("root.txt").exists()); - assert!(!checkout_root.join("plugins/other/marker.txt").exists()); - } -} diff --git a/vendor/codex/codex-rs/core-plugins/src/manifest.rs b/vendor/codex/codex-rs/core-plugins/src/manifest.rs deleted file mode 100644 index 5b536625..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/manifest.rs +++ /dev/null @@ -1,545 +0,0 @@ -use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_plugins::find_plugin_manifest_path; -use serde::Deserialize; -use serde_json::Value as JsonValue; -use std::fs; -use std::path::Component; -use std::path::Path; -const MAX_DEFAULT_PROMPT_COUNT: usize = 3; -const MAX_DEFAULT_PROMPT_LEN: usize = 128; - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawPluginManifest { - #[serde(default)] - name: String, - #[serde(default)] - version: Option, - #[serde(default)] - description: Option, - // Keep manifest paths as raw strings so we can validate the required `./...` syntax before - // resolving them under the plugin root. - #[serde(default)] - skills: Option, - #[serde(default)] - mcp_servers: Option, - #[serde(default)] - apps: Option, - #[serde(default)] - interface: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PluginManifest { - pub name: String, - pub version: Option, - pub description: Option, - pub paths: PluginManifestPaths, - pub interface: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PluginManifestPaths { - pub skills: Option, - pub mcp_servers: Option, - pub apps: Option, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct PluginManifestInterface { - pub display_name: Option, - pub short_description: Option, - pub long_description: Option, - pub developer_name: Option, - pub category: Option, - pub capabilities: Vec, - pub website_url: Option, - pub privacy_policy_url: Option, - pub terms_of_service_url: Option, - pub default_prompt: Option>, - pub brand_color: Option, - pub composer_icon: Option, - pub logo: Option, - pub screenshots: Vec, -} - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawPluginManifestInterface { - #[serde(default)] - display_name: Option, - #[serde(default)] - short_description: Option, - #[serde(default)] - long_description: Option, - #[serde(default)] - developer_name: Option, - #[serde(default)] - category: Option, - #[serde(default)] - capabilities: Vec, - #[serde(default)] - #[serde(alias = "websiteURL")] - website_url: Option, - #[serde(default)] - #[serde(alias = "privacyPolicyURL")] - privacy_policy_url: Option, - #[serde(default)] - #[serde(alias = "termsOfServiceURL")] - terms_of_service_url: Option, - #[serde(default)] - default_prompt: Option, - #[serde(default)] - brand_color: Option, - #[serde(default)] - composer_icon: Option, - #[serde(default)] - logo: Option, - #[serde(default)] - screenshots: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum RawPluginManifestDefaultPrompt { - String(String), - List(Vec), - Invalid(JsonValue), -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum RawPluginManifestDefaultPromptEntry { - String(String), - Invalid(JsonValue), -} - -pub fn load_plugin_manifest(plugin_root: &Path) -> Option { - let manifest_path = find_plugin_manifest_path(plugin_root)?; - let contents = fs::read_to_string(&manifest_path).ok()?; - match serde_json::from_str::(&contents) { - Ok(manifest) => { - let RawPluginManifest { - name: raw_name, - version, - description, - skills, - mcp_servers, - apps, - interface, - } = manifest; - let name = plugin_root - .file_name() - .and_then(|entry| entry.to_str()) - .filter(|_| raw_name.trim().is_empty()) - .unwrap_or(&raw_name) - .to_string(); - let version = version.and_then(|version| { - let version = version.trim(); - (!version.is_empty()).then(|| version.to_string()) - }); - let interface = interface.and_then(|interface| { - let RawPluginManifestInterface { - display_name, - short_description, - long_description, - developer_name, - category, - capabilities, - website_url, - privacy_policy_url, - terms_of_service_url, - default_prompt, - brand_color, - composer_icon, - logo, - screenshots, - } = interface; - - let interface = PluginManifestInterface { - display_name, - short_description, - long_description, - developer_name, - category, - capabilities, - website_url, - privacy_policy_url, - terms_of_service_url, - default_prompt: resolve_default_prompts(plugin_root, default_prompt.as_ref()), - brand_color, - composer_icon: resolve_interface_asset_path( - plugin_root, - "interface.composerIcon", - composer_icon.as_deref(), - ), - logo: resolve_interface_asset_path( - plugin_root, - "interface.logo", - logo.as_deref(), - ), - screenshots: screenshots - .iter() - .filter_map(|screenshot| { - resolve_interface_asset_path( - plugin_root, - "interface.screenshots", - Some(screenshot), - ) - }) - .collect(), - }; - - let has_fields = interface.display_name.is_some() - || interface.short_description.is_some() - || interface.long_description.is_some() - || interface.developer_name.is_some() - || interface.category.is_some() - || !interface.capabilities.is_empty() - || interface.website_url.is_some() - || interface.privacy_policy_url.is_some() - || interface.terms_of_service_url.is_some() - || interface.default_prompt.is_some() - || interface.brand_color.is_some() - || interface.composer_icon.is_some() - || interface.logo.is_some() - || !interface.screenshots.is_empty(); - - has_fields.then_some(interface) - }); - Some(PluginManifest { - name, - version, - description, - paths: PluginManifestPaths { - skills: resolve_manifest_path(plugin_root, "skills", skills.as_deref()), - mcp_servers: resolve_manifest_path( - plugin_root, - "mcpServers", - mcp_servers.as_deref(), - ), - apps: resolve_manifest_path(plugin_root, "apps", apps.as_deref()), - }, - interface, - }) - } - Err(err) => { - tracing::warn!( - path = %manifest_path.display(), - "failed to parse plugin manifest: {err}" - ); - None - } - } -} - -fn resolve_interface_asset_path( - plugin_root: &Path, - field: &'static str, - path: Option<&str>, -) -> Option { - resolve_manifest_path(plugin_root, field, path) -} - -fn resolve_default_prompts( - plugin_root: &Path, - value: Option<&RawPluginManifestDefaultPrompt>, -) -> Option> { - match value? { - RawPluginManifestDefaultPrompt::String(prompt) => { - resolve_default_prompt_str(plugin_root, "interface.defaultPrompt", prompt) - .map(|prompt| vec![prompt]) - } - RawPluginManifestDefaultPrompt::List(values) => { - let mut prompts = Vec::new(); - for (index, item) in values.iter().enumerate() { - if prompts.len() >= MAX_DEFAULT_PROMPT_COUNT { - warn_invalid_default_prompt( - plugin_root, - "interface.defaultPrompt", - &format!("maximum of {MAX_DEFAULT_PROMPT_COUNT} prompts is supported"), - ); - break; - } - - match item { - RawPluginManifestDefaultPromptEntry::String(prompt) => { - let field = format!("interface.defaultPrompt[{index}]"); - if let Some(prompt) = - resolve_default_prompt_str(plugin_root, &field, prompt) - { - prompts.push(prompt); - } - } - RawPluginManifestDefaultPromptEntry::Invalid(value) => { - let field = format!("interface.defaultPrompt[{index}]"); - warn_invalid_default_prompt( - plugin_root, - &field, - &format!("expected a string, found {}", json_value_type(value)), - ); - } - } - } - - (!prompts.is_empty()).then_some(prompts) - } - RawPluginManifestDefaultPrompt::Invalid(value) => { - warn_invalid_default_prompt( - plugin_root, - "interface.defaultPrompt", - &format!( - "expected a string or array of strings, found {}", - json_value_type(value) - ), - ); - None - } - } -} - -fn resolve_default_prompt_str(plugin_root: &Path, field: &str, prompt: &str) -> Option { - let prompt = prompt.split_whitespace().collect::>().join(" "); - if prompt.is_empty() { - warn_invalid_default_prompt(plugin_root, field, "prompt must not be empty"); - return None; - } - if prompt.chars().count() > MAX_DEFAULT_PROMPT_LEN { - warn_invalid_default_prompt( - plugin_root, - field, - &format!("prompt must be at most {MAX_DEFAULT_PROMPT_LEN} characters"), - ); - return None; - } - Some(prompt) -} - -fn warn_invalid_default_prompt(plugin_root: &Path, field: &str, message: &str) { - if let Some(manifest_path) = find_plugin_manifest_path(plugin_root) { - tracing::warn!( - path = %manifest_path.display(), - "ignoring {field}: {message}" - ); - } else { - tracing::warn!("ignoring {field}: {message}"); - } -} - -fn json_value_type(value: &JsonValue) -> &'static str { - match value { - JsonValue::Null => "null", - JsonValue::Bool(_) => "boolean", - JsonValue::Number(_) => "number", - JsonValue::String(_) => "string", - JsonValue::Array(_) => "array", - JsonValue::Object(_) => "object", - } -} - -fn resolve_manifest_path( - plugin_root: &Path, - field: &'static str, - path: Option<&str>, -) -> Option { - // `plugin.json` paths are required to be relative to the plugin root and we return the - // normalized absolute path to the rest of the system. - let path = path?; - if path.is_empty() { - return None; - } - let Some(relative_path) = path.strip_prefix("./") else { - tracing::warn!("ignoring {field}: path must start with `./` relative to plugin root"); - return None; - }; - if relative_path.is_empty() { - tracing::warn!("ignoring {field}: path must not be `./`"); - return None; - } - - let mut normalized = std::path::PathBuf::new(); - for component in Path::new(relative_path).components() { - match component { - Component::Normal(component) => normalized.push(component), - Component::ParentDir => { - tracing::warn!("ignoring {field}: path must not contain '..'"); - return None; - } - _ => { - tracing::warn!("ignoring {field}: path must stay within the plugin root"); - return None; - } - } - } - - AbsolutePathBuf::try_from(plugin_root.join(normalized)) - .map_err(|err| { - tracing::warn!("ignoring {field}: path must resolve to an absolute path: {err}"); - err - }) - .ok() -} - -#[cfg(test)] -mod tests { - use super::MAX_DEFAULT_PROMPT_LEN; - use super::PluginManifest; - use super::load_plugin_manifest; - use pretty_assertions::assert_eq; - use std::fs; - use std::path::Path; - use tempfile::tempdir; - - const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json"; - - fn write_manifest(plugin_root: &Path, version: Option<&str>, interface: &str) { - fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create manifest dir"); - let version = version - .map(|version| format!(" \"version\": \"{version}\",\n")) - .unwrap_or_default(); - fs::write( - plugin_root.join(".codex-plugin/plugin.json"), - format!( - r#"{{ - "name": "demo-plugin", -{version} - "interface": {interface} -}}"# - ), - ) - .expect("write manifest"); - } - - fn write_alternate_plugin_manifest(plugin_root: &Path, contents: &str) { - let manifest_path = plugin_root.join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH); - fs::create_dir_all(manifest_path.parent().expect("manifest parent")) - .expect("create manifest dir"); - fs::write(manifest_path, contents).expect("write manifest"); - } - - fn load_manifest(plugin_root: &Path) -> PluginManifest { - load_plugin_manifest(plugin_root).expect("load plugin manifest") - } - - #[test] - fn plugin_interface_accepts_legacy_default_prompt_string() { - let tmp = tempdir().expect("tempdir"); - let plugin_root = tmp.path().join("demo-plugin"); - write_manifest( - &plugin_root, - /*version*/ None, - r#"{ - "displayName": "Demo Plugin", - "defaultPrompt": " Summarize my inbox " - }"#, - ); - - let manifest = load_manifest(&plugin_root); - let interface = manifest.interface.expect("plugin interface"); - - assert_eq!( - interface.default_prompt, - Some(vec!["Summarize my inbox".to_string()]) - ); - } - - #[test] - fn plugin_interface_normalizes_default_prompt_array() { - let tmp = tempdir().expect("tempdir"); - let plugin_root = tmp.path().join("demo-plugin"); - let too_long = "x".repeat(MAX_DEFAULT_PROMPT_LEN + 1); - write_manifest( - &plugin_root, - /*version*/ None, - &format!( - r#"{{ - "displayName": "Demo Plugin", - "defaultPrompt": [ - " Summarize my inbox ", - 123, - "{too_long}", - " ", - "Draft the reply ", - "Find my next action", - "Archive old mail" - ] - }}"# - ), - ); - - let manifest = load_manifest(&plugin_root); - let interface = manifest.interface.expect("plugin interface"); - - assert_eq!( - interface.default_prompt, - Some(vec![ - "Summarize my inbox".to_string(), - "Draft the reply".to_string(), - "Find my next action".to_string(), - ]) - ); - } - - #[test] - fn plugin_interface_ignores_invalid_default_prompt_shape() { - let tmp = tempdir().expect("tempdir"); - let plugin_root = tmp.path().join("demo-plugin"); - write_manifest( - &plugin_root, - /*version*/ None, - r#"{ - "displayName": "Demo Plugin", - "defaultPrompt": { "text": "Summarize my inbox" } - }"#, - ); - - let manifest = load_manifest(&plugin_root); - let interface = manifest.interface.expect("plugin interface"); - - assert_eq!(interface.default_prompt, None); - } - - #[test] - fn plugin_manifest_reads_trimmed_version() { - let tmp = tempdir().expect("tempdir"); - let plugin_root = tmp.path().join("demo-plugin"); - write_manifest( - &plugin_root, - Some(" 1.2.3-beta+7 "), - r#"{ - "displayName": "Demo Plugin" - }"#, - ); - - let manifest = load_manifest(&plugin_root); - - assert_eq!(manifest.version, Some("1.2.3-beta+7".to_string())); - } - - #[test] - fn plugin_manifest_uses_alternate_discoverable_path() { - let tmp = tempdir().expect("tempdir"); - let plugin_root = tmp.path().join("demo-plugin"); - write_alternate_plugin_manifest( - &plugin_root, - r#"{ - "name": "demo-plugin", - "version": " 2.0.0 ", - "interface": { - "displayName": "Fallback Plugin" - } -}"#, - ); - - let manifest = load_manifest(&plugin_root); - - assert_eq!(manifest.version, Some("2.0.0".to_string())); - assert_eq!( - manifest - .interface - .as_ref() - .and_then(|interface| interface.display_name.as_deref()), - Some("Fallback Plugin") - ); - } -} diff --git a/vendor/codex/codex-rs/core-plugins/src/marketplace.rs b/vendor/codex/codex-rs/core-plugins/src/marketplace.rs deleted file mode 100644 index dc6d01ad..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/marketplace.rs +++ /dev/null @@ -1,778 +0,0 @@ -use crate::manifest::PluginManifestInterface; -use crate::manifest::load_plugin_manifest; -use codex_app_server_protocol::PluginAuthPolicy; -use codex_app_server_protocol::PluginInstallPolicy; -use codex_git_utils::get_git_repo_root; -use codex_plugin::PluginId; -use codex_plugin::PluginIdError; -use codex_protocol::protocol::Product; -use codex_utils_absolute_path::AbsolutePathBuf; -use dirs::home_dir; -use serde::Deserialize; -use serde_json::Value as JsonValue; -use std::fs; -use std::io; -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; -use tracing::warn; - -const MARKETPLACE_MANIFEST_RELATIVE_PATHS: &[&str] = &[ - ".agents/plugins/marketplace.json", - ".claude-plugin/marketplace.json", -]; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResolvedMarketplacePlugin { - pub plugin_id: PluginId, - pub source: MarketplacePluginSource, - pub policy: MarketplacePluginPolicy, - pub interface: Option, - pub manifest: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Marketplace { - pub name: String, - pub path: AbsolutePathBuf, - pub interface: Option, - pub plugins: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MarketplaceListError { - pub path: AbsolutePathBuf, - pub message: String, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct MarketplaceListOutcome { - pub marketplaces: Vec, - pub errors: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MarketplaceInterface { - pub display_name: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MarketplacePlugin { - pub name: String, - pub source: MarketplacePluginSource, - pub policy: MarketplacePluginPolicy, - pub interface: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MarketplacePluginSource { - Local { - path: AbsolutePathBuf, - }, - Git { - url: String, - path: Option, - ref_name: Option, - sha: Option, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MarketplacePluginPolicy { - pub installation: MarketplacePluginInstallPolicy, - pub authentication: MarketplacePluginAuthPolicy, - // TODO: Surface or enforce product gating at the Codex/plugin consumer boundary instead of - // only carrying it through core marketplace metadata. - pub products: Option>, -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] -pub enum MarketplacePluginInstallPolicy { - #[serde(rename = "NOT_AVAILABLE")] - NotAvailable, - #[default] - #[serde(rename = "AVAILABLE")] - Available, - #[serde(rename = "INSTALLED_BY_DEFAULT")] - InstalledByDefault, -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] -pub enum MarketplacePluginAuthPolicy { - #[default] - #[serde(rename = "ON_INSTALL")] - OnInstall, - #[serde(rename = "ON_USE")] - OnUse, -} - -impl From for PluginInstallPolicy { - fn from(value: MarketplacePluginInstallPolicy) -> Self { - match value { - MarketplacePluginInstallPolicy::NotAvailable => Self::NotAvailable, - MarketplacePluginInstallPolicy::Available => Self::Available, - MarketplacePluginInstallPolicy::InstalledByDefault => Self::InstalledByDefault, - } - } -} - -impl From for PluginAuthPolicy { - fn from(value: MarketplacePluginAuthPolicy) -> Self { - match value { - MarketplacePluginAuthPolicy::OnInstall => Self::OnInstall, - MarketplacePluginAuthPolicy::OnUse => Self::OnUse, - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum MarketplaceError { - #[error("{context}: {source}")] - Io { - context: &'static str, - #[source] - source: io::Error, - }, - - #[error("marketplace file `{path}` does not exist")] - MarketplaceNotFound { path: PathBuf }, - - #[error("invalid marketplace file `{path}`: {message}")] - InvalidMarketplaceFile { path: PathBuf, message: String }, - - #[error("plugin `{plugin_name}` was not found in marketplace `{marketplace_name}`")] - PluginNotFound { - plugin_name: String, - marketplace_name: String, - }, - - #[error( - "plugin `{plugin_name}` is not available for install in marketplace `{marketplace_name}`" - )] - PluginNotAvailable { - plugin_name: String, - marketplace_name: String, - }, - - #[error("plugins feature is disabled")] - PluginsDisabled, - - #[error("{0}")] - InvalidPlugin(String), -} - -impl MarketplaceError { - fn io(context: &'static str, source: io::Error) -> Self { - Self::Io { context, source } - } -} - -pub fn find_marketplace_plugin( - marketplace_path: &AbsolutePathBuf, - plugin_name: &str, -) -> Result { - let marketplace = load_raw_marketplace_manifest(marketplace_path)?; - let marketplace_name = marketplace.name; - let marketplace_name_for_not_found = marketplace_name.clone(); - for plugin in marketplace.plugins { - if plugin.name != plugin_name { - continue; - } - - if let Some(plugin) = - resolve_marketplace_plugin_entry(marketplace_path, &marketplace_name, plugin)? - { - return Ok(plugin); - } - } - - Err(MarketplaceError::PluginNotFound { - plugin_name: plugin_name.to_string(), - marketplace_name: marketplace_name_for_not_found, - }) -} - -pub fn find_installable_marketplace_plugin( - marketplace_path: &AbsolutePathBuf, - plugin_name: &str, - restriction_product: Option, -) -> Result { - let resolved = find_marketplace_plugin(marketplace_path, plugin_name)?; - let product_allowed = match resolved.policy.products.as_deref() { - None => true, - Some([]) => false, - Some(products) => { - restriction_product.is_some_and(|product| product.matches_product_restriction(products)) - } - }; - if resolved.policy.installation == MarketplacePluginInstallPolicy::NotAvailable - || !product_allowed - { - return Err(MarketplaceError::PluginNotAvailable { - plugin_name: resolved.plugin_id.plugin_name, - marketplace_name: resolved.plugin_id.marketplace_name, - }); - } - - Ok(resolved) -} - -pub fn list_marketplaces( - additional_roots: &[AbsolutePathBuf], -) -> Result { - list_marketplaces_with_home(additional_roots, home_dir().as_deref()) -} - -pub fn validate_marketplace_root(root: &Path) -> Result { - let Some(path) = find_marketplace_manifest_path(root) else { - return Err(MarketplaceError::InvalidMarketplaceFile { - path: root.to_path_buf(), - message: "marketplace root does not contain a supported manifest".to_string(), - }); - }; - let marketplace = load_marketplace(&path)?; - Ok(marketplace.name) -} - -pub fn find_marketplace_manifest_path(root: &Path) -> Option { - MARKETPLACE_MANIFEST_RELATIVE_PATHS - .iter() - .find_map(|relative_path| { - let path = root.join(relative_path); - if !path.is_file() { - return None; - } - AbsolutePathBuf::try_from(path).ok() - }) -} - -fn invalid_marketplace_layout_error(path: &AbsolutePathBuf) -> MarketplaceError { - MarketplaceError::InvalidMarketplaceFile { - path: path.to_path_buf(), - message: "marketplace file is not in a supported location".to_string(), - } -} - -fn marketplace_root_from_layout(marketplace_path: &Path, relative_path: &str) -> Option { - let mut current = marketplace_path; - for component in Path::new(relative_path).components().rev() { - let expected = match component { - Component::Normal(expected) => expected, - _ => return None, - }; - if current.file_name() != Some(expected) { - return None; - } - current = current.parent()?; - } - Some(current.to_path_buf()) -} - -pub fn load_marketplace(path: &AbsolutePathBuf) -> Result { - let marketplace = load_raw_marketplace_manifest(path)?; - let mut plugins = Vec::new(); - - for plugin in marketplace.plugins { - let plugin = match resolve_marketplace_plugin_entry(path, &marketplace.name, plugin) { - Ok(Some(plugin)) => plugin, - Ok(None) => continue, - Err(MarketplaceError::InvalidPlugin(message)) => { - warn!( - path = %path.display(), - marketplace = %marketplace.name, - error = %message, - "skipping invalid marketplace plugin" - ); - continue; - } - Err(err) => return Err(err), - }; - - plugins.push(MarketplacePlugin { - name: plugin.plugin_id.plugin_name, - source: plugin.source, - policy: plugin.policy, - interface: plugin.interface, - }); - } - - Ok(Marketplace { - name: marketplace.name, - path: path.clone(), - interface: resolve_marketplace_interface(marketplace.interface), - plugins, - }) -} - -#[doc(hidden)] -pub fn list_marketplaces_with_home( - additional_roots: &[AbsolutePathBuf], - home_dir: Option<&Path>, -) -> Result { - let mut outcome = MarketplaceListOutcome::default(); - - for marketplace_path in discover_marketplace_paths_from_roots(additional_roots, home_dir) { - match load_marketplace(&marketplace_path) { - Ok(marketplace) => outcome.marketplaces.push(marketplace), - Err(err) => { - warn!( - path = %marketplace_path.display(), - error = %err, - "skipping marketplace that failed to load" - ); - outcome.errors.push(MarketplaceListError { - path: marketplace_path, - message: err.to_string(), - }); - } - } - } - - Ok(outcome) -} - -fn discover_marketplace_paths_from_roots( - additional_roots: &[AbsolutePathBuf], - home_dir: Option<&Path>, -) -> Vec { - let mut paths = Vec::new(); - - if let Some(home) = home_dir - && let Some(path) = find_marketplace_manifest_path(home) - { - paths.push(path); - } - - for root in additional_roots { - // Curated marketplaces can now come from an HTTP-downloaded directory that is not a git - // checkout, so check the root directly before falling back to repo-root discovery. - if let Some(path) = find_marketplace_manifest_path(root.as_path()) - && !paths.contains(&path) - { - paths.push(path); - continue; - } - if let Some(repo_root) = get_git_repo_root(root.as_path()) - && let Ok(repo_root) = AbsolutePathBuf::try_from(repo_root) - && let Some(path) = find_marketplace_manifest_path(repo_root.as_path()) - && !paths.contains(&path) - { - paths.push(path); - } - } - - paths -} - -fn load_raw_marketplace_manifest( - path: &AbsolutePathBuf, -) -> Result { - let contents = fs::read_to_string(path.as_path()).map_err(|err| { - if err.kind() == io::ErrorKind::NotFound { - MarketplaceError::MarketplaceNotFound { - path: path.to_path_buf(), - } - } else { - MarketplaceError::io("failed to read marketplace file", err) - } - })?; - serde_json::from_str(&contents).map_err(|err| MarketplaceError::InvalidMarketplaceFile { - path: path.to_path_buf(), - message: err.to_string(), - }) -} - -fn resolve_marketplace_plugin_entry( - marketplace_path: &AbsolutePathBuf, - marketplace_name: &str, - plugin: RawMarketplaceManifestPlugin, -) -> Result, MarketplaceError> { - let RawMarketplaceManifestPlugin { - name, - source, - policy, - category, - } = plugin; - let Some(source) = resolve_supported_plugin_source(marketplace_path, &name, source) else { - return Ok(None); - }; - - let manifest = match &source { - MarketplacePluginSource::Local { path } => load_plugin_manifest(path.as_path()), - MarketplacePluginSource::Git { .. } => None, - }; - let interface = plugin_interface_with_marketplace_category( - manifest - .as_ref() - .and_then(|manifest| manifest.interface.clone()), - category, - ); - - Ok(Some(ResolvedMarketplacePlugin { - plugin_id: PluginId::new(name, marketplace_name.to_string()).map_err(|err| match err { - PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message), - })?, - source, - policy: MarketplacePluginPolicy { - installation: policy.installation, - authentication: policy.authentication, - products: policy.products, - }, - interface, - manifest, - })) -} - -fn resolve_supported_plugin_source( - marketplace_path: &AbsolutePathBuf, - plugin_name: &str, - source: RawMarketplaceManifestPluginSource, -) -> Option { - match source { - RawMarketplaceManifestPluginSource::Unsupported(_) => { - warn!( - path = %marketplace_path.display(), - plugin = plugin_name, - "skipping marketplace plugin with unsupported source" - ); - None - } - source => match resolve_plugin_source(marketplace_path, source) { - Ok(source) => Some(source), - Err(err) => { - warn!( - path = %marketplace_path.display(), - plugin = plugin_name, - error = %err, - "skipping marketplace plugin that failed to resolve" - ); - None - } - }, - } -} - -fn resolve_plugin_source( - marketplace_path: &AbsolutePathBuf, - source: RawMarketplaceManifestPluginSource, -) -> Result { - match source { - RawMarketplaceManifestPluginSource::Path(path) - | RawMarketplaceManifestPluginSource::Object( - RawMarketplaceManifestPluginSourceObject::Local { path }, - ) => Ok(MarketplacePluginSource::Local { - path: resolve_local_plugin_source_path(marketplace_path, &path)?, - }), - RawMarketplaceManifestPluginSource::Object( - RawMarketplaceManifestPluginSourceObject::Url { - url, - path, - ref_name, - sha, - }, - ) => Ok(MarketplacePluginSource::Git { - url: normalize_git_plugin_source_url(marketplace_path, &url)?, - path: path - .as_deref() - .map(|path| normalize_remote_plugin_subdir(marketplace_path, path)) - .transpose()?, - ref_name: normalize_optional_git_selector(&ref_name), - sha: normalize_optional_git_selector(&sha), - }), - RawMarketplaceManifestPluginSource::Object( - RawMarketplaceManifestPluginSourceObject::GitSubdir { - url, - path, - ref_name, - sha, - }, - ) => Ok(MarketplacePluginSource::Git { - url: normalize_git_plugin_source_url(marketplace_path, &url)?, - path: Some(normalize_remote_plugin_subdir(marketplace_path, &path)?), - ref_name: normalize_optional_git_selector(&ref_name), - sha: normalize_optional_git_selector(&sha), - }), - RawMarketplaceManifestPluginSource::Unsupported(_) => { - unreachable!("unsupported plugin sources should be filtered before resolution") - } - } -} - -fn resolve_local_plugin_source_path( - marketplace_path: &AbsolutePathBuf, - path: &str, -) -> Result { - let Some(path) = path.strip_prefix("./") else { - return Err(MarketplaceError::InvalidMarketplaceFile { - path: marketplace_path.to_path_buf(), - message: "local plugin source path must start with `./`".to_string(), - }); - }; - if path.is_empty() { - return Err(MarketplaceError::InvalidMarketplaceFile { - path: marketplace_path.to_path_buf(), - message: "local plugin source path must not be empty".to_string(), - }); - } - - let relative_source_path = Path::new(path); - if relative_source_path - .components() - .any(|component| !matches!(component, Component::Normal(_))) - { - return Err(MarketplaceError::InvalidMarketplaceFile { - path: marketplace_path.to_path_buf(), - message: "local plugin source path must stay within the marketplace root".to_string(), - }); - } - - // `marketplace.json` lives under a supported marketplace layout beneath ``, - // but local plugin paths are resolved relative to ``. - Ok(marketplace_root_dir(marketplace_path)?.join(relative_source_path)) -} - -fn normalize_remote_plugin_subdir( - marketplace_path: &AbsolutePathBuf, - path: &str, -) -> Result { - let path = path.trim(); - let path = path.strip_prefix("./").unwrap_or(path); - if path.is_empty() { - return Err(MarketplaceError::InvalidMarketplaceFile { - path: marketplace_path.to_path_buf(), - message: "git plugin source path must not be empty".to_string(), - }); - } - let relative_path = Path::new(path); - if relative_path - .components() - .any(|component| !matches!(component, Component::Normal(_))) - { - return Err(MarketplaceError::InvalidMarketplaceFile { - path: marketplace_path.to_path_buf(), - message: "git plugin source path must stay within the repository root".to_string(), - }); - } - Ok(path.to_string()) -} - -fn normalize_git_plugin_source_url( - marketplace_path: &AbsolutePathBuf, - url: &str, -) -> Result { - let url = url.trim(); - if url.is_empty() { - return Err(MarketplaceError::InvalidMarketplaceFile { - path: marketplace_path.to_path_buf(), - message: "git plugin source url must not be empty".to_string(), - }); - } - if url.starts_with("http://") || url.starts_with("https://") { - return Ok(normalize_github_git_url(url)); - } - if url.starts_with("./") - || url.starts_with("../") - || url.starts_with(".\\") - || url.starts_with("..\\") - { - return normalize_relative_git_plugin_source_url(marketplace_path, url); - } - if url.starts_with("file://") || url.starts_with('/') { - return Ok(url.to_string()); - } - if url.starts_with("ssh://") || url.starts_with("git@") && url.contains(':') { - return Ok(url.to_string()); - } - if let Some(url) = normalize_github_shorthand_url(url) { - return Ok(url); - } - - Err(MarketplaceError::InvalidMarketplaceFile { - path: marketplace_path.to_path_buf(), - message: format!("invalid git plugin source url: {url}"), - }) -} - -fn normalize_relative_git_plugin_source_url( - marketplace_path: &AbsolutePathBuf, - url: &str, -) -> Result { - let mut normalized = marketplace_root_dir(marketplace_path)? - .as_path() - .to_path_buf(); - for segment in url.split(['/', '\\']) { - match segment { - "" | "." => {} - ".." => { - return Err(MarketplaceError::InvalidMarketplaceFile { - path: marketplace_path.to_path_buf(), - message: "relative git plugin source url must stay within the marketplace root" - .to_string(), - }); - } - segment => normalized.push(segment), - } - } - Ok(normalized.display().to_string()) -} - -fn normalize_optional_git_selector(value: &Option) -> Option { - value - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string) -} - -fn normalize_github_git_url(url: &str) -> String { - if url.starts_with("https://github.com/") && !url.ends_with(".git") { - format!("{url}.git") - } else { - url.to_string() - } -} - -fn normalize_github_shorthand_url(source: &str) -> Option { - if !looks_like_github_shorthand(source) { - return None; - } - let mut segments = source.split('/'); - let owner = segments.next()?; - let repo = segments.next()?; - let repo = repo.strip_suffix(".git").unwrap_or(repo); - if repo.is_empty() { - return None; - } - Some(format!("https://github.com/{owner}/{repo}.git")) -} - -fn looks_like_github_shorthand(source: &str) -> bool { - let mut segments = source.split('/'); - let owner = segments.next(); - let repo = segments.next(); - let extra = segments.next(); - owner.is_some_and(is_github_shorthand_segment) - && repo.is_some_and(is_github_shorthand_segment) - && extra.is_none() -} - -fn is_github_shorthand_segment(segment: &str) -> bool { - !segment.is_empty() - && segment - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.')) -} - -pub fn plugin_interface_with_marketplace_category( - mut interface: Option, - category: Option, -) -> Option { - if let Some(category) = category { - // Marketplace taxonomy wins when both sources provide a category. - interface - .get_or_insert_with(PluginManifestInterface::default) - .category = Some(category); - } - interface -} - -fn marketplace_root_dir( - marketplace_path: &AbsolutePathBuf, -) -> Result { - for relative_path in MARKETPLACE_MANIFEST_RELATIVE_PATHS { - if let Some(marketplace_root) = - marketplace_root_from_layout(marketplace_path.as_path(), relative_path) - { - return AbsolutePathBuf::try_from(marketplace_root) - .map_err(|_| invalid_marketplace_layout_error(marketplace_path)); - } - } - - Err(invalid_marketplace_layout_error(marketplace_path)) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawMarketplaceManifest { - name: String, - #[serde(default)] - interface: Option, - plugins: Vec, -} - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawMarketplaceManifestInterface { - #[serde(default)] - display_name: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawMarketplaceManifestPlugin { - name: String, - source: RawMarketplaceManifestPluginSource, - #[serde(default)] - policy: RawMarketplaceManifestPluginPolicy, - #[serde(default)] - category: Option, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawMarketplaceManifestPluginPolicy { - #[serde(default)] - installation: MarketplacePluginInstallPolicy, - #[serde(default)] - authentication: MarketplacePluginAuthPolicy, - products: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum RawMarketplaceManifestPluginSource { - Path(String), - Object(RawMarketplaceManifestPluginSourceObject), - #[allow(dead_code)] - Unsupported(JsonValue), -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "source", rename_all = "lowercase")] -enum RawMarketplaceManifestPluginSourceObject { - Local { - path: String, - }, - Url { - url: String, - path: Option, - #[serde(rename = "ref")] - ref_name: Option, - sha: Option, - }, - #[serde(rename = "git-subdir")] - GitSubdir { - url: String, - path: String, - #[serde(rename = "ref")] - ref_name: Option, - sha: Option, - }, -} - -fn resolve_marketplace_interface( - interface: Option, -) -> Option { - let interface = interface?; - if interface.display_name.is_some() { - Some(MarketplaceInterface { - display_name: interface.display_name, - }) - } else { - None - } -} - -#[cfg(test)] -#[path = "marketplace_tests.rs"] -mod tests; diff --git a/vendor/codex/codex-rs/core-plugins/src/marketplace_tests.rs b/vendor/codex/codex-rs/core-plugins/src/marketplace_tests.rs deleted file mode 100644 index 217e2d82..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/marketplace_tests.rs +++ /dev/null @@ -1,1530 +0,0 @@ -use super::*; -use codex_protocol::protocol::Product; -use pretty_assertions::assert_eq; -use std::path::Path; -use tempfile::tempdir; - -const ALTERNATE_MARKETPLACE_RELATIVE_PATH: &str = ".claude-plugin/marketplace.json"; -const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json"; - -fn write_alternate_marketplace(repo_root: &Path, contents: &str) -> AbsolutePathBuf { - let marketplace_path = repo_root.join(ALTERNATE_MARKETPLACE_RELATIVE_PATH); - fs::create_dir_all(marketplace_path.parent().unwrap()).unwrap(); - fs::write(&marketplace_path, contents).unwrap(); - AbsolutePathBuf::try_from(marketplace_path).unwrap() -} - -fn write_alternate_plugin_manifest(plugin_root: &Path, contents: &str) { - let manifest_path = plugin_root.join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH); - fs::create_dir_all(manifest_path.parent().unwrap()).unwrap(); - fs::write(manifest_path, contents).unwrap(); -} - -#[test] -fn find_marketplace_plugin_finds_repo_marketplace_plugin() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(repo_root.join("nested")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "local-plugin", - "source": { - "source": "local", - "path": "./plugin-1" - } - } - ] -}"#, - ) - .unwrap(); - - let resolved = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "local-plugin", - ) - .unwrap(); - - assert_eq!( - resolved, - ResolvedMarketplacePlugin { - plugin_id: PluginId::new("local-plugin".to_string(), "codex-curated".to_string()) - .unwrap(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("plugin-1")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - manifest: None, - } - ); -} - -#[test] -fn find_marketplace_plugin_supports_alternate_layout_and_string_local_source() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - let marketplace_path = write_alternate_marketplace( - &repo_root, - r#"{ - "name": "alternate-marketplace", - "plugins": [ - { - "name": "string-source-plugin", - "source": "./plugins/string-source-plugin" - } - ] -}"#, - ); - - let resolved = find_marketplace_plugin(&marketplace_path, "string-source-plugin").unwrap(); - - assert_eq!( - resolved, - ResolvedMarketplacePlugin { - plugin_id: PluginId::new( - "string-source-plugin".to_string(), - "alternate-marketplace".to_string() - ) - .unwrap(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("plugins/string-source-plugin")) - .unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - manifest: None, - } - ); -} - -#[test] -fn find_marketplace_plugin_supports_git_subdir_sources() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "remote-plugin", - "source": { - "source": "git-subdir", - "url": "openai/joey_marketplace3", - "path": "plugins/toolkit", - "ref": "main", - "sha": "abc123" - } - } - ] -}"#, - ) - .unwrap(); - - let resolved = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "remote-plugin", - ) - .unwrap(); - - assert_eq!( - resolved, - ResolvedMarketplacePlugin { - plugin_id: PluginId::new("remote-plugin".to_string(), "codex-curated".to_string()) - .unwrap(), - source: MarketplacePluginSource::Git { - url: "https://github.com/openai/joey_marketplace3.git".to_string(), - path: Some("plugins/toolkit".to_string()), - ref_name: Some("main".to_string()), - sha: Some("abc123".to_string()), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - manifest: None, - } - ); -} - -#[test] -fn find_marketplace_plugin_normalizes_github_shorthand_with_dot_git_suffix() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "remote-plugin", - "source": { - "source": "git-subdir", - "url": "openai/toolkit.git", - "path": "plugins/toolkit" - } - } - ] -}"#, - ) - .unwrap(); - - let resolved = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "remote-plugin", - ) - .unwrap(); - - assert_eq!( - resolved.source, - MarketplacePluginSource::Git { - url: "https://github.com/openai/toolkit.git".to_string(), - path: Some("plugins/toolkit".to_string()), - ref_name: None, - sha: None, - } - ); -} - -#[test] -fn find_marketplace_plugin_normalizes_relative_git_source_urls_to_marketplace_root() { - for source_url in ["./remotes/toolkit.git", ".\\remotes\\toolkit.git"] { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - let remote_repo = repo_root.join("remotes").join("toolkit.git"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(&remote_repo).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - format!( - r#"{{ - "name": "codex-curated", - "plugins": [ - {{ - "name": "remote-plugin", - "source": {{ - "source": "git-subdir", - "url": "{}", - "path": "plugins/toolkit" - }} - }} - ] -}}"#, - source_url.replace('\\', "\\\\") - ), - ) - .unwrap(); - - let resolved = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "remote-plugin", - ) - .unwrap(); - - assert_eq!( - resolved.source, - MarketplacePluginSource::Git { - url: remote_repo.display().to_string(), - path: Some("plugins/toolkit".to_string()), - ref_name: None, - sha: None, - } - ); - } -} - -#[test] -fn normalize_relative_git_plugin_source_url_rejects_parent_traversal() { - for source_url in [ - "../toolkit.git", - "./../toolkit.git", - "..\\toolkit.git", - ".\\..\\toolkit.git", - ] { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - let marketplace_path = repo_root.join(".agents/plugins/marketplace.json"); - let marketplace_path = AbsolutePathBuf::try_from(marketplace_path).unwrap(); - let err = - normalize_relative_git_plugin_source_url(&marketplace_path, source_url).unwrap_err(); - - assert_eq!( - err.to_string(), - format!( - "invalid marketplace file `{}`: relative git plugin source url must stay within the marketplace root", - marketplace_path.display() - ) - ); - } -} - -#[test] -fn find_marketplace_plugin_skips_root_equivalent_git_subdir_paths() { - for path in [".", "./", "plugins/.."] { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - format!( - r#"{{ - "name": "codex-curated", - "plugins": [ - {{ - "name": "remote-plugin", - "source": {{ - "source": "git-subdir", - "url": "openai/toolkit", - "path": "{path}" - }} - }} - ] -}}"# - ), - ) - .unwrap(); - - let err = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "remote-plugin", - ) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "plugin `remote-plugin` was not found in marketplace `codex-curated`" - ); - } -} - -#[test] -fn find_marketplace_plugin_reports_missing_plugin() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{"name":"codex-curated","plugins":[]}"#, - ) - .unwrap(); - - let err = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "missing", - ) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "plugin `missing` was not found in marketplace `codex-curated`" - ); -} - -#[test] -fn list_marketplaces_supports_alternate_manifest_layout() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - let plugin_root = repo_root.join("plugins/string-source-plugin"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - write_alternate_plugin_manifest( - &plugin_root, - r#"{ - "name":"string-source-plugin", - "interface": { - "displayName": "String Source Plugin" - } -}"#, - ); - let marketplace_path = write_alternate_marketplace( - &repo_root, - r#"{ - "name": "alternate-marketplace", - "plugins": [ - { - "name": "string-source-plugin", - "source": "./plugins/string-source-plugin" - } - ] -}"#, - ); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces, - vec![Marketplace { - name: "alternate-marketplace".to_string(), - path: marketplace_path, - interface: None, - plugins: vec![MarketplacePlugin { - name: "string-source-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("plugins/string-source-plugin")) - .unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: Some(PluginManifestInterface { - display_name: Some("String Source Plugin".to_string()), - short_description: None, - long_description: None, - developer_name: None, - category: None, - capabilities: Vec::new(), - website_url: None, - privacy_policy_url: None, - terms_of_service_url: None, - default_prompt: None, - brand_color: None, - composer_icon: None, - logo: None, - screenshots: Vec::new(), - }), - }], - }] - ); -} - -#[test] -fn list_marketplaces_includes_plugins_without_discoverable_manifest() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - let marketplace_path = write_alternate_marketplace( - &repo_root, - r#"{ - "name": "alternate-marketplace", - "plugins": [ - { - "name": "missing-plugin", - "source": "./plugins/missing-plugin" - } - ] -}"#, - ); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces, - vec![Marketplace { - name: "alternate-marketplace".to_string(), - path: marketplace_path, - interface: None, - plugins: vec![MarketplacePlugin { - name: "missing-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("plugins/missing-plugin"),) - .unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }], - }] - ); -} - -#[test] -fn list_marketplaces_prefers_first_supported_manifest_layout() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "agents-marketplace", - "plugins": [ - { - "name": "agents-plugin", - "source": { - "source": "local", - "path": "./plugins/agents-plugin" - } - } - ] -}"#, - ) - .unwrap(); - write_alternate_marketplace( - &repo_root, - r#"{ - "name": "alternate-marketplace", - "plugins": [ - { - "name": "string-source-plugin", - "source": "./plugins/string-source-plugin" - } - ] -}"#, - ); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!(marketplaces.len(), 1); - assert_eq!(marketplaces[0].name, "agents-marketplace"); - assert_eq!( - marketplaces[0].path, - AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap() - ); -} - -#[test] -fn list_marketplaces_returns_home_and_repo_marketplaces() { - let tmp = tempdir().unwrap(); - let home_root = tmp.path().join("home"); - let repo_root = tmp.path().join("repo"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(home_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - home_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "shared-plugin", - "source": { - "source": "local", - "path": "./home-shared" - } - }, - { - "name": "home-only", - "source": { - "source": "local", - "path": "./home-only" - } - } - ] -}"#, - ) - .unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "shared-plugin", - "source": { - "source": "local", - "path": "./repo-shared" - } - }, - { - "name": "repo-only", - "source": { - "source": "local", - "path": "./repo-only" - } - } - ] -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()], - Some(&home_root), - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces, - vec![ - Marketplace { - name: "codex-curated".to_string(), - path: - AbsolutePathBuf::try_from(home_root.join(".agents/plugins/marketplace.json"),) - .unwrap(), - interface: None, - plugins: vec![ - MarketplacePlugin { - name: "shared-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(home_root.join("home-shared")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }, - MarketplacePlugin { - name: "home-only".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(home_root.join("home-only")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }, - ], - }, - Marketplace { - name: "codex-curated".to_string(), - path: - AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json"),) - .unwrap(), - interface: None, - plugins: vec![ - MarketplacePlugin { - name: "shared-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("repo-shared")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }, - MarketplacePlugin { - name: "repo-only".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("repo-only")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }, - ], - }, - ] - ); -} - -#[test] -fn list_marketplaces_keeps_distinct_entries_for_same_name() { - let tmp = tempdir().unwrap(); - let home_root = tmp.path().join("home"); - let repo_root = tmp.path().join("repo"); - let home_marketplace = home_root.join(".agents/plugins/marketplace.json"); - let repo_marketplace = repo_root.join(".agents/plugins/marketplace.json"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(home_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - - fs::write( - home_marketplace.clone(), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "local-plugin", - "source": { - "source": "local", - "path": "./home-plugin" - } - } - ] -}"#, - ) - .unwrap(); - fs::write( - repo_marketplace.clone(), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "local-plugin", - "source": { - "source": "local", - "path": "./repo-plugin" - } - } - ] -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()], - Some(&home_root), - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces, - vec![ - Marketplace { - name: "codex-curated".to_string(), - path: AbsolutePathBuf::try_from(home_marketplace).unwrap(), - interface: None, - plugins: vec![MarketplacePlugin { - name: "local-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(home_root.join("home-plugin")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }], - }, - Marketplace { - name: "codex-curated".to_string(), - path: AbsolutePathBuf::try_from(repo_marketplace.clone()).unwrap(), - interface: None, - plugins: vec![MarketplacePlugin { - name: "local-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }], - }, - ] - ); - - let resolved = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_marketplace).unwrap(), - "local-plugin", - ) - .unwrap(); - - assert_eq!( - resolved.source, - MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap(), - } - ); -} - -#[test] -fn list_marketplaces_dedupes_multiple_roots_in_same_repo() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - let nested_root = repo_root.join("nested/project"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(&nested_root).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "local-plugin", - "source": { - "source": "local", - "path": "./plugin" - } - } - ] -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[ - AbsolutePathBuf::try_from(repo_root.clone()).unwrap(), - AbsolutePathBuf::try_from(nested_root).unwrap(), - ], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces, - vec![Marketplace { - name: "codex-curated".to_string(), - path: AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")) - .unwrap(), - interface: None, - plugins: vec![MarketplacePlugin { - name: "local-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("plugin")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }], - }] - ); -} - -#[test] -fn list_marketplaces_reads_marketplace_display_name() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "openai-curated", - "interface": { - "displayName": "ChatGPT Official" - }, - "plugins": [ - { - "name": "local-plugin", - "source": { - "source": "local", - "path": "./plugin" - } - } - ] -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces[0].interface, - Some(MarketplaceInterface { - display_name: Some("ChatGPT Official".to_string()), - }) - ); -} - -#[test] -fn list_marketplaces_skips_invalid_plugins_but_keeps_marketplace() { - let tmp = tempdir().unwrap(); - let valid_repo_root = tmp.path().join("valid-repo"); - let invalid_repo_root = tmp.path().join("invalid-repo"); - - fs::create_dir_all(valid_repo_root.join(".git")).unwrap(); - fs::create_dir_all(valid_repo_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(invalid_repo_root.join(".git")).unwrap(); - fs::create_dir_all(invalid_repo_root.join(".agents/plugins")).unwrap(); - fs::write( - valid_repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "valid-marketplace", - "plugins": [ - { - "name": "valid-plugin", - "source": { - "source": "local", - "path": "./plugin" - } - } - ] -}"#, - ) - .unwrap(); - fs::write( - invalid_repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "invalid-marketplace", - "plugins": [ - { - "name": "broken-plugin", - "source": { - "source": "local", - "path": "plugin-without-dot-slash" - } - } - ] -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[ - AbsolutePathBuf::try_from(valid_repo_root).unwrap(), - AbsolutePathBuf::try_from(invalid_repo_root).unwrap(), - ], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!(marketplaces.len(), 2); - assert_eq!(marketplaces[0].name, "valid-marketplace"); - assert_eq!(marketplaces[1].name, "invalid-marketplace"); - assert!(marketplaces[1].plugins.is_empty()); -} - -#[test] -fn list_marketplaces_skips_plugins_with_invalid_names_but_keeps_marketplace() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "invalid-name-marketplace", - "plugins": [ - { - "name": "valid-plugin", - "source": { - "source": "local", - "path": "./valid-plugin" - } - }, - { - "name": "invalid.plugin", - "source": { - "source": "local", - "path": "./invalid-plugin" - } - } - ] -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces, - vec![Marketplace { - name: "invalid-name-marketplace".to_string(), - path: AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")) - .unwrap(), - interface: None, - plugins: vec![MarketplacePlugin { - name: "valid-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("valid-plugin")).unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }], - }] - ); -} - -#[test] -fn list_marketplaces_reports_marketplace_load_errors() { - let tmp = tempdir().unwrap(); - let valid_repo_root = tmp.path().join("valid-repo"); - let invalid_repo_root = tmp.path().join("invalid-repo"); - - fs::create_dir_all(valid_repo_root.join(".git")).unwrap(); - fs::create_dir_all(valid_repo_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(invalid_repo_root.join(".git")).unwrap(); - fs::create_dir_all(invalid_repo_root.join(".agents/plugins")).unwrap(); - fs::write( - valid_repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "valid-marketplace", - "plugins": [ - { - "name": "valid-plugin", - "source": { - "source": "local", - "path": "./plugin" - } - } - ] -}"#, - ) - .unwrap(); - let invalid_marketplace_path = - AbsolutePathBuf::try_from(invalid_repo_root.join(".agents/plugins/marketplace.json")) - .unwrap(); - fs::write(invalid_marketplace_path.as_path(), "{not json").unwrap(); - - let outcome = list_marketplaces_with_home( - &[ - AbsolutePathBuf::try_from(valid_repo_root).unwrap(), - AbsolutePathBuf::try_from(invalid_repo_root).unwrap(), - ], - /*home_dir*/ None, - ) - .unwrap(); - - assert_eq!(outcome.marketplaces.len(), 1); - assert_eq!(outcome.marketplaces[0].name, "valid-marketplace"); - assert_eq!(outcome.errors.len(), 1); - assert_eq!(outcome.errors[0].path, invalid_marketplace_path); - assert!( - outcome.errors[0] - .message - .contains("invalid marketplace file"), - "unexpected errors: {:?}", - outcome.errors - ); -} - -#[test] -fn list_marketplaces_keeps_remote_and_local_plugin_sources() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - write_alternate_marketplace( - &repo_root, - r#"{ - "name": "mixed-source-marketplace", - "plugins": [ - { - "name": "local-plugin", - "source": "./plugins/local-plugin" - }, - { - "name": "url-plugin", - "source": { - "source": "url", - "url": "https://github.com/example/plugin" - } - }, - { - "name": "git-subdir-plugin", - "source": { - "source": "git-subdir", - "url": "owner/repo", - "path": "plugins/example", - "ref": "main", - "sha": "abc123" - } - } - ] -}"#, - ); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!(marketplaces.len(), 1); - assert_eq!( - marketplaces[0].plugins, - vec![ - MarketplacePlugin { - name: "local-plugin".to_string(), - source: MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("plugins/local-plugin")) - .unwrap(), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }, - MarketplacePlugin { - name: "url-plugin".to_string(), - source: MarketplacePluginSource::Git { - url: "https://github.com/example/plugin.git".to_string(), - path: None, - ref_name: None, - sha: None, - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }, - MarketplacePlugin { - name: "git-subdir-plugin".to_string(), - source: MarketplacePluginSource::Git { - url: "https://github.com/owner/repo.git".to_string(), - path: Some("plugins/example".to_string()), - ref_name: Some("main".to_string()), - sha: Some("abc123".to_string()), - }, - policy: MarketplacePluginPolicy { - installation: MarketplacePluginInstallPolicy::Available, - authentication: MarketplacePluginAuthPolicy::OnInstall, - products: None, - }, - interface: None, - }, - ] - ); -} - -#[test] -fn list_marketplaces_resolves_plugin_interface_paths_to_absolute() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - let plugin_root = repo_root.join("plugins/demo-plugin"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "demo-plugin", - "source": { - "source": "local", - "path": "./plugins/demo-plugin" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL", - "products": ["CODEX", "CHATGPT", "ATLAS"] - }, - "category": "Design" - } - ] -}"#, - ) - .unwrap(); - fs::write( - plugin_root.join(".codex-plugin/plugin.json"), - r#"{ - "name": "demo-plugin", - "interface": { - "displayName": "Demo", - "category": "Productivity", - "capabilities": ["Interactive", "Write"], - "composerIcon": "./assets/icon.png", - "logo": "./assets/logo.png", - "screenshots": ["./assets/shot1.png"] - } -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces[0].plugins[0].policy.installation, - MarketplacePluginInstallPolicy::Available - ); - assert_eq!( - marketplaces[0].plugins[0].policy.authentication, - MarketplacePluginAuthPolicy::OnInstall - ); - assert_eq!( - marketplaces[0].plugins[0].policy.products, - Some(vec![Product::Codex, Product::Chatgpt, Product::Atlas]) - ); - assert_eq!( - marketplaces[0].plugins[0].interface, - Some(PluginManifestInterface { - display_name: Some("Demo".to_string()), - short_description: None, - long_description: None, - developer_name: None, - category: Some("Design".to_string()), - capabilities: vec!["Interactive".to_string(), "Write".to_string()], - website_url: None, - privacy_policy_url: None, - terms_of_service_url: None, - default_prompt: None, - brand_color: None, - composer_icon: Some( - AbsolutePathBuf::try_from(plugin_root.join("assets/icon.png")).unwrap(), - ), - logo: Some(AbsolutePathBuf::try_from(plugin_root.join("assets/logo.png")).unwrap()), - screenshots: vec![ - AbsolutePathBuf::try_from(plugin_root.join("assets/shot1.png")).unwrap(), - ], - }) - ); -} - -#[test] -fn list_marketplaces_ignores_legacy_top_level_policy_fields() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "demo-plugin", - "source": { - "source": "local", - "path": "./plugins/demo-plugin" - }, - "installPolicy": "NOT_AVAILABLE", - "authPolicy": "ON_USE" - } - ] -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces[0].plugins[0].policy.installation, - MarketplacePluginInstallPolicy::Available - ); - assert_eq!( - marketplaces[0].plugins[0].policy.authentication, - MarketplacePluginAuthPolicy::OnInstall - ); - assert_eq!(marketplaces[0].plugins[0].policy.products, None); -} - -#[test] -fn list_marketplaces_ignores_plugin_interface_assets_without_dot_slash() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - let plugin_root = repo_root.join("plugins/demo-plugin"); - - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "demo-plugin", - "source": { - "source": "local", - "path": "./plugins/demo-plugin" - } - } - ] -}"#, - ) - .unwrap(); - fs::write( - plugin_root.join(".codex-plugin/plugin.json"), - r#"{ - "name": "demo-plugin", - "interface": { - "displayName": "Demo", - "capabilities": ["Interactive"], - "composerIcon": "assets/icon.png", - "logo": "/tmp/logo.png", - "screenshots": ["assets/shot1.png"] - } -}"#, - ) - .unwrap(); - - let marketplaces = list_marketplaces_with_home( - &[AbsolutePathBuf::try_from(repo_root).unwrap()], - /*home_dir*/ None, - ) - .unwrap() - .marketplaces; - - assert_eq!( - marketplaces[0].plugins[0].interface, - Some(PluginManifestInterface { - display_name: Some("Demo".to_string()), - short_description: None, - long_description: None, - developer_name: None, - category: None, - capabilities: vec!["Interactive".to_string()], - website_url: None, - privacy_policy_url: None, - terms_of_service_url: None, - default_prompt: None, - brand_color: None, - composer_icon: None, - logo: None, - screenshots: Vec::new(), - }) - ); - assert_eq!( - marketplaces[0].plugins[0].policy.installation, - MarketplacePluginInstallPolicy::Available - ); - assert_eq!( - marketplaces[0].plugins[0].policy.authentication, - MarketplacePluginAuthPolicy::OnInstall - ); - assert_eq!(marketplaces[0].plugins[0].policy.products, None); -} - -#[test] -fn find_marketplace_plugin_skips_invalid_local_paths() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "local-plugin", - "source": { - "source": "local", - "path": "../plugin-1" - } - } - ] -}"#, - ) - .unwrap(); - - let err = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "local-plugin", - ) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "plugin `local-plugin` was not found in marketplace `codex-curated`" - ); -} - -#[test] -fn find_marketplace_plugin_uses_first_duplicate_entry() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "local-plugin", - "source": { - "source": "local", - "path": "./first" - } - }, - { - "name": "local-plugin", - "source": { - "source": "local", - "path": "./second" - } - } - ] -}"#, - ) - .unwrap(); - - let resolved = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "local-plugin", - ) - .unwrap(); - - assert_eq!( - resolved.source, - MarketplacePluginSource::Local { - path: AbsolutePathBuf::try_from(repo_root.join("first")).unwrap(), - } - ); -} - -#[test] -fn find_installable_marketplace_plugin_rejects_disallowed_product() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "chatgpt-plugin", - "source": { - "source": "local", - "path": "./plugin" - }, - "policy": { - "products": ["CHATGPT"] - } - } - ] -}"#, - ) - .unwrap(); - - let err = find_installable_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "chatgpt-plugin", - Some(Product::Atlas), - ) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "plugin `chatgpt-plugin` is not available for install in marketplace `codex-curated`" - ); -} - -#[test] -fn find_marketplace_plugin_allows_missing_products_field() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "default-plugin", - "source": { - "source": "local", - "path": "./plugin" - }, - "policy": {} - } - ] -}"#, - ) - .unwrap(); - - let resolved = find_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "default-plugin", - ) - .unwrap(); - - assert_eq!(resolved.plugin_id.as_key(), "default-plugin@codex-curated"); -} - -#[test] -fn find_installable_marketplace_plugin_rejects_explicit_empty_products() { - let tmp = tempdir().unwrap(); - let repo_root = tmp.path().join("repo"); - fs::create_dir_all(repo_root.join(".git")).unwrap(); - fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap(); - fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "codex-curated", - "plugins": [ - { - "name": "disabled-plugin", - "source": { - "source": "local", - "path": "./plugin" - }, - "policy": { - "products": [] - } - } - ] -}"#, - ) - .unwrap(); - - let err = find_installable_marketplace_plugin( - &AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(), - "disabled-plugin", - Some(Product::Codex), - ) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "plugin `disabled-plugin` is not available for install in marketplace `codex-curated`" - ); -} diff --git a/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade.rs b/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade.rs deleted file mode 100644 index 81474cf6..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade.rs +++ /dev/null @@ -1,298 +0,0 @@ -mod activation; -mod git; - -use self::activation::activate_marketplace_root; -use self::activation::installed_marketplace_metadata_matches; -use self::activation::write_installed_marketplace_metadata; -use self::git::clone_git_source; -use self::git::git_remote_revision; -use crate::marketplace::validate_marketplace_root; -use codex_config::CONFIG_TOML_FILE; -use codex_config::ConfigLayerStack; -use codex_config::MarketplaceConfigUpdate; -use codex_config::record_user_marketplace; -use codex_config::types::MarketplaceConfig; -use codex_config::types::MarketplaceSourceType; -use codex_plugin::validate_plugin_segment; -use codex_utils_absolute_path::AbsolutePathBuf; -use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; -use std::time::Duration; -use tracing::warn; - -const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces"; -const MARKETPLACE_UPGRADE_GIT_TIMEOUT: Duration = Duration::from_secs(30); - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConfiguredMarketplaceUpgradeError { - pub marketplace_name: String, - pub message: String, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct ConfiguredMarketplaceUpgradeOutcome { - pub selected_marketplaces: Vec, - pub upgraded_roots: Vec, - pub errors: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ConfiguredGitMarketplace { - name: String, - source: String, - ref_name: Option, - sparse_paths: Vec, - last_revision: Option, -} - -impl ConfiguredMarketplaceUpgradeOutcome { - pub fn all_succeeded(&self) -> bool { - self.errors.is_empty() - } -} - -pub fn configured_git_marketplace_names(config_layer_stack: &ConfigLayerStack) -> Vec { - let mut names = configured_git_marketplaces(config_layer_stack) - .into_iter() - .map(|marketplace| marketplace.name) - .collect::>(); - names.sort_unstable(); - names -} - -pub fn upgrade_configured_git_marketplaces( - codex_home: &Path, - config_layer_stack: &ConfigLayerStack, - marketplace_name: Option<&str>, -) -> ConfiguredMarketplaceUpgradeOutcome { - let marketplaces = configured_git_marketplaces(config_layer_stack) - .into_iter() - .filter(|marketplace| marketplace_name.is_none_or(|name| marketplace.name.as_str() == name)) - .collect::>(); - if marketplaces.is_empty() { - return ConfiguredMarketplaceUpgradeOutcome::default(); - } - - let install_root = marketplace_install_root(codex_home); - let selected_marketplaces = marketplaces - .iter() - .map(|marketplace| marketplace.name.clone()) - .collect(); - let mut upgraded_roots = Vec::new(); - let mut errors = Vec::new(); - for marketplace in marketplaces { - match upgrade_configured_git_marketplace(codex_home, &install_root, &marketplace) { - Ok(Some(upgraded_root)) => upgraded_roots.push(upgraded_root), - Ok(None) => {} - Err(err) => { - errors.push(ConfiguredMarketplaceUpgradeError { - marketplace_name: marketplace.name, - message: err, - }); - } - } - } - - ConfiguredMarketplaceUpgradeOutcome { - selected_marketplaces, - upgraded_roots, - errors, - } -} - -fn marketplace_install_root(codex_home: &Path) -> PathBuf { - codex_home.join(INSTALLED_MARKETPLACES_DIR) -} - -fn configured_git_marketplaces( - config_layer_stack: &ConfigLayerStack, -) -> Vec { - let Some(user_layer) = config_layer_stack.get_user_layer() else { - return Vec::new(); - }; - let Some(marketplaces_value) = user_layer.config.get("marketplaces") else { - return Vec::new(); - }; - let marketplaces = match marketplaces_value - .clone() - .try_into::>() - { - Ok(marketplaces) => marketplaces, - Err(err) => { - warn!("invalid marketplaces config while preparing auto-upgrade: {err}"); - return Vec::new(); - } - }; - - let mut configured = marketplaces - .into_iter() - .filter_map(|(name, marketplace)| configured_git_marketplace_from_config(name, marketplace)) - .collect::>(); - configured.sort_unstable_by(|left, right| left.name.cmp(&right.name)); - configured -} - -fn configured_git_marketplace_from_config( - name: String, - marketplace: MarketplaceConfig, -) -> Option { - let MarketplaceConfig { - last_updated: _, - last_revision, - source_type, - source, - ref_name, - sparse_paths, - } = marketplace; - if source_type != Some(MarketplaceSourceType::Git) { - return None; - } - let Some(source) = source else { - warn!( - marketplace = name, - "ignoring configured Git marketplace without source" - ); - return None; - }; - Some(ConfiguredGitMarketplace { - name, - source, - ref_name, - sparse_paths: sparse_paths.unwrap_or_default(), - last_revision, - }) -} - -fn upgrade_configured_git_marketplace( - codex_home: &Path, - install_root: &Path, - marketplace: &ConfiguredGitMarketplace, -) -> Result, String> { - validate_plugin_segment(&marketplace.name, "marketplace name")?; - let remote_revision = git_remote_revision( - &marketplace.source, - marketplace.ref_name.as_deref(), - MARKETPLACE_UPGRADE_GIT_TIMEOUT, - )?; - let destination = install_root.join(&marketplace.name); - if destination - .join(".agents/plugins/marketplace.json") - .is_file() - && marketplace.last_revision.as_deref() == Some(remote_revision.as_str()) - && installed_marketplace_metadata_matches(&destination, marketplace, &remote_revision) - { - return Ok(None); - } - - let staging_parent = install_root.join(".staging"); - std::fs::create_dir_all(&staging_parent).map_err(|err| { - format!( - "failed to create marketplace upgrade staging directory {}: {err}", - staging_parent.display() - ) - })?; - let staged_dir = tempfile::Builder::new() - .prefix("marketplace-upgrade-") - .tempdir_in(&staging_parent) - .map_err(|err| { - format!( - "failed to create temporary marketplace upgrade directory in {}: {err}", - staging_parent.display() - ) - })?; - - let activated_revision = clone_git_source( - &marketplace.source, - marketplace.ref_name.as_deref(), - &marketplace.sparse_paths, - staged_dir.path(), - MARKETPLACE_UPGRADE_GIT_TIMEOUT, - )?; - let marketplace_name = validate_marketplace_root(staged_dir.path()) - .map_err(|err| format!("failed to validate upgraded marketplace root: {err}"))?; - if marketplace_name != marketplace.name { - return Err(format!( - "upgraded marketplace name `{marketplace_name}` does not match configured marketplace `{}`", - marketplace.name - )); - } - write_installed_marketplace_metadata(staged_dir.path(), marketplace, &activated_revision)?; - - let last_updated = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); - let update = MarketplaceConfigUpdate { - last_updated: &last_updated, - last_revision: Some(&activated_revision), - source_type: "git", - source: &marketplace.source, - ref_name: marketplace.ref_name.as_deref(), - sparse_paths: &marketplace.sparse_paths, - }; - activate_marketplace_root(&destination, staged_dir, || { - ensure_configured_git_marketplace_unchanged(codex_home, marketplace)?; - record_user_marketplace(codex_home, &marketplace.name, &update).map_err(|err| { - format!( - "failed to record upgraded marketplace `{}` in user config.toml: {err}", - marketplace.name - ) - }) - })?; - - AbsolutePathBuf::try_from(destination) - .map(Some) - .map_err(|err| format!("upgraded marketplace path is not absolute: {err}")) -} -fn ensure_configured_git_marketplace_unchanged( - codex_home: &Path, - expected: &ConfiguredGitMarketplace, -) -> Result<(), String> { - let current = read_configured_git_marketplace(codex_home, &expected.name)?; - match current { - Some(current) if current == *expected => Ok(()), - Some(_) => Err(format!( - "configured marketplace `{}` changed while auto-upgrade was in flight", - expected.name - )), - None => Err(format!( - "configured marketplace `{}` was removed or is no longer a Git marketplace", - expected.name - )), - } -} - -fn read_configured_git_marketplace( - codex_home: &Path, - marketplace_name: &str, -) -> Result, String> { - let config_path = codex_home.join(CONFIG_TOML_FILE); - let raw_config = match std::fs::read_to_string(&config_path) { - Ok(raw_config) => raw_config, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => { - return Err(format!( - "failed to read user config {} while checking marketplace auto-upgrade: {err}", - config_path.display() - )); - } - }; - let config: toml::Value = toml::from_str(&raw_config).map_err(|err| { - format!( - "failed to parse user config {} while checking marketplace auto-upgrade: {err}", - config_path.display() - ) - })?; - let Some(marketplaces_value) = config.get("marketplaces") else { - return Ok(None); - }; - let mut marketplaces = marketplaces_value - .clone() - .try_into::>() - .map_err(|err| format!("invalid marketplaces config while checking auto-upgrade: {err}"))?; - let Some(marketplace) = marketplaces.remove(marketplace_name) else { - return Ok(None); - }; - Ok(configured_git_marketplace_from_config( - marketplace_name.to_string(), - marketplace, - )) -} diff --git a/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade/activation.rs b/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade/activation.rs deleted file mode 100644 index 366b35fb..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade/activation.rs +++ /dev/null @@ -1,167 +0,0 @@ -use super::ConfiguredGitMarketplace; -use codex_config::types::MarketplaceSourceType; -use serde::Deserialize; -use serde::Serialize; -use std::path::Path; -use std::path::PathBuf; -use tempfile::TempDir; -use tracing::warn; - -const MARKETPLACE_INSTALL_METADATA_FILE: &str = ".codex-marketplace-install.json"; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -struct InstalledMarketplaceMetadata { - source_type: MarketplaceSourceType, - source: String, - ref_name: Option, - sparse_paths: Vec, - revision: String, -} - -pub(super) fn installed_marketplace_metadata_matches( - root: &Path, - marketplace: &ConfiguredGitMarketplace, - revision: &str, -) -> bool { - let metadata = match std::fs::read_to_string(installed_marketplace_metadata_path(root)) { - Ok(metadata) => metadata, - Err(_) => return false, - }; - let metadata = match serde_json::from_str::(&metadata) { - Ok(metadata) => metadata, - Err(err) => { - warn!( - marketplace = marketplace.name, - error = %err, - "failed to parse activated marketplace metadata" - ); - return false; - } - }; - metadata == installed_marketplace_metadata(marketplace, revision) -} - -pub(super) fn write_installed_marketplace_metadata( - root: &Path, - marketplace: &ConfiguredGitMarketplace, - revision: &str, -) -> Result<(), String> { - let metadata = installed_marketplace_metadata(marketplace, revision); - let contents = serde_json::to_string_pretty(&metadata) - .map_err(|err| format!("failed to serialize activated marketplace metadata: {err}"))?; - std::fs::write(installed_marketplace_metadata_path(root), contents) - .map_err(|err| format!("failed to write activated marketplace metadata: {err}")) -} - -pub(super) fn activate_marketplace_root( - destination: &Path, - staged_dir: TempDir, - after_activate: impl FnOnce() -> Result<(), String>, -) -> Result<(), String> { - let staged_root = staged_dir.path(); - let Some(parent) = destination.parent() else { - return Err(format!( - "failed to determine marketplace install parent for {}", - destination.display() - )); - }; - std::fs::create_dir_all(parent).map_err(|err| { - format!( - "failed to create marketplace install parent {}: {err}", - parent.display() - ) - })?; - - if destination.exists() { - let backup_dir = tempfile::Builder::new() - .prefix("marketplace-backup-") - .tempdir_in(parent) - .map_err(|err| { - format!( - "failed to create marketplace backup directory in {}: {err}", - parent.display() - ) - })?; - let backup_root = backup_dir.path().join("root"); - std::fs::rename(destination, &backup_root).map_err(|err| { - format!( - "failed to move previous marketplace root out of the way at {}: {err}", - destination.display() - ) - })?; - - if let Err(err) = std::fs::rename(staged_root, destination) { - let rollback_result = std::fs::rename(&backup_root, destination); - return match rollback_result { - Ok(()) => Err(format!( - "failed to activate upgraded marketplace at {}: {err}", - destination.display() - )), - Err(rollback_err) => { - let backup_path = backup_dir.keep().join("root"); - Err(format!( - "failed to activate upgraded marketplace at {}: {err}; failed to restore previous marketplace root (left at {}): {rollback_err}", - destination.display(), - backup_path.display() - )) - } - }; - } - - if let Err(err) = after_activate() { - let remove_result = std::fs::remove_dir_all(destination); - let rollback_result = - remove_result.and_then(|()| std::fs::rename(&backup_root, destination)); - return match rollback_result { - Ok(()) => Err(err), - Err(rollback_err) => { - let backup_path = backup_dir.keep().join("root"); - Err(format!( - "{err}; failed to restore previous marketplace root at {} (left at {}): {rollback_err}", - destination.display(), - backup_path.display() - )) - } - }; - } - - return Ok(()); - } - - std::fs::rename(staged_root, destination).map_err(|err| { - format!( - "failed to activate upgraded marketplace at {}: {err}", - destination.display() - ) - })?; - if let Err(err) = after_activate() { - let remove_result = std::fs::remove_dir_all(destination); - return match remove_result { - Ok(()) => Err(err), - Err(remove_err) => Err(format!( - "{err}; failed to remove newly activated marketplace root at {}: {remove_err}", - destination.display() - )), - }; - } - - Ok(()) -} - -fn installed_marketplace_metadata( - marketplace: &ConfiguredGitMarketplace, - revision: &str, -) -> InstalledMarketplaceMetadata { - InstalledMarketplaceMetadata { - source_type: MarketplaceSourceType::Git, - source: marketplace.source.clone(), - ref_name: marketplace.ref_name.clone(), - sparse_paths: marketplace.sparse_paths.clone(), - revision: revision.to_string(), - } -} - -fn installed_marketplace_metadata_path(root: &Path) -> PathBuf { - root.join(MARKETPLACE_INSTALL_METADATA_FILE) -} diff --git a/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade/git.rs b/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade/git.rs deleted file mode 100644 index 80a7c68f..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/marketplace_upgrade/git.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::path::Path; -use std::process::Command; -use std::process::Output; -use std::process::Stdio; -use std::time::Duration; - -pub(super) fn git_remote_revision( - source: &str, - ref_name: Option<&str>, - timeout: Duration, -) -> Result { - if let Some(ref_name) = ref_name - && is_full_git_sha(ref_name) - { - return Ok(ref_name.to_string()); - } - - let ref_name = ref_name.unwrap_or("HEAD"); - let output = run_git_command_with_timeout( - git_command().arg("ls-remote").arg(source).arg(ref_name), - "git ls-remote marketplace source", - timeout, - )?; - ensure_git_success(&output, "git ls-remote marketplace source")?; - - let stdout = String::from_utf8_lossy(&output.stdout); - let Some(first_line) = stdout.lines().next() else { - return Err("git ls-remote returned empty output for marketplace source".to_string()); - }; - let Some((revision, _)) = first_line.split_once('\t') else { - return Err(format!( - "unexpected git ls-remote output for marketplace source: {first_line}" - )); - }; - let revision = revision.trim(); - if revision.is_empty() { - return Err("git ls-remote returned empty revision for marketplace source".to_string()); - } - Ok(revision.to_string()) -} - -pub(super) fn clone_git_source( - source: &str, - ref_name: Option<&str>, - sparse_paths: &[String], - destination: &Path, - timeout: Duration, -) -> Result { - if sparse_paths.is_empty() { - let output = run_git_command_with_timeout( - git_command().arg("clone").arg(source).arg(destination), - "git clone marketplace source", - timeout, - )?; - ensure_git_success(&output, "git clone marketplace source")?; - if let Some(ref_name) = ref_name { - let output = run_git_command_with_timeout( - git_command() - .arg("-C") - .arg(destination) - .arg("checkout") - .arg(ref_name), - "git checkout marketplace ref", - timeout, - )?; - ensure_git_success(&output, "git checkout marketplace ref")?; - } - return git_worktree_revision(destination, timeout); - } - - let output = run_git_command_with_timeout( - git_command() - .arg("clone") - .arg("--filter=blob:none") - .arg("--no-checkout") - .arg(source) - .arg(destination), - "git clone marketplace source", - timeout, - )?; - ensure_git_success(&output, "git clone marketplace source")?; - - let mut sparse_checkout = git_command(); - sparse_checkout - .arg("-C") - .arg(destination) - .arg("sparse-checkout") - .arg("set") - .args(sparse_paths); - let output = run_git_command_with_timeout( - &mut sparse_checkout, - "git sparse-checkout marketplace source", - timeout, - )?; - ensure_git_success(&output, "git sparse-checkout marketplace source")?; - - let output = run_git_command_with_timeout( - git_command() - .arg("-C") - .arg(destination) - .arg("checkout") - .arg(ref_name.unwrap_or("HEAD")), - "git checkout marketplace ref", - timeout, - )?; - ensure_git_success(&output, "git checkout marketplace ref")?; - git_worktree_revision(destination, timeout) -} - -fn git_worktree_revision(destination: &Path, timeout: Duration) -> Result { - let output = run_git_command_with_timeout( - git_command() - .arg("-C") - .arg(destination) - .arg("rev-parse") - .arg("HEAD"), - "git rev-parse marketplace revision", - timeout, - )?; - ensure_git_success(&output, "git rev-parse marketplace revision")?; - - let revision = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if revision.is_empty() { - Err("git rev-parse returned empty revision for marketplace source".to_string()) - } else { - Ok(revision) - } -} - -fn is_full_git_sha(value: &str) -> bool { - value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit()) -} - -fn git_command() -> Command { - let mut command = Command::new("git"); - command - .env("GIT_OPTIONAL_LOCKS", "0") - .env("GIT_TERMINAL_PROMPT", "0"); - command -} - -fn run_git_command_with_timeout( - command: &mut Command, - context: &str, - timeout: Duration, -) -> Result { - let mut child = command - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|err| format!("failed to run {context}: {err}"))?; - let start = std::time::Instant::now(); - loop { - match child.try_wait() { - Ok(Some(_)) => { - return child - .wait_with_output() - .map_err(|err| format!("failed to wait for {context}: {err}")); - } - Ok(None) => {} - Err(err) => return Err(format!("failed to poll {context}: {err}")), - } - - if start.elapsed() >= timeout { - let _ = child.kill(); - let output = child - .wait_with_output() - .map_err(|err| format!("failed to wait for {context} after timeout: {err}"))?; - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - return if stderr.is_empty() { - Err(format!("{context} timed out after {}s", timeout.as_secs())) - } else { - Err(format!( - "{context} timed out after {}s: {stderr}", - timeout.as_secs() - )) - }; - } - - std::thread::sleep(Duration::from_millis(100)); - } -} - -fn ensure_git_success(output: &Output, context: &str) -> Result<(), String> { - if output.status.success() { - return Ok(()); - } - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - if stderr.is_empty() { - Err(format!("{context} failed with status {}", output.status)) - } else { - Err(format!( - "{context} failed with status {}: {stderr}", - output.status - )) - } -} - -#[cfg(test)] -mod tests { - use super::git_command; - use super::is_full_git_sha; - use std::ffi::OsStr; - - #[test] - fn full_git_sha_ref_is_already_a_remote_revision() { - assert!(is_full_git_sha("0123456789abcdef0123456789abcdef01234567")); - assert!(!is_full_git_sha("main")); - assert!(!is_full_git_sha("0123456")); - } - - #[test] - fn git_command_uses_path_lookup_with_stable_noninteractive_env() { - let command = git_command(); - - assert_eq!(command.get_program(), OsStr::new("git")); - assert_eq!( - command_env(&command, "GIT_OPTIONAL_LOCKS"), - Some(Some(OsStr::new("0"))) - ); - assert_eq!( - command_env(&command, "GIT_TERMINAL_PROMPT"), - Some(Some(OsStr::new("0"))) - ); - assert_eq!(command_env(&command, "PATH"), None); - } - - fn command_env<'a>( - command: &'a std::process::Command, - name: &str, - ) -> Option> { - command - .get_envs() - .find(|(key, _)| key == &OsStr::new(name)) - .map(|(_, value)| value) - } -} diff --git a/vendor/codex/codex-rs/core-plugins/src/remote.rs b/vendor/codex/codex-rs/core-plugins/src/remote.rs deleted file mode 100644 index 89dabc84..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/remote.rs +++ /dev/null @@ -1,656 +0,0 @@ -use codex_app_server_protocol::PluginAuthPolicy; -use codex_app_server_protocol::PluginInstallPolicy; -use codex_app_server_protocol::PluginInterface; -use codex_app_server_protocol::SkillInterface; -use codex_login::CodexAuth; -use codex_login::default_client::build_reqwest_client; -use reqwest::RequestBuilder; -use serde::Deserialize; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::collections::HashSet; -use std::time::Duration; - -pub const REMOTE_GLOBAL_MARKETPLACE_NAME: &str = "chatgpt-global"; -pub const REMOTE_WORKSPACE_MARKETPLACE_NAME: &str = "chatgpt-workspace"; -pub const REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME: &str = "ChatGPT Plugins"; -pub const REMOTE_WORKSPACE_MARKETPLACE_DISPLAY_NAME: &str = "ChatGPT Workspace Plugins"; - -const REMOTE_PLUGIN_CATALOG_TIMEOUT: Duration = Duration::from_secs(30); -const MAX_REMOTE_DEFAULT_PROMPT_LEN: usize = 128; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RemotePluginServiceConfig { - pub chatgpt_base_url: String, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct RemoteMarketplace { - pub name: String, - pub display_name: String, - pub plugins: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct RemotePluginSummary { - pub id: String, - pub name: String, - pub installed: bool, - pub enabled: bool, - pub install_policy: PluginInstallPolicy, - pub auth_policy: PluginAuthPolicy, - pub interface: Option, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct RemotePluginDetail { - pub marketplace_name: String, - pub marketplace_display_name: String, - pub summary: RemotePluginSummary, - pub description: Option, - pub skills: Vec, - pub app_ids: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct RemotePluginSkill { - pub name: String, - pub description: String, - pub short_description: Option, - pub interface: Option, - pub enabled: bool, -} - -#[derive(Debug, thiserror::Error)] -pub enum RemotePluginCatalogError { - #[error("chatgpt authentication required for remote plugin catalog")] - AuthRequired, - - #[error( - "chatgpt authentication required for remote plugin catalog; api key auth is not supported" - )] - UnsupportedAuthMode, - - #[error("failed to read auth token for remote plugin catalog: {0}")] - AuthToken(#[source] std::io::Error), - - #[error("failed to send remote plugin catalog request to {url}: {source}")] - Request { - url: String, - #[source] - source: reqwest::Error, - }, - - #[error("remote plugin catalog request to {url} failed with status {status}: {body}")] - UnexpectedStatus { - url: String, - status: reqwest::StatusCode, - body: String, - }, - - #[error("failed to parse remote plugin catalog response from {url}: {source}")] - Decode { - url: String, - #[source] - source: serde_json::Error, - }, - - #[error("remote marketplace `{marketplace_name}` is not supported")] - UnknownMarketplace { marketplace_name: String }, - - #[error( - "remote plugin `{plugin_id}` belongs to marketplace `{actual_marketplace_name}`, not `{expected_marketplace_name}`" - )] - MarketplaceMismatch { - plugin_id: String, - expected_marketplace_name: String, - actual_marketplace_name: String, - }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] -enum RemotePluginScope { - #[serde(rename = "GLOBAL")] - Global, - #[serde(rename = "WORKSPACE")] - Workspace, -} - -impl RemotePluginScope { - fn all() -> [Self; 2] { - [Self::Global, Self::Workspace] - } - - fn api_value(self) -> &'static str { - match self { - Self::Global => "GLOBAL", - Self::Workspace => "WORKSPACE", - } - } - - fn marketplace_name(self) -> &'static str { - match self { - Self::Global => REMOTE_GLOBAL_MARKETPLACE_NAME, - Self::Workspace => REMOTE_WORKSPACE_MARKETPLACE_NAME, - } - } - - fn marketplace_display_name(self) -> &'static str { - match self { - Self::Global => REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME, - Self::Workspace => REMOTE_WORKSPACE_MARKETPLACE_DISPLAY_NAME, - } - } - - fn from_marketplace_name(name: &str) -> Option { - match name { - REMOTE_GLOBAL_MARKETPLACE_NAME => Some(Self::Global), - REMOTE_WORKSPACE_MARKETPLACE_NAME => Some(Self::Workspace), - _ => None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginPagination { - #[serde(alias = "nextPageToken")] - next_page_token: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginSkillInterfaceResponse { - #[serde(alias = "displayName")] - display_name: Option, - #[serde(alias = "shortDescription")] - short_description: Option, - #[serde(alias = "brandColor")] - brand_color: Option, - #[serde(alias = "defaultPrompt")] - default_prompt: Option, - #[serde(alias = "iconSmallUrl")] - icon_small_url: Option, - #[serde(alias = "iconLargeUrl")] - icon_large_url: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginSkillResponse { - name: String, - description: String, - interface: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginReleaseInterfaceResponse { - #[serde(alias = "shortDescription")] - short_description: Option, - #[serde(alias = "longDescription")] - long_description: Option, - #[serde(alias = "developerName")] - developer_name: Option, - category: Option, - #[serde(default)] - capabilities: Vec, - #[serde(alias = "websiteUrl")] - website_url: Option, - #[serde(alias = "privacyPolicyUrl")] - privacy_policy_url: Option, - #[serde(alias = "termsOfServiceUrl")] - terms_of_service_url: Option, - #[serde(alias = "brandColor")] - brand_color: Option, - #[serde(alias = "defaultPrompt")] - default_prompt: Option, - #[serde(alias = "composerIconUrl")] - composer_icon_url: Option, - #[serde(alias = "logoUrl")] - logo_url: Option, - #[serde(default)] - #[serde(alias = "screenshotUrls")] - screenshot_urls: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginReleaseResponse { - #[serde(alias = "displayName")] - display_name: String, - description: String, - #[serde(default)] - #[serde(alias = "appIds")] - app_ids: Vec, - interface: RemotePluginReleaseInterfaceResponse, - #[serde(default)] - skills: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginDirectoryItem { - id: String, - name: String, - scope: RemotePluginScope, - #[serde(alias = "installationPolicy")] - installation_policy: PluginInstallPolicy, - #[serde(alias = "authenticationPolicy")] - authentication_policy: PluginAuthPolicy, - release: RemotePluginReleaseResponse, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginInstalledItem { - #[serde(flatten)] - plugin: RemotePluginDirectoryItem, - enabled: bool, - #[serde(default)] - #[serde(alias = "disabledSkillNames")] - disabled_skill_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginListResponse { - plugins: Vec, - pagination: RemotePluginPagination, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemotePluginInstalledResponse { - plugins: Vec, - pagination: RemotePluginPagination, -} - -pub async fn fetch_remote_marketplaces( - config: &RemotePluginServiceConfig, - auth: Option<&CodexAuth>, -) -> Result, RemotePluginCatalogError> { - let auth = ensure_chatgpt_auth(auth)?; - let mut directory_by_scope = - BTreeMap::>::new(); - let mut installed_by_scope = - BTreeMap::>::new(); - - let global = async { - let scope = RemotePluginScope::Global; - let (directory_plugins, installed_plugins) = tokio::try_join!( - fetch_directory_plugins_for_scope(config, auth, scope), - fetch_installed_plugins_for_scope(config, auth, scope), - )?; - Ok::<_, RemotePluginCatalogError>((scope, directory_plugins, installed_plugins)) - }; - let workspace = async { - let scope = RemotePluginScope::Workspace; - let (directory_plugins, installed_plugins) = tokio::try_join!( - fetch_directory_plugins_for_scope(config, auth, scope), - fetch_installed_plugins_for_scope(config, auth, scope), - )?; - Ok::<_, RemotePluginCatalogError>((scope, directory_plugins, installed_plugins)) - }; - - let (global, workspace) = tokio::try_join!(global, workspace)?; - for (scope, directory_plugins, installed_plugins) in [global, workspace] { - if !directory_plugins.is_empty() { - directory_by_scope.insert( - scope, - directory_plugins - .into_iter() - .map(|plugin| (plugin.id.clone(), plugin)) - .collect(), - ); - } - if !installed_plugins.is_empty() { - installed_by_scope.insert( - scope, - installed_plugins - .into_iter() - .map(|plugin| (plugin.plugin.id.clone(), plugin)) - .collect(), - ); - } - } - - let mut marketplaces = Vec::new(); - for scope in RemotePluginScope::all() { - let directory_plugins = directory_by_scope.get(&scope); - let installed_plugins = installed_by_scope.get(&scope); - let plugin_ids = directory_plugins - .into_iter() - .flat_map(|plugins| plugins.keys()) - .chain( - installed_plugins - .into_iter() - .flat_map(|plugins| plugins.keys()), - ) - .cloned() - .collect::>(); - if plugin_ids.is_empty() { - continue; - } - - let mut plugins = plugin_ids - .into_iter() - .filter_map(|plugin_id| { - let directory_plugin = - directory_plugins.and_then(|plugins| plugins.get(&plugin_id)); - let installed_plugin = - installed_plugins.and_then(|plugins| plugins.get(&plugin_id)); - directory_plugin - .or_else(|| installed_plugin.map(|plugin| &plugin.plugin)) - .map(|plugin| build_remote_plugin_summary(plugin, installed_plugin)) - }) - .collect::>(); - plugins.sort_by(|left, right| { - remote_plugin_display_name(left) - .to_ascii_lowercase() - .cmp(&remote_plugin_display_name(right).to_ascii_lowercase()) - .then_with(|| { - remote_plugin_display_name(left).cmp(remote_plugin_display_name(right)) - }) - .then_with(|| left.id.cmp(&right.id)) - }); - marketplaces.push(RemoteMarketplace { - name: scope.marketplace_name().to_string(), - display_name: scope.marketplace_display_name().to_string(), - plugins, - }); - } - - Ok(marketplaces) -} - -pub async fn fetch_remote_plugin_detail( - config: &RemotePluginServiceConfig, - auth: Option<&CodexAuth>, - marketplace_name: &str, - plugin_id: &str, -) -> Result { - let auth = ensure_chatgpt_auth(auth)?; - let scope = RemotePluginScope::from_marketplace_name(marketplace_name).ok_or_else(|| { - RemotePluginCatalogError::UnknownMarketplace { - marketplace_name: marketplace_name.to_string(), - } - })?; - let plugin = fetch_plugin_detail(config, auth, plugin_id).await?; - let actual_marketplace_name = plugin.scope.marketplace_name(); - if actual_marketplace_name != marketplace_name { - return Err(RemotePluginCatalogError::MarketplaceMismatch { - plugin_id: plugin_id.to_string(), - expected_marketplace_name: marketplace_name.to_string(), - actual_marketplace_name: actual_marketplace_name.to_string(), - }); - } - - let installed_plugin = fetch_installed_plugins_for_scope(config, auth, scope) - .await? - .into_iter() - .find(|installed_plugin| installed_plugin.plugin.id == plugin_id); - let disabled_skill_names = installed_plugin - .as_ref() - .map(|plugin| { - plugin - .disabled_skill_names - .iter() - .cloned() - .collect::>() - }) - .unwrap_or_default(); - let skills = plugin - .release - .skills - .iter() - .map(|skill| RemotePluginSkill { - name: skill.name.clone(), - description: skill.description.clone(), - short_description: skill - .interface - .as_ref() - .and_then(|interface| interface.short_description.clone()), - interface: remote_skill_interface_to_info(skill.interface.clone()), - enabled: !disabled_skill_names.contains(&skill.name), - }) - .collect(); - - Ok(RemotePluginDetail { - marketplace_name: marketplace_name.to_string(), - marketplace_display_name: scope.marketplace_display_name().to_string(), - summary: build_remote_plugin_summary(&plugin, installed_plugin.as_ref()), - description: non_empty_string(Some(&plugin.release.description)), - skills, - app_ids: plugin.release.app_ids, - }) -} - -fn build_remote_plugin_summary( - plugin: &RemotePluginDirectoryItem, - installed_plugin: Option<&RemotePluginInstalledItem>, -) -> RemotePluginSummary { - RemotePluginSummary { - id: plugin.id.clone(), - name: plugin.name.clone(), - installed: installed_plugin.is_some(), - enabled: installed_plugin.is_some_and(|plugin| plugin.enabled), - install_policy: plugin.installation_policy, - auth_policy: plugin.authentication_policy, - interface: remote_plugin_interface_to_info(plugin), - } -} - -fn remote_plugin_interface_to_info(plugin: &RemotePluginDirectoryItem) -> Option { - let interface = &plugin.release.interface; - let display_name = non_empty_string(Some(&plugin.release.display_name)); - let default_prompt = interface - .default_prompt - .as_ref() - .and_then(|prompt| normalize_remote_default_prompt(prompt)); - let result = PluginInterface { - display_name, - short_description: interface.short_description.clone(), - long_description: interface.long_description.clone(), - developer_name: interface.developer_name.clone(), - category: interface.category.clone(), - capabilities: interface.capabilities.clone(), - website_url: interface.website_url.clone(), - privacy_policy_url: interface.privacy_policy_url.clone(), - terms_of_service_url: interface.terms_of_service_url.clone(), - default_prompt, - brand_color: interface.brand_color.clone(), - composer_icon: None, - composer_icon_url: interface.composer_icon_url.clone(), - logo: None, - logo_url: interface.logo_url.clone(), - screenshots: Vec::new(), - screenshot_urls: interface.screenshot_urls.clone(), - }; - let has_fields = result.display_name.is_some() - || result.short_description.is_some() - || result.long_description.is_some() - || result.developer_name.is_some() - || result.category.is_some() - || !result.capabilities.is_empty() - || result.website_url.is_some() - || result.privacy_policy_url.is_some() - || result.terms_of_service_url.is_some() - || result.default_prompt.is_some() - || result.brand_color.is_some() - || result.composer_icon_url.is_some() - || result.logo_url.is_some() - || !result.screenshot_urls.is_empty(); - has_fields.then_some(result) -} - -fn remote_skill_interface_to_info( - interface: Option, -) -> Option { - interface.and_then(|interface| { - let result = SkillInterface { - display_name: interface.display_name, - short_description: interface.short_description, - icon_small: None, - icon_large: None, - brand_color: interface.brand_color, - default_prompt: interface.default_prompt, - }; - let has_fields = result.display_name.is_some() - || result.short_description.is_some() - || result.brand_color.is_some() - || result.default_prompt.is_some(); - has_fields.then_some(result) - }) -} - -fn remote_plugin_display_name(plugin: &RemotePluginSummary) -> &str { - plugin - .interface - .as_ref() - .and_then(|interface| interface.display_name.as_deref()) - .unwrap_or(&plugin.name) -} - -fn non_empty_string(value: Option<&str>) -> Option { - value.and_then(|value| { - let value = value.trim(); - (!value.is_empty()).then(|| value.to_string()) - }) -} - -fn normalize_remote_default_prompt(prompt: &str) -> Option> { - let prompt = prompt.trim(); - if prompt.is_empty() || prompt.chars().count() > MAX_REMOTE_DEFAULT_PROMPT_LEN { - return None; - } - Some(vec![prompt.to_string()]) -} - -async fn fetch_directory_plugins_for_scope( - config: &RemotePluginServiceConfig, - auth: &CodexAuth, - scope: RemotePluginScope, -) -> Result, RemotePluginCatalogError> { - let mut plugins = Vec::new(); - let mut page_token = None; - loop { - let response = - get_remote_plugin_list_page(config, auth, scope, page_token.as_deref()).await?; - plugins.extend(response.plugins); - let Some(next_page_token) = response.pagination.next_page_token else { - break; - }; - page_token = Some(next_page_token); - } - Ok(plugins) -} - -async fn fetch_installed_plugins_for_scope( - config: &RemotePluginServiceConfig, - auth: &CodexAuth, - scope: RemotePluginScope, -) -> Result, RemotePluginCatalogError> { - let mut plugins = Vec::new(); - let mut page_token = None; - loop { - let response = - get_remote_plugin_installed_page(config, auth, scope, page_token.as_deref()).await?; - plugins.extend(response.plugins); - let Some(next_page_token) = response.pagination.next_page_token else { - break; - }; - page_token = Some(next_page_token); - } - Ok(plugins) -} - -async fn get_remote_plugin_list_page( - config: &RemotePluginServiceConfig, - auth: &CodexAuth, - scope: RemotePluginScope, - page_token: Option<&str>, -) -> Result { - let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let url = format!("{base_url}/ps/plugins/list"); - let client = build_reqwest_client(); - let mut request = authenticated_request(client.get(&url), auth)?; - request = request.query(&[("scope", scope.api_value())]); - if let Some(page_token) = page_token { - request = request.query(&[("pageToken", page_token)]); - } - send_and_decode(request, &url).await -} - -async fn get_remote_plugin_installed_page( - config: &RemotePluginServiceConfig, - auth: &CodexAuth, - scope: RemotePluginScope, - page_token: Option<&str>, -) -> Result { - let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let url = format!("{base_url}/ps/plugins/installed"); - let client = build_reqwest_client(); - let mut request = authenticated_request(client.get(&url), auth)?; - request = request.query(&[("scope", scope.api_value())]); - if let Some(page_token) = page_token { - request = request.query(&[("pageToken", page_token)]); - } - send_and_decode(request, &url).await -} - -async fn fetch_plugin_detail( - config: &RemotePluginServiceConfig, - auth: &CodexAuth, - plugin_id: &str, -) -> Result { - let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let url = format!("{base_url}/ps/plugins/{plugin_id}"); - let client = build_reqwest_client(); - let request = authenticated_request(client.get(&url), auth)?; - send_and_decode(request, &url).await -} - -fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePluginCatalogError> { - let Some(auth) = auth else { - return Err(RemotePluginCatalogError::AuthRequired); - }; - if !auth.is_chatgpt_auth() { - return Err(RemotePluginCatalogError::UnsupportedAuthMode); - } - Ok(auth) -} - -fn authenticated_request( - request: RequestBuilder, - auth: &CodexAuth, -) -> Result { - let token = auth - .get_token() - .map_err(RemotePluginCatalogError::AuthToken)?; - let mut request = request - .timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT) - .bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } - Ok(request) -} - -async fn send_and_decode Deserialize<'de>>( - request: RequestBuilder, - url: &str, -) -> Result { - let response = request - .send() - .await - .map_err(|source| RemotePluginCatalogError::Request { - url: url.to_string(), - source, - })?; - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - if !status.is_success() { - return Err(RemotePluginCatalogError::UnexpectedStatus { - url: url.to_string(), - status, - body, - }); - } - - serde_json::from_str(&body).map_err(|source| RemotePluginCatalogError::Decode { - url: url.to_string(), - source, - }) -} diff --git a/vendor/codex/codex-rs/core-plugins/src/remote_legacy.rs b/vendor/codex/codex-rs/core-plugins/src/remote_legacy.rs deleted file mode 100644 index 7b57ab13..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/remote_legacy.rs +++ /dev/null @@ -1,313 +0,0 @@ -use crate::remote::RemotePluginServiceConfig; -use codex_login::CodexAuth; -use codex_login::default_client::build_reqwest_client; -use codex_protocol::protocol::Product; -use serde::Deserialize; -use std::time::Duration; -use url::Url; - -const DEFAULT_REMOTE_MARKETPLACE_NAME: &str = "openai-curated"; -const REMOTE_PLUGIN_FETCH_TIMEOUT: Duration = Duration::from_secs(30); -const REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT: Duration = Duration::from_secs(10); -const REMOTE_PLUGIN_MUTATION_TIMEOUT: Duration = Duration::from_secs(30); - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub struct RemotePluginStatusSummary { - pub name: String, - #[serde(default = "default_remote_marketplace_name")] - pub marketplace_name: String, - pub enabled: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RemotePluginMutationResponse { - pub id: String, - pub enabled: bool, -} - -#[derive(Debug, thiserror::Error)] -pub enum RemotePluginMutationError { - #[error("chatgpt authentication required for remote plugin mutation")] - AuthRequired, - - #[error( - "chatgpt authentication required for remote plugin mutation; api key auth is not supported" - )] - UnsupportedAuthMode, - - #[error("failed to read auth token for remote plugin mutation: {0}")] - AuthToken(#[source] std::io::Error), - - #[error("invalid chatgpt base url for remote plugin mutation: {0}")] - InvalidBaseUrl(#[source] url::ParseError), - - #[error("chatgpt base url cannot be used for plugin mutation")] - InvalidBaseUrlPath, - - #[error("failed to send remote plugin mutation request to {url}: {source}")] - Request { - url: String, - #[source] - source: reqwest::Error, - }, - - #[error("remote plugin mutation failed with status {status} from {url}: {body}")] - UnexpectedStatus { - url: String, - status: reqwest::StatusCode, - body: String, - }, - - #[error("failed to parse remote plugin mutation response from {url}: {source}")] - Decode { - url: String, - #[source] - source: serde_json::Error, - }, - - #[error( - "remote plugin mutation returned unexpected plugin id: expected `{expected}`, got `{actual}`" - )] - UnexpectedPluginId { expected: String, actual: String }, - - #[error( - "remote plugin mutation returned unexpected enabled state for `{plugin_id}`: expected {expected_enabled}, got {actual_enabled}" - )] - UnexpectedEnabledState { - plugin_id: String, - expected_enabled: bool, - actual_enabled: bool, - }, -} - -#[derive(Debug, thiserror::Error)] -pub enum RemotePluginFetchError { - #[error("chatgpt authentication required to sync remote plugins")] - AuthRequired, - - #[error( - "chatgpt authentication required to sync remote plugins; api key auth is not supported" - )] - UnsupportedAuthMode, - - #[error("failed to read auth token for remote plugin sync: {0}")] - AuthToken(#[source] std::io::Error), - - #[error("failed to send remote plugin sync request to {url}: {source}")] - Request { - url: String, - #[source] - source: reqwest::Error, - }, - - #[error("remote plugin sync request to {url} failed with status {status}: {body}")] - UnexpectedStatus { - url: String, - status: reqwest::StatusCode, - body: String, - }, - - #[error("failed to parse remote plugin sync response from {url}: {source}")] - Decode { - url: String, - #[source] - source: serde_json::Error, - }, -} - -pub async fn fetch_remote_plugin_status( - config: &RemotePluginServiceConfig, - auth: Option<&CodexAuth>, -) -> Result, RemotePluginFetchError> { - let Some(auth) = auth else { - return Err(RemotePluginFetchError::AuthRequired); - }; - if !auth.is_chatgpt_auth() { - return Err(RemotePluginFetchError::UnsupportedAuthMode); - } - - let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let url = format!("{base_url}/plugins/list"); - let client = build_reqwest_client(); - let token = auth - .get_token() - .map_err(RemotePluginFetchError::AuthToken)?; - let mut request = client - .get(&url) - .timeout(REMOTE_PLUGIN_FETCH_TIMEOUT) - .bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } - - let response = request - .send() - .await - .map_err(|source| RemotePluginFetchError::Request { - url: url.clone(), - source, - })?; - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - if !status.is_success() { - return Err(RemotePluginFetchError::UnexpectedStatus { url, status, body }); - } - - serde_json::from_str(&body).map_err(|source| RemotePluginFetchError::Decode { - url: url.clone(), - source, - }) -} - -pub async fn fetch_remote_featured_plugin_ids( - config: &RemotePluginServiceConfig, - auth: Option<&CodexAuth>, - product: Option, -) -> Result, RemotePluginFetchError> { - let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let url = format!("{base_url}/plugins/featured"); - let client = build_reqwest_client(); - let mut request = client - .get(&url) - .query(&[( - "platform", - product.unwrap_or(Product::Codex).to_app_platform(), - )]) - .timeout(REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT); - - if let Some(auth) = auth.filter(|auth| auth.is_chatgpt_auth()) { - let token = auth - .get_token() - .map_err(RemotePluginFetchError::AuthToken)?; - request = request.bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } - } - - let response = request - .send() - .await - .map_err(|source| RemotePluginFetchError::Request { - url: url.clone(), - source, - })?; - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - if !status.is_success() { - return Err(RemotePluginFetchError::UnexpectedStatus { url, status, body }); - } - - serde_json::from_str(&body).map_err(|source| RemotePluginFetchError::Decode { - url: url.clone(), - source, - }) -} - -pub async fn enable_remote_plugin( - config: &RemotePluginServiceConfig, - auth: Option<&CodexAuth>, - plugin_id: &str, -) -> Result<(), RemotePluginMutationError> { - post_remote_plugin_mutation(config, auth, plugin_id, "enable").await?; - Ok(()) -} - -pub async fn uninstall_remote_plugin( - config: &RemotePluginServiceConfig, - auth: Option<&CodexAuth>, - plugin_id: &str, -) -> Result<(), RemotePluginMutationError> { - post_remote_plugin_mutation(config, auth, plugin_id, "uninstall").await?; - Ok(()) -} - -fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePluginMutationError> { - let Some(auth) = auth else { - return Err(RemotePluginMutationError::AuthRequired); - }; - if !auth.is_chatgpt_auth() { - return Err(RemotePluginMutationError::UnsupportedAuthMode); - } - Ok(auth) -} - -fn default_remote_marketplace_name() -> String { - DEFAULT_REMOTE_MARKETPLACE_NAME.to_string() -} - -async fn post_remote_plugin_mutation( - config: &RemotePluginServiceConfig, - auth: Option<&CodexAuth>, - plugin_id: &str, - action: &str, -) -> Result { - let auth = ensure_chatgpt_auth(auth)?; - let url = remote_plugin_mutation_url(config, plugin_id, action)?; - let client = build_reqwest_client(); - let token = auth - .get_token() - .map_err(RemotePluginMutationError::AuthToken)?; - let mut request = client - .post(url.clone()) - .timeout(REMOTE_PLUGIN_MUTATION_TIMEOUT) - .bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } - - let response = request - .send() - .await - .map_err(|source| RemotePluginMutationError::Request { - url: url.clone(), - source, - })?; - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - if !status.is_success() { - return Err(RemotePluginMutationError::UnexpectedStatus { url, status, body }); - } - - let parsed: RemotePluginMutationResponse = - serde_json::from_str(&body).map_err(|source| RemotePluginMutationError::Decode { - url: url.clone(), - source, - })?; - let expected_enabled = action == "enable"; - if parsed.id != plugin_id { - return Err(RemotePluginMutationError::UnexpectedPluginId { - expected: plugin_id.to_string(), - actual: parsed.id, - }); - } - if parsed.enabled != expected_enabled { - return Err(RemotePluginMutationError::UnexpectedEnabledState { - plugin_id: plugin_id.to_string(), - expected_enabled, - actual_enabled: parsed.enabled, - }); - } - - Ok(parsed) -} - -fn remote_plugin_mutation_url( - config: &RemotePluginServiceConfig, - plugin_id: &str, - action: &str, -) -> Result { - let mut url = Url::parse(config.chatgpt_base_url.trim_end_matches('/')) - .map_err(RemotePluginMutationError::InvalidBaseUrl)?; - { - let mut segments = url - .path_segments_mut() - .map_err(|()| RemotePluginMutationError::InvalidBaseUrlPath)?; - segments.pop_if_empty(); - segments.push("plugins"); - segments.push(plugin_id); - segments.push(action); - } - Ok(url.to_string()) -} diff --git a/vendor/codex/codex-rs/core-plugins/src/store.rs b/vendor/codex/codex-rs/core-plugins/src/store.rs deleted file mode 100644 index 757aec8b..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/store.rs +++ /dev/null @@ -1,341 +0,0 @@ -use crate::manifest::PluginManifest; -use crate::manifest::load_plugin_manifest; -use codex_plugin::PluginId; -use codex_plugin::validate_plugin_segment; -use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_plugins::find_plugin_manifest_path; -use serde::Deserialize; -use serde_json::Value as JsonValue; -use std::fs; -use std::io; -use std::path::Path; -use std::path::PathBuf; - -pub const DEFAULT_PLUGIN_VERSION: &str = "local"; -pub const PLUGINS_CACHE_DIR: &str = "plugins/cache"; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PluginInstallResult { - pub plugin_id: PluginId, - pub plugin_version: String, - pub installed_path: AbsolutePathBuf, -} - -#[derive(Debug, Clone)] -pub struct PluginStore { - root: AbsolutePathBuf, -} - -impl PluginStore { - pub fn new(codex_home: PathBuf) -> Self { - Self::try_new(codex_home) - .unwrap_or_else(|err| panic!("plugin cache root should be absolute: {err}")) - } - - pub fn try_new(codex_home: PathBuf) -> Result { - let root = AbsolutePathBuf::from_absolute_path_checked(codex_home.join(PLUGINS_CACHE_DIR)) - .map_err(|err| PluginStoreError::io("failed to resolve plugin cache root", err))?; - - Ok(Self { root }) - } - - pub fn root(&self) -> &AbsolutePathBuf { - &self.root - } - - pub fn plugin_base_root(&self, plugin_id: &PluginId) -> AbsolutePathBuf { - self.root - .join(&plugin_id.marketplace_name) - .join(&plugin_id.plugin_name) - } - - pub fn plugin_root(&self, plugin_id: &PluginId, plugin_version: &str) -> AbsolutePathBuf { - self.plugin_base_root(plugin_id).join(plugin_version) - } - - pub fn active_plugin_version(&self, plugin_id: &PluginId) -> Option { - let mut discovered_versions = fs::read_dir(self.plugin_base_root(plugin_id).as_path()) - .ok()? - .filter_map(Result::ok) - .filter_map(|entry| { - entry.file_type().ok().filter(std::fs::FileType::is_dir)?; - entry.file_name().into_string().ok() - }) - .filter(|version| validate_plugin_version_segment(version).is_ok()) - .collect::>(); - discovered_versions.sort_unstable(); - if discovered_versions.is_empty() { - None - } else if discovered_versions - .iter() - .any(|version| version == DEFAULT_PLUGIN_VERSION) - { - Some(DEFAULT_PLUGIN_VERSION.to_string()) - } else { - discovered_versions.pop() - } - } - - pub fn active_plugin_root(&self, plugin_id: &PluginId) -> Option { - self.active_plugin_version(plugin_id) - .map(|plugin_version| self.plugin_root(plugin_id, &plugin_version)) - } - - pub fn is_installed(&self, plugin_id: &PluginId) -> bool { - self.active_plugin_version(plugin_id).is_some() - } - - pub fn install( - &self, - source_path: AbsolutePathBuf, - plugin_id: PluginId, - ) -> Result { - let plugin_version = plugin_version_for_source(source_path.as_path())?; - self.install_with_version(source_path, plugin_id, plugin_version) - } - - pub fn install_with_version( - &self, - source_path: AbsolutePathBuf, - plugin_id: PluginId, - plugin_version: String, - ) -> Result { - if !source_path.as_path().is_dir() { - return Err(PluginStoreError::Invalid(format!( - "plugin source path is not a directory: {}", - source_path.display() - ))); - } - - let plugin_name = plugin_name_for_source(source_path.as_path())?; - if plugin_name != plugin_id.plugin_name { - return Err(PluginStoreError::Invalid(format!( - "plugin.json name `{plugin_name}` does not match marketplace plugin name `{}`", - plugin_id.plugin_name - ))); - } - validate_plugin_version_segment(&plugin_version).map_err(PluginStoreError::Invalid)?; - let installed_path = self.plugin_root(&plugin_id, &plugin_version); - replace_plugin_root_atomically( - source_path.as_path(), - self.plugin_base_root(&plugin_id).as_path(), - &plugin_version, - )?; - - Ok(PluginInstallResult { - plugin_id, - plugin_version, - installed_path, - }) - } - - pub fn uninstall(&self, plugin_id: &PluginId) -> Result<(), PluginStoreError> { - remove_existing_target(self.plugin_base_root(plugin_id).as_path()) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum PluginStoreError { - #[error("{context}: {source}")] - Io { - context: &'static str, - #[source] - source: io::Error, - }, - - #[error("{0}")] - Invalid(String), -} - -impl PluginStoreError { - fn io(context: &'static str, source: io::Error) -> Self { - Self::Io { context, source } - } -} - -pub fn plugin_version_for_source(source_path: &Path) -> Result { - let plugin_version = plugin_manifest_version_for_source(source_path)? - .unwrap_or_else(|| DEFAULT_PLUGIN_VERSION.to_string()); - validate_plugin_version_segment(&plugin_version).map_err(PluginStoreError::Invalid)?; - Ok(plugin_version) -} - -fn validate_plugin_version_segment(plugin_version: &str) -> Result<(), String> { - if plugin_version.is_empty() { - return Err("invalid plugin version: must not be empty".to_string()); - } - if matches!(plugin_version, "." | "..") { - return Err("invalid plugin version: path traversal is not allowed".to_string()); - } - if !plugin_version - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | '+')) - { - return Err( - "invalid plugin version: only ASCII letters, digits, `.`, `+`, `_`, and `-` are allowed" - .to_string(), - ); - } - Ok(()) -} - -fn plugin_manifest_for_source(source_path: &Path) -> Result { - load_plugin_manifest(source_path) - .ok_or_else(|| PluginStoreError::Invalid("missing or invalid plugin.json".to_string())) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawPluginManifestVersion { - #[serde(default)] - version: Option, -} - -fn plugin_manifest_version_for_source( - source_path: &Path, -) -> Result, PluginStoreError> { - let manifest_path = find_plugin_manifest_path(source_path) - .ok_or_else(|| PluginStoreError::Invalid("missing plugin.json".to_string()))?; - - let contents = fs::read_to_string(&manifest_path) - .map_err(|err| PluginStoreError::io("failed to read plugin.json", err))?; - let manifest: RawPluginManifestVersion = serde_json::from_str(&contents) - .map_err(|err| PluginStoreError::Invalid(format!("failed to parse plugin.json: {err}")))?; - let Some(version) = manifest.version else { - return Ok(None); - }; - let Some(version) = version.as_str() else { - return Err(PluginStoreError::Invalid( - "invalid plugin version in plugin.json: expected string".to_string(), - )); - }; - let version = version.trim(); - if version.is_empty() { - return Err(PluginStoreError::Invalid( - "invalid plugin version in plugin.json: must not be blank".to_string(), - )); - } - Ok(Some(version.to_string())) -} - -fn plugin_name_for_source(source_path: &Path) -> Result { - let manifest = plugin_manifest_for_source(source_path)?; - - let plugin_name = manifest.name; - validate_plugin_segment(&plugin_name, "plugin name") - .map_err(PluginStoreError::Invalid) - .map(|_| plugin_name) -} - -fn remove_existing_target(path: &Path) -> Result<(), PluginStoreError> { - if !path.exists() { - return Ok(()); - } - - if path.is_dir() { - fs::remove_dir_all(path).map_err(|err| { - PluginStoreError::io("failed to remove existing plugin cache entry", err) - }) - } else { - fs::remove_file(path).map_err(|err| { - PluginStoreError::io("failed to remove existing plugin cache entry", err) - }) - } -} - -fn replace_plugin_root_atomically( - source: &Path, - target_root: &Path, - plugin_version: &str, -) -> Result<(), PluginStoreError> { - let Some(parent) = target_root.parent() else { - return Err(PluginStoreError::Invalid(format!( - "plugin cache path has no parent: {}", - target_root.display() - ))); - }; - - fs::create_dir_all(parent) - .map_err(|err| PluginStoreError::io("failed to create plugin cache directory", err))?; - - let Some(plugin_dir_name) = target_root.file_name() else { - return Err(PluginStoreError::Invalid(format!( - "plugin cache path has no directory name: {}", - target_root.display() - ))); - }; - let staged_dir = tempfile::Builder::new() - .prefix("plugin-install-") - .tempdir_in(parent) - .map_err(|err| { - PluginStoreError::io("failed to create temporary plugin cache directory", err) - })?; - let staged_root = staged_dir.path().join(plugin_dir_name); - let staged_version_root = staged_root.join(plugin_version); - copy_dir_recursive(source, &staged_version_root)?; - - if target_root.exists() { - let backup_dir = tempfile::Builder::new() - .prefix("plugin-backup-") - .tempdir_in(parent) - .map_err(|err| { - PluginStoreError::io("failed to create plugin cache backup directory", err) - })?; - let backup_root = backup_dir.path().join(plugin_dir_name); - fs::rename(target_root, &backup_root) - .map_err(|err| PluginStoreError::io("failed to back up plugin cache entry", err))?; - - if let Err(err) = fs::rename(&staged_root, target_root) { - let rollback_result = fs::rename(&backup_root, target_root); - return match rollback_result { - Ok(()) => Err(PluginStoreError::io( - "failed to activate updated plugin cache entry", - err, - )), - Err(rollback_err) => { - let backup_path = backup_dir.keep().join(plugin_dir_name); - Err(PluginStoreError::Invalid(format!( - "failed to activate updated plugin cache entry at {}: {err}; failed to restore previous cache entry (left at {}): {rollback_err}", - target_root.display(), - backup_path.display() - ))) - } - }; - } - } else { - fs::rename(&staged_root, target_root) - .map_err(|err| PluginStoreError::io("failed to activate plugin cache entry", err))?; - } - - Ok(()) -} - -fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), PluginStoreError> { - fs::create_dir_all(target) - .map_err(|err| PluginStoreError::io("failed to create plugin target directory", err))?; - - for entry in fs::read_dir(source) - .map_err(|err| PluginStoreError::io("failed to read plugin source directory", err))? - { - let entry = - entry.map_err(|err| PluginStoreError::io("failed to enumerate plugin source", err))?; - let source_path = entry.path(); - let target_path = target.join(entry.file_name()); - let file_type = entry - .file_type() - .map_err(|err| PluginStoreError::io("failed to inspect plugin source entry", err))?; - - if file_type.is_dir() { - copy_dir_recursive(&source_path, &target_path)?; - } else if file_type.is_file() { - fs::copy(&source_path, &target_path) - .map_err(|err| PluginStoreError::io("failed to copy plugin file", err))?; - } - } - - Ok(()) -} - -#[cfg(test)] -#[path = "store_tests.rs"] -mod tests; diff --git a/vendor/codex/codex-rs/core-plugins/src/store_tests.rs b/vendor/codex/codex-rs/core-plugins/src/store_tests.rs deleted file mode 100644 index 45feff61..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/store_tests.rs +++ /dev/null @@ -1,318 +0,0 @@ -use super::*; -use codex_plugin::PluginId; -use pretty_assertions::assert_eq; -use tempfile::tempdir; - -fn write_plugin_with_version( - root: &Path, - dir_name: &str, - manifest_name: &str, - manifest_version: Option<&str>, -) { - let plugin_root = root.join(dir_name); - fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap(); - fs::create_dir_all(plugin_root.join("skills")).unwrap(); - let version = manifest_version - .map(|manifest_version| format!(r#","version":"{manifest_version}""#)) - .unwrap_or_default(); - fs::write( - plugin_root.join(".codex-plugin/plugin.json"), - format!(r#"{{"name":"{manifest_name}"{version}}}"#), - ) - .unwrap(); - fs::write(plugin_root.join("skills/SKILL.md"), "skill").unwrap(); - fs::write(plugin_root.join(".mcp.json"), r#"{"mcpServers":{}}"#).unwrap(); -} - -fn write_plugin(root: &Path, dir_name: &str, manifest_name: &str) { - write_plugin_with_version( - root, - dir_name, - manifest_name, - /*manifest_version*/ None, - ); -} - -#[test] -fn try_new_rejects_relative_codex_home() { - let err = PluginStore::try_new(PathBuf::from("relative")) - .expect_err("relative codex home should fail"); - let err = err.to_string().replace('\\', "/"); - - assert_eq!( - err, - "failed to resolve plugin cache root: path is not absolute: relative/plugins/cache" - ); -} - -#[test] -fn install_copies_plugin_into_default_marketplace() { - let tmp = tempdir().unwrap(); - write_plugin(tmp.path(), "sample-plugin", "sample-plugin"); - let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(); - - let result = PluginStore::new(tmp.path().to_path_buf()) - .install( - AbsolutePathBuf::try_from(tmp.path().join("sample-plugin")).unwrap(), - plugin_id.clone(), - ) - .unwrap(); - - let installed_path = tmp.path().join("plugins/cache/debug/sample-plugin/local"); - assert_eq!( - result, - PluginInstallResult { - plugin_id, - plugin_version: "local".to_string(), - installed_path: AbsolutePathBuf::try_from(installed_path.clone()).unwrap(), - } - ); - assert!(installed_path.join(".codex-plugin/plugin.json").is_file()); - assert!(installed_path.join("skills/SKILL.md").is_file()); -} - -#[test] -fn install_uses_manifest_name_for_destination_and_key() { - let tmp = tempdir().unwrap(); - write_plugin(tmp.path(), "source-dir", "manifest-name"); - let plugin_id = PluginId::new("manifest-name".to_string(), "market".to_string()).unwrap(); - - let result = PluginStore::new(tmp.path().to_path_buf()) - .install( - AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(), - plugin_id.clone(), - ) - .unwrap(); - - assert_eq!( - result, - PluginInstallResult { - plugin_id, - plugin_version: "local".to_string(), - installed_path: AbsolutePathBuf::try_from( - tmp.path().join("plugins/cache/market/manifest-name/local"), - ) - .unwrap(), - } - ); -} - -#[test] -fn plugin_root_derives_path_from_key_and_version() { - let tmp = tempdir().unwrap(); - let store = PluginStore::new(tmp.path().to_path_buf()); - let plugin_id = PluginId::new("sample".to_string(), "debug".to_string()).unwrap(); - - assert_eq!( - store.plugin_root(&plugin_id, "local").as_path(), - tmp.path().join("plugins/cache/debug/sample/local") - ); -} - -#[test] -fn install_with_version_uses_requested_cache_version() { - let tmp = tempdir().unwrap(); - write_plugin(tmp.path(), "sample-plugin", "sample-plugin"); - let plugin_id = - PluginId::new("sample-plugin".to_string(), "openai-curated".to_string()).unwrap(); - let plugin_version = "0123456789abcdef".to_string(); - - let result = PluginStore::new(tmp.path().to_path_buf()) - .install_with_version( - AbsolutePathBuf::try_from(tmp.path().join("sample-plugin")).unwrap(), - plugin_id.clone(), - plugin_version.clone(), - ) - .unwrap(); - - let installed_path = tmp.path().join(format!( - "plugins/cache/openai-curated/sample-plugin/{plugin_version}" - )); - assert_eq!( - result, - PluginInstallResult { - plugin_id, - plugin_version, - installed_path: AbsolutePathBuf::try_from(installed_path.clone()).unwrap(), - } - ); - assert!(installed_path.join(".codex-plugin/plugin.json").is_file()); -} - -#[test] -fn install_uses_manifest_version_when_present() { - let tmp = tempdir().unwrap(); - write_plugin_with_version( - tmp.path(), - "sample-plugin", - "sample-plugin", - Some("1.2.3-beta+7"), - ); - let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(); - - let result = PluginStore::new(tmp.path().to_path_buf()) - .install( - AbsolutePathBuf::try_from(tmp.path().join("sample-plugin")).unwrap(), - plugin_id.clone(), - ) - .unwrap(); - - let installed_path = tmp - .path() - .join("plugins/cache/debug/sample-plugin/1.2.3-beta+7"); - assert_eq!( - result, - PluginInstallResult { - plugin_id, - plugin_version: "1.2.3-beta+7".to_string(), - installed_path: AbsolutePathBuf::try_from(installed_path.clone()).unwrap(), - } - ); - assert!(installed_path.join(".codex-plugin/plugin.json").is_file()); -} - -#[test] -fn install_rejects_blank_manifest_version() { - let tmp = tempdir().unwrap(); - write_plugin_with_version(tmp.path(), "sample-plugin", "sample-plugin", Some(" ")); - let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(); - - let err = PluginStore::new(tmp.path().to_path_buf()) - .install( - AbsolutePathBuf::try_from(tmp.path().join("sample-plugin")).unwrap(), - plugin_id, - ) - .expect_err("blank manifest version should be rejected"); - let err = err.to_string().replace('\\', "/"); - - assert_eq!( - err, - "invalid plugin version in plugin.json: must not be blank" - ); -} - -#[test] -fn active_plugin_version_reads_version_directory_name() { - let tmp = tempdir().unwrap(); - write_plugin( - &tmp.path().join("plugins/cache/debug"), - "sample-plugin/local", - "sample-plugin", - ); - let store = PluginStore::new(tmp.path().to_path_buf()); - let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(); - - assert_eq!( - store.active_plugin_version(&plugin_id), - Some("local".to_string()) - ); - assert_eq!( - store.active_plugin_root(&plugin_id).unwrap().as_path(), - tmp.path().join("plugins/cache/debug/sample-plugin/local") - ); -} - -#[test] -fn active_plugin_version_prefers_default_local_version_when_multiple_versions_exist() { - let tmp = tempdir().unwrap(); - write_plugin( - &tmp.path().join("plugins/cache/debug"), - "sample-plugin/0123456789abcdef", - "sample-plugin", - ); - write_plugin( - &tmp.path().join("plugins/cache/debug"), - "sample-plugin/local", - "sample-plugin", - ); - let store = PluginStore::new(tmp.path().to_path_buf()); - let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(); - - assert_eq!( - store.active_plugin_version(&plugin_id), - Some("local".to_string()) - ); -} - -#[test] -fn active_plugin_version_returns_last_sorted_version_when_default_is_missing() { - let tmp = tempdir().unwrap(); - write_plugin( - &tmp.path().join("plugins/cache/debug"), - "sample-plugin/0123456789abcdef", - "sample-plugin", - ); - write_plugin( - &tmp.path().join("plugins/cache/debug"), - "sample-plugin/fedcba9876543210", - "sample-plugin", - ); - let store = PluginStore::new(tmp.path().to_path_buf()); - let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(); - - assert_eq!( - store.active_plugin_version(&plugin_id), - Some("fedcba9876543210".to_string()) - ); -} - -#[test] -fn plugin_root_rejects_path_separators_in_key_segments() { - let err = PluginId::parse("../../etc@debug").unwrap_err(); - assert_eq!( - err.to_string(), - "invalid plugin name: only ASCII letters, digits, `_`, and `-` are allowed in `../../etc@debug`" - ); - - let err = PluginId::parse("sample@../../etc").unwrap_err(); - assert_eq!( - err.to_string(), - "invalid marketplace name: only ASCII letters, digits, `_`, and `-` are allowed in `sample@../../etc`" - ); -} - -#[test] -fn install_rejects_manifest_names_with_path_separators() { - let tmp = tempdir().unwrap(); - write_plugin(tmp.path(), "source-dir", "../../etc"); - - let err = PluginStore::new(tmp.path().to_path_buf()) - .install( - AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(), - PluginId::new("source-dir".to_string(), "debug".to_string()).unwrap(), - ) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "invalid plugin name: only ASCII letters, digits, `_`, and `-` are allowed" - ); -} - -#[test] -fn install_rejects_marketplace_names_with_path_separators() { - let err = PluginId::new("sample-plugin".to_string(), "../../etc".to_string()).unwrap_err(); - - assert_eq!( - err.to_string(), - "invalid marketplace name: only ASCII letters, digits, `_`, and `-` are allowed" - ); -} - -#[test] -fn install_rejects_manifest_names_that_do_not_match_marketplace_plugin_name() { - let tmp = tempdir().unwrap(); - write_plugin(tmp.path(), "source-dir", "manifest-name"); - - let err = PluginStore::new(tmp.path().to_path_buf()) - .install( - AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(), - PluginId::new("different-name".to_string(), "debug".to_string()).unwrap(), - ) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "plugin.json name `manifest-name` does not match marketplace plugin name `different-name`" - ); -} diff --git a/vendor/codex/codex-rs/core-plugins/src/toggles.rs b/vendor/codex/codex-rs/core-plugins/src/toggles.rs deleted file mode 100644 index 215943dc..00000000 --- a/vendor/codex/codex-rs/core-plugins/src/toggles.rs +++ /dev/null @@ -1,100 +0,0 @@ -use serde_json::Value as JsonValue; -use std::collections::BTreeMap; - -pub fn collect_plugin_enabled_candidates<'a>( - edits: impl Iterator, -) -> BTreeMap { - let mut pending_changes = BTreeMap::new(); - for (key_path, value) in edits { - let segments = key_path - .split('.') - .map(str::to_string) - .collect::>(); - match segments.as_slice() { - [plugins, plugin_id, enabled] - if plugins == "plugins" && enabled == "enabled" && value.is_boolean() => - { - if let Some(enabled) = value.as_bool() { - pending_changes.insert(plugin_id.clone(), enabled); - } - } - [plugins, plugin_id] if plugins == "plugins" => { - if let Some(enabled) = value.get("enabled").and_then(JsonValue::as_bool) { - pending_changes.insert(plugin_id.clone(), enabled); - } - } - [plugins] if plugins == "plugins" => { - let Some(entries) = value.as_object() else { - continue; - }; - for (plugin_id, plugin_value) in entries { - let Some(enabled) = plugin_value.get("enabled").and_then(JsonValue::as_bool) - else { - continue; - }; - pending_changes.insert(plugin_id.clone(), enabled); - } - } - _ => {} - } - } - - pending_changes -} - -#[cfg(test)] -mod tests { - use super::collect_plugin_enabled_candidates; - use pretty_assertions::assert_eq; - use serde_json::json; - use std::collections::BTreeMap; - - #[test] - fn collect_plugin_enabled_candidates_tracks_direct_and_table_writes() { - let candidates = collect_plugin_enabled_candidates( - [ - (&"plugins.sample@test.enabled".to_string(), &json!(true)), - ( - &"plugins.other@test".to_string(), - &json!({ "enabled": false, "ignored": true }), - ), - ( - &"plugins".to_string(), - &json!({ - "nested@test": { "enabled": true }, - "skip@test": { "name": "skip" }, - }), - ), - ] - .into_iter(), - ); - - assert_eq!( - candidates, - BTreeMap::from([ - ("nested@test".to_string(), true), - ("other@test".to_string(), false), - ("sample@test".to_string(), true), - ]) - ); - } - - #[test] - fn collect_plugin_enabled_candidates_uses_last_write_for_same_plugin() { - let candidates = collect_plugin_enabled_candidates( - [ - (&"plugins.sample@test.enabled".to_string(), &json!(true)), - ( - &"plugins.sample@test".to_string(), - &json!({ "enabled": false }), - ), - ] - .into_iter(), - ); - - assert_eq!( - candidates, - BTreeMap::from([("sample@test".to_string(), false)]) - ); - } -}