Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion codex-rs/exec/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ pub struct Cli {
/// Select the sandbox policy to use when executing model-generated shell
/// commands.
#[arg(long = "sandbox", short = 's', value_enum)]
pub sandbox_mode: Option<codex_common::SandboxModeCliArg>,
pub sandbox_mode: Option<SandboxCliArg>,

/// When using `--sandbox external-sandbox`, declare whether outbound
/// network access is available to the external sandbox.
#[arg(long = "network-access", value_enum)]
pub external_sandbox_network_access: Option<NetworkAccessCliArg>,

/// Configuration profile from config.toml to specify default options.
#[arg(long = "profile", short = 'p')]
Expand Down Expand Up @@ -155,3 +160,30 @@ pub enum Color {
#[default]
Auto,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum SandboxCliArg {
ReadOnly,
WorkspaceWrite,
DangerFullAccess,

/// Indicates the process is already in an external sandbox.
ExternalSandbox,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum NetworkAccessCliArg {
Restricted,
Enabled,
}

impl NetworkAccessCliArg {
pub fn to_network_access(self) -> codex_core::protocol::NetworkAccess {
match self {
Self::Restricted => codex_core::protocol::NetworkAccess::Restricted,
Self::Enabled => codex_core::protocol::NetworkAccess::Enabled,
}
}
}
52 changes: 50 additions & 2 deletions codex-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ pub mod exec_events;

pub use cli::Cli;
pub use cli::Command;
pub use cli::NetworkAccessCliArg;
pub use cli::ReviewArgs;
pub use cli::SandboxCliArg;
use codex_common::oss::ensure_oss_provider_ready;
use codex_common::oss::get_default_model_for_oss_provider;
use codex_core::AuthManager;
Expand All @@ -33,6 +35,7 @@ use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use codex_core::protocol::ReviewRequest;
use codex_core::protocol::ReviewTarget;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::SessionSource;
use codex_protocol::approvals::ElicitationAction;
use codex_protocol::config_types::SandboxMode;
Expand Down Expand Up @@ -88,6 +91,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
last_message_file,
json: json_mode,
sandbox_mode: sandbox_mode_cli_arg,
external_sandbox_network_access,
prompt,
output_schema: output_schema_path,
config_overrides,
Expand Down Expand Up @@ -115,12 +119,49 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
.with_writer(std::io::stderr)
.with_filter(env_filter);

if full_auto && matches!(sandbox_mode_cli_arg, Some(SandboxCliArg::ExternalSandbox)) {
return Err(anyhow::anyhow!(
"--sandbox external-sandbox cannot be used with --full-auto"
));
}
if dangerously_bypass_approvals_and_sandbox
&& matches!(sandbox_mode_cli_arg, Some(SandboxCliArg::ExternalSandbox))
{
return Err(anyhow::anyhow!(
"--sandbox external-sandbox cannot be used with --dangerously-bypass-approvals-and-sandbox"
));
}
if external_sandbox_network_access.is_some()
&& !matches!(sandbox_mode_cli_arg, Some(SandboxCliArg::ExternalSandbox))
{
return Err(anyhow::anyhow!(
"--network-access can only be used with --sandbox external-sandbox"
));
}

let external_sandbox_override: Option<SandboxPolicy> =
if matches!(sandbox_mode_cli_arg, Some(SandboxCliArg::ExternalSandbox)) {
Some(SandboxPolicy::ExternalSandbox {
network_access: external_sandbox_network_access
.unwrap_or(NetworkAccessCliArg::Restricted)
.to_network_access(),
})
} else {
None
};

let sandbox_mode = if full_auto {
Some(SandboxMode::WorkspaceWrite)
} else if dangerously_bypass_approvals_and_sandbox {
Some(SandboxMode::DangerFullAccess)
} else {
sandbox_mode_cli_arg.map(Into::<SandboxMode>::into)
match sandbox_mode_cli_arg {
Some(SandboxCliArg::ReadOnly) => Some(SandboxMode::ReadOnly),
Some(SandboxCliArg::WorkspaceWrite) => Some(SandboxMode::WorkspaceWrite),
Some(SandboxCliArg::DangerFullAccess) => Some(SandboxMode::DangerFullAccess),
Some(SandboxCliArg::ExternalSandbox) => None,
None => None,
}
};

// Parse `-c` overrides from the CLI.
Expand Down Expand Up @@ -215,9 +256,16 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
additional_writable_roots: add_dir,
};

let config =
let mut config =
Config::load_with_cli_overrides_and_harness_overrides(cli_kv_overrides, overrides).await?;

if let Some(sandbox_policy) = external_sandbox_override {
config
.sandbox_policy
.set(sandbox_policy)
.map_err(|err| anyhow::anyhow!("Invalid external sandbox policy: {err}"))?;
}

if let Err(err) = enforce_login_restrictions(&config).await {
eprintln!("{err}");
std::process::exit(1);
Expand Down
1 change: 1 addition & 0 deletions codex-rs/exec/tests/suite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ mod originator;
mod output_schema;
mod resume;
mod sandbox;
mod sandbox_mode;
mod server_error_exit;
163 changes: 163 additions & 0 deletions codex-rs/exec/tests/suite/sandbox_mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#![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;
use pretty_assertions::assert_eq;

async fn run_exec_with_server(args: &[&str], prompt: &str) -> anyhow::Result<String> {
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;

let output = {
let mut cmd = test.cmd_with_server(&server);
cmd.arg("--skip-git-repo-check");
for arg in args {
cmd.arg(arg);
}
cmd.arg(prompt).output()?
};

assert!(output.status.success(), "run failed: {output:?}");
Ok(String::from_utf8(output.stderr)?)
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn accepts_read_only_sandbox_flag() -> anyhow::Result<()> {
let stderr =
run_exec_with_server(&["--sandbox", "read-only"], "test read-only sandbox").await?;
assert!(stderr.contains("sandbox: read-only"), "{stderr}");
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn accepts_workspace_write_sandbox_flag() -> anyhow::Result<()> {
let stderr = run_exec_with_server(
&["--sandbox", "workspace-write"],
"test workspace-write sandbox",
)
.await?;
assert!(stderr.contains("sandbox: workspace-write"), "{stderr}");
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn accepts_danger_full_access_sandbox_flag() -> anyhow::Result<()> {
let stderr = run_exec_with_server(
&["--sandbox", "danger-full-access"],
"test danger-full-access sandbox",
)
.await?;
assert!(stderr.contains("sandbox: danger-full-access"), "{stderr}");
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn accepts_external_sandbox_flag_defaults_to_restricted_network() -> anyhow::Result<()> {
let stderr =
run_exec_with_server(&["--sandbox", "external-sandbox"], "test external sandbox").await?;
assert!(stderr.contains("sandbox: external-sandbox"), "{stderr}");
assert!(
!stderr.contains("network access enabled"),
"stderr unexpectedly claims network access enabled: {stderr}"
);

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn accepts_external_sandbox_with_enabled_network_access() -> anyhow::Result<()> {
let stderr = run_exec_with_server(
&[
"--sandbox",
"external-sandbox",
"--network-access",
"enabled",
],
"test external sandbox network enabled",
)
.await?;
assert!(
stderr.contains("sandbox: external-sandbox (network access enabled)"),
"{stderr}"
);

Ok(())
}

#[test]
fn rejects_network_access_without_external_sandbox() -> anyhow::Result<()> {
let test = test_codex_exec();

let output = test
.cmd()
.arg("--skip-git-repo-check")
.arg("--network-access")
.arg("enabled")
.arg("test")
.output()?;

assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8(output.stderr)?;
assert!(
stderr.contains("--network-access can only be used with --sandbox external-sandbox"),
"{stderr}"
);

Ok(())
}

#[test]
fn rejects_external_sandbox_with_full_auto() -> anyhow::Result<()> {
let test = test_codex_exec();

let output = test
.cmd()
.arg("--skip-git-repo-check")
.arg("--full-auto")
.arg("--sandbox")
.arg("external-sandbox")
.arg("test")
.output()?;

assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8(output.stderr)?;
assert!(
stderr.contains("--sandbox external-sandbox cannot be used with --full-auto"),
"{stderr}"
);

Ok(())
}

#[test]
fn rejects_external_sandbox_with_dangerously_bypass_approvals_and_sandbox() -> anyhow::Result<()> {
let test = test_codex_exec();

let output = test
.cmd()
.arg("--skip-git-repo-check")
.arg("--sandbox")
.arg("external-sandbox")
.arg("--dangerously-bypass-approvals-and-sandbox")
.arg("test")
.output()?;

assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8(output.stderr)?;
assert!(
stderr.contains(
"--sandbox external-sandbox cannot be used with --dangerously-bypass-approvals-and-sandbox"
),
"{stderr}"
);

Ok(())
}
Loading