From 314029ffa3521718193e31e73f7ce52d2c403bac Mon Sep 17 00:00:00 2001 From: xl-openai Date: Tue, 17 Feb 2026 11:05:22 -0800 Subject: [PATCH] Add remote skill scope/product_surface/enabled params and cleanup (#11801) skills/remote/list: params=hazelnutScope, productSurface, enabled; returns=data: { id, name, description }[] skills/remote/export: params=hazelnutId; returns={ id, path } --- .../schema/json/ClientRequest.json | 60 ++++- .../codex_app_server_protocol.schemas.json | 64 +++-- .../json/v2/SkillsRemoteReadParams.json | 44 +++- .../json/v2/SkillsRemoteWriteParams.json | 6 +- .../json/v2/SkillsRemoteWriteResponse.json | 6 +- .../schema/typescript/ClientRequest.ts | 2 +- .../schema/typescript/v2/HazelnutScope.ts | 5 + .../schema/typescript/v2/ProductSurface.ts | 5 + .../typescript/v2/SkillsRemoteReadParams.ts | 4 +- .../typescript/v2/SkillsRemoteWriteParams.ts | 2 +- .../v2/SkillsRemoteWriteResponse.ts | 2 +- .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 4 +- .../app-server-protocol/src/protocol/v2.rs | 35 ++- codex-rs/app-server/README.md | 4 +- .../app-server/src/codex_message_processor.rs | 64 +++-- codex-rs/core/src/codex.rs | 81 ++++--- codex-rs/core/src/skills/remote.rs | 224 +++++++----------- codex-rs/protocol/src/protocol.rs | 31 ++- 19 files changed, 402 insertions(+), 243 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a8b66a202..d5ce1e2f8 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -711,6 +711,15 @@ ], "type": "object" }, + "HazelnutScope": { + "enum": [ + "example", + "workspace-shared", + "all-shared", + "personal" + ], + "type": "string" + }, "InitializeCapabilities": { "description": "Client-declared capabilities negotiated during initialize.", "properties": { @@ -1250,6 +1259,15 @@ ], "type": "string" }, + "ProductSurface": { + "enum": [ + "chatgpt", + "codex", + "api", + "atlas" + ], + "type": "string" + }, "ReadOnlyAccess": { "oneOf": [ { @@ -2421,20 +2439,38 @@ "type": "object" }, "SkillsRemoteReadParams": { + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "hazelnutScope": { + "allOf": [ + { + "$ref": "#/definitions/HazelnutScope" + } + ], + "default": "example" + }, + "productSurface": { + "allOf": [ + { + "$ref": "#/definitions/ProductSurface" + } + ], + "default": "codex" + } + }, "type": "object" }, "SkillsRemoteWriteParams": { "properties": { "hazelnutId": { "type": "string" - }, - "isPreload": { - "type": "boolean" } }, "required": [ - "hazelnutId", - "isPreload" + "hazelnutId" ], "type": "object" }, @@ -3599,9 +3635,9 @@ }, "method": { "enum": [ - "skills/remote/read" + "skills/remote/list" ], - "title": "Skills/remote/readRequestMethod", + "title": "Skills/remote/listRequestMethod", "type": "string" }, "params": { @@ -3613,7 +3649,7 @@ "method", "params" ], - "title": "Skills/remote/readRequest", + "title": "Skills/remote/listRequest", "type": "object" }, { @@ -3623,9 +3659,9 @@ }, "method": { "enum": [ - "skills/remote/write" + "skills/remote/export" ], - "title": "Skills/remote/writeRequestMethod", + "title": "Skills/remote/exportRequestMethod", "type": "string" }, "params": { @@ -3637,7 +3673,7 @@ "method", "params" ], - "title": "Skills/remote/writeRequest", + "title": "Skills/remote/exportRequest", "type": "object" }, { @@ -4720,4 +4756,4 @@ } ], "title": "ClientRequest" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 09375c5ed..ad1342d12 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -755,9 +755,9 @@ }, "method": { "enum": [ - "skills/remote/read" + "skills/remote/list" ], - "title": "Skills/remote/readRequestMethod", + "title": "Skills/remote/listRequestMethod", "type": "string" }, "params": { @@ -769,7 +769,7 @@ "method", "params" ], - "title": "Skills/remote/readRequest", + "title": "Skills/remote/listRequest", "type": "object" }, { @@ -779,9 +779,9 @@ }, "method": { "enum": [ - "skills/remote/write" + "skills/remote/export" ], - "title": "Skills/remote/writeRequestMethod", + "title": "Skills/remote/exportRequestMethod", "type": "string" }, "params": { @@ -793,7 +793,7 @@ "method", "params" ], - "title": "Skills/remote/writeRequest", + "title": "Skills/remote/exportRequest", "type": "object" }, { @@ -12169,6 +12169,15 @@ }, "type": "object" }, + "HazelnutScope": { + "enum": [ + "example", + "workspace-shared", + "all-shared", + "personal" + ], + "type": "string" + }, "InputModality": { "description": "Canonical user-input modality tags advertised by a model.", "oneOf": [ @@ -13057,6 +13066,15 @@ ], "type": "string" }, + "ProductSurface": { + "enum": [ + "chatgpt", + "codex", + "api", + "atlas" + ], + "type": "string" + }, "ProfileV2": { "additionalProperties": true, "properties": { @@ -14588,6 +14606,28 @@ }, "SkillsRemoteReadParams": { "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "hazelnutScope": { + "allOf": [ + { + "$ref": "#/definitions/v2/HazelnutScope" + } + ], + "default": "example" + }, + "productSurface": { + "allOf": [ + { + "$ref": "#/definitions/v2/ProductSurface" + } + ], + "default": "codex" + } + }, "title": "SkillsRemoteReadParams", "type": "object" }, @@ -14612,14 +14652,10 @@ "properties": { "hazelnutId": { "type": "string" - }, - "isPreload": { - "type": "boolean" } }, "required": [ - "hazelnutId", - "isPreload" + "hazelnutId" ], "title": "SkillsRemoteWriteParams", "type": "object" @@ -14630,16 +14666,12 @@ "id": { "type": "string" }, - "name": { - "type": "string" - }, "path": { "type": "string" } }, "required": [ "id", - "name", "path" ], "title": "SkillsRemoteWriteResponse", @@ -16793,4 +16825,4 @@ }, "title": "CodexAppServerProtocol", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json index ace2fb5ba..3fdf78f92 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json @@ -1,5 +1,47 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "HazelnutScope": { + "enum": [ + "example", + "workspace-shared", + "all-shared", + "personal" + ], + "type": "string" + }, + "ProductSurface": { + "enum": [ + "chatgpt", + "codex", + "api", + "atlas" + ], + "type": "string" + } + }, + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "hazelnutScope": { + "allOf": [ + { + "$ref": "#/definitions/HazelnutScope" + } + ], + "default": "example" + }, + "productSurface": { + "allOf": [ + { + "$ref": "#/definitions/ProductSurface" + } + ], + "default": "codex" + } + }, "title": "SkillsRemoteReadParams", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json index 871a5a428..f1a70eeeb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json @@ -3,14 +3,10 @@ "properties": { "hazelnutId": { "type": "string" - }, - "isPreload": { - "type": "boolean" } }, "required": [ - "hazelnutId", - "isPreload" + "hazelnutId" ], "title": "SkillsRemoteWriteParams", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json index 1a9473d05..d1e85bc5b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json @@ -4,18 +4,14 @@ "id": { "type": "string" }, - "name": { - "type": "string" - }, "path": { "type": "string" } }, "required": [ "id", - "name", "path" ], "title": "SkillsRemoteWriteResponse", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 3b5f6787b..40757d9d9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -57,4 +57,4 @@ import type { TurnSteerParams } from "./v2/TurnSteerParams"; /** * 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/name/set", id: RequestId, params: ThreadSetNameParams, } | { "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/read", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/write", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "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": "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": "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": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; +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/name/set", id: RequestId, params: ThreadSetNameParams, } | { "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": "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": "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": "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": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts new file mode 100644 index 000000000..e623f1860 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HazelnutScope = "example" | "workspace-shared" | "all-shared" | "personal"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts new file mode 100644 index 000000000..9998c727a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProductSurface = "chatgpt" | "codex" | "api" | "atlas"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts index 9f9178768..1257f0d79 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts @@ -1,5 +1,7 @@ // 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 { HazelnutScope } from "./HazelnutScope"; +import type { ProductSurface } from "./ProductSurface"; -export type SkillsRemoteReadParams = Record; +export type SkillsRemoteReadParams = { hazelnutScope: HazelnutScope, productSurface: ProductSurface, enabled: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts index 857b609ef..ea42595bf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SkillsRemoteWriteParams = { hazelnutId: string, isPreload: boolean, }; +export type SkillsRemoteWriteParams = { hazelnutId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts index cf1665ab9..228b723b1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SkillsRemoteWriteResponse = { id: string, name: string, path: string, }; +export type SkillsRemoteWriteResponse = { id: string, path: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 423ee9dd1..8a1456f42 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -70,6 +70,7 @@ export type { GetAccountParams } from "./GetAccountParams"; export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; export type { GetAccountResponse } from "./GetAccountResponse"; export type { GitInfo } from "./GitInfo"; +export type { HazelnutScope } from "./HazelnutScope"; export type { ItemCompletedNotification } from "./ItemCompletedNotification"; export type { ItemStartedNotification } from "./ItemStartedNotification"; export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams"; @@ -99,6 +100,7 @@ export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; export type { PatchChangeKind } from "./PatchChangeKind"; export type { PlanDeltaNotification } from "./PlanDeltaNotification"; +export type { ProductSurface } from "./ProductSurface"; export type { ProfileV2 } from "./ProfileV2"; export type { RateLimitSnapshot } from "./RateLimitSnapshot"; export type { RateLimitWindow } from "./RateLimitWindow"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index e6b285d9d..88a17390d 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -239,11 +239,11 @@ client_request_definitions! { params: v2::SkillsListParams, response: v2::SkillsListResponse, }, - SkillsRemoteRead => "skills/remote/read" { + SkillsRemoteList => "skills/remote/list" { params: v2::SkillsRemoteReadParams, response: v2::SkillsRemoteReadResponse, }, - SkillsRemoteWrite => "skills/remote/write" { + SkillsRemoteExport => "skills/remote/export" { params: v2::SkillsRemoteWriteParams, response: v2::SkillsRemoteWriteResponse, }, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index fc4a7742f..4d4b15127 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1837,7 +1837,38 @@ pub struct SkillsListResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct SkillsRemoteReadParams {} +pub struct SkillsRemoteReadParams { + #[serde(default)] + pub hazelnut_scope: HazelnutScope, + #[serde(default)] + pub product_surface: ProductSurface, + #[serde(default)] + pub enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case")] +#[ts(export_to = "v2/")] +pub enum HazelnutScope { + #[default] + Example, + WorkspaceShared, + AllShared, + Personal, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(rename_all = "lowercase")] +#[ts(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum ProductSurface { + Chatgpt, + #[default] + Codex, + Api, + Atlas, +} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -1860,7 +1891,6 @@ pub struct SkillsRemoteReadResponse { #[ts(export_to = "v2/")] pub struct SkillsRemoteWriteParams { pub hazelnut_id: String, - pub is_preload: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -1868,7 +1898,6 @@ pub struct SkillsRemoteWriteParams { #[ts(export_to = "v2/")] pub struct SkillsRemoteWriteResponse { pub id: String, - pub name: String, pub path: PathBuf, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 669e8a157..c65793d24 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -135,8 +135,8 @@ 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). - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). -- `skills/remote/read` — list public remote skills (**under development; do not call from production clients yet**). -- `skills/remote/write` — download a public remote skill by `hazelnutId`; `isPreload=true` writes to `.codex/vendor_imports/skills` under `codex_home` (**under development; do not call from production clients yet**). +- `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. - `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. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 0439068db..b966942cb 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -66,6 +66,7 @@ use codex_app_server_protocol::GetUserAgentResponse; use codex_app_server_protocol::GetUserSavedConfigResponse; use codex_app_server_protocol::GitDiffToRemoteResponse; use codex_app_server_protocol::GitInfo as ApiGitInfo; +use codex_app_server_protocol::HazelnutScope as ApiHazelnutScope; use codex_app_server_protocol::InputItem as WireInputItem; use codex_app_server_protocol::InterruptConversationParams; use codex_app_server_protocol::JSONRPCErrorError; @@ -92,6 +93,7 @@ use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; use codex_app_server_protocol::NewConversationParams; use codex_app_server_protocol::NewConversationResponse; +use codex_app_server_protocol::ProductSurface as ApiProductSurface; use codex_app_server_protocol::RemoveConversationListenerParams; use codex_app_server_protocol::RemoveConversationSubscriptionResponse; use codex_app_server_protocol::ResumeConversationParams; @@ -207,7 +209,7 @@ use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; use codex_core::rollout_date_parts; use codex_core::sandboxing::SandboxPermissions; -use codex_core::skills::remote::download_remote_skill; +use codex_core::skills::remote::export_remote_skill; use codex_core::skills::remote::list_remote_skills; use codex_core::state_db::StateDbHandle; use codex_core::state_db::get_state_db; @@ -229,6 +231,8 @@ use codex_protocol::protocol::GitInfo as CoreGitInfo; use codex_protocol::protocol::McpAuthStatus as CoreMcpAuthStatus; use codex_protocol::protocol::McpServerRefreshConfig; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; +use codex_protocol::protocol::RemoteSkillHazelnutScope; +use codex_protocol::protocol::RemoteSkillProductSurface; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::USER_MESSAGE_BEGIN; @@ -291,6 +295,24 @@ enum AppListLoadResult { Directory(Result, String>), } +fn convert_remote_scope(scope: ApiHazelnutScope) -> RemoteSkillHazelnutScope { + match scope { + ApiHazelnutScope::WorkspaceShared => RemoteSkillHazelnutScope::WorkspaceShared, + ApiHazelnutScope::AllShared => RemoteSkillHazelnutScope::AllShared, + ApiHazelnutScope::Personal => RemoteSkillHazelnutScope::Personal, + ApiHazelnutScope::Example => RemoteSkillHazelnutScope::Example, + } +} + +fn convert_remote_product_surface(product_surface: ApiProductSurface) -> RemoteSkillProductSurface { + match product_surface { + ApiProductSurface::Chatgpt => RemoteSkillProductSurface::Chatgpt, + ApiProductSurface::Codex => RemoteSkillProductSurface::Codex, + ApiProductSurface::Api => RemoteSkillProductSurface::Api, + ApiProductSurface::Atlas => RemoteSkillProductSurface::Atlas, + } +} + impl Drop for ActiveLogin { fn drop(&mut self) { self.shutdown_handle.shutdown(); @@ -549,12 +571,12 @@ impl CodexMessageProcessor { self.skills_list(to_connection_request_id(request_id), params) .await; } - ClientRequest::SkillsRemoteRead { request_id, params } => { - self.skills_remote_read(to_connection_request_id(request_id), params) + ClientRequest::SkillsRemoteList { request_id, params } => { + self.skills_remote_list(to_connection_request_id(request_id), params) .await; } - ClientRequest::SkillsRemoteWrite { request_id, params } => { - self.skills_remote_write(to_connection_request_id(request_id), params) + ClientRequest::SkillsRemoteExport { request_id, params } => { + self.skills_remote_export(to_connection_request_id(request_id), params) .await; } ClientRequest::AppsList { request_id, params } => { @@ -5048,12 +5070,25 @@ impl CodexMessageProcessor { .await; } - async fn skills_remote_read( + async fn skills_remote_list( &self, request_id: ConnectionRequestId, - _params: SkillsRemoteReadParams, + params: SkillsRemoteReadParams, ) { - match list_remote_skills(&self.config).await { + let hazelnut_scope = convert_remote_scope(params.hazelnut_scope); + let product_surface = convert_remote_product_surface(params.product_surface); + let enabled = if params.enabled { Some(true) } else { None }; + + let auth = self.auth_manager.auth().await; + match list_remote_skills( + &self.config, + auth.as_ref(), + hazelnut_scope, + product_surface, + enabled, + ) + .await + { Ok(skills) => { let data = skills .into_iter() @@ -5070,23 +5105,21 @@ impl CodexMessageProcessor { Err(err) => { self.send_internal_error( request_id, - format!("failed to read remote skills: {err}"), + format!("failed to list remote skills: {err}"), ) .await; } } } - async fn skills_remote_write( + async fn skills_remote_export( &self, request_id: ConnectionRequestId, params: SkillsRemoteWriteParams, ) { - let SkillsRemoteWriteParams { - hazelnut_id, - is_preload, - } = params; - let response = download_remote_skill(&self.config, hazelnut_id.as_str(), is_preload).await; + let SkillsRemoteWriteParams { hazelnut_id } = params; + let auth = self.auth_manager.auth().await; + let response = export_remote_skill(&self.config, auth.as_ref(), hazelnut_id.as_str()).await; match response { Ok(downloaded) => { @@ -5095,7 +5128,6 @@ impl CodexMessageProcessor { request_id, SkillsRemoteWriteResponse { id: downloaded.id, - name: downloaded.name, path: downloaded.path, }, ) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index f73cef0eb..b96af69f0 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3174,22 +3174,24 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv Op::ListSkills { cwds, force_reload } => { handlers::list_skills(&sess, sub.id.clone(), cwds, force_reload).await; } - Op::ListRemoteSkills => { - handlers::list_remote_skills(&sess, &config, sub.id.clone()).await; - } - Op::DownloadRemoteSkill { - hazelnut_id, - is_preload, + Op::ListRemoteSkills { + hazelnut_scope, + product_surface, + enabled, } => { - handlers::download_remote_skill( + handlers::list_remote_skills( &sess, &config, sub.id.clone(), - hazelnut_id, - is_preload, + hazelnut_scope, + product_surface, + enabled, ) .await; } + Op::DownloadRemoteSkill { hazelnut_id } => { + handlers::export_remote_skill(&sess, &config, sub.id.clone(), hazelnut_id).await; + } Op::Undo => { handlers::undo(&sess, sub.id.clone()).await; } @@ -3269,6 +3271,8 @@ mod handlers { use codex_protocol::protocol::McpServerRefreshConfig; use codex_protocol::protocol::Op; use codex_protocol::protocol::RemoteSkillDownloadedEvent; + use codex_protocol::protocol::RemoteSkillHazelnutScope; + use codex_protocol::protocol::RemoteSkillProductSurface; use codex_protocol::protocol::RemoteSkillSummary; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; @@ -3665,19 +3669,33 @@ mod handlers { sess.send_event_raw(event).await; } - pub async fn list_remote_skills(sess: &Session, config: &Arc, sub_id: String) { - let response = crate::skills::remote::list_remote_skills(config) - .await - .map(|skills| { - skills - .into_iter() - .map(|skill| RemoteSkillSummary { - id: skill.id, - name: skill.name, - description: skill.description, - }) - .collect::>() - }); + pub async fn list_remote_skills( + sess: &Session, + config: &Arc, + sub_id: String, + hazelnut_scope: RemoteSkillHazelnutScope, + product_surface: RemoteSkillProductSurface, + enabled: Option, + ) { + let auth = sess.services.auth_manager.auth().await; + let response = crate::skills::remote::list_remote_skills( + config, + auth.as_ref(), + hazelnut_scope, + product_surface, + enabled, + ) + .await + .map(|skills| { + skills + .into_iter() + .map(|skill| RemoteSkillSummary { + id: skill.id, + name: skill.name, + description: skill.description, + }) + .collect::>() + }); match response { Ok(skills) => { @@ -3702,22 +3720,27 @@ mod handlers { } } - pub async fn download_remote_skill( + pub async fn export_remote_skill( sess: &Session, config: &Arc, sub_id: String, hazelnut_id: String, - is_preload: bool, ) { - match crate::skills::remote::download_remote_skill(config, hazelnut_id.as_str(), is_preload) - .await + let auth = sess.services.auth_manager.auth().await; + match crate::skills::remote::export_remote_skill( + config, + auth.as_ref(), + hazelnut_id.as_str(), + ) + .await { Ok(result) => { + let id = result.id; let event = Event { id: sub_id, msg: EventMsg::RemoteSkillDownloaded(RemoteSkillDownloadedEvent { - id: result.id, - name: result.name, + id: id.clone(), + name: id, path: result.path, }), }; @@ -3727,7 +3750,7 @@ mod handlers { let event = Event { id: sub_id, msg: EventMsg::Error(ErrorEvent { - message: format!("failed to download remote skill {hazelnut_id}: {err}"), + message: format!("failed to export remote skill {hazelnut_id}: {err}"), codex_error_info: Some(CodexErrorInfo::Other), }), }; diff --git a/codex-rs/core/src/skills/remote.rs b/codex-rs/core/src/skills/remote.rs index af6dd5593..6abd40258 100644 --- a/codex-rs/core/src/skills/remote.rs +++ b/codex-rs/core/src/skills/remote.rs @@ -1,18 +1,49 @@ use anyhow::Context; use anyhow::Result; use serde::Deserialize; -use std::collections::HashMap; -use std::collections::HashSet; use std::path::Component; use std::path::Path; use std::path::PathBuf; use std::time::Duration; +use crate::auth::CodexAuth; use crate::config::Config; use crate::default_client::build_reqwest_client; +use codex_protocol::protocol::RemoteSkillHazelnutScope; +use codex_protocol::protocol::RemoteSkillProductSurface; const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30); +fn as_query_hazelnut_scope(scope: RemoteSkillHazelnutScope) -> Option<&'static str> { + match scope { + RemoteSkillHazelnutScope::WorkspaceShared => Some("workspace-shared"), + RemoteSkillHazelnutScope::AllShared => Some("all-shared"), + RemoteSkillHazelnutScope::Personal => Some("personal"), + RemoteSkillHazelnutScope::Example => Some("example"), + } +} + +fn as_query_product_surface(product_surface: RemoteSkillProductSurface) -> &'static str { + match product_surface { + RemoteSkillProductSurface::Chatgpt => "chatgpt", + RemoteSkillProductSurface::Codex => "codex", + RemoteSkillProductSurface::Api => "api", + RemoteSkillProductSurface::Atlas => "atlas", + } +} + +fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth> { + let Some(auth) = auth else { + anyhow::bail!("chatgpt authentication required for hazelnut scopes"); + }; + if !auth.is_chatgpt_auth() { + anyhow::bail!( + "chatgpt authentication required for hazelnut scopes; api key auth is not supported" + ); + } + Ok(auth) +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct RemoteSkillSummary { pub id: String, @@ -20,27 +51,12 @@ pub struct RemoteSkillSummary { pub description: String, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RemoteSkillDownload { - pub id: String, - pub name: String, - pub base_sediment_id: String, - pub files: HashMap, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct RemoteSkillDownloadResult { pub id: String, - pub name: String, pub path: PathBuf, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RemoteSkillFileRange { - pub start: u64, - pub length: u64, -} - #[derive(Debug, Deserialize)] struct RemoteSkillsResponse { hazelnuts: Vec, @@ -53,36 +69,40 @@ struct RemoteSkill { description: String, } -#[derive(Debug, Deserialize)] -struct RemoteSkillsDownloadResponse { - hazelnuts: Vec, -} - -#[derive(Debug, Deserialize)] -struct RemoteSkillDownloadPayload { - id: String, - name: String, - #[serde(rename = "base_sediment_id")] - base_sediment_id: String, - files: HashMap, -} - -#[derive(Debug, Deserialize)] -struct RemoteSkillFileRangePayload { - start: u64, - length: u64, -} - -pub async fn list_remote_skills(config: &Config) -> Result> { +pub async fn list_remote_skills( + config: &Config, + auth: Option<&CodexAuth>, + hazelnut_scope: RemoteSkillHazelnutScope, + product_surface: RemoteSkillProductSurface, + enabled: Option, +) -> Result> { let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url); - let url = format!("{base_url}/public-api/hazelnuts/"); + let auth = ensure_chatgpt_auth(auth)?; + + let url = format!("{base_url}/hazelnuts"); + let product_surface = as_query_product_surface(product_surface); + let mut query_params = vec![("product_surface", product_surface)]; + if let Some(scope) = as_query_hazelnut_scope(hazelnut_scope) { + query_params.push(("scope", scope)); + } + if let Some(enabled) = enabled { + let enabled = if enabled { "true" } else { "false" }; + query_params.push(("enabled", enabled)); + } let client = build_reqwest_client(); - let response = client + let mut request = client .get(&url) .timeout(REMOTE_SKILLS_API_TIMEOUT) - .query(&[("product_surface", "codex")]) + .query(&query_params); + let token = auth + .get_token() + .context("Failed to read auth token for remote skills")?; + request = request.bearer_auth(token); + if let Some(account_id) = auth.get_account_id() { + request = request.header("chatgpt-account-id", account_id); + } + let response = request .send() .await .with_context(|| format!("Failed to send request to {url}"))?; @@ -107,20 +127,27 @@ pub async fn list_remote_skills(config: &Config) -> Result, hazelnut_id: &str, - is_preload: bool, ) -> Result { - let hazelnut = fetch_remote_skill(config, hazelnut_id).await?; + let auth = ensure_chatgpt_auth(auth)?; let client = build_reqwest_client(); let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url); - let url = format!("{base_url}/public-api/hazelnuts/{hazelnut_id}/export"); - let response = client - .get(&url) - .timeout(REMOTE_SKILLS_API_TIMEOUT) + let url = format!("{base_url}/hazelnuts/{hazelnut_id}/export"); + let mut request = client.get(&url).timeout(REMOTE_SKILLS_API_TIMEOUT); + + let token = auth + .get_token() + .context("Failed to read auth token for remote skills")?; + request = request.bearer_auth(token); + if let Some(account_id) = auth.get_account_id() { + request = request.header("chatgpt-account-id", account_id); + } + + let response = request .send() .await .with_context(|| format!("Failed to send download request to {url}"))?; @@ -136,48 +163,22 @@ pub async fn download_remote_skill( anyhow::bail!("Downloaded remote skill payload is not a zip archive"); } - let preferred_dir_name = if hazelnut.name.trim().is_empty() { - None - } else { - Some(hazelnut.name.as_str()) - }; - let dir_name = preferred_dir_name - .and_then(validate_dir_name_format) - .or_else(|| validate_dir_name_format(&hazelnut.id)) - .ok_or_else(|| anyhow::anyhow!("Remote skill has no valid directory name"))?; - let output_root = if is_preload { - config - .codex_home - .join("vendor_imports") - .join("skills") - .join("skills") - .join(".curated") - } else { - config.codex_home.join("skills").join("downloaded") - }; - let output_dir = output_root.join(dir_name); + let output_dir = config.codex_home.join("skills").join(hazelnut_id); tokio::fs::create_dir_all(&output_dir) .await .context("Failed to create downloaded skills directory")?; - let allowed_files = hazelnut.files.keys().cloned().collect::>(); let zip_bytes = body.to_vec(); let output_dir_clone = output_dir.clone(); - let prefix_candidates = vec![hazelnut.name.clone(), hazelnut.id.clone()]; + let prefix_candidates = vec![hazelnut_id.to_string()]; tokio::task::spawn_blocking(move || { - extract_zip_to_dir( - zip_bytes, - &output_dir_clone, - &allowed_files, - &prefix_candidates, - ) + extract_zip_to_dir(zip_bytes, &output_dir_clone, &prefix_candidates) }) .await .context("Zip extraction task failed")??; Ok(RemoteSkillDownloadResult { - id: hazelnut.id, - name: hazelnut.name, + id: hazelnut_id.to_string(), path: output_dir, }) } @@ -195,17 +196,6 @@ fn safe_join(base: &Path, name: &str) -> Result { Ok(base.join(path)) } -fn validate_dir_name_format(name: &str) -> Option { - let mut components = Path::new(name).components(); - match (components.next(), components.next()) { - (Some(Component::Normal(component)), None) => { - let value = component.to_string_lossy().to_string(); - if value.is_empty() { None } else { Some(value) } - } - _ => None, - } -} - fn is_zip_payload(bytes: &[u8]) -> bool { bytes.starts_with(b"PK\x03\x04") || bytes.starts_with(b"PK\x05\x06") @@ -215,7 +205,6 @@ fn is_zip_payload(bytes: &[u8]) -> bool { fn extract_zip_to_dir( bytes: Vec, output_dir: &Path, - allowed_files: &HashSet, prefix_candidates: &[String], ) -> Result<()> { let cursor = std::io::Cursor::new(bytes); @@ -230,9 +219,6 @@ fn extract_zip_to_dir( let Some(normalized) = normalized else { continue; }; - if !allowed_files.contains(&normalized) { - continue; - } let file_path = safe_join(output_dir, &normalized)?; if let Some(parent) = file_path.parent() { std::fs::create_dir_all(parent) @@ -264,51 +250,3 @@ fn normalize_zip_name(name: &str, prefix_candidates: &[String]) -> Option Result { - let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url); - let url = format!("{base_url}/public-api/hazelnuts/"); - - let client = build_reqwest_client(); - let response = client - .get(&url) - .timeout(REMOTE_SKILLS_API_TIMEOUT) - .query(&[("product_surface", "codex")]) - .send() - .await - .with_context(|| format!("Failed to send request to {url}"))?; - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - if !status.is_success() { - anyhow::bail!("Request failed with status {status} from {url}: {body}"); - } - - let parsed: RemoteSkillsDownloadResponse = - serde_json::from_str(&body).context("Failed to parse skills response")?; - let hazelnut = parsed - .hazelnuts - .into_iter() - .find(|hazelnut| hazelnut.id == hazelnut_id) - .ok_or_else(|| anyhow::anyhow!("Remote skill {hazelnut_id} not found"))?; - - Ok(RemoteSkillDownload { - id: hazelnut.id, - name: hazelnut.name, - base_sediment_id: hazelnut.base_sediment_id, - files: hazelnut - .files - .into_iter() - .map(|(name, range)| { - ( - name, - RemoteSkillFileRange { - start: range.start, - length: range.length, - }, - ) - }) - .collect(), - }) -} diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index bf489059b..f9c56c5bc 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -285,13 +285,14 @@ pub enum Op { }, /// Request the list of remote skills available via ChatGPT sharing. - ListRemoteSkills, + ListRemoteSkills { + hazelnut_scope: RemoteSkillHazelnutScope, + product_surface: RemoteSkillProductSurface, + enabled: Option, + }, /// Download a remote skill by id into the local skills cache. - DownloadRemoteSkill { - hazelnut_id: String, - is_preload: bool, - }, + DownloadRemoteSkill { hazelnut_id: String }, /// Request the agent to summarize the current conversation context. /// The agent will use its existing context (either conversation history or previous response id) @@ -2422,6 +2423,26 @@ pub struct RemoteSkillSummary { pub description: String, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case")] +pub enum RemoteSkillHazelnutScope { + WorkspaceShared, + AllShared, + Personal, + Example, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(rename_all = "lowercase")] +pub enum RemoteSkillProductSurface { + Chatgpt, + Codex, + Api, + Atlas, +} + /// Response payload for `Op::ListRemoteSkills`. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ListRemoteSkillsResponseEvent {