From 2405c4002643efed9db402fd2ebe6ddf28fc7ecd Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sun, 27 Jul 2025 20:01:35 -0700 Subject: [PATCH 1/2] chore: update Codex::spawn() to return a struct instead of a tuple (#1677) Also update `init_codex()` to return a `struct` instead of a tuple, as well. --- codex-rs/cli/src/proto.rs | 3 ++- codex-rs/core/src/codex.rs | 21 ++++++++++++---- codex-rs/core/src/codex_wrapper.rs | 25 +++++++++++++++++--- codex-rs/core/src/lib.rs | 1 + codex-rs/core/tests/client.rs | 5 ++-- codex-rs/core/tests/live_agent.rs | 3 ++- codex-rs/core/tests/stream_no_completed.rs | 3 ++- codex-rs/exec/src/lib.rs | 12 +++++++--- codex-rs/mcp-server/src/codex_tool_runner.rs | 12 ++++++++-- codex-rs/tui/src/chatwidget.rs | 24 +++++++++++-------- 10 files changed, 81 insertions(+), 28 deletions(-) diff --git a/codex-rs/cli/src/proto.rs b/codex-rs/cli/src/proto.rs index ec395dd108..64b292d50b 100644 --- a/codex-rs/cli/src/proto.rs +++ b/codex-rs/cli/src/proto.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use clap::Parser; use codex_common::CliConfigOverrides; use codex_core::Codex; +use codex_core::CodexSpawnOk; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::protocol::Submission; @@ -35,7 +36,7 @@ pub async fn run_main(opts: ProtoCli) -> anyhow::Result<()> { let config = Config::load_with_cli_overrides(overrides_vec, ConfigOverrides::default())?; let ctrl_c = notify_on_sigint(); - let (codex, _init_id, _session_id) = Codex::spawn(config, ctrl_c.clone()).await?; + let CodexSpawnOk { codex, .. } = Codex::spawn(config, ctrl_c.clone()).await?; let codex = Arc::new(codex); // Task that reads JSON lines from stdin and forwards to Submission Queue diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d9d40a8a47..5764440e79 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -97,11 +97,18 @@ pub struct Codex { rx_event: Receiver, } +/// Wrapper returned by [`Codex::spawn`] containing the spawned [`Codex`], +/// the submission id for the initial `ConfigureSession` request and the +/// unique session id. +pub struct CodexSpawnOk { + pub codex: Codex, + pub init_id: String, + pub session_id: Uuid, +} + impl Codex { - /// Spawn a new [`Codex`] and initialize the session. Returns the instance - /// of `Codex` and the ID of the `SessionInitialized` event that was - /// submitted to start the session. - pub async fn spawn(config: Config, ctrl_c: Arc) -> CodexResult<(Codex, String, Uuid)> { + /// Spawn a new [`Codex`] and initialize the session. + pub async fn spawn(config: Config, ctrl_c: Arc) -> CodexResult { // experimental resume path (undocumented) let resume_path = config.experimental_resume.clone(); info!("resume_path: {resume_path:?}"); @@ -139,7 +146,11 @@ impl Codex { }; let init_id = codex.submit(configure_session).await?; - Ok((codex, init_id, session_id)) + Ok(CodexSpawnOk { + codex, + init_id, + session_id, + }) } /// Submit the `op` wrapped in a `Submission` with a unique ID. diff --git a/codex-rs/core/src/codex_wrapper.rs b/codex-rs/core/src/codex_wrapper.rs index 31f8295ed4..b80579297a 100644 --- a/codex-rs/core/src/codex_wrapper.rs +++ b/codex-rs/core/src/codex_wrapper.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use crate::Codex; +use crate::CodexSpawnOk; use crate::config::Config; use crate::protocol::Event; use crate::protocol::EventMsg; @@ -8,14 +9,27 @@ use crate::util::notify_on_sigint; use tokio::sync::Notify; use uuid::Uuid; +/// Represents an active Codex conversation, including the first event +/// (which is [`EventMsg::SessionConfigured`]). +pub struct CodexConversation { + pub codex: Codex, + pub session_id: Uuid, + pub session_configured: Event, + pub ctrl_c: Arc, +} + /// Spawn a new [`Codex`] and initialize the session. /// /// 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) -> anyhow::Result<(Codex, Event, Arc, Uuid)> { +pub async fn init_codex(config: Config) -> anyhow::Result { let ctrl_c = notify_on_sigint(); - let (codex, init_id, session_id) = Codex::spawn(config, ctrl_c.clone()).await?; + let CodexSpawnOk { + codex, + init_id, + session_id, + } = Codex::spawn(config, ctrl_c.clone()).await?; // The first event must be `SessionInitialized`. Validate and forward it to // the caller so that they can display it in the conversation history. @@ -34,5 +48,10 @@ pub async fn init_codex(config: Config) -> anyhow::Result<(Codex, Event, Arc Result { let mut config = load_default_config_for_test(&codex_home); config.model_provider.request_max_retries = Some(2); config.model_provider.stream_max_retries = Some(2); - let (agent, _init_id, _session_id) = + let CodexSpawnOk { codex: agent, .. } = Codex::spawn(config, std::sync::Arc::new(Notify::new())).await?; Ok(agent) diff --git a/codex-rs/core/tests/stream_no_completed.rs b/codex-rs/core/tests/stream_no_completed.rs index 153330bf10..8e5d83a03e 100644 --- a/codex-rs/core/tests/stream_no_completed.rs +++ b/codex-rs/core/tests/stream_no_completed.rs @@ -4,6 +4,7 @@ use std::time::Duration; use codex_core::Codex; +use codex_core::CodexSpawnOk; use codex_core::ModelProviderInfo; use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_core::protocol::EventMsg; @@ -94,7 +95,7 @@ async fn retries_on_early_close() { let codex_home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&codex_home); config.model_provider = model_provider; - let (codex, _init_id, _session_id) = Codex::spawn(config, ctrl_c).await.unwrap(); + let CodexSpawnOk { codex, .. } = Codex::spawn(config, ctrl_c).await.unwrap(); codex .submit(Op::UserInput { diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 126e92f597..f966d200a1 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -9,7 +9,8 @@ use std::path::PathBuf; use std::sync::Arc; pub use cli::Cli; -use codex_core::codex_wrapper; +use codex_core::codex_wrapper::CodexConversation; +use codex_core::codex_wrapper::{self}; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config_types::SandboxMode; @@ -155,9 +156,14 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any .with_writer(std::io::stderr) .try_init(); - let (codex_wrapper, event, ctrl_c, _session_id) = codex_wrapper::init_codex(config).await?; + let CodexConversation { + codex: codex_wrapper, + session_configured, + ctrl_c, + .. + } = codex_wrapper::init_codex(config).await?; let codex = Arc::new(codex_wrapper); - info!("Codex initialized with event: {event:?}"); + info!("Codex initialized with event: {session_configured:?}"); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); { diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index f2cacf6c8e..22a36b8366 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::sync::Arc; use codex_core::Codex; +use codex_core::codex_wrapper::CodexConversation; use codex_core::codex_wrapper::init_codex; use codex_core::config::Config as CodexConfig; use codex_core::protocol::AgentMessageEvent; @@ -42,7 +43,12 @@ pub async fn run_codex_tool_session( session_map: Arc>>>, running_requests_id_to_codex_uuid: Arc>>, ) { - let (codex, first_event, _ctrl_c, session_id) = match init_codex(config).await { + let CodexConversation { + codex, + session_configured, + session_id, + .. + } = match init_codex(config).await { Ok(res) => res, Err(e) => { let result = CallToolResult { @@ -66,7 +72,9 @@ pub async fn run_codex_tool_session( drop(session_map); // Send initial SessionConfigured event. - outgoing.send_event_as_notification(&first_event).await; + outgoing + .send_event_as_notification(&session_configured) + .await; // Use the original MCP request ID as the `sub_id` for the Codex submission so that // any events emitted for this tool-call can be correlated with the diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 01285c02c9..5e839d1419 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use std::sync::Arc; +use codex_core::codex_wrapper::CodexConversation; use codex_core::codex_wrapper::init_codex; use codex_core::config::Config; use codex_core::protocol::AgentMessageDeltaEvent; @@ -89,19 +90,22 @@ 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, _session_id) = - 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; - } - }; + let CodexConversation { + codex, + session_configured, + .. + } = 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. - app_event_tx_clone.send(AppEvent::CodexEvent(session_event.clone())); + app_event_tx_clone.send(AppEvent::CodexEvent(session_configured.clone())); let codex = Arc::new(codex); let codex_clone = codex.clone(); tokio::spawn(async move { From aa8fa29be37d4635758a2ae3aee664c8cb107600 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sun, 27 Jul 2025 20:07:24 -0700 Subject: [PATCH 2/2] fix: move arg0 handling out of codex-linux-sandbox and into its own crate --- codex-rs/Cargo.lock | 20 +++++++++++++++----- codex-rs/Cargo.toml | 1 + codex-rs/arg0/Cargo.toml | 18 ++++++++++++++++++ codex-rs/{linux-sandbox => arg0}/src/lib.rs | 17 ++--------------- codex-rs/cli/Cargo.toml | 2 +- codex-rs/cli/src/main.rs | 3 ++- codex-rs/exec/Cargo.toml | 2 +- codex-rs/exec/src/main.rs | 3 ++- codex-rs/linux-sandbox/Cargo.toml | 14 +++++--------- codex-rs/mcp-server/Cargo.toml | 2 +- codex-rs/mcp-server/src/main.rs | 3 ++- codex-rs/tui/Cargo.toml | 2 +- codex-rs/tui/src/main.rs | 3 ++- 13 files changed, 53 insertions(+), 37 deletions(-) create mode 100644 codex-rs/arg0/Cargo.toml rename codex-rs/{linux-sandbox => arg0}/src/lib.rs (84%) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ba71596ecd..57711362ff 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -605,6 +605,17 @@ dependencies = [ "tree-sitter-bash", ] +[[package]] +name = "codex-arg0" +version = "0.0.0" +dependencies = [ + "anyhow", + "codex-core", + "codex-linux-sandbox", + "dotenvy", + "tokio", +] + [[package]] name = "codex-chatgpt" version = "0.0.0" @@ -628,11 +639,11 @@ dependencies = [ "anyhow", "clap", "clap_complete", + "codex-arg0", "codex-chatgpt", "codex-common", "codex-core", "codex-exec", - "codex-linux-sandbox", "codex-login", "codex-mcp-server", "codex-tui", @@ -709,9 +720,9 @@ dependencies = [ "anyhow", "chrono", "clap", + "codex-arg0", "codex-common", "codex-core", - "codex-linux-sandbox", "owo-colors", "serde_json", "shlex", @@ -761,7 +772,6 @@ dependencies = [ "clap", "codex-common", "codex-core", - "dotenvy", "landlock", "libc", "seccompiler", @@ -799,8 +809,8 @@ version = "0.0.0" dependencies = [ "anyhow", "assert_cmd", + "codex-arg0", "codex-core", - "codex-linux-sandbox", "mcp-types", "mcp_test_support", "pretty_assertions", @@ -826,10 +836,10 @@ dependencies = [ "base64 0.22.1", "clap", "codex-ansi-escape", + "codex-arg0", "codex-common", "codex-core", "codex-file-search", - "codex-linux-sandbox", "codex-login", "color-eyre", "crossterm", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index eba43e548b..190256c211 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "ansi-escape", "apply-patch", + "arg0", "cli", "common", "core", diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml new file mode 100644 index 0000000000..9ad1896746 --- /dev/null +++ b/codex-rs/arg0/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "codex-arg0" +version = { workspace = true } +edition = "2024" + +[lib] +name = "codex_arg0" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +anyhow = "1" +codex-core = { path = "../core" } +codex-linux-sandbox = { path = "../linux-sandbox" } +dotenvy = "0.15.7" +tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/arg0/src/lib.rs similarity index 84% rename from codex-rs/linux-sandbox/src/lib.rs rename to codex-rs/arg0/src/lib.rs index 960678467c..221f86f01a 100644 --- a/codex-rs/linux-sandbox/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -1,11 +1,3 @@ -#[cfg(target_os = "linux")] -mod landlock; -#[cfg(target_os = "linux")] -mod linux_run_main; - -#[cfg(target_os = "linux")] -pub use linux_run_main::run_main; - use std::future::Future; use std::path::PathBuf; @@ -24,7 +16,7 @@ use std::path::PathBuf; /// /// This function eliminates duplicated code across the various `main.rs` /// entry-points. -pub fn run_with_sandbox(main_fn: F) -> anyhow::Result<()> +pub fn arg0_dispatch_or_else(main_fn: F) -> anyhow::Result<()> where F: FnOnce(Option) -> Fut, Fut: Future>, @@ -40,7 +32,7 @@ where if exe_name == "codex-linux-sandbox" { // Safety: [`run_main`] never returns. - crate::run_main(); + codex_linux_sandbox::run_main(); } // This modifies the environment, which is not thread-safe, so do this @@ -61,11 +53,6 @@ where }) } -#[cfg(not(target_os = "linux"))] -pub fn run_main() -> ! { - panic!("codex-linux-sandbox is only supported on Linux"); -} - /// Load env vars from ~/.codex/.env and `$(pwd)/.env`. fn load_dotenv() { if let Ok(codex_home) = codex_core::config::find_codex_home() { diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 943788157b..ab98764bed 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -18,12 +18,12 @@ workspace = true anyhow = "1" clap = { version = "4", features = ["derive"] } clap_complete = "4" +codex-arg0 = { path = "../arg0" } codex-chatgpt = { path = "../chatgpt" } codex-core = { path = "../core" } codex-common = { path = "../common", features = ["cli"] } codex-exec = { path = "../exec" } codex-login = { path = "../login" } -codex-linux-sandbox = { path = "../linux-sandbox" } codex-mcp-server = { path = "../mcp-server" } codex-tui = { path = "../tui" } serde_json = "1" diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7916a7dc79..efda03bda4 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -2,6 +2,7 @@ use clap::CommandFactory; use clap::Parser; use clap_complete::Shell; use clap_complete::generate; +use codex_arg0::arg0_dispatch_or_else; use codex_chatgpt::apply_command::ApplyCommand; use codex_chatgpt::apply_command::run_apply_command; use codex_cli::LandlockCommand; @@ -92,7 +93,7 @@ struct LoginCommand { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { cli_main(codex_linux_sandbox_exe).await?; Ok(()) }) diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index ed01b78ec8..c9d94deb5a 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -18,13 +18,13 @@ workspace = true anyhow = "1" chrono = "0.4.40" clap = { version = "4", features = ["derive"] } +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } codex-common = { path = "../common", features = [ "cli", "elapsed", "sandbox_summary", ] } -codex-linux-sandbox = { path = "../linux-sandbox" } owo-colors = "4.2.0" serde_json = "1" shlex = "1.3.0" diff --git a/codex-rs/exec/src/main.rs b/codex-rs/exec/src/main.rs index 3a8e1f9411..03ee533ea9 100644 --- a/codex-rs/exec/src/main.rs +++ b/codex-rs/exec/src/main.rs @@ -10,6 +10,7 @@ //! This allows us to ship a completely separate set of functionality as part //! of the `codex-exec` binary. use clap::Parser; +use codex_arg0::arg0_dispatch_or_else; use codex_common::CliConfigOverrides; use codex_exec::Cli; use codex_exec::run_main; @@ -24,7 +25,7 @@ struct TopCli { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { let top_cli = TopCli::parse(); // Merge root-level overrides into inner CLI struct so downstream logic remains unchanged. let mut inner = top_cli.inner; diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml index 5c2dea6083..4b173ea17a 100644 --- a/codex-rs/linux-sandbox/Cargo.toml +++ b/codex-rs/linux-sandbox/Cargo.toml @@ -14,15 +14,16 @@ path = "src/lib.rs" [lints] workspace = true -[dependencies] +[target.'cfg(target_os = "linux")'.dependencies] anyhow = "1" clap = { version = "4", features = ["derive"] } codex-common = { path = "../common", features = ["cli"] } codex-core = { path = "../core" } -dotenvy = "0.15.7" -tokio = { version = "1", features = ["rt-multi-thread"] } +libc = "0.2.172" +landlock = "0.4.1" +seccompiler = "0.5.0" -[dev-dependencies] +[target.'cfg(target_os = "linux")'.dev-dependencies] tempfile = "3" tokio = { version = "1", features = [ "io-std", @@ -31,8 +32,3 @@ tokio = { version = "1", features = [ "rt-multi-thread", "signal", ] } - -[target.'cfg(target_os = "linux")'.dependencies] -libc = "0.2.172" -landlock = "0.4.1" -seccompiler = "0.5.0" diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 1088b92481..488ee6a67c 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -16,8 +16,8 @@ workspace = true [dependencies] anyhow = "1" +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } -codex-linux-sandbox = { path = "../linux-sandbox" } mcp-types = { path = "../mcp-types" } schemars = "0.8.22" serde = { version = "1", features = ["derive"] } diff --git a/codex-rs/mcp-server/src/main.rs b/codex-rs/mcp-server/src/main.rs index 51c46c44d2..60ddeeab41 100644 --- a/codex-rs/mcp-server/src/main.rs +++ b/codex-rs/mcp-server/src/main.rs @@ -1,7 +1,8 @@ +use codex_arg0::arg0_dispatch_or_else; use codex_mcp_server::run_main; fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { run_main(codex_linux_sandbox_exe).await?; Ok(()) }) diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 9d73e3b386..9161cfbb35 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -19,6 +19,7 @@ anyhow = "1" base64 = "0.22.1" clap = { version = "4", features = ["derive"] } codex-ansi-escape = { path = "../ansi-escape" } +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } codex-common = { path = "../common", features = [ "cli", @@ -26,7 +27,6 @@ codex-common = { path = "../common", features = [ "sandbox_summary", ] } codex-file-search = { path = "../file-search" } -codex-linux-sandbox = { path = "../linux-sandbox" } codex-login = { path = "../login" } color-eyre = "0.6.3" crossterm = { version = "0.28.1", features = ["bracketed-paste"] } diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index fdb3cdaf82..480e56e88e 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use codex_arg0::arg0_dispatch_or_else; use codex_common::CliConfigOverrides; use codex_tui::Cli; use codex_tui::run_main; @@ -13,7 +14,7 @@ struct TopCli { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { let top_cli = TopCli::parse(); let mut inner = top_cli.inner; inner