mirror of
https://github.com/openai/codex.git
synced 2026-05-04 11:26:33 +00:00
Move plugin out of core. (#20348)
This commit is contained in:
@@ -14,6 +14,7 @@ mod mcp_types;
|
||||
mod merge;
|
||||
mod overrides;
|
||||
pub mod permissions_toml;
|
||||
mod plugin_edit;
|
||||
pub mod profile_toml;
|
||||
mod project_root_markers;
|
||||
mod requirements_exec_policy;
|
||||
@@ -95,6 +96,10 @@ pub use mcp_types::McpServerTransportConfig;
|
||||
pub use mcp_types::RawMcpServerConfig;
|
||||
pub use merge::merge_toml_values;
|
||||
pub use overrides::build_cli_overrides_layer;
|
||||
pub use plugin_edit::PluginConfigEdit;
|
||||
pub use plugin_edit::apply_user_plugin_config_edits;
|
||||
pub use plugin_edit::clear_user_plugin;
|
||||
pub use plugin_edit::set_user_plugin_enabled;
|
||||
pub use project_root_markers::default_project_root_markers;
|
||||
pub use project_root_markers::project_root_markers_from_config;
|
||||
pub use requirements_exec_policy::RequirementsExecPolicy;
|
||||
|
||||
307
codex-rs/config/src/plugin_edit.rs
Normal file
307
codex-rs/config/src/plugin_edit.rs
Normal file
@@ -0,0 +1,307 @@
|
||||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
|
||||
use codex_utils_path::resolve_symlink_write_paths;
|
||||
use codex_utils_path::write_atomically;
|
||||
use tokio::task;
|
||||
use toml_edit::DocumentMut;
|
||||
use toml_edit::Item as TomlItem;
|
||||
use toml_edit::Table as TomlTable;
|
||||
use toml_edit::value;
|
||||
|
||||
use crate::CONFIG_TOML_FILE;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PluginConfigEdit {
|
||||
SetEnabled { plugin_key: String, enabled: bool },
|
||||
Clear { plugin_key: String },
|
||||
}
|
||||
|
||||
pub async fn set_user_plugin_enabled(
|
||||
codex_home: &Path,
|
||||
plugin_key: String,
|
||||
enabled: bool,
|
||||
) -> std::io::Result<()> {
|
||||
apply_user_plugin_config_edits(
|
||||
codex_home,
|
||||
vec![PluginConfigEdit::SetEnabled {
|
||||
plugin_key,
|
||||
enabled,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn clear_user_plugin(codex_home: &Path, plugin_key: String) -> std::io::Result<()> {
|
||||
apply_user_plugin_config_edits(codex_home, vec![PluginConfigEdit::Clear { plugin_key }]).await
|
||||
}
|
||||
|
||||
pub async fn apply_user_plugin_config_edits(
|
||||
codex_home: &Path,
|
||||
edits: Vec<PluginConfigEdit>,
|
||||
) -> std::io::Result<()> {
|
||||
let codex_home = codex_home.to_path_buf();
|
||||
task::spawn_blocking(move || apply_user_plugin_config_edits_blocking(&codex_home, edits))
|
||||
.await
|
||||
.map_err(|err| std::io::Error::other(format!("config persistence task panicked: {err}")))?
|
||||
}
|
||||
|
||||
fn apply_user_plugin_config_edits_blocking(
|
||||
codex_home: &Path,
|
||||
edits: Vec<PluginConfigEdit>,
|
||||
) -> std::io::Result<()> {
|
||||
if edits.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let write_paths = resolve_symlink_write_paths(&config_path)?;
|
||||
let mut doc = read_or_create_document(write_paths.read_path.as_deref())?;
|
||||
let mut mutated = false;
|
||||
for edit in edits {
|
||||
mutated |= match edit {
|
||||
PluginConfigEdit::SetEnabled {
|
||||
plugin_key,
|
||||
enabled,
|
||||
} => set_plugin_enabled(&mut doc, &plugin_key, enabled),
|
||||
PluginConfigEdit::Clear { plugin_key } => clear_plugin(&mut doc, &plugin_key),
|
||||
};
|
||||
}
|
||||
if !mutated {
|
||||
return Ok(());
|
||||
}
|
||||
write_atomically(&write_paths.write_path, &doc.to_string())
|
||||
}
|
||||
|
||||
fn read_or_create_document(config_path: Option<&Path>) -> std::io::Result<DocumentMut> {
|
||||
let Some(config_path) = config_path else {
|
||||
return Ok(DocumentMut::new());
|
||||
};
|
||||
match fs::read_to_string(config_path) {
|
||||
Ok(raw) => raw
|
||||
.parse::<DocumentMut>()
|
||||
.map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err)),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => Ok(DocumentMut::new()),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_plugin_enabled(doc: &mut DocumentMut, plugin_key: &str, enabled: bool) -> bool {
|
||||
let Some(plugins) = ensure_plugins_table(doc) else {
|
||||
return false;
|
||||
};
|
||||
let Some(plugin) = ensure_table_for_write(&mut plugins[plugin_key]) else {
|
||||
return false;
|
||||
};
|
||||
let mut replacement = value(enabled);
|
||||
if let Some(existing) = plugin.get("enabled") {
|
||||
preserve_decor(existing, &mut replacement);
|
||||
}
|
||||
plugin["enabled"] = replacement;
|
||||
true
|
||||
}
|
||||
|
||||
fn clear_plugin(doc: &mut DocumentMut, plugin_key: &str) -> bool {
|
||||
let root = doc.as_table_mut();
|
||||
let Some(plugins_item) = root.get_mut("plugins") else {
|
||||
return false;
|
||||
};
|
||||
let Some(plugins) = ensure_table_for_read(plugins_item) else {
|
||||
return false;
|
||||
};
|
||||
plugins.remove(plugin_key).is_some()
|
||||
}
|
||||
|
||||
fn ensure_plugins_table(doc: &mut DocumentMut) -> Option<&mut TomlTable> {
|
||||
let root = doc.as_table_mut();
|
||||
if !root.contains_key("plugins") {
|
||||
root.insert("plugins", TomlItem::Table(new_implicit_table()));
|
||||
}
|
||||
ensure_table_for_write(root.get_mut("plugins")?)
|
||||
}
|
||||
|
||||
fn ensure_table_for_write(item: &mut TomlItem) -> Option<&mut TomlTable> {
|
||||
match item {
|
||||
TomlItem::Table(table) => Some(table),
|
||||
TomlItem::Value(value) => {
|
||||
let table = value
|
||||
.as_inline_table()
|
||||
.map_or_else(new_implicit_table, table_from_inline);
|
||||
*item = TomlItem::Table(table);
|
||||
item.as_table_mut()
|
||||
}
|
||||
TomlItem::None => {
|
||||
*item = TomlItem::Table(new_implicit_table());
|
||||
item.as_table_mut()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_table_for_read(item: &mut TomlItem) -> Option<&mut TomlTable> {
|
||||
match item {
|
||||
TomlItem::Table(_) => {}
|
||||
TomlItem::Value(value) => {
|
||||
let inline = value.as_inline_table()?.clone();
|
||||
*item = TomlItem::Table(table_from_inline(&inline));
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
item.as_table_mut()
|
||||
}
|
||||
|
||||
fn table_from_inline(inline: &toml_edit::InlineTable) -> TomlTable {
|
||||
let mut table = new_implicit_table();
|
||||
for (key, value) in inline.iter() {
|
||||
let mut value = value.clone();
|
||||
value.decor_mut().set_suffix("");
|
||||
table.insert(key, TomlItem::Value(value));
|
||||
}
|
||||
table
|
||||
}
|
||||
|
||||
fn new_implicit_table() -> TomlTable {
|
||||
let mut table = TomlTable::new();
|
||||
table.set_implicit(true);
|
||||
table
|
||||
}
|
||||
|
||||
fn preserve_decor(existing: &TomlItem, replacement: &mut TomlItem) {
|
||||
if let (TomlItem::Value(existing_value), TomlItem::Value(replacement_value)) =
|
||||
(existing, replacement)
|
||||
{
|
||||
replacement_value
|
||||
.decor_mut()
|
||||
.clone_from(existing_value.decor());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_user_plugin_enabled_writes_plugin_entry() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
set_user_plugin_enabled(
|
||||
codex_home.path(),
|
||||
"demo@market".to_string(),
|
||||
/*enabled*/ true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let config = read_config(codex_home.path());
|
||||
let expected: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[plugins."demo@market"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_user_plugin_enabled_preserves_existing_plugin_fields() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[plugins."demo@market"]
|
||||
enabled = false
|
||||
source = "/tmp/plugin"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
set_user_plugin_enabled(
|
||||
codex_home.path(),
|
||||
"demo@market".to_string(),
|
||||
/*enabled*/ true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let config = read_config(codex_home.path());
|
||||
let expected: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[plugins."demo@market"]
|
||||
enabled = true
|
||||
source = "/tmp/plugin"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clear_user_plugin_removes_empty_plugins_table() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[plugins."demo@market"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
clear_user_plugin(codex_home.path(), "demo@market".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).unwrap(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clear_user_plugin_missing_entry_does_not_create_config() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
clear_user_plugin(codex_home.path(), "demo@market".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!codex_home.path().join(CONFIG_TOML_FILE).exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn set_user_plugin_enabled_follows_config_symlink() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let target_path = codex_home.path().join("target_config.toml");
|
||||
symlink(&target_path, codex_home.path().join(CONFIG_TOML_FILE)).unwrap();
|
||||
|
||||
set_user_plugin_enabled(
|
||||
codex_home.path(),
|
||||
"demo@market".to_string(),
|
||||
/*enabled*/ true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let config =
|
||||
toml::from_str::<toml::Value>(&fs::read_to_string(target_path).unwrap()).unwrap();
|
||||
let expected: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[plugins."demo@market"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
|
||||
fn read_config(codex_home: &Path) -> toml::Value {
|
||||
toml::from_str(&fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user