Expose configured marketplace source in plugin list JSON (#26417)

## Summary
- Follow-up to #25330
- Add `marketplaceSource` to `codex plugin list --json` entries for
configured marketplaces
- Keep the existing per-plugin `source` field unchanged; this still
reports the local plugin source path
- Include only the configured marketplace `sourceType` and `source` from
`config.toml`
- Keep human-readable output unchanged
- Add CLI coverage for configured local and git marketplace sources

Example:

```json
{
  "source": {
    "source": "local",
    "path": "/path/to/.codex/.tmp/marketplaces/debug/plugins/sample"
  },
  "marketplaceSource": {
    "sourceType": "git",
    "source": "https://example.com/acme/agent-skills.git"
  }
}
```

## Validation
- `just fmt`
- `just fix -p codex-cli`
- `just test -p codex-cli plugin_list`
This commit is contained in:
mpc-oai
2026-06-04 12:20:32 -05:00
committed by GitHub
Unverified
parent cbf62f64cb
commit cdc1d592df
2 changed files with 126 additions and 2 deletions
+56 -2
View File
@@ -20,6 +20,7 @@ use codex_plugin::PluginId;
use codex_plugin::validate_plugin_segment;
use codex_utils_cli::CliConfigOverrides;
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
@@ -177,9 +178,14 @@ pub async fn run_plugin_list(
.is_none_or(|name| marketplace.name == *name)
})
.collect::<Vec<_>>();
let marketplace_sources = configured_marketplace_sources(&plugins_input);
if args.json {
let output = JsonPluginListOutput::from_marketplaces(marketplaces, args.available);
let output = JsonPluginListOutput::from_marketplaces(
marketplaces,
args.available,
&marketplace_sources,
);
println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}
@@ -269,13 +275,19 @@ impl JsonPluginListOutput {
fn from_marketplaces(
marketplaces: Vec<codex_core_plugins::ConfiguredMarketplace>,
include_available: bool,
marketplace_sources: &HashMap<String, JsonMarketplaceSource>,
) -> Self {
let mut installed = Vec::new();
let mut available = Vec::new();
for marketplace in marketplaces {
let marketplace_source = marketplace_sources.get(&marketplace.name).cloned();
for plugin in marketplace.plugins {
let entry = JsonPluginListEntry::from_configured_plugin(&marketplace.name, plugin);
let entry = JsonPluginListEntry::from_configured_plugin(
&marketplace.name,
marketplace_source.clone(),
plugin,
);
if entry.installed {
installed.push(entry);
} else if include_available {
@@ -301,6 +313,8 @@ struct JsonPluginListEntry {
installed: bool,
enabled: bool,
source: JsonPluginSource,
#[serde(skip_serializing_if = "Option::is_none")]
marketplace_source: Option<JsonMarketplaceSource>,
install_policy: &'static str,
auth_policy: &'static str,
}
@@ -308,6 +322,7 @@ struct JsonPluginListEntry {
impl JsonPluginListEntry {
fn from_configured_plugin(
marketplace_name: &str,
marketplace_source: Option<JsonMarketplaceSource>,
plugin: codex_core_plugins::ConfiguredMarketplacePlugin,
) -> Self {
let version = plugin.installed_version.or(plugin.local_version);
@@ -319,6 +334,7 @@ impl JsonPluginListEntry {
installed: plugin.installed,
enabled: plugin.enabled,
source: JsonPluginSource::from_marketplace_source(plugin.source),
marketplace_source,
install_policy: install_policy_label(plugin.policy.installation),
auth_policy: auth_policy_label(plugin.policy.authentication),
}
@@ -375,6 +391,44 @@ impl JsonPluginSource {
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonMarketplaceSource {
source_type: String,
source: String,
}
fn configured_marketplace_sources(
plugins_input: &PluginsConfigInput,
) -> HashMap<String, JsonMarketplaceSource> {
let Some(user_config) = plugins_input.config_layer_stack.effective_user_config() else {
return HashMap::new();
};
let Some(marketplaces) = user_config
.get("marketplaces")
.and_then(toml::Value::as_table)
else {
return HashMap::new();
};
marketplaces
.iter()
.filter_map(|(marketplace_name, marketplace)| {
let source_type = marketplace
.get("source_type")
.and_then(toml::Value::as_str)?;
let source = marketplace.get("source").and_then(toml::Value::as_str)?;
Some((
marketplace_name.clone(),
JsonMarketplaceSource {
source_type: source_type.to_string(),
source: source.to_string(),
},
))
})
.collect()
}
fn install_policy_label(policy: MarketplacePluginInstallPolicy) -> &'static str {
match policy {
MarketplacePluginInstallPolicy::NotAvailable => "NOT_AVAILABLE",