feat: load defaults into Config and introduce ConfigOverrides

This commit is contained in:
Michael Bolin
2025-04-25 19:50:58 -07:00
parent b0ba65a936
commit ffbcccc219
17 changed files with 145 additions and 106 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ use clap::ValueEnum;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
#[derive(Clone, Debug, ValueEnum)]
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum ApprovalModeCliArg {
/// Run all commands without asking for user approval.
@@ -24,7 +24,7 @@ pub enum ApprovalModeCliArg {
Never,
}
#[derive(Clone, Debug, ValueEnum)]
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum SandboxModeCliArg {
/// Network syscalls will be blocked
-2
View File
@@ -36,7 +36,6 @@ use crate::exec::process_exec_tool_call;
use crate::exec::ExecParams;
use crate::exec::ExecToolCallOutput;
use crate::exec::SandboxType;
use crate::flags::OPENAI_DEFAULT_MODEL;
use crate::flags::OPENAI_STREAM_MAX_RETRIES;
use crate::models::ContentItem;
use crate::models::FunctionCallOutputPayload;
@@ -486,7 +485,6 @@ async fn submission_loop(
sandbox_policy,
disable_response_storage,
} => {
let model = model.unwrap_or_else(|| OPENAI_DEFAULT_MODEL.to_string());
info!(model, "Configuring session");
let client = ModelClient::new(model.clone());
+5 -12
View File
@@ -2,16 +2,13 @@ use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use crate::config::Config;
use crate::protocol::AskForApproval;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::Op;
use crate::protocol::SandboxPolicy;
use crate::protocol::Submission;
use crate::util::notify_on_sigint;
use crate::Codex;
use tokio::sync::Notify;
use tracing::debug;
/// 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
/// that callers can surface the information to the UI.
pub async fn init_codex(
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
config: Config,
disable_response_storage: bool,
model_override: Option<String>,
) -> anyhow::Result<(CodexWrapper, Event, Arc<Notify>)> {
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 init_id = codex
.submit(Op::ConfigureSession {
model: model_override.or_else(|| config.model.clone()),
instructions: config.instructions,
approval_policy,
sandbox_policy,
model: config.model.clone(),
instructions: config.instructions.clone(),
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy,
disable_response_storage,
})
.await?;
+45 -13
View File
@@ -1,32 +1,60 @@
use std::path::PathBuf;
use crate::flags::OPENAI_DEFAULT_MODEL;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use dirs::home_dir;
use serde::Deserialize;
use std::path::PathBuf;
/// Embedded fallback instructions that mirror the TypeScript CLI’s default system prompt. These
/// are compiled into the binary so a clean install behaves correctly even if the user has not
/// created `~/.codex/instructions.md`.
/// Embedded fallback instructions that mirror the TypeScript CLI’s default
/// system prompt. These are compiled into the binary so a clean install behaves
/// correctly even if the user has not created `~/.codex/instructions.md`.
const EMBEDDED_INSTRUCTIONS: &str = include_str!("../prompt.md");
/// Application configuration loaded from disk and merged with overrides.
#[derive(Default, Deserialize, Debug, Clone)]
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>,
}
/// 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 {
/// Load ~/.codex/config.toml and ~/.codex/instructions.md (if present).
/// Returns `None` if neither file exists.
pub fn load() -> Option<Self> {
/// 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) -> Self {
let mut cfg: Config = Self::load_from_toml().unwrap_or_default();
// Highest precedence → user‑provided ~/.codex/instructions.md (if present)
// Fallback → embedded default instructions baked into the binary
// Instructions: user-provided instructions.md > embedded default.
cfg.instructions =
Self::load_instructions().or_else(|| Some(EMBEDDED_INSTRUCTIONS.to_string()));
Some(cfg)
// Apply overrides.
if let Some(model) = overrides.model {
cfg.model = model;
}
if let Some(policy) = overrides.approval_policy {
cfg.approval_policy = policy;
}
if let Some(policy) = overrides.sandbox_policy {
cfg.sandbox_policy = policy;
}
cfg
}
fn load_from_toml() -> Option<Self> {
@@ -43,6 +71,10 @@ impl Config {
}
}
fn default_model() -> String {
OPENAI_DEFAULT_MODEL.to_string()
}
/// Returns the path to the Codex configuration directory, which is `~/.codex`.
/// Does not verify that the directory exists.
pub fn codex_dir() -> std::io::Result<PathBuf> {
+5 -3
View File
@@ -26,7 +26,7 @@ pub enum Op {
/// Configure the model session.
ConfigureSession {
/// If not specified, server will use its default model.
model: Option<String>,
model: String,
/// Model instructions
instructions: Option<String>,
/// When to escalate for approval for execution
@@ -66,11 +66,12 @@ pub enum Op {
}
/// Determines how liberally commands are auto‑approved by the system.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AskForApproval {
/// Under this policy, only “known safe” commands—as determined by
/// `is_safe_command()`—that **only read files** are auto‑approved.
/// Everything else will ask the user to approve.
#[default]
UnlessAllowListed,
/// In addition to everything allowed by **`Suggest`**, commands that
@@ -91,13 +92,14 @@ pub enum AskForApproval {
}
/// 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)]
pub enum SandboxPolicy {
/// Network syscalls will be blocked
NetworkRestricted,
/// Filesystem writes will be restricted
FileWriteRestricted,
/// Network and filesystem writes will be restricted
#[default]
NetworkAndFileWriteRestricted,
/// No restrictions; full "unsandboxed" mode
DangerousNoRestrictions,
+5 -3
View File
@@ -17,7 +17,8 @@
use std::time::Duration;
use codex_core::protocol::AskForApproval;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
@@ -47,13 +48,14 @@ async fn spawn_codex() -> Codex {
let agent = Codex::spawn(std::sync::Arc::new(Notify::new())).unwrap();
let config = Config::load_with_overrides(ConfigOverrides::default());
agent
.submit(Submission {
id: "init".into(),
op: Op::ConfigureSession {
model: None,
model: config.model,
instructions: None,
approval_policy: AskForApproval::OnFailure,
approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
disable_response_storage: false,
},
+5 -3
View File
@@ -1,6 +1,7 @@
use std::time::Duration;
use codex_core::protocol::AskForApproval;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
@@ -87,13 +88,14 @@ async fn keeps_previous_response_id_between_tasks() {
let codex = Codex::spawn(std::sync::Arc::new(tokio::sync::Notify::new())).unwrap();
// Init session
let config = Config::load_with_overrides(ConfigOverrides::default());
codex
.submit(Submission {
id: "init".into(),
op: Op::ConfigureSession {
model: None,
model: config.model,
instructions: None,
approval_policy: AskForApproval::OnFailure,
approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
disable_response_storage: false,
},
+5 -3
View File
@@ -3,7 +3,8 @@
use std::time::Duration;
use codex_core::protocol::AskForApproval;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
@@ -70,13 +71,14 @@ async fn retries_on_early_close() {
let codex = Codex::spawn(std::sync::Arc::new(tokio::sync::Notify::new())).unwrap();
let config = Config::load_with_overrides(ConfigOverrides::default());
codex
.submit(Submission {
id: "init".into(),
op: Op::ConfigureSession {
model: None,
model: config.model,
instructions: None,
approval_policy: AskForApproval::OnFailure,
approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
disable_response_storage: false,
},