//! Support for `-c key=value` overrides shared across Codex CLI tools. //! //! This module provides a [`CliConfigOverrides`] struct that can be embedded //! into a `clap`-derived CLI struct using `#[clap(flatten)]`. Each occurrence //! of `-c key=value` (or `--config key=value`) will be collected as a raw //! string. Helper methods are provided to convert the raw strings into //! key/value pairs as well as to apply them onto a mutable //! `serde_json::Value` representing the configuration tree. use clap::ArgAction; use clap::Parser; use serde::de::Error as SerdeError; use toml::Value; /// CLI option that captures arbitrary configuration overrides specified as /// `-c key=value`. It intentionally keeps both halves **unparsed** so that the /// calling code can decide how to interpret the right-hand side. #[derive(Parser, Debug, Default, Clone)] pub struct CliConfigOverrides { /// Override a configuration value that would otherwise be loaded from /// `~/.codex/config.toml`. Use a dotted path (`foo.bar.baz`) to override /// nested values. The `value` portion is parsed as TOML. If it fails to /// parse as TOML, the raw string is used as a literal. /// /// Examples: /// - `-c model="o3"` /// - `-c 'sandbox_permissions=["disk-full-read-access"]'` /// - `-c shell_environment_policy.inherit=all` #[arg( short = 'c', long = "config", value_name = "key=value", action = ArgAction::Append, global = true, )] pub raw_overrides: Vec, } impl CliConfigOverrides { /// Parse the raw strings captured from the CLI into a list of `(path, /// value)` tuples where `value` is a `serde_json::Value`. pub fn parse_overrides(&self) -> Result, String> { self.raw_overrides .iter() .map(|s| { // Only split on the *first* '=' so values are free to contain // the character. let mut parts = s.splitn(2, '='); let key = match parts.next() { Some(k) => k.trim(), None => return Err("Override missing key".to_string()), }; let value_str = parts .next() .ok_or_else(|| format!("Invalid override (missing '='): {s}"))? .trim(); if key.is_empty() { return Err(format!("Empty key in override: {s}")); } // Attempt to parse as TOML. If that fails, treat it as a raw // string. This allows convenient usage such as // `-c model=o3` without the quotes. let value: Value = match parse_toml_value(value_str) { Ok(v) => v, Err(_) => { // Strip leading/trailing quotes if present let trimmed = value_str.trim().trim_matches(|c| c == '"' || c == '\''); Value::String(trimmed.to_string()) } }; Ok((canonicalize_override_key(key), value)) }) .collect() } /// Apply all parsed overrides onto `target`. Intermediate objects will be /// created as necessary. Values located at the destination path will be /// replaced. pub fn apply_on_value(&self, target: &mut Value) -> Result<(), String> { let overrides = self.parse_overrides()?; for (path, value) in overrides { apply_single_override(target, &path, value); } Ok(()) } } fn canonicalize_override_key(key: &str) -> String { if key == "use_legacy_landlock" { "features.use_legacy_landlock".to_string() } else { key.to_string() } } /// Apply a single override onto `root`, creating intermediate objects as /// necessary. fn apply_single_override(root: &mut Value, path: &str, value: Value) { use toml::value::Table; let parts: Vec<&str> = path.split('.').collect(); let mut current = root; for (i, part) in parts.iter().enumerate() { let is_last = i == parts.len() - 1; if is_last { match current { Value::Table(tbl) => { tbl.insert((*part).to_string(), value); } _ => { let mut tbl = Table::new(); tbl.insert((*part).to_string(), value); *current = Value::Table(tbl); } } return; } // Traverse or create intermediate table. match current { Value::Table(tbl) => { current = tbl .entry((*part).to_string()) .or_insert_with(|| Value::Table(Table::new())); } _ => { *current = Value::Table(Table::new()); if let Value::Table(tbl) = current { current = tbl .entry((*part).to_string()) .or_insert_with(|| Value::Table(Table::new())); } } } } } fn parse_toml_value(raw: &str) -> Result { let wrapped = format!("_x_ = {raw}"); let table: toml::Table = toml::from_str(&wrapped)?; table .get("_x_") .cloned() .ok_or_else(|| SerdeError::custom("missing sentinel key")) } #[cfg(test)] mod tests { use super::*; #[test] fn parses_basic_scalar() { let v = parse_toml_value("42").expect("parse"); assert_eq!(v.as_integer(), Some(42)); } #[test] fn parses_bool() { let true_literal = parse_toml_value("true").expect("parse"); assert_eq!(true_literal.as_bool(), Some(true)); let false_literal = parse_toml_value("false").expect("parse"); assert_eq!(false_literal.as_bool(), Some(false)); } #[test] fn fails_on_unquoted_string() { assert!(parse_toml_value("hello").is_err()); } #[test] fn parses_array() { let v = parse_toml_value("[1, 2, 3]").expect("parse"); let arr = v.as_array().expect("array"); assert_eq!(arr.len(), 3); } #[test] fn canonicalizes_use_legacy_landlock_alias() { let overrides = CliConfigOverrides { raw_overrides: vec!["use_legacy_landlock=true".to_string()], }; let parsed = overrides.parse_overrides().expect("parse_overrides"); assert_eq!(parsed[0].0.as_str(), "features.use_legacy_landlock"); assert_eq!(parsed[0].1.as_bool(), Some(true)); } #[test] fn parses_inline_table() { let v = parse_toml_value("{a = 1, b = 2}").expect("parse"); let tbl = v.as_table().expect("table"); assert_eq!(tbl.get("a").unwrap().as_integer(), Some(1)); assert_eq!(tbl.get("b").unwrap().as_integer(), Some(2)); } }