feat: load defaults into Config and introduce ConfigOverrides (#677)

This changes how instantiating `Config` works and also adds
`approval_policy` and `sandbox_policy` as fields. The idea is:

* All fields of `Config` have appropriate default values.
* `Config` is initially loaded from `~/.codex/config.toml`, so values in
`config.toml` will override those defaults.
* Clients must instantiate `Config` via
`Config::load_with_overrides(ConfigOverrides)` where `ConfigOverrides`
has optional overrides that are expected to be settable based on CLI
flags.

The `Config` should be defined early in the program and then passed
down. Now functions like `init_codex()` take fewer individual parameters
because they can just take a `Config`.

Also, `Config::load()` used to fail silently if `~/.codex/config.toml`
had a parse error and fell back to the default config. This seemed
really bad because it wasn't clear why the values in my `config.toml`
weren't getting picked up. I changed things so that
`load_with_overrides()` returns `Result<Config>` and verified that the
various CLIs print a reasonable error if `config.toml` is malformed.

Finally, I also updated the TUI to show which **sandbox** value is being
used, as we do for other key values like **model** and **approval**.
This was also a reminder that the various values of `--sandbox` are
honored on Linux but not macOS today, so I added some TODOs about fixing
that.
This commit is contained in:
Michael Bolin
2025-04-27 21:47:50 -07:00
committed by GitHub
parent e9d16d3c2b
commit 4eda4dd772
23 changed files with 234 additions and 139 deletions

View File

@@ -5,6 +5,7 @@ use std::path::PathBuf;
use clap::ArgAction; use clap::ArgAction;
use clap::Parser; use clap::Parser;
use codex_core::SandboxModeCliArg;
use codex_exec::Cli as ExecCli; use codex_exec::Cli as ExecCli;
use codex_interactive::Cli as InteractiveCli; use codex_interactive::Cli as InteractiveCli;
use codex_repl::Cli as ReplCli; use codex_repl::Cli as ReplCli;
@@ -70,6 +71,10 @@ struct SeatbeltCommand {
#[arg(long = "writable-root", short = 'w', value_name = "DIR", action = ArgAction::Append, use_value_delimiter = false)] #[arg(long = "writable-root", short = 'w', value_name = "DIR", action = ArgAction::Append, use_value_delimiter = false)]
writable_roots: Vec<PathBuf>, writable_roots: Vec<PathBuf>,
/// Configure the process restrictions for the command.
#[arg(long = "sandbox", short = 's')]
sandbox_policy: SandboxModeCliArg,
/// Full command args to run under seatbelt. /// Full command args to run under seatbelt.
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]
command: Vec<String>, command: Vec<String>,
@@ -101,9 +106,10 @@ async fn main() -> anyhow::Result<()> {
Some(Subcommand::Debug(debug_args)) => match debug_args.cmd { Some(Subcommand::Debug(debug_args)) => match debug_args.cmd {
DebugCommand::Seatbelt(SeatbeltCommand { DebugCommand::Seatbelt(SeatbeltCommand {
command, command,
sandbox_policy,
writable_roots, writable_roots,
}) => { }) => {
seatbelt::run_seatbelt(command, writable_roots).await?; seatbelt::run_seatbelt(command, sandbox_policy.into(), writable_roots).await?;
} }
}, },
} }

View File

@@ -1,11 +1,13 @@
use codex_core::exec::create_seatbelt_command; use codex_core::exec::create_seatbelt_command;
use codex_core::protocol::SandboxPolicy;
use std::path::PathBuf; use std::path::PathBuf;
pub(crate) async fn run_seatbelt( pub(crate) async fn run_seatbelt(
command: Vec<String>, command: Vec<String>,
sandbox_policy: SandboxPolicy,
writable_roots: Vec<PathBuf>, writable_roots: Vec<PathBuf>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let seatbelt_command = create_seatbelt_command(command, &writable_roots); let seatbelt_command = create_seatbelt_command(command, sandbox_policy, &writable_roots);
let status = tokio::process::Command::new(seatbelt_command[0].clone()) let status = tokio::process::Command::new(seatbelt_command[0].clone())
.args(&seatbelt_command[1..]) .args(&seatbelt_command[1..])
.spawn() .spawn()

View File

@@ -6,7 +6,7 @@ use clap::ValueEnum;
use crate::protocol::AskForApproval; use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy; use crate::protocol::SandboxPolicy;
#[derive(Clone, Debug, ValueEnum)] #[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")] #[value(rename_all = "kebab-case")]
pub enum ApprovalModeCliArg { pub enum ApprovalModeCliArg {
/// Run all commands without asking for user approval. /// Run all commands without asking for user approval.
@@ -24,7 +24,7 @@ pub enum ApprovalModeCliArg {
Never, Never,
} }
#[derive(Clone, Debug, ValueEnum)] #[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")] #[value(rename_all = "kebab-case")]
pub enum SandboxModeCliArg { pub enum SandboxModeCliArg {
/// Network syscalls will be blocked /// Network syscalls will be blocked

View File

@@ -36,7 +36,6 @@ use crate::exec::process_exec_tool_call;
use crate::exec::ExecParams; use crate::exec::ExecParams;
use crate::exec::ExecToolCallOutput; use crate::exec::ExecToolCallOutput;
use crate::exec::SandboxType; use crate::exec::SandboxType;
use crate::flags::OPENAI_DEFAULT_MODEL;
use crate::flags::OPENAI_STREAM_MAX_RETRIES; use crate::flags::OPENAI_STREAM_MAX_RETRIES;
use crate::models::ContentItem; use crate::models::ContentItem;
use crate::models::FunctionCallOutputPayload; use crate::models::FunctionCallOutputPayload;
@@ -486,7 +485,6 @@ async fn submission_loop(
sandbox_policy, sandbox_policy,
disable_response_storage, disable_response_storage,
} => { } => {
let model = model.unwrap_or_else(|| OPENAI_DEFAULT_MODEL.to_string());
info!(model, "Configuring session"); info!(model, "Configuring session");
let client = ModelClient::new(model.clone()); let client = ModelClient::new(model.clone());

View File

@@ -2,16 +2,13 @@ use std::sync::atomic::AtomicU64;
use std::sync::Arc; use std::sync::Arc;
use crate::config::Config; use crate::config::Config;
use crate::protocol::AskForApproval;
use crate::protocol::Event; use crate::protocol::Event;
use crate::protocol::EventMsg; use crate::protocol::EventMsg;
use crate::protocol::Op; use crate::protocol::Op;
use crate::protocol::SandboxPolicy;
use crate::protocol::Submission; use crate::protocol::Submission;
use crate::util::notify_on_sigint; use crate::util::notify_on_sigint;
use crate::Codex; use crate::Codex;
use tokio::sync::Notify; use tokio::sync::Notify;
use tracing::debug;
/// Spawn a new [`Codex`] and initialise the session. /// Spawn a new [`Codex`] and initialise the session.
/// ///
@@ -19,21 +16,17 @@ use tracing::debug;
/// is received as a response to the initial `ConfigureSession` submission so /// is received as a response to the initial `ConfigureSession` submission so
/// that callers can surface the information to the UI. /// that callers can surface the information to the UI.
pub async fn init_codex( pub async fn init_codex(
approval_policy: AskForApproval, config: Config,
sandbox_policy: SandboxPolicy,
disable_response_storage: bool, disable_response_storage: bool,
model_override: Option<String>,
) -> anyhow::Result<(CodexWrapper, Event, Arc<Notify>)> { ) -> anyhow::Result<(CodexWrapper, Event, Arc<Notify>)> {
let ctrl_c = notify_on_sigint(); let ctrl_c = notify_on_sigint();
let config = Config::load().unwrap_or_default();
debug!("loaded config: {config:?}");
let codex = CodexWrapper::new(Codex::spawn(ctrl_c.clone())?); let codex = CodexWrapper::new(Codex::spawn(ctrl_c.clone())?);
let init_id = codex let init_id = codex
.submit(Op::ConfigureSession { .submit(Op::ConfigureSession {
model: model_override.or_else(|| config.model.clone()), model: config.model.clone(),
instructions: config.instructions, instructions: config.instructions.clone(),
approval_policy, approval_policy: config.approval_policy,
sandbox_policy, sandbox_policy: config.sandbox_policy,
disable_response_storage, disable_response_storage,
}) })
.await?; .await?;

View File

@@ -1,39 +1,98 @@
use std::path::PathBuf; use crate::flags::OPENAI_DEFAULT_MODEL;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use dirs::home_dir; use dirs::home_dir;
use serde::Deserialize; use serde::Deserialize;
use std::path::PathBuf;
/// Embedded fallback instructions that mirror the TypeScript CLIs default system prompt. These /// Embedded fallback instructions that mirror the TypeScript CLIs default
/// are compiled into the binary so a clean install behaves correctly even if the user has not /// system prompt. These are compiled into the binary so a clean install behaves
/// created `~/.codex/instructions.md`. /// correctly even if the user has not created `~/.codex/instructions.md`.
const EMBEDDED_INSTRUCTIONS: &str = include_str!("../prompt.md"); const EMBEDDED_INSTRUCTIONS: &str = include_str!("../prompt.md");
#[derive(Default, Deserialize, Debug, Clone)] /// Application configuration loaded from disk and merged with overrides.
#[derive(Deserialize, Debug, Clone)]
pub struct Config { pub struct Config {
pub model: Option<String>, /// Optional override of model selection.
#[serde(default = "default_model")]
pub model: String,
/// Default approval policy for executing commands.
#[serde(default)]
pub approval_policy: AskForApproval,
#[serde(default)]
pub sandbox_policy: SandboxPolicy,
/// System instructions.
pub instructions: Option<String>, pub instructions: Option<String>,
} }
/// Optional overrides for user configuration (e.g., from CLI flags).
#[derive(Default, Debug, Clone)]
pub struct ConfigOverrides {
pub model: Option<String>,
pub approval_policy: Option<AskForApproval>,
pub sandbox_policy: Option<SandboxPolicy>,
}
impl Config { impl Config {
/// Load ~/.codex/config.toml and ~/.codex/instructions.md (if present). /// Load configuration, optionally applying overrides (CLI flags). Merges
/// Returns `None` if neither file exists. /// ~/.codex/config.toml, ~/.codex/instructions.md, embedded defaults, and
pub fn load() -> Option<Self> { /// any values provided in `overrides` (highest precedence).
let mut cfg: Config = Self::load_from_toml().unwrap_or_default(); pub fn load_with_overrides(overrides: ConfigOverrides) -> std::io::Result<Self> {
let mut cfg: Config = Self::load_from_toml()?;
// Highest precedence → userprovided ~/.codex/instructions.md (if present) tracing::warn!("Config parsed from config.toml: {cfg:?}");
// Fallback → embedded default instructions baked into the binary
// Instructions: user-provided instructions.md > embedded default.
cfg.instructions = cfg.instructions =
Self::load_instructions().or_else(|| Some(EMBEDDED_INSTRUCTIONS.to_string())); Self::load_instructions().or_else(|| Some(EMBEDDED_INSTRUCTIONS.to_string()));
Some(cfg) // Destructure ConfigOverrides fully to ensure all overrides are applied.
let ConfigOverrides {
model,
approval_policy,
sandbox_policy,
} = overrides;
if let Some(model) = model {
cfg.model = model;
}
if let Some(approval_policy) = approval_policy {
cfg.approval_policy = approval_policy;
}
if let Some(sandbox_policy) = sandbox_policy {
cfg.sandbox_policy = sandbox_policy;
}
Ok(cfg)
} }
fn load_from_toml() -> Option<Self> { /// Attempt to parse the file at `~/.codex/config.toml` into a Config.
let mut p = codex_dir().ok()?; fn load_from_toml() -> std::io::Result<Self> {
p.push("config.toml"); let config_toml_path = codex_dir()?.join("config.toml");
let contents = std::fs::read_to_string(&p).ok()?; match std::fs::read_to_string(&config_toml_path) {
toml::from_str(&contents).ok() 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::load_default_config())
}
Err(e) => {
tracing::error!("Failed to read config.toml: {e}");
Err(e)
}
}
}
/// Meant to be used exclusively for tests: load_with_overrides() should be
/// used in all other cases.
pub fn load_default_config_for_test() -> Self {
Self::load_default_config()
}
fn load_default_config() -> Self {
// Load from an empty string to exercise #[serde(default)] to
// get the default values for each field.
toml::from_str::<Self>("").expect("empty string should parse as TOML")
} }
fn load_instructions() -> Option<String> { fn load_instructions() -> Option<String> {
@@ -43,6 +102,10 @@ impl Config {
} }
} }
fn default_model() -> String {
OPENAI_DEFAULT_MODEL.to_string()
}
/// Returns the path to the Codex configuration directory, which is `~/.codex`. /// Returns the path to the Codex configuration directory, which is `~/.codex`.
/// Does not verify that the directory exists. /// Does not verify that the directory exists.
pub fn codex_dir() -> std::io::Result<PathBuf> { pub fn codex_dir() -> std::io::Result<PathBuf> {

View File

@@ -98,7 +98,7 @@ pub async fn process_exec_tool_call(
workdir, workdir,
timeout_ms, timeout_ms,
} = params; } = params;
let seatbelt_command = create_seatbelt_command(command, writable_roots); let seatbelt_command = create_seatbelt_command(command, sandbox_policy, writable_roots);
exec( exec(
ExecParams { ExecParams {
command: seatbelt_command, command: seatbelt_command,
@@ -154,7 +154,11 @@ pub async fn process_exec_tool_call(
} }
} }
pub fn create_seatbelt_command(command: Vec<String>, writable_roots: &[PathBuf]) -> Vec<String> { pub fn create_seatbelt_command(
command: Vec<String>,
sandbox_policy: SandboxPolicy,
writable_roots: &[PathBuf],
) -> Vec<String> {
let (policies, cli_args): (Vec<String>, Vec<String>) = writable_roots let (policies, cli_args): (Vec<String>, Vec<String>) = writable_roots
.iter() .iter()
.enumerate() .enumerate()
@@ -166,6 +170,14 @@ pub fn create_seatbelt_command(command: Vec<String>, writable_roots: &[PathBuf])
}) })
.unzip(); .unzip();
// TODO(ragona): The seatbelt policy should reflect the SandboxPolicy that
// is passed, but everything is currently hardcoded to use
// MACOS_SEATBELT_READONLY_POLICY.
// TODO(mbolin): apply_patch calls must also honor the SandboxPolicy.
if !matches!(sandbox_policy, SandboxPolicy::NetworkRestricted) {
tracing::error!("specified sandbox policy {sandbox_policy:?} will not be honroed");
}
let full_policy = if policies.is_empty() { let full_policy = if policies.is_empty() {
MACOS_SEATBELT_READONLY_POLICY.to_string() MACOS_SEATBELT_READONLY_POLICY.to_string()
} else { } else {

View File

@@ -26,7 +26,7 @@ pub enum Op {
/// Configure the model session. /// Configure the model session.
ConfigureSession { ConfigureSession {
/// If not specified, server will use its default model. /// If not specified, server will use its default model.
model: Option<String>, model: String,
/// Model instructions /// Model instructions
instructions: Option<String>, instructions: Option<String>,
/// When to escalate for approval for execution /// When to escalate for approval for execution
@@ -66,11 +66,13 @@ pub enum Op {
} }
/// Determines how liberally commands are autoapproved by the system. /// Determines how liberally commands are autoapproved by the system.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AskForApproval { pub enum AskForApproval {
/// Under this policy, only “known safe” commands—as determined by /// Under this policy, only “known safe” commands—as determined by
/// `is_safe_command()`—that **only read files** are autoapproved. /// `is_safe_command()`—that **only read files** are autoapproved.
/// Everything else will ask the user to approve. /// Everything else will ask the user to approve.
#[default]
UnlessAllowListed, UnlessAllowListed,
/// In addition to everything allowed by **`Suggest`**, commands that /// In addition to everything allowed by **`Suggest`**, commands that
@@ -91,13 +93,15 @@ pub enum AskForApproval {
} }
/// Determines execution restrictions for model shell commands /// Determines execution restrictions for model shell commands
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SandboxPolicy { pub enum SandboxPolicy {
/// Network syscalls will be blocked /// Network syscalls will be blocked
NetworkRestricted, NetworkRestricted,
/// Filesystem writes will be restricted /// Filesystem writes will be restricted
FileWriteRestricted, FileWriteRestricted,
/// Network and filesystem writes will be restricted /// Network and filesystem writes will be restricted
#[default]
NetworkAndFileWriteRestricted, NetworkAndFileWriteRestricted,
/// No restrictions; full "unsandboxed" mode /// No restrictions; full "unsandboxed" mode
DangerousNoRestrictions, DangerousNoRestrictions,

View File

@@ -17,7 +17,7 @@
use std::time::Duration; use std::time::Duration;
use codex_core::protocol::AskForApproval; use codex_core::config::Config;
use codex_core::protocol::EventMsg; use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
@@ -47,13 +47,14 @@ async fn spawn_codex() -> Codex {
let agent = Codex::spawn(std::sync::Arc::new(Notify::new())).unwrap(); let agent = Codex::spawn(std::sync::Arc::new(Notify::new())).unwrap();
let config = Config::load_default_config_for_test();
agent agent
.submit(Submission { .submit(Submission {
id: "init".into(), id: "init".into(),
op: Op::ConfigureSession { op: Op::ConfigureSession {
model: None, model: config.model,
instructions: None, instructions: None,
approval_policy: AskForApproval::OnFailure, approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted, sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
disable_response_storage: false, disable_response_storage: false,
}, },

View File

@@ -1,6 +1,6 @@
use std::time::Duration; use std::time::Duration;
use codex_core::protocol::AskForApproval; use codex_core::config::Config;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SandboxPolicy;
@@ -87,13 +87,14 @@ async fn keeps_previous_response_id_between_tasks() {
let codex = Codex::spawn(std::sync::Arc::new(tokio::sync::Notify::new())).unwrap(); let codex = Codex::spawn(std::sync::Arc::new(tokio::sync::Notify::new())).unwrap();
// Init session // Init session
let config = Config::load_default_config_for_test();
codex codex
.submit(Submission { .submit(Submission {
id: "init".into(), id: "init".into(),
op: Op::ConfigureSession { op: Op::ConfigureSession {
model: None, model: config.model,
instructions: None, instructions: None,
approval_policy: AskForApproval::OnFailure, approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted, sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
disable_response_storage: false, disable_response_storage: false,
}, },

View File

@@ -3,7 +3,7 @@
use std::time::Duration; use std::time::Duration;
use codex_core::protocol::AskForApproval; use codex_core::config::Config;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SandboxPolicy;
@@ -70,13 +70,14 @@ async fn retries_on_early_close() {
let codex = Codex::spawn(std::sync::Arc::new(tokio::sync::Notify::new())).unwrap(); let codex = Codex::spawn(std::sync::Arc::new(tokio::sync::Notify::new())).unwrap();
let config = Config::load_default_config_for_test();
codex codex
.submit(Submission { .submit(Submission {
id: "init".into(), id: "init".into(),
op: Op::ConfigureSession { op: Op::ConfigureSession {
model: None, model: config.model,
instructions: None, instructions: None,
approval_policy: AskForApproval::OnFailure, approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted, sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
disable_response_storage: false, disable_response_storage: false,
}, },

View File

@@ -14,7 +14,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
codex-core = { path = "../core" } codex-core = { path = "../core", features = ["cli"] }
tokio = { version = "1", features = [ tokio = { version = "1", features = [
"io-std", "io-std",
"macros", "macros",

View File

@@ -1,4 +1,5 @@
use clap::Parser; use clap::Parser;
use codex_core::SandboxModeCliArg;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -12,6 +13,12 @@ pub struct Cli {
#[arg(long, short = 'm')] #[arg(long, short = 'm')]
pub model: Option<String>, pub model: Option<String>,
/// Configure the process restrictions when a command is executed.
///
/// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux.
#[arg(long = "sandbox", short = 's')]
pub sandbox_policy: Option<SandboxModeCliArg>,
/// Allow running Codex outside a Git repository. /// Allow running Codex outside a Git repository.
#[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,

View File

@@ -3,13 +3,14 @@ use std::sync::Arc;
pub use cli::Cli; pub use cli::Cli;
use codex_core::codex_wrapper; use codex_core::codex_wrapper;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::protocol::AskForApproval; use codex_core::protocol::AskForApproval;
use codex_core::protocol::Event; use codex_core::protocol::Event;
use codex_core::protocol::EventMsg; use codex_core::protocol::EventMsg;
use codex_core::protocol::FileChange; use codex_core::protocol::FileChange;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::util::is_inside_git_repo; use codex_core::util::is_inside_git_repo;
use tracing::debug; use tracing::debug;
use tracing::error; use tracing::error;
@@ -33,6 +34,7 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
let Cli { let Cli {
images, images,
model, model,
sandbox_policy,
skip_git_repo_check, skip_git_repo_check,
disable_response_storage, disable_response_storage,
prompt, prompt,
@@ -47,17 +49,17 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
std::process::exit(1); std::process::exit(1);
} }
// TODO(mbolin): We are reworking the CLI args right now, so this will // Load configuration and determine approval policy
// likely come from a new --execution-policy arg. let overrides = ConfigOverrides {
let approval_policy = AskForApproval::Never; model: model.clone(),
let sandbox_policy = SandboxPolicy::NetworkAndFileWriteRestricted; // This CLI is intended to be headless and has no affordances for asking
let (codex_wrapper, event, ctrl_c) = codex_wrapper::init_codex( // the user for approval.
approval_policy, approval_policy: Some(AskForApproval::Never),
sandbox_policy, sandbox_policy: sandbox_policy.map(Into::into),
disable_response_storage, };
model, let config = Config::load_with_overrides(overrides)?;
) let (codex_wrapper, event, ctrl_c) =
.await?; codex_wrapper::init_codex(config, disable_response_storage).await?;
let codex = Arc::new(codex_wrapper); let codex = Arc::new(codex_wrapper);
info!("Codex initialized with event: {event:?}"); info!("Codex initialized with event: {event:?}");

View File

@@ -21,8 +21,8 @@ pub struct Cli {
/// Configure the process restrictions when a command is executed. /// Configure the process restrictions when a command is executed.
/// ///
/// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux. /// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux.
#[arg(long = "sandbox", short = 's', value_enum, default_value_t = SandboxModeCliArg::NetworkAndFileWriteRestricted)] #[arg(long = "sandbox", short = 's')]
pub sandbox_policy: SandboxModeCliArg, pub sandbox_policy: Option<SandboxModeCliArg>,
/// Allow running Codex outside a Git repository. /// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", default_value_t = false)] #[arg(long = "skip-git-repo-check", default_value_t = false)]

View File

@@ -34,14 +34,14 @@ pub struct Cli {
pub no_ansi: bool, pub no_ansi: bool,
/// Configure when the model requires human approval before executing a command. /// Configure when the model requires human approval before executing a command.
#[arg(long = "ask-for-approval", short = 'a', value_enum, default_value_t = ApprovalModeCliArg::OnFailure)] #[arg(long = "ask-for-approval", short = 'a')]
pub approval_policy: ApprovalModeCliArg, pub approval_policy: Option<ApprovalModeCliArg>,
/// Configure the process restrictions when a command is executed. /// Configure the process restrictions when a command is executed.
/// ///
/// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux. /// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux.
#[arg(long = "sandbox", short = 's', value_enum, default_value_t = SandboxModeCliArg::NetworkAndFileWriteRestricted)] #[arg(long = "sandbox", short = 's')]
pub sandbox_policy: SandboxModeCliArg, pub sandbox_policy: Option<SandboxModeCliArg>,
/// Allow running Codex outside a Git repository. By default the CLI /// Allow running Codex outside a Git repository. By default the CLI
/// aborts early when the current working directory is **not** inside a /// aborts early when the current working directory is **not** inside a

View File

@@ -4,6 +4,7 @@ use std::io::Write;
use std::sync::Arc; use std::sync::Arc;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::protocol; use codex_core::protocol;
use codex_core::protocol::FileChange; use codex_core::protocol::FileChange;
use codex_core::util::is_inside_git_repo; use codex_core::util::is_inside_git_repo;
@@ -75,12 +76,18 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
// Initialize logging before any other work so early errors are captured. // Initialize logging before any other work so early errors are captured.
init_logger(cli.verbose, !cli.no_ansi); init_logger(cli.verbose, !cli.no_ansi);
let config = Config::load().unwrap_or_default(); // Load config file and apply CLI overrides (model & approval policy)
let overrides = ConfigOverrides {
model: cli.model.clone(),
approval_policy: cli.approval_policy.map(Into::into),
sandbox_policy: cli.sandbox_policy.map(Into::into),
};
let config = Config::load_with_overrides(overrides)?;
codex_main(cli, config, ctrl_c).await codex_main(cli, config, ctrl_c).await
} }
async fn codex_main(mut cli: Cli, cfg: Config, ctrl_c: Arc<Notify>) -> anyhow::Result<()> { async fn codex_main(cli: Cli, cfg: Config, ctrl_c: Arc<Notify>) -> anyhow::Result<()> {
let mut builder = Codex::builder(); let mut builder = Codex::builder();
if let Some(path) = cli.record_submissions { if let Some(path) = cli.record_submissions {
builder = builder.record_submissions(path); builder = builder.record_submissions(path);
@@ -93,10 +100,10 @@ async fn codex_main(mut cli: Cli, cfg: Config, ctrl_c: Arc<Notify>) -> anyhow::R
let init = protocol::Submission { let init = protocol::Submission {
id: init_id.clone(), id: init_id.clone(),
op: protocol::Op::ConfigureSession { op: protocol::Op::ConfigureSession {
model: cli.model.or(cfg.model), model: cfg.model,
instructions: cfg.instructions, instructions: cfg.instructions,
approval_policy: cli.approval_policy.into(), approval_policy: cfg.approval_policy,
sandbox_policy: cli.sandbox_policy.into(), sandbox_policy: cfg.sandbox_policy,
disable_response_storage: cli.disable_response_storage, disable_response_storage: cli.disable_response_storage,
}, },
}; };
@@ -133,8 +140,8 @@ async fn codex_main(mut cli: Cli, cfg: Config, ctrl_c: Arc<Notify>) -> anyhow::R
// run loop // run loop
let mut reader = InputReader::new(ctrl_c.clone()); let mut reader = InputReader::new(ctrl_c.clone());
loop { loop {
let text = match cli.prompt.take() { let text = match &cli.prompt {
Some(input) => input, Some(input) => input.clone(),
None => match reader.request_input().await? { None => match reader.request_input().await? {
Some(input) => input, Some(input) => input,
None => { None => {

View File

@@ -4,10 +4,9 @@ use crate::git_warning_screen::GitWarningOutcome;
use crate::git_warning_screen::GitWarningScreen; use crate::git_warning_screen::GitWarningScreen;
use crate::scroll_event_helper::ScrollEventHelper; use crate::scroll_event_helper::ScrollEventHelper;
use crate::tui; use crate::tui;
use codex_core::protocol::AskForApproval; use codex_core::config::Config;
use codex_core::protocol::Event; use codex_core::protocol::Event;
use codex_core::protocol::Op; use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
@@ -34,12 +33,10 @@ pub(crate) struct App<'a> {
impl App<'_> { impl App<'_> {
pub(crate) fn new( pub(crate) fn new(
approval_policy: AskForApproval, config: Config,
sandbox_policy: SandboxPolicy,
initial_prompt: Option<String>, initial_prompt: Option<String>,
show_git_warning: bool, show_git_warning: bool,
initial_images: Vec<std::path::PathBuf>, initial_images: Vec<std::path::PathBuf>,
model: Option<String>,
disable_response_storage: bool, disable_response_storage: bool,
) -> Self { ) -> Self {
let (app_event_tx, app_event_rx) = channel(); let (app_event_tx, app_event_rx) = channel();
@@ -80,12 +77,10 @@ impl App<'_> {
} }
let chat_widget = ChatWidget::new( let chat_widget = ChatWidget::new(
approval_policy, config,
sandbox_policy,
app_event_tx.clone(), app_event_tx.clone(),
initial_prompt.clone(), initial_prompt.clone(),
initial_images, initial_images,
model,
disable_response_storage, disable_response_storage,
); );

View File

@@ -3,12 +3,11 @@ use std::sync::mpsc::Sender;
use std::sync::Arc; use std::sync::Arc;
use codex_core::codex_wrapper::init_codex; use codex_core::codex_wrapper::init_codex;
use codex_core::protocol::AskForApproval; use codex_core::config::Config;
use codex_core::protocol::Event; use codex_core::protocol::Event;
use codex_core::protocol::EventMsg; use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Constraint; use ratatui::layout::Constraint;
@@ -34,7 +33,7 @@ pub(crate) struct ChatWidget<'a> {
conversation_history: ConversationHistoryWidget, conversation_history: ConversationHistoryWidget,
bottom_pane: BottomPane<'a>, bottom_pane: BottomPane<'a>,
input_focus: InputFocus, input_focus: InputFocus,
approval_policy: AskForApproval, config: Config,
cwd: std::path::PathBuf, cwd: std::path::PathBuf,
} }
@@ -46,12 +45,10 @@ enum InputFocus {
impl ChatWidget<'_> { impl ChatWidget<'_> {
pub(crate) fn new( pub(crate) fn new(
approval_policy: AskForApproval, config: Config,
sandbox_policy: SandboxPolicy,
app_event_tx: Sender<AppEvent>, app_event_tx: Sender<AppEvent>,
initial_prompt: Option<String>, initial_prompt: Option<String>,
initial_images: Vec<std::path::PathBuf>, initial_images: Vec<std::path::PathBuf>,
model: Option<String>,
disable_response_storage: bool, disable_response_storage: bool,
) -> Self { ) -> Self {
let (codex_op_tx, mut codex_op_rx) = unbounded_channel::<Op>(); let (codex_op_tx, mut codex_op_rx) = unbounded_channel::<Op>();
@@ -63,23 +60,17 @@ impl ChatWidget<'_> {
let app_event_tx_clone = app_event_tx.clone(); let app_event_tx_clone = app_event_tx.clone();
// Create the Codex asynchronously so the UI loads as quickly as possible. // Create the Codex asynchronously so the UI loads as quickly as possible.
let config_for_agent_loop = config.clone();
tokio::spawn(async move { tokio::spawn(async move {
// Initialize session; storage enabled by default let (codex, session_event, _ctrl_c) =
let (codex, session_event, _ctrl_c) = match init_codex( match init_codex(config_for_agent_loop, disable_response_storage).await {
approval_policy, Ok(vals) => vals,
sandbox_policy, Err(e) => {
disable_response_storage, // TODO: surface this error to the user.
model, tracing::error!("failed to initialize codex: {e}");
) return;
.await }
{ };
Ok(vals) => vals,
Err(e) => {
// TODO(mbolin): This error needs to be surfaced to the user.
tracing::error!("failed to initialize codex: {e}");
return;
}
};
// Forward the captured `SessionInitialized` event that was consumed // Forward the captured `SessionInitialized` event that was consumed
// inside `init_codex()` so it can be rendered in the UI. // inside `init_codex()` so it can be rendered in the UI.
@@ -115,7 +106,7 @@ impl ChatWidget<'_> {
has_input_focus: true, has_input_focus: true,
}), }),
input_focus: InputFocus::BottomPane, input_focus: InputFocus::BottomPane,
approval_policy, config,
cwd: cwd.clone(), cwd: cwd.clone(),
}; };
@@ -243,11 +234,8 @@ impl ChatWidget<'_> {
match msg { match msg {
EventMsg::SessionConfigured { model } => { EventMsg::SessionConfigured { model } => {
// Record session information at the top of the conversation. // Record session information at the top of the conversation.
self.conversation_history.add_session_info( self.conversation_history
model, .add_session_info(&self.config, model, self.cwd.clone());
self.cwd.clone(),
self.approval_policy,
);
self.request_redraw()?; self.request_redraw()?;
} }
EventMsg::AgentMessage { message } => { EventMsg::AgentMessage { message } => {

View File

@@ -18,14 +18,14 @@ pub struct Cli {
pub model: Option<String>, pub model: Option<String>,
/// Configure when the model requires human approval before executing a command. /// Configure when the model requires human approval before executing a command.
#[arg(long = "ask-for-approval", short = 'a', value_enum, default_value_t = ApprovalModeCliArg::OnFailure)] #[arg(long = "ask-for-approval", short = 'a')]
pub approval_policy: ApprovalModeCliArg, pub approval_policy: Option<ApprovalModeCliArg>,
/// Configure the process restrictions when a command is executed. /// Configure the process restrictions when a command is executed.
/// ///
/// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux. /// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux.
#[arg(long = "sandbox", short = 's', value_enum, default_value_t = SandboxModeCliArg::NetworkAndFileWriteRestricted)] #[arg(long = "sandbox", short = 's')]
pub sandbox_policy: SandboxModeCliArg, pub sandbox_policy: Option<SandboxModeCliArg>,
/// Allow running Codex outside a Git repository. /// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", default_value_t = false)] #[arg(long = "skip-git-repo-check", default_value_t = false)]

View File

@@ -1,6 +1,7 @@
use crate::history_cell::CommandOutput; use crate::history_cell::CommandOutput;
use crate::history_cell::HistoryCell; use crate::history_cell::HistoryCell;
use crate::history_cell::PatchEventType; use crate::history_cell::PatchEventType;
use codex_core::config::Config;
use codex_core::protocol::FileChange; use codex_core::protocol::FileChange;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
@@ -181,13 +182,10 @@ impl ConversationHistoryWidget {
self.add_to_history(HistoryCell::new_patch_event(event_type, changes)); self.add_to_history(HistoryCell::new_patch_event(event_type, changes));
} }
pub fn add_session_info( /// Note `model` could differ from `config.model` if the agent decided to
&mut self, /// use a different model than the one requested by the user.
model: String, pub fn add_session_info(&mut self, config: &Config, model: String, cwd: PathBuf) {
cwd: std::path::PathBuf, self.add_to_history(HistoryCell::new_session_info(config, model, cwd));
approval_policy: codex_core::protocol::AskForApproval,
) {
self.add_to_history(HistoryCell::new_session_info(model, cwd, approval_policy));
} }
pub fn add_active_exec_command(&mut self, call_id: String, command: Vec<String>) { pub fn add_active_exec_command(&mut self, call_id: String, command: Vec<String>) {

View File

@@ -1,4 +1,5 @@
use codex_ansi_escape::ansi_escape_line; use codex_ansi_escape::ansi_escape_line;
use codex_core::config::Config;
use codex_core::protocol::FileChange; use codex_core::protocol::FileChange;
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::style::Color; use ratatui::style::Color;
@@ -144,9 +145,9 @@ impl HistoryCell {
} }
pub(crate) fn new_session_info( pub(crate) fn new_session_info(
config: &Config,
model: String, model: String,
cwd: std::path::PathBuf, cwd: std::path::PathBuf,
approval_policy: codex_core::protocol::AskForApproval,
) -> Self { ) -> Self {
let mut lines: Vec<Line<'static>> = Vec::new(); let mut lines: Vec<Line<'static>> = Vec::new();
@@ -158,7 +159,11 @@ impl HistoryCell {
])); ]));
lines.push(Line::from(vec![ lines.push(Line::from(vec![
"↳ approval: ".bold(), "↳ approval: ".bold(),
format!("{:?}", approval_policy).into(), format!("{:?}", config.approval_policy).into(),
]));
lines.push(Line::from(vec![
"↳ sandbox: ".bold(),
format!("{:?}", config.sandbox_policy).into(),
])); ]));
lines.push(Line::from("")); lines.push(Line::from(""));

View File

@@ -4,6 +4,8 @@
#![deny(clippy::print_stdout, clippy::print_stderr)] #![deny(clippy::print_stdout, clippy::print_stderr)]
use app::App; use app::App;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::util::is_inside_git_repo; use codex_core::util::is_inside_git_repo;
use log_layer::TuiLogLayer; use log_layer::TuiLogLayer;
use std::fs::OpenOptions; use std::fs::OpenOptions;
@@ -31,6 +33,23 @@ pub use cli::Cli;
pub fn run_main(cli: Cli) -> std::io::Result<()> { pub fn run_main(cli: Cli) -> std::io::Result<()> {
assert_env_var_set(); assert_env_var_set();
let config = {
// Load configuration and support CLI overrides.
let overrides = ConfigOverrides {
model: cli.model.clone(),
approval_policy: cli.approval_policy.map(Into::into),
sandbox_policy: cli.sandbox_policy.map(Into::into),
};
#[allow(clippy::print_stderr)]
match Config::load_with_overrides(overrides) {
Ok(config) => config,
Err(err) => {
eprintln!("Error loading configuration: {err}");
std::process::exit(1);
}
}
};
let log_dir = codex_core::config::log_dir()?; let log_dir = codex_core::config::log_dir()?;
std::fs::create_dir_all(&log_dir)?; std::fs::create_dir_all(&log_dir)?;
// Open (or create) your log file, appending to it. // Open (or create) your log file, appending to it.
@@ -79,7 +98,7 @@ pub fn run_main(cli: Cli) -> std::io::Result<()> {
// `--allow-no-git-exec` flag. // `--allow-no-git-exec` flag.
let show_git_warning = !cli.skip_git_repo_check && !is_inside_git_repo(); let show_git_warning = !cli.skip_git_repo_check && !is_inside_git_repo();
try_run_ratatui_app(cli, show_git_warning, log_rx); try_run_ratatui_app(cli, config, show_git_warning, log_rx);
Ok(()) Ok(())
} }
@@ -89,16 +108,18 @@ pub fn run_main(cli: Cli) -> std::io::Result<()> {
)] )]
fn try_run_ratatui_app( fn try_run_ratatui_app(
cli: Cli, cli: Cli,
config: Config,
show_git_warning: bool, show_git_warning: bool,
log_rx: tokio::sync::mpsc::UnboundedReceiver<String>, log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
) { ) {
if let Err(report) = run_ratatui_app(cli, show_git_warning, log_rx) { if let Err(report) = run_ratatui_app(cli, config, show_git_warning, log_rx) {
eprintln!("Error: {report:?}"); eprintln!("Error: {report:?}");
} }
} }
fn run_ratatui_app( fn run_ratatui_app(
cli: Cli, cli: Cli,
config: Config,
show_git_warning: bool, show_git_warning: bool,
mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>, mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
) -> color_eyre::Result<()> { ) -> color_eyre::Result<()> {
@@ -116,23 +137,14 @@ fn run_ratatui_app(
let Cli { let Cli {
prompt, prompt,
images, images,
approval_policy,
sandbox_policy: sandbox,
model,
disable_response_storage, disable_response_storage,
.. ..
} = cli; } = cli;
let approval_policy = approval_policy.into();
let sandbox_policy = sandbox.into();
let mut app = App::new( let mut app = App::new(
approval_policy, config,
sandbox_policy,
prompt, prompt,
show_git_warning, show_git_warning,
images, images,
model,
disable_response_storage, disable_response_storage,
); );