feat: Handle alternate plugin manifest paths (#18182)

Load plugin manifests through a shared discoverable-path helper so
manifest reads, installs, and skill names all see the same alternate
manifest location.
This commit is contained in:
xl-openai authored and GitHub committed 2026-04-16 19:43:19 -07:00
1 parent a803790a10
commit 37161bc76e
13 files changed
+581 -222

No files matched your search

+93 -65
View File
@@ -26,8 +26,10 @@ const MARKETPLACE_MANIFEST_RELATIVE_PATHS: &[&str] = &[
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedMarketplacePlugin {
pub plugin_id: PluginId,
pub source_path: AbsolutePathBuf,
pub auth_policy: MarketplacePluginAuthPolicy,
pub source: MarketplacePluginSource,
pub policy: MarketplacePluginPolicy,
pub interface: Option<PluginManifestInterface>,
pub manifest: Option<crate::manifest::PluginManifest>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -158,12 +160,9 @@ impl MarketplaceError {
}
}
// Always read the specified marketplace file from disk so installs see the
// latest marketplace.json contents without any in-memory cache invalidation.
pub fn resolve_marketplace_plugin(
pub fn find_marketplace_plugin(
marketplace_path: &AbsolutePathBuf,
plugin_name: &str,
restriction_product: Option<Product>,
) -> Result<ResolvedMarketplacePlugin, MarketplaceError> {
let marketplace = load_raw_marketplace_manifest(marketplace_path)?;
let marketplace_name = marketplace.name;
@@ -173,39 +172,11 @@ pub fn resolve_marketplace_plugin(
continue;
}
let RawMarketplaceManifestPlugin {
name,
source,
policy,
..
} = plugin;
let install_policy = policy.installation;
let product_allowed = match policy.products.as_deref() {
None => true,
Some([]) => false,
Some(products) => restriction_product
.is_some_and(|product| product.matches_product_restriction(products)),
};
if install_policy == MarketplacePluginInstallPolicy::NotAvailable || !product_allowed {
return Err(MarketplaceError::PluginNotAvailable {
plugin_name: name,
marketplace_name,
});
if let Some(plugin) =
resolve_marketplace_plugin_entry(marketplace_path, &marketplace_name, plugin)?
{
return Ok(plugin);
}
let Some(source_path) =
resolve_supported_plugin_source_path(marketplace_path, &name, source)
else {
continue;
};
return Ok(ResolvedMarketplacePlugin {
plugin_id: PluginId::new(name, marketplace_name).map_err(|err| match err {
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
})?,
source_path,
auth_policy: policy.authentication,
});
}
Err(MarketplaceError::PluginNotFound {
@@ -214,6 +185,31 @@ pub fn resolve_marketplace_plugin(
})
}
pub fn find_installable_marketplace_plugin(
marketplace_path: &AbsolutePathBuf,
plugin_name: &str,
restriction_product: Option<Product>,
) -> Result<ResolvedMarketplacePlugin, MarketplaceError> {
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<MarketplaceListOutcome, MarketplaceError> {
@@ -270,36 +266,26 @@ pub fn load_marketplace(path: &AbsolutePathBuf) -> Result<Marketplace, Marketpla
let mut plugins = Vec::new();
for plugin in marketplace.plugins {
let RawMarketplaceManifestPlugin {
name,
source,
policy,
category,
} = plugin;
let Some(source_path) = resolve_supported_plugin_source_path(path, &name, source) else {
continue;
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),
};
let source = MarketplacePluginSource::Local {
path: source_path.clone(),
};
let mut interface =
load_plugin_manifest(source_path.as_path()).and_then(|manifest| manifest.interface);
if let Some(category) = category {
// Marketplace taxonomy wins when both sources provide a category.
interface
.get_or_insert_with(PluginManifestInterface::default)
.category = Some(category);
}
plugins.push(MarketplacePlugin {
name,
source,
policy: MarketplacePluginPolicy {
installation: policy.installation,
authentication: policy.authentication,
products: policy.products,
},
interface,
name: plugin.plugin_id.plugin_name,
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
});
}
@@ -389,6 +375,48 @@ fn load_raw_marketplace_manifest(
})
}
fn resolve_marketplace_plugin_entry(
marketplace_path: &AbsolutePathBuf,
marketplace_name: &str,
plugin: RawMarketplaceManifestPlugin,
) -> Result<Option<ResolvedMarketplacePlugin>, MarketplaceError> {
let RawMarketplaceManifestPlugin {
name,
source,
policy,
category,
} = plugin;
let Some(source_path) = resolve_supported_plugin_source_path(marketplace_path, &name, source)
else {
return Ok(None);
};
let manifest = load_plugin_manifest(source_path.as_path());
let mut interface = manifest
.as_ref()
.and_then(|manifest| manifest.interface.clone());
if let Some(category) = category {
// Marketplace taxonomy wins when both sources provide a category.
interface
.get_or_insert_with(PluginManifestInterface::default)
.category = Some(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: MarketplacePluginSource::Local { path: source_path },
policy: MarketplacePluginPolicy {
installation: policy.installation,
authentication: policy.authentication,
products: policy.products,
},
interface,
manifest,
}))
}
fn resolve_supported_plugin_source_path(
marketplace_path: &AbsolutePathBuf,
plugin_name: &str,