mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
skills: expose remote skill resource tools (#27388)
## Why PR #27387 makes backend plugin skills discoverable and invocable without an executor, but resources referenced by those skills still sit behind the generic MCP resource surface. The model needs a skills-owned API that preserves the provider authority and package boundary instead of treating remote resources like local files. This is stacked on #27387. ## What - Adds one `skills` namespace with bounded `list` and `read` tools for remote skill providers. - Revalidates `authority + package` against the live remote catalog on every read, then routes the opaque resource ID back through that provider. - Allows the backend provider to read canonical child `skill://` resources while rejecting cross-package, non-canonical, query, fragment, and traversal-shaped URIs. - Caps each serialized tool result at 8 KB. Lists are paginated; reads return an opaque continuation cursor. - Marks the JSON output as external context so memory generation can apply its normal suppression policy. - Deliberately does not add `skills.search`; that waits for a bounded plugin-service search contract. ## Tool contract Pseudo-Python matching the wire shape: ```python from typing import Literal, NotRequired, TypedDict class RemoteSkillAuthority(TypedDict): kind: Literal["remote"] id: str # e.g. "codex_apps" class RemoteSkill(TypedDict): authority: RemoteSkillAuthority package: str # opaque provider-owned package ID name: str description: str main_resource: str # opaque provider-owned SKILL.md ID class SkillsListParams(TypedDict): cursor: NotRequired[str] class SkillsListResult(TypedDict): skills: list[RemoteSkill] next_cursor: str | None warnings: list[str] truncated: bool class SkillsReadParams(TypedDict): authority: RemoteSkillAuthority # copied from skills.list package: str # copied from skills.list resource: str # provider-owned child resource ID cursor: NotRequired[str] # copy next_cursor to continue class SkillsReadResult(TypedDict): resource: str contents: str next_cursor: str | None truncated: bool class Skills: def list(self, params: SkillsListParams) -> SkillsListResult: ... def read(self, params: SkillsReadParams) -> SkillsReadResult: ... ``` There is one namespace for all remote skills, not one tool or MCP server per skill. No resource ID is converted into a filesystem path. ## Backend dependency `/ps/mcp` must support direct reads of child resources such as `skill://plugin_demo/deploy/references/deploy.md`. This PR implements and tests the Codex side of that contract; production child reads remain dependent on the corresponding plugin-service support. Search remains out of scope until that service exposes a bounded search/resource API. ## Validation - Added an app-server integration test covering `skills.list` followed by `skills.read` with no executor. - Ran `just fmt`. - Ran `just bazel-lock-update` and `just bazel-lock-check`. - Did not run Rust tests or Clippy locally, per request; CI will run them.
This commit is contained in:
@@ -93,6 +93,7 @@ where
|
||||
pub struct JsonToolOutput {
|
||||
value: JsonValue,
|
||||
success: Option<bool>,
|
||||
contains_external_context: bool,
|
||||
}
|
||||
|
||||
impl JsonToolOutput {
|
||||
@@ -100,11 +101,21 @@ impl JsonToolOutput {
|
||||
Self {
|
||||
value,
|
||||
success: Some(true),
|
||||
contains_external_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_success(value: JsonValue, success: Option<bool>) -> Self {
|
||||
Self { value, success }
|
||||
Self {
|
||||
value,
|
||||
success,
|
||||
contains_external_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_external_context(mut self) -> Self {
|
||||
self.contains_external_context = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +128,10 @@ impl ToolOutput for JsonToolOutput {
|
||||
self.success.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn contains_external_context(&self) -> bool {
|
||||
self.contains_external_context
|
||||
}
|
||||
|
||||
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
|
||||
let output = FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text(self.value.to_string()),
|
||||
|
||||
Reference in New Issue
Block a user