diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 065f4826a..0b5b0b4b6 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -52,6 +52,10 @@ pub struct Cli { #[arg(long = "skip-git-repo-check", default_value_t = false)] pub skip_git_repo_check: bool, + /// Additional directories that should be writable alongside the primary workspace. + #[arg(long = "add-dir", value_name = "DIR", value_hint = clap::ValueHint::DirPath)] + pub add_dir: Vec, + /// Path to a JSON Schema file describing the model's final response shape. #[arg(long = "output-schema", value_name = "FILE")] pub output_schema: Option, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 13d43f61e..a9cf6b2c6 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -62,6 +62,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any dangerously_bypass_approvals_and_sandbox, cwd, skip_git_repo_check, + add_dir, color, last_message_file, json: json_mode, @@ -180,7 +181,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any show_raw_agent_reasoning: oss.then_some(true), tools_web_search_request: None, experimental_sandbox_command_assessment: None, - additional_writable_roots: Vec::new(), + additional_writable_roots: add_dir, }; // Parse `-c` overrides. let cli_kv_overrides = match config_overrides.parse_overrides() { diff --git a/codex-rs/exec/tests/suite/add_dir.rs b/codex-rs/exec/tests/suite/add_dir.rs new file mode 100644 index 000000000..2093c46ac --- /dev/null +++ b/codex-rs/exec/tests/suite/add_dir.rs @@ -0,0 +1,72 @@ +#![cfg(not(target_os = "windows"))] +#![allow(clippy::expect_used, clippy::unwrap_used)] + +use core_test_support::responses; +use core_test_support::test_codex_exec::test_codex_exec; + +/// Verify that the --add-dir flag is accepted and the command runs successfully. +/// This test confirms the CLI argument is properly wired up. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn accepts_add_dir_flag() -> anyhow::Result<()> { + let test = test_codex_exec(); + + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("response_1"), + responses::ev_assistant_message("response_1", "Task completed"), + responses::ev_completed("response_1"), + ]); + responses::mount_sse_once(&server, body).await; + + // Create temporary directories to use with --add-dir + let temp_dir1 = tempfile::tempdir()?; + let temp_dir2 = tempfile::tempdir()?; + + test.cmd_with_server(&server) + .arg("--skip-git-repo-check") + .arg("--sandbox") + .arg("workspace-write") + .arg("--add-dir") + .arg(temp_dir1.path()) + .arg("--add-dir") + .arg(temp_dir2.path()) + .arg("test with additional directories") + .assert() + .code(0); + + Ok(()) +} + +/// Verify that multiple --add-dir flags can be specified. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn accepts_multiple_add_dir_flags() -> anyhow::Result<()> { + let test = test_codex_exec(); + + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("response_1"), + responses::ev_assistant_message("response_1", "Multiple directories accepted"), + responses::ev_completed("response_1"), + ]); + responses::mount_sse_once(&server, body).await; + + let temp_dir1 = tempfile::tempdir()?; + let temp_dir2 = tempfile::tempdir()?; + let temp_dir3 = tempfile::tempdir()?; + + test.cmd_with_server(&server) + .arg("--skip-git-repo-check") + .arg("--sandbox") + .arg("workspace-write") + .arg("--add-dir") + .arg(temp_dir1.path()) + .arg("--add-dir") + .arg(temp_dir2.path()) + .arg("--add-dir") + .arg(temp_dir3.path()) + .arg("test with three directories") + .assert() + .code(0); + + Ok(()) +} diff --git a/codex-rs/exec/tests/suite/mod.rs b/codex-rs/exec/tests/suite/mod.rs index 052c43bf3..77012ee3b 100644 --- a/codex-rs/exec/tests/suite/mod.rs +++ b/codex-rs/exec/tests/suite/mod.rs @@ -1,4 +1,5 @@ // Aggregates all former standalone integration tests as modules. +mod add_dir; mod apply_patch; mod auth_env; mod originator; diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index be05e7c3e..3c47c5572 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -18,6 +18,8 @@ export type CodexExecArgs = { sandboxMode?: SandboxMode; // --cd workingDirectory?: string; + // --add-dir + additionalDirectories?: string[]; // --skip-git-repo-check skipGitRepoCheck?: boolean; // --output-schema @@ -58,6 +60,12 @@ export class CodexExec { commandArgs.push("--cd", args.workingDirectory); } + if (args.additionalDirectories?.length) { + for (const dir of args.additionalDirectories) { + commandArgs.push("--add-dir", dir); + } + } + if (args.skipGitRepoCheck) { commandArgs.push("--skip-git-repo-check"); } diff --git a/sdk/typescript/src/thread.ts b/sdk/typescript/src/thread.ts index 90c7a8fac..6aed29c84 100644 --- a/sdk/typescript/src/thread.ts +++ b/sdk/typescript/src/thread.ts @@ -90,6 +90,7 @@ export class Thread { networkAccessEnabled: options?.networkAccessEnabled, webSearchEnabled: options?.webSearchEnabled, approvalPolicy: options?.approvalPolicy, + additionalDirectories: options?.additionalDirectories, }); try { for await (const item of generator) { diff --git a/sdk/typescript/src/threadOptions.ts b/sdk/typescript/src/threadOptions.ts index 77d91b89e..d81ffc60c 100644 --- a/sdk/typescript/src/threadOptions.ts +++ b/sdk/typescript/src/threadOptions.ts @@ -13,4 +13,5 @@ export type ThreadOptions = { networkAccessEnabled?: boolean; webSearchEnabled?: boolean; approvalPolicy?: ApprovalMode; + additionalDirectories?: string[]; }; diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 1a85b5823..586e396a3 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -348,6 +348,48 @@ describe("Codex", () => { } }); + it("passes additionalDirectories as repeated flags", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("Additional directories applied", "item_1"), + responseCompleted("response_1"), + ), + ], + }); + + const { args: spawnArgs, restore } = codexExecSpy(); + + try { + const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" }); + + const thread = client.startThread({ + additionalDirectories: ["../backend", "/tmp/shared"], + }); + await thread.run("test additional dirs"); + + const commandArgs = spawnArgs[0]; + expect(commandArgs).toBeDefined(); + if (!commandArgs) { + throw new Error("Command args missing"); + } + + // Find the --add-dir flags + const addDirArgs: string[] = []; + for (let i = 0; i < commandArgs.length; i += 1) { + if (commandArgs[i] === "--add-dir") { + addDirArgs.push(commandArgs[i + 1] ?? ""); + } + } + expect(addDirArgs).toEqual(["../backend", "/tmp/shared"]); + } finally { + restore(); + await close(); + } + }); + it("writes output schema to a temporary file and forwards it", async () => { const { url, close, requests } = await startResponsesTestProxy({ statusCode: 200,