diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index f3903d6e..653c3e4e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -610,6 +610,7 @@ name = "codex-arg0" version = "0.0.0" dependencies = [ "anyhow", + "codex-apply-patch", "codex-core", "codex-linux-sandbox", "dotenvy", @@ -718,14 +719,17 @@ name = "codex-exec" version = "0.0.0" dependencies = [ "anyhow", + "assert_cmd", "chrono", "clap", "codex-arg0", "codex-common", "codex-core", "owo-colors", + "predicates", "serde_json", "shlex", + "tempfile", "tokio", "tracing", "tracing-subscriber", diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index 9ad18967..7c55ac0d 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -12,6 +12,7 @@ workspace = true [dependencies] anyhow = "1" +codex-apply-patch = { path = "../apply-patch" } codex-core = { path = "../core" } codex-linux-sandbox = { path = "../linux-sandbox" } dotenvy = "0.15.7" diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 624583b8..d7109176 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -30,7 +30,8 @@ where Fut: Future>, { // Determine if we were invoked via the special alias. - let argv0 = std::env::args_os().next().unwrap_or_default(); + let mut args = std::env::args_os(); + let argv0 = args.next().unwrap_or_default(); let exe_name = Path::new(&argv0) .file_name() .and_then(|s| s.to_str()) @@ -41,6 +42,26 @@ where codex_linux_sandbox::run_main(); } + let argv1 = args.next().unwrap_or_default(); + if argv1 == "--codex-run-as-apply-patch" { + let patch_arg = args.next().and_then(|s| s.to_str().map(|s| s.to_owned())); + let exit_code = match patch_arg { + Some(patch_arg) => { + let mut stdout = std::io::stdout(); + let mut stderr = std::io::stderr(); + match codex_apply_patch::apply_patch(&patch_arg, &mut stdout, &mut stderr) { + Ok(()) => 0, + Err(_) => 1, + } + } + None => { + eprintln!("Error: --codex-run-as-apply-patch requires a UTF-8 PATCH argument."); + 1 + } + }; + std::process::exit(exit_code); + } + // This modifies the environment, which is not thread-safe, so do this // before creating any threads/the Tokio runtime. load_dotenv(); diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 9b3e59c8..9a4c255a 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -2,9 +2,18 @@ This crate implements the business logic for Codex. It is designed to be used by the various Codex UIs written in Rust. -Though for non-Rust UIs, we are also working to define a _protocol_ for talking to Codex. See: +## Dependencies -- [Specification](../docs/protocol_v1.md) -- [Rust types](./src/protocol.rs) +Note that `codex-core` makes some assumptions about certain helper utilities being available in the environment. Currently, this -You can use the `proto` subcommand using the executable in the [`cli` crate](../cli) to speak the protocol using newline-delimited-JSON over stdin/stdout. +### macOS + +Expects `/usr/bin/sandbox-exec` to be present. + +### Linux + +Expects the binary containing `codex-core` to run the equivalent of `codex debug landlock` when `arg0` is `codex-linux-sandbox`. See the `codex-arg0` crate for details. + +### All Platforms + +Expects the binary containing `codex-core` to simulate the virtual `apply_patch` CLI when `arg1` is `--codex-run-as-apply-patch`. See the `codex-arg0` crate for details. diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index c9d94deb..ced771f2 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -37,3 +37,8 @@ tokio = { version = "1", features = [ ] } tracing = { version = "0.1.41", features = ["log"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } + +[dev-dependencies] +assert_cmd = "2" +predicates = "3" +tempfile = "3.13.0" diff --git a/codex-rs/exec/tests/apply_patch.rs b/codex-rs/exec/tests/apply_patch.rs new file mode 100644 index 00000000..69ac1b8c --- /dev/null +++ b/codex-rs/exec/tests/apply_patch.rs @@ -0,0 +1,38 @@ +use anyhow::Context; +use assert_cmd::prelude::*; +use std::fs; +use std::process::Command; +use tempfile::tempdir; + +/// While we may add an `apply-patch` subcommand to the `codex` CLI multitool +/// at some point, we must ensure that the smaller `codex-exec` CLI can still +/// emulate the `apply_patch` CLI. +#[test] +fn test_standalone_exec_cli_can_use_apply_patch() -> anyhow::Result<()> { + let tmp = tempdir()?; + let relative_path = "source.txt"; + let absolute_path = tmp.path().join(relative_path); + fs::write(&absolute_path, "original content\n")?; + + Command::cargo_bin("codex-exec") + .context("should find binary for codex-exec")? + .arg("--codex-run-as-apply-patch") + .arg( + r#"*** Begin Patch +*** Update File: source.txt +@@ +-original content ++modified by apply_patch +*** End Patch"#, + ) + .current_dir(tmp.path()) + .assert() + .success() + .stdout("Success. Updated the following files:\nM source.txt\n") + .stderr(predicates::str::is_empty()); + assert_eq!( + fs::read_to_string(absolute_path)?, + "modified by apply_patch\n" + ); + Ok(()) +}