Files
codex/codex-rs/core-skills/src/config_rules.rs
T
felixxia-oai 56c11cf658 Move shared skill models into codex-skills (#34429)
## What changed

- Define and export skill metadata, policy, dependency, interface, and configuration rule types from `codex-skills`.
- Update core, plugin, and extension consumers to use the shared models while retaining compatibility re-exports from `codex-core-skills`.
- Share product-restriction handling between host and environment skill metadata.

## Testing

- Add unit coverage for default and explicit implicit-invocation and product policies.

GitOrigin-RevId: 9093469f8fff8e2d902dc9781235f64709827a87
2026-07-20 23:22:48 +00:00

115 lines
3.6 KiB
Rust

use std::collections::HashSet;
use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
use codex_config::ConfigLayerStackOrdering;
use codex_config::SkillConfig;
use codex_config::SkillsConfig;
pub use codex_skills::SkillConfigRule;
pub use codex_skills::SkillConfigRuleSelector;
pub use codex_skills::SkillConfigRules;
use codex_utils_absolute_path::AbsolutePathBuf;
use tracing::warn;
use crate::SkillMetadata;
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
}
}
}