use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; #[derive(Debug, Clone, PartialEq)] pub struct SkillMetadata { pub name: String, pub description: String, pub short_description: Option, pub interface: Option, pub dependencies: Option, pub policy: Option, /// Path to the SKILLS.md file that declares this skill. pub path_to_skills_md: PathBuf, pub scope: SkillScope, } impl SkillMetadata { fn allow_implicit_invocation(&self) -> bool { self.policy .as_ref() .and_then(|policy| policy.allow_implicit_invocation) .unwrap_or(true) } pub fn matches_product_restriction_for_product( &self, restriction_product: Option, ) -> bool { match &self.policy { Some(policy) => { policy.products.is_empty() || restriction_product.is_some_and(|product| { product.matches_product_restriction(&policy.products) }) } None => true, } } } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SkillPolicy { pub allow_implicit_invocation: Option, // TODO: Enforce product gating in Codex skill selection/injection instead of only parsing and // storing this metadata. pub products: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SkillInterface { pub display_name: Option, pub short_description: Option, pub icon_small: Option, pub icon_large: Option, pub brand_color: Option, pub default_prompt: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SkillDependencies { pub tools: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SkillToolDependency { pub r#type: String, pub value: String, pub description: Option, pub transport: Option, pub command: Option, pub url: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SkillError { pub path: PathBuf, pub message: String, } #[derive(Debug, Clone, Default)] pub struct SkillLoadOutcome { pub skills: Vec, pub errors: Vec, pub disabled_paths: HashSet, pub(crate) implicit_skills_by_scripts_dir: Arc>, pub(crate) implicit_skills_by_doc_path: Arc>, } impl SkillLoadOutcome { pub fn is_skill_enabled(&self, skill: &SkillMetadata) -> bool { !self.disabled_paths.contains(&skill.path_to_skills_md) } pub fn is_skill_allowed_for_implicit_invocation(&self, skill: &SkillMetadata) -> bool { self.is_skill_enabled(skill) && skill.allow_implicit_invocation() } pub fn allowed_skills_for_implicit_invocation(&self) -> Vec { self.skills .iter() .filter(|skill| self.is_skill_allowed_for_implicit_invocation(skill)) .cloned() .collect() } pub fn skills_with_enabled(&self) -> impl Iterator { self.skills .iter() .map(|skill| (skill, self.is_skill_enabled(skill))) } } pub fn filter_skill_load_outcome_for_product( mut outcome: SkillLoadOutcome, restriction_product: Option, ) -> SkillLoadOutcome { outcome .skills .retain(|skill| skill.matches_product_restriction_for_product(restriction_product)); outcome.implicit_skills_by_scripts_dir = Arc::new( outcome .implicit_skills_by_scripts_dir .iter() .filter(|(_, skill)| skill.matches_product_restriction_for_product(restriction_product)) .map(|(path, skill)| (path.clone(), skill.clone())) .collect(), ); outcome.implicit_skills_by_doc_path = Arc::new( outcome .implicit_skills_by_doc_path .iter() .filter(|(_, skill)| skill.matches_product_restriction_for_product(restriction_product)) .map(|(path, skill)| (path.clone(), skill.clone())) .collect(), ); outcome }