[plugins] Enforce marketplace source admission requirements (#29753)

## Why

Managed marketplace source requirements only become effective when every
local marketplace mutation path applies the same admission decision.
This change centralizes that decision so CLI, app-server, and
external-agent migration flows cannot add, install from, or refresh a
disallowed source.

## What changed

- Match exact normalized Git repository URLs with an optional exact
`ref`.
- Match Git hosts with managed regular expressions.
- Match local marketplaces by exact absolute path.
- Preserve the expected path/name boundary for managed OpenAI
marketplaces.
- Enforce source admission during marketplace add, plugin install, and
configured Git marketplace upgrade.
- Continue upgrading independent marketplaces when one source is
rejected and return a per-marketplace error.
- Load the effective requirements stack at CLI, app-server, and
external-agent migration entry points.

This PR does not filter already configured marketplaces at runtime; that
remains in draft follow-up #29691.

## Stack

This is PR 2 of 3 and is based on #29690, which introduces the
requirements data shape and merge behavior.

## Test plan

- Source matcher coverage for Git URL/ref, host-pattern, local-path, and
managed marketplace cases.
- Marketplace add and plugin install coverage for allowed and rejected
sources.
- Marketplace upgrade coverage for rejection and per-marketplace
continuation.
This commit is contained in:
xl-openai authored and GitHub committed 2026-06-23 20:13:11 -07:00
1 parent 31372078d1
commit 4fe02f4fcf
18 files changed
+1621 -194

No files matched your search

+40 -27
View File
@@ -34,9 +34,9 @@ use crate::marketplace::find_installable_marketplace_plugin;
use crate::marketplace::find_marketplace_plugin;
use crate::marketplace::list_marketplaces;
use crate::marketplace::plugin_interface_with_marketplace_category;
use crate::marketplace_policy::MarketplacePolicy;
use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeError;
use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome;
use crate::marketplace_upgrade::configured_git_marketplace_names;
use crate::marketplace_upgrade::upgrade_configured_git_marketplaces;
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
use crate::remote::RecommendedPluginsMode;
@@ -1253,19 +1253,10 @@ impl PluginsManager {
pub async fn install_plugin(
&self,
config_layer_stack: &ConfigLayerStack,
request: PluginInstallRequest,
) -> Result<PluginInstallOutcome, PluginInstallError> {
let resolved = match find_installable_marketplace_plugin(
&request.marketplace_path,
&request.plugin_name,
self.restriction_product,
) {
Ok(resolved) => resolved,
Err(err) => {
self.track_plugin_install_resolution_failed(&err);
return Err(err.into());
}
};
let resolved = self.resolve_installable_plugin(config_layer_stack, &request)?;
let plugin_id = resolved.plugin_id.clone();
match self.install_resolved_plugin(resolved).await {
Ok(outcome) => Ok(outcome),
@@ -1280,12 +1271,11 @@ impl PluginsManager {
}
}
pub async fn install_plugin_with_remote_sync(
fn resolve_installable_plugin(
&self,
config: &PluginsConfigInput,
auth: Option<&CodexAuth>,
request: PluginInstallRequest,
) -> Result<PluginInstallOutcome, PluginInstallError> {
config_layer_stack: &ConfigLayerStack,
request: &PluginInstallRequest,
) -> Result<ResolvedMarketplacePlugin, PluginInstallError> {
let resolved = match find_installable_marketplace_plugin(
&request.marketplace_path,
&request.plugin_name,
@@ -1297,6 +1287,32 @@ impl PluginsManager {
return Err(err.into());
}
};
if let Err(message) =
MarketplacePolicy::from_requirements(config_layer_stack.requirements())
.validate_install(
config_layer_stack,
self.codex_home.as_path(),
&request.marketplace_path,
&resolved.plugin_id.marketplace_name,
)
{
let err = MarketplaceError::InvalidMarketplaceFile {
path: request.marketplace_path.to_path_buf(),
message,
};
self.track_plugin_install_resolution_failed(&err);
return Err(err.into());
}
Ok(resolved)
}
pub async fn install_plugin_with_remote_sync(
&self,
config: &PluginsConfigInput,
auth: Option<&CodexAuth>,
request: PluginInstallRequest,
) -> Result<PluginInstallOutcome, PluginInstallError> {
let resolved = self.resolve_installable_plugin(&config.config_layer_stack, &request)?;
let plugin_id = resolved.plugin_id.as_key();
// This only forwards the backend mutation before the local install flow.
if let Err(err) = crate::remote_legacy::enable_remote_plugin(
@@ -1995,21 +2011,18 @@ impl PluginsManager {
config: &PluginsConfigInput,
marketplace_name: Option<&str>,
) -> Result<ConfiguredMarketplaceUpgradeOutcome, String> {
if let Some(marketplace_name) = marketplace_name
&& !configured_git_marketplace_names(&config.config_layer_stack)
.iter()
.any(|name| name == marketplace_name)
{
return Err(format!(
"marketplace `{marketplace_name}` is not configured as a Git marketplace"
));
}
let mut outcome = upgrade_configured_git_marketplaces(
self.codex_home.as_path(),
&config.config_layer_stack,
marketplace_name,
);
if let Some(marketplace_name) = marketplace_name
&& outcome.selected_marketplaces.is_empty()
{
return Err(format!(
"marketplace `{marketplace_name}` is not configured as a Git marketplace"
));
}
if !outcome.upgraded_roots.is_empty() {
match refresh_non_curated_plugin_cache_force_reinstall(
self.codex_home.as_path(),