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,10 +7,10 @@ use crate::loader::PluginHookLoadOutcome;
|
||||
use crate::loader::configured_curated_plugin_ids_from_codex_home;
|
||||
use crate::loader::curated_plugin_cache_version;
|
||||
use crate::loader::installed_plugin_telemetry_metadata;
|
||||
use crate::loader::load_plugin_apps;
|
||||
use crate::loader::load_plugin_apps_from_manifest;
|
||||
use crate::loader::load_plugin_hooks;
|
||||
use crate::loader::load_plugin_hooks_from_layer_stack;
|
||||
use crate::loader::load_plugin_mcp_servers;
|
||||
use crate::loader::load_plugin_mcp_servers_from_manifest;
|
||||
use crate::loader::load_plugin_skills;
|
||||
use crate::loader::load_plugins_from_layer_stack;
|
||||
use crate::loader::log_plugin_load_errors;
|
||||
@@ -27,6 +27,7 @@ use crate::marketplace::MarketplaceInterface;
|
||||
use crate::marketplace::MarketplaceListError;
|
||||
use crate::marketplace::MarketplaceListOutcome;
|
||||
use crate::marketplace::MarketplacePluginAuthPolicy;
|
||||
use crate::marketplace::MarketplacePluginManifestFallback;
|
||||
use crate::marketplace::MarketplacePluginPolicy;
|
||||
use crate::marketplace::MarketplacePluginSource;
|
||||
use crate::marketplace::ResolvedMarketplacePlugin;
|
||||
@@ -315,6 +316,7 @@ pub struct ConfiguredMarketplacePlugin {
|
||||
pub policy: MarketplacePluginPolicy,
|
||||
pub interface: Option<PluginManifestInterface>,
|
||||
pub keywords: Vec<String>,
|
||||
pub manifest_fallback: Option<MarketplacePluginManifestFallback>,
|
||||
pub installed: bool,
|
||||
pub enabled: bool,
|
||||
}
|
||||
@@ -1256,15 +1258,32 @@ impl PluginsManager {
|
||||
};
|
||||
let store = self.store.clone();
|
||||
let codex_home = self.codex_home.clone();
|
||||
let manifest_fallback_contents = resolved
|
||||
.manifest_fallback
|
||||
.contents_if_has_metadata()
|
||||
.map(str::to_string);
|
||||
let result: StorePluginInstallResult = tokio::task::spawn_blocking(move || {
|
||||
let materialized =
|
||||
materialize_marketplace_plugin_source(codex_home.as_path(), &resolved.source)
|
||||
.map_err(PluginStoreError::Invalid)?;
|
||||
let source_path = materialized.path;
|
||||
if let Some(plugin_version) = plugin_version {
|
||||
store.install_with_version(source_path, resolved.plugin_id, plugin_version)
|
||||
} else {
|
||||
store.install(source_path, resolved.plugin_id)
|
||||
match (plugin_version, manifest_fallback_contents.as_deref()) {
|
||||
(Some(plugin_version), Some(manifest_contents)) => store
|
||||
.install_with_version_and_fallback_manifest(
|
||||
source_path,
|
||||
resolved.plugin_id,
|
||||
plugin_version,
|
||||
manifest_contents,
|
||||
),
|
||||
(Some(plugin_version), None) => {
|
||||
store.install_with_version(source_path, resolved.plugin_id, plugin_version)
|
||||
}
|
||||
(None, Some(manifest_contents)) => store.install_with_fallback_manifest(
|
||||
source_path,
|
||||
resolved.plugin_id,
|
||||
manifest_contents,
|
||||
),
|
||||
(None, None) => store.install(source_path, resolved.plugin_id),
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -1394,6 +1413,7 @@ impl PluginsManager {
|
||||
let enabled = enabled_plugins.contains(&plugin_key);
|
||||
let mut interface = plugin.interface;
|
||||
let mut local_version = plugin.local_version;
|
||||
let manifest_fallback = plugin.manifest_fallback.clone();
|
||||
if installed
|
||||
&& matches!(&plugin.source, MarketplacePluginSource::Git { .. })
|
||||
&& let Some(plugin_id) = plugin_id.as_ref()
|
||||
@@ -1424,6 +1444,7 @@ impl PluginsManager {
|
||||
policy: plugin.policy,
|
||||
keywords: plugin.keywords,
|
||||
interface,
|
||||
manifest_fallback,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1491,6 +1512,10 @@ impl PluginsManager {
|
||||
|
||||
let marketplace_name = plugin.plugin_id.marketplace_name.clone();
|
||||
let plugin_key = plugin.plugin_id.as_key();
|
||||
let manifest_fallback = plugin
|
||||
.manifest_fallback
|
||||
.contents_if_has_metadata()
|
||||
.map(|_| plugin.manifest_fallback.clone());
|
||||
let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config);
|
||||
let installed = installed_plugins.contains(&plugin_key);
|
||||
let installed_version = if installed {
|
||||
@@ -1518,6 +1543,7 @@ impl PluginsManager {
|
||||
.as_ref()
|
||||
.map(|manifest| manifest.keywords.clone())
|
||||
.unwrap_or_default(),
|
||||
manifest_fallback,
|
||||
installed,
|
||||
enabled: enabled_plugins.contains(&plugin_key),
|
||||
},
|
||||
@@ -1604,9 +1630,18 @@ impl PluginsManager {
|
||||
"path does not exist or is not a directory".to_string(),
|
||||
));
|
||||
}
|
||||
let manifest = load_plugin_manifest(source_path.as_path()).ok_or_else(|| {
|
||||
MarketplaceError::InvalidPlugin("missing or invalid plugin.json".to_string())
|
||||
})?;
|
||||
let manifest =
|
||||
if codex_utils_plugins::find_plugin_manifest_path(source_path.as_path()).is_some() {
|
||||
load_plugin_manifest(source_path.as_path())
|
||||
} else {
|
||||
plugin
|
||||
.manifest_fallback
|
||||
.as_ref()
|
||||
.and_then(|fallback| fallback.parse_for_plugin_root(source_path.as_path()))
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
MarketplaceError::InvalidPlugin("missing or invalid plugin.json".to_string())
|
||||
})?;
|
||||
let description = manifest.description.clone();
|
||||
let marketplace_category = plugin
|
||||
.interface
|
||||
@@ -1637,8 +1672,14 @@ impl PluginsManager {
|
||||
})
|
||||
.collect();
|
||||
let auth_mode = self.auth_mode();
|
||||
let mut app_declarations = load_plugin_apps(source_path.as_path()).await;
|
||||
let mut mcp_servers = load_plugin_mcp_servers(source_path.as_path(), auth_mode).await;
|
||||
let mut app_declarations =
|
||||
load_plugin_apps_from_manifest(source_path.as_path(), &manifest.paths).await;
|
||||
let mut mcp_servers = load_plugin_mcp_servers_from_manifest(
|
||||
source_path.as_path(),
|
||||
&manifest.paths,
|
||||
/*plugin_policy*/ None,
|
||||
)
|
||||
.await;
|
||||
if auth_mode.is_some() {
|
||||
apply_app_mcp_routing_policy(
|
||||
&mut app_declarations,
|
||||
|
||||
Reference in New Issue
Block a user