mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Support marketplace plugin manifest fallback (#28789)
## Summary Support marketplace plugins whose source directory does not include a discoverable plugin manifest. Metadata-rich `marketplace.json` entries now act as fallback plugin manifests for listing, local detail reads, install, and non-curated cache refresh. The fallback preserves marketplace-entry plugin fields wholesale, then adds the small Codex-facing compatibility bridge for presentation metadata. A real source `plugin.json` always wins when present. ## Details - Capture flattened marketplace-entry fields into `MarketplacePluginManifestFallback`, preserving fields such as `version`, `description`, `skills`, `mcpServers`, `apps`, `hooks`, `agents`, `commands`, `strict`, `author`, and future manifest fields without a per-field translation list. - Bridge Claude-style top-level `displayName`, `author.name`, `homepage`, and marketplace `category` into Codex's nested `interface` fields only when the nested values are absent. - Treat fallback metadata as installable only when the marketplace entry contributes metadata beyond bare `name` and `source`; existing missing-manifest behavior remains for metadata-free entries. - Read local plugin details from the already parsed fallback manifest, including fallback-declared app and MCP paths, instead of rereading only an on-disk manifest. - Pass fallback contents into `PluginStore`, which validates them and injects `.codex-plugin/plugin.json` into Store's existing atomic copy. Local marketplace source directories are never mutated, and the fallback path no longer needs an additional staging directory. - Keep Git source materialization unchanged; Git clones still use the existing marketplace source staging area before Store installation.
This commit is contained in:
committed by
GitHub
Unverified
parent
dce673905a
commit
772c5c5195
@@ -7,12 +7,14 @@ use crate::manifest::PluginManifestMcpServers;
|
||||
use crate::manifest::PluginManifestPaths;
|
||||
use crate::manifest::load_plugin_manifest;
|
||||
use crate::marketplace::MarketplacePluginSource;
|
||||
use crate::marketplace::find_marketplace_plugin;
|
||||
use crate::marketplace::list_marketplaces;
|
||||
use crate::marketplace::load_marketplace;
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use crate::remote::RemoteInstalledPlugin;
|
||||
use crate::store::PluginStore;
|
||||
use crate::store::plugin_version_for_source;
|
||||
use crate::store::plugin_version_for_source_with_fallback_manifest;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::HooksFile;
|
||||
@@ -460,7 +462,7 @@ fn refresh_non_curated_plugin_cache_with_mode(
|
||||
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::<String, MarketplacePluginSource>::new();
|
||||
let mut plugin_sources = HashMap::<String, (MarketplacePluginSource, Option<String>)>::new();
|
||||
|
||||
for marketplace in marketplace_outcome.marketplaces {
|
||||
if is_openai_curated_marketplace_name(&marketplace.name) {
|
||||
@@ -489,14 +491,31 @@ fn refresh_non_curated_plugin_cache_with_mode(
|
||||
continue;
|
||||
}
|
||||
|
||||
plugin_sources.insert(plugin_key, plugin.source);
|
||||
let manifest_fallback = find_marketplace_plugin(&marketplace.path, &plugin.name)
|
||||
.map(|resolved| {
|
||||
resolved
|
||||
.manifest_fallback
|
||||
.contents_if_has_metadata()
|
||||
.map(str::to_string)
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
warn!(
|
||||
plugin = plugin.name,
|
||||
marketplace = marketplace.name,
|
||||
error = %err,
|
||||
"failed to resolve marketplace plugin manifest fallback during cache refresh"
|
||||
);
|
||||
None
|
||||
});
|
||||
plugin_sources.insert(plugin_key, (plugin.source, manifest_fallback));
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let Some((source, manifest_fallback_contents)) = plugin_sources.get(&plugin_key).cloned()
|
||||
else {
|
||||
warn!(
|
||||
plugin = plugin_id.plugin_name,
|
||||
marketplace = plugin_id.marketplace_name,
|
||||
@@ -509,8 +528,14 @@ fn refresh_non_curated_plugin_cache_with_mode(
|
||||
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}"))?;
|
||||
let plugin_version = match manifest_fallback_contents.as_deref() {
|
||||
Some(manifest_contents) => plugin_version_for_source_with_fallback_manifest(
|
||||
source_path.as_path(),
|
||||
manifest_contents,
|
||||
),
|
||||
None => 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())
|
||||
@@ -518,9 +543,16 @@ fn refresh_non_curated_plugin_cache_with_mode(
|
||||
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}"))?;
|
||||
match manifest_fallback_contents.as_deref() {
|
||||
Some(manifest_contents) => store.install_with_version_and_fallback_manifest(
|
||||
source_path,
|
||||
plugin_id.clone(),
|
||||
plugin_version,
|
||||
manifest_contents,
|
||||
),
|
||||
None => 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;
|
||||
}
|
||||
|
||||
@@ -901,15 +933,22 @@ fn default_mcp_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf> {
|
||||
|
||||
pub async fn load_plugin_apps(plugin_root: &Path) -> Vec<AppDeclaration> {
|
||||
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;
|
||||
return load_plugin_apps_from_manifest(plugin_root, &manifest.paths).await;
|
||||
}
|
||||
load_apps_from_paths(plugin_root, default_app_config_paths(plugin_root)).await
|
||||
}
|
||||
|
||||
pub(crate) async fn load_plugin_apps_from_manifest(
|
||||
plugin_root: &Path,
|
||||
manifest_paths: &PluginManifestPaths,
|
||||
) -> Vec<AppDeclaration> {
|
||||
load_apps_from_paths(
|
||||
plugin_root,
|
||||
plugin_app_config_paths(plugin_root, manifest_paths),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn plugin_app_declarations_from_value(value: &JsonValue) -> Vec<AppDeclaration> {
|
||||
let Ok(parsed) = serde_json::from_value::<PluginAppFile>(value.clone()) else {
|
||||
return Vec::new();
|
||||
@@ -1181,7 +1220,7 @@ async fn load_declared_plugin_mcp_servers(plugin_root: &Path) -> HashMap<String,
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_plugin_mcp_servers_from_manifest(
|
||||
pub(crate) async fn load_plugin_mcp_servers_from_manifest(
|
||||
plugin_root: &Path,
|
||||
manifest_paths: &PluginManifestPaths,
|
||||
plugin_policy: Option<&HashMap<String, PluginMcpServerConfig>>,
|
||||
|
||||
Reference in New Issue
Block a user