From f38d181795bd13a50c0775974f96d9afb3e7276b Mon Sep 17 00:00:00 2001 From: xl-openai Date: Tue, 3 Feb 2026 14:09:37 -0800 Subject: [PATCH] feat: add APIs to list and download public remote skills (#10448) Add API to list / download from remote public skills --- codex-rs/Cargo.lock | 124 +++++++ codex-rs/Cargo.toml | 1 + .../schema/json/ClientRequest.json | 66 ++++ .../schema/json/EventMsg.json | 125 +++++++ .../schema/json/ServerNotification.json | 72 ++++ .../codex_app_server_protocol.schemas.json | 198 +++++++++++ .../json/v1/ForkConversationResponse.json | 72 ++++ .../json/v1/ResumeConversationResponse.json | 72 ++++ .../v1/SessionConfiguredNotification.json | 72 ++++ .../json/v2/SkillsRemoteReadParams.json | 5 + .../json/v2/SkillsRemoteReadResponse.json | 37 +++ .../json/v2/SkillsRemoteWriteParams.json | 17 + .../json/v2/SkillsRemoteWriteResponse.json | 21 ++ .../schema/typescript/ClientRequest.ts | 4 +- .../schema/typescript/EventMsg.ts | 4 +- .../ListRemoteSkillsResponseEvent.ts | 9 + .../typescript/RemoteSkillDownloadedEvent.ts | 8 + .../schema/typescript/RemoteSkillSummary.ts | 5 + .../schema/typescript/index.ts | 3 + .../typescript/v2/RemoteSkillSummary.ts | 5 + .../typescript/v2/SkillsRemoteReadParams.ts | 5 + .../typescript/v2/SkillsRemoteReadResponse.ts | 6 + .../typescript/v2/SkillsRemoteWriteParams.ts | 5 + .../v2/SkillsRemoteWriteResponse.ts | 5 + .../schema/typescript/v2/index.ts | 5 + .../src/protocol/common.rs | 8 + .../app-server-protocol/src/protocol/v2.rs | 38 +++ codex-rs/app-server/README.md | 2 + .../app-server/src/codex_message_processor.rs | 67 ++++ codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/codex.rs | 90 +++++ codex-rs/core/src/rollout/policy.rs | 2 + codex-rs/core/src/skills/mod.rs | 1 + codex-rs/core/src/skills/remote.rs | 314 ++++++++++++++++++ .../src/event_processor_with_human_output.rs | 2 + codex-rs/mcp-server/src/codex_tool_runner.rs | 2 + codex-rs/protocol/src/protocol.rs | 36 ++ codex-rs/tui/src/chatwidget.rs | 1 + 38 files changed, 1508 insertions(+), 2 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadResponse.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/ListRemoteSkillsResponseEvent.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/RemoteSkillDownloadedEvent.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/RemoteSkillSummary.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RemoteSkillSummary.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts create mode 100644 codex-rs/core/src/skills/remote.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 259636152..3d9daeb7e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -398,6 +398,15 @@ dependencies = [ "wiremock", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arboard" version = "3.6.1" @@ -924,6 +933,25 @@ dependencies = [ "bytes", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -1578,6 +1606,7 @@ dependencies = [ "which", "wildmatch", "wiremock", + "zip", "zstd", ] @@ -2353,6 +2382,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.6.0" @@ -2771,6 +2806,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + [[package]] name = "der" version = "0.7.10" @@ -2803,6 +2844,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -4929,6 +4981,27 @@ dependencies = [ "url", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "maplit" version = "1.0.2" @@ -10515,6 +10588,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yansi" version = "1.0.1" @@ -10701,12 +10783,54 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.13.0", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.18", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + [[package]] name = "zmij" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 22f53644f..2c92f0b46 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -249,6 +249,7 @@ walkdir = "2.5.0" webbrowser = "1.0" which = "8" wildmatch = "2.6.1" +zip = "2.4.2" wiremock = "0.6" zeroize = "1.8.2" diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 346157277..39faeb137 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2183,6 +2183,24 @@ }, "type": "object" }, + "SkillsRemoteReadParams": { + "type": "object" + }, + "SkillsRemoteWriteParams": { + "properties": { + "hazelnutId": { + "type": "string" + }, + "isPreload": { + "type": "boolean" + } + }, + "required": [ + "hazelnutId", + "isPreload" + ], + "type": "object" + }, "TextElement": { "properties": { "byteRange": { @@ -3312,6 +3330,54 @@ "title": "Skills/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/remote/read" + ], + "title": "Skills/remote/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsRemoteReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/remote/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/remote/write" + ], + "title": "Skills/remote/writeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsRemoteWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/remote/writeRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 1274e1cd7..ab556a77c 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -2012,6 +2012,59 @@ "title": "ListSkillsResponseEventMsg", "type": "object" }, + { + "description": "List of remote skills available to the agent.", + "properties": { + "skills": { + "items": { + "$ref": "#/definitions/RemoteSkillSummary" + }, + "type": "array" + }, + "type": { + "enum": [ + "list_remote_skills_response" + ], + "title": "ListRemoteSkillsResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "skills", + "type" + ], + "title": "ListRemoteSkillsResponseEventMsg", + "type": "object" + }, + { + "description": "Remote skill downloaded to local cache.", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "remote_skill_downloaded" + ], + "title": "RemoteSkillDownloadedEventMsgType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "title": "RemoteSkillDownloadedEventMsg", + "type": "object" + }, { "description": "Notification that skill data may have been updated and clients may want to reload.", "properties": { @@ -3429,6 +3482,25 @@ } ] }, + "RemoteSkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + }, "RequestId": { "anyOf": [ { @@ -6536,6 +6608,59 @@ "title": "ListSkillsResponseEventMsg", "type": "object" }, + { + "description": "List of remote skills available to the agent.", + "properties": { + "skills": { + "items": { + "$ref": "#/definitions/RemoteSkillSummary" + }, + "type": "array" + }, + "type": { + "enum": [ + "list_remote_skills_response" + ], + "title": "ListRemoteSkillsResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "skills", + "type" + ], + "title": "ListRemoteSkillsResponseEventMsg", + "type": "object" + }, + { + "description": "Remote skill downloaded to local cache.", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "remote_skill_downloaded" + ], + "title": "RemoteSkillDownloadedEventMsgType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "title": "RemoteSkillDownloadedEventMsg", + "type": "object" + }, { "description": "Notification that skill data may have been updated and clients may want to reload.", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index f89bcf24e..f6d10c11c 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2590,6 +2590,59 @@ "title": "ListSkillsResponseEventMsg", "type": "object" }, + { + "description": "List of remote skills available to the agent.", + "properties": { + "skills": { + "items": { + "$ref": "#/definitions/RemoteSkillSummary" + }, + "type": "array" + }, + "type": { + "enum": [ + "list_remote_skills_response" + ], + "title": "ListRemoteSkillsResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "skills", + "type" + ], + "title": "ListRemoteSkillsResponseEventMsg", + "type": "object" + }, + { + "description": "Remote skill downloaded to local cache.", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "remote_skill_downloaded" + ], + "title": "RemoteSkillDownloadedEventMsgType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "title": "RemoteSkillDownloadedEventMsg", + "type": "object" + }, { "description": "Notification that skill data may have been updated and clients may want to reload.", "properties": { @@ -4469,6 +4522,25 @@ ], "type": "object" }, + "RemoteSkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + }, "RequestId": { "anyOf": [ { 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 bc72ceb21..83a7c781b 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 @@ -718,6 +718,54 @@ "title": "Skills/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/remote/read" + ], + "title": "Skills/remote/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/SkillsRemoteReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/remote/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/remote/write" + ], + "title": "Skills/remote/writeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/SkillsRemoteWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/remote/writeRequest", + "type": "object" + }, { "properties": { "id": { @@ -3852,6 +3900,59 @@ "title": "ListSkillsResponseEventMsg", "type": "object" }, + { + "description": "List of remote skills available to the agent.", + "properties": { + "skills": { + "items": { + "$ref": "#/definitions/v2/RemoteSkillSummary" + }, + "type": "array" + }, + "type": { + "enum": [ + "list_remote_skills_response" + ], + "title": "ListRemoteSkillsResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "skills", + "type" + ], + "title": "ListRemoteSkillsResponseEventMsg", + "type": "object" + }, + { + "description": "Remote skill downloaded to local cache.", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "remote_skill_downloaded" + ], + "title": "RemoteSkillDownloadedEventMsgType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "title": "RemoteSkillDownloadedEventMsg", + "type": "object" + }, { "description": "Notification that skill data may have been updated and clients may want to reload.", "properties": { @@ -6351,6 +6452,25 @@ } ] }, + "RemoteSkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + }, "RemoveConversationListenerParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -12382,6 +12502,25 @@ "title": "ReasoningTextDeltaNotification", "type": "object" }, + "RemoteSkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + }, "ResidencyRequirement": { "enum": [ "us" @@ -13417,6 +13556,65 @@ "title": "SkillsListResponse", "type": "object" }, + "SkillsRemoteReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsRemoteReadParams", + "type": "object" + }, + "SkillsRemoteReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/v2/RemoteSkillSummary" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "SkillsRemoteReadResponse", + "type": "object" + }, + "SkillsRemoteWriteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "hazelnutId": { + "type": "string" + }, + "isPreload": { + "type": "boolean" + } + }, + "required": [ + "hazelnutId", + "isPreload" + ], + "title": "SkillsRemoteWriteParams", + "type": "object" + }, + "SkillsRemoteWriteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "path" + ], + "title": "SkillsRemoteWriteResponse", + "type": "object" + }, "SubAgentSource": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index 9894c47c1..215c80b28 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -2012,6 +2012,59 @@ "title": "ListSkillsResponseEventMsg", "type": "object" }, + { + "description": "List of remote skills available to the agent.", + "properties": { + "skills": { + "items": { + "$ref": "#/definitions/RemoteSkillSummary" + }, + "type": "array" + }, + "type": { + "enum": [ + "list_remote_skills_response" + ], + "title": "ListRemoteSkillsResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "skills", + "type" + ], + "title": "ListRemoteSkillsResponseEventMsg", + "type": "object" + }, + { + "description": "Remote skill downloaded to local cache.", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "remote_skill_downloaded" + ], + "title": "RemoteSkillDownloadedEventMsgType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "title": "RemoteSkillDownloadedEventMsg", + "type": "object" + }, { "description": "Notification that skill data may have been updated and clients may want to reload.", "properties": { @@ -3429,6 +3482,25 @@ } ] }, + "RemoteSkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + }, "RequestId": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index fe4037a40..bd6d80b6e 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -2012,6 +2012,59 @@ "title": "ListSkillsResponseEventMsg", "type": "object" }, + { + "description": "List of remote skills available to the agent.", + "properties": { + "skills": { + "items": { + "$ref": "#/definitions/RemoteSkillSummary" + }, + "type": "array" + }, + "type": { + "enum": [ + "list_remote_skills_response" + ], + "title": "ListRemoteSkillsResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "skills", + "type" + ], + "title": "ListRemoteSkillsResponseEventMsg", + "type": "object" + }, + { + "description": "Remote skill downloaded to local cache.", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "remote_skill_downloaded" + ], + "title": "RemoteSkillDownloadedEventMsgType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "title": "RemoteSkillDownloadedEventMsg", + "type": "object" + }, { "description": "Notification that skill data may have been updated and clients may want to reload.", "properties": { @@ -3429,6 +3482,25 @@ } ] }, + "RemoteSkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + }, "RequestId": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index 8e4c36197..a61d4141a 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -2012,6 +2012,59 @@ "title": "ListSkillsResponseEventMsg", "type": "object" }, + { + "description": "List of remote skills available to the agent.", + "properties": { + "skills": { + "items": { + "$ref": "#/definitions/RemoteSkillSummary" + }, + "type": "array" + }, + "type": { + "enum": [ + "list_remote_skills_response" + ], + "title": "ListRemoteSkillsResponseEventMsgType", + "type": "string" + } + }, + "required": [ + "skills", + "type" + ], + "title": "ListRemoteSkillsResponseEventMsg", + "type": "object" + }, + { + "description": "Remote skill downloaded to local cache.", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "remote_skill_downloaded" + ], + "title": "RemoteSkillDownloadedEventMsgType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "path", + "type" + ], + "title": "RemoteSkillDownloadedEventMsg", + "type": "object" + }, { "description": "Notification that skill data may have been updated and clients may want to reload.", "properties": { @@ -3429,6 +3482,25 @@ } ] }, + "RemoteSkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + }, "RequestId": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json new file mode 100644 index 000000000..ace2fb5ba --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsRemoteReadParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadResponse.json new file mode 100644 index 000000000..a8e19c65b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadResponse.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "RemoteSkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + } + }, + "properties": { + "data": { + "items": { + "$ref": "#/definitions/RemoteSkillSummary" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "SkillsRemoteReadResponse", + "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 new file mode 100644 index 000000000..871a5a428 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "hazelnutId": { + "type": "string" + }, + "isPreload": { + "type": "boolean" + } + }, + "required": [ + "hazelnutId", + "isPreload" + ], + "title": "SkillsRemoteWriteParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json new file mode 100644 index 000000000..1a9473d05 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "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 b65d9ee93..d526e5c5e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -36,6 +36,8 @@ import type { ModelListParams } from "./v2/ModelListParams"; import type { ReviewStartParams } from "./v2/ReviewStartParams"; import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; import type { SkillsListParams } from "./v2/SkillsListParams"; +import type { SkillsRemoteReadParams } from "./v2/SkillsRemoteReadParams"; +import type { SkillsRemoteWriteParams } from "./v2/SkillsRemoteWriteParams"; import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams"; import type { ThreadForkParams } from "./v2/ThreadForkParams"; import type { ThreadListParams } from "./v2/ThreadListParams"; @@ -52,4 +54,4 @@ import type { TurnStartParams } from "./v2/TurnStartParams"; /** * 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/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": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "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/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/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "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/EventMsg.ts b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts index 5f158de2c..c18088eaa 100644 --- a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts +++ b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts @@ -33,6 +33,7 @@ import type { GetHistoryEntryResponseEvent } from "./GetHistoryEntryResponseEven import type { ItemCompletedEvent } from "./ItemCompletedEvent"; import type { ItemStartedEvent } from "./ItemStartedEvent"; import type { ListCustomPromptsResponseEvent } from "./ListCustomPromptsResponseEvent"; +import type { ListRemoteSkillsResponseEvent } from "./ListRemoteSkillsResponseEvent"; import type { ListSkillsResponseEvent } from "./ListSkillsResponseEvent"; import type { McpListToolsResponseEvent } from "./McpListToolsResponseEvent"; import type { McpStartupCompleteEvent } from "./McpStartupCompleteEvent"; @@ -45,6 +46,7 @@ import type { PlanDeltaEvent } from "./PlanDeltaEvent"; import type { RawResponseItemEvent } from "./RawResponseItemEvent"; import type { ReasoningContentDeltaEvent } from "./ReasoningContentDeltaEvent"; import type { ReasoningRawContentDeltaEvent } from "./ReasoningRawContentDeltaEvent"; +import type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent"; import type { RequestUserInputEvent } from "./RequestUserInputEvent"; import type { ReviewRequest } from "./ReviewRequest"; import type { SessionConfiguredEvent } from "./SessionConfiguredEvent"; @@ -70,4 +72,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent"; * Response event from the agent * NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen. */ -export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent; +export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent; diff --git a/codex-rs/app-server-protocol/schema/typescript/ListRemoteSkillsResponseEvent.ts b/codex-rs/app-server-protocol/schema/typescript/ListRemoteSkillsResponseEvent.ts new file mode 100644 index 000000000..e3b277f4d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/ListRemoteSkillsResponseEvent.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RemoteSkillSummary } from "./RemoteSkillSummary"; + +/** + * Response payload for `Op::ListRemoteSkills`. + */ +export type ListRemoteSkillsResponseEvent = { skills: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/RemoteSkillDownloadedEvent.ts b/codex-rs/app-server-protocol/schema/typescript/RemoteSkillDownloadedEvent.ts new file mode 100644 index 000000000..83082f2a5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/RemoteSkillDownloadedEvent.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * Response payload for `Op::DownloadRemoteSkill`. + */ +export type RemoteSkillDownloadedEvent = { id: string, name: string, path: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/RemoteSkillSummary.ts b/codex-rs/app-server-protocol/schema/typescript/RemoteSkillSummary.ts new file mode 100644 index 000000000..7bf57b3b0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/RemoteSkillSummary.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 RemoteSkillSummary = { id: string, name: string, description: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 91da0708e..7d3ecb818 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -98,6 +98,7 @@ export type { ItemStartedEvent } from "./ItemStartedEvent"; export type { ListConversationsParams } from "./ListConversationsParams"; export type { ListConversationsResponse } from "./ListConversationsResponse"; export type { ListCustomPromptsResponseEvent } from "./ListCustomPromptsResponseEvent"; +export type { ListRemoteSkillsResponseEvent } from "./ListRemoteSkillsResponseEvent"; export type { ListSkillsResponseEvent } from "./ListSkillsResponseEvent"; export type { LocalShellAction } from "./LocalShellAction"; export type { LocalShellExecAction } from "./LocalShellExecAction"; @@ -140,6 +141,8 @@ export type { ReasoningItemContent } from "./ReasoningItemContent"; export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary"; export type { ReasoningRawContentDeltaEvent } from "./ReasoningRawContentDeltaEvent"; export type { ReasoningSummary } from "./ReasoningSummary"; +export type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent"; +export type { RemoteSkillSummary } from "./RemoteSkillSummary"; export type { RemoveConversationListenerParams } from "./RemoveConversationListenerParams"; export type { RemoveConversationSubscriptionResponse } from "./RemoveConversationSubscriptionResponse"; export type { RequestId } from "./RequestId"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RemoteSkillSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RemoteSkillSummary.ts new file mode 100644 index 000000000..7bf57b3b0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RemoteSkillSummary.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 RemoteSkillSummary = { id: string, name: string, description: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts new file mode 100644 index 000000000..9f9178768 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.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 SkillsRemoteReadParams = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadResponse.ts new file mode 100644 index 000000000..c1c7b1cc7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RemoteSkillSummary } from "./RemoteSkillSummary"; + +export type SkillsRemoteReadResponse = { data: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts new file mode 100644 index 000000000..857b609ef --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.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 SkillsRemoteWriteParams = { hazelnutId: string, isPreload: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts new file mode 100644 index 000000000..cf1665ab9 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.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 SkillsRemoteWriteResponse = { id: string, name: 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 0c50231a8..83357ff5c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -96,6 +96,7 @@ export type { ReasoningEffortOption } from "./ReasoningEffortOption"; export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification"; export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification"; export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification"; +export type { RemoteSkillSummary } from "./RemoteSkillSummary"; export type { ResidencyRequirement } from "./ResidencyRequirement"; export type { ReviewDelivery } from "./ReviewDelivery"; export type { ReviewStartParams } from "./ReviewStartParams"; @@ -116,6 +117,10 @@ export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse"; export type { SkillsListEntry } from "./SkillsListEntry"; export type { SkillsListParams } from "./SkillsListParams"; export type { SkillsListResponse } from "./SkillsListResponse"; +export type { SkillsRemoteReadParams } from "./SkillsRemoteReadParams"; +export type { SkillsRemoteReadResponse } from "./SkillsRemoteReadResponse"; +export type { SkillsRemoteWriteParams } from "./SkillsRemoteWriteParams"; +export type { SkillsRemoteWriteResponse } from "./SkillsRemoteWriteResponse"; export type { TerminalInteractionNotification } from "./TerminalInteractionNotification"; export type { TextElement } from "./TextElement"; export type { TextPosition } from "./TextPosition"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index d20f23a88..bb2510684 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -228,6 +228,14 @@ client_request_definitions! { params: v2::SkillsListParams, response: v2::SkillsListResponse, }, + SkillsRemoteRead => "skills/remote/read" { + params: v2::SkillsRemoteReadParams, + response: v2::SkillsRemoteReadResponse, + }, + SkillsRemoteWrite => "skills/remote/write" { + params: v2::SkillsRemoteWriteParams, + response: v2::SkillsRemoteWriteResponse, + }, AppsList => "app/list" { params: v2::AppsListParams, response: v2::AppsListResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 7ce8cc2ed..67e7013a3 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1554,6 +1554,44 @@ pub struct SkillsListResponse { pub data: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsRemoteReadParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RemoteSkillSummary { + pub id: String, + pub name: String, + pub description: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsRemoteReadResponse { + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsRemoteWriteParams { + pub hazelnut_id: String, + pub is_preload: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsRemoteWriteResponse { + pub id: String, + pub name: String, + pub path: PathBuf, +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 1d5b6d15f..d6fae4846 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -93,6 +93,8 @@ Example (from OpenAI's official VSCode extension): - `model/list` — list available models (with reasoning effort options). - `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**). - `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 2a1c53405..866998a4f 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -97,6 +97,10 @@ use codex_app_server_protocol::SkillsConfigWriteParams; use codex_app_server_protocol::SkillsConfigWriteResponse; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; +use codex_app_server_protocol::SkillsRemoteReadParams; +use codex_app_server_protocol::SkillsRemoteReadResponse; +use codex_app_server_protocol::SkillsRemoteWriteParams; +use codex_app_server_protocol::SkillsRemoteWriteResponse; use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadArchiveParams; use codex_app_server_protocol::ThreadArchiveResponse; @@ -176,6 +180,8 @@ 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::list_remote_skills; use codex_core::state_db::get_state_db; use codex_core::token_data::parse_id_token; use codex_core::windows_sandbox::WindowsSandboxLevelExt; @@ -464,6 +470,12 @@ impl CodexMessageProcessor { ClientRequest::SkillsList { request_id, params } => { self.skills_list(request_id, params).await; } + ClientRequest::SkillsRemoteRead { request_id, params } => { + self.skills_remote_read(request_id, params).await; + } + ClientRequest::SkillsRemoteWrite { request_id, params } => { + self.skills_remote_write(request_id, params).await; + } ClientRequest::AppsList { request_id, params } => { self.apps_list(request_id, params).await; } @@ -4053,6 +4065,61 @@ impl CodexMessageProcessor { .await; } + async fn skills_remote_read(&self, request_id: RequestId, _params: SkillsRemoteReadParams) { + match list_remote_skills(&self.config).await { + Ok(skills) => { + let data = skills + .into_iter() + .map(|skill| codex_app_server_protocol::RemoteSkillSummary { + id: skill.id, + name: skill.name, + description: skill.description, + }) + .collect(); + self.outgoing + .send_response(request_id, SkillsRemoteReadResponse { data }) + .await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to read remote skills: {err}"), + ) + .await; + } + } + } + + async fn skills_remote_write(&self, request_id: RequestId, params: SkillsRemoteWriteParams) { + let SkillsRemoteWriteParams { + hazelnut_id, + is_preload, + } = params; + let response = download_remote_skill(&self.config, hazelnut_id.as_str(), is_preload).await; + + match response { + Ok(downloaded) => { + self.outgoing + .send_response( + request_id, + SkillsRemoteWriteResponse { + id: downloaded.id, + name: downloaded.name, + path: downloaded.path, + }, + ) + .await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to download remote skill: {err}"), + ) + .await; + } + } + } + async fn skills_config_write(&self, request_id: RequestId, params: SkillsConfigWriteParams) { let SkillsConfigWriteParams { path, enabled } = params; let edits = vec![ConfigEdit::SetSkillConfig { path, enabled }]; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 38da38b92..f3ba5fd2c 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -107,6 +107,7 @@ url = { workspace = true } uuid = { workspace = true, features = ["serde", "v4", "v5"] } which = { workspace = true } wildmatch = { workspace = true } +zip = { workspace = true } [features] deterministic_process_ids = [] diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ba34af5ed..b7c040405 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2469,6 +2469,22 @@ 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, + } => { + handlers::download_remote_skill( + &sess, + &config, + sub.id.clone(), + hazelnut_id, + is_preload, + ) + .await; + } Op::Undo => { handlers::undo(&sess, sub.id.clone()).await; } @@ -2535,9 +2551,12 @@ mod handlers { use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ListCustomPromptsResponseEvent; + use codex_protocol::protocol::ListRemoteSkillsResponseEvent; use codex_protocol::protocol::ListSkillsResponseEvent; use codex_protocol::protocol::McpServerRefreshConfig; use codex_protocol::protocol::Op; + use codex_protocol::protocol::RemoteSkillDownloadedEvent; + use codex_protocol::protocol::RemoteSkillSummary; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::SkillsListEntry; @@ -2895,6 +2914,77 @@ 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::>() + }); + + match response { + Ok(skills) => { + let event = Event { + id: sub_id, + msg: EventMsg::ListRemoteSkillsResponse(ListRemoteSkillsResponseEvent { + skills, + }), + }; + sess.send_event_raw(event).await; + } + Err(err) => { + let event = Event { + id: sub_id, + msg: EventMsg::Error(ErrorEvent { + message: format!("failed to list remote skills: {err}"), + codex_error_info: Some(CodexErrorInfo::Other), + }), + }; + sess.send_event_raw(event).await; + } + } + } + + pub async fn download_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 + { + Ok(result) => { + let event = Event { + id: sub_id, + msg: EventMsg::RemoteSkillDownloaded(RemoteSkillDownloadedEvent { + id: result.id, + name: result.name, + path: result.path, + }), + }; + sess.send_event_raw(event).await; + } + Err(err) => { + let event = Event { + id: sub_id, + msg: EventMsg::Error(ErrorEvent { + message: format!("failed to download remote skill {hazelnut_id}: {err}"), + codex_error_info: Some(CodexErrorInfo::Other), + }), + }; + sess.send_event_raw(event).await; + } + } + } + pub async fn undo(sess: &Arc, sub_id: String) { let turn_context = sess.new_default_turn_with_sub_id(sub_id).await; sess.spawn_task(turn_context, Vec::new(), UndoTask::new()) diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 5796bd196..587b68a91 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -90,6 +90,8 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::McpStartupComplete(_) | EventMsg::ListCustomPromptsResponse(_) | EventMsg::ListSkillsResponse(_) + | EventMsg::ListRemoteSkillsResponse(_) + | EventMsg::RemoteSkillDownloaded(_) | EventMsg::PlanUpdate(_) | EventMsg::ShutdownComplete | EventMsg::ViewImageToolCall(_) diff --git a/codex-rs/core/src/skills/mod.rs b/codex-rs/core/src/skills/mod.rs index 4d79bca25..6148092a1 100644 --- a/codex-rs/core/src/skills/mod.rs +++ b/codex-rs/core/src/skills/mod.rs @@ -3,6 +3,7 @@ pub mod injection; pub mod loader; pub mod manager; pub mod model; +pub mod remote; pub mod render; pub mod system; diff --git a/codex-rs/core/src/skills/remote.rs b/codex-rs/core/src/skills/remote.rs new file mode 100644 index 000000000..af6dd5593 --- /dev/null +++ b/codex-rs/core/src/skills/remote.rs @@ -0,0 +1,314 @@ +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::config::Config; +use crate::default_client::build_reqwest_client; + +const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RemoteSkillSummary { + pub id: String, + pub name: String, + 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, +} + +#[derive(Debug, Deserialize)] +struct RemoteSkill { + id: String, + name: String, + 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> { + 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: RemoteSkillsResponse = + serde_json::from_str(&body).context("Failed to parse skills response")?; + + Ok(parsed + .hazelnuts + .into_iter() + .map(|skill| RemoteSkillSummary { + id: skill.id, + name: skill.name, + description: skill.description, + }) + .collect()) +} + +pub async fn download_remote_skill( + config: &Config, + hazelnut_id: &str, + is_preload: bool, +) -> Result { + let hazelnut = fetch_remote_skill(config, hazelnut_id).await?; + + 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) + .send() + .await + .with_context(|| format!("Failed to send download request to {url}"))?; + + let status = response.status(); + let body = response.bytes().await.context("Failed to read download")?; + if !status.is_success() { + let body_text = String::from_utf8_lossy(&body); + anyhow::bail!("Download failed with status {status} from {url}: {body_text}"); + } + + if !is_zip_payload(&body) { + 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); + 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()]; + tokio::task::spawn_blocking(move || { + extract_zip_to_dir( + zip_bytes, + &output_dir_clone, + &allowed_files, + &prefix_candidates, + ) + }) + .await + .context("Zip extraction task failed")??; + + Ok(RemoteSkillDownloadResult { + id: hazelnut.id, + name: hazelnut.name, + path: output_dir, + }) +} + +fn safe_join(base: &Path, name: &str) -> Result { + let path = Path::new(name); + for component in path.components() { + match component { + Component::Normal(_) => {} + _ => { + anyhow::bail!("Invalid file path in remote skill payload: {name}"); + } + } + } + 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") + || bytes.starts_with(b"PK\x07\x08") +} + +fn extract_zip_to_dir( + bytes: Vec, + output_dir: &Path, + allowed_files: &HashSet, + prefix_candidates: &[String], +) -> Result<()> { + let cursor = std::io::Cursor::new(bytes); + let mut archive = zip::ZipArchive::new(cursor).context("Failed to open zip archive")?; + for i in 0..archive.len() { + let mut file = archive.by_index(i).context("Failed to read zip entry")?; + if file.is_dir() { + continue; + } + let raw_name = file.name().to_string(); + let normalized = normalize_zip_name(&raw_name, prefix_candidates); + 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) + .with_context(|| format!("Failed to create parent dir for {normalized}"))?; + } + let mut out = std::fs::File::create(&file_path) + .with_context(|| format!("Failed to create file {normalized}"))?; + std::io::copy(&mut file, &mut out) + .with_context(|| format!("Failed to write skill file {normalized}"))?; + } + Ok(()) +} + +fn normalize_zip_name(name: &str, prefix_candidates: &[String]) -> Option { + let mut trimmed = name.trim_start_matches("./"); + for prefix in prefix_candidates { + if prefix.is_empty() { + continue; + } + let prefix = format!("{prefix}/"); + if let Some(rest) = trimmed.strip_prefix(&prefix) { + trimmed = rest; + break; + } + } + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } +} + +async fn fetch_remote_skill(config: &Config, hazelnut_id: &str) -> 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/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 3b4a3bf86..9175b100a 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -773,6 +773,8 @@ impl EventProcessor for EventProcessorWithHumanOutput { | EventMsg::McpListToolsResponse(_) | EventMsg::ListCustomPromptsResponse(_) | EventMsg::ListSkillsResponse(_) + | EventMsg::ListRemoteSkillsResponse(_) + | EventMsg::RemoteSkillDownloaded(_) | EventMsg::RawResponseItem(_) | EventMsg::UserMessage(_) | EventMsg::EnteredReviewMode(_) diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 6273607a2..6513c9e6f 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -322,6 +322,8 @@ async fn run_codex_tool_session_inner( | EventMsg::McpListToolsResponse(_) | EventMsg::ListCustomPromptsResponse(_) | EventMsg::ListSkillsResponse(_) + | EventMsg::ListRemoteSkillsResponse(_) + | EventMsg::RemoteSkillDownloaded(_) | EventMsg::ExecCommandBegin(_) | EventMsg::TerminalInteraction(_) | EventMsg::ExecCommandOutputDelta(_) diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 140d3931e..078ea31d1 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -269,6 +269,15 @@ pub enum Op { force_reload: bool, }, + /// Request the list of remote skills available via ChatGPT sharing. + ListRemoteSkills, + + /// Download a remote skill by id into the local skills cache. + DownloadRemoteSkill { + hazelnut_id: String, + is_preload: bool, + }, + /// Request the agent to summarize the current conversation context. /// The agent will use its existing context (either conversation history or previous response id) /// to generate a summary which will be returned as an AgentMessage event. @@ -822,6 +831,12 @@ pub enum EventMsg { /// List of skills available to the agent. ListSkillsResponse(ListSkillsResponseEvent), + /// List of remote skills available to the agent. + ListRemoteSkillsResponse(ListRemoteSkillsResponseEvent), + + /// Remote skill downloaded to local cache. + RemoteSkillDownloaded(RemoteSkillDownloadedEvent), + /// Notification that skill data may have been updated and clients may want to reload. SkillsUpdateAvailable, @@ -2127,6 +2142,27 @@ pub struct ListSkillsResponseEvent { pub skills: Vec, } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +pub struct RemoteSkillSummary { + pub id: String, + pub name: String, + pub description: String, +} + +/// Response payload for `Op::ListRemoteSkills`. +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +pub struct ListRemoteSkillsResponseEvent { + pub skills: Vec, +} + +/// Response payload for `Op::DownloadRemoteSkill`. +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +pub struct RemoteSkillDownloadedEvent { + pub id: String, + pub name: String, + pub path: PathBuf, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a952b3354..40247c35a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3526,6 +3526,7 @@ impl ChatWidget { EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev), EventMsg::ListCustomPromptsResponse(ev) => self.on_list_custom_prompts(ev), EventMsg::ListSkillsResponse(ev) => self.on_list_skills(ev), + EventMsg::ListRemoteSkillsResponse(_) | EventMsg::RemoteSkillDownloaded(_) => {} EventMsg::SkillsUpdateAvailable => { self.submit_op(Op::ListSkills { cwds: Vec::new(),