feat: add plugin/read. (#14445)

return more information for a specific plugin.
This commit is contained in:
xl-openai 2026-03-12 16:52:21 -07:00 committed by GitHub
parent b7dba72dbd
commit 1ea69e8d50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1569 additions and 179 deletions

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": [

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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, };

View file

@ -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, };

View file

@ -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<SkillSummary>, apps: Array<AppSummary>, mcpServers: Array<string>, };

View file

@ -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, };

View file

@ -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, };

View file

@ -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, };

View file

@ -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";

View file

@ -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,

View file

@ -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<String>,
}
#[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<PluginInterface>,
}
#[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<String>,
pub skills: Vec<SkillSummary>,
pub apps: Vec<AppSummary>,
pub mcp_servers: Vec<String>,
}
#[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<String>,
pub interface: Option<SkillInterface>,
pub path: PathBuf,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View file

@ -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**).

View file

@ -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<AppInfo> {
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<AppSummary> {
if !codex_apps_ready {
return Vec::new();
}
let accessible_ids = accessible_connectors
.iter()
.map(|connector| connector.id.as_str())
.collect::<HashSet<_>>();
let plugin_app_ids = plugin_apps
.iter()
.map(|connector_id| connector_id.0.as_str())
.collect::<HashSet<_>>();
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<u32>,
) -> Result<AppsListResponse, JSONRPCErrorError> {
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<OutgoingMessageSender>,
data: Vec<AppInfo>,
) {
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<SkillSummary> {
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<codex_app_server_protocol::SkillErrorInfo> {
@ -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::<AppSummary>::new()
);
}
#[test]
fn collect_resume_override_mismatches_includes_service_tier() {
let request = ThreadResumeParams {

View file

@ -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<AppInfo> {
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<u32>,
) -> Result<AppsListResponse, JSONRPCErrorError> {
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<OutgoingMessageSender>,
data: Vec<AppInfo>,
) {
outgoing
.send_server_notification(ServerNotification::AppListUpdated(
AppListUpdatedNotification { data },
))
.await;
}

View file

@ -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<AppSummary> {
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<AppSummary> {
if !codex_apps_ready {
return Vec::new();
}
let accessible_ids = accessible_connectors
.iter()
.map(|connector| connector.id.as_str())
.collect::<HashSet<_>>();
let plugin_app_ids = plugin_apps
.iter()
.map(|connector_id| connector_id.0.as_str())
.collect::<HashSet<_>>();
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()
);
}
}

View file

@ -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<i64> {
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,

View file

@ -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;

View file

@ -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(())
}

View file

@ -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<codex_core::plugins::AppConnec
.effective_apps()
}
pub fn connectors_for_plugin_apps(
connectors: Vec<AppInfo>,
plugin_apps: &[AppConnectorId],
) -> Vec<AppInfo> {
let plugin_app_ids = plugin_apps
.iter()
.map(|connector_id| connector_id.0.as_str())
.collect::<HashSet<_>>();
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<AppInfo>,
accessible_connectors: Vec<AppInfo>,
@ -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::<AppInfo>::new());
}
}

View file

@ -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<String>,
pub source: MarketplacePluginSourceSummary,
pub install_policy: MarketplacePluginInstallPolicy,
pub auth_policy: MarketplacePluginAuthPolicy,
pub interface: Option<PluginManifestInterfaceSummary>,
pub installed: bool,
pub enabled: bool,
pub skills: Vec<SkillMetadata>,
pub apps: Vec<AppConnectorId>,
pub mcp_server_names: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfiguredMarketplaceSummary {
pub name: String,
@ -647,20 +680,7 @@ impl PluginsManager {
config: &Config,
additional_roots: &[AbsolutePathBuf],
) -> Result<Vec<ConfiguredMarketplaceSummary>, 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::<HashSet<_>>();
let configured_plugins = self
.plugins_for_config(config)
.plugins()
.iter()
.map(|plugin| (plugin.config_name.clone(), plugin.enabled))
.collect::<HashMap<String, bool>>();
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<PluginReadOutcome, MarketplaceError> {
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<Self>, 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<String>, HashMap<String, bool>) {
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::<HashSet<_>>();
let configured_plugins = self
.plugins_for_config(config)
.plugins()
.iter()
.map(|plugin| (plugin.config_name.clone(), plugin.enabled))
.collect::<HashMap<String, bool>>();
(installed_plugins, configured_plugins)
}
fn marketplace_roots(&self, additional_roots: &[AbsolutePathBuf]) -> Vec<AbsolutePathBuf> {
// 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.

View file

@ -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;