Add remote plugin skill read API (#20150)

## Summary

Adds an app-server `plugin/skill/read` method for remote plugin skill
markdown. The new method calls the plugin-service skill detail endpoint
and returns `skill_md_contents`, so clients can preview skills for
remote plugins before the bundle is installed locally.

## Why

Uninstalled remote plugin skills do not have local `SKILL.md` files.
Without an on-demand remote read, the desktop plugin details UI cannot
render the skill details modal for those skills.

## Validation

- `just write-app-server-schema`
- `just fmt`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-app-server --test all --
suite::v2::plugin_read::plugin_skill_read_reads_remote_skill_contents_when_remote_plugin_enabled
--exact`
- `just fix -p codex-app-server-protocol -p codex-core-plugins -p
codex-app-server`
This commit is contained in:
xli-oai
2026-05-01 00:16:25 -07:00
committed by GitHub
parent a62b52f826
commit 96d2ea9058
21 changed files with 1212 additions and 529 deletions

View File

@@ -2217,6 +2217,25 @@
],
"type": "object"
},
"PluginSkillReadParams": {
"properties": {
"remoteMarketplaceName": {
"type": "string"
},
"remotePluginId": {
"type": "string"
},
"skillName": {
"type": "string"
}
},
"required": [
"remoteMarketplaceName",
"remotePluginId",
"skillName"
],
"type": "object"
},
"PluginUninstallParams": {
"properties": {
"pluginId": {
@@ -5036,6 +5055,30 @@
"title": "Plugin/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"plugin/skill/read"
],
"title": "Plugin/skill/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/PluginSkillReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Plugin/skill/readRequest",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -762,6 +762,30 @@
"title": "Plugin/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"plugin/skill/read"
],
"title": "Plugin/skill/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/PluginSkillReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Plugin/skill/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -12391,6 +12415,40 @@
"title": "PluginShareSaveResponse",
"type": "object"
},
"PluginSkillReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"remoteMarketplaceName": {
"type": "string"
},
"remotePluginId": {
"type": "string"
},
"skillName": {
"type": "string"
}
},
"required": [
"remoteMarketplaceName",
"remotePluginId",
"skillName"
],
"title": "PluginSkillReadParams",
"type": "object"
},
"PluginSkillReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"contents": {
"type": [
"string",
"null"
]
}
},
"title": "PluginSkillReadResponse",
"type": "object"
},
"PluginSource": {
"oneOf": [
{

View File

@@ -1521,6 +1521,30 @@
"title": "Plugin/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"plugin/skill/read"
],
"title": "Plugin/skill/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/PluginSkillReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Plugin/skill/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -9044,6 +9068,40 @@
"title": "PluginShareSaveResponse",
"type": "object"
},
"PluginSkillReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"remoteMarketplaceName": {
"type": "string"
},
"remotePluginId": {
"type": "string"
},
"skillName": {
"type": "string"
}
},
"required": [
"remoteMarketplaceName",
"remotePluginId",
"skillName"
],
"title": "PluginSkillReadParams",
"type": "object"
},
"PluginSkillReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"contents": {
"type": [
"string",
"null"
]
}
},
"title": "PluginSkillReadResponse",
"type": "object"
},
"PluginSource": {
"oneOf": [
{

View File

@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"remoteMarketplaceName": {
"type": "string"
},
"remotePluginId": {
"type": "string"
},
"skillName": {
"type": "string"
}
},
"required": [
"remoteMarketplaceName",
"remotePluginId",
"skillName"
],
"title": "PluginSkillReadParams",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"contents": {
"type": [
"string",
"null"
]
}
},
"title": "PluginSkillReadResponse",
"type": "object"
}

File diff suppressed because one or more lines are too long

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 PluginSkillReadParams = { remoteMarketplaceName: string, remotePluginId: string, skillName: string, };

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 PluginSkillReadResponse = { contents: string | null, };

View File

@@ -287,6 +287,8 @@ export type { PluginShareListParams } from "./PluginShareListParams";
export type { PluginShareListResponse } from "./PluginShareListResponse";
export type { PluginShareSaveParams } from "./PluginShareSaveParams";
export type { PluginShareSaveResponse } from "./PluginShareSaveResponse";
export type { PluginSkillReadParams } from "./PluginSkillReadParams";
export type { PluginSkillReadResponse } from "./PluginSkillReadResponse";
export type { PluginSource } from "./PluginSource";
export type { PluginSummary } from "./PluginSummary";
export type { PluginUninstallParams } from "./PluginUninstallParams";

View File

@@ -612,6 +612,11 @@ client_request_definitions! {
serialization: global("config"),
response: v2::PluginReadResponse,
},
PluginSkillRead => "plugin/skill/read" {
params: v2::PluginSkillReadParams,
serialization: global("config"),
response: v2::PluginSkillReadResponse,
},
PluginShareSave => "plugin/share/save" {
params: v2::PluginShareSaveParams,
serialization: global("config"),

View File

@@ -4609,6 +4609,22 @@ pub struct PluginReadResponse {
pub plugin: PluginDetail,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginSkillReadParams {
pub remote_marketplace_name: String,
pub remote_plugin_id: String,
pub skill_name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginSkillReadResponse {
pub contents: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -10667,6 +10683,23 @@ mod tests {
);
}
#[test]
fn plugin_skill_read_params_serialization_uses_remote_plugin_id() {
assert_eq!(
serde_json::to_value(PluginSkillReadParams {
remote_marketplace_name: "chatgpt-global".to_string(),
remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
skill_name: "plan-work".to_string(),
})
.unwrap(),
json!({
"remoteMarketplaceName": "chatgpt-global",
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
"skillName": "plan-work",
}),
);
}
#[test]
fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
let plugin_path = if cfg!(windows) {