mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
986c60467b
## Why #23813 switches the Windows sandbox runner path to `PermissionProfile`, but it still left one runtime anchor for resolving symbolic `:workspace_roots` entries. That is not enough once a turn has multiple effective workspace roots: exact entries and deny globs under `:workspace_roots` need to be materialized for every runtime root before the command runner chooses token mode or builds ACL plans. ## What Changed - Replaces the Windows runner/setup `permission_profile_cwd` plumbing with `workspace_roots: Vec<AbsolutePathBuf>`. - Resolves Windows-local `PermissionProfile` data with `materialize_project_roots_with_workspace_roots(...)` instead of the single-cwd helper. - Threads `Config::effective_workspace_roots()` through core execution, unified exec, TUI setup/read-grant flows, app-server setup, app-server `command/exec`, and `debug sandbox` on Windows. - Preserves those workspace roots through the zsh-fork escalation executor instead of rebuilding them from `sandbox_policy_cwd`. - Makes `ExecRequest::new(...)` and the remaining `build_exec_request(...)` helper path take `windows_sandbox_workspace_roots` explicitly so new call sites cannot silently fall back to `vec![cwd]`. - Clarifies the `debug sandbox` non-Windows comment: remaining cwd-dependent resolution still uses `sandbox_policy_cwd`, while `:workspace_roots` entries are already materialized from config roots. - Updates elevated runner IPC `SpawnRequest` to send `workspace_roots` and bumps the framed IPC protocol version to `3` for the payload shape change. - Adds Windows-local resolver coverage for expanding exact and glob `:workspace_roots` entries across multiple roots, plus core helper coverage proving explicit roots are preserved. ## Verification - `cargo check -p codex-windows-sandbox -p codex-core -p codex-tui -p codex-cli -p codex-app-server` - `cargo test -p codex-windows-sandbox` - `cargo test -p codex-core windows_sandbox` - `cargo test -p codex-core unix_escalation` - `cargo test -p codex-app-server windows_sandbox` - `cargo test -p codex-tui windows_sandbox` - `cargo test -p codex-cli debug_sandbox` - `just test -p codex-core unified_exec` - `just test -p codex-core build_exec_request_preserves_windows_workspace_roots` - `env -u CODEX_NETWORK_PROXY_ACTIVE -u CODEX_NETWORK_ALLOW_LOCAL_BINDING just test -p codex-app-server --lib command_exec` - `just test -p codex-windows-sandbox` - `just test -p codex-exec sandbox` - `just fix -p codex-core -p codex-app-server -p codex-windows-sandbox` A local macOS cross-check with `cargo check --target x86_64-pc-windows-msvc ...` did not reach crate Rust code because native dependencies require Windows SDK headers (`windows.h` / `assert.h`) in this environment; Windows CI remains the real target validation. Two local targeted filters compile but do not run assertions on macOS: `env -u CODEX_NETWORK_PROXY_ACTIVE -u CODEX_NETWORK_ALLOW_LOCAL_BINDING just test -p codex-app-server --lib command_exec_processor` matched zero tests, and `just test -p codex-linux-sandbox landlock` matched zero tests because the landlock suite is Linux-only.
176 lines
4.9 KiB
Rust
176 lines
4.9 KiB
Rust
#![cfg(target_os = "macos")]
|
|
|
|
use codex_core::exec::ExecCapturePolicy;
|
|
use codex_core::exec::ExecParams;
|
|
use codex_core::exec::process_exec_tool_call;
|
|
use codex_core::sandboxing::SandboxPermissions;
|
|
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::error::Result;
|
|
use codex_protocol::exec_output::ExecToolCallOutput;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_sandboxing::SandboxType;
|
|
use codex_sandboxing::get_platform_sandbox;
|
|
use core_test_support::PathExt;
|
|
use std::collections::HashMap;
|
|
use tempfile::TempDir;
|
|
|
|
fn skip_test() -> bool {
|
|
if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) {
|
|
eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test.");
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
#[expect(clippy::expect_used)]
|
|
async fn run_test_cmd<I, S>(tmp: TempDir, command: I) -> Result<ExecToolCallOutput>
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: Into<String>,
|
|
{
|
|
let sandbox_type = get_platform_sandbox(/*windows_sandbox_enabled*/ false)
|
|
.expect("should be able to get sandbox type");
|
|
assert_eq!(sandbox_type, SandboxType::MacosSeatbelt);
|
|
let cwd = tmp.path().abs();
|
|
|
|
let params = ExecParams {
|
|
command: command.into_iter().map(Into::into).collect(),
|
|
cwd: cwd.clone(),
|
|
expiration: 1000.into(),
|
|
capture_policy: ExecCapturePolicy::ShellTool,
|
|
env: HashMap::new(),
|
|
network: None,
|
|
sandbox_permissions: SandboxPermissions::UseDefault,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
windows_sandbox_private_desktop: false,
|
|
justification: None,
|
|
arg0: None,
|
|
};
|
|
|
|
process_exec_tool_call(
|
|
params,
|
|
&PermissionProfile::read_only(),
|
|
&cwd,
|
|
std::slice::from_ref(&cwd),
|
|
&None,
|
|
/*use_legacy_landlock*/ false,
|
|
/*stdout_stream*/ None,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Command succeeds with exit code 0 normally
|
|
#[tokio::test]
|
|
async fn exit_code_0_succeeds() {
|
|
if skip_test() {
|
|
return;
|
|
}
|
|
|
|
let tmp = TempDir::new().expect("should be able to create temp dir");
|
|
let cmd = vec!["echo", "hello"];
|
|
|
|
let output = run_test_cmd(tmp, cmd).await.unwrap();
|
|
assert_eq!(output.stdout.text, "hello\n");
|
|
assert_eq!(output.stderr.text, "");
|
|
assert_eq!(output.stdout.truncated_after_lines, None);
|
|
}
|
|
|
|
/// Command succeeds with exit code 0 normally
|
|
#[tokio::test]
|
|
async fn truncates_output_lines() {
|
|
if skip_test() {
|
|
return;
|
|
}
|
|
|
|
let tmp = TempDir::new().expect("should be able to create temp dir");
|
|
let cmd = vec!["seq", "300"];
|
|
|
|
let output = run_test_cmd(tmp, cmd).await.unwrap();
|
|
|
|
let expected_output = (1..=300)
|
|
.map(|i| format!("{i}\n"))
|
|
.collect::<Vec<_>>()
|
|
.join("");
|
|
assert_eq!(output.stdout.text, expected_output);
|
|
assert_eq!(output.stdout.truncated_after_lines, None);
|
|
}
|
|
|
|
/// Command succeeds with exit code 0 normally
|
|
#[tokio::test]
|
|
async fn truncates_output_bytes() {
|
|
if skip_test() {
|
|
return;
|
|
}
|
|
|
|
let tmp = TempDir::new().expect("should be able to create temp dir");
|
|
// each line is 1000 bytes
|
|
let cmd = vec!["bash", "-lc", "seq 15 | awk '{printf \"%-1000s\\n\", $0}'"];
|
|
|
|
let output = run_test_cmd(tmp, cmd).await.unwrap();
|
|
|
|
assert!(output.stdout.text.len() >= 15000);
|
|
assert_eq!(output.stdout.truncated_after_lines, None);
|
|
}
|
|
|
|
/// Command not found returns exit code 127, this is not considered a sandbox error
|
|
#[tokio::test]
|
|
async fn exit_command_not_found_is_ok() {
|
|
if skip_test() {
|
|
return;
|
|
}
|
|
|
|
let tmp = TempDir::new().expect("should be able to create temp dir");
|
|
let cmd = vec!["/bin/bash", "-c", "nonexistent_command_12345"];
|
|
run_test_cmd(tmp, cmd).await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn openpty_works_under_real_exec_seatbelt_path() {
|
|
if skip_test() {
|
|
return;
|
|
}
|
|
|
|
let python = match which::which("python3") {
|
|
Ok(path) => path,
|
|
Err(_) => {
|
|
eprintln!("python3 not found in PATH, skipping test.");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let tmp = TempDir::new().expect("should be able to create temp dir");
|
|
let cmd = vec![
|
|
python.to_string_lossy().into_owned(),
|
|
"-c".to_string(),
|
|
r#"import os
|
|
|
|
master, slave = os.openpty()
|
|
os.write(slave, b"ping")
|
|
assert os.read(master, 4) == b"ping""#
|
|
.to_string(),
|
|
];
|
|
|
|
let output = run_test_cmd(tmp, cmd).await.unwrap();
|
|
assert_eq!(output.stdout.text, "");
|
|
assert_eq!(output.stderr.text, "");
|
|
}
|
|
|
|
/// Writing a file fails and should be considered a sandbox error
|
|
#[tokio::test]
|
|
async fn write_file_fails_as_sandbox_error() {
|
|
if skip_test() {
|
|
return;
|
|
}
|
|
|
|
let tmp = TempDir::new().expect("should be able to create temp dir");
|
|
let path = tmp.path().join("test.txt");
|
|
let cmd = vec![
|
|
"/usr/bin/touch",
|
|
path.to_str().expect("should be able to get path"),
|
|
];
|
|
|
|
assert!(run_test_cmd(tmp, cmd).await.is_err());
|
|
}
|