[codex] Add marketplace/remove app-server RPC (#17751)

## Summary

Add a new app-server `marketplace/remove` RPC on top of the shared
marketplace-remove implementation.

This change:
- adds `MarketplaceRemoveParams` / `MarketplaceRemoveResponse` to the
app-server protocol
- wires the new request through `codex_message_processor`
- reuses the shared core marketplace-remove flow from the stacked
refactor PR
- updates generated schema files and adds focused app-server coverage

## Validation

- `just write-app-server-schema`
- `just fmt`
- heavy compile/test coverage deferred to GitHub CI per request
This commit is contained in:
xli-oai
2026-04-19 23:22:49 -07:00
committed by GitHub
Unverified
parent b44d2851cf
commit 1dc3535e17
16 changed files with 425 additions and 1 deletions
@@ -1287,6 +1287,17 @@
],
"type": "object"
},
"MarketplaceRemoveParams": {
"properties": {
"marketplaceName": {
"type": "string"
}
},
"required": [
"marketplaceName"
],
"type": "object"
},
"McpResourceReadParams": {
"properties": {
"server": {
@@ -4245,6 +4256,30 @@
"title": "Marketplace/addRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"marketplace/remove"
],
"title": "Marketplace/removeRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/MarketplaceRemoveParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Marketplace/removeRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -675,6 +675,30 @@
"title": "Marketplace/addRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"marketplace/remove"
],
"title": "Marketplace/removeRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/MarketplaceRemoveParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Marketplace/removeRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -9333,6 +9357,42 @@
],
"type": "object"
},
"MarketplaceRemoveParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"marketplaceName": {
"type": "string"
}
},
"required": [
"marketplaceName"
],
"title": "MarketplaceRemoveParams",
"type": "object"
},
"MarketplaceRemoveResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"installedRoot": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"marketplaceName": {
"type": "string"
}
},
"required": [
"marketplaceName"
],
"title": "MarketplaceRemoveResponse",
"type": "object"
},
"McpAuthStatus": {
"enum": [
"unsupported",
@@ -1271,6 +1271,30 @@
"title": "Marketplace/addRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"marketplace/remove"
],
"title": "Marketplace/removeRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/MarketplaceRemoveParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Marketplace/removeRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -6065,6 +6089,42 @@
],
"type": "object"
},
"MarketplaceRemoveParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"marketplaceName": {
"type": "string"
}
},
"required": [
"marketplaceName"
],
"title": "MarketplaceRemoveParams",
"type": "object"
},
"MarketplaceRemoveResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"installedRoot": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"marketplaceName": {
"type": "string"
}
},
"required": [
"marketplaceName"
],
"title": "MarketplaceRemoveResponse",
"type": "object"
},
"McpAuthStatus": {
"enum": [
"unsupported",
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"marketplaceName": {
"type": "string"
}
},
"required": [
"marketplaceName"
],
"title": "MarketplaceRemoveParams",
"type": "object"
}
@@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
}
},
"properties": {
"installedRoot": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"marketplaceName": {
"type": "string"
}
},
"required": [
"marketplaceName"
],
"title": "MarketplaceRemoveResponse",
"type": "object"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type MarketplaceRemoveParams = { marketplaceName: string, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
export type MarketplaceRemoveResponse = { marketplaceName: string, installedRoot: AbsolutePathBuf | null, };
@@ -157,6 +157,8 @@ export type { MarketplaceAddParams } from "./MarketplaceAddParams";
export type { MarketplaceAddResponse } from "./MarketplaceAddResponse";
export type { MarketplaceInterface } from "./MarketplaceInterface";
export type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo";
export type { MarketplaceRemoveParams } from "./MarketplaceRemoveParams";
export type { MarketplaceRemoveResponse } from "./MarketplaceRemoveResponse";
export type { McpAuthStatus } from "./McpAuthStatus";
export type { McpElicitationArrayType } from "./McpElicitationArrayType";
export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema";
@@ -344,6 +344,10 @@ client_request_definitions! {
params: v2::MarketplaceAddParams,
response: v2::MarketplaceAddResponse,
},
MarketplaceRemove => "marketplace/remove" {
params: v2::MarketplaceRemoveParams,
response: v2::MarketplaceRemoveResponse,
},
PluginList => "plugin/list" {
params: v2::PluginListParams,
response: v2::PluginListResponse,
@@ -3481,6 +3481,21 @@ pub struct MarketplaceAddResponse {
pub already_added: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct MarketplaceRemoveParams {
pub marketplace_name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct MarketplaceRemoveResponse {
pub marketplace_name: String,
pub installed_root: Option<AbsolutePathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -8890,6 +8905,40 @@ mod tests {
);
}
#[test]
fn marketplace_remove_response_serializes_nullable_installed_root() {
let installed_root = if cfg!(windows) {
r"C:\marketplaces\debug"
} else {
"/tmp/marketplaces/debug"
};
let installed_root = AbsolutePathBuf::try_from(PathBuf::from(installed_root)).unwrap();
let installed_root_json = installed_root.as_path().display().to_string();
assert_eq!(
serde_json::to_value(MarketplaceRemoveResponse {
marketplace_name: "debug".to_string(),
installed_root: Some(installed_root),
})
.unwrap(),
json!({
"marketplaceName": "debug",
"installedRoot": installed_root_json,
}),
);
assert_eq!(
serde_json::to_value(MarketplaceRemoveResponse {
marketplace_name: "debug".to_string(),
installed_root: None,
})
.unwrap(),
json!({
"marketplaceName": "debug",
"installedRoot": null,
}),
);
}
#[test]
fn codex_error_info_serializes_http_status_code_in_camel_case() {
let value = CodexErrorInfo::ResponseTooManyFailedAttempts {
+1
View File
@@ -185,6 +185,7 @@ Example with notification opt-out:
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
- `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present.
- `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists.
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**).
- `skills/changed` — notification emitted when watched local skill files change.
@@ -85,6 +85,8 @@ use codex_app_server_protocol::LogoutAccountResponse;
use codex_app_server_protocol::MarketplaceAddParams;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::MarketplaceInterface;
use codex_app_server_protocol::MarketplaceRemoveParams;
use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::McpResourceReadParams;
use codex_app_server_protocol::McpResourceReadResponse;
use codex_app_server_protocol::McpServerOauthLoginCompletedNotification;
@@ -243,12 +245,15 @@ use codex_core::find_thread_names_by_ids;
use codex_core::find_thread_path_by_id_str;
use codex_core::path_utils;
use codex_core::plugins::MarketplaceAddError;
use codex_core::plugins::MarketplaceRemoveError;
use codex_core::plugins::MarketplaceRemoveRequest as CoreMarketplaceRemoveRequest;
use codex_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME;
use codex_core::plugins::PluginInstallError as CorePluginInstallError;
use codex_core::plugins::PluginInstallRequest;
use codex_core::plugins::PluginReadRequest;
use codex_core::plugins::PluginUninstallError as CorePluginUninstallError;
use codex_core::plugins::add_marketplace as add_marketplace_to_codex_home;
use codex_core::plugins::remove_marketplace;
use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
use codex_core::sandboxing::SandboxPermissions;
@@ -980,6 +985,10 @@ impl CodexMessageProcessor {
self.marketplace_add(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::MarketplaceRemove { request_id, params } => {
self.marketplace_remove(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::PluginList { request_id, params } => {
self.plugin_list(to_connection_request_id(request_id), params)
.await;
@@ -6537,6 +6546,40 @@ impl CodexMessageProcessor {
.await;
}
async fn marketplace_remove(
&self,
request_id: ConnectionRequestId,
params: MarketplaceRemoveParams,
) {
let result = remove_marketplace(
self.config.codex_home.to_path_buf(),
CoreMarketplaceRemoveRequest {
marketplace_name: params.marketplace_name,
},
)
.await;
match result {
Ok(outcome) => {
self.outgoing
.send_response(
request_id,
MarketplaceRemoveResponse {
marketplace_name: outcome.marketplace_name,
installed_root: outcome.removed_installed_root,
},
)
.await;
}
Err(MarketplaceRemoveError::InvalidRequest(message)) => {
self.send_invalid_request_error(request_id, message).await;
}
Err(MarketplaceRemoveError::Internal(message)) => {
self.send_internal_error(request_id, message).await;
}
}
}
async fn plugin_list(&self, request_id: ConnectionRequestId, params: PluginListParams) {
let plugins_manager = self.thread_manager.plugins_manager();
let PluginListParams { cwds } = params;
@@ -48,6 +48,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ListMcpServerStatusParams;
use codex_app_server_protocol::LoginAccountParams;
use codex_app_server_protocol::MarketplaceAddParams;
use codex_app_server_protocol::MarketplaceRemoveParams;
use codex_app_server_protocol::McpResourceReadParams;
use codex_app_server_protocol::McpServerToolCallParams;
use codex_app_server_protocol::MockExperimentalMethodParams;
@@ -555,6 +556,15 @@ impl McpProcess {
self.send_request("marketplace/add", params).await
}
/// Send a `marketplace/remove` JSON-RPC request.
pub async fn send_marketplace_remove_request(
&mut self,
params: MarketplaceRemoveParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("marketplace/remove", params).await
}
/// Send a `plugin/install` JSON-RPC request.
pub async fn send_plugin_install_request(
&mut self,
@@ -0,0 +1,105 @@
use std::time::Duration;
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::MarketplaceRemoveParams;
use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::RequestId;
use codex_config::MarketplaceConfigUpdate;
use codex_config::record_user_marketplace;
use codex_core::plugins::marketplace_install_root;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
fn configured_marketplace_update() -> MarketplaceConfigUpdate<'static> {
MarketplaceConfigUpdate {
last_updated: "2026-04-13T00:00:00Z",
last_revision: None,
source_type: "git",
source: "https://github.com/owner/repo.git",
ref_name: Some("main"),
sparse_paths: &[],
}
}
fn write_installed_marketplace(codex_home: &std::path::Path, marketplace_name: &str) -> Result<()> {
let root = marketplace_install_root(codex_home).join(marketplace_name);
std::fs::create_dir_all(root.join(".agents/plugins"))?;
std::fs::write(root.join(".agents/plugins/marketplace.json"), "{}")?;
Ok(())
}
#[tokio::test]
async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
let codex_home = TempDir::new()?;
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
write_installed_marketplace(codex_home.path(), "debug")?;
let installed_root = marketplace_install_root(codex_home.path())
.join("debug")
.canonicalize()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_marketplace_remove_request(MarketplaceRemoveParams {
marketplace_name: "debug".to_string(),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: MarketplaceRemoveResponse = to_response(response)?;
assert_eq!(
response,
MarketplaceRemoveResponse {
marketplace_name: "debug".to_string(),
installed_root: Some(AbsolutePathBuf::try_from(installed_root)?),
}
);
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
assert!(!config.contains("[marketplaces.debug]"));
assert!(
!marketplace_install_root(codex_home.path())
.join("debug")
.exists()
);
Ok(())
}
#[tokio::test]
async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_marketplace_remove_request(MarketplaceRemoveParams {
marketplace_name: "debug".to_string(),
})
.await?;
let err = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(err.error.code, -32600);
assert_eq!(
err.error.message,
"marketplace `debug` is not configured or installed",
);
Ok(())
}
@@ -17,6 +17,7 @@ mod external_agent_config;
mod fs;
mod initialize;
mod marketplace_add;
mod marketplace_remove;
mod mcp_resource;
mod mcp_server_elicitation;
mod mcp_server_status;