mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
Support disabling skills by name, primarily for plugin skills. We can’t use the path, since plugin skill paths may change across versions.
136 lines
4.0 KiB
Rust
136 lines
4.0 KiB
Rust
use std::collections::HashSet;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use codex_app_server_protocol::ConfigLayerSource;
|
|
use tracing::warn;
|
|
|
|
use crate::config::types::SkillConfig;
|
|
use crate::config::types::SkillsConfig;
|
|
use crate::config_loader::ConfigLayerStack;
|
|
use crate::config_loader::ConfigLayerStackOrdering;
|
|
use crate::skills::SkillMetadata;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
pub(crate) enum SkillConfigRuleSelector {
|
|
Name(String),
|
|
Path(PathBuf),
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub(crate) struct SkillConfigRule {
|
|
pub selector: SkillConfigRuleSelector,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
|
|
pub(crate) struct SkillConfigRules {
|
|
pub entries: Vec<SkillConfigRule>,
|
|
}
|
|
|
|
pub(crate) 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(crate) fn resolve_disabled_skill_paths(
|
|
skills: &[SkillMetadata],
|
|
rules: &SkillConfigRules,
|
|
) -> HashSet<PathBuf> {
|
|
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(normalize_rule_path(
|
|
path.as_path(),
|
|
))),
|
|
(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
|
|
}
|
|
}
|
|
}
|
|
|
|
fn normalize_rule_path(path: &Path) -> PathBuf {
|
|
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
|
}
|