ext/skills/src/tools/list.rs
112 lines
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::SkillCatalogEntry;use crate::render::truncate_utf8_to_bytes;use super::MAX_HANDLE_BYTES;use super::SkillToolAuthority;use super::SkillToolContext;use super::external_json_output;use super::is_bounded_handle;use super::parse_args;use super::skill_function_tool;use super::skill_tool_name;const TOOL_NAME: &str = "list";const MAX_WARNINGS: usize = 4;const MAX_WARNING_BYTES: usize = 256;#[derive(Deserialize, JsonSchema)]#[serde(deny_unknown_fields)]struct ListArgs { authority: SkillToolAuthority,}#[derive(Debug, Eq, JsonSchema, PartialEq, Serialize)]#[schemars(deny_unknown_fields)]struct ListedSkill { authority: SkillToolAuthority, package: String, name: String, description: String, main_resource: String,}#[derive(Debug, Eq, JsonSchema, PartialEq, Serialize)]#[schemars(deny_unknown_fields)]struct ListResponse { skills: Vec<ListedSkill>, warnings: Vec<String>,}#[derive(Clone)]pub(super) struct ListTool { pub(super) context: SkillToolContext,}impl ToolExecutor<ToolCall> for ListTool { fn tool_name(&self) -> ToolName { skill_tool_name(TOOL_NAME) } fn spec(&self) -> ToolSpec { skill_function_tool::<ListArgs, ListResponse>( TOOL_NAME, "List enabled skills owned by the requested authority. Only orchestrator-owned skills are currently supported. Returns the opaque package and main-resource handles required by skills.read.", ) } fn handle(&self, call: ToolCall) -> ToolExecutorFuture<'_> { Box::pin(async move { let args: ListArgs = parse_args(&call)?; let authority = args.authority.into_authority(); let catalog = self.context.catalog(&call.turn_id, args.authority).await; let response = ListResponse { skills: catalog .entries .into_iter() .filter(|entry| entry.enabled && entry.authority == authority) .filter_map(listed_skill) .collect(), warnings: bounded_warnings(catalog.warnings), }; external_json_output(&response) }) }}fn listed_skill(entry: SkillCatalogEntry) -> Option<ListedSkill> { let authority = SkillToolAuthority::from_authority(&entry.authority)?; if !is_bounded_handle(&entry.id.0, MAX_HANDLE_BYTES) || !is_bounded_handle(entry.main_prompt.as_str(), MAX_HANDLE_BYTES) { return None; } Some(ListedSkill { authority, package: entry.id.0, name: entry.name, description: entry.description, main_resource: entry.main_prompt.as_str().to_string(), })}fn bounded_warnings(warnings: Vec<String>) -> Vec<String> { warnings .into_iter() .take(MAX_WARNINGS) .map(|warning| { let (warning, _) = truncate_utf8_to_bytes(&warning, MAX_WARNING_BYTES); warning }) .collect()}