diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 87c28d0b7..47db32075 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1143,6 +1143,21 @@ }, "type": "object" }, + "PluginReadParams": { + "properties": { + "marketplacePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "pluginName": { + "type": "string" + } + }, + "required": [ + "marketplacePath", + "pluginName" + ], + "type": "object" + }, "PluginUninstallParams": { "properties": { "pluginId": { @@ -3559,6 +3574,30 @@ "title": "Plugin/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/read" + ], + "title": "Plugin/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/readRequest", + "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 3eb6da033..353507a47 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 @@ -643,6 +643,30 @@ "title": "Plugin/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "plugin/read" + ], + "title": "Plugin/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/PluginReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/readRequest", + "type": "object" + }, { "properties": { "id": { @@ -5027,7 +5051,7 @@ "type": "object" }, "AppSummary": { - "description": "EXPERIMENTAL - app metadata summary for plugin-install responses.", + "description": "EXPERIMENTAL - app metadata summary for plugin responses.", "properties": { "description": { "type": [ @@ -8516,6 +8540,52 @@ ], "type": "string" }, + "PluginDetail": { + "properties": { + "apps": { + "items": { + "$ref": "#/definitions/v2/AppSummary" + }, + "type": "array" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "marketplaceName": { + "type": "string" + }, + "marketplacePath": { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + "mcpServers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "skills": { + "items": { + "$ref": "#/definitions/v2/SkillSummary" + }, + "type": "array" + }, + "summary": { + "$ref": "#/definitions/v2/PluginSummary" + } + }, + "required": [ + "apps", + "marketplaceName", + "marketplacePath", + "mcpServers", + "skills", + "summary" + ], + "type": "object" + }, "PluginInstallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -8727,6 +8797,36 @@ ], "type": "object" }, + "PluginReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplacePath": { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + "pluginName": { + "type": "string" + } + }, + "required": [ + "marketplacePath", + "pluginName" + ], + "title": "PluginReadParams", + "type": "object" + }, + "PluginReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "plugin": { + "$ref": "#/definitions/v2/PluginDetail" + } + }, + "required": [ + "plugin" + ], + "title": "PluginReadResponse", + "type": "object" + }, "PluginSource": { "oneOf": [ { @@ -10471,6 +10571,41 @@ ], "type": "string" }, + "SkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "interface": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SkillInterface" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "shortDescription": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "description", + "name", + "path" + ], + "type": "object" + }, "SkillToolDependency": { "properties": { "command": { 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 3512fd5c7..99fe87f0a 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 @@ -473,7 +473,7 @@ "type": "object" }, "AppSummary": { - "description": "EXPERIMENTAL - app metadata summary for plugin-install responses.", + "description": "EXPERIMENTAL - app metadata summary for plugin responses.", "properties": { "description": { "type": [ @@ -1162,6 +1162,30 @@ "title": "Plugin/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/read" + ], + "title": "Plugin/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/readRequest", + "type": "object" + }, { "properties": { "id": { @@ -5311,6 +5335,52 @@ ], "type": "string" }, + "PluginDetail": { + "properties": { + "apps": { + "items": { + "$ref": "#/definitions/AppSummary" + }, + "type": "array" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "marketplaceName": { + "type": "string" + }, + "marketplacePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "mcpServers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "skills": { + "items": { + "$ref": "#/definitions/SkillSummary" + }, + "type": "array" + }, + "summary": { + "$ref": "#/definitions/PluginSummary" + } + }, + "required": [ + "apps", + "marketplaceName", + "marketplacePath", + "mcpServers", + "skills", + "summary" + ], + "type": "object" + }, "PluginInstallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -5522,6 +5592,36 @@ ], "type": "object" }, + "PluginReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplacePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "pluginName": { + "type": "string" + } + }, + "required": [ + "marketplacePath", + "pluginName" + ], + "title": "PluginReadParams", + "type": "object" + }, + "PluginReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "plugin": { + "$ref": "#/definitions/PluginDetail" + } + }, + "required": [ + "plugin" + ], + "title": "PluginReadResponse", + "type": "object" + }, "PluginSource": { "oneOf": [ { @@ -8198,6 +8298,41 @@ ], "type": "string" }, + "SkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "interface": { + "anyOf": [ + { + "$ref": "#/definitions/SkillInterface" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "shortDescription": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "description", + "name", + "path" + ], + "type": "object" + }, "SkillToolDependency": { "properties": { "command": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginInstallResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginInstallResponse.json index a95f47cd3..b02af0bf5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginInstallResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginInstallResponse.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "AppSummary": { - "description": "EXPERIMENTAL - app metadata summary for plugin-install responses.", + "description": "EXPERIMENTAL - app metadata summary for plugin responses.", "properties": { "description": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadParams.json new file mode 100644 index 000000000..a720ae3b5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadParams.json @@ -0,0 +1,23 @@ +{ + "$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": { + "marketplacePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "pluginName": { + "type": "string" + } + }, + "required": [ + "marketplacePath", + "pluginName" + ], + "title": "PluginReadParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json new file mode 100644 index 000000000..f14607283 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -0,0 +1,354 @@ +{ + "$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" + }, + "AppSummary": { + "description": "EXPERIMENTAL - app metadata summary for plugin responses.", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "installUrl": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "PluginAuthPolicy": { + "enum": [ + "ON_INSTALL", + "ON_USE" + ], + "type": "string" + }, + "PluginDetail": { + "properties": { + "apps": { + "items": { + "$ref": "#/definitions/AppSummary" + }, + "type": "array" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "marketplaceName": { + "type": "string" + }, + "marketplacePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "mcpServers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "skills": { + "items": { + "$ref": "#/definitions/SkillSummary" + }, + "type": "array" + }, + "summary": { + "$ref": "#/definitions/PluginSummary" + } + }, + "required": [ + "apps", + "marketplaceName", + "marketplacePath", + "mcpServers", + "skills", + "summary" + ], + "type": "object" + }, + "PluginInstallPolicy": { + "enum": [ + "NOT_AVAILABLE", + "AVAILABLE", + "INSTALLED_BY_DEFAULT" + ], + "type": "string" + }, + "PluginInterface": { + "properties": { + "brandColor": { + "type": [ + "string", + "null" + ] + }, + "capabilities": { + "items": { + "type": "string" + }, + "type": "array" + }, + "category": { + "type": [ + "string", + "null" + ] + }, + "composerIcon": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "defaultPrompt": { + "type": [ + "string", + "null" + ] + }, + "developerName": { + "type": [ + "string", + "null" + ] + }, + "displayName": { + "type": [ + "string", + "null" + ] + }, + "logo": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "longDescription": { + "type": [ + "string", + "null" + ] + }, + "privacyPolicyUrl": { + "type": [ + "string", + "null" + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "shortDescription": { + "type": [ + "string", + "null" + ] + }, + "termsOfServiceUrl": { + "type": [ + "string", + "null" + ] + }, + "websiteUrl": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "capabilities", + "screenshots" + ], + "type": "object" + }, + "PluginSource": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "local" + ], + "title": "LocalPluginSourceType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "LocalPluginSource", + "type": "object" + } + ] + }, + "PluginSummary": { + "properties": { + "authPolicy": { + "$ref": "#/definitions/PluginAuthPolicy" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "installPolicy": { + "$ref": "#/definitions/PluginInstallPolicy" + }, + "installed": { + "type": "boolean" + }, + "interface": { + "anyOf": [ + { + "$ref": "#/definitions/PluginInterface" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "source": { + "$ref": "#/definitions/PluginSource" + } + }, + "required": [ + "authPolicy", + "enabled", + "id", + "installPolicy", + "installed", + "name", + "source" + ], + "type": "object" + }, + "SkillInterface": { + "properties": { + "brandColor": { + "type": [ + "string", + "null" + ] + }, + "defaultPrompt": { + "type": [ + "string", + "null" + ] + }, + "displayName": { + "type": [ + "string", + "null" + ] + }, + "iconLarge": { + "type": [ + "string", + "null" + ] + }, + "iconSmall": { + "type": [ + "string", + "null" + ] + }, + "shortDescription": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "SkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "interface": { + "anyOf": [ + { + "$ref": "#/definitions/SkillInterface" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "shortDescription": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "description", + "name", + "path" + ], + "type": "object" + } + }, + "properties": { + "plugin": { + "$ref": "#/definitions/PluginDetail" + } + }, + "required": [ + "plugin" + ], + "title": "PluginReadResponse", + "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 5fa8f27b0..51bf05961 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -27,6 +27,7 @@ import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; import type { ModelListParams } from "./v2/ModelListParams"; import type { PluginInstallParams } from "./v2/PluginInstallParams"; import type { PluginListParams } from "./v2/PluginListParams"; +import type { PluginReadParams } from "./v2/PluginReadParams"; import type { PluginUninstallParams } from "./v2/PluginUninstallParams"; import type { ReviewStartParams } from "./v2/ReviewStartParams"; import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; @@ -54,4 +55,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/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": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "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": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "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": "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/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": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "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": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "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": "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/AppSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppSummary.ts index d5777b185..3cdb17d70 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppSummary.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppSummary.ts @@ -3,6 +3,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. /** - * EXPERIMENTAL - app metadata summary for plugin-install responses. + * EXPERIMENTAL - app metadata summary for plugin responses. */ export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginDetail.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginDetail.ts new file mode 100644 index 000000000..4bfd35fe7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginDetail.ts @@ -0,0 +1,9 @@ +// 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"; +import type { AppSummary } from "./AppSummary"; +import type { PluginSummary } from "./PluginSummary"; +import type { SkillSummary } from "./SkillSummary"; + +export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf, summary: PluginSummary, description: string | null, skills: Array, apps: Array, mcpServers: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadParams.ts new file mode 100644 index 000000000..cd6696873 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadParams.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 PluginReadParams = { marketplacePath: AbsolutePathBuf, pluginName: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadResponse.ts new file mode 100644 index 000000000..841b916eb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadResponse.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 { PluginDetail } from "./PluginDetail"; + +export type PluginReadResponse = { plugin: PluginDetail, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillSummary.ts new file mode 100644 index 000000000..818e0b05d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillSummary.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 { SkillInterface } from "./SkillInterface"; + +export type SkillSummary = { name: string, description: string, shortDescription: string | null, interface: SkillInterface | null, path: string, }; 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 b57daaac3..68a8369fe 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -176,6 +176,7 @@ export type { PermissionsRequestApprovalParams } from "./PermissionsRequestAppro export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse"; export type { PlanDeltaNotification } from "./PlanDeltaNotification"; export type { PluginAuthPolicy } from "./PluginAuthPolicy"; +export type { PluginDetail } from "./PluginDetail"; export type { PluginInstallParams } from "./PluginInstallParams"; export type { PluginInstallPolicy } from "./PluginInstallPolicy"; export type { PluginInstallResponse } from "./PluginInstallResponse"; @@ -183,6 +184,8 @@ export type { PluginInterface } from "./PluginInterface"; export type { PluginListParams } from "./PluginListParams"; export type { PluginListResponse } from "./PluginListResponse"; export type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry"; +export type { PluginReadParams } from "./PluginReadParams"; +export type { PluginReadResponse } from "./PluginReadResponse"; export type { PluginSource } from "./PluginSource"; export type { PluginSummary } from "./PluginSummary"; export type { PluginUninstallParams } from "./PluginUninstallParams"; @@ -213,6 +216,7 @@ export type { SkillErrorInfo } from "./SkillErrorInfo"; export type { SkillInterface } from "./SkillInterface"; export type { SkillMetadata } from "./SkillMetadata"; export type { SkillScope } from "./SkillScope"; +export type { SkillSummary } from "./SkillSummary"; export type { SkillToolDependency } from "./SkillToolDependency"; export type { SkillsChangedNotification } from "./SkillsChangedNotification"; export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index c7fa75c10..59757679c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -296,6 +296,10 @@ client_request_definitions! { params: v2::PluginListParams, response: v2::PluginListResponse, }, + PluginRead => "plugin/read" { + params: v2::PluginReadParams, + response: v2::PluginReadResponse, + }, SkillsRemoteList => "skills/remote/list" { params: v2::SkillsRemoteReadParams, response: v2::SkillsRemoteReadResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 49c2d321e..fcbce5197 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1979,7 +1979,7 @@ pub struct AppInfo { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -/// EXPERIMENTAL - app metadata summary for plugin-install responses. +/// EXPERIMENTAL - app metadata summary for plugin responses. pub struct AppSummary { pub id: String, pub name: String, @@ -2881,6 +2881,21 @@ pub struct PluginListResponse { pub remote_sync_error: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginReadParams { + pub marketplace_path: AbsolutePathBuf, + pub plugin_name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginReadResponse { + pub plugin: PluginDetail, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3092,6 +3107,30 @@ pub struct PluginSummary { pub interface: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginDetail { + pub marketplace_name: String, + pub marketplace_path: AbsolutePathBuf, + pub summary: PluginSummary, + pub description: Option, + pub skills: Vec, + pub apps: Vec, + pub mcp_servers: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillSummary { + pub name: String, + pub description: String, + pub short_description: Option, + pub interface: Option, + pub path: PathBuf, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 881dff52a..4a6cd58cc 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -158,6 +158,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`). - `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**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 (**under development; do not call from production clients yet**). - `skills/changed` — notification emitted when watched local skill files change. - `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**). - `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**). diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 73d979b5d..7a73304f5 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -24,8 +24,6 @@ use codex_app_server_protocol::Account; use codex_app_server_protocol::AccountLoginCompletedNotification; use codex_app_server_protocol::AccountUpdatedNotification; use codex_app_server_protocol::AppInfo; -use codex_app_server_protocol::AppListUpdatedNotification; -use codex_app_server_protocol::AppSummary; use codex_app_server_protocol::AppsListParams; use codex_app_server_protocol::AppsListResponse; use codex_app_server_protocol::AskForApproval; @@ -83,12 +81,15 @@ use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::MockExperimentalMethodResponse; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; +use codex_app_server_protocol::PluginDetail; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallResponse; use codex_app_server_protocol::PluginInterface; use codex_app_server_protocol::PluginListParams; use codex_app_server_protocol::PluginListResponse; use codex_app_server_protocol::PluginMarketplaceEntry; +use codex_app_server_protocol::PluginReadParams; +use codex_app_server_protocol::PluginReadResponse; use codex_app_server_protocol::PluginSource; use codex_app_server_protocol::PluginSummary; use codex_app_server_protocol::PluginUninstallParams; @@ -102,6 +103,7 @@ use codex_app_server_protocol::ReviewTarget as ApiReviewTarget; use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestResolvedNotification; +use codex_app_server_protocol::SkillSummary; use codex_app_server_protocol::SkillsConfigWriteParams; use codex_app_server_protocol::SkillsConfigWriteResponse; use codex_app_server_protocol::SkillsListParams; @@ -200,8 +202,6 @@ use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config::types::McpServerTransportConfig; use codex_core::config_loader::CloudRequirementsLoader; -use codex_core::connectors::filter_disallowed_connectors; -use codex_core::connectors::merge_plugin_apps; use codex_core::default_client::set_default_client_residency_requirement; use codex_core::error::CodexErr; use codex_core::error::Result as CodexResult; @@ -222,11 +222,11 @@ use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; -use codex_core::plugins::AppConnectorId; use codex_core::plugins::MarketplaceError; use codex_core::plugins::MarketplacePluginSourceSummary; 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::load_plugin_apps; use codex_core::read_head_for_summary; @@ -312,6 +312,9 @@ use uuid::Uuid; #[cfg(test)] use codex_app_server_protocol::ServerRequest; +mod apps_list_helpers; +mod plugin_app_helpers; + use crate::filters::compute_source_filters; use crate::filters::source_kind_matches; use crate::thread_state::ThreadListenerCommand; @@ -720,6 +723,10 @@ impl CodexMessageProcessor { self.plugin_list(to_connection_request_id(request_id), params) .await; } + ClientRequest::PluginRead { request_id, params } => { + self.plugin_read(to_connection_request_id(request_id), params) + .await; + } ClientRequest::SkillsRemoteList { request_id, params } => { self.skills_remote_list(to_connection_request_id(request_id), params) .await; @@ -5134,18 +5141,19 @@ impl CodexMessageProcessor { if accessible_connectors.is_some() || all_connectors.is_some() { let merged = connectors::with_app_enabled_state( - Self::merge_loaded_apps( + apps_list_helpers::merge_loaded_apps( all_connectors.as_deref(), accessible_connectors.as_deref(), ), &config, ); - if Self::should_send_app_list_updated_notification( + if apps_list_helpers::should_send_app_list_updated_notification( merged.as_slice(), accessible_loaded, all_loaded, ) { - Self::send_app_list_updated_notification(&outgoing, merged.clone()).await; + apps_list_helpers::send_app_list_updated_notification(&outgoing, merged.clone()) + .await; last_notified_apps = Some(merged); } } @@ -5219,24 +5227,25 @@ impl CodexMessageProcessor { accessible_connectors.as_deref() }; let merged = connectors::with_app_enabled_state( - Self::merge_loaded_apps( + apps_list_helpers::merge_loaded_apps( all_connectors_for_update, accessible_connectors_for_update, ), &config, ); - if Self::should_send_app_list_updated_notification( + if apps_list_helpers::should_send_app_list_updated_notification( merged.as_slice(), accessible_loaded, all_loaded, ) && last_notified_apps.as_ref() != Some(&merged) { - Self::send_app_list_updated_notification(&outgoing, merged.clone()).await; + apps_list_helpers::send_app_list_updated_notification(&outgoing, merged.clone()) + .await; last_notified_apps = Some(merged.clone()); } if accessible_loaded && all_loaded { - match Self::paginate_apps(merged.as_slice(), start, limit) { + match apps_list_helpers::paginate_apps(merged.as_slice(), start, limit) { Ok(response) => { outgoing.send_response(request_id, response).await; return; @@ -5250,92 +5259,6 @@ impl CodexMessageProcessor { } } - fn merge_loaded_apps( - all_connectors: Option<&[AppInfo]>, - accessible_connectors: Option<&[AppInfo]>, - ) -> Vec { - let all_connectors_loaded = all_connectors.is_some(); - let all = all_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec); - let accessible = accessible_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec); - connectors::merge_connectors_with_accessible(all, accessible, all_connectors_loaded) - } - - fn plugin_apps_needing_auth( - all_connectors: &[AppInfo], - accessible_connectors: &[AppInfo], - plugin_apps: &[AppConnectorId], - codex_apps_ready: bool, - ) -> Vec { - if !codex_apps_ready { - return Vec::new(); - } - - let accessible_ids = accessible_connectors - .iter() - .map(|connector| connector.id.as_str()) - .collect::>(); - let plugin_app_ids = plugin_apps - .iter() - .map(|connector_id| connector_id.0.as_str()) - .collect::>(); - - all_connectors - .iter() - .filter(|connector| { - plugin_app_ids.contains(connector.id.as_str()) - && !accessible_ids.contains(connector.id.as_str()) - }) - .cloned() - .map(AppSummary::from) - .collect() - } - - fn should_send_app_list_updated_notification( - connectors: &[AppInfo], - accessible_loaded: bool, - all_loaded: bool, - ) -> bool { - connectors.iter().any(|connector| connector.is_accessible) - || (accessible_loaded && all_loaded) - } - - fn paginate_apps( - connectors: &[AppInfo], - start: usize, - limit: Option, - ) -> Result { - let total = connectors.len(); - if start > total { - return Err(JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("cursor {start} exceeds total apps {total}"), - data: None, - }); - } - - let effective_limit = limit.unwrap_or(total as u32).max(1) as usize; - let end = start.saturating_add(effective_limit).min(total); - let data = connectors[start..end].to_vec(); - let next_cursor = if end < total { - Some(end.to_string()) - } else { - None - }; - - Ok(AppsListResponse { data, next_cursor }) - } - - async fn send_app_list_updated_notification( - outgoing: &Arc, - data: Vec, - ) { - outgoing - .send_server_notification(ServerNotification::AppListUpdated( - AppListUpdatedNotification { data }, - )) - .await; - } - async fn skills_list(&self, request_id: ConnectionRequestId, params: SkillsListParams) { let SkillsListParams { cwds, @@ -5468,29 +5391,10 @@ impl CodexMessageProcessor { installed: plugin.installed, enabled: plugin.enabled, name: plugin.name, - source: match plugin.source { - MarketplacePluginSourceSummary::Local { path } => { - PluginSource::Local { path } - } - }, + source: marketplace_plugin_source_to_info(plugin.source), install_policy: plugin.install_policy.into(), auth_policy: plugin.auth_policy.into(), - interface: plugin.interface.map(|interface| PluginInterface { - display_name: interface.display_name, - short_description: interface.short_description, - long_description: interface.long_description, - developer_name: interface.developer_name, - category: interface.category, - capabilities: interface.capabilities, - website_url: interface.website_url, - privacy_policy_url: interface.privacy_policy_url, - terms_of_service_url: interface.terms_of_service_url, - default_prompt: interface.default_prompt, - brand_color: interface.brand_color, - composer_icon: interface.composer_icon, - logo: interface.logo, - screenshots: interface.screenshots, - }), + interface: plugin.interface.map(plugin_interface_to_info), }) .collect(), }) @@ -5526,6 +5430,73 @@ impl CodexMessageProcessor { .await; } + async fn plugin_read(&self, request_id: ConnectionRequestId, params: PluginReadParams) { + let plugins_manager = self.thread_manager.plugins_manager(); + let PluginReadParams { + marketplace_path, + plugin_name, + } = params; + let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf); + + let config = match self.load_latest_config(config_cwd).await { + Ok(config) => config, + Err(err) => { + self.outgoing.send_error(request_id, err).await; + return; + } + }; + + let request = PluginReadRequest { + plugin_name, + marketplace_path, + }; + let config_for_read = config.clone(); + let outcome = match tokio::task::spawn_blocking(move || { + plugins_manager.read_plugin_for_config(&config_for_read, &request) + }) + .await + { + Ok(Ok(outcome)) => outcome, + Ok(Err(err)) => { + self.send_marketplace_error(request_id, err, "read plugin details") + .await; + return; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to read plugin details: {err}"), + ) + .await; + return; + } + }; + let app_summaries = + plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await; + let plugin = PluginDetail { + marketplace_name: outcome.marketplace_name, + marketplace_path: outcome.marketplace_path, + summary: PluginSummary { + id: outcome.plugin.id, + name: outcome.plugin.name, + source: marketplace_plugin_source_to_info(outcome.plugin.source), + installed: outcome.plugin.installed, + enabled: outcome.plugin.enabled, + install_policy: outcome.plugin.install_policy.into(), + auth_policy: outcome.plugin.auth_policy.into(), + interface: outcome.plugin.interface.map(plugin_interface_to_info), + }, + description: outcome.plugin.description, + skills: plugin_skills_to_info(&outcome.plugin.skills), + apps: app_summaries, + mcp_servers: outcome.plugin.mcp_server_names, + }; + + self.outgoing + .send_response(request_id, PluginReadResponse { plugin }) + .await; + } + async fn skills_remote_list( &self, request_id: ConnectionRequestId, @@ -5672,23 +5643,19 @@ impl CodexMessageProcessor { ); let all_connectors = match all_connectors_result { - Ok(connectors) => filter_disallowed_connectors(merge_plugin_apps( - connectors, - plugin_apps.clone(), - )), + Ok(connectors) => connectors, Err(err) => { warn!( plugin = result.plugin_id.as_key(), "failed to load app metadata after plugin install: {err:#}" ); - filter_disallowed_connectors(merge_plugin_apps( - connectors::list_cached_all_connectors(&config) - .await - .unwrap_or_default(), - plugin_apps.clone(), - )) + connectors::list_cached_all_connectors(&config) + .await + .unwrap_or_default() } }; + let all_connectors = + connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps); let (accessible_connectors, codex_apps_ready) = match accessible_connectors_result { Ok(status) => (status.connectors, status.codex_apps_ready), @@ -5714,7 +5681,7 @@ impl CodexMessageProcessor { ); } - Self::plugin_apps_needing_auth( + plugin_app_helpers::plugin_apps_needing_auth( &all_connectors, &accessible_connectors, &plugin_apps, @@ -7436,6 +7403,55 @@ fn skills_to_info( .collect() } +fn plugin_skills_to_info(skills: &[codex_core::skills::SkillMetadata]) -> Vec { + skills + .iter() + .map(|skill| SkillSummary { + name: skill.name.clone(), + description: skill.description.clone(), + short_description: skill.short_description.clone(), + interface: skill.interface.clone().map(|interface| { + codex_app_server_protocol::SkillInterface { + display_name: interface.display_name, + short_description: interface.short_description, + icon_small: interface.icon_small, + icon_large: interface.icon_large, + brand_color: interface.brand_color, + default_prompt: interface.default_prompt, + } + }), + path: skill.path_to_skills_md.clone(), + }) + .collect() +} + +fn plugin_interface_to_info( + interface: codex_core::plugins::PluginManifestInterfaceSummary, +) -> PluginInterface { + PluginInterface { + display_name: interface.display_name, + short_description: interface.short_description, + long_description: interface.long_description, + developer_name: interface.developer_name, + category: interface.category, + capabilities: interface.capabilities, + website_url: interface.website_url, + privacy_policy_url: interface.privacy_policy_url, + terms_of_service_url: interface.terms_of_service_url, + default_prompt: interface.default_prompt, + brand_color: interface.brand_color, + composer_icon: interface.composer_icon, + logo: interface.logo, + screenshots: interface.screenshots, + } +} + +fn marketplace_plugin_source_to_info(source: MarketplacePluginSourceSummary) -> PluginSource { + match source { + MarketplacePluginSourceSummary::Local { path } => PluginSource::Local { path }, + } +} + fn errors_to_info( errors: &[codex_core::skills::SkillError], ) -> Vec { @@ -8083,35 +8099,6 @@ mod tests { validate_dynamic_tools(&tools).expect("valid schema"); } - #[test] - fn plugin_apps_needing_auth_returns_empty_when_codex_apps_is_not_ready() { - let all_connectors = vec![AppInfo { - id: "alpha".to_string(), - name: "Alpha".to_string(), - description: Some("Alpha connector".to_string()), - logo_url: None, - logo_url_dark: None, - distribution_channel: None, - branding: None, - app_metadata: None, - labels: None, - install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), - is_accessible: false, - is_enabled: true, - plugin_display_names: Vec::new(), - }]; - - assert_eq!( - CodexMessageProcessor::plugin_apps_needing_auth( - &all_connectors, - &[], - &[AppConnectorId("alpha".to_string())], - false, - ), - Vec::::new() - ); - } - #[test] fn collect_resume_override_mismatches_includes_service_tier() { let request = ThreadResumeParams { diff --git a/codex-rs/app-server/src/codex_message_processor/apps_list_helpers.rs b/codex-rs/app-server/src/codex_message_processor/apps_list_helpers.rs new file mode 100644 index 000000000..b0a6df4a8 --- /dev/null +++ b/codex-rs/app-server/src/codex_message_processor/apps_list_helpers.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use codex_app_server_protocol::AppInfo; +use codex_app_server_protocol::AppListUpdatedNotification; +use codex_app_server_protocol::AppsListResponse; +use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::ServerNotification; +use codex_chatgpt::connectors; + +use crate::error_code::INVALID_REQUEST_ERROR_CODE; +use crate::outgoing_message::OutgoingMessageSender; + +pub(super) fn merge_loaded_apps( + all_connectors: Option<&[AppInfo]>, + accessible_connectors: Option<&[AppInfo]>, +) -> Vec { + let all_connectors_loaded = all_connectors.is_some(); + let all = all_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec); + let accessible = accessible_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec); + connectors::merge_connectors_with_accessible(all, accessible, all_connectors_loaded) +} + +pub(super) fn should_send_app_list_updated_notification( + connectors: &[AppInfo], + accessible_loaded: bool, + all_loaded: bool, +) -> bool { + connectors.iter().any(|connector| connector.is_accessible) || (accessible_loaded && all_loaded) +} + +pub(super) fn paginate_apps( + connectors: &[AppInfo], + start: usize, + limit: Option, +) -> Result { + let total = connectors.len(); + if start > total { + return Err(JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("cursor {start} exceeds total apps {total}"), + data: None, + }); + } + + let effective_limit = limit.unwrap_or(total as u32).max(1) as usize; + let end = start.saturating_add(effective_limit).min(total); + let data = connectors[start..end].to_vec(); + let next_cursor = if end < total { + Some(end.to_string()) + } else { + None + }; + + Ok(AppsListResponse { data, next_cursor }) +} + +pub(super) async fn send_app_list_updated_notification( + outgoing: &Arc, + data: Vec, +) { + outgoing + .send_server_notification(ServerNotification::AppListUpdated( + AppListUpdatedNotification { data }, + )) + .await; +} diff --git a/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs b/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs new file mode 100644 index 000000000..f9508c6aa --- /dev/null +++ b/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs @@ -0,0 +1,100 @@ +use std::collections::HashSet; + +use codex_app_server_protocol::AppInfo; +use codex_app_server_protocol::AppSummary; +use codex_chatgpt::connectors; +use codex_core::config::Config; +use codex_core::plugins::AppConnectorId; +use tracing::warn; + +pub(super) async fn load_plugin_app_summaries( + config: &Config, + plugin_apps: &[AppConnectorId], +) -> Vec { + if plugin_apps.is_empty() { + return Vec::new(); + } + + let connectors = match connectors::list_all_connectors_with_options(config, false).await { + Ok(connectors) => connectors, + Err(err) => { + warn!("failed to load app metadata for plugin/read: {err:#}"); + connectors::list_cached_all_connectors(config) + .await + .unwrap_or_default() + } + }; + + connectors::connectors_for_plugin_apps(connectors, plugin_apps) + .into_iter() + .map(AppSummary::from) + .collect() +} + +pub(super) fn plugin_apps_needing_auth( + all_connectors: &[AppInfo], + accessible_connectors: &[AppInfo], + plugin_apps: &[AppConnectorId], + codex_apps_ready: bool, +) -> Vec { + if !codex_apps_ready { + return Vec::new(); + } + + let accessible_ids = accessible_connectors + .iter() + .map(|connector| connector.id.as_str()) + .collect::>(); + let plugin_app_ids = plugin_apps + .iter() + .map(|connector_id| connector_id.0.as_str()) + .collect::>(); + + all_connectors + .iter() + .filter(|connector| { + plugin_app_ids.contains(connector.id.as_str()) + && !accessible_ids.contains(connector.id.as_str()) + }) + .cloned() + .map(AppSummary::from) + .collect() +} + +#[cfg(test)] +mod tests { + use codex_app_server_protocol::AppInfo; + use codex_core::plugins::AppConnectorId; + use pretty_assertions::assert_eq; + + use super::plugin_apps_needing_auth; + + #[test] + fn plugin_apps_needing_auth_returns_empty_when_codex_apps_is_not_ready() { + let all_connectors = vec![AppInfo { + id: "alpha".to_string(), + name: "Alpha".to_string(), + description: Some("Alpha connector".to_string()), + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), + is_accessible: false, + is_enabled: true, + plugin_display_names: Vec::new(), + }]; + + assert_eq!( + plugin_apps_needing_auth( + &all_connectors, + &[], + &[AppConnectorId("alpha".to_string())], + false, + ), + Vec::new() + ); + } +} diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index f84b70ba0..5ce3d456c 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -41,6 +41,7 @@ use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginListParams; +use codex_app_server_protocol::PluginReadParams; use codex_app_server_protocol::PluginUninstallParams; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ReviewStartParams; @@ -473,6 +474,15 @@ impl McpProcess { self.send_request("plugin/list", params).await } + /// Send a `plugin/read` JSON-RPC request. + pub async fn send_plugin_read_request( + &mut self, + params: PluginReadParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("plugin/read", params).await + } + /// Send a JSON-RPC request with raw params for protocol-level validation tests. pub async fn send_raw_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 20e975879..daaca1cf8 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -19,6 +19,7 @@ mod output_schema; mod plan_item; mod plugin_install; mod plugin_list; +mod plugin_read; mod plugin_uninstall; mod rate_limits; mod realtime_conversation; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs new file mode 100644 index 000000000..8e454ee7b --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -0,0 +1,303 @@ +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::PluginAuthPolicy; +use codex_app_server_protocol::PluginInstallPolicy; +use codex_app_server_protocol::PluginReadParams; +use codex_app_server_protocol::PluginReadResponse; +use codex_app_server_protocol::RequestId; +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); + +#[tokio::test] +async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()> { + let codex_home = TempDir::new()?; + let repo_root = TempDir::new()?; + let plugin_root = repo_root.path().join("plugins/demo-plugin"); + std::fs::create_dir_all(repo_root.path().join(".git"))?; + std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?; + std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?; + std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer"))?; + std::fs::write( + repo_root.path().join(".agents/plugins/marketplace.json"), + r#"{ + "name": "codex-curated", + "plugins": [ + { + "name": "demo-plugin", + "source": { + "source": "local", + "path": "./plugins/demo-plugin" + }, + "installPolicy": "AVAILABLE", + "authPolicy": "ON_INSTALL", + "category": "Design" + } + ] +}"#, + )?; + std::fs::write( + plugin_root.join(".codex-plugin/plugin.json"), + r##"{ + "name": "demo-plugin", + "description": "Longer manifest description", + "interface": { + "displayName": "Plugin Display Name", + "shortDescription": "Short description for subtitle", + "longDescription": "Long description for details page", + "developerName": "OpenAI", + "category": "Productivity", + "capabilities": ["Interactive", "Write"], + "websiteURL": "https://openai.com/", + "privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/", + "termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/", + "defaultPrompt": "Starter prompt for trying a plugin", + "brandColor": "#3B82F6", + "composerIcon": "./assets/icon.png", + "logo": "./assets/logo.png", + "screenshots": ["./assets/screenshot1.png"] + } +}"##, + )?; + std::fs::write( + plugin_root.join("skills/thread-summarizer/SKILL.md"), + r#"--- +name: thread-summarizer +description: Summarize email threads +--- + +# Thread Summarizer +"#, + )?; + std::fs::write( + plugin_root.join(".app.json"), + r#"{ + "apps": { + "gmail": { + "id": "gmail" + } + } +}"#, + )?; + std::fs::write( + plugin_root.join(".mcp.json"), + r#"{ + "mcpServers": { + "demo": { + "command": "demo-server" + } + } +}"#, + )?; + std::fs::write( + codex_home.path().join("config.toml"), + r#"[features] +plugins = true + +[plugins."demo-plugin@codex-curated"] +enabled = true +"#, + )?; + write_installed_plugin(&codex_home, "codex-curated", "demo-plugin")?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let marketplace_path = + AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; + let request_id = mcp + .send_plugin_read_request(PluginReadParams { + marketplace_path: marketplace_path.clone(), + plugin_name: "demo-plugin".to_string(), + }) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let response: PluginReadResponse = to_response(response)?; + + assert_eq!(response.plugin.marketplace_name, "codex-curated"); + assert_eq!(response.plugin.marketplace_path, marketplace_path); + assert_eq!(response.plugin.summary.id, "demo-plugin@codex-curated"); + assert_eq!(response.plugin.summary.name, "demo-plugin"); + assert_eq!( + response.plugin.description.as_deref(), + Some("Longer manifest description") + ); + assert_eq!(response.plugin.summary.installed, true); + assert_eq!(response.plugin.summary.enabled, true); + assert_eq!( + response.plugin.summary.install_policy, + PluginInstallPolicy::Available + ); + assert_eq!( + response.plugin.summary.auth_policy, + PluginAuthPolicy::OnInstall + ); + assert_eq!( + response + .plugin + .summary + .interface + .as_ref() + .and_then(|interface| interface.display_name.as_deref()), + Some("Plugin Display Name") + ); + assert_eq!( + response + .plugin + .summary + .interface + .as_ref() + .and_then(|interface| interface.category.as_deref()), + Some("Design") + ); + assert_eq!(response.plugin.skills.len(), 1); + assert_eq!( + response.plugin.skills[0].name, + "demo-plugin:thread-summarizer" + ); + assert_eq!( + response.plugin.skills[0].description, + "Summarize email threads" + ); + assert_eq!(response.plugin.apps.len(), 1); + assert_eq!(response.plugin.apps[0].id, "gmail"); + assert_eq!(response.plugin.apps[0].name, "gmail"); + assert_eq!( + response.plugin.apps[0].install_url.as_deref(), + Some("https://chatgpt.com/apps/gmail/gmail") + ); + assert_eq!(response.plugin.mcp_servers.len(), 1); + assert_eq!(response.plugin.mcp_servers[0], "demo"); + Ok(()) +} + +#[tokio::test] +async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result<()> { + let codex_home = TempDir::new()?; + let repo_root = TempDir::new()?; + std::fs::create_dir_all(repo_root.path().join(".git"))?; + std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?; + std::fs::write( + repo_root.path().join(".agents/plugins/marketplace.json"), + r#"{ + "name": "codex-curated", + "plugins": [ + { + "name": "demo-plugin", + "source": { + "source": "local", + "path": "./plugins/demo-plugin" + } + } + ] +}"#, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_read_request(PluginReadParams { + marketplace_path: AbsolutePathBuf::try_from( + repo_root.path().join(".agents/plugins/marketplace.json"), + )?, + plugin_name: "missing-plugin".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!( + err.error + .message + .contains("plugin `missing-plugin` was not found") + ); + Ok(()) +} + +#[tokio::test] +async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() -> Result<()> { + let codex_home = TempDir::new()?; + let repo_root = TempDir::new()?; + let plugin_root = repo_root.path().join("plugins/demo-plugin"); + std::fs::create_dir_all(repo_root.path().join(".git"))?; + std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?; + std::fs::create_dir_all(&plugin_root)?; + std::fs::write( + repo_root.path().join(".agents/plugins/marketplace.json"), + r#"{ + "name": "codex-curated", + "plugins": [ + { + "name": "demo-plugin", + "source": { + "source": "local", + "path": "./plugins/demo-plugin" + } + } + ] +}"#, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_read_request(PluginReadParams { + marketplace_path: AbsolutePathBuf::try_from( + repo_root.path().join(".agents/plugins/marketplace.json"), + )?, + plugin_name: "demo-plugin".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!( + err.error + .message + .contains("missing or invalid .codex-plugin/plugin.json") + ); + Ok(()) +} + +fn write_installed_plugin( + codex_home: &TempDir, + marketplace_name: &str, + plugin_name: &str, +) -> Result<()> { + let plugin_root = codex_home + .path() + .join("plugins/cache") + .join(marketplace_name) + .join(plugin_name) + .join("local/.codex-plugin"); + std::fs::create_dir_all(&plugin_root)?; + std::fs::write( + plugin_root.join("plugin.json"), + format!(r#"{{"name":"{plugin_name}"}}"#), + )?; + Ok(()) +} diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 54e2590c6..27efab1e5 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -21,6 +21,7 @@ pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools use codex_core::connectors::merge_connectors; use codex_core::connectors::merge_plugin_apps; pub use codex_core::connectors::with_app_enabled_state; +use codex_core::plugins::AppConnectorId; use codex_core::plugins::PluginsManager; const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60); @@ -118,6 +119,21 @@ fn plugin_apps_for_config(config: &Config) -> Vec, + plugin_apps: &[AppConnectorId], +) -> Vec { + let plugin_app_ids = plugin_apps + .iter() + .map(|connector_id| connector_id.0.as_str()) + .collect::>(); + + filter_disallowed_connectors(merge_plugin_apps(connectors, plugin_apps.to_vec())) + .into_iter() + .filter(|connector| plugin_app_ids.contains(connector.id.as_str())) + .collect() +} + pub fn merge_connectors_with_accessible( connectors: Vec, accessible_connectors: Vec, @@ -143,6 +159,7 @@ pub fn merge_connectors_with_accessible( mod tests { use super::*; use codex_core::connectors::connector_install_url; + use codex_core::plugins::AppConnectorId; use pretty_assertions::assert_eq; fn app(id: &str) -> AppInfo { @@ -243,4 +260,27 @@ mod tests { vec![merged_app("alpha", true), merged_app("beta", true)] ); } + + #[test] + fn connectors_for_plugin_apps_returns_only_requested_plugin_apps() { + let connectors = connectors_for_plugin_apps( + vec![app("alpha"), app("beta")], + &[ + AppConnectorId("alpha".to_string()), + AppConnectorId("gmail".to_string()), + ], + ); + assert_eq!(connectors, vec![app("alpha"), merged_app("gmail", false)]); + } + + #[test] + fn connectors_for_plugin_apps_filters_disallowed_plugin_apps() { + let connectors = connectors_for_plugin_apps( + Vec::new(), + &[AppConnectorId( + "asdk_app_6938a94a61d881918ef32cb999ff937c".to_string(), + )], + ); + assert_eq!(connectors, Vec::::new()); + } } diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index 032437c19..0c198c6a7 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -34,8 +34,12 @@ use crate::default_client::build_reqwest_client; use crate::features::Feature; use crate::features::FeatureOverrides; use crate::features::Features; +use crate::skills::SkillMetadata; +use crate::skills::loader::SkillRoot; +use crate::skills::loader::load_skills_from_roots; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::MergeStrategy; +use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde_json::Map as JsonMap; @@ -71,6 +75,12 @@ pub struct PluginInstallRequest { pub marketplace_path: AbsolutePathBuf, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PluginReadRequest { + pub plugin_name: String, + pub marketplace_path: AbsolutePathBuf, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PluginInstallOutcome { pub plugin_id: PluginId, @@ -79,6 +89,29 @@ pub struct PluginInstallOutcome { pub auth_policy: MarketplacePluginAuthPolicy, } +#[derive(Debug, Clone, PartialEq)] +pub struct PluginReadOutcome { + pub marketplace_name: String, + pub marketplace_path: AbsolutePathBuf, + pub plugin: PluginDetailSummary, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PluginDetailSummary { + pub id: String, + pub name: String, + pub description: Option, + pub source: MarketplacePluginSourceSummary, + pub install_policy: MarketplacePluginInstallPolicy, + pub auth_policy: MarketplacePluginAuthPolicy, + pub interface: Option, + pub installed: bool, + pub enabled: bool, + pub skills: Vec, + pub apps: Vec, + pub mcp_server_names: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConfiguredMarketplaceSummary { pub name: String, @@ -647,20 +680,7 @@ impl PluginsManager { config: &Config, additional_roots: &[AbsolutePathBuf], ) -> Result, MarketplaceError> { - let installed_plugins = configured_plugins_from_stack(&config.config_layer_stack) - .into_keys() - .filter(|plugin_key| { - PluginId::parse(plugin_key) - .ok() - .is_some_and(|plugin_id| self.store.is_installed(&plugin_id)) - }) - .collect::>(); - let configured_plugins = self - .plugins_for_config(config) - .plugins() - .iter() - .map(|plugin| (plugin.config_name.clone(), plugin.enabled)) - .collect::>(); + let (installed_plugins, configured_plugins) = self.configured_plugin_states(config); let marketplaces = list_marketplaces(&self.marketplace_roots(additional_roots))?; let mut seen_plugin_keys = HashSet::new(); @@ -705,6 +725,83 @@ impl PluginsManager { .collect()) } + pub fn read_plugin_for_config( + &self, + config: &Config, + request: &PluginReadRequest, + ) -> Result { + let marketplace = load_marketplace_summary(&request.marketplace_path)?; + let marketplace_name = marketplace.name.clone(); + let plugin = marketplace + .plugins + .into_iter() + .find(|plugin| plugin.name == request.plugin_name); + let Some(plugin) = plugin else { + return Err(MarketplaceError::PluginNotFound { + plugin_name: request.plugin_name.clone(), + marketplace_name, + }); + }; + + let plugin_id = PluginId::new(plugin.name.clone(), marketplace.name.clone()).map_err( + |err| match err { + PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message), + }, + )?; + let plugin_key = plugin_id.as_key(); + let (installed_plugins, configured_plugins) = self.configured_plugin_states(config); + let source_path = match &plugin.source { + MarketplacePluginSourceSummary::Local { path } => path.clone(), + }; + let manifest = load_plugin_manifest(source_path.as_path()).ok_or_else(|| { + MarketplaceError::InvalidPlugin( + "missing or invalid .codex-plugin/plugin.json".to_string(), + ) + })?; + let description = manifest.description.clone(); + let manifest_paths = plugin_manifest_paths(&manifest, source_path.as_path()); + let skill_roots = plugin_skill_roots(source_path.as_path(), &manifest_paths); + let skills = load_skills_from_roots(skill_roots.into_iter().map(|path| SkillRoot { + path, + scope: SkillScope::User, + })) + .skills; + let apps = load_plugin_apps(source_path.as_path()); + let mcp_config_paths = plugin_mcp_config_paths(source_path.as_path(), &manifest_paths); + let mut mcp_server_names = Vec::new(); + for mcp_config_path in mcp_config_paths { + mcp_server_names.extend( + load_mcp_servers_from_file(source_path.as_path(), &mcp_config_path) + .mcp_servers + .into_keys(), + ); + } + mcp_server_names.sort_unstable(); + mcp_server_names.dedup(); + + Ok(PluginReadOutcome { + marketplace_name: marketplace.name, + marketplace_path: marketplace.path, + plugin: PluginDetailSummary { + id: plugin_key.clone(), + name: plugin.name, + description, + source: plugin.source, + install_policy: plugin.install_policy, + auth_policy: plugin.auth_policy, + interface: plugin.interface, + installed: installed_plugins.contains(&plugin_key), + enabled: configured_plugins + .get(&plugin_key) + .copied() + .unwrap_or(false), + skills, + apps, + mcp_server_names, + }, + }) + } + pub fn maybe_start_curated_repo_sync_for_config(self: &Arc, config: &Config) { if plugins_feature_enabled_from_stack(&config.config_layer_stack) { let mut configured_curated_plugin_ids = @@ -772,6 +869,27 @@ impl PluginsManager { } } + fn configured_plugin_states( + &self, + config: &Config, + ) -> (HashSet, HashMap) { + let installed_plugins = configured_plugins_from_stack(&config.config_layer_stack) + .into_keys() + .filter(|plugin_key| { + PluginId::parse(plugin_key) + .ok() + .is_some_and(|plugin_id| self.store.is_installed(&plugin_id)) + }) + .collect::>(); + let configured_plugins = self + .plugins_for_config(config) + .plugins() + .iter() + .map(|plugin| (plugin.config_name.clone(), plugin.enabled)) + .collect::>(); + (installed_plugins, configured_plugins) + } + fn marketplace_roots(&self, additional_roots: &[AbsolutePathBuf]) -> Vec { // Treat the curated catalog as an extra marketplace root so plugin listing can surface it // without requiring every caller to know where it is stored. diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 1540459aa..9aac37926 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -15,10 +15,13 @@ pub use manager::ConfiguredMarketplacePluginSummary; pub use manager::ConfiguredMarketplaceSummary; pub use manager::LoadedPlugin; pub use manager::PluginCapabilitySummary; +pub use manager::PluginDetailSummary; pub use manager::PluginInstallError; pub use manager::PluginInstallOutcome; pub use manager::PluginInstallRequest; pub use manager::PluginLoadOutcome; +pub use manager::PluginReadOutcome; +pub use manager::PluginReadRequest; pub use manager::PluginRemoteSyncError; pub use manager::PluginUninstallError; pub use manager::PluginsManager;