Codex Handbook
core-skills/src/model.rs 241 lines
use std::collections::HashMap;use std::collections::HashSet;use std::fmt;use std::io;use std::sync::Arc;use codex_exec_server::ExecutorFileSystem;use codex_exec_server::LOCAL_FS;use codex_protocol::protocol::Product;use codex_protocol::protocol::SkillScope;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path_uri::PathUri;#[derive(Debug, Clone, PartialEq)]pub struct SkillMetadata {    pub name: String,    pub description: String,    pub short_description: Option<String>,    pub interface: Option<SkillInterface>,    pub dependencies: Option<SkillDependencies>,    pub policy: Option<SkillPolicy>,    /// Path to the SKILLS.md file that declares this skill.    pub path_to_skills_md: AbsolutePathBuf,    pub scope: SkillScope,    pub plugin_id: Option<String>,}impl SkillMetadata {    pub fn allows_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<Product>,    ) -> 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<bool>,    // TODO: Enforce product gating in Codex skill selection/injection instead of only parsing and    // storing this metadata.    pub products: Vec<Product>,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct SkillInterface {    pub display_name: Option<String>,    pub short_description: Option<String>,    pub icon_small: Option<AbsolutePathBuf>,    pub icon_large: Option<AbsolutePathBuf>,    pub brand_color: Option<String>,    pub default_prompt: Option<String>,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct SkillDependencies {    pub tools: Vec<SkillToolDependency>,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct SkillToolDependency {    pub r#type: String,    pub value: String,    pub description: Option<String>,    pub transport: Option<String>,    pub command: Option<String>,    pub url: Option<String>,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct SkillError {    pub path: AbsolutePathBuf,    pub message: String,}#[derive(Debug, Clone, Default)]pub struct SkillLoadOutcome {    pub skills: Vec<SkillMetadata>,    pub errors: Vec<SkillError>,    pub disabled_paths: HashSet<AbsolutePathBuf>,    pub(crate) skill_roots: Vec<AbsolutePathBuf>,    pub(crate) skill_root_by_path: Arc<HashMap<AbsolutePathBuf, AbsolutePathBuf>>,    pub(crate) file_systems_by_skill_path: SkillFileSystemsByPath,    pub(crate) implicit_skills_by_scripts_dir: Arc<HashMap<AbsolutePathBuf, SkillMetadata>>,    pub(crate) implicit_skills_by_doc_path: Arc<HashMap<AbsolutePathBuf, SkillMetadata>>,}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.allows_implicit_invocation()    }    pub fn allowed_skills_for_implicit_invocation(&self) -> Vec<SkillMetadata> {        self.skills            .iter()            .filter(|skill| self.is_skill_allowed_for_implicit_invocation(skill))            .cloned()            .collect()    }    pub fn skills_with_enabled(&self) -> impl Iterator<Item = (&SkillMetadata, bool)> {        self.skills            .iter()            .map(|skill| (skill, self.is_skill_enabled(skill)))    }    pub(crate) fn file_system_for_skill(        &self,        skill: &SkillMetadata,    ) -> Option<Arc<dyn ExecutorFileSystem>> {        self.file_systems_by_skill_path            .get(&skill.path_to_skills_md)    }}/// Host-loaded skills for one turn, including the filesystem mapping needed to/// read skill bodies through the environment that loaded them.#[derive(Debug, Clone)]pub struct HostLoadedSkills {    outcome: Arc<SkillLoadOutcome>,}impl HostLoadedSkills {    pub fn new(outcome: Arc<SkillLoadOutcome>) -> Self {        Self { outcome }    }    pub fn outcome(&self) -> &SkillLoadOutcome {        self.outcome.as_ref()    }    pub async fn read_skill_text(&self, skill: &SkillMetadata) -> io::Result<String> {        let fs = self            .outcome            .file_system_for_skill(skill)            .unwrap_or_else(|| Arc::clone(&LOCAL_FS));        let path = PathUri::from_abs_path(&skill.path_to_skills_md);        fs.read_file_text(&path, /*sandbox*/ None).await    }}#[derive(Clone, Default)]pub(crate) struct SkillFileSystemsByPath {    values: Arc<HashMap<AbsolutePathBuf, Arc<dyn ExecutorFileSystem>>>,}impl SkillFileSystemsByPath {    pub(crate) fn new(values: HashMap<AbsolutePathBuf, Arc<dyn ExecutorFileSystem>>) -> Self {        Self {            values: Arc::new(values),        }    }    fn get(&self, path: &AbsolutePathBuf) -> Option<Arc<dyn ExecutorFileSystem>> {        self.values.get(path).map(Arc::clone)    }    fn retain_paths(&mut self, paths: &HashSet<AbsolutePathBuf>) {        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<Product>,) -> SkillLoadOutcome {    outcome        .skills        .retain(|skill| skill.matches_product_restriction_for_product(restriction_product));    let retained_paths: HashSet<AbsolutePathBuf> = 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<AbsolutePathBuf> =        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}