mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
feat: add support for -c/--config to override individual config items
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -506,6 +506,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codex-core",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -9,6 +9,7 @@ workspace = true
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive", "wrap_help"], optional = true }
|
||||
codex-core = { path = "../core" }
|
||||
serde_json = { version = "1" }
|
||||
|
||||
[features]
|
||||
# Separate feature so that `clap` is not a mandatory dependency.
|
||||
|
||||
130
codex-rs/common/src/config_override.rs
Normal file
130
codex-rs/common/src/config_override.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
//! 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_json::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 JSON. If it fails to
|
||||
/// parse as JSON, the raw string is used as a literal.
|
||||
///
|
||||
/// Examples:
|
||||
/// - `-c model="o4-mini"`
|
||||
/// - `-c sandbox-permissions=[\"disk-write-cwd\"]`
|
||||
/// - `-c history.max-lines=0`
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "config",
|
||||
value_name = "key=value",
|
||||
action = ArgAction::Append,
|
||||
)]
|
||||
pub raw_overrides: Vec<String>,
|
||||
}
|
||||
|
||||
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<Vec<(String, Value)>, 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 JSON. If that fails, treat it as a raw
|
||||
// string. This allows convenient usage such as
|
||||
// `-c model=o4-mini` without the quotes.
|
||||
let value: Value = match serde_json::from_str(value_str) {
|
||||
Ok(v) => v,
|
||||
Err(_) => Value::String(value_str.to_string()),
|
||||
};
|
||||
|
||||
Ok((key.to_string(), 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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a single override onto `root`, creating intermediate objects as
|
||||
/// necessary.
|
||||
fn apply_single_override(root: &mut Value, path: &str, value: Value) {
|
||||
use serde_json::Map;
|
||||
|
||||
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 {
|
||||
// Replace value at leaf.
|
||||
if let Value::Object(obj) = current {
|
||||
obj.insert(part.to_string(), value);
|
||||
} else {
|
||||
// Replace non-object with object containing the leaf.
|
||||
*current = Value::Object({
|
||||
let mut m = Map::new();
|
||||
m.insert(part.to_string(), value);
|
||||
m
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse or create intermediate object.
|
||||
match current {
|
||||
Value::Object(obj) => {
|
||||
current = obj
|
||||
.entry(part.to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()));
|
||||
}
|
||||
_ => {
|
||||
// Non-object encountered, replace with object so we can
|
||||
// continue traversal.
|
||||
*current = Value::Object(Map::new());
|
||||
if let Value::Object(obj) = current {
|
||||
current = obj
|
||||
.entry((*part).to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,9 @@ pub mod elapsed;
|
||||
pub use approval_mode_cli_arg::ApprovalModeCliArg;
|
||||
#[cfg(feature = "cli")]
|
||||
pub use approval_mode_cli_arg::SandboxPermissionOption;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
mod config_override;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub use config_override::CliConfigOverrides;
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::protocol::SandboxPermission;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use dirs::home_dir;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -108,6 +109,126 @@ pub struct Config {
|
||||
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// CLI-level JSON overrides ("-c key=value") support //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
impl Config {
|
||||
/// Load configuration with *generic* CLI overrides (`-c key=value`) applied **in between**
|
||||
/// the values parsed from `config.toml` and the strongly-typed overrides specified via
|
||||
/// [`ConfigOverrides`].
|
||||
///
|
||||
/// The precedence order is therefore: `config.toml` < `-c` overrides < `ConfigOverrides`.
|
||||
///
|
||||
/// Most CLI binaries should call this method – `load_with_overrides()` is still available
|
||||
/// for non-CLI callers (tests, servers) which do not accept the `-c` flag.
|
||||
pub fn load_with_cli_overrides(
|
||||
cli_overrides: Vec<(String, JsonValue)>,
|
||||
overrides: ConfigOverrides,
|
||||
) -> std::io::Result<Self> {
|
||||
// Resolve CODEX_HOME first; needed by sandbox deserializer later.
|
||||
let codex_home = find_codex_home()?;
|
||||
|
||||
// Step 1: parse `config.toml` into a generic JSON value.
|
||||
let mut root_value = load_config_as_json(&codex_home)?;
|
||||
|
||||
// Step 2: apply the `-c` overrides.
|
||||
for (path, value) in cli_overrides.into_iter() {
|
||||
apply_json_override(&mut root_value, &path, value);
|
||||
}
|
||||
|
||||
// Step 3: deserialize into `ConfigToml` so that Serde can enforce the
|
||||
// correct types.
|
||||
let cfg: ConfigToml = serde_json::from_value(root_value).map_err(|e| {
|
||||
tracing::error!("Failed to deserialize overridden config: {e}");
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
||||
})?;
|
||||
|
||||
// Step 4: merge with the strongly-typed overrides.
|
||||
Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `~/.codex/config.toml` (or the resolved CODEX_HOME location) and
|
||||
/// return it as a generic JSON value. Returns an empty JSON object when the
|
||||
/// file does not exist.
|
||||
fn load_config_as_json(codex_home: &Path) -> std::io::Result<JsonValue> {
|
||||
let config_path = codex_home.join("config.toml");
|
||||
match std::fs::read_to_string(&config_path) {
|
||||
Ok(contents) => {
|
||||
// Parse as TOML first, then convert to JSON for easier mutation.
|
||||
match toml::from_str::<toml::Value>(&contents) {
|
||||
Ok(toml_val) => {
|
||||
let json_val = serde_json::to_value(toml_val).map_err(|e| {
|
||||
tracing::error!("Failed to convert TOML config to JSON value: {e}");
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
||||
})?;
|
||||
Ok(json_val)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to parse config.toml: {e}");
|
||||
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
tracing::info!("config.toml not found, using defaults");
|
||||
Ok(JsonValue::Object(Default::default()))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read config.toml: {e}");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a single dotted-path override onto a JSON value.
|
||||
fn apply_json_override(root: &mut JsonValue, path: &str, value: JsonValue) {
|
||||
use serde_json::Map;
|
||||
|
||||
let segments: Vec<&str> = path.split('.').collect();
|
||||
let mut current = root;
|
||||
|
||||
for (idx, segment) in segments.iter().enumerate() {
|
||||
let is_last = idx == segments.len() - 1;
|
||||
|
||||
if is_last {
|
||||
match current {
|
||||
JsonValue::Object(map) => {
|
||||
map.insert(segment.to_string(), value);
|
||||
}
|
||||
_ => {
|
||||
*current = JsonValue::Object({
|
||||
let mut m = Map::new();
|
||||
m.insert(segment.to_string(), value);
|
||||
m
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse or create intermediate object.
|
||||
match current {
|
||||
JsonValue::Object(map) => {
|
||||
current = map
|
||||
.entry(segment.to_string())
|
||||
.or_insert_with(|| JsonValue::Object(Map::new()));
|
||||
}
|
||||
_ => {
|
||||
*current = JsonValue::Object(Map::new());
|
||||
if let JsonValue::Object(map) = current {
|
||||
// Safe unwrap: we just replaced current with an empty
|
||||
// object and immediately inserted the key.
|
||||
current = map
|
||||
.entry(segment.to_string())
|
||||
.or_insert_with(|| JsonValue::Object(Map::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base config deserialized from ~/.codex/config.toml.
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
pub struct ConfigToml {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use clap::Parser;
|
||||
use clap::ValueEnum;
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_common::SandboxPermissionOption;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -37,6 +38,9 @@ pub struct Cli {
|
||||
#[arg(long = "disable-response-storage", default_value_t = false)]
|
||||
pub disable_response_storage: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
/// Specifies color settings for use in the output.
|
||||
#[arg(long = "color", value_enum, default_value_t = Color::Auto)]
|
||||
pub color: Color,
|
||||
|
||||
@@ -38,6 +38,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
color,
|
||||
last_message_file,
|
||||
prompt,
|
||||
config_overrides,
|
||||
} = cli;
|
||||
|
||||
let (stdout_with_ansi, stderr_with_ansi) = match color {
|
||||
@@ -72,7 +73,16 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
model_provider: None,
|
||||
codex_linux_sandbox_exe,
|
||||
};
|
||||
let config = Config::load_with_overrides(overrides)?;
|
||||
// Parse `-c` overrides.
|
||||
let cli_kv_overrides = match config_overrides.parse_overrides() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error parsing -c overrides: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?;
|
||||
// Print the effective configuration so users can see what Codex is using.
|
||||
print_config_summary(&config, stdout_with_ansi);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use clap::Parser;
|
||||
use codex_common::ApprovalModeCliArg;
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_common::SandboxPermissionOption;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -43,4 +44,7 @@ pub struct Cli {
|
||||
/// Disable server‑side response storage (sends the full conversation context with every request)
|
||||
#[arg(long = "disable-response-storage", default_value_t = false)]
|
||||
pub disable_response_storage: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
}
|
||||
|
||||
@@ -64,8 +64,18 @@ pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> std::io::
|
||||
config_profile: cli.config_profile.clone(),
|
||||
codex_linux_sandbox_exe,
|
||||
};
|
||||
// Parse `-c` overrides from the CLI.
|
||||
let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
|
||||
Ok(v) => v,
|
||||
#[allow(clippy::print_stderr)]
|
||||
Err(e) => {
|
||||
eprintln!("Error parsing -c overrides: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
match Config::load_with_overrides(overrides) {
|
||||
match Config::load_with_cli_overrides(cli_kv_overrides, overrides) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
eprintln!("Error loading configuration: {err}");
|
||||
|
||||
Reference in New Issue
Block a user