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.
287 lines
11 KiB
Rust
287 lines
11 KiB
Rust
use codex_protocol::models::PermissionProfile;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
pub struct ElevatedSandboxProfileCaptureRequest<'a> {
|
|
pub permission_profile: &'a PermissionProfile,
|
|
pub workspace_roots: &'a [AbsolutePathBuf],
|
|
pub codex_home: &'a Path,
|
|
pub command: Vec<String>,
|
|
pub cwd: &'a Path,
|
|
pub env_map: HashMap<String, String>,
|
|
pub timeout_ms: Option<u64>,
|
|
pub cancellation: Option<crate::WindowsSandboxCancellationToken>,
|
|
pub use_private_desktop: bool,
|
|
pub proxy_enforced: bool,
|
|
pub read_roots_override: Option<&'a [PathBuf]>,
|
|
pub read_roots_include_platform_defaults: bool,
|
|
pub write_roots_override: Option<&'a [PathBuf]>,
|
|
pub deny_read_paths_override: &'a [AbsolutePathBuf],
|
|
pub deny_write_paths_override: &'a [AbsolutePathBuf],
|
|
}
|
|
|
|
mod windows_impl {
|
|
use super::ElevatedSandboxProfileCaptureRequest;
|
|
use crate::acl::allow_null_device;
|
|
use crate::cap::load_or_create_cap_sids;
|
|
use crate::cap::workspace_write_cap_sid_for_root;
|
|
use crate::env::ensure_non_interactive_pager;
|
|
use crate::env::inherit_path_env;
|
|
use crate::env::normalize_null_device_env;
|
|
use crate::identity::require_logon_sandbox_creds;
|
|
use crate::ipc_framed::EmptyPayload;
|
|
use crate::ipc_framed::FramedMessage;
|
|
use crate::ipc_framed::Message;
|
|
use crate::ipc_framed::OutputStream;
|
|
use crate::ipc_framed::SpawnRequest;
|
|
use crate::ipc_framed::decode_bytes;
|
|
use crate::ipc_framed::read_frame;
|
|
use crate::ipc_framed::write_frame;
|
|
use crate::logging::log_failure;
|
|
use crate::logging::log_start;
|
|
use crate::logging::log_success;
|
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
|
use crate::runner_client::spawn_runner_transport;
|
|
use crate::sandbox_utils::ensure_codex_home_exists;
|
|
use crate::sandbox_utils::inject_git_safe_directory;
|
|
use crate::setup::effective_write_roots_for_permissions;
|
|
use crate::token::LocalSid;
|
|
use anyhow::Result;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use std::fs::File;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::sync::atomic::Ordering;
|
|
use std::time::Duration;
|
|
|
|
pub use crate::windows_impl::CaptureResult;
|
|
|
|
/// Polls for cancellation and sends the runner's terminate IPC frame when requested.
|
|
///
|
|
/// The 50 ms park bounds cancellation latency without busy-waiting.
|
|
fn spawn_cancel_writer(
|
|
pipe_write: &File,
|
|
cancellation: Option<crate::WindowsSandboxCancellationToken>,
|
|
) -> Result<Option<(std::thread::JoinHandle<()>, Arc<AtomicBool>)>> {
|
|
let Some(cancellation) = cancellation else {
|
|
return Ok(None);
|
|
};
|
|
let mut pipe_write = pipe_write.try_clone()?;
|
|
let done = Arc::new(AtomicBool::new(false));
|
|
let done_for_thread = Arc::clone(&done);
|
|
let handle = std::thread::spawn(move || {
|
|
while !done_for_thread.load(Ordering::SeqCst) {
|
|
if cancellation.is_cancelled() {
|
|
let _ = write_frame(
|
|
&mut pipe_write,
|
|
&FramedMessage {
|
|
version: 1,
|
|
message: Message::Terminate {
|
|
payload: EmptyPayload::default(),
|
|
},
|
|
},
|
|
);
|
|
break;
|
|
}
|
|
std::thread::park_timeout(Duration::from_millis(50));
|
|
}
|
|
});
|
|
Ok(Some((handle, done)))
|
|
}
|
|
|
|
/// Launches the command runner under the sandbox user and captures its output.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn run_windows_sandbox_capture_for_permission_profile(
|
|
request: ElevatedSandboxProfileCaptureRequest<'_>,
|
|
) -> Result<CaptureResult> {
|
|
let ElevatedSandboxProfileCaptureRequest {
|
|
permission_profile,
|
|
workspace_roots,
|
|
codex_home,
|
|
command,
|
|
cwd,
|
|
mut env_map,
|
|
timeout_ms,
|
|
cancellation,
|
|
use_private_desktop,
|
|
proxy_enforced,
|
|
read_roots_override,
|
|
read_roots_include_platform_defaults,
|
|
write_roots_override,
|
|
deny_read_paths_override,
|
|
deny_write_paths_override,
|
|
} = request;
|
|
let permissions =
|
|
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
|
|
permission_profile,
|
|
workspace_roots,
|
|
)?;
|
|
let deny_read_paths_override = deny_read_paths_override
|
|
.iter()
|
|
.map(AbsolutePathBuf::to_path_buf)
|
|
.collect::<Vec<_>>();
|
|
let deny_write_paths_override = deny_write_paths_override
|
|
.iter()
|
|
.map(AbsolutePathBuf::to_path_buf)
|
|
.collect::<Vec<_>>();
|
|
normalize_null_device_env(&mut env_map);
|
|
ensure_non_interactive_pager(&mut env_map);
|
|
inherit_path_env(&mut env_map);
|
|
inject_git_safe_directory(&mut env_map, cwd);
|
|
// Use a temp-based log dir that the sandbox user can write.
|
|
let sandbox_base = codex_home.join(".sandbox");
|
|
ensure_codex_home_exists(&sandbox_base)?;
|
|
|
|
let logs_base_dir: Option<&Path> = Some(sandbox_base.as_path());
|
|
log_start(&command, logs_base_dir);
|
|
let sandbox_creds = require_logon_sandbox_creds(
|
|
&permissions,
|
|
cwd,
|
|
&env_map,
|
|
codex_home,
|
|
read_roots_override,
|
|
read_roots_include_platform_defaults,
|
|
write_roots_override,
|
|
&deny_read_paths_override,
|
|
&deny_write_paths_override,
|
|
proxy_enforced,
|
|
)?;
|
|
// Build capability SID for ACL grants.
|
|
let caps = load_or_create_cap_sids(codex_home)?;
|
|
let (sid_for_null, cap_sids) = if permissions.uses_write_capabilities_for_cwd(cwd, &env_map)
|
|
{
|
|
let write_roots = effective_write_roots_for_permissions(
|
|
&permissions,
|
|
cwd,
|
|
&env_map,
|
|
codex_home,
|
|
write_roots_override,
|
|
);
|
|
let cap_sids = write_roots
|
|
.iter()
|
|
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
if cap_sids.is_empty() {
|
|
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
|
|
}
|
|
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
|
|
} else {
|
|
let sid = LocalSid::from_string(&caps.readonly)?;
|
|
(sid, vec![caps.readonly])
|
|
};
|
|
|
|
unsafe {
|
|
allow_null_device(sid_for_null.as_ptr());
|
|
}
|
|
|
|
(|| -> Result<CaptureResult> {
|
|
let spawn_request = SpawnRequest {
|
|
command: command.clone(),
|
|
cwd: cwd.to_path_buf(),
|
|
env: env_map.clone(),
|
|
permission_profile: permission_profile.clone(),
|
|
workspace_roots: workspace_roots.to_vec(),
|
|
codex_home: sandbox_base.clone(),
|
|
real_codex_home: codex_home.to_path_buf(),
|
|
cap_sids,
|
|
timeout_ms,
|
|
tty: false,
|
|
stdin_open: false,
|
|
use_private_desktop,
|
|
};
|
|
let transport = spawn_runner_transport(
|
|
codex_home,
|
|
cwd,
|
|
&sandbox_creds,
|
|
logs_base_dir,
|
|
spawn_request,
|
|
)?;
|
|
let (pipe_write, mut pipe_read) = transport.into_files();
|
|
let cancel_writer = spawn_cancel_writer(&pipe_write, cancellation)?;
|
|
|
|
let mut stdout = Vec::new();
|
|
let mut stderr = Vec::new();
|
|
let result = loop {
|
|
let msg = match read_frame(&mut pipe_read) {
|
|
Ok(Some(msg)) => msg,
|
|
Ok(None) => break Err(anyhow::anyhow!("runner pipe closed before exit")),
|
|
Err(err) => break Err(err),
|
|
};
|
|
match msg.message {
|
|
Message::SpawnReady { .. } => {}
|
|
Message::Output { payload } => match decode_bytes(&payload.data_b64) {
|
|
Ok(bytes) => match payload.stream {
|
|
OutputStream::Stdout => stdout.extend_from_slice(&bytes),
|
|
OutputStream::Stderr => stderr.extend_from_slice(&bytes),
|
|
},
|
|
Err(err) => {
|
|
break Err(err);
|
|
}
|
|
},
|
|
Message::Exit { payload } => break Ok((payload.exit_code, payload.timed_out)),
|
|
Message::Error { payload } => {
|
|
break Err(anyhow::anyhow!("runner error: {}", payload.message));
|
|
}
|
|
other => {
|
|
break Err(anyhow::anyhow!(
|
|
"unexpected runner message during capture: {other:?}"
|
|
));
|
|
}
|
|
}
|
|
};
|
|
if let Some((cancel_handle, done)) = cancel_writer {
|
|
done.store(true, Ordering::SeqCst);
|
|
cancel_handle.thread().unpark();
|
|
let _ = cancel_handle.join();
|
|
}
|
|
drop(pipe_write);
|
|
let (exit_code, timed_out) = result?;
|
|
|
|
if exit_code == 0 {
|
|
log_success(&command, logs_base_dir);
|
|
} else {
|
|
log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir);
|
|
}
|
|
|
|
Ok(CaptureResult {
|
|
exit_code,
|
|
stdout,
|
|
stderr,
|
|
timed_out,
|
|
})
|
|
})()
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub use windows_impl::run_windows_sandbox_capture_for_permission_profile;
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
mod stub {
|
|
use super::ElevatedSandboxProfileCaptureRequest;
|
|
use anyhow::Result;
|
|
use anyhow::bail;
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct CaptureResult {
|
|
pub exit_code: i32,
|
|
pub stdout: Vec<u8>,
|
|
pub stderr: Vec<u8>,
|
|
pub timed_out: bool,
|
|
}
|
|
|
|
/// Stub implementation for non-Windows targets; sandboxing only works on Windows.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn run_windows_sandbox_capture_for_permission_profile(
|
|
_request: ElevatedSandboxProfileCaptureRequest<'_>,
|
|
) -> Result<CaptureResult> {
|
|
bail!("Windows sandbox is only available on Windows")
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub use stub::run_windows_sandbox_capture_for_permission_profile;
|