Files
codex/codex-rs/cli/src/marketplace_cmd.rs
T
cooper-oai 44f1b0e867 Apply runtime authentication to realtime connections (#60003)
## Why

Realtime connections bypassed runtime authentication and reused captured authentication headers for sideband handshakes. They need to honor the selected credential source and its transport policy across call creation, retries, and reconnects.

## What changed

- Use runtime authentication for realtime WebSocket connections, WebRTC call creation, and sideband attachment when enabled for supported OpenAI destinations.
- Resolve authentication on every WebSocket handshake, preserving credential ownership and request scope while generating fresh assertions. Reject authentication failures without falling back to bearer authentication.
- Keep call creation and sideband attachment bound to the selected credentials and account network policy, including when inference uses a different provider.
- Avoid runtime initialization for local marketplace and MCP operations, plugin commands that do not use the remote catalog, and model debugging with a configured catalog.

## Testing

Add coverage for fresh handshake credentials on retries and reconnects, credential and account changes, runtime authentication modes, destination validation, and registration failures.

GitOrigin-RevId: 57e53122912fa78a524427b0f51013f76f9f7681
2026-11-06 22:27:23 +00:00

588 lines
18 KiB
Rust

use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use clap::Parser;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::LoaderOverrides;
use codex_core::plugins_manager_for_config;
use codex_core_plugins::PluginMarketplaceUpgradeOutcome;
use codex_core_plugins::PluginsConfigInput;
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
use codex_core_plugins::installed_marketplaces::resolve_configured_marketplace_root;
use codex_core_plugins::marketplace::marketplace_root_dir;
use codex_core_plugins::marketplace_add::MarketplaceAddOutcome;
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
use codex_core_plugins::marketplace_add::add_marketplace;
use codex_core_plugins::marketplace_remove::MarketplaceRemoveOutcome;
use codex_core_plugins::marketplace_remove::MarketplaceRemoveRequest;
use codex_core_plugins::marketplace_remove::remove_marketplace;
use codex_utils_cli::CliConfigOverrides;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use crate::cloud_config;
use crate::plugin_cmd::JsonMarketplaceSource;
use crate::plugin_cmd::configured_marketplace_snapshot_issues;
use crate::plugin_cmd::configured_marketplace_sources;
use crate::plugin_cmd::load_cli_auth_manager;
#[derive(Debug, Parser)]
#[command(bin_name = "codex plugin marketplace")]
pub struct MarketplaceCli {
#[clap(flatten)]
pub config_overrides: CliConfigOverrides,
#[command(subcommand)]
subcommand: MarketplaceSubcommand,
}
#[derive(Debug, clap::Subcommand)]
enum MarketplaceSubcommand {
/// Add a local or Git marketplace to the configured marketplace sources.
Add(AddMarketplaceArgs),
/// List plugin marketplaces Codex is currently considering and their roots.
List(ListMarketplaceArgs),
/// Refresh configured Git marketplace snapshots.
///
/// Omit MARKETPLACE_NAME to upgrade all configured Git marketplaces.
Upgrade(UpgradeMarketplaceArgs),
/// Remove a configured marketplace source by name.
Remove(RemoveMarketplaceArgs),
}
#[derive(Debug, Parser)]
#[command(
bin_name = "codex plugin marketplace add",
after_help = "Examples:\n codex plugin marketplace add ./path/to/marketplace\n codex plugin marketplace add owner/repo --ref main\n codex plugin marketplace add https://github.com/owner/repo --sparse plugins/foo"
)]
struct AddMarketplaceArgs {
/// Marketplace source: a local path, owner/repo[@ref], HTTPS Git URL, or SSH Git URL.
#[arg(value_name = "SOURCE")]
source: String,
/// Git ref to fetch for Git marketplace sources.
#[arg(long = "ref", value_name = "REF")]
ref_name: Option<String>,
/// Sparse checkout path for Git marketplace sources. Can be repeated.
#[arg(
long = "sparse",
value_name = "PATH",
action = clap::ArgAction::Append
)]
sparse_paths: Vec<String>,
/// Output add result as JSON.
#[arg(long = "json")]
json: bool,
}
#[derive(Debug, Parser)]
#[command(bin_name = "codex plugin marketplace list")]
struct ListMarketplaceArgs {
/// Output marketplace list as JSON.
#[arg(long = "json")]
json: bool,
}
#[derive(Debug, Parser)]
#[command(
bin_name = "codex plugin marketplace upgrade",
after_help = "Examples:\n codex plugin marketplace upgrade\n codex plugin marketplace upgrade debug"
)]
struct UpgradeMarketplaceArgs {
/// Optional configured marketplace name to upgrade. Omit to upgrade all Git marketplaces.
#[arg(value_name = "MARKETPLACE_NAME")]
marketplace_name: Option<String>,
/// Output upgrade result as JSON.
#[arg(long = "json")]
json: bool,
}
#[derive(Debug, Parser)]
#[command(
bin_name = "codex plugin marketplace remove",
after_help = "Example:\n codex plugin marketplace remove debug"
)]
struct RemoveMarketplaceArgs {
/// Configured marketplace name to remove.
#[arg(value_name = "MARKETPLACE_NAME")]
marketplace_name: String,
/// Output remove result as JSON.
#[arg(long = "json")]
json: bool,
}
impl MarketplaceCli {
pub async fn run(self) -> Result<()> {
let MarketplaceCli {
config_overrides,
subcommand,
} = self;
let builder = cloud_config::config_builder(
&config_overrides,
LoaderOverrides::default(),
ConfigOverrides::default(),
)
.await?;
let config = builder.clone().build().await?;
match subcommand {
MarketplaceSubcommand::Add(args) => run_add(config, args).await?,
MarketplaceSubcommand::List(args) => run_list(config, args).await?,
MarketplaceSubcommand::Upgrade(args) => run_upgrade(config, args, builder).await?,
MarketplaceSubcommand::Remove(args) => run_remove(config, args).await?,
}
Ok(())
}
}
async fn run_add(config: Config, args: AddMarketplaceArgs) -> Result<()> {
let AddMarketplaceArgs {
source,
ref_name,
sparse_paths,
json,
} = args;
let outcome = add_marketplace(
config.codex_home.to_path_buf(),
config.config_layer_stack.requirements().clone(),
MarketplaceAddRequest {
source,
ref_name,
sparse_paths,
},
)
.await?;
if json {
let output = JsonMarketplaceAddOutput::from_outcome(outcome);
println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}
if outcome.already_added {
println!(
"Marketplace `{}` is already added from {}.",
outcome.marketplace_name, outcome.source_display
);
} else {
println!(
"Added marketplace `{}` from {}.",
outcome.marketplace_name, outcome.source_display
);
}
println!(
"Installed marketplace root: {}",
outcome.installed_root.as_path().display()
);
Ok(())
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonMarketplaceAddOutput {
marketplace_name: String,
installed_root: String,
already_added: bool,
}
impl JsonMarketplaceAddOutput {
fn from_outcome(outcome: MarketplaceAddOutcome) -> Self {
Self {
marketplace_name: outcome.marketplace_name,
installed_root: outcome.installed_root.as_path().display().to_string(),
already_added: outcome.already_added,
}
}
}
async fn run_list(config: Config, args: ListMarketplaceArgs) -> Result<()> {
let manager = plugins_manager_for_config(&config, load_cli_auth_manager(&config).await?);
let plugins_input = config.plugins_config_input();
let marketplace_listing = manager
.discover_marketplaces_for_config(&plugins_input, &[])
.context("failed to list plugin marketplaces")?;
let mut load_issues = configured_marketplace_snapshot_issues(
config.codex_home.as_path(),
&plugins_input,
&marketplace_listing.errors,
/*marketplace_name*/ None,
);
let mut issue_paths = load_issues
.iter()
.map(|issue| issue.path.clone())
.collect::<HashSet<_>>();
for error in &marketplace_listing.errors {
if issue_paths.insert(error.path.to_path_buf()) {
load_issues.push(crate::plugin_cmd::ConfiguredMarketplaceSnapshotIssue {
marketplace_name: error.path.display().to_string(),
path: error.path.to_path_buf(),
message: error.message.clone(),
});
}
}
if !load_issues.is_empty() {
let issue_lines = load_issues
.iter()
.map(|issue| {
format!(
"- `{}` at {}: {}",
issue.marketplace_name,
issue.path.display(),
issue.message
)
})
.collect::<Vec<_>>()
.join("\n");
bail!("failed to load marketplace(s):\n{issue_lines}");
}
let marketplaces = marketplace_listing.marketplaces;
if args.json {
let marketplace_sources =
configured_marketplace_sources_by_root(config.codex_home.as_path(), &plugins_input);
let output =
JsonMarketplaceListOutput::from_marketplaces(marketplaces, &marketplace_sources);
println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}
if marketplaces.is_empty() {
println!("No plugin marketplaces in scope.");
return Ok(());
}
let mut seen_roots = HashSet::new();
let mut rows = Vec::new();
for marketplace in marketplaces {
let Ok(root) = marketplace_root_dir(&marketplace.path) else {
continue;
};
if !seen_roots.insert(root.clone()) {
continue;
}
rows.push((marketplace.name, root));
}
let marketplace_width = rows
.iter()
.map(|(name, _)| name.len())
.max()
.unwrap_or("MARKETPLACE".len())
.max("MARKETPLACE".len());
println!("{:<marketplace_width$} ROOT", "MARKETPLACE");
for (marketplace_name, root) in rows {
println!(
"{:<marketplace_width$} {}",
marketplace_name,
root.display()
);
}
Ok(())
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonMarketplaceListOutput {
marketplaces: Vec<JsonMarketplaceListEntry>,
}
impl JsonMarketplaceListOutput {
fn from_marketplaces(
marketplaces: Vec<codex_core_plugins::marketplace::Marketplace>,
marketplace_sources: &HashMap<PathBuf, JsonMarketplaceSource>,
) -> Self {
let mut seen_roots = HashSet::new();
let marketplaces = marketplaces
.into_iter()
.filter_map(|marketplace| {
let root = marketplace_root_dir(&marketplace.path).ok()?;
if !seen_roots.insert(root.clone()) {
return None;
}
Some(JsonMarketplaceListEntry {
marketplace_source: marketplace_sources.get(root.as_path()).cloned(),
name: marketplace.name,
root: root.display().to_string(),
})
})
.collect();
Self { marketplaces }
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonMarketplaceListEntry {
name: String,
root: String,
#[serde(skip_serializing_if = "Option::is_none")]
marketplace_source: Option<JsonMarketplaceSource>,
}
fn configured_marketplace_sources_by_root(
codex_home: &Path,
plugins_input: &PluginsConfigInput,
) -> HashMap<PathBuf, JsonMarketplaceSource> {
let marketplace_sources = configured_marketplace_sources(plugins_input, codex_home);
let effective_config = plugins_input.config_layer_stack.effective_config();
let Some(marketplaces) = effective_config
.get("marketplaces")
.and_then(toml::Value::as_table)
else {
return HashMap::new();
};
let default_install_root = marketplace_install_root(codex_home);
marketplaces
.iter()
.filter_map(|(marketplace_name, marketplace)| {
let marketplace_source = marketplace_sources.get(marketplace_name)?;
let root = resolve_configured_marketplace_root(
marketplace_name,
marketplace,
&default_install_root,
)?;
Some((root, marketplace_source.clone()))
})
.collect()
}
async fn run_upgrade(
config: Config,
args: UpgradeMarketplaceArgs,
builder: codex_core::config::ConfigBuilder,
) -> Result<()> {
let UpgradeMarketplaceArgs {
marketplace_name,
json,
} = args;
let manager = plugins_manager_for_config(&config, load_cli_auth_manager(&config).await?);
let plugins_input = config.plugins_config_input();
let runtime = tokio::runtime::Handle::current();
let reload_config: codex_core_plugins::ConfigLayerReload = std::sync::Arc::new(move || {
runtime
.block_on(builder.clone().build())
.map(|config| config.config_layer_stack)
});
let requested_name = marketplace_name.clone();
let outcome = tokio::task::spawn_blocking(move || {
manager.upgrade_configured_marketplaces_for_config(
&plugins_input,
requested_name.as_deref(),
&reload_config,
)
})
.await?
.map_err(anyhow::Error::msg)?;
if json {
print_upgrade_outcome_json(&outcome)
} else {
print_upgrade_outcome(&outcome, marketplace_name.as_deref())
}
}
async fn run_remove(config: Config, args: RemoveMarketplaceArgs) -> Result<()> {
let RemoveMarketplaceArgs {
marketplace_name,
json,
} = args;
let outcome = remove_marketplace(
config.codex_home.to_path_buf(),
config.config_layer_stack,
MarketplaceRemoveRequest { marketplace_name },
)
.await?;
if json {
let output = JsonMarketplaceRemoveOutput::from_outcome(outcome);
println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}
println!("Removed marketplace `{}`.", outcome.marketplace_name);
if let Some(installed_root) = outcome.removed_installed_root {
println!(
"Removed installed marketplace root: {}",
installed_root.as_path().display()
);
}
Ok(())
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonMarketplaceRemoveOutput {
marketplace_name: String,
installed_root: Option<String>,
}
impl JsonMarketplaceRemoveOutput {
fn from_outcome(outcome: MarketplaceRemoveOutcome) -> Self {
Self {
marketplace_name: outcome.marketplace_name,
installed_root: outcome
.removed_installed_root
.map(|root| root.as_path().display().to_string()),
}
}
}
fn print_upgrade_outcome_json(outcome: &PluginMarketplaceUpgradeOutcome) -> Result<()> {
for error in &outcome.errors {
eprintln!(
"Failed to upgrade marketplace `{}`: {}",
error.marketplace_name, error.message
);
}
if !outcome.all_succeeded() {
bail!("{} upgrade failure(s) occurred.", outcome.errors.len());
}
let output = JsonMarketplaceUpgradeOutput::from_outcome(outcome);
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonMarketplaceUpgradeOutput {
selected_marketplaces: Vec<String>,
upgraded_roots: Vec<String>,
errors: Vec<JsonMarketplaceUpgradeError>,
}
impl JsonMarketplaceUpgradeOutput {
fn from_outcome(outcome: &PluginMarketplaceUpgradeOutcome) -> Self {
Self {
selected_marketplaces: outcome.selected_marketplaces.clone(),
upgraded_roots: outcome
.upgraded_roots
.iter()
.map(|root| root.display().to_string())
.collect(),
errors: outcome
.errors
.iter()
.map(|error| JsonMarketplaceUpgradeError {
marketplace_name: error.marketplace_name.clone(),
message: error.message.clone(),
})
.collect(),
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonMarketplaceUpgradeError {
marketplace_name: String,
message: String,
}
fn print_upgrade_outcome(
outcome: &PluginMarketplaceUpgradeOutcome,
marketplace_name: Option<&str>,
) -> Result<()> {
for error in &outcome.errors {
eprintln!(
"Failed to upgrade marketplace `{}`: {}",
error.marketplace_name, error.message
);
}
if !outcome.all_succeeded() {
bail!("{} upgrade failure(s) occurred.", outcome.errors.len());
}
let selection_label = marketplace_name.unwrap_or("all configured Git marketplaces");
if outcome.selected_marketplaces.is_empty() {
println!("No configured Git marketplaces to upgrade.");
} else if outcome.upgraded_roots.is_empty() {
if marketplace_name.is_some() {
println!("Marketplace `{selection_label}` is already up to date.");
} else {
println!("All configured Git marketplaces are already up to date.");
}
} else if marketplace_name.is_some() {
println!("Upgraded marketplace `{selection_label}` to the latest configured revision.");
for root in &outcome.upgraded_roots {
println!("Installed marketplace root: {}", root.display());
}
} else {
println!("Upgraded {} marketplace(s).", outcome.upgraded_roots.len());
for root in &outcome.upgraded_roots {
println!("Installed marketplace root: {}", root.display());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn sparse_paths_parse_before_or_after_source() {
let sparse_before_source =
AddMarketplaceArgs::try_parse_from(["add", "--sparse", "plugins/foo", "owner/repo"])
.unwrap();
assert_eq!(sparse_before_source.source, "owner/repo");
assert_eq!(sparse_before_source.sparse_paths, vec!["plugins/foo"]);
let sparse_after_source =
AddMarketplaceArgs::try_parse_from(["add", "owner/repo", "--sparse", "plugins/foo"])
.unwrap();
assert_eq!(sparse_after_source.source, "owner/repo");
assert_eq!(sparse_after_source.sparse_paths, vec!["plugins/foo"]);
let repeated_sparse = AddMarketplaceArgs::try_parse_from([
"add",
"--sparse",
"plugins/foo",
"--sparse",
"skills/bar",
"owner/repo",
])
.unwrap();
assert_eq!(repeated_sparse.source, "owner/repo");
assert_eq!(
repeated_sparse.sparse_paths,
vec!["plugins/foo", "skills/bar"]
);
}
#[test]
fn upgrade_subcommand_parses_optional_marketplace_name() {
let upgrade_all = UpgradeMarketplaceArgs::try_parse_from(["upgrade"]).unwrap();
assert_eq!(upgrade_all.marketplace_name, None);
let upgrade_one = UpgradeMarketplaceArgs::try_parse_from(["upgrade", "debug"]).unwrap();
assert_eq!(upgrade_one.marketplace_name.as_deref(), Some("debug"));
}
#[test]
fn remove_subcommand_parses_marketplace_name() {
let remove = RemoveMarketplaceArgs::try_parse_from(["remove", "debug"]).unwrap();
assert_eq!(remove.marketplace_name, "debug");
}
}