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",
+70
View File
@@ -2,6 +2,7 @@ use anyhow::Result;
use codex_config::CONFIG_TOML_FILE;
use codex_config::MarketplaceConfigUpdate;
use codex_config::record_user_marketplace;
use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use pretty_assertions::assert_eq;
@@ -495,6 +496,7 @@ async fn plugin_list_prints_plugins_in_a_table() -> Result<()> {
async fn plugin_list_json_prints_available_plugins_when_requested() -> Result<()> {
let (codex_home, source) = setup_local_marketplace()?;
let plugin_path = source.path().join("plugins").join("sample");
let source_path = source.path().to_string_lossy().into_owned();
let assert = codex_command(codex_home.path())?
.args(["plugin", "list", "--available", "--json"])
@@ -519,6 +521,69 @@ async fn plugin_list_json_prints_available_plugins_when_requested() -> Result<()
"source": "local",
"path": plugin_path.display().to_string(),
},
"marketplaceSource": {
"sourceType": "local",
"source": source_path,
},
"installPolicy": "AVAILABLE",
"authPolicy": "ON_INSTALL",
},
],
})
);
Ok(())
}
#[tokio::test]
async fn plugin_list_json_includes_configured_git_marketplace_source() -> Result<()> {
let codex_home = TempDir::new()?;
let marketplace_root = codex_home
.path()
.join(".tmp")
.join("marketplaces")
.join("debug");
write_plugins_enabled_config(codex_home.path())?;
write_marketplace_source(&marketplace_root)?;
let update = MarketplaceConfigUpdate {
last_updated: "2026-06-04T08:39:49Z",
last_revision: Some("abc123"),
source_type: "git",
source: "https://example.com/acme/agent-skills.git",
ref_name: None,
sparse_paths: &[],
};
record_user_marketplace(codex_home.path(), "debug", &update)?;
let plugin_path = marketplace_root.join("plugins").join("sample");
let normalized_plugin_path = canonicalize_existing_preserving_symlinks(&plugin_path)?;
let assert = codex_command(codex_home.path())?
.args(["plugin", "list", "--available", "--json"])
.assert()
.success();
let stdout = assert.get_output().stdout.as_slice();
let actual: serde_json::Value = serde_json::from_slice(stdout)?;
assert_eq!(
actual,
json!({
"installed": [],
"available": [
{
"pluginId": "sample@debug",
"name": "sample",
"marketplaceName": "debug",
"version": "1.2.3",
"installed": false,
"enabled": false,
"source": {
"source": "local",
"path": normalized_plugin_path.display().to_string(),
},
"marketplaceSource": {
"sourceType": "git",
"source": "https://example.com/acme/agent-skills.git",
},
"installPolicy": "AVAILABLE",
"authPolicy": "ON_INSTALL",
},
@@ -533,6 +598,7 @@ async fn plugin_list_json_prints_available_plugins_when_requested() -> Result<()
async fn plugin_list_json_prints_installed_plugins() -> Result<()> {
let (codex_home, source) = setup_local_marketplace()?;
let plugin_path = source.path().join("plugins").join("sample");
let source_path = source.path().to_string_lossy().into_owned();
codex_command(codex_home.path())?
.args(["plugin", "add", "sample@debug"])
@@ -561,6 +627,10 @@ async fn plugin_list_json_prints_installed_plugins() -> Result<()> {
"source": "local",
"path": plugin_path.display().to_string(),
},
"marketplaceSource": {
"sourceType": "local",
"source": source_path,
},
"installPolicy": "AVAILABLE",
"authPolicy": "ON_INSTALL",
},