[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
2026-06-23 20:13:11 -07:00
committed by GitHub
parent 31372078d1
commit 4fe02f4fcf
18 changed files with 1621 additions and 194 deletions
@@ -877,6 +877,16 @@ impl ExternalAgentConfigService {
"plugins migration item is missing details".to_string(),
));
};
let config = ConfigBuilder::default()
.codex_home(self.codex_home.clone())
.fallback_cwd(Some(
cwd.map(Path::to_path_buf)
.unwrap_or_else(|| self.codex_home.clone()),
))
.build()
.await
.map_err(|err| io::Error::other(format!("failed to load config: {err}")))?;
let requirements = config.config_layer_stack.requirements().clone();
let mut outcome = PluginImportOutcome::default();
let plugins_manager = PluginsManager::new(self.codex_home.clone());
for plugin_group in plugins {
@@ -916,7 +926,8 @@ impl ExternalAgentConfigService {
ref_name: import_source.ref_name,
sparse_paths: Vec::new(),
};
let add_marketplace_outcome = add_marketplace(self.codex_home.clone(), request).await;
let add_marketplace_outcome =
add_marketplace(self.codex_home.clone(), requirements.clone(), request).await;
let marketplace_path = match add_marketplace_outcome {
Ok(add_marketplace_outcome) => {
let Some(marketplace_path) = find_marketplace_manifest_path(
@@ -954,12 +965,37 @@ impl ExternalAgentConfigService {
continue;
}
};
let install_config = match ConfigBuilder::default()
.codex_home(self.codex_home.clone())
.fallback_cwd(Some(
cwd.map(Path::to_path_buf)
.unwrap_or_else(|| self.codex_home.clone()),
))
.build()
.await
{
Ok(config) => config,
Err(err) => {
record_plugin_import_errors(
&mut outcome,
cwd,
&plugin_ids,
"plugin_import",
format!("failed to reload config after adding marketplace: {err}"),
);
outcome.failed_plugin_ids.extend(plugin_ids);
continue;
}
};
for plugin_name in plugin_names {
match plugins_manager
.install_plugin(PluginInstallRequest {
plugin_name: plugin_name.clone(),
marketplace_path: marketplace_path.clone(),
})
.install_plugin(
&install_config.config_layer_stack,
PluginInstallRequest {
plugin_name: plugin_name.clone(),
marketplace_path: marketplace_path.clone(),
},
)
.await
{
Ok(_) => outcome
@@ -105,8 +105,10 @@ impl MarketplaceRequestProcessor {
&self,
params: MarketplaceAddParams,
) -> Result<MarketplaceAddResponse, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
add_marketplace_to_codex_home(
self.config.codex_home.to_path_buf(),
config.config_layer_stack.requirements().clone(),
MarketplaceAddRequest {
source: params.source,
ref_name: params.ref_name,
@@ -1449,7 +1449,10 @@ impl PluginRequestProcessor {
marketplace_path,
};
let result = match plugins_manager.install_plugin(request).await {
let result = match plugins_manager
.install_plugin(&config.config_layer_stack, request)
.await
{
Ok(result) => result,
Err(err) => {
warn!(