Files
codex/codex-rs/cli/tests/sandbox_network_proxy.rs
T
viyatb-oai d2efdf1986 Route sandbox CLI launches through the shared sandbox manager (#60517)
## What changed

- Use `SandboxManager` to prepare `codex sandbox` commands, including permission profiles, workspace roots, and managed network proxy settings.
- Return a `WrappedSandboxCommand` that can produce an execution request or a configured process command, preserving native executable paths and Linux helper `argv[0]`. Require the session launcher for native Windows token sandboxes.
- Move macOS inherited-terminal policy handling into the shared manager, retaining Unix socket grants and the terminal input injection denial.
- Prepare managed network context alongside proxy environment variables and pass it through sandbox transformation.

## Testing

- Extend sandbox network proxy tests to macOS, covering blocked direct loopback access and explicitly allowed requests.
- Add coverage for Linux helper executable paths and dispatch names, including non-UTF-8 paths.

GitOrigin-RevId: 8b7396d21b2f34122699388f16047329a3f5e5c6
2026-11-10 04:01:23 +00:00

171 lines
4.5 KiB
Rust

#![cfg(any(target_os = "linux", target_os = "macos"))]
use std::net::TcpListener;
use anyhow::Result;
use tempfile::TempDir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
#[cfg(target_os = "macos")]
const LOOPBACK: &str = "127.0.0.1";
#[cfg(target_os = "linux")]
const LOOPBACK: &str = "127.0.0.2";
fn sandbox_unavailable(stderr: &str) -> Result<bool> {
#[cfg(target_os = "linux")]
if stderr.contains("bubblewrap is unavailable") {
eprintln!("skipping network proxy sandbox test: bubblewrap is unavailable");
return Ok(true);
}
#[cfg(target_os = "macos")]
if stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted") {
let control = std::process::Command::new("/usr/bin/sandbox-exec")
.args(["-p", "(version 1)(allow default)", "/usr/bin/true"])
.output()?;
if String::from_utf8_lossy(&control.stderr)
.contains("sandbox_apply: Operation not permitted")
{
eprintln!("skipping network proxy sandbox test: nested Seatbelt is unavailable");
return Ok(true);
}
}
Ok(false)
}
#[test]
fn sandbox_with_network_proxy_blocks_direct_loopback_access() -> Result<()> {
let codex_home = TempDir::new()?;
let listener = TcpListener::bind((LOOPBACK, 0))?;
let port = listener.local_addr()?.port();
std::fs::write(
codex_home.path().join("config.toml"),
r#"
default_permissions = "network-test"
[features]
network_proxy = true
use_legacy_landlock = true
[permissions.network-test]
extends = ":workspace"
[permissions.network-test.network]
enabled = true
mode = "full"
"#,
)?;
let url = format!("http://{LOOPBACK}:{port}/");
let output = std::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
.env("CODEX_HOME", codex_home.path())
.args([
"sandbox",
"--permission-profile",
"network-test",
"--",
"curl",
"--noproxy",
"*",
"--silent",
"--show-error",
"--connect-timeout",
"1",
"--max-time",
"2",
url.as_str(),
])
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
if sandbox_unavailable(&stderr)? {
return Ok(());
}
assert_eq!(
output.status.code(),
Some(7),
"expected direct loopback access to be blocked; status={:?}; stdout={}; stderr={}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
stderr,
);
Ok(())
}
#[tokio::test]
async fn sandbox_with_network_proxy_allows_explicit_loopback_access() -> Result<()> {
let codex_home = TempDir::new()?;
let listener = TcpListener::bind((LOOPBACK, 0))?;
let port = listener.local_addr()?.port();
let server = MockServer::builder().listener(listener).start().await;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
std::fs::write(
codex_home.path().join("config.toml"),
format!(
r#"
default_permissions = "network-test"
[features]
network_proxy = true
use_legacy_landlock = true
[permissions.network-test]
extends = ":workspace"
[permissions.network-test.network]
enabled = true
mode = "full"
allow_local_binding = false
[permissions.network-test.network.domains]
"{LOOPBACK}" = "allow"
"#
),
)?;
let url = format!("http://{LOOPBACK}:{port}/");
let output = tokio::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
.env("CODEX_HOME", codex_home.path())
.args([
"sandbox",
"--permission-profile",
"network-test",
"--",
"curl",
"--fail",
"--silent",
"--show-error",
"--connect-timeout",
"2",
"--max-time",
"4",
url.as_str(),
])
.output()
.await?;
let stderr = String::from_utf8_lossy(&output.stderr);
if sandbox_unavailable(&stderr)? {
return Ok(());
}
assert!(
output.status.success(),
"expected allowlisted loopback access to succeed; status={:?}; stdout={}; stderr={}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
stderr,
);
assert_eq!(server.received_requests().await.unwrap().len(), 1);
Ok(())
}