use std::path::Path;
use toml_edit::{DocumentMut, Item, Table};
use crate::error::AgentConfigError;
use crate::status::ConfigPresence;
use crate::util::fs_atomic;
pub(crate) fn read_or_empty(path: &Path) -> Result<DocumentMut, AgentConfigError> {
let text = fs_atomic::read_to_string_capped(path)?;
if text.trim().is_empty() {
return Ok(DocumentMut::new());
}
text.parse::<DocumentMut>()
.map_err(|e| AgentConfigError::toml(path, e))
}
pub(crate) fn to_string(doc: &DocumentMut) -> Vec<u8> {
let mut out = doc.to_string();
if !out.ends_with('\n') {
out.push('\n');
}
out.into_bytes()
}
pub(crate) fn upsert_named_table(
doc: &mut DocumentMut,
parent: &[&str],
name: &str,
table: Table,
) -> Result<bool, AgentConfigError> {
let parent_table = ensure_table(doc, parent)?;
let new_item = Item::Table(table);
let changed = match parent_table.get(name) {
Some(existing) => existing.to_string() != new_item.to_string(),
None => true,
};
if changed {
parent_table.insert(name, new_item);
}
Ok(changed)
}
pub(crate) fn remove_named_table(
doc: &mut DocumentMut,
parent: &[&str],
name: &str,
) -> Result<bool, AgentConfigError> {
let Some(parent_table) = traverse_table_mut(doc, parent) else {
return Ok(false);
};
let removed = parent_table.remove(name).is_some();
if !removed {
return Ok(false);
}
let parent_now_empty = parent_table.is_empty();
if parent_now_empty && !parent.is_empty() {
prune_empty_parent(doc, parent);
}
Ok(true)
}
pub(crate) fn contains_named_table(doc: &DocumentMut, parent: &[&str], name: &str) -> bool {
let mut cur: &Item = doc.as_item();
for key in parent {
let Some(next) = cur.as_table().and_then(|t| t.get(key)) else {
return false;
};
cur = next;
}
cur.as_table()
.map(|t| t.contains_key(name))
.unwrap_or(false)
}
pub(crate) fn config_presence(
config_path: &Path,
parent: &[&str],
name: &str,
) -> Result<ConfigPresence, AgentConfigError> {
if !config_path.exists() {
return Ok(ConfigPresence::Absent);
}
let doc = match read_or_empty(config_path) {
Ok(d) => d,
Err(AgentConfigError::TomlInvalid { source, .. }) => {
return Ok(ConfigPresence::Invalid {
reason: source.to_string(),
});
}
Err(e) => return Err(e),
};
Ok(if contains_named_table(&doc, parent, name) {
ConfigPresence::Single
} else {
ConfigPresence::Absent
})
}
fn ensure_table<'a>(
doc: &'a mut DocumentMut,
path: &[&str],
) -> Result<&'a mut Table, AgentConfigError> {
let mut cur: &mut Table = doc.as_table_mut();
for key in path {
if !cur.contains_key(key) {
cur.insert(key, Item::Table(make_implicit_table()));
}
let next = cur
.get_mut(key)
.and_then(Item::as_table_mut)
.ok_or_else(|| {
AgentConfigError::Other(anyhow::anyhow!(
"expected TOML table at path segment {:?}, found a non-table item",
key
))
})?;
cur = next;
}
Ok(cur)
}
fn traverse_table_mut<'a>(doc: &'a mut DocumentMut, path: &[&str]) -> Option<&'a mut Table> {
let mut cur: &mut Table = doc.as_table_mut();
for key in path {
cur = cur.get_mut(key)?.as_table_mut()?;
}
Some(cur)
}
fn prune_empty_parent(doc: &mut DocumentMut, path: &[&str]) {
for depth in (1..=path.len()).rev() {
let (parent, leaf) = path[..depth].split_at(depth - 1);
let key = leaf[0];
let Some(parent_table) = traverse_table_mut(doc, parent) else {
return;
};
let leaf_empty = parent_table
.get(key)
.and_then(Item::as_table)
.map(Table::is_empty)
.unwrap_or(false);
if !leaf_empty {
return;
}
parent_table.remove(key);
}
}
fn make_implicit_table() -> Table {
let mut t = Table::new();
t.set_implicit(true);
t
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
use toml_edit::value;
#[test]
fn read_missing_returns_empty_doc() {
let dir = tempdir().unwrap();
let doc = read_or_empty(&dir.path().join("absent.toml")).unwrap();
assert!(doc.as_table().is_empty());
}
#[test]
fn read_invalid_errors() {
let dir = tempdir().unwrap();
let p = dir.path().join("bad.toml");
fs::write(&p, b"=oops\n").unwrap();
let err = read_or_empty(&p).unwrap_err();
assert!(matches!(err, AgentConfigError::TomlInvalid { .. }));
}
#[test]
fn upsert_inserts_named_table() {
let mut doc = DocumentMut::new();
let mut t = Table::new();
t["command"] = value("npx");
t["args"] = value("[\"-y\"]"); let changed = upsert_named_table(&mut doc, &["mcp_servers"], "github", t).unwrap();
assert!(changed);
let rendered = doc.to_string();
assert!(
rendered.contains("[mcp_servers.github]"),
"got:\n{rendered}"
);
assert!(rendered.contains(r#"command = "npx""#), "got:\n{rendered}");
}
#[test]
fn upsert_idempotent_on_identical_table() {
let mut doc = DocumentMut::new();
let mut t = Table::new();
t["command"] = value("npx");
upsert_named_table(&mut doc, &["mcp_servers"], "github", t.clone()).unwrap();
let changed_again = upsert_named_table(&mut doc, &["mcp_servers"], "github", t).unwrap();
assert!(!changed_again);
}
#[test]
fn upsert_replaces_existing_table() {
let mut doc = DocumentMut::new();
let mut t1 = Table::new();
t1["command"] = value("old");
upsert_named_table(&mut doc, &["mcp_servers"], "github", t1).unwrap();
let mut t2 = Table::new();
t2["command"] = value("new");
let changed = upsert_named_table(&mut doc, &["mcp_servers"], "github", t2).unwrap();
assert!(changed);
assert!(doc.to_string().contains(r#"command = "new""#));
}
#[test]
fn remove_named_table_prunes_empty_parent() {
let mut doc = DocumentMut::new();
let mut t = Table::new();
t["command"] = value("npx");
upsert_named_table(&mut doc, &["mcp_servers"], "github", t).unwrap();
let removed = remove_named_table(&mut doc, &["mcp_servers"], "github").unwrap();
assert!(removed);
assert!(!doc.contains_key("mcp_servers"), "got:\n{}", doc);
}
#[test]
fn remove_named_table_keeps_siblings() {
let mut doc = DocumentMut::new();
let mut a = Table::new();
a["command"] = value("a");
let mut b = Table::new();
b["command"] = value("b");
upsert_named_table(&mut doc, &["mcp_servers"], "alpha", a).unwrap();
upsert_named_table(&mut doc, &["mcp_servers"], "beta", b).unwrap();
remove_named_table(&mut doc, &["mcp_servers"], "alpha").unwrap();
assert!(doc.to_string().contains("[mcp_servers.beta]"));
assert!(!doc.to_string().contains("[mcp_servers.alpha]"));
}
#[test]
fn remove_unknown_is_noop() {
let mut doc = DocumentMut::new();
let mut t = Table::new();
t["command"] = value("a");
upsert_named_table(&mut doc, &["mcp_servers"], "alpha", t).unwrap();
let removed = remove_named_table(&mut doc, &["mcp_servers"], "ghost").unwrap();
assert!(!removed);
}
#[test]
fn contains_named_table_works() {
let mut doc = DocumentMut::new();
let mut t = Table::new();
t["command"] = value("npx");
upsert_named_table(&mut doc, &["mcp_servers"], "github", t).unwrap();
assert!(contains_named_table(&doc, &["mcp_servers"], "github"));
assert!(!contains_named_table(&doc, &["mcp_servers"], "ghost"));
assert!(!contains_named_table(&doc, &["other"], "github"));
}
#[test]
fn user_comments_preserved_across_round_trip() {
let dir = tempdir().unwrap();
let p = dir.path().join("config.toml");
let original = "\
# Codex configuration
# Hand-edited; do not delete comments.
[some.other.section]
foo = \"bar\"
";
fs::write(&p, original).unwrap();
let mut doc = read_or_empty(&p).unwrap();
let mut t = Table::new();
t["command"] = value("npx");
upsert_named_table(&mut doc, &["mcp_servers"], "github", t).unwrap();
let rendered = String::from_utf8(to_string(&doc)).unwrap();
assert!(
rendered.contains("# Codex configuration"),
"comment lost. got:\n{rendered}"
);
assert!(rendered.contains("# Hand-edited"), "comment lost");
assert!(rendered.contains("[some.other.section]"));
assert!(rendered.contains("[mcp_servers.github]"));
}
#[test]
fn read_or_empty_rejects_oversized_config() {
use std::fs::File;
let dir = tempdir().unwrap();
let cfg = dir.path().join("config.toml");
File::create(&cfg)
.unwrap()
.set_len(crate::util::fs_atomic::MAX_CONFIG_BYTES + 1)
.unwrap();
let err = read_or_empty(&cfg).unwrap_err();
assert!(matches!(
err,
crate::error::AgentConfigError::ConfigTooLarge { .. }
));
}
}