feat: add support for -c/--config to override individual config items (#1137)
This PR introduces support for `-c`/`--config` so users can override individual config values on the command line using `--config name=value`. Example: ``` codex --config model=o4-mini ``` Making it possible to set arbitrary config values on the command line results in a more flexible configuration scheme and makes it easier to provide single-line examples that can be copy-pasted from documentation. Effectively, it means there are four levels of configuration for some values: - Default value (e.g., `model` currently defaults to `o4-mini`) - Value in `config.toml` (e.g., user could override the default to be `model = "o3"` in their `config.toml`) - Specifying `-c` or `--config` to override `model` (e.g., user can include `-c model=o3` in their list of args to Codex) - If available, a config-specific flag can be used, which takes precedence over `-c` (e.g., user can specify `--model o3` in their list of args to Codex) Now that it is possible to specify anything that could be configured in `config.toml` on the command line using `-c`, we do not need to have a custom flag for every possible config option (which can clutter the output of `--help`). To that end, as part of this PR, we drop support for the `--disable-response-storage` flag, as users can now specify `-c disable_response_storage=true` to get the equivalent functionality. Under the hood, this works by loading the `config.toml` into a `toml::Value`. Then for each `key=value`, we create a small synthetic TOML file with `value` so that we can run the TOML parser to get the equivalent `toml::Value`. We then parse `key` to determine the point in the original `toml::Value` to do the insert/replace. Once all of the overrides from `-c` args have been applied, the `toml::Value` is deserialized into a `ConfigToml` and then the `ConfigOverrides` are applied, as before.
This commit is contained in:
3
codex-rs/Cargo.lock
generated
3
codex-rs/Cargo.lock
generated
@@ -506,6 +506,8 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"codex-core",
|
"codex-core",
|
||||||
|
"serde",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -634,6 +636,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_common::SandboxPermissionOption;
|
use codex_common::SandboxPermissionOption;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::config::ConfigOverrides;
|
use codex_core::config::ConfigOverrides;
|
||||||
@@ -20,12 +21,14 @@ pub async fn run_command_under_seatbelt(
|
|||||||
let SeatbeltCommand {
|
let SeatbeltCommand {
|
||||||
full_auto,
|
full_auto,
|
||||||
sandbox,
|
sandbox,
|
||||||
|
config_overrides,
|
||||||
command,
|
command,
|
||||||
} = command;
|
} = command;
|
||||||
run_command_under_sandbox(
|
run_command_under_sandbox(
|
||||||
full_auto,
|
full_auto,
|
||||||
sandbox,
|
sandbox,
|
||||||
command,
|
command,
|
||||||
|
config_overrides,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
SandboxType::Seatbelt,
|
SandboxType::Seatbelt,
|
||||||
)
|
)
|
||||||
@@ -39,12 +42,14 @@ pub async fn run_command_under_landlock(
|
|||||||
let LandlockCommand {
|
let LandlockCommand {
|
||||||
full_auto,
|
full_auto,
|
||||||
sandbox,
|
sandbox,
|
||||||
|
config_overrides,
|
||||||
command,
|
command,
|
||||||
} = command;
|
} = command;
|
||||||
run_command_under_sandbox(
|
run_command_under_sandbox(
|
||||||
full_auto,
|
full_auto,
|
||||||
sandbox,
|
sandbox,
|
||||||
command,
|
command,
|
||||||
|
config_overrides,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
SandboxType::Landlock,
|
SandboxType::Landlock,
|
||||||
)
|
)
|
||||||
@@ -60,16 +65,22 @@ async fn run_command_under_sandbox(
|
|||||||
full_auto: bool,
|
full_auto: bool,
|
||||||
sandbox: SandboxPermissionOption,
|
sandbox: SandboxPermissionOption,
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
|
config_overrides: CliConfigOverrides,
|
||||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||||
sandbox_type: SandboxType,
|
sandbox_type: SandboxType,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
|
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
|
||||||
let cwd = std::env::current_dir()?;
|
let cwd = std::env::current_dir()?;
|
||||||
let config = Config::load_with_overrides(ConfigOverrides {
|
let config = Config::load_with_cli_overrides(
|
||||||
sandbox_policy: Some(sandbox_policy),
|
config_overrides
|
||||||
codex_linux_sandbox_exe,
|
.parse_overrides()
|
||||||
..Default::default()
|
.map_err(anyhow::Error::msg)?,
|
||||||
})?;
|
ConfigOverrides {
|
||||||
|
sandbox_policy: Some(sandbox_policy),
|
||||||
|
codex_linux_sandbox_exe,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
let stdio_policy = StdioPolicy::Inherit;
|
let stdio_policy = StdioPolicy::Inherit;
|
||||||
let env = create_env(&config.shell_environment_policy);
|
let env = create_env(&config.shell_environment_policy);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ mod exit_status;
|
|||||||
pub mod proto;
|
pub mod proto;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_common::SandboxPermissionOption;
|
use codex_common::SandboxPermissionOption;
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -14,6 +15,9 @@ pub struct SeatbeltCommand {
|
|||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
pub sandbox: SandboxPermissionOption,
|
pub sandbox: SandboxPermissionOption,
|
||||||
|
|
||||||
|
#[clap(skip)]
|
||||||
|
pub config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
/// Full command args to run under seatbelt.
|
/// Full command args to run under seatbelt.
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
pub command: Vec<String>,
|
pub command: Vec<String>,
|
||||||
@@ -28,6 +32,9 @@ pub struct LandlockCommand {
|
|||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
pub sandbox: SandboxPermissionOption,
|
pub sandbox: SandboxPermissionOption,
|
||||||
|
|
||||||
|
#[clap(skip)]
|
||||||
|
pub config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
/// Full command args to run under landlock.
|
/// Full command args to run under landlock.
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
pub command: Vec<String>,
|
pub command: Vec<String>,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use clap::Parser;
|
|||||||
use codex_cli::LandlockCommand;
|
use codex_cli::LandlockCommand;
|
||||||
use codex_cli::SeatbeltCommand;
|
use codex_cli::SeatbeltCommand;
|
||||||
use codex_cli::proto;
|
use codex_cli::proto;
|
||||||
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_exec::Cli as ExecCli;
|
use codex_exec::Cli as ExecCli;
|
||||||
use codex_tui::Cli as TuiCli;
|
use codex_tui::Cli as TuiCli;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -19,6 +20,9 @@ use crate::proto::ProtoCli;
|
|||||||
subcommand_negates_reqs = true
|
subcommand_negates_reqs = true
|
||||||
)]
|
)]
|
||||||
struct MultitoolCli {
|
struct MultitoolCli {
|
||||||
|
#[clap(flatten)]
|
||||||
|
pub config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
interactive: TuiCli,
|
interactive: TuiCli,
|
||||||
|
|
||||||
@@ -73,28 +77,34 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
|||||||
|
|
||||||
match cli.subcommand {
|
match cli.subcommand {
|
||||||
None => {
|
None => {
|
||||||
codex_tui::run_main(cli.interactive, codex_linux_sandbox_exe)?;
|
let mut tui_cli = cli.interactive;
|
||||||
|
prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides);
|
||||||
|
codex_tui::run_main(tui_cli, codex_linux_sandbox_exe)?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Exec(exec_cli)) => {
|
Some(Subcommand::Exec(mut exec_cli)) => {
|
||||||
|
prepend_config_flags(&mut exec_cli.config_overrides, cli.config_overrides);
|
||||||
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
|
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Mcp) => {
|
Some(Subcommand::Mcp) => {
|
||||||
codex_mcp_server::run_main(codex_linux_sandbox_exe).await?;
|
codex_mcp_server::run_main(codex_linux_sandbox_exe).await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Proto(proto_cli)) => {
|
Some(Subcommand::Proto(mut proto_cli)) => {
|
||||||
|
prepend_config_flags(&mut proto_cli.config_overrides, cli.config_overrides);
|
||||||
proto::run_main(proto_cli).await?;
|
proto::run_main(proto_cli).await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Debug(debug_args)) => match debug_args.cmd {
|
Some(Subcommand::Debug(debug_args)) => match debug_args.cmd {
|
||||||
DebugCommand::Seatbelt(seatbelt_command) => {
|
DebugCommand::Seatbelt(mut seatbelt_cli) => {
|
||||||
|
prepend_config_flags(&mut seatbelt_cli.config_overrides, cli.config_overrides);
|
||||||
codex_cli::debug_sandbox::run_command_under_seatbelt(
|
codex_cli::debug_sandbox::run_command_under_seatbelt(
|
||||||
seatbelt_command,
|
seatbelt_cli,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
DebugCommand::Landlock(landlock_command) => {
|
DebugCommand::Landlock(mut landlock_cli) => {
|
||||||
|
prepend_config_flags(&mut landlock_cli.config_overrides, cli.config_overrides);
|
||||||
codex_cli::debug_sandbox::run_command_under_landlock(
|
codex_cli::debug_sandbox::run_command_under_landlock(
|
||||||
landlock_command,
|
landlock_cli,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -104,3 +114,14 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepend root-level overrides so they have lower precedence than
|
||||||
|
/// CLI-specific ones specified after the subcommand (if any).
|
||||||
|
fn prepend_config_flags(
|
||||||
|
subcommand_config_overrides: &mut CliConfigOverrides,
|
||||||
|
cli_config_overrides: CliConfigOverrides,
|
||||||
|
) {
|
||||||
|
subcommand_config_overrides
|
||||||
|
.raw_overrides
|
||||||
|
.splice(0..0, cli_config_overrides.raw_overrides);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::io::IsTerminal;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_core::Codex;
|
use codex_core::Codex;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::config::ConfigOverrides;
|
use codex_core::config::ConfigOverrides;
|
||||||
@@ -13,9 +14,12 @@ use tracing::error;
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct ProtoCli {}
|
pub struct ProtoCli {
|
||||||
|
#[clap(skip)]
|
||||||
|
pub config_overrides: CliConfigOverrides,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_main(_opts: ProtoCli) -> anyhow::Result<()> {
|
pub async fn run_main(opts: ProtoCli) -> anyhow::Result<()> {
|
||||||
if std::io::stdin().is_terminal() {
|
if std::io::stdin().is_terminal() {
|
||||||
anyhow::bail!("Protocol mode expects stdin to be a pipe, not a terminal");
|
anyhow::bail!("Protocol mode expects stdin to be a pipe, not a terminal");
|
||||||
}
|
}
|
||||||
@@ -24,7 +28,12 @@ pub async fn run_main(_opts: ProtoCli) -> anyhow::Result<()> {
|
|||||||
.with_writer(std::io::stderr)
|
.with_writer(std::io::stderr)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let config = Config::load_with_overrides(ConfigOverrides::default())?;
|
let ProtoCli { config_overrides } = opts;
|
||||||
|
let overrides_vec = config_overrides
|
||||||
|
.parse_overrides()
|
||||||
|
.map_err(anyhow::Error::msg)?;
|
||||||
|
|
||||||
|
let config = Config::load_with_cli_overrides(overrides_vec, ConfigOverrides::default())?;
|
||||||
let ctrl_c = notify_on_sigint();
|
let ctrl_c = notify_on_sigint();
|
||||||
let (codex, _init_id) = Codex::spawn(config, ctrl_c.clone()).await?;
|
let (codex, _init_id) = Codex::spawn(config, ctrl_c.clone()).await?;
|
||||||
let codex = Arc::new(codex);
|
let codex = Arc::new(codex);
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4", features = ["derive", "wrap_help"], optional = true }
|
clap = { version = "4", features = ["derive", "wrap_help"], optional = true }
|
||||||
codex-core = { path = "../core" }
|
codex-core = { path = "../core" }
|
||||||
|
toml = { version = "0.8", optional = true }
|
||||||
|
serde = { version = "1", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Separate feature so that `clap` is not a mandatory dependency.
|
# Separate feature so that `clap` is not a mandatory dependency.
|
||||||
cli = ["clap"]
|
cli = ["clap", "toml", "serde"]
|
||||||
elapsed = []
|
elapsed = []
|
||||||
|
|||||||
170
codex-rs/common/src/config_override.rs
Normal file
170
codex-rs/common/src/config_override.rs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
//! 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 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-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<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 parse_toml_value(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 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<Value, toml::de::Error> {
|
||||||
|
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(all(test, feature = "cli"))]
|
||||||
|
#[allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
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 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 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,3 +8,9 @@ pub mod elapsed;
|
|||||||
pub use approval_mode_cli_arg::ApprovalModeCliArg;
|
pub use approval_mode_cli_arg::ApprovalModeCliArg;
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
pub use approval_mode_cli_arg::SandboxPermissionOption;
|
pub use approval_mode_cli_arg::SandboxPermissionOption;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "cli", test))]
|
||||||
|
mod config_override;
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
pub use config_override::CliConfigOverrides;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use serde::Deserialize;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use toml::Value as TomlValue;
|
||||||
|
|
||||||
/// Maximum number of bytes of the documentation that will be embedded. Larger
|
/// Maximum number of bytes of the documentation that will be embedded. Larger
|
||||||
/// files are *silently truncated* to this size so we do not take up too much of
|
/// files are *silently truncated* to this size so we do not take up too much of
|
||||||
@@ -108,6 +109,108 @@ pub struct Config {
|
|||||||
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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`.
|
||||||
|
pub fn load_with_cli_overrides(
|
||||||
|
cli_overrides: Vec<(String, TomlValue)>,
|
||||||
|
overrides: ConfigOverrides,
|
||||||
|
) -> std::io::Result<Self> {
|
||||||
|
// Resolve the directory that stores Codex state (e.g. ~/.codex or the
|
||||||
|
// value of $CODEX_HOME) so we can embed it into the resulting
|
||||||
|
// `Config` instance.
|
||||||
|
let codex_home = find_codex_home()?;
|
||||||
|
|
||||||
|
// Step 1: parse `config.toml` into a generic JSON value.
|
||||||
|
let mut root_value = load_config_as_toml(&codex_home)?;
|
||||||
|
|
||||||
|
// Step 2: apply the `-c` overrides.
|
||||||
|
for (path, value) in cli_overrides.into_iter() {
|
||||||
|
apply_toml_override(&mut root_value, &path, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: deserialize into `ConfigToml` so that Serde can enforce the
|
||||||
|
// correct types.
|
||||||
|
let cfg: ConfigToml = root_value.try_into().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_HOME/config.toml` and return it as a generic TOML value. Returns
|
||||||
|
/// an empty TOML table when the file does not exist.
|
||||||
|
fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
|
||||||
|
let config_path = codex_home.join("config.toml");
|
||||||
|
match std::fs::read_to_string(&config_path) {
|
||||||
|
Ok(contents) => match toml::from_str::<TomlValue>(&contents) {
|
||||||
|
Ok(val) => Ok(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(TomlValue::Table(Default::default()))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to read config.toml: {e}");
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a single dotted-path override onto a TOML value.
|
||||||
|
fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
|
||||||
|
use toml::value::Table;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
TomlValue::Table(table) => {
|
||||||
|
table.insert(segment.to_string(), value);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.insert(segment.to_string(), value);
|
||||||
|
*current = TomlValue::Table(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverse or create intermediate object.
|
||||||
|
match current {
|
||||||
|
TomlValue::Table(table) => {
|
||||||
|
current = table
|
||||||
|
.entry(segment.to_string())
|
||||||
|
.or_insert_with(|| TomlValue::Table(Table::new()));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
*current = TomlValue::Table(Table::new());
|
||||||
|
if let TomlValue::Table(tbl) = current {
|
||||||
|
current = tbl
|
||||||
|
.entry(segment.to_string())
|
||||||
|
.or_insert_with(|| TomlValue::Table(Table::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Base config deserialized from ~/.codex/config.toml.
|
/// Base config deserialized from ~/.codex/config.toml.
|
||||||
#[derive(Deserialize, Debug, Clone, Default)]
|
#[derive(Deserialize, Debug, Clone, Default)]
|
||||||
pub struct ConfigToml {
|
pub struct ConfigToml {
|
||||||
@@ -171,29 +274,6 @@ pub struct ConfigToml {
|
|||||||
pub tui: Option<Tui>,
|
pub tui: Option<Tui>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigToml {
|
|
||||||
/// Attempt to parse the file at `~/.codex/config.toml`. If it does not
|
|
||||||
/// exist, return a default config. Though if it exists and cannot be
|
|
||||||
/// parsed, report that to the user and force them to fix it.
|
|
||||||
fn load_from_toml(codex_home: &Path) -> std::io::Result<Self> {
|
|
||||||
let config_toml_path = codex_home.join("config.toml");
|
|
||||||
match std::fs::read_to_string(&config_toml_path) {
|
|
||||||
Ok(contents) => toml::from_str::<Self>(&contents).map_err(|e| {
|
|
||||||
tracing::error!("Failed to parse config.toml: {e}");
|
|
||||||
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(Self::default())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to read config.toml: {e}");
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_sandbox_permissions<'de, D>(
|
fn deserialize_sandbox_permissions<'de, D>(
|
||||||
deserializer: D,
|
deserializer: D,
|
||||||
) -> Result<Option<Vec<SandboxPermission>>, D::Error>
|
) -> Result<Option<Vec<SandboxPermission>>, D::Error>
|
||||||
@@ -227,28 +307,12 @@ pub struct ConfigOverrides {
|
|||||||
pub cwd: Option<PathBuf>,
|
pub cwd: Option<PathBuf>,
|
||||||
pub approval_policy: Option<AskForApproval>,
|
pub approval_policy: Option<AskForApproval>,
|
||||||
pub sandbox_policy: Option<SandboxPolicy>,
|
pub sandbox_policy: Option<SandboxPolicy>,
|
||||||
pub disable_response_storage: Option<bool>,
|
|
||||||
pub model_provider: Option<String>,
|
pub model_provider: Option<String>,
|
||||||
pub config_profile: Option<String>,
|
pub config_profile: Option<String>,
|
||||||
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Load configuration, optionally applying overrides (CLI flags). Merges
|
|
||||||
/// ~/.codex/config.toml, ~/.codex/instructions.md, embedded defaults, and
|
|
||||||
/// any values provided in `overrides` (highest precedence).
|
|
||||||
pub fn load_with_overrides(overrides: ConfigOverrides) -> std::io::Result<Self> {
|
|
||||||
// Resolve the directory that stores Codex state (e.g. ~/.codex or the
|
|
||||||
// value of $CODEX_HOME) so we can embed it into the resulting
|
|
||||||
// `Config` instance.
|
|
||||||
let codex_home = find_codex_home()?;
|
|
||||||
|
|
||||||
let cfg: ConfigToml = ConfigToml::load_from_toml(&codex_home)?;
|
|
||||||
tracing::warn!("Config parsed from config.toml: {cfg:?}");
|
|
||||||
|
|
||||||
Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Meant to be used exclusively for tests: `load_with_overrides()` should
|
/// Meant to be used exclusively for tests: `load_with_overrides()` should
|
||||||
/// be used in all other cases.
|
/// be used in all other cases.
|
||||||
pub fn load_from_base_config_with_overrides(
|
pub fn load_from_base_config_with_overrides(
|
||||||
@@ -264,7 +328,6 @@ impl Config {
|
|||||||
cwd,
|
cwd,
|
||||||
approval_policy,
|
approval_policy,
|
||||||
sandbox_policy,
|
sandbox_policy,
|
||||||
disable_response_storage,
|
|
||||||
model_provider,
|
model_provider,
|
||||||
config_profile: config_profile_key,
|
config_profile: config_profile_key,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
@@ -356,8 +419,8 @@ impl Config {
|
|||||||
.unwrap_or_else(AskForApproval::default),
|
.unwrap_or_else(AskForApproval::default),
|
||||||
sandbox_policy,
|
sandbox_policy,
|
||||||
shell_environment_policy,
|
shell_environment_policy,
|
||||||
disable_response_storage: disable_response_storage
|
disable_response_storage: config_profile
|
||||||
.or(config_profile.disable_response_storage)
|
.disable_response_storage
|
||||||
.or(cfg.disable_response_storage)
|
.or(cfg.disable_response_storage)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
notify: cfg.notify,
|
notify: cfg.notify,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ pub struct Tui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum ShellEnvironmentPolicyInherit {
|
pub enum ShellEnvironmentPolicyInherit {
|
||||||
/// "Core" environment variables for the platform. On UNIX, this would
|
/// "Core" environment variables for the platform. On UNIX, this would
|
||||||
/// include HOME, LOGNAME, PATH, SHELL, and USER, among others.
|
/// include HOME, LOGNAME, PATH, SHELL, and USER, among others.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_common::SandboxPermissionOption;
|
use codex_common::SandboxPermissionOption;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -33,9 +34,8 @@ pub struct Cli {
|
|||||||
#[arg(long = "skip-git-repo-check", default_value_t = false)]
|
#[arg(long = "skip-git-repo-check", default_value_t = false)]
|
||||||
pub skip_git_repo_check: bool,
|
pub skip_git_repo_check: bool,
|
||||||
|
|
||||||
/// Disable server‑side response storage (sends the full conversation context with every request)
|
#[clap(skip)]
|
||||||
#[arg(long = "disable-response-storage", default_value_t = false)]
|
pub config_overrides: CliConfigOverrides,
|
||||||
pub disable_response_storage: bool,
|
|
||||||
|
|
||||||
/// Specifies color settings for use in the output.
|
/// Specifies color settings for use in the output.
|
||||||
#[arg(long = "color", value_enum, default_value_t = Color::Auto)]
|
#[arg(long = "color", value_enum, default_value_t = Color::Auto)]
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
|||||||
sandbox,
|
sandbox,
|
||||||
cwd,
|
cwd,
|
||||||
skip_git_repo_check,
|
skip_git_repo_check,
|
||||||
disable_response_storage,
|
|
||||||
color,
|
color,
|
||||||
last_message_file,
|
last_message_file,
|
||||||
prompt,
|
prompt,
|
||||||
|
config_overrides,
|
||||||
} = cli;
|
} = cli;
|
||||||
|
|
||||||
let (stdout_with_ansi, stderr_with_ansi) = match color {
|
let (stdout_with_ansi, stderr_with_ansi) = match color {
|
||||||
@@ -63,16 +63,20 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
|||||||
// the user for approval.
|
// the user for approval.
|
||||||
approval_policy: Some(AskForApproval::Never),
|
approval_policy: Some(AskForApproval::Never),
|
||||||
sandbox_policy,
|
sandbox_policy,
|
||||||
disable_response_storage: if disable_response_storage {
|
|
||||||
Some(true)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)),
|
cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)),
|
||||||
model_provider: None,
|
model_provider: None,
|
||||||
codex_linux_sandbox_exe,
|
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 the effective configuration so users can see what Codex is using.
|
||||||
print_config_summary(&config, stdout_with_ansi);
|
print_config_summary(&config, stdout_with_ansi);
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,30 @@
|
|||||||
//! This allows us to ship a completely separate set of functionality as part
|
//! This allows us to ship a completely separate set of functionality as part
|
||||||
//! of the `codex-exec` binary.
|
//! of the `codex-exec` binary.
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_exec::Cli;
|
use codex_exec::Cli;
|
||||||
use codex_exec::run_main;
|
use codex_exec::run_main;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct TopCli {
|
||||||
|
#[clap(flatten)]
|
||||||
|
config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
inner: Cli,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move {
|
codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move {
|
||||||
let cli = Cli::parse();
|
let top_cli = TopCli::parse();
|
||||||
run_main(cli, codex_linux_sandbox_exe).await?;
|
// Merge root-level overrides into inner CLI struct so downstream logic remains unchanged.
|
||||||
|
let mut inner = top_cli.inner;
|
||||||
|
inner
|
||||||
|
.config_overrides
|
||||||
|
.raw_overrides
|
||||||
|
.splice(0..0, top_cli.config_overrides.raw_overrides);
|
||||||
|
|
||||||
|
run_main(inner, codex_linux_sandbox_exe).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ mcp-types = { path = "../mcp-types" }
|
|||||||
schemars = "0.8.22"
|
schemars = "0.8.22"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
toml = "0.8"
|
||||||
tracing = { version = "0.1.41", features = ["log"] }
|
tracing = { version = "0.1.41", features = ["log"] }
|
||||||
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = [
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
//! Configuration object accepted by the `codex` MCP tool-call.
|
//! Configuration object accepted by the `codex` MCP tool-call.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use codex_core::protocol::AskForApproval;
|
||||||
|
use codex_core::protocol::SandboxPolicy;
|
||||||
use mcp_types::Tool;
|
use mcp_types::Tool;
|
||||||
use mcp_types::ToolInputSchema;
|
use mcp_types::ToolInputSchema;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use schemars::r#gen::SchemaSettings;
|
use schemars::r#gen::SchemaSettings;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use codex_core::protocol::AskForApproval;
|
use crate::json_to_toml::json_to_toml;
|
||||||
use codex_core::protocol::SandboxPolicy;
|
|
||||||
|
|
||||||
/// Client-supplied configuration for a `codex` tool-call.
|
/// Client-supplied configuration for a `codex` tool-call.
|
||||||
#[derive(Debug, Clone, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, JsonSchema)]
|
||||||
@@ -41,12 +42,10 @@ pub(crate) struct CodexToolCallParam {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub sandbox_permissions: Option<Vec<CodexToolCallSandboxPermission>>,
|
pub sandbox_permissions: Option<Vec<CodexToolCallSandboxPermission>>,
|
||||||
|
|
||||||
/// Disable server-side response storage.
|
/// Individual config settings that will override what is in
|
||||||
|
/// CODEX_HOME/config.toml.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub disable_response_storage: Option<bool>,
|
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||||
// Custom system instructions.
|
|
||||||
// #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
// pub instructions: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create custom enums for use with `CodexToolCallApprovalPolicy` where we
|
// Create custom enums for use with `CodexToolCallApprovalPolicy` where we
|
||||||
@@ -155,7 +154,7 @@ impl CodexToolCallParam {
|
|||||||
cwd,
|
cwd,
|
||||||
approval_policy,
|
approval_policy,
|
||||||
sandbox_permissions,
|
sandbox_permissions,
|
||||||
disable_response_storage,
|
config: cli_overrides,
|
||||||
} = self;
|
} = self;
|
||||||
let sandbox_policy = sandbox_permissions.map(|perms| {
|
let sandbox_policy = sandbox_permissions.map(|perms| {
|
||||||
SandboxPolicy::from(perms.into_iter().map(Into::into).collect::<Vec<_>>())
|
SandboxPolicy::from(perms.into_iter().map(Into::into).collect::<Vec<_>>())
|
||||||
@@ -168,12 +167,17 @@ impl CodexToolCallParam {
|
|||||||
cwd: cwd.map(PathBuf::from),
|
cwd: cwd.map(PathBuf::from),
|
||||||
approval_policy: approval_policy.map(Into::into),
|
approval_policy: approval_policy.map(Into::into),
|
||||||
sandbox_policy,
|
sandbox_policy,
|
||||||
disable_response_storage,
|
|
||||||
model_provider: None,
|
model_provider: None,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cfg = codex_core::config::Config::load_with_overrides(overrides)?;
|
let cli_overrides = cli_overrides
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, json_to_toml(v)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let cfg = codex_core::config::Config::load_with_cli_overrides(cli_overrides, overrides)?;
|
||||||
|
|
||||||
Ok((prompt, cfg))
|
Ok((prompt, cfg))
|
||||||
}
|
}
|
||||||
@@ -216,14 +220,15 @@ mod tests {
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"description": "Individual config settings that will override what is in CODEX_HOME/config.toml.",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"cwd": {
|
"cwd": {
|
||||||
"description": "Working directory for the session. If relative, it is resolved against the server process's current working directory.",
|
"description": "Working directory for the session. If relative, it is resolved against the server process's current working directory.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"disable-response-storage": {
|
|
||||||
"description": "Disable server-side response storage.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"model": {
|
"model": {
|
||||||
"description": "Optional override for the model name (e.g. \"o3\", \"o4-mini\")",
|
"description": "Optional override for the model name (e.g. \"o3\", \"o4-mini\")",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
84
codex-rs/mcp-server/src/json_to_toml.rs
Normal file
84
codex-rs/mcp-server/src/json_to_toml.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
use toml::Value as TomlValue;
|
||||||
|
|
||||||
|
/// Convert a `serde_json::Value` into a semantically equivalent `toml::Value`.
|
||||||
|
pub(crate) fn json_to_toml(v: JsonValue) -> TomlValue {
|
||||||
|
match v {
|
||||||
|
JsonValue::Null => TomlValue::String(String::new()),
|
||||||
|
JsonValue::Bool(b) => TomlValue::Boolean(b),
|
||||||
|
JsonValue::Number(n) => {
|
||||||
|
if let Some(i) = n.as_i64() {
|
||||||
|
TomlValue::Integer(i)
|
||||||
|
} else if let Some(f) = n.as_f64() {
|
||||||
|
TomlValue::Float(f)
|
||||||
|
} else {
|
||||||
|
TomlValue::String(n.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JsonValue::String(s) => TomlValue::String(s),
|
||||||
|
JsonValue::Array(arr) => TomlValue::Array(arr.into_iter().map(json_to_toml).collect()),
|
||||||
|
JsonValue::Object(map) => {
|
||||||
|
let tbl = map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, json_to_toml(v)))
|
||||||
|
.collect::<toml::value::Table>();
|
||||||
|
TomlValue::Table(tbl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_number_to_toml() {
|
||||||
|
let json_value = json!(123);
|
||||||
|
assert_eq!(TomlValue::Integer(123), json_to_toml(json_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_array_to_toml() {
|
||||||
|
let json_value = json!([true, 1]);
|
||||||
|
assert_eq!(
|
||||||
|
TomlValue::Array(vec![TomlValue::Boolean(true), TomlValue::Integer(1)]),
|
||||||
|
json_to_toml(json_value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_bool_to_toml() {
|
||||||
|
let json_value = json!(false);
|
||||||
|
assert_eq!(TomlValue::Boolean(false), json_to_toml(json_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_float_to_toml() {
|
||||||
|
let json_value = json!(1.25);
|
||||||
|
assert_eq!(TomlValue::Float(1.25), json_to_toml(json_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_null_to_toml() {
|
||||||
|
let json_value = serde_json::Value::Null;
|
||||||
|
assert_eq!(TomlValue::String(String::new()), json_to_toml(json_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_object_nested() {
|
||||||
|
let json_value = json!({ "outer": { "inner": 2 } });
|
||||||
|
let expected = {
|
||||||
|
let mut inner = toml::value::Table::new();
|
||||||
|
inner.insert("inner".into(), TomlValue::Integer(2));
|
||||||
|
|
||||||
|
let mut outer = toml::value::Table::new();
|
||||||
|
outer.insert("outer".into(), TomlValue::Table(inner));
|
||||||
|
TomlValue::Table(outer)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(json_to_toml(json_value), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ use tracing::info;
|
|||||||
|
|
||||||
mod codex_tool_config;
|
mod codex_tool_config;
|
||||||
mod codex_tool_runner;
|
mod codex_tool_runner;
|
||||||
|
mod json_to_toml;
|
||||||
mod message_processor;
|
mod message_processor;
|
||||||
|
|
||||||
use crate::message_processor::MessageProcessor;
|
use crate::message_processor::MessageProcessor;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use codex_common::ApprovalModeCliArg;
|
use codex_common::ApprovalModeCliArg;
|
||||||
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_common::SandboxPermissionOption;
|
use codex_common::SandboxPermissionOption;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -40,7 +41,6 @@ pub struct Cli {
|
|||||||
#[arg(long = "skip-git-repo-check", default_value_t = false)]
|
#[arg(long = "skip-git-repo-check", default_value_t = false)]
|
||||||
pub skip_git_repo_check: bool,
|
pub skip_git_repo_check: bool,
|
||||||
|
|
||||||
/// Disable server‑side response storage (sends the full conversation context with every request)
|
#[clap(skip)]
|
||||||
#[arg(long = "disable-response-storage", default_value_t = false)]
|
pub config_overrides: CliConfigOverrides,
|
||||||
pub disable_response_storage: bool,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,18 +54,23 @@ pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> std::io::
|
|||||||
model: cli.model.clone(),
|
model: cli.model.clone(),
|
||||||
approval_policy,
|
approval_policy,
|
||||||
sandbox_policy,
|
sandbox_policy,
|
||||||
disable_response_storage: if cli.disable_response_storage {
|
|
||||||
Some(true)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)),
|
cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)),
|
||||||
model_provider: None,
|
model_provider: None,
|
||||||
config_profile: cli.config_profile.clone(),
|
config_profile: cli.config_profile.clone(),
|
||||||
codex_linux_sandbox_exe,
|
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)]
|
#[allow(clippy::print_stderr)]
|
||||||
match Config::load_with_overrides(overrides) {
|
match Config::load_with_cli_overrides(cli_kv_overrides, overrides) {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error loading configuration: {err}");
|
eprintln!("Error loading configuration: {err}");
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_tui::Cli;
|
use codex_tui::Cli;
|
||||||
use codex_tui::run_main;
|
use codex_tui::run_main;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct TopCli {
|
||||||
|
#[clap(flatten)]
|
||||||
|
config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
inner: Cli,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move {
|
codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move {
|
||||||
let cli = Cli::parse();
|
let top_cli = TopCli::parse();
|
||||||
run_main(cli, codex_linux_sandbox_exe)?;
|
let mut inner = top_cli.inner;
|
||||||
|
inner
|
||||||
|
.config_overrides
|
||||||
|
.raw_overrides
|
||||||
|
.splice(0..0, top_cli.config_overrides.raw_overrides);
|
||||||
|
run_main(inner, codex_linux_sandbox_exe)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user