mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why Hosted orchestrator skills are read through the remote MCP resource server. Within one thread, the same catalog or skill resource can be requested multiple times by prompt injection and the `skills.list` / `skills.read` tools. Re-fetching adds latency and can make those surfaces observe different remote contents during the same thread. This is a follow-up to #28333: orchestrator skills remain limited to threads without a local executor, and those threads now get a stable per-thread view of the remote skill data they use. ## What changed - Reuse the existing per-thread orchestrator catalog snapshot for `skills.list` and `skills.read` availability checks. - Cache successful orchestrator resource reads by authority, package, and resource so prompt injection and tool calls share the same contents. - Keep the cache memory-only and bounded to 100 resources and 8 MiB per thread. - Leave host and executor skill reads unchanged, and do not cache failed remote reads. ## Verification - Extended the app-server MCP resource integration test to read the same hosted skill resource twice and verify that the remote server receives one read. - The same test verifies that catalog discovery and the selected skill's main prompt are each fetched only once per thread.
113 lines
3.8 KiB
Rust
113 lines
3.8 KiB
Rust
use codex_extension_api::FunctionCallError;
|
|
use codex_extension_api::ToolCall;
|
|
use codex_extension_api::ToolExecutor;
|
|
use codex_extension_api::ToolExecutorFuture;
|
|
use codex_extension_api::ToolName;
|
|
use codex_extension_api::ToolSpec;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
use crate::catalog::SkillPackageId;
|
|
use crate::catalog::SkillResourceId;
|
|
use crate::provider::SkillReadRequest;
|
|
|
|
use super::MAX_HANDLE_BYTES;
|
|
use super::SkillToolAuthority;
|
|
use super::SkillToolContext;
|
|
use super::external_json_output;
|
|
use super::parse_args;
|
|
use super::skill_function_tool;
|
|
use super::skill_tool_name;
|
|
use super::validate_handle;
|
|
|
|
const TOOL_NAME: &str = "read";
|
|
|
|
#[derive(Deserialize, JsonSchema)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct ReadArgs {
|
|
authority: SkillToolAuthority,
|
|
package: String,
|
|
resource: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, JsonSchema, PartialEq, Serialize)]
|
|
#[schemars(deny_unknown_fields)]
|
|
struct ReadResponse {
|
|
resource: String,
|
|
contents: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(super) struct ReadTool {
|
|
pub(super) context: SkillToolContext,
|
|
}
|
|
|
|
impl ToolExecutor<ToolCall> for ReadTool {
|
|
fn tool_name(&self) -> ToolName {
|
|
skill_tool_name(TOOL_NAME)
|
|
}
|
|
|
|
fn spec(&self) -> ToolSpec {
|
|
skill_function_tool::<ReadArgs, ReadResponse>(
|
|
TOOL_NAME,
|
|
"Read one complete resource from an enabled skill. Pass the exact authority and package returned by skills.list; resource identifiers remain opaque and are routed to that authority.",
|
|
)
|
|
}
|
|
|
|
fn handle(&self, call: ToolCall) -> ToolExecutorFuture<'_> {
|
|
Box::pin(async move {
|
|
let args: ReadArgs = parse_args(&call)?;
|
|
let authority = args.authority.into_authority();
|
|
validate_handle("package", &args.package, MAX_HANDLE_BYTES)?;
|
|
validate_handle("resource", &args.resource, MAX_HANDLE_BYTES)?;
|
|
|
|
let catalog = self.context.catalog(&call.turn_id, args.authority).await;
|
|
let package_is_available = catalog.entries.iter().any(|entry| {
|
|
entry.enabled && entry.authority == authority && entry.id.0 == args.package
|
|
});
|
|
if !package_is_available {
|
|
return Err(FunctionCallError::RespondToModel(
|
|
"skill package is not available from the requested authority".to_string(),
|
|
));
|
|
}
|
|
|
|
let requested_resource = SkillResourceId::new(args.resource);
|
|
let result = self
|
|
.context
|
|
.thread_state
|
|
.read_skill(
|
|
&self.context.providers,
|
|
SkillReadRequest {
|
|
authority,
|
|
package: SkillPackageId(args.package),
|
|
resource: requested_resource.clone(),
|
|
host: None,
|
|
mcp_resources: self.context.mcp_resources.clone(),
|
|
},
|
|
)
|
|
.await
|
|
.map_err(|err| {
|
|
tracing::warn!(
|
|
error = %err,
|
|
turn_id = %call.turn_id,
|
|
call_id = %call.call_id,
|
|
resource = requested_resource.as_str(),
|
|
"skills.read provider request failed"
|
|
);
|
|
FunctionCallError::RespondToModel("failed to read skill resource".to_string())
|
|
})?;
|
|
if result.resource != requested_resource {
|
|
return Err(FunctionCallError::Fatal(
|
|
"skill provider returned a different resource".to_string(),
|
|
));
|
|
}
|
|
|
|
external_json_output(&ReadResponse {
|
|
resource: result.resource.as_str().to_string(),
|
|
contents: result.contents,
|
|
})
|
|
})
|
|
}
|
|
}
|