From 1dc3535e17666884800ada37d7eb94cf974d38fe Mon Sep 17 00:00:00 2001 From: xli-oai Date: Sun, 19 Apr 2026 23:22:49 -0700 Subject: [PATCH] [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 --- .../schema/json/ClientRequest.json | 35 ++++++ .../codex_app_server_protocol.schemas.json | 60 ++++++++++ .../codex_app_server_protocol.v2.schemas.json | 60 ++++++++++ .../json/v2/MarketplaceRemoveParams.json | 13 +++ .../json/v2/MarketplaceRemoveResponse.json | 29 +++++ .../schema/typescript/ClientRequest.ts | 3 +- .../typescript/v2/MarketplaceRemoveParams.ts | 5 + .../v2/MarketplaceRemoveResponse.ts | 6 + .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 4 + .../app-server-protocol/src/protocol/v2.rs | 49 ++++++++ codex-rs/app-server/README.md | 1 + .../app-server/src/codex_message_processor.rs | 43 +++++++ .../app-server/tests/common/mcp_process.rs | 10 ++ .../tests/suite/v2/marketplace_remove.rs | 105 ++++++++++++++++++ codex-rs/app-server/tests/suite/v2/mod.rs | 1 + 16 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/MarketplaceRemoveParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/MarketplaceRemoveResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceRemoveParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceRemoveResponse.ts create mode 100644 codex-rs/app-server/tests/suite/v2/marketplace_remove.rs diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 5aae5fd1d..f9cdca83e 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -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": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index bf8dbb166..f6200ccc0 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -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", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 59b830d4f..7dc8afb75 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -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", diff --git a/codex-rs/app-server-protocol/schema/json/v2/MarketplaceRemoveParams.json b/codex-rs/app-server-protocol/schema/json/v2/MarketplaceRemoveParams.json new file mode 100644 index 000000000..2c145686c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/MarketplaceRemoveParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplaceName": { + "type": "string" + } + }, + "required": [ + "marketplaceName" + ], + "title": "MarketplaceRemoveParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/MarketplaceRemoveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/MarketplaceRemoveResponse.json new file mode 100644 index 000000000..ae4945078 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/MarketplaceRemoveResponse.json @@ -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" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index b9ef6401b..c26ff9161 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -34,6 +34,7 @@ import type { GetAccountParams } from "./v2/GetAccountParams"; import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; import type { LoginAccountParams } from "./v2/LoginAccountParams"; import type { MarketplaceAddParams } from "./v2/MarketplaceAddParams"; +import type { MarketplaceRemoveParams } from "./v2/MarketplaceRemoveParams"; import type { McpResourceReadParams } from "./v2/McpResourceReadParams"; import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams"; @@ -70,4 +71,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceRemoveParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceRemoveParams.ts new file mode 100644 index 000000000..086dd52a4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceRemoveParams.ts @@ -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, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceRemoveResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceRemoveResponse.ts new file mode 100644 index 000000000..68a04ecdf --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceRemoveResponse.ts @@ -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, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index cbdef1210..53952627f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -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"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 4e5882f83..72dc1552f 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -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, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 508b90b34..72ac0db65 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -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, +} + #[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 { diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index e34e44d8c..b2d30d865 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -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. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 4cd97e99a..0342cc84c 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -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; diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 05bb66d57..2279939ec 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -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 { + 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, diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_remove.rs b/codex-rs/app-server/tests/suite/v2/marketplace_remove.rs new file mode 100644 index 000000000..93cacef35 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/marketplace_remove.rs @@ -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(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 38097eabf..ea4f7cb5d 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -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;