feat: Add support for --add-dir to exec and TypeScript SDK (#6565)

## Summary

Adds support for specifying additional directories in the TypeScript SDK
through a new `additionalDirectories` option in `ThreadOptions`.

## Changes

- Added `additionalDirectories` parameter to `ThreadOptions` interface
- Updated `CodexExec` to accept and pass through additional directories
via the `--config` flag for `sandbox_workspace_write.writable_roots`
- Added comprehensive test coverage for the new functionality

## Test plan

- Added test case that verifies `additionalDirectories` is correctly
passed as repeated flags
- Existing tests continue to pass

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Dan Hernandez
2025-11-13 16:47:10 -05:00
committed by GitHub
parent 439bc5dbbe
commit b4a53aef47
8 changed files with 131 additions and 1 deletions

View File

@@ -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<PathBuf>,
/// Path to a JSON Schema file describing the model's final response shape.
#[arg(long = "output-schema", value_name = "FILE")]
pub output_schema: Option<PathBuf>,

View File

@@ -62,6 +62,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> 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<PathBuf>) -> 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() {

View File

@@ -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(())
}

View File

@@ -1,4 +1,5 @@
// Aggregates all former standalone integration tests as modules.
mod add_dir;
mod apply_patch;
mod auth_env;
mod originator;

View File

@@ -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");
}

View File

@@ -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) {

View File

@@ -13,4 +13,5 @@ export type ThreadOptions = {
networkAccessEnabled?: boolean;
webSearchEnabled?: boolean;
approvalPolicy?: ApprovalMode;
additionalDirectories?: string[];
};

View File

@@ -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,