use std::collections::HashMap; use std::collections::HashSet; use std::fmt; use std::sync::Arc; use codex_exec_server::ExecutorFileSystem; use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; #[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: AbsolutePathBuf, 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: AbsolutePathBuf, pub message: String, } #[derive(Debug, Clone, Default)] pub struct SkillLoadOutcome { pub skills: Vec, pub errors: Vec, pub disabled_paths: HashSet, pub(crate) skill_roots: Vec, pub(crate) skill_root_by_path: Arc>, pub(crate) file_systems_by_skill_path: SkillFileSystemsByPath, 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(crate) fn file_system_for_skill( &self, skill: &SkillMetadata, ) -> Option> { self.file_systems_by_skill_path .get(&skill.path_to_skills_md) } } #[derive(Clone, Default)] pub(crate) struct SkillFileSystemsByPath { values: Arc>>, } impl SkillFileSystemsByPath { pub(crate) fn new(values: HashMap>) -> Self { Self { values: Arc::new(values), } } fn get(&self, path: &AbsolutePathBuf) -> Option> { self.values.get(path).map(Arc::clone) } fn retain_paths(&mut self, paths: &HashSet) { self.values = Arc::new( self.values .iter() .filter(|(path, _)| paths.contains(*path)) .map(|(path, fs)| (path.clone(), Arc::clone(fs))) .collect(), ); } } impl fmt::Debug for SkillFileSystemsByPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SkillFileSystemsByPath") .field("len", &self.values.len()) .finish() } } 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)); let retained_paths: HashSet = outcome .skills .iter() .map(|skill| skill.path_to_skills_md.clone()) .collect(); outcome .file_systems_by_skill_path .retain_paths(&retained_paths); outcome.skill_root_by_path = Arc::new( outcome .skill_root_by_path .iter() .filter(|(path, _)| retained_paths.contains(*path)) .map(|(path, root)| (path.clone(), root.clone())) .collect(), ); let retained_roots: HashSet = outcome.skill_root_by_path.values().cloned().collect(); outcome .skill_roots .retain(|root| retained_roots.contains(root)); 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 }