Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions codex-rs/app-server/src/config/external_agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use codex_core_plugins::PluginsManager;
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
use codex_core_plugins::marketplace_add::add_marketplace;
use codex_core_plugins::marketplace_add::add_marketplace_for_config;
use codex_core_plugins::marketplace_add::is_local_marketplace_source;
use codex_external_agent_migration::build_mcp_config_from_external;
use codex_external_agent_migration::count_missing_commands;
Expand Down Expand Up @@ -701,13 +701,49 @@ impl ExternalAgentConfigService {
};
let mut outcome = PluginImportOutcome::default();
let plugins_manager = PluginsManager::new(self.codex_home.clone());
let config = ConfigBuilder::default()
.codex_home(self.codex_home.clone())
.fallback_cwd(Some(self.codex_home.clone()))
.build()
.await
.map_err(|err| {
invalid_data_error(format!("failed to load config before plugin import: {err}"))
})?;
let plugin_marketplace_requirements = config
.config_layer_stack
.requirements()
.plugin_marketplaces
.as_ref();
if plugin_marketplace_requirements
.is_some_and(|requirements| !requirements.value.allows_user_additions())
{
for plugin_group in plugins {
let marketplace_name = plugin_group.marketplace_name;
outcome.failed_plugin_ids.extend(
plugin_group
.plugin_names
.into_iter()
.map(|plugin_name| format!("{plugin_name}@{marketplace_name}")),
);
outcome.failed_marketplaces.push(marketplace_name);
}
return Ok(outcome);
}
let plugins_input = config.plugins_config_input();
for plugin_group in plugins {
let marketplace_name = plugin_group.marketplace_name.clone();
let plugin_names = plugin_group.plugin_names;
let plugin_ids = plugin_names
.iter()
.map(|plugin_name| format!("{plugin_name}@{marketplace_name}"))
.collect::<Vec<_>>();
if plugin_marketplace_requirements.is_some_and(|requirements| {
!requirements.value.allows_marketplace(&marketplace_name)
}) {
outcome.failed_marketplaces.push(marketplace_name);
outcome.failed_plugin_ids.extend(plugin_ids);
continue;
}
let source_settings = cwd.map_or_else(
|| self.external_agent_home.join("settings.json"),
|cwd| cwd.join(EXTERNAL_AGENT_DIR).join("settings.json"),
Expand All @@ -728,7 +764,12 @@ 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_for_config(
&config.config_layer_stack,
self.codex_home.clone(),
request,
)
.await;
let marketplace_path = match add_marketplace_outcome {
Ok(add_marketplace_outcome) => {
let Some(marketplace_path) = find_marketplace_manifest_path(
Expand All @@ -751,10 +792,13 @@ impl ExternalAgentConfigService {
};
for plugin_name in plugin_names {
match plugins_manager
.install_plugin(PluginInstallRequest {
plugin_name: plugin_name.clone(),
marketplace_path: marketplace_path.clone(),
})
.install_plugin_for_config(
&plugins_input,
PluginInstallRequest {
plugin_name: plugin_name.clone(),
marketplace_path: marketplace_path.clone(),
},
)
.await
{
Ok(_) => outcome
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ use codex_core_plugins::marketplace::MarketplaceError;
use codex_core_plugins::marketplace::MarketplacePluginSource;
use codex_core_plugins::marketplace_add::MarketplaceAddError;
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
use codex_core_plugins::marketplace_add::add_marketplace as add_marketplace_to_codex_home;
use codex_core_plugins::marketplace_add::add_marketplace_for_config as add_marketplace_to_codex_home;
use codex_core_plugins::marketplace_remove::MarketplaceRemoveError;
use codex_core_plugins::marketplace_remove::MarketplaceRemoveRequest as CoreMarketplaceRemoveRequest;
use codex_core_plugins::marketplace_remove::remove_marketplace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ impl MarketplaceRequestProcessor {
&self,
params: MarketplaceAddParams,
) -> Result<MarketplaceAddResponse, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
add_marketplace_to_codex_home(
&config.config_layer_stack,
self.config.codex_home.to_path_buf(),
MarketplaceAddRequest {
source: params.source,
Expand Down
56 changes: 55 additions & 1 deletion codex-rs/app-server/src/request_processors/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ fn plugin_share_principal_from_remote(
}
}

fn remote_marketplace_is_allowed(config: &Config, marketplace_name: &str) -> bool {
config
.config_layer_stack
.requirements()
.plugin_marketplaces
.as_ref()
.is_none_or(|requirements| requirements.value.allows_marketplace(marketplace_name))
}

impl PluginRequestProcessor {
pub(crate) fn new(
auth_manager: Arc<AuthManager>,
Expand Down Expand Up @@ -485,6 +494,9 @@ impl PluginRequestProcessor {
Ok(remote_marketplaces) => {
for remote_marketplace in remote_marketplaces
.into_iter()
.filter(|marketplace| {
remote_marketplace_is_allowed(&config, &marketplace.name)
})
.map(remote_marketplace_to_info)
{
if let Some(existing) = data
Expand Down Expand Up @@ -620,6 +632,11 @@ impl PluginRequestProcessor {
}
}
Err(remote_marketplace_name) => {
if !remote_marketplace_is_allowed(&config, &remote_marketplace_name) {
return Err(invalid_request(format!(
"remote marketplace {remote_marketplace_name} is not allowed by managed requirements"
)));
}
if !config.features.enabled(Feature::Plugins) {
return Err(invalid_request(format!(
"remote plugin read is not enabled for marketplace {remote_marketplace_name}"
Expand All @@ -640,6 +657,12 @@ impl PluginRequestProcessor {
.map_err(|err| {
remote_plugin_catalog_error_to_jsonrpc(err, "read remote plugin details")
})?;
if !remote_marketplace_is_allowed(&config, &remote_detail.marketplace_name) {
return Err(invalid_request(format!(
"remote marketplace {} is not allowed by managed requirements",
remote_detail.marketplace_name
)));
}
let plugin_apps = remote_detail
.app_ids
.iter()
Expand Down Expand Up @@ -667,6 +690,11 @@ impl PluginRequestProcessor {
} = params;

let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
if !remote_marketplace_is_allowed(&config, &remote_marketplace_name) {
return Err(invalid_request(format!(
"remote marketplace {remote_marketplace_name} is not allowed by managed requirements"
)));
}
if !config.features.enabled(Feature::Plugins) {
return Err(invalid_request(format!(
"remote plugin skill read is not enabled for marketplace {remote_marketplace_name}"
Expand All @@ -683,6 +711,20 @@ impl PluginRequestProcessor {
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail(
&remote_plugin_service_config,
auth.as_ref(),
&remote_marketplace_name,
&remote_plugin_id,
)
.await
.map_err(|err| remote_plugin_catalog_error_to_jsonrpc(err, "read remote plugin details"))?;
if !remote_marketplace_is_allowed(&config, &remote_detail.marketplace_name) {
return Err(invalid_request(format!(
"remote marketplace {} is not allowed by managed requirements",
remote_detail.marketplace_name
)));
}
let remote_skill_detail = codex_core_plugins::remote::fetch_remote_plugin_skill_detail(
&remote_plugin_service_config,
auth.as_ref(),
Expand Down Expand Up @@ -888,13 +930,14 @@ impl PluginRequestProcessor {
}

let plugins_manager = self.thread_manager.plugins_manager();
let plugins_input = config.plugins_config_input();
let request = PluginInstallRequest {
plugin_name,
marketplace_path,
};

let result = plugins_manager
.install_plugin(request)
.install_plugin_for_config(&plugins_input, request)
.await
.map_err(Self::plugin_install_error)?;
let config = match self.load_latest_config(config_cwd).await {
Expand Down Expand Up @@ -938,6 +981,11 @@ impl PluginRequestProcessor {
remote_plugin_id: String,
) -> Result<PluginInstallResponse, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
if !remote_marketplace_is_allowed(&config, &remote_marketplace_name) {
return Err(invalid_request(format!(
"remote marketplace {remote_marketplace_name} is not allowed by managed requirements"
)));
}
if !config.features.enabled(Feature::Plugins) {
return Err(invalid_request(format!(
"remote plugin install is not enabled for marketplace {remote_marketplace_name}"
Expand All @@ -963,6 +1011,12 @@ impl PluginRequestProcessor {
"read remote plugin details before install",
)
})?;
if !remote_marketplace_is_allowed(&config, &remote_detail.marketplace_name) {
return Err(invalid_request(format!(
"remote marketplace {} is not allowed by managed requirements",
remote_detail.marketplace_name
)));
}
if remote_detail.summary.availability == PluginAvailability::DisabledByAdmin {
let remote_plugin_id = &remote_detail.summary.id;
return Err(invalid_request(format!(
Expand Down
40 changes: 40 additions & 0 deletions codex-rs/app-server/tests/suite/v2/plugin_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,46 @@ async fn plugin_skill_read_reads_remote_skill_contents_when_remote_plugin_enable
"plugin_release_skill_id": "skill-1",
"skill_md_contents": "# Plan Work\n\nUse Linear issues to create a plan."
}"##;
let detail_body = r#"{
"id": "plugins~Plugin_00000000000000000000000000000000",
"name": "linear",
"scope": "GLOBAL",
"installation_policy": "AVAILABLE",
"authentication_policy": "ON_USE",
"release": {
"display_name": "Linear",
"description": "Track work in Linear",
"app_ids": [],
"keywords": [],
"interface": {},
"skills": []
}
}"#;
let installed_body = r#"{
"plugins": [],
"pagination": {
"limit": 50,
"next_page_token": null
}
}"#;

Mock::given(method("GET"))
.and(path(
"/backend-api/ps/plugins/plugins~Plugin_00000000000000000000000000000000",
))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.respond_with(ResponseTemplate::new(200).set_body_string(detail_body))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/backend-api/ps/plugins/installed"))
.and(query_param("scope", "GLOBAL"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.respond_with(ResponseTemplate::new(200).set_body_string(installed_body))
.mount(&server)
.await;

Mock::given(method("GET"))
.and(path(
Expand Down
12 changes: 8 additions & 4 deletions codex-rs/cli/src/marketplace_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use codex_core::config::find_codex_home;
use codex_core_plugins::PluginMarketplaceUpgradeOutcome;
use codex_core_plugins::PluginsManager;
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
use codex_core_plugins::marketplace_add::add_marketplace;
use codex_core_plugins::marketplace_add::add_marketplace_for_config;
use codex_core_plugins::marketplace_remove::MarketplaceRemoveRequest;
use codex_core_plugins::marketplace_remove::remove_marketplace;
use codex_utils_cli::CliConfigOverrides;
Expand Down Expand Up @@ -72,7 +72,7 @@ impl MarketplaceCli {
.map_err(anyhow::Error::msg)?;

match subcommand {
MarketplaceSubcommand::Add(args) => run_add(args).await?,
MarketplaceSubcommand::Add(args) => run_add(overrides, args).await?,
MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?,
MarketplaceSubcommand::Remove(args) => run_remove(args).await?,
}
Expand All @@ -81,15 +81,19 @@ impl MarketplaceCli {
}
}

async fn run_add(args: AddMarketplaceArgs) -> Result<()> {
async fn run_add(overrides: Vec<(String, toml::Value)>, args: AddMarketplaceArgs) -> Result<()> {
let AddMarketplaceArgs {
source,
ref_name,
sparse_paths,
} = args;

let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
let outcome = add_marketplace(
let outcome = add_marketplace_for_config(
&config.config_layer_stack,
codex_home.to_path_buf(),
MarketplaceAddRequest {
source,
Expand Down
Loading
Loading