mirror of
https://github.com/openai/codex.git
synced 2026-11-11 21:10:39 +00:00
## 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
171 lines
4.5 KiB
Rust
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(())
|
|
}
|