Codex Handbook
core-skills/src/config_rules.rs 128 lines
use std::collections::HashSet;use codex_app_server_protocol::ConfigLayerSource;use codex_config::ConfigLayerStack;use codex_config::ConfigLayerStackOrdering;use codex_config::SkillConfig;use codex_config::SkillsConfig;use codex_utils_absolute_path::AbsolutePathBuf;use tracing::warn;use crate::SkillMetadata;#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]pub enum SkillConfigRuleSelector {    Name(String),    Path(AbsolutePathBuf),}#[derive(Debug, Clone, PartialEq, Eq, Hash)]pub struct SkillConfigRule {    pub selector: SkillConfigRuleSelector,    pub enabled: bool,}#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]pub struct SkillConfigRules {    pub entries: Vec<SkillConfigRule>,}pub fn skill_config_rules_from_stack(config_layer_stack: &ConfigLayerStack) -> SkillConfigRules {    let mut entries = Vec::new();    for layer in config_layer_stack.get_layers(        ConfigLayerStackOrdering::LowestPrecedenceFirst,        /*include_disabled*/ true,    ) {        if !matches!(            layer.name,            ConfigLayerSource::User { .. } | ConfigLayerSource::SessionFlags        ) {            continue;        }        let Some(skills_value) = layer.config.get("skills") else {            continue;        };        let skills: SkillsConfig = match skills_value.clone().try_into() {            Ok(skills) => skills,            Err(err) => {                warn!("invalid skills config: {err}");                continue;            }        };        for entry in skills.config {            let Some(selector) = skill_config_rule_selector(&entry) else {                continue;            };            // Preserve layer order so a later name selector can override an earlier path selector            // for the same loaded skill.            entries.retain(|entry: &SkillConfigRule| entry.selector != selector);            entries.push(SkillConfigRule {                selector,                enabled: entry.enabled,            });        }    }    SkillConfigRules { entries }}pub fn resolve_disabled_skill_paths(    skills: &[SkillMetadata],    rules: &SkillConfigRules,) -> HashSet<AbsolutePathBuf> {    let mut disabled_paths = HashSet::new();    for entry in &rules.entries {        match &entry.selector {            SkillConfigRuleSelector::Path(path) => {                if entry.enabled {                    disabled_paths.remove(path);                } else {                    disabled_paths.insert(path.clone());                }            }            SkillConfigRuleSelector::Name(name) => {                for path in skills                    .iter()                    .filter(|skill| skill.name == *name)                    .map(|skill| skill.path_to_skills_md.clone())                {                    if entry.enabled {                        disabled_paths.remove(&path);                    } else {                        disabled_paths.insert(path);                    }                }            }        }    }    disabled_paths}fn skill_config_rule_selector(entry: &SkillConfig) -> Option<SkillConfigRuleSelector> {    match (entry.path.as_ref(), entry.name.as_deref()) {        (Some(path), None) => Some(SkillConfigRuleSelector::Path(            path.canonicalize().unwrap_or_else(|_| path.clone()),        )),        (None, Some(name)) => {            let name = name.trim();            if name.is_empty() {                warn!("ignoring empty skills.config name override");                None            } else {                Some(SkillConfigRuleSelector::Name(name.to_string()))            }        }        (Some(_), Some(_)) => {            warn!("ignoring skills.config entry with both path and name selectors");            None        }        (None, None) => {            warn!("ignoring skills.config entry without a path or name selector");            None        }    }}