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 }
This commit is contained in:
xl-openai 2026-02-17 11:05:22 -08:00 committed by GitHub
parent 48018e9eac
commit 314029ffa3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 402 additions and 243 deletions

View file

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

View file

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

View file

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

View file

@ -3,14 +3,10 @@
"properties": {
"hazelnutId": {
"type": "string"
},
"isPreload": {
"type": "boolean"
}
},
"required": [
"hazelnutId",
"isPreload"
"hazelnutId"
],
"title": "SkillsRemoteWriteParams",
"type": "object"

View file

@ -4,18 +4,14 @@
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"id",
"name",
"path"
],
"title": "SkillsRemoteWriteResponse",
"type": "object"
}
}

View file

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

View file

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

View file

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

View file

@ -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<string, never>;
export type SkillsRemoteReadParams = { hazelnutScope: HazelnutScope, productSurface: ProductSurface, enabled: boolean, };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Vec<AppInfo>, 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,
},
)

View file

@ -3174,22 +3174,24 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, 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<Config>, 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::<Vec<_>>()
});
pub async fn list_remote_skills(
sess: &Session,
config: &Arc<Config>,
sub_id: String,
hazelnut_scope: RemoteSkillHazelnutScope,
product_surface: RemoteSkillProductSurface,
enabled: Option<bool>,
) {
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::<Vec<_>>()
});
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<Config>,
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),
}),
};

View file

@ -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<String, RemoteSkillFileRange>,
}
#[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<RemoteSkill>,
@ -53,36 +69,40 @@ struct RemoteSkill {
description: String,
}
#[derive(Debug, Deserialize)]
struct RemoteSkillsDownloadResponse {
hazelnuts: Vec<RemoteSkillDownloadPayload>,
}
#[derive(Debug, Deserialize)]
struct RemoteSkillDownloadPayload {
id: String,
name: String,
#[serde(rename = "base_sediment_id")]
base_sediment_id: String,
files: HashMap<String, RemoteSkillFileRangePayload>,
}
#[derive(Debug, Deserialize)]
struct RemoteSkillFileRangePayload {
start: u64,
length: u64,
}
pub async fn list_remote_skills(config: &Config) -> Result<Vec<RemoteSkillSummary>> {
pub async fn list_remote_skills(
config: &Config,
auth: Option<&CodexAuth>,
hazelnut_scope: RemoteSkillHazelnutScope,
product_surface: RemoteSkillProductSurface,
enabled: Option<bool>,
) -> Result<Vec<RemoteSkillSummary>> {
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<Vec<RemoteSkillSummar
.collect())
}
pub async fn download_remote_skill(
pub async fn export_remote_skill(
config: &Config,
auth: Option<&CodexAuth>,
hazelnut_id: &str,
is_preload: bool,
) -> Result<RemoteSkillDownloadResult> {
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::<HashSet<String>>();
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<PathBuf> {
Ok(base.join(path))
}
fn validate_dir_name_format(name: &str) -> Option<String> {
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<u8>,
output_dir: &Path,
allowed_files: &HashSet<String>,
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<String
Some(trimmed.to_string())
}
}
async fn fetch_remote_skill(config: &Config, hazelnut_id: &str) -> Result<RemoteSkillDownload> {
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(),
})
}

View file

@ -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<bool>,
},
/// 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 {