mirror of
https://github.com/openai/codex.git
synced 2026-05-01 09:56:37 +00:00
Support enable/disable skill via config/api. (#9328)
In config.toml: ``` [[skills.config]] path = "/Users/xl/.codex/skills/my_skill/SKILL.md" enabled = false ``` API: skills/list, skills/config/write
This commit is contained in:
@@ -9,6 +9,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::task;
|
||||
use toml_edit::ArrayOfTables;
|
||||
use toml_edit::DocumentMut;
|
||||
use toml_edit::Item as TomlItem;
|
||||
use toml_edit::Table as TomlTable;
|
||||
@@ -36,6 +37,8 @@ pub enum ConfigEdit {
|
||||
RecordModelMigrationSeen { from: String, to: String },
|
||||
/// Replace the entire `[mcp_servers]` table.
|
||||
ReplaceMcpServers(BTreeMap<String, McpServerConfig>),
|
||||
/// Set or clear a skill config entry under `[[skills.config]]`.
|
||||
SetSkillConfig { path: PathBuf, enabled: bool },
|
||||
/// Set trust_level under `[projects."<path>"]`,
|
||||
/// migrating inline tables to explicit tables.
|
||||
SetProjectTrustLevel { path: PathBuf, level: TrustLevel },
|
||||
@@ -298,6 +301,9 @@ impl ConfigDocument {
|
||||
value(*acknowledged),
|
||||
)),
|
||||
ConfigEdit::ReplaceMcpServers(servers) => Ok(self.replace_mcp_servers(servers)),
|
||||
ConfigEdit::SetSkillConfig { path, enabled } => {
|
||||
Ok(self.set_skill_config(path.as_path(), *enabled))
|
||||
}
|
||||
ConfigEdit::SetPath { segments, value } => Ok(self.insert(segments, value.clone())),
|
||||
ConfigEdit::ClearPath { segments } => Ok(self.clear_owned(segments)),
|
||||
ConfigEdit::SetProjectTrustLevel { path, level } => {
|
||||
@@ -387,6 +393,113 @@ impl ConfigDocument {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_skill_config(&mut self, path: &Path, enabled: bool) -> bool {
|
||||
let normalized_path = normalize_skill_config_path(path);
|
||||
let mut remove_skills_table = false;
|
||||
let mut mutated = false;
|
||||
|
||||
{
|
||||
let root = self.doc.as_table_mut();
|
||||
let skills_item = match root.get_mut("skills") {
|
||||
Some(item) => item,
|
||||
None => {
|
||||
if enabled {
|
||||
return false;
|
||||
}
|
||||
root.insert(
|
||||
"skills",
|
||||
TomlItem::Table(document_helpers::new_implicit_table()),
|
||||
);
|
||||
let Some(item) = root.get_mut("skills") else {
|
||||
return false;
|
||||
};
|
||||
item
|
||||
}
|
||||
};
|
||||
|
||||
if document_helpers::ensure_table_for_write(skills_item).is_none() {
|
||||
if enabled {
|
||||
return false;
|
||||
}
|
||||
*skills_item = TomlItem::Table(document_helpers::new_implicit_table());
|
||||
}
|
||||
let Some(skills_table) = skills_item.as_table_mut() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let config_item = match skills_table.get_mut("config") {
|
||||
Some(item) => item,
|
||||
None => {
|
||||
if enabled {
|
||||
return false;
|
||||
}
|
||||
skills_table.insert("config", TomlItem::ArrayOfTables(ArrayOfTables::new()));
|
||||
let Some(item) = skills_table.get_mut("config") else {
|
||||
return false;
|
||||
};
|
||||
item
|
||||
}
|
||||
};
|
||||
|
||||
if !matches!(config_item, TomlItem::ArrayOfTables(_)) {
|
||||
if enabled {
|
||||
return false;
|
||||
}
|
||||
*config_item = TomlItem::ArrayOfTables(ArrayOfTables::new());
|
||||
}
|
||||
|
||||
let TomlItem::ArrayOfTables(overrides) = config_item else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let existing_index = overrides.iter().enumerate().find_map(|(idx, table)| {
|
||||
table
|
||||
.get("path")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(Path::new)
|
||||
.map(normalize_skill_config_path)
|
||||
.filter(|value| *value == normalized_path)
|
||||
.map(|_| idx)
|
||||
});
|
||||
|
||||
if enabled {
|
||||
if let Some(index) = existing_index {
|
||||
overrides.remove(index);
|
||||
mutated = true;
|
||||
if overrides.is_empty() {
|
||||
skills_table.remove("config");
|
||||
if skills_table.is_empty() {
|
||||
remove_skills_table = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(index) = existing_index {
|
||||
for (idx, table) in overrides.iter_mut().enumerate() {
|
||||
if idx == index {
|
||||
table["path"] = value(normalized_path);
|
||||
table["enabled"] = value(false);
|
||||
mutated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut entry = TomlTable::new();
|
||||
entry.set_implicit(false);
|
||||
entry["path"] = value(normalized_path);
|
||||
entry["enabled"] = value(false);
|
||||
overrides.push(entry);
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if remove_skills_table {
|
||||
let root = self.doc.as_table_mut();
|
||||
root.remove("skills");
|
||||
}
|
||||
|
||||
mutated
|
||||
}
|
||||
|
||||
fn scoped_segments(&self, scope: Scope, segments: &[&str]) -> Vec<String> {
|
||||
let resolved: Vec<String> = segments
|
||||
.iter()
|
||||
@@ -494,6 +607,13 @@ impl ConfigDocument {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_skill_config_path(path: &Path) -> String {
|
||||
dunce::canonicalize(path)
|
||||
.unwrap_or_else(|_| path.to_path_buf())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Persist edits using a blocking strategy.
|
||||
pub fn apply_blocking(
|
||||
codex_home: &Path,
|
||||
@@ -737,6 +857,54 @@ model_reasoning_effort = "high"
|
||||
assert_eq!(contents, "enabled = true\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_skill_config_writes_disabled_entry() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits([ConfigEdit::SetSkillConfig {
|
||||
path: PathBuf::from("/tmp/skills/demo/SKILL.md"),
|
||||
enabled: false,
|
||||
}])
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents =
|
||||
std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[[skills.config]]
|
||||
path = "/tmp/skills/demo/SKILL.md"
|
||||
enabled = false
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_skill_config_removes_entry_when_enabled() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
std::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"[[skills.config]]
|
||||
path = "/tmp/skills/demo/SKILL.md"
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.expect("seed config");
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits([ConfigEdit::SetSkillConfig {
|
||||
path: PathBuf::from("/tmp/skills/demo/SKILL.md"),
|
||||
enabled: true,
|
||||
}])
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents =
|
||||
std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
assert_eq!(contents, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_set_model_preserves_inline_table_contents() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
|
||||
Reference in New Issue
Block a user