mirror of
https://github.com/openai/codex.git
synced 2026-03-05 14:13:21 +00:00
Compare commits
1 Commits
dev/sayan/
...
xl/plugins
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ea5996f33 |
@@ -953,25 +953,34 @@
|
||||
},
|
||||
"PluginInstallParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
"marketplacePath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"marketplacePath",
|
||||
"pluginName"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListParams": {
|
||||
"properties": {
|
||||
"additionalRoots": {
|
||||
"description": "Additional roots to use when discovering repo-scoped marketplaces. Home-scoped marketplaces are always considered.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ProductSurface": {
|
||||
"enum": [
|
||||
"chatgpt",
|
||||
@@ -3264,6 +3273,30 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/list"
|
||||
],
|
||||
"title": "Plugin/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -730,6 +730,30 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/list"
|
||||
],
|
||||
"title": "Plugin/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PluginListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -10924,21 +10948,15 @@
|
||||
"PluginInstallParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
"marketplacePath": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"marketplacePath",
|
||||
"pluginName"
|
||||
],
|
||||
"title": "PluginInstallParams",
|
||||
@@ -10949,6 +10967,104 @@
|
||||
"title": "PluginInstallResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"additionalRoots": {
|
||||
"description": "Additional roots to use when discovering repo-scoped marketplaces. Home-scoped marketplaces are always considered.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginMarketplaceEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PluginListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginMarketplaceEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path",
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"local"
|
||||
],
|
||||
"title": "LocalPluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "LocalPluginSource",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginSummary": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v2/PluginSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"name",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProductSurface": {
|
||||
"enum": [
|
||||
"chatgpt",
|
||||
|
||||
@@ -1204,6 +1204,30 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/list"
|
||||
],
|
||||
"title": "Plugin/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -8262,21 +8286,15 @@
|
||||
"PluginInstallParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
"marketplacePath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"marketplacePath",
|
||||
"pluginName"
|
||||
],
|
||||
"title": "PluginInstallParams",
|
||||
@@ -8287,6 +8305,104 @@
|
||||
"title": "PluginInstallResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"additionalRoots": {
|
||||
"description": "Additional roots to use when discovering repo-scoped marketplaces. Home-scoped marketplaces are always considered.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginMarketplaceEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PluginListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginMarketplaceEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path",
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"local"
|
||||
],
|
||||
"title": "LocalPluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "LocalPluginSource",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginSummary": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"name",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProductSurface": {
|
||||
"enum": [
|
||||
"chatgpt",
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"marketplaceName": {
|
||||
"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": [
|
||||
"marketplaceName",
|
||||
"marketplacePath",
|
||||
"pluginName"
|
||||
],
|
||||
"title": "PluginInstallParams",
|
||||
|
||||
@@ -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": {
|
||||
"additionalRoots": {
|
||||
"description": "Additional roots to use when discovering repo-scoped marketplaces. Home-scoped marketplaces are always considered.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginListParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"PluginMarketplaceEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path",
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"local"
|
||||
],
|
||||
"title": "LocalPluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "LocalPluginSource",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginSummary": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"name",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginMarketplaceEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PluginListResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import type { LoginAccountParams } from "./v2/LoginAccountParams";
|
||||
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 { ReviewStartParams } from "./v2/ReviewStartParams";
|
||||
import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams";
|
||||
import type { SkillsListParams } from "./v2/SkillsListParams";
|
||||
@@ -49,4 +50,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": "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": "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": "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": "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": "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": "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, };
|
||||
|
||||
@@ -1,5 +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 PluginInstallParams = { marketplaceName: string, pluginName: string, cwd?: string | null, };
|
||||
export type PluginInstallParams = { marketplacePath: AbsolutePathBuf, pluginName: string, };
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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 PluginListParams = {
|
||||
/**
|
||||
* Additional roots to use when discovering repo-scoped marketplaces. Home-scoped
|
||||
* marketplaces are always considered.
|
||||
*/
|
||||
additionalRoots?: Array<AbsolutePathBuf> | null, };
|
||||
@@ -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 { PluginMarketplaceEntry } from "./PluginMarketplaceEntry";
|
||||
|
||||
export type PluginListResponse = { data: Array<PluginMarketplaceEntry>, };
|
||||
@@ -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 { PluginSummary } from "./PluginSummary";
|
||||
|
||||
export type PluginMarketplaceEntry = { name: string, path: string, plugins: Array<PluginSummary>, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginSource = { "type": "local", path: string, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginSource } from "./PluginSource";
|
||||
|
||||
export type PluginSummary = { name: string, source: PluginSource, enabled: boolean, };
|
||||
@@ -126,6 +126,11 @@ export type { PatchChangeKind } from "./PatchChangeKind";
|
||||
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
|
||||
export type { PluginInstallParams } from "./PluginInstallParams";
|
||||
export type { PluginInstallResponse } from "./PluginInstallResponse";
|
||||
export type { PluginListParams } from "./PluginListParams";
|
||||
export type { PluginListResponse } from "./PluginListResponse";
|
||||
export type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry";
|
||||
export type { PluginSource } from "./PluginSource";
|
||||
export type { PluginSummary } from "./PluginSummary";
|
||||
export type { ProductSurface } from "./ProductSurface";
|
||||
export type { ProfileV2 } from "./ProfileV2";
|
||||
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
|
||||
|
||||
@@ -248,6 +248,10 @@ client_request_definitions! {
|
||||
params: v2::SkillsListParams,
|
||||
response: v2::SkillsListResponse,
|
||||
},
|
||||
PluginList => "plugin/list" {
|
||||
params: v2::PluginListParams,
|
||||
response: v2::PluginListResponse,
|
||||
},
|
||||
SkillsRemoteList => "skills/remote/list" {
|
||||
params: v2::SkillsRemoteReadParams,
|
||||
response: v2::SkillsRemoteReadResponse,
|
||||
|
||||
@@ -2368,6 +2368,23 @@ pub struct SkillsListResponse {
|
||||
pub data: Vec<SkillsListEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginListParams {
|
||||
/// Additional roots to use when discovering repo-scoped marketplaces. Home-scoped
|
||||
/// marketplaces are always considered.
|
||||
#[ts(optional = nullable)]
|
||||
pub additional_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginListResponse {
|
||||
pub data: Vec<PluginMarketplaceEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -2531,6 +2548,34 @@ pub struct SkillsListEntry {
|
||||
pub errors: Vec<SkillErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginMarketplaceEntry {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub plugins: Vec<PluginSummary>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginSummary {
|
||||
pub name: String,
|
||||
pub source: PluginSource,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginSource {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Local { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -2550,10 +2595,8 @@ pub struct SkillsConfigWriteResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginInstallParams {
|
||||
pub marketplace_name: String,
|
||||
pub marketplace_path: AbsolutePathBuf,
|
||||
pub plugin_name: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -148,12 +148,13 @@ Example with notification opt-out:
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `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 marketplaces (home plus any repo marketplaces reachable from `additionalRoots`), including each plugin's current `enabled` state from config (**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**).
|
||||
- `app/list` — list available apps.
|
||||
- `skills/config/write` — write user-level skill config by path.
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry by `pluginName` and `marketplaceName` (**under development; do not call from production clients yet**).
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry by `pluginName` and `marketplacePath` (**under development; do not call from production clients yet**).
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
|
||||
@@ -79,6 +79,11 @@ use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::ProductSurface as ApiProductSurface;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewDelivery as ApiReviewDelivery;
|
||||
@@ -198,6 +203,8 @@ 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::MarketplaceError;
|
||||
use codex_core::plugins::MarketplacePluginSourceSummary;
|
||||
use codex_core::plugins::PluginInstallError as CorePluginInstallError;
|
||||
use codex_core::plugins::PluginInstallRequest;
|
||||
use codex_core::read_head_for_summary;
|
||||
@@ -646,6 +653,10 @@ impl CodexMessageProcessor {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::PluginList { request_id, params } => {
|
||||
self.plugin_list(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;
|
||||
@@ -4884,6 +4895,80 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn plugin_list(&self, request_id: ConnectionRequestId, params: PluginListParams) {
|
||||
let additional_roots = params.additional_roots.unwrap_or_default();
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let config = match self.load_latest_config().await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let data = match tokio::task::spawn_blocking(move || {
|
||||
let marketplaces =
|
||||
plugins_manager.list_marketplaces_for_config(&config, &additional_roots)?;
|
||||
Ok::<Vec<PluginMarketplaceEntry>, MarketplaceError>(
|
||||
marketplaces
|
||||
.into_iter()
|
||||
.map(|marketplace| PluginMarketplaceEntry {
|
||||
name: marketplace.name,
|
||||
path: marketplace.path,
|
||||
plugins: marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| PluginSummary {
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
source: match plugin.source {
|
||||
MarketplacePluginSourceSummary::Local { path } => {
|
||||
PluginSource::Local { path }
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(data)) => data,
|
||||
Ok(Err(err)) => {
|
||||
match err {
|
||||
MarketplaceError::Io { .. } => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to list marketplace plugins: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
MarketplaceError::InvalidMarketplaceFile { .. }
|
||||
| MarketplaceError::PluginNotFound { .. }
|
||||
| MarketplaceError::DuplicatePlugin { .. }
|
||||
| MarketplaceError::InvalidPlugin(_) => {
|
||||
self.send_invalid_request_error(request_id, err.to_string())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to list marketplace plugins: {err}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.outgoing
|
||||
.send_response(request_id, PluginListResponse { data })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn skills_remote_list(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -4994,16 +5079,14 @@ impl CodexMessageProcessor {
|
||||
|
||||
async fn plugin_install(&self, request_id: ConnectionRequestId, params: PluginInstallParams) {
|
||||
let PluginInstallParams {
|
||||
marketplace_name,
|
||||
marketplace_path,
|
||||
plugin_name,
|
||||
cwd,
|
||||
} = params;
|
||||
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let request = PluginInstallRequest {
|
||||
plugin_name,
|
||||
marketplace_name,
|
||||
cwd: cwd.unwrap_or_else(|| self.config.cwd.clone()),
|
||||
marketplace_path,
|
||||
};
|
||||
|
||||
match plugins_manager.install_plugin(request).await {
|
||||
@@ -5036,7 +5119,14 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
CorePluginInstallError::Marketplace(_) | CorePluginInstallError::Store(_) => {}
|
||||
err @ (CorePluginInstallError::Marketplace(_)
|
||||
| CorePluginInstallError::Store(_)) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to install plugin: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LoginAccountParams;
|
||||
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::RequestId;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
@@ -439,6 +441,33 @@ impl McpProcess {
|
||||
self.send_request("skills/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `plugin/install` JSON-RPC request.
|
||||
pub async fn send_plugin_install_request(
|
||||
&mut self,
|
||||
params: PluginInstallParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("plugin/install", params).await
|
||||
}
|
||||
|
||||
/// Send a `plugin/list` JSON-RPC request.
|
||||
pub async fn send_plugin_list_request(
|
||||
&mut self,
|
||||
params: PluginListParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("plugin/list", params).await
|
||||
}
|
||||
|
||||
/// Send a JSON-RPC request with raw params for protocol-level validation tests.
|
||||
pub async fn send_raw_request(
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> anyhow::Result<i64> {
|
||||
self.send_request(method, params).await
|
||||
}
|
||||
|
||||
/// Send a `collaborationMode/list` JSON-RPC request.
|
||||
pub async fn send_list_collaboration_modes_request(
|
||||
&mut self,
|
||||
|
||||
@@ -14,6 +14,8 @@ mod initialize;
|
||||
mod model_list;
|
||||
mod output_schema;
|
||||
mod plan_item;
|
||||
mod plugin_install;
|
||||
mod plugin_list;
|
||||
mod rate_limits;
|
||||
mod realtime_conversation;
|
||||
mod request_user_input;
|
||||
|
||||
65
codex-rs/app-server/tests/suite/v2/plugin_install.rs
Normal file
65
codex-rs/app-server/tests/suite/v2/plugin_install.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
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_install_rejects_relative_marketplace_paths() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/install",
|
||||
Some(serde_json::json!({
|
||||
"marketplacePath": "relative-marketplace.json",
|
||||
"pluginName": "missing-plugin",
|
||||
})),
|
||||
)
|
||||
.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("Invalid request"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_returns_internal_error_for_missing_marketplace_file() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_install_request(PluginInstallParams {
|
||||
marketplace_path: AbsolutePathBuf::try_from(
|
||||
codex_home.path().join("missing-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, -32603);
|
||||
assert!(err.error.message.contains("failed to install plugin"));
|
||||
Ok(())
|
||||
}
|
||||
145
codex-rs/app-server/tests/suite/v2/plugin_list.rs
Normal file
145
codex-rs/app-server/tests/suite/v2/plugin_list.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
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::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
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_list_returns_invalid_request_for_invalid_marketplace_file() -> 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"),
|
||||
"{not json",
|
||||
)?;
|
||||
|
||||
let home = codex_home.path().to_string_lossy().into_owned();
|
||||
let mut mcp =
|
||||
McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home.as_str()))]).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
additional_roots: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
|
||||
})
|
||||
.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("invalid marketplace file"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_rejects_relative_additional_roots() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/list",
|
||||
Some(serde_json::json!({
|
||||
"additionalRoots": ["relative-root"],
|
||||
})),
|
||||
)
|
||||
.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("Invalid request"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_includes_enabled_state_from_config() -> 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": "enabled-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./enabled-plugin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disabled-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./disabled-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"[plugins."enabled-plugin@codex-curated"]
|
||||
enabled = true
|
||||
|
||||
[plugins."disabled-plugin@codex-curated"]
|
||||
enabled = false
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
additional_roots: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
let marketplace = response
|
||||
.data
|
||||
.into_iter()
|
||||
.find(|marketplace| {
|
||||
marketplace.path == repo_root.path().join(".agents/plugins/marketplace.json")
|
||||
})
|
||||
.expect("expected repo marketplace entry");
|
||||
|
||||
assert_eq!(marketplace.name, "codex-curated");
|
||||
assert_eq!(marketplace.plugins.len(), 2);
|
||||
assert_eq!(marketplace.plugins[0].name, "enabled-plugin");
|
||||
assert_eq!(marketplace.plugins[0].enabled, true);
|
||||
assert_eq!(marketplace.plugins[1].name, "disabled-plugin");
|
||||
assert_eq!(marketplace.plugins[1].enabled, false);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::load_plugin_manifest;
|
||||
use super::marketplace::MarketplaceError;
|
||||
use super::marketplace::MarketplacePluginSourceSummary;
|
||||
use super::marketplace::list_marketplaces;
|
||||
use super::marketplace::resolve_marketplace_plugin;
|
||||
use super::plugin_manifest_name;
|
||||
use super::store::DEFAULT_PLUGIN_VERSION;
|
||||
@@ -42,8 +44,21 @@ pub struct AppConnectorId(pub String);
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PluginInstallRequest {
|
||||
pub plugin_name: String,
|
||||
pub marketplace_name: String,
|
||||
pub cwd: PathBuf,
|
||||
pub marketplace_path: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConfiguredMarketplaceSummary {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub plugins: Vec<ConfiguredMarketplacePluginSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConfiguredMarketplacePluginSummary {
|
||||
pub name: String,
|
||||
pub source: MarketplacePluginSourceSummary,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -177,11 +192,7 @@ impl PluginsManager {
|
||||
&self,
|
||||
request: PluginInstallRequest,
|
||||
) -> Result<PluginInstallResult, PluginInstallError> {
|
||||
let resolved = resolve_marketplace_plugin(
|
||||
&request.cwd,
|
||||
&request.plugin_name,
|
||||
&request.marketplace_name,
|
||||
)?;
|
||||
let resolved = resolve_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;
|
||||
let store = self.store.clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
store.install(resolved.source_path.into_path_buf(), resolved.plugin_id)
|
||||
@@ -205,6 +216,40 @@ impl PluginsManager {
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn list_marketplaces_for_config(
|
||||
&self,
|
||||
config: &Config,
|
||||
additional_roots: &[AbsolutePathBuf],
|
||||
) -> Result<Vec<ConfiguredMarketplaceSummary>, MarketplaceError> {
|
||||
let configured_plugins = configured_plugins_from_stack(&config.config_layer_stack);
|
||||
let marketplaces = list_marketplaces(additional_roots)?;
|
||||
|
||||
Ok(marketplaces
|
||||
.into_iter()
|
||||
.map(|marketplace| {
|
||||
let marketplace_name = marketplace.name.clone();
|
||||
ConfiguredMarketplaceSummary {
|
||||
name: marketplace.name,
|
||||
path: marketplace.path,
|
||||
plugins: marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| ConfiguredMarketplacePluginSummary {
|
||||
// Enabled state is keyed by `<plugin>@<marketplace>`, so duplicate
|
||||
// marketplace files with the same declared name intentionally share
|
||||
// the same config toggle.
|
||||
enabled: configured_plugins
|
||||
.get(&format!("{}@{marketplace_name}", plugin.name))
|
||||
.is_some_and(|config| config.enabled),
|
||||
name: plugin.name,
|
||||
source: plugin.source,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -937,8 +982,10 @@ mod tests {
|
||||
let result = PluginsManager::new(tmp.path().to_path_buf())
|
||||
.install_plugin(PluginInstallRequest {
|
||||
plugin_name: "sample-plugin".to_string(),
|
||||
marketplace_name: "debug".to_string(),
|
||||
cwd: repo_root.clone(),
|
||||
marketplace_path: AbsolutePathBuf::try_from(
|
||||
repo_root.join(".agents/plugins/marketplace.json"),
|
||||
)
|
||||
.unwrap(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -957,4 +1004,88 @@ mod tests {
|
||||
assert!(config.contains(r#"[plugins."sample-plugin@debug"]"#));
|
||||
assert!(config.contains("enabled = true"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_marketplaces_for_config_includes_enabled_state() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo_root = tmp.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).unwrap();
|
||||
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
|
||||
fs::write(
|
||||
repo_root.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "debug",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "enabled-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./enabled-plugin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disabled-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./disabled-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."enabled-plugin@debug"]
|
||||
enabled = true
|
||||
|
||||
[plugins."disabled-plugin@debug"]
|
||||
enabled = false
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(tmp.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("config should load");
|
||||
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(&config, &[AbsolutePathBuf::try_from(repo_root).unwrap()])
|
||||
.unwrap();
|
||||
|
||||
let marketplace = marketplaces
|
||||
.into_iter()
|
||||
.find(|marketplace| {
|
||||
marketplace.path == tmp.path().join("repo/.agents/plugins/marketplace.json")
|
||||
})
|
||||
.expect("expected repo marketplace entry");
|
||||
|
||||
assert_eq!(
|
||||
marketplace,
|
||||
ConfiguredMarketplaceSummary {
|
||||
name: "debug".to_string(),
|
||||
path: tmp.path().join("repo/.agents/plugins/marketplace.json"),
|
||||
plugins: vec![
|
||||
ConfiguredMarketplacePluginSummary {
|
||||
name: "enabled-plugin".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: tmp.path().join("repo/.agents/plugins/enabled-plugin"),
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
ConfiguredMarketplacePluginSummary {
|
||||
name: "disabled-plugin".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: tmp.path().join("repo/.agents/plugins/disabled-plugin"),
|
||||
},
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,24 @@ pub struct ResolvedMarketplacePlugin {
|
||||
pub source_path: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MarketplaceSummary {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub plugins: Vec<MarketplacePluginSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MarketplacePluginSummary {
|
||||
pub name: String,
|
||||
pub source: MarketplacePluginSourceSummary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MarketplacePluginSourceSummary {
|
||||
Local { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MarketplaceError {
|
||||
#[error("{context}: {source}")]
|
||||
@@ -54,80 +72,112 @@ impl MarketplaceError {
|
||||
}
|
||||
}
|
||||
|
||||
// For now, marketplace discovery always reads from disk so installs see the latest
|
||||
// marketplace.json contents without any in-memory cache invalidation.
|
||||
// Always read the specified marketplace file from disk so installs see the
|
||||
// latest marketplace.json contents without any in-memory cache invalidation.
|
||||
pub fn resolve_marketplace_plugin(
|
||||
cwd: &Path,
|
||||
marketplace_path: &AbsolutePathBuf,
|
||||
plugin_name: &str,
|
||||
marketplace_name: &str,
|
||||
) -> Result<ResolvedMarketplacePlugin, MarketplaceError> {
|
||||
resolve_marketplace_plugin_from_paths(
|
||||
&discover_marketplace_paths(cwd),
|
||||
plugin_name,
|
||||
marketplace_name,
|
||||
)
|
||||
let marketplace = load_marketplace(marketplace_path.as_path())?;
|
||||
let marketplace_name = marketplace.name;
|
||||
let mut matches = marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.filter(|plugin| plugin.name == plugin_name)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if matches.is_empty() {
|
||||
return Err(MarketplaceError::PluginNotFound {
|
||||
plugin_name: plugin_name.to_string(),
|
||||
marketplace_name,
|
||||
});
|
||||
}
|
||||
|
||||
if matches.len() > 1 {
|
||||
return Err(MarketplaceError::DuplicatePlugin {
|
||||
plugin_name: plugin_name.to_string(),
|
||||
marketplace_name,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(plugin) = matches.pop() {
|
||||
let plugin_id = PluginId::new(plugin.name, marketplace_name).map_err(|err| match err {
|
||||
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
|
||||
})?;
|
||||
return Ok(ResolvedMarketplacePlugin {
|
||||
plugin_id,
|
||||
source_path: resolve_plugin_source_path(marketplace_path.as_path(), plugin.source)?,
|
||||
});
|
||||
}
|
||||
|
||||
unreachable!("non-empty matches should have returned above")
|
||||
}
|
||||
|
||||
fn resolve_marketplace_plugin_from_paths(
|
||||
marketplace_paths: &[PathBuf],
|
||||
plugin_name: &str,
|
||||
marketplace_name: &str,
|
||||
) -> Result<ResolvedMarketplacePlugin, MarketplaceError> {
|
||||
for marketplace_path in marketplace_paths {
|
||||
let marketplace = load_marketplace(marketplace_path)?;
|
||||
let discovered_marketplace_name = marketplace.name;
|
||||
let mut matches = marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.filter(|plugin| plugin.name == plugin_name)
|
||||
.collect::<Vec<_>>();
|
||||
pub fn list_marketplaces(
|
||||
additional_roots: &[AbsolutePathBuf],
|
||||
) -> Result<Vec<MarketplaceSummary>, MarketplaceError> {
|
||||
list_marketplaces_with_home(additional_roots, home_dir().as_deref())
|
||||
}
|
||||
|
||||
if discovered_marketplace_name != marketplace_name || matches.is_empty() {
|
||||
continue;
|
||||
}
|
||||
fn list_marketplaces_with_home(
|
||||
additional_roots: &[AbsolutePathBuf],
|
||||
home_dir: Option<&Path>,
|
||||
) -> Result<Vec<MarketplaceSummary>, MarketplaceError> {
|
||||
let mut marketplaces = Vec::new();
|
||||
|
||||
if matches.len() > 1 {
|
||||
return Err(MarketplaceError::DuplicatePlugin {
|
||||
plugin_name: plugin_name.to_string(),
|
||||
marketplace_name: marketplace_name.to_string(),
|
||||
});
|
||||
}
|
||||
for marketplace_path in discover_marketplace_paths_from_roots(additional_roots, home_dir) {
|
||||
let marketplace = load_marketplace(marketplace_path.as_path())?;
|
||||
let mut plugins = Vec::new();
|
||||
|
||||
if let Some(plugin) = matches.pop() {
|
||||
let plugin_id = PluginId::new(plugin.name, marketplace_name.to_string()).map_err(
|
||||
|err| match err {
|
||||
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
|
||||
for plugin in marketplace.plugins {
|
||||
let source = match plugin.source {
|
||||
MarketplacePluginSource::Local { path } => MarketplacePluginSourceSummary::Local {
|
||||
path: resolve_plugin_source_path(
|
||||
marketplace_path.as_path(),
|
||||
MarketplacePluginSource::Local { path },
|
||||
)?
|
||||
.into_path_buf(),
|
||||
},
|
||||
)?;
|
||||
return Ok(ResolvedMarketplacePlugin {
|
||||
plugin_id,
|
||||
source_path: resolve_plugin_source_path(marketplace_path, plugin.source)?,
|
||||
};
|
||||
|
||||
plugins.push(MarketplacePluginSummary {
|
||||
name: plugin.name,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
marketplaces.push(MarketplaceSummary {
|
||||
name: marketplace.name,
|
||||
path: marketplace_path,
|
||||
plugins,
|
||||
});
|
||||
}
|
||||
|
||||
Err(MarketplaceError::PluginNotFound {
|
||||
plugin_name: plugin_name.to_string(),
|
||||
marketplace_name: marketplace_name.to_string(),
|
||||
})
|
||||
Ok(marketplaces)
|
||||
}
|
||||
|
||||
fn discover_marketplace_paths(cwd: &Path) -> Vec<PathBuf> {
|
||||
fn discover_marketplace_paths_from_roots(
|
||||
additional_roots: &[AbsolutePathBuf],
|
||||
home_dir: Option<&Path>,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
if let Some(repo_root) = get_git_repo_root(cwd) {
|
||||
let path = repo_root.join(MARKETPLACE_RELATIVE_PATH);
|
||||
if path.is_file() {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(home) = home_dir() {
|
||||
if let Some(home) = home_dir {
|
||||
let path = home.join(MARKETPLACE_RELATIVE_PATH);
|
||||
if path.is_file() {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
for root in additional_roots {
|
||||
if let Some(repo_root) = get_git_repo_root(root.as_path()) {
|
||||
let path = repo_root.join(MARKETPLACE_RELATIVE_PATH);
|
||||
if path.is_file() && !paths.contains(&path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
@@ -233,9 +283,11 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let resolved =
|
||||
resolve_marketplace_plugin(&repo_root.join("nested"), "local-plugin", "codex-curated")
|
||||
.unwrap();
|
||||
let resolved = resolve_marketplace_plugin(
|
||||
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||
"local-plugin",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resolved,
|
||||
@@ -260,7 +312,11 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err = resolve_marketplace_plugin(&repo_root, "missing", "codex-curated").unwrap_err();
|
||||
let err = resolve_marketplace_plugin(
|
||||
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||
"missing",
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
@@ -269,7 +325,112 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_marketplace_plugin_prefers_repo_over_home_for_same_plugin() {
|
||||
fn list_marketplaces_returns_home_and_repo_marketplaces() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let home_root = tmp.path().join("home");
|
||||
let repo_root = tmp.path().join("repo");
|
||||
|
||||
fs::create_dir_all(repo_root.join(".git")).unwrap();
|
||||
fs::create_dir_all(home_root.join(".agents/plugins")).unwrap();
|
||||
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
|
||||
fs::write(
|
||||
home_root.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "shared-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./home-shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "home-only",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./home-only"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
repo_root.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "shared-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./repo-shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "repo-only",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./repo-only"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let marketplaces = list_marketplaces_with_home(
|
||||
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
|
||||
Some(&home_root),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
marketplaces,
|
||||
vec![
|
||||
MarketplaceSummary {
|
||||
name: "codex-curated".to_string(),
|
||||
path: home_root.join(".agents/plugins/marketplace.json"),
|
||||
plugins: vec![
|
||||
MarketplacePluginSummary {
|
||||
name: "shared-plugin".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: home_root.join(".agents/plugins/home-shared"),
|
||||
},
|
||||
},
|
||||
MarketplacePluginSummary {
|
||||
name: "home-only".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: home_root.join(".agents/plugins/home-only"),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
MarketplaceSummary {
|
||||
name: "codex-curated".to_string(),
|
||||
path: repo_root.join(".agents/plugins/marketplace.json"),
|
||||
plugins: vec![
|
||||
MarketplacePluginSummary {
|
||||
name: "shared-plugin".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: repo_root.join(".agents/plugins/repo-shared"),
|
||||
},
|
||||
},
|
||||
MarketplacePluginSummary {
|
||||
name: "repo-only".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: repo_root.join(".agents/plugins/repo-only"),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_marketplaces_keeps_distinct_entries_for_same_name() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let home_root = tmp.path().join("home");
|
||||
let repo_root = tmp.path().join("repo");
|
||||
@@ -313,23 +474,97 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let resolved = resolve_marketplace_plugin_from_paths(
|
||||
&[repo_marketplace, home_marketplace],
|
||||
"local-plugin",
|
||||
"codex-curated",
|
||||
let marketplaces = list_marketplaces_with_home(
|
||||
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
|
||||
Some(&home_root),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resolved,
|
||||
ResolvedMarketplacePlugin {
|
||||
plugin_id: PluginId::new("local-plugin".to_string(), "codex-curated".to_string())
|
||||
.unwrap(),
|
||||
source_path: AbsolutePathBuf::try_from(
|
||||
repo_root.join(".agents/plugins/repo-plugin"),
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
marketplaces,
|
||||
vec![
|
||||
MarketplaceSummary {
|
||||
name: "codex-curated".to_string(),
|
||||
path: home_marketplace,
|
||||
plugins: vec![MarketplacePluginSummary {
|
||||
name: "local-plugin".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: home_root.join(".agents/plugins/home-plugin"),
|
||||
},
|
||||
}],
|
||||
},
|
||||
MarketplaceSummary {
|
||||
name: "codex-curated".to_string(),
|
||||
path: repo_marketplace.clone(),
|
||||
plugins: vec![MarketplacePluginSummary {
|
||||
name: "local-plugin".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: repo_root.join(".agents/plugins/repo-plugin"),
|
||||
},
|
||||
}],
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
let resolved = resolve_marketplace_plugin(
|
||||
&AbsolutePathBuf::try_from(repo_marketplace).unwrap(),
|
||||
"local-plugin",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resolved.source_path,
|
||||
AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/repo-plugin")).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_marketplaces_dedupes_multiple_roots_in_same_repo() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo_root = tmp.path().join("repo");
|
||||
let nested_root = repo_root.join("nested/project");
|
||||
|
||||
fs::create_dir_all(repo_root.join(".git")).unwrap();
|
||||
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
|
||||
fs::create_dir_all(&nested_root).unwrap();
|
||||
fs::write(
|
||||
repo_root.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "local-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let marketplaces = list_marketplaces_with_home(
|
||||
&[
|
||||
AbsolutePathBuf::try_from(repo_root.clone()).unwrap(),
|
||||
AbsolutePathBuf::try_from(nested_root).unwrap(),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
marketplaces,
|
||||
vec![MarketplaceSummary {
|
||||
name: "codex-curated".to_string(),
|
||||
path: repo_root.join(".agents/plugins/marketplace.json"),
|
||||
plugins: vec![MarketplacePluginSummary {
|
||||
name: "local-plugin".to_string(),
|
||||
source: MarketplacePluginSourceSummary::Local {
|
||||
path: repo_root.join(".agents/plugins/plugin"),
|
||||
},
|
||||
}],
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -356,8 +591,11 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err =
|
||||
resolve_marketplace_plugin(&repo_root, "local-plugin", "codex-curated").unwrap_err();
|
||||
let err = resolve_marketplace_plugin(
|
||||
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||
"local-plugin",
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
|
||||
@@ -4,6 +4,8 @@ mod marketplace;
|
||||
mod store;
|
||||
|
||||
pub use manager::AppConnectorId;
|
||||
pub use manager::ConfiguredMarketplacePluginSummary;
|
||||
pub use manager::ConfiguredMarketplaceSummary;
|
||||
pub use manager::LoadedPlugin;
|
||||
pub use manager::PluginInstallError;
|
||||
pub use manager::PluginInstallRequest;
|
||||
@@ -12,5 +14,7 @@ pub use manager::PluginsManager;
|
||||
pub(crate) use manager::plugin_namespace_for_skill_path;
|
||||
pub(crate) use manifest::load_plugin_manifest;
|
||||
pub(crate) use manifest::plugin_manifest_name;
|
||||
pub use marketplace::MarketplaceError;
|
||||
pub use marketplace::MarketplacePluginSourceSummary;
|
||||
pub use store::PluginId;
|
||||
pub use store::PluginInstallResult;
|
||||
|
||||
Reference in New Issue
Block a user