From e7ad9449ea28542363ddb7d3d273a34f1953fc77 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 28 Apr 2025 15:39:34 -0700 Subject: [PATCH 1/2] feat: make it possible to set `disable_response_storage = true` in config.toml (#714) https://github.com/openai/codex/pull/642 introduced support for the `--disable-response-storage` flag, but if you are a ZDR customer, it is tedious to set this every time, so this PR makes it possible to set this once in `config.toml` and be done with it. Incidentally, this tidies things up such that now `init_codex()` takes only one parameter: `Config`. --- codex-rs/core/src/codex_wrapper.rs | 7 ++----- codex-rs/core/src/config.rs | 12 ++++++++++++ codex-rs/exec/src/lib.rs | 8 ++++++-- codex-rs/repl/src/lib.rs | 7 ++++++- codex-rs/tui/src/app.rs | 2 -- codex-rs/tui/src/chatwidget.rs | 18 ++++++++---------- codex-rs/tui/src/lib.rs | 20 +++++++------------- 7 files changed, 41 insertions(+), 33 deletions(-) diff --git a/codex-rs/core/src/codex_wrapper.rs b/codex-rs/core/src/codex_wrapper.rs index 3aeff67615..146a812eb8 100644 --- a/codex-rs/core/src/codex_wrapper.rs +++ b/codex-rs/core/src/codex_wrapper.rs @@ -15,10 +15,7 @@ use tokio::sync::Notify; /// Returns the wrapped [`Codex`] **and** the `SessionInitialized` event that /// 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( - config: Config, - disable_response_storage: bool, -) -> anyhow::Result<(CodexWrapper, Event, Arc)> { +pub async fn init_codex(config: Config) -> anyhow::Result<(CodexWrapper, Event, Arc)> { let ctrl_c = notify_on_sigint(); let codex = CodexWrapper::new(Codex::spawn(ctrl_c.clone())?); let init_id = codex @@ -27,7 +24,7 @@ pub async fn init_codex( instructions: config.instructions.clone(), approval_policy: config.approval_policy, sandbox_policy: config.sandbox_policy, - disable_response_storage, + disable_response_storage: config.disable_response_storage, }) .await?; diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index d9ad333679..95abae52e9 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -21,6 +21,13 @@ pub struct Config { pub approval_policy: AskForApproval, #[serde(default)] pub sandbox_policy: SandboxPolicy, + + /// Disable server-side response storage (sends the full conversation + /// context with every request). Currently necessary for OpenAI customers + /// who have opted into Zero Data Retention (ZDR). + #[serde(default)] + pub disable_response_storage: bool, + /// System instructions. pub instructions: Option, } @@ -31,6 +38,7 @@ pub struct ConfigOverrides { pub model: Option, pub approval_policy: Option, pub sandbox_policy: Option, + pub disable_response_storage: Option, } impl Config { @@ -50,6 +58,7 @@ impl Config { model, approval_policy, sandbox_policy, + disable_response_storage, } = overrides; if let Some(model) = model { @@ -61,6 +70,9 @@ impl Config { if let Some(sandbox_policy) = sandbox_policy { cfg.sandbox_policy = sandbox_policy; } + if let Some(disable_response_storage) = disable_response_storage { + cfg.disable_response_storage = disable_response_storage; + } Ok(cfg) } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index daa07e4629..d37e5a9500 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -56,10 +56,14 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> { // the user for approval. approval_policy: Some(AskForApproval::Never), sandbox_policy: sandbox_policy.map(Into::into), + disable_response_storage: if disable_response_storage { + Some(true) + } else { + None + }, }; let config = Config::load_with_overrides(overrides)?; - let (codex_wrapper, event, ctrl_c) = - codex_wrapper::init_codex(config, disable_response_storage).await?; + let (codex_wrapper, event, ctrl_c) = codex_wrapper::init_codex(config).await?; let codex = Arc::new(codex_wrapper); info!("Codex initialized with event: {event:?}"); diff --git a/codex-rs/repl/src/lib.rs b/codex-rs/repl/src/lib.rs index 74e54181c3..17586332fd 100644 --- a/codex-rs/repl/src/lib.rs +++ b/codex-rs/repl/src/lib.rs @@ -81,6 +81,11 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> { model: cli.model.clone(), approval_policy: cli.approval_policy.map(Into::into), sandbox_policy: cli.sandbox_policy.map(Into::into), + disable_response_storage: if cli.disable_response_storage { + Some(true) + } else { + None + }, }; let config = Config::load_with_overrides(overrides)?; @@ -104,7 +109,7 @@ async fn codex_main(cli: Cli, cfg: Config, ctrl_c: Arc) -> anyhow::Resul instructions: cfg.instructions, approval_policy: cfg.approval_policy, sandbox_policy: cfg.sandbox_policy, - disable_response_storage: cli.disable_response_storage, + disable_response_storage: cfg.disable_response_storage, }, }; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index c5da0b56bc..cb2b44e0c3 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -37,7 +37,6 @@ impl App<'_> { initial_prompt: Option, show_git_warning: bool, initial_images: Vec, - disable_response_storage: bool, ) -> Self { let (app_event_tx, app_event_rx) = channel(); let scroll_event_helper = ScrollEventHelper::new(app_event_tx.clone()); @@ -81,7 +80,6 @@ impl App<'_> { app_event_tx.clone(), initial_prompt.clone(), initial_images, - disable_response_storage, ); let app_state = if show_git_warning { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e2224f99be..06bf1bc8b4 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -49,7 +49,6 @@ impl ChatWidget<'_> { app_event_tx: Sender, initial_prompt: Option, initial_images: Vec, - disable_response_storage: bool, ) -> Self { let (codex_op_tx, mut codex_op_rx) = unbounded_channel::(); @@ -62,15 +61,14 @@ impl ChatWidget<'_> { // Create the Codex asynchronously so the UI loads as quickly as possible. let config_for_agent_loop = config.clone(); tokio::spawn(async move { - let (codex, session_event, _ctrl_c) = - match init_codex(config_for_agent_loop, disable_response_storage).await { - Ok(vals) => vals, - Err(e) => { - // TODO: surface this error to the user. - tracing::error!("failed to initialize codex: {e}"); - return; - } - }; + let (codex, session_event, _ctrl_c) = match init_codex(config_for_agent_loop).await { + Ok(vals) => vals, + Err(e) => { + // TODO: surface this error to the user. + tracing::error!("failed to initialize codex: {e}"); + return; + } + }; // Forward the captured `SessionInitialized` event that was consumed // inside `init_codex()` so it can be rendered in the UI. diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 8e987ad743..bf4ebec43c 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -39,6 +39,11 @@ pub fn run_main(cli: Cli) -> std::io::Result<()> { model: cli.model.clone(), approval_policy: cli.approval_policy.map(Into::into), sandbox_policy: cli.sandbox_policy.map(Into::into), + disable_response_storage: if cli.disable_response_storage { + Some(true) + } else { + None + }, }; #[allow(clippy::print_stderr)] match Config::load_with_overrides(overrides) { @@ -134,19 +139,8 @@ fn run_ratatui_app( let mut terminal = tui::init()?; terminal.clear()?; - let Cli { - prompt, - images, - disable_response_storage, - .. - } = cli; - let mut app = App::new( - config, - prompt, - show_git_warning, - images, - disable_response_storage, - ); + let Cli { prompt, images, .. } = cli; + let mut app = App::new(config.clone(), prompt, show_git_warning, images); // Bridge log receiver into the AppEvent channel so latest log lines update the UI. { From d1abb9f6cdc27071c8caee72ec43a54062fc5b43 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 28 Apr 2025 15:46:46 -0700 Subject: [PATCH 2/2] feat: add `debug landlock` subcommand comparable to `debug seatbelt` --- codex-rs/cli/src/landlock.rs | 51 ++++++++++++++++++++++++++++++++++++ codex-rs/cli/src/main.rs | 34 +++++++++++++++++++++++- codex-rs/core/src/lib.rs | 2 +- codex-rs/core/src/linux.rs | 14 ++++++++-- 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 codex-rs/cli/src/landlock.rs diff --git a/codex-rs/cli/src/landlock.rs b/codex-rs/cli/src/landlock.rs new file mode 100644 index 0000000000..be2ba1e354 --- /dev/null +++ b/codex-rs/cli/src/landlock.rs @@ -0,0 +1,51 @@ +//! `debug landlock` implementation for the Codex CLI. +//! +//! On Linux the command is executed inside a Landlock + seccomp sandbox by +//! calling the low-level `exec_linux` helper from `codex_core::linux`. + +use codex_core::protocol::SandboxPolicy; +use std::os::unix::process::ExitStatusExt; +use std::path::PathBuf; +use std::process; +use std::process::Command; +use std::process::ExitStatus; + +/// Execute `command` in a Linux sandbox (Landlock + seccomp) the way Codex +/// would. +pub(crate) fn run_landlock( + command: Vec, + sandbox_policy: SandboxPolicy, + writable_roots: Vec, +) -> anyhow::Result<()> { + if command.is_empty() { + anyhow::bail!("command args are empty"); + } + + // Spawn a new thread and apply the sandbox policies there. + let handle = std::thread::spawn(move || -> anyhow::Result { + // Apply sandbox policies inside this thread so only the child inherits + // them, not the entire CLI process. + if sandbox_policy.is_network_restricted() { + codex_core::linux::install_network_seccomp_filter_on_current_thread()?; + } + + if sandbox_policy.is_file_write_restricted() { + codex_core::linux::install_filesystem_landlock_rules_on_current_thread(writable_roots)?; + } + + let status = Command::new(&command[0]).args(&command[1..]).status()?; + Ok(status) + }); + let status = handle + .join() + .map_err(|e| anyhow::anyhow!("Failed to join thread: {e:?}"))??; + + // Use ExitStatus to derive the exit code. + if let Some(code) = status.code() { + process::exit(code); + } else if let Some(signal) = status.signal() { + process::exit(128 + signal); + } else { + process::exit(1); + } +} diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7d8987c0ab..d8a58de8ff 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -1,3 +1,5 @@ +#[cfg(target_os = "linux")] +mod landlock; mod proto; mod seatbelt; @@ -58,11 +60,14 @@ struct DebugArgs { enum DebugCommand { /// Run a command under Seatbelt (macOS only). Seatbelt(SeatbeltCommand), + + /// Run a command under Landlock+seccomp (Linux only). + Landlock(LandlockCommand), } #[derive(Debug, Parser)] struct SeatbeltCommand { - /// Writable folder for sandbox in full-auto mode (can be specified multiple times). + /// Writable folder for sandbox (can be specified multiple times). #[arg(long = "writable-root", short = 'w', value_name = "DIR", action = ArgAction::Append, use_value_delimiter = false)] writable_roots: Vec, @@ -75,6 +80,21 @@ struct SeatbeltCommand { command: Vec, } +#[derive(Debug, Parser)] +struct LandlockCommand { + /// Writable folder for sandbox (can be specified multiple times). + #[arg(long = "writable-root", short = 'w', value_name = "DIR", action = ArgAction::Append, use_value_delimiter = false)] + writable_roots: Vec, + + /// Configure the process restrictions for the command. + #[arg(long = "sandbox", short = 's')] + sandbox_policy: SandboxModeCliArg, + + /// Full command args to run under landlock. + #[arg(trailing_var_arg = true)] + command: Vec, +} + #[derive(Debug, Parser)] struct ReplProto {} @@ -103,6 +123,18 @@ async fn main() -> anyhow::Result<()> { }) => { seatbelt::run_seatbelt(command, sandbox_policy.into(), writable_roots).await?; } + #[cfg(target_os = "linux")] + DebugCommand::Landlock(LandlockCommand { + command, + sandbox_policy, + writable_roots, + }) => { + landlock::run_landlock(command, sandbox_policy.into(), writable_roots)?; + } + #[cfg(not(target_os = "linux"))] + DebugCommand::Landlock(_) => { + anyhow::bail!("Landlock is only supported on Linux."); + } }, } diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index d517e68824..e7d4e32a0f 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -14,7 +14,7 @@ pub mod exec; mod flags; mod is_safe_command; #[cfg(target_os = "linux")] -mod linux; +pub mod linux; mod models; pub mod protocol; mod safety; diff --git a/codex-rs/core/src/linux.rs b/codex-rs/core/src/linux.rs index 75d70e798f..9f9d44b04f 100644 --- a/codex-rs/core/src/linux.rs +++ b/codex-rs/core/src/linux.rs @@ -72,7 +72,15 @@ pub async fn exec_linux( } } -fn install_filesystem_landlock_rules_on_current_thread(writable_roots: Vec) -> Result<()> { +/// Installs Landlock file-system rules on the current thread allowing read +/// access to the entire file-system while restricting write access to +/// `/dev/null` and the provided list of `writable_roots`. +/// +/// # Errors +/// Returns [`CodexErr::Sandbox`] variants when the ruleset fails to apply. +pub fn install_filesystem_landlock_rules_on_current_thread( + writable_roots: Vec, +) -> Result<()> { let abi = ABI::V5; let access_rw = AccessFs::from_all(abi); let access_ro = AccessFs::from_read(abi); @@ -98,7 +106,9 @@ fn install_filesystem_landlock_rules_on_current_thread(writable_roots: Vec std::result::Result<(), SandboxErr> { +/// Installs a seccomp filter that blocks outbound network access except for +/// AF_UNIX domain sockets. +pub fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(), SandboxErr> { // Build rule map. let mut rules: BTreeMap> = BTreeMap::new();