Compare commits

...

15 Commits

Author SHA1 Message Date
Jeremy Rose
e11a3823f6 Merge remote-tracking branch 'refs/remotes/origin/nornagon/sandbox-refactor' into nornagon/sandbox-refactor 2025-12-04 11:34:53 -08:00
Jeremy Rose
4f8689e44e Merge remote-tracking branch 'origin/main' into nornagon/sandbox-refactor 2025-12-04 10:28:56 -08:00
Jeremy Rose
662b422f4f Handle log_denials on non-mac builds 2025-12-03 14:47:28 -08:00
Jeremy Rose
04fd2f788f Merge remote-tracking branch 'origin/main' into nornagon/sandbox-refactor 2025-12-02 12:08:55 -08:00
Jeremy Rose
cce4b26169 fix 2025-11-21 15:09:48 -08:00
Jeremy Rose
a0dfec72f5 macos 2025-11-21 13:21:51 -08:00
Jeremy Rose
7e92099157 fix 2025-11-21 13:09:31 -08:00
Jeremy Rose
26feebe89d fix 2025-11-21 12:47:59 -08:00
Jeremy Rose
1e5f476e8a fix 2025-11-21 12:46:56 -08:00
Jeremy Rose
552568c40b dynamic cfg checks 2025-11-21 12:46:36 -08:00
Jeremy Rose
59d933589c clippy 2025-11-21 11:39:38 -08:00
Jeremy Rose
2123c851a2 fix 2025-11-21 11:30:50 -08:00
Jeremy Rose
fe4f419c1f Merge remote-tracking branch 'origin/main' into nornagon/sandbox-refactor 2025-11-21 10:52:28 -08:00
Jeremy Rose
e754c0326d Merge branch 'main' into nornagon/sandbox-refactor 2025-11-20 16:15:17 -08:00
Jeremy Rose
6c4fe1f1f3 refactor sandboxing (1/?) 2025-11-20 16:12:33 -08:00
23 changed files with 341 additions and 366 deletions

View File

@@ -1178,7 +1178,6 @@ impl CodexMessageProcessor {
.sandbox_policy
.map(|policy| policy.to_core())
.unwrap_or_else(|| self.config.sandbox_policy.clone());
let codex_linux_sandbox_exe = self.config.codex_linux_sandbox_exe.clone();
let outgoing = self.outgoing.clone();
let req_id = request_id;

View File

@@ -9,12 +9,14 @@ use codex_common::CliConfigOverrides;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::exec_env::create_env;
use codex_core::landlock::spawn_command_under_linux_sandbox;
#[cfg(target_os = "macos")]
use codex_core::seatbelt::spawn_command_under_seatbelt;
use codex_core::spawn::StdioPolicy;
use codex_protocol::config_types::SandboxMode;
#[cfg(target_os = "linux")]
use codex_core::landlock::spawn_command_under_linux_sandbox;
use crate::LandlockCommand;
use crate::SeatbeltCommand;
use crate::WindowsCommand;
@@ -39,7 +41,6 @@ pub async fn run_command_under_seatbelt(
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Seatbelt,
log_denials,
)
.await
@@ -53,6 +54,7 @@ pub async fn run_command_under_seatbelt(
anyhow::bail!("Seatbelt sandbox is only available on macOS");
}
#[cfg(target_os = "linux")]
pub async fn run_command_under_landlock(
command: LandlockCommand,
codex_linux_sandbox_exe: Option<PathBuf>,
@@ -67,12 +69,19 @@ pub async fn run_command_under_landlock(
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Landlock,
false,
)
.await
}
#[cfg(not(target_os = "linux"))]
pub async fn run_command_under_landlock(
_command: LandlockCommand,
_codex_linux_sandbox_exe: Option<PathBuf>,
) -> anyhow::Result<()> {
anyhow::bail!("Landlock sandbox is only available on Linux");
}
pub async fn run_command_under_windows(
command: WindowsCommand,
codex_linux_sandbox_exe: Option<PathBuf>,
@@ -87,27 +96,21 @@ pub async fn run_command_under_windows(
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Windows,
false,
)
.await
}
enum SandboxType {
#[cfg(target_os = "macos")]
Seatbelt,
Landlock,
Windows,
}
async fn run_command_under_sandbox(
full_auto: bool,
command: Vec<String>,
config_overrides: CliConfigOverrides,
codex_linux_sandbox_exe: Option<PathBuf>,
sandbox_type: SandboxType,
log_denials: bool,
) -> anyhow::Result<()> {
#[cfg(not(target_os = "macos"))]
let _ = log_denials;
let sandbox_mode = create_sandbox_mode(full_auto);
let config = Config::load_with_cli_overrides(
config_overrides
@@ -133,120 +136,106 @@ async fn run_command_under_sandbox(
let env = create_env(&config.shell_environment_policy);
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
if let SandboxType::Windows = sandbox_type {
#[cfg(target_os = "windows")]
{
use codex_windows_sandbox::run_windows_sandbox_capture;
#[cfg(target_os = "windows")]
{
use codex_windows_sandbox::run_windows_sandbox_capture;
let policy_str = serde_json::to_string(&config.sandbox_policy)?;
let policy_str = serde_json::to_string(&config.sandbox_policy)?;
let sandbox_cwd = sandbox_policy_cwd.clone();
let cwd_clone = cwd.clone();
let env_map = env.clone();
let command_vec = command.clone();
let base_dir = config.codex_home.clone();
let sandbox_cwd = sandbox_policy_cwd.clone();
let cwd_clone = cwd.clone();
let env_map = env.clone();
let command_vec = command.clone();
let base_dir = config.codex_home.clone();
// Preflight audit is invoked elsewhere at the appropriate times.
let res = tokio::task::spawn_blocking(move || {
run_windows_sandbox_capture(
policy_str.as_str(),
&sandbox_cwd,
base_dir.as_path(),
command_vec,
&cwd_clone,
env_map,
None,
)
})
.await;
// Preflight audit is invoked elsewhere at the appropriate times.
let res = tokio::task::spawn_blocking(move || {
run_windows_sandbox_capture(
policy_str.as_str(),
&sandbox_cwd,
base_dir.as_path(),
command_vec,
&cwd_clone,
env_map,
None,
)
})
.await;
let capture = match res {
Ok(Ok(v)) => v,
Ok(Err(err)) => {
eprintln!("windows sandbox failed: {err}");
std::process::exit(1);
}
Err(join_err) => {
eprintln!("windows sandbox join error: {join_err}");
std::process::exit(1);
}
};
if !capture.stdout.is_empty() {
use std::io::Write;
let _ = std::io::stdout().write_all(&capture.stdout);
let capture = match res {
Ok(Ok(v)) => v,
Ok(Err(err)) => {
eprintln!("windows sandbox failed: {err}");
std::process::exit(1);
}
if !capture.stderr.is_empty() {
use std::io::Write;
let _ = std::io::stderr().write_all(&capture.stderr);
Err(join_err) => {
eprintln!("windows sandbox join error: {join_err}");
std::process::exit(1);
}
};
std::process::exit(capture.exit_code);
if !capture.stdout.is_empty() {
use std::io::Write;
let _ = std::io::stdout().write_all(&capture.stdout);
}
#[cfg(not(target_os = "windows"))]
{
anyhow::bail!("Windows sandbox is only available on Windows");
if !capture.stderr.is_empty() {
use std::io::Write;
let _ = std::io::stderr().write_all(&capture.stderr);
}
std::process::exit(capture.exit_code);
}
#[cfg(target_os = "macos")]
let mut denial_logger = log_denials.then(DenialLogger::new).flatten();
#[cfg(not(target_os = "macos"))]
let _ = log_denials;
let status = {
let mut denial_logger = log_denials.then(DenialLogger::new).flatten();
let mut child = spawn_command_under_seatbelt(
command,
cwd,
&config.sandbox_policy,
sandbox_policy_cwd.as_path(),
stdio_policy,
env,
)
.await?;
if let Some(denial_logger) = &mut denial_logger {
denial_logger.on_child_spawn(&child);
}
let mut child = match sandbox_type {
#[cfg(target_os = "macos")]
SandboxType::Seatbelt => {
spawn_command_under_seatbelt(
command,
cwd,
&config.sandbox_policy,
sandbox_policy_cwd.as_path(),
stdio_policy,
env,
)
.await?
}
SandboxType::Landlock => {
#[expect(clippy::expect_used)]
let codex_linux_sandbox_exe = config
.codex_linux_sandbox_exe
.expect("codex-linux-sandbox executable not found");
spawn_command_under_linux_sandbox(
codex_linux_sandbox_exe,
command,
cwd,
&config.sandbox_policy,
sandbox_policy_cwd.as_path(),
stdio_policy,
env,
)
.await?
}
SandboxType::Windows => {
unreachable!("Windows sandbox should have been handled above");
let status = child.wait().await?;
if let Some(denial_logger) = denial_logger {
let denials = denial_logger.finish().await;
eprintln!("\n=== Sandbox denials ===");
if denials.is_empty() {
eprintln!("None found.");
} else {
for seatbelt::SandboxDenial { name, capability } in denials {
eprintln!("({name}) {capability}");
}
}
}
status
};
#[cfg(target_os = "linux")]
let status = {
#[expect(clippy::expect_used)]
let codex_linux_sandbox_exe = config
.codex_linux_sandbox_exe
.expect("codex-linux-sandbox executable not found");
let mut child = spawn_command_under_linux_sandbox(
codex_linux_sandbox_exe,
command,
cwd,
&config.sandbox_policy,
sandbox_policy_cwd.as_path(),
stdio_policy,
env,
)
.await?;
child.wait().await?
};
#[cfg(target_os = "macos")]
if let Some(denial_logger) = &mut denial_logger {
denial_logger.on_child_spawn(&child);
}
let status = child.wait().await?;
#[cfg(target_os = "macos")]
if let Some(denial_logger) = denial_logger {
let denials = denial_logger.finish().await;
eprintln!("\n=== Sandbox denials ===");
if denials.is_empty() {
eprintln!("None found.");
} else {
for seatbelt::SandboxDenial { name, capability } in denials {
eprintln!("({name}) {capability}");
}
}
}
handle_exit_status(status);
}

View File

@@ -210,10 +210,9 @@ pub struct Config {
/// output will be hyperlinked using the specified URI scheme.
pub file_opener: UriBasedFileOpener,
/// Path to the `codex-linux-sandbox` executable. This must be set if
/// [`crate::exec::SandboxType::LinuxSeccomp`] is used. Note that this
/// cannot be set in the config file: it must be set in code via
/// [`ConfigOverrides`].
/// Path to the `codex-linux-sandbox` executable. This must be set when the
/// Linux sandbox is used. Note that this cannot be set in the config file:
/// it must be set in code via [`ConfigOverrides`].
///
/// When this program is invoked, arg0 will be set to `codex-linux-sandbox`.
pub codex_linux_sandbox_exe: Option<PathBuf>,
@@ -834,7 +833,7 @@ impl ConfigToml {
if cfg!(target_os = "windows")
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
// If the experimental Windows sandbox is enabled, do not force a downgrade.
&& crate::safety::get_platform_sandbox().is_none()
&& !crate::safety::get_platform_has_sandbox()
{
sandbox_policy = SandboxPolicy::new_read_only_policy();
forced_auto_mode_downgraded_on_windows = true;

View File

@@ -19,7 +19,7 @@ use tokio_util::sync::CancellationToken;
use crate::error::CodexErr;
use crate::error::Result;
use crate::error::SandboxErr;
use crate::get_platform_sandbox;
use crate::get_platform_has_sandbox;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::ExecCommandOutputDeltaEvent;
@@ -105,20 +105,6 @@ impl ExecExpiration {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SandboxType {
None,
/// Only available on macOS.
MacosSeatbelt,
/// Only available on Linux.
LinuxSeccomp,
/// Only available on Windows.
WindowsRestrictedToken,
}
#[derive(Clone)]
pub struct StdoutStream {
pub sub_id: String,
@@ -133,11 +119,8 @@ pub async fn process_exec_tool_call(
codex_linux_sandbox_exe: &Option<PathBuf>,
stdout_stream: Option<StdoutStream>,
) -> Result<ExecToolCallOutput> {
let sandbox_type = match &sandbox_policy {
SandboxPolicy::DangerFullAccess => SandboxType::None,
_ => get_platform_sandbox().unwrap_or(SandboxType::None),
};
tracing::debug!("Sandbox type: {sandbox_type:?}");
let sandboxed =
!matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) && get_platform_has_sandbox();
let ExecParams {
command,
@@ -171,7 +154,7 @@ pub async fn process_exec_tool_call(
.transform(
spec,
sandbox_policy,
sandbox_type,
sandboxed,
sandbox_cwd,
codex_linux_sandbox_exe.as_ref(),
)
@@ -191,7 +174,7 @@ pub(crate) async fn execute_exec_env(
cwd,
env,
expiration,
sandbox,
sandboxed,
with_escalated_permissions,
justification,
arg0,
@@ -208,9 +191,9 @@ pub(crate) async fn execute_exec_env(
};
let start = Instant::now();
let raw_output_result = exec(params, sandbox, sandbox_policy, stdout_stream).await;
let raw_output_result = exec(params, sandboxed, sandbox_policy, stdout_stream).await;
let duration = start.elapsed();
finalize_exec_result(raw_output_result, sandbox, duration)
finalize_exec_result(raw_output_result, sandboxed, duration)
}
#[cfg(target_os = "windows")]
@@ -299,7 +282,7 @@ async fn exec_windows_sandbox(
fn finalize_exec_result(
raw_output_result: std::result::Result<RawExecToolCallOutput, CodexErr>,
sandbox_type: SandboxType,
sandboxed: bool,
duration: Duration,
) -> Result<ExecToolCallOutput> {
match raw_output_result {
@@ -341,7 +324,7 @@ fn finalize_exec_result(
}));
}
if is_likely_sandbox_denied(sandbox_type, &exec_output) {
if sandboxed && is_likely_sandbox_denied(&exec_output) {
return Err(CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(exec_output),
}));
@@ -366,10 +349,6 @@ pub(crate) mod errors {
SandboxTransformError::MissingLinuxSandboxExecutable => {
CodexErr::LandlockSandboxExecutableNotProvided
}
#[cfg(not(target_os = "macos"))]
SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation(
"seatbelt sandbox is only available on macOS".to_string(),
),
}
}
}
@@ -380,11 +359,8 @@ pub(crate) mod errors {
/// error, but the command itself might fail or succeed for other reasons.
/// For now, we conservatively check for well known command failure exit codes and
/// also look for common sandbox denial keywords in the command output.
pub(crate) fn is_likely_sandbox_denied(
sandbox_type: SandboxType,
exec_output: &ExecToolCallOutput,
) -> bool {
if sandbox_type == SandboxType::None || exec_output.exit_code == 0 {
pub(crate) fn is_likely_sandbox_denied(exec_output: &ExecToolCallOutput) -> bool {
if exec_output.exit_code == 0 {
return false;
}
@@ -424,12 +400,10 @@ pub(crate) fn is_likely_sandbox_denied(
return false;
}
#[cfg(unix)]
#[cfg(target_os = "linux")]
{
const SIGSYS_CODE: i32 = libc::SIGSYS;
if sandbox_type == SandboxType::LinuxSeccomp
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE
{
if exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE {
return true;
}
}
@@ -501,14 +475,12 @@ impl Default for ExecToolCallOutput {
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
async fn exec(
params: ExecParams,
sandbox: SandboxType,
sandboxed: bool,
sandbox_policy: &SandboxPolicy,
stdout_stream: Option<StdoutStream>,
) -> Result<RawExecToolCallOutput> {
#[cfg(target_os = "windows")]
if sandbox == SandboxType::WindowsRestrictedToken
&& !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
{
if sandboxed && !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
return exec_windows_sandbox(params, sandbox_policy).await;
}
let ExecParams {
@@ -785,31 +757,19 @@ mod tests {
#[test]
fn sandbox_detection_requires_keywords() {
let output = make_exec_output(1, "", "", "");
assert!(!is_likely_sandbox_denied(
SandboxType::LinuxSeccomp,
&output
));
assert!(!is_likely_sandbox_denied(&output));
}
#[test]
fn sandbox_detection_identifies_keyword_in_stderr() {
let output = make_exec_output(1, "", "Operation not permitted", "");
assert!(is_likely_sandbox_denied(SandboxType::LinuxSeccomp, &output));
assert!(is_likely_sandbox_denied(&output));
}
#[test]
fn sandbox_detection_respects_quick_reject_exit_codes() {
let output = make_exec_output(127, "", "command not found", "");
assert!(!is_likely_sandbox_denied(
SandboxType::LinuxSeccomp,
&output
));
}
#[test]
fn sandbox_detection_ignores_non_sandbox_mode() {
let output = make_exec_output(1, "", "Operation not permitted", "");
assert!(!is_likely_sandbox_denied(SandboxType::None, &output));
assert!(!is_likely_sandbox_denied(&output));
}
#[test]
@@ -820,18 +780,15 @@ mod tests {
"",
"cargo failed: Read-only file system when writing target",
);
assert!(is_likely_sandbox_denied(
SandboxType::MacosSeatbelt,
&output
));
assert!(is_likely_sandbox_denied(&output));
}
#[cfg(unix)]
#[cfg(target_os = "linux")]
#[test]
fn sandbox_detection_flags_sigsys_exit_code() {
let exit_code = EXIT_CODE_SIGNAL_BASE + libc::SIGSYS;
let output = make_exec_output(exit_code, "", "", "");
assert!(is_likely_sandbox_denied(SandboxType::LinuxSeccomp, &output));
assert!(is_likely_sandbox_denied(&output));
}
#[cfg(unix)]
@@ -862,7 +819,7 @@ mod tests {
arg0: None,
};
let output = exec(params, SandboxType::None, &SandboxPolicy::ReadOnly, None).await?;
let output = exec(params, false, &SandboxPolicy::ReadOnly, None).await?;
assert!(output.timed_out);
let stdout = output.stdout.from_utf8_lossy().text;

View File

@@ -29,7 +29,6 @@ mod exec_policy;
pub mod features;
mod flags;
pub mod git_info;
pub mod landlock;
pub mod mcp;
mod mcp_connection_manager;
pub mod openai_models;
@@ -72,7 +71,6 @@ mod openai_model_info;
pub mod project_doc;
mod rollout;
pub(crate) mod safety;
pub mod seatbelt;
pub mod shell;
pub mod skills;
pub mod spawn;
@@ -90,6 +88,10 @@ pub use rollout::list::ConversationsPage;
pub use rollout::list::Cursor;
pub use rollout::list::parse_cursor;
pub use rollout::list::read_head_for_summary;
#[cfg(target_os = "linux")]
pub use sandboxing::linux::landlock;
#[cfg(target_os = "macos")]
pub use sandboxing::mac::seatbelt;
mod function_tool;
mod state;
mod tasks;
@@ -99,7 +101,7 @@ pub mod util;
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
pub use command_safety::is_safe_command;
pub use safety::get_platform_sandbox;
pub use safety::get_platform_has_sandbox;
pub use safety::set_windows_sandbox_enabled;
// Re-export the protocol types from the standalone `codex-protocol` crate so existing
// `codex_core::protocol::...` references continue to work across the workspace.

View File

@@ -5,11 +5,9 @@ use std::path::PathBuf;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ApplyPatchFileChange;
use crate::exec::SandboxType;
use crate::util::resolve_path;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use crate::util::resolve_path;
#[cfg(target_os = "windows")]
use std::sync::atomic::AtomicBool;
@@ -31,7 +29,7 @@ pub fn set_windows_sandbox_enabled(_enabled: bool) {}
#[derive(Debug, PartialEq)]
pub enum SafetyCheck {
AutoApprove {
sandbox_type: SandboxType,
sandboxed: bool,
user_explicitly_approved: bool,
},
AskUser,
@@ -72,19 +70,19 @@ pub fn assess_patch_safety(
if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
// DangerFullAccess is intended to bypass sandboxing entirely.
SafetyCheck::AutoApprove {
sandbox_type: SandboxType::None,
sandboxed: false,
user_explicitly_approved: false,
}
} else {
// Only autoapprove when we can actually enforce a sandbox. Otherwise
// fall back to asking the user because the patch may touch arbitrary
// paths outside the project.
match get_platform_sandbox() {
Some(sandbox_type) => SafetyCheck::AutoApprove {
sandbox_type,
match get_platform_has_sandbox() {
true => SafetyCheck::AutoApprove {
sandboxed: true,
user_explicitly_approved: false,
},
None => SafetyCheck::AskUser,
false => SafetyCheck::AskUser,
}
}
} else if policy == AskForApproval::Never {
@@ -97,22 +95,24 @@ pub fn assess_patch_safety(
}
}
pub fn get_platform_sandbox() -> Option<SandboxType> {
if cfg!(target_os = "macos") {
Some(SandboxType::MacosSeatbelt)
} else if cfg!(target_os = "linux") {
Some(SandboxType::LinuxSeccomp)
} else if cfg!(target_os = "windows") {
#[cfg(target_os = "windows")]
{
if WINDOWS_SANDBOX_ENABLED.load(Ordering::Relaxed) {
return Some(SandboxType::WindowsRestrictedToken);
}
}
None
} else {
None
}
#[cfg(target_os = "macos")]
pub fn get_platform_has_sandbox() -> bool {
true
}
#[cfg(target_os = "linux")]
pub fn get_platform_has_sandbox() -> bool {
true
}
#[cfg(target_os = "windows")]
pub fn get_platform_has_sandbox() -> bool {
WINDOWS_SANDBOX_ENABLED.load(Ordering::Relaxed)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
pub fn get_platform_has_sandbox() -> bool {
false
}
fn is_write_patch_constrained_to_writable_paths(

View File

@@ -0,0 +1 @@
pub mod landlock;

View File

@@ -0,0 +1,4 @@
pub mod seatbelt;
#[cfg(target_os = "macos")]
pub mod sys;

View File

@@ -1,18 +1,17 @@
#![cfg(target_os = "macos")]
use std::collections::HashMap;
use std::ffi::CStr;
use std::path::Path;
use std::path::PathBuf;
use tokio::process::Child;
use crate::protocol::SandboxPolicy;
#[cfg(target_os = "macos")]
use crate::sandboxing::mac::sys;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("../../seatbelt_base_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("../../seatbelt_network_policy.sbpl");
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
/// to defend against an attacker trying to inject a malicious version on the
@@ -104,18 +103,26 @@ pub(crate) fn create_seatbelt_command_args(
""
};
// TODO(mbolin): apply_patch calls must also honor the SandboxPolicy.
let network_policy = if sandbox_policy.has_full_network_access() {
MACOS_SEATBELT_NETWORK_POLICY
} else {
""
};
let (user_cache_dir_policy, user_cache_dir_params) = user_cache_dir()
.map(|p| {
(
"(allow file-write* (subpath (param \"DARWIN_USER_CACHE_DIR\")))",
vec![("DARWIN_USER_CACHE_DIR".to_string(), p)],
)
})
.unwrap_or_default();
let full_policy = format!(
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}"
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}\n{user_cache_dir_policy}"
);
let dir_params = [file_write_dir_params, macos_dir_params()].concat();
let dir_params = [file_write_dir_params, user_cache_dir_params].concat();
let mut seatbelt_args: Vec<String> = vec!["-p".to_string(), full_policy];
let definition_args = dir_params
@@ -127,38 +134,22 @@ pub(crate) fn create_seatbelt_command_args(
seatbelt_args
}
/// Wraps libc::confstr to return a String.
fn confstr(name: libc::c_int) -> Option<String> {
let mut buf = vec![0_i8; (libc::PATH_MAX as usize) + 1];
let len = unsafe { libc::confstr(name, buf.as_mut_ptr(), buf.len()) };
if len == 0 {
return None;
}
// confstr guarantees NUL-termination when len > 0.
let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
cstr.to_str().ok().map(ToString::to_string)
#[cfg(target_os = "macos")]
fn user_cache_dir() -> Option<PathBuf> {
sys::user_cache_dir()
}
/// Wraps confstr to return a canonicalized PathBuf.
fn confstr_path(name: libc::c_int) -> Option<PathBuf> {
let s = confstr(name)?;
let path = PathBuf::from(s);
path.canonicalize().ok().or(Some(path))
}
fn macos_dir_params() -> Vec<(String, PathBuf)> {
if let Some(p) = confstr_path(libc::_CS_DARWIN_USER_CACHE_DIR) {
return vec![("DARWIN_USER_CACHE_DIR".to_string(), p)];
}
vec![]
#[cfg(not(target_os = "macos"))]
fn user_cache_dir() -> Option<PathBuf> {
None
}
#[cfg(test)]
mod tests {
use super::MACOS_SEATBELT_BASE_POLICY;
use super::create_seatbelt_command_args;
use super::macos_dir_params;
use crate::protocol::SandboxPolicy;
use crate::seatbelt::user_cache_dir;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
@@ -194,12 +185,13 @@ mod tests {
&cwd,
);
let user_cache_dir = user_cache_dir();
// Build the expected policy text using a raw string for readability.
// Note that the policy includes:
// - the base policy,
// - read-only access to the filesystem,
// - write access to WRITABLE_ROOT_0 (but not its .git) and WRITABLE_ROOT_1.
let expected_policy = format!(
let mut expected_policy = format!(
r#"{MACOS_SEATBELT_BASE_POLICY}
; allow read-only file operations
(allow file-read*)
@@ -208,6 +200,10 @@ mod tests {
)
"#,
);
if user_cache_dir.is_some() {
expected_policy
.push_str("\n(allow file-write* (subpath (param \"DARWIN_USER_CACHE_DIR\")))");
}
let mut expected_args = vec![
"-p".to_string(),
@@ -227,11 +223,9 @@ mod tests {
format!("-DWRITABLE_ROOT_2={}", cwd.to_string_lossy()),
];
expected_args.extend(
macos_dir_params()
.into_iter()
.map(|(key, value)| format!("-D{key}={value}", value = value.to_string_lossy())),
);
if let Some(p) = &user_cache_dir {
expected_args.push(format!("-DDARWIN_USER_CACHE_DIR={}", p.to_string_lossy()));
}
expected_args.extend(vec![
"--".to_string(),
@@ -275,6 +269,7 @@ mod tests {
.map(PathBuf::from)
.and_then(|p| p.canonicalize().ok())
.map(|p| p.to_string_lossy().to_string());
let user_cache_dir = user_cache_dir();
let tempdir_policy_entry = if tmpdir_env_var.is_some() {
r#" (subpath (param "WRITABLE_ROOT_2"))"#
@@ -287,7 +282,7 @@ mod tests {
// - the base policy,
// - read-only access to the filesystem,
// - write access to WRITABLE_ROOT_0 (but not its .git) and WRITABLE_ROOT_1.
let expected_policy = format!(
let mut expected_policy = format!(
r#"{MACOS_SEATBELT_BASE_POLICY}
; allow read-only file operations
(allow file-read*)
@@ -296,6 +291,10 @@ mod tests {
)
"#,
);
if user_cache_dir.is_some() {
expected_policy
.push_str("\n(allow file-write* (subpath (param \"DARWIN_USER_CACHE_DIR\")))");
}
let mut expected_args = vec![
"-p".to_string(),
@@ -321,11 +320,9 @@ mod tests {
expected_args.push(format!("-DWRITABLE_ROOT_2={p}"));
}
expected_args.extend(
macos_dir_params()
.into_iter()
.map(|(key, value)| format!("-D{key}={value}", value = value.to_string_lossy())),
);
if let Some(p) = &user_cache_dir {
expected_args.push(format!("-DDARWIN_USER_CACHE_DIR={}", p.to_string_lossy()));
}
expected_args.extend(vec![
"--".to_string(),

View File

@@ -0,0 +1,28 @@
use std::ffi::CStr;
use std::path::PathBuf;
use std::string::ToString;
use libc;
/// Wraps libc::confstr to return a String.
fn confstr(name: libc::c_int) -> Option<String> {
let mut buf = vec![0_i8; (libc::PATH_MAX as usize) + 1];
let len = unsafe { libc::confstr(name, buf.as_mut_ptr(), buf.len()) };
if len == 0 {
return None;
}
// confstr guarantees NUL-termination when len > 0.
let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
cstr.to_str().ok().map(ToString::to_string)
}
/// Wraps confstr to return a canonicalized PathBuf.
fn confstr_path(name: libc::c_int) -> Option<PathBuf> {
let s = confstr(name)?;
let path = PathBuf::from(s);
path.canonicalize().ok().or(Some(path))
}
pub fn user_cache_dir() -> Option<PathBuf> {
confstr_path(libc::_CS_DARWIN_USER_CACHE_DIR)
}

View File

@@ -1,32 +1,34 @@
/*
Module: sandboxing
Build platform wrappers and produce ExecEnv for execution. Owns lowlevel
sandbox placement and transformation of portable CommandSpec into a
readytospawn environment.
*/
//! # Sandboxing
//!
//! This module provides platform wrappers and constructs `ExecEnv` objects for
//! command execution. It owns low-level sandbox placement logic and transforms
//! portable `CommandSpec` structs into ready-to-spawn execution environments.
pub mod assessment;
pub mod linux;
pub mod mac;
use crate::exec::ExecExpiration;
use crate::exec::ExecToolCallOutput;
use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec::execute_exec_env;
use crate::landlock::create_linux_sandbox_command_args;
use crate::protocol::SandboxPolicy;
#[cfg(target_os = "macos")]
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
#[cfg(target_os = "macos")]
use crate::seatbelt::create_seatbelt_command_args;
#[cfg(target_os = "macos")]
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use crate::tools::sandboxing::SandboxablePreference;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use mac::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use mac::seatbelt::create_seatbelt_command_args;
use linux::landlock::create_linux_sandbox_command_args;
type TransformResult =
Result<(Vec<String>, HashMap<String, String>, Option<String>), SandboxTransformError>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SandboxPermissions {
UseDefault,
@@ -66,7 +68,7 @@ pub struct ExecEnv {
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub expiration: ExecExpiration,
pub sandbox: SandboxType,
pub sandboxed: bool,
pub with_escalated_permissions: Option<bool>,
pub justification: Option<String>,
pub arg0: Option<String>,
@@ -82,9 +84,6 @@ pub enum SandboxPreference {
pub(crate) enum SandboxTransformError {
#[error("missing codex-linux-sandbox executable path")]
MissingLinuxSandboxExecutable,
#[cfg(not(target_os = "macos"))]
#[error("seatbelt sandbox is only available on macOS")]
SeatbeltUnavailable,
}
#[derive(Default)]
@@ -99,17 +98,17 @@ impl SandboxManager {
&self,
policy: &SandboxPolicy,
pref: SandboxablePreference,
) -> SandboxType {
) -> bool {
match pref {
SandboxablePreference::Forbid => SandboxType::None,
SandboxablePreference::Forbid => false,
SandboxablePreference::Require => {
// Require a platform sandbox when available; on Windows this
// respects the enable_experimental_windows_sandbox feature.
crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None)
crate::safety::get_platform_has_sandbox()
}
SandboxablePreference::Auto => match policy {
SandboxPolicy::DangerFullAccess => SandboxType::None,
_ => crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None),
SandboxPolicy::DangerFullAccess => false,
_ => crate::safety::get_platform_has_sandbox(),
},
}
}
@@ -118,7 +117,7 @@ impl SandboxManager {
&self,
mut spec: CommandSpec,
policy: &SandboxPolicy,
sandbox: SandboxType,
sandboxed: bool,
sandbox_policy_cwd: &Path,
codex_linux_sandbox_exe: Option<&PathBuf>,
) -> Result<ExecEnv, SandboxTransformError> {
@@ -134,43 +133,25 @@ impl SandboxManager {
command.push(spec.program);
command.append(&mut spec.args);
let (command, sandbox_env, arg0_override) = match sandbox {
SandboxType::None => (command, HashMap::new(), None),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => {
let mut seatbelt_env = HashMap::new();
seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
let mut args =
create_seatbelt_command_args(command.clone(), policy, sandbox_policy_cwd);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
full_command.append(&mut args);
(full_command, seatbelt_env, None)
}
#[cfg(not(target_os = "macos"))]
SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
SandboxType::LinuxSeccomp => {
let exe = codex_linux_sandbox_exe
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
let mut args =
create_linux_sandbox_command_args(command.clone(), policy, sandbox_policy_cwd);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(exe.to_string_lossy().to_string());
full_command.append(&mut args);
(
full_command,
HashMap::new(),
Some("codex-linux-sandbox".to_string()),
)
}
// On Windows, the restricted token sandbox executes in-process via the
// codex-windows-sandbox crate. We leave the command unchanged here and
// branch during execution based on the sandbox type.
#[cfg(target_os = "windows")]
SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None),
// When building for non-Windows targets, this variant is never constructed.
#[cfg(not(target_os = "windows"))]
SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None),
if !sandboxed {
return Ok(ExecEnv {
command,
cwd: spec.cwd.clone(),
env,
expiration: spec.expiration,
sandboxed,
with_escalated_permissions: spec.with_escalated_permissions,
justification: spec.justification.clone(),
arg0: None,
});
}
let (command, sandbox_env, arg0_override) = if cfg!(target_os = "macos") {
transform_macos(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)?
} else if cfg!(target_os = "linux") {
transform_linux(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)?
} else {
(command, HashMap::new(), None)
};
env.extend(sandbox_env);
@@ -180,16 +161,46 @@ impl SandboxManager {
cwd: spec.cwd,
env,
expiration: spec.expiration,
sandbox,
sandboxed,
with_escalated_permissions: spec.with_escalated_permissions,
justification: spec.justification,
arg0: arg0_override,
})
}
}
pub fn denied(&self, sandbox: SandboxType, out: &ExecToolCallOutput) -> bool {
crate::exec::is_likely_sandbox_denied(sandbox, out)
}
fn transform_macos(
command: Vec<String>,
policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
_codex_linux_sandbox_exe: Option<&PathBuf>,
) -> TransformResult {
let mut seatbelt_env = HashMap::new();
seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
let mut args = create_seatbelt_command_args(command, policy, sandbox_policy_cwd);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
full_command.append(&mut args);
Ok((full_command, seatbelt_env, None))
}
fn transform_linux(
command: Vec<String>,
policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
codex_linux_sandbox_exe: Option<&PathBuf>,
) -> TransformResult {
let exe =
codex_linux_sandbox_exe.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
let mut args = create_linux_sandbox_command_args(command, policy, sandbox_policy_cwd);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(exe.to_string_lossy().to_string());
full_command.append(&mut args);
Ok((
full_command,
HashMap::new(),
Some("codex-linux-sandbox".to_string()),
))
}
pub async fn execute_env(

View File

@@ -24,7 +24,3 @@
(allow sysctl-read
(sysctl-name-regex #"^net.routetable")
)
(allow file-write*
(subpath (param "DARWIN_USER_CACHE_DIR"))
)

View File

@@ -11,7 +11,6 @@ use uuid::Uuid;
use crate::codex::TurnContext;
use crate::exec::ExecToolCallOutput;
use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec::StreamOutput;
use crate::exec::execute_exec_env;
@@ -99,7 +98,7 @@ impl SessionTask for UserShellCommandTask {
// TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we
// should use that instead of an "arbitrarily large" timeout here.
expiration: USER_SHELL_TIMEOUT_MS.into(),
sandbox: SandboxType::None,
sandboxed: false,
with_escalated_permissions: None,
justification: None,
arg0: None,

View File

@@ -102,16 +102,15 @@ impl ToolOrchestrator {
// 2) First attempt under the selected sandbox.
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None,
SandboxOverride::BypassSandboxFirstAttempt => false,
SandboxOverride::NoOverride => self
.sandbox
.select_initial(&turn_ctx.sandbox_policy, tool.sandbox_preference()),
};
// Platform-specific flag gating is handled by SandboxManager::select_initial
// via crate::safety::get_platform_sandbox().
// via crate::safety::get_platform_has_sandbox().
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
sandboxed: initial_sandbox,
policy: &turn_ctx.sandbox_policy,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
@@ -180,7 +179,7 @@ impl ToolOrchestrator {
}
let escalated_attempt = SandboxAttempt {
sandbox: crate::exec::SandboxType::None,
sandboxed: false,
policy: &turn_ctx.sandbox_policy,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,

View File

@@ -218,7 +218,7 @@ pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable {
}
pub(crate) struct SandboxAttempt<'a> {
pub sandbox: crate::exec::SandboxType,
pub sandboxed: bool,
pub policy: &'a crate::protocol::SandboxPolicy,
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a Path,
@@ -233,7 +233,7 @@ impl<'a> SandboxAttempt<'a> {
self.manager.transform(
spec,
self.policy,
self.sandbox,
self.sandboxed,
self.sandbox_cwd,
self.codex_linux_sandbox_exe,
)

View File

@@ -13,7 +13,7 @@
//! 1) Build a small request `{ command, cwd }`.
//! 2) Orchestrator: approval (bypass/cache/prompt) → select sandbox → run.
//! 3) Runtime: transform `CommandSpec` → `ExecEnv` → spawn PTY.
//! 4) If denial, orchestrator retries with `SandboxType::None`.
//! 4) If denial, orchestrator retries without sandboxing.
//! 5) Session is returned with streaming output + metadata.
//!
//! This keeps policy logic and user interaction centralized while the PTY/session

View File

@@ -11,7 +11,6 @@ use tokio::time::Duration;
use tokio_util::sync::CancellationToken;
use crate::exec::ExecToolCallOutput;
use crate::exec::SandboxType;
use crate::exec::StreamOutput;
use crate::exec::is_likely_sandbox_denied;
use crate::truncate::TruncationPolicy;
@@ -80,14 +79,14 @@ pub(crate) struct UnifiedExecSession {
output_notify: Arc<Notify>,
cancellation_token: CancellationToken,
output_task: JoinHandle<()>,
sandbox_type: SandboxType,
sandboxed: bool,
}
impl UnifiedExecSession {
pub(super) fn new(
session: ExecCommandSession,
initial_output_rx: tokio::sync::broadcast::Receiver<Vec<u8>>,
sandbox_type: SandboxType,
sandboxed: bool,
) -> Self {
let output_buffer = Arc::new(Mutex::new(OutputBufferState::default()));
let output_notify = Arc::new(Notify::new());
@@ -120,7 +119,7 @@ impl UnifiedExecSession {
output_notify,
cancellation_token,
output_task,
sandbox_type,
sandboxed,
}
}
@@ -149,12 +148,12 @@ impl UnifiedExecSession {
guard.snapshot()
}
pub(crate) fn sandbox_type(&self) -> SandboxType {
self.sandbox_type
pub(crate) fn sandboxed(&self) -> bool {
self.sandboxed
}
pub(super) async fn check_for_sandbox_denial(&self) -> Result<(), UnifiedExecError> {
if self.sandbox_type() == SandboxType::None || !self.has_exited() {
if !self.sandboxed() || !self.has_exited() {
return Ok(());
}
@@ -176,7 +175,7 @@ impl UnifiedExecSession {
..Default::default()
};
if is_likely_sandbox_denied(self.sandbox_type(), &exec_output) {
if is_likely_sandbox_denied(&exec_output) {
let snippet = formatted_truncate_text(
&aggregated_text,
TruncationPolicy::Tokens(UNIFIED_EXEC_OUTPUT_MAX_TOKENS),
@@ -194,14 +193,14 @@ impl UnifiedExecSession {
pub(super) async fn from_spawned(
spawned: SpawnedPty,
sandbox_type: SandboxType,
sandboxed: bool,
) -> Result<Self, UnifiedExecError> {
let SpawnedPty {
session,
output_rx,
mut exit_rx,
} = spawned;
let managed = Self::new(session, output_rx, sandbox_type);
let managed = Self::new(session, output_rx, sandboxed);
let exit_ready = matches!(exit_rx.try_recv(), Ok(_) | Err(TryRecvError::Closed));

View File

@@ -153,7 +153,7 @@ impl UnifiedExecSessionManager {
let output = formatted_truncate_text(&text, TruncationPolicy::Tokens(max_tokens));
let has_exited = session.has_exited();
let exit_code = session.exit_code();
let sandbox_type = session.sandbox_type();
let sandboxed = session.sandboxed();
let chunk_id = generate_chunk_id();
let process_id = if has_exited {
None
@@ -204,7 +204,7 @@ impl UnifiedExecSessionManager {
.await;
// Exit code should always be Some
sandboxing::check_sandboxing(sandbox_type, &text, exit_code.unwrap_or_default())?;
sandboxing::check_sandboxing(sandboxed, &text, exit_code.unwrap_or_default())?;
}
Ok(response)
@@ -544,7 +544,7 @@ impl UnifiedExecSessionManager {
)
.await
.map_err(|err| UnifiedExecError::create_session(err.to_string()))?;
UnifiedExecSession::from_spawned(spawned, env.sandbox).await
UnifiedExecSession::from_spawned(spawned, env.sandboxed).await
}
pub(super) async fn open_session_with_sandbox(
@@ -709,22 +709,24 @@ impl UnifiedExecSessionManager {
mod sandboxing {
use super::*;
use crate::exec::SandboxType;
use crate::exec::is_likely_sandbox_denied;
use crate::unified_exec::UNIFIED_EXEC_OUTPUT_MAX_TOKENS;
pub(crate) fn check_sandboxing(
sandbox_type: SandboxType,
sandboxed: bool,
text: &str,
exit_code: i32,
) -> Result<(), UnifiedExecError> {
if !sandboxed {
return Ok(());
}
let exec_output = ExecToolCallOutput {
exit_code,
stderr: StreamOutput::new(text.to_string()),
aggregated_output: StreamOutput::new(text.to_string()),
..Default::default()
};
if is_likely_sandbox_denied(sandbox_type, &exec_output) {
if is_likely_sandbox_denied(&exec_output) {
let snippet = formatted_truncate_text(
text,
TruncationPolicy::Tokens(UNIFIED_EXEC_OUTPUT_MAX_TOKENS),

View File

@@ -5,7 +5,6 @@ use std::string::ToString;
use codex_core::exec::ExecParams;
use codex_core::exec::ExecToolCallOutput;
use codex_core::exec::SandboxType;
use codex_core::exec::process_exec_tool_call;
use codex_core::protocol::SandboxPolicy;
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
@@ -13,8 +12,6 @@ use tempfile::TempDir;
use codex_core::error::Result;
use codex_core::get_platform_sandbox;
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.");
@@ -24,11 +21,7 @@ fn skip_test() -> bool {
false
}
#[expect(clippy::expect_used)]
async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput> {
let sandbox_type = get_platform_sandbox().expect("should be able to get sandbox type");
assert_eq!(sandbox_type, SandboxType::MacosSeatbelt);
let params = ExecParams {
command: cmd.iter().map(ToString::to_string).collect(),
cwd: tmp.path().to_path_buf(),

View File

@@ -373,7 +373,7 @@ impl App {
// On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
#[cfg(target_os = "windows")]
{
let should_check = codex_core::get_platform_sandbox().is_some()
let should_check = codex_core::get_platform_has_sandbox()
&& matches!(
app.config.sandbox_policy,
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
@@ -818,7 +818,7 @@ impl App {
self.config.sandbox_policy = policy.clone();
#[cfg(target_os = "windows")]
if !matches!(policy, codex_core::protocol::SandboxPolicy::ReadOnly)
|| codex_core::get_platform_sandbox().is_some()
|| codex_core::get_platform_has_sandbox()
{
self.config.forced_auto_mode_downgraded_on_windows = false;
}
@@ -833,7 +833,7 @@ impl App {
return Ok(true);
}
let should_check = codex_core::get_platform_sandbox().is_some()
let should_check = codex_core::get_platform_has_sandbox()
&& policy_is_workspace_write_or_ro
&& !self.chat_widget.world_writable_warning_hidden();
if should_check {

View File

@@ -2396,7 +2396,7 @@ impl ChatWidget {
} else if preset.id == "auto" {
#[cfg(target_os = "windows")]
{
if codex_core::get_platform_sandbox().is_none() {
if !codex_core::get_platform_has_sandbox() {
let preset_clone = preset.clone();
vec![Box::new(move |tx| {
tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {
@@ -2745,7 +2745,7 @@ impl ChatWidget {
#[cfg(target_os = "windows")]
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) {
if self.config.forced_auto_mode_downgraded_on_windows
&& codex_core::get_platform_sandbox().is_none()
&& !codex_core::get_platform_has_sandbox()
&& let Some(preset) = builtin_approval_presets()
.into_iter()
.find(|preset| preset.id == "auto")
@@ -2774,8 +2774,8 @@ impl ChatWidget {
/// Set the sandbox policy in the widget's config copy.
pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) {
#[cfg(target_os = "windows")]
let should_clear_downgrade = !matches!(policy, SandboxPolicy::ReadOnly)
|| codex_core::get_platform_sandbox().is_some();
let should_clear_downgrade =
!matches!(policy, SandboxPolicy::ReadOnly) || codex_core::get_platform_has_sandbox();
self.config.sandbox_policy = policy;

View File

@@ -20,7 +20,7 @@ use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::config::resolve_oss_provider;
use codex_core::find_conversation_path_by_id_str;
use codex_core::get_platform_sandbox;
use codex_core::get_platform_has_sandbox;
use codex_core::protocol::AskForApproval;
use codex_protocol::config_types::SandboxMode;
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
@@ -571,7 +571,7 @@ async fn load_config_or_exit(
/// or if the current cwd project is already trusted. If not, we need to
/// show the trust screen.
fn should_show_trust_screen(config: &Config) -> bool {
if cfg!(target_os = "windows") && get_platform_sandbox().is_none() {
if cfg!(target_os = "windows") && !get_platform_has_sandbox() {
// If the experimental sandbox is not enabled, Native Windows cannot enforce sandboxed write access; skip the trust prompt entirely.
return false;
}