Separate executor sandbox selection from Windows sandbox levels (#45730)

## Why

MXC is a sandbox implementation, not a restricted-token sandbox level. Executor requests need to represent that choice separately from `WindowsSandboxLevel`.

## What changed

- Introduce `WindowsSandboxSelection` for executor sandbox contexts and remove `Mxc` from `WindowsSandboxLevel`.
- Preserve the `windowsSandboxLevel` wire field and its serialized values for compatibility.
- Share sandbox selection between executor process launches and filesystem helpers, and use the new selection in capability discovery and skill reads.
- Disable Windows sandbox selection for executor paths that do not use Windows path conventions.

## Testing

Extend coverage for MXC wire serialization, Windows skill-read sandbox checks, and capability discovery with distinct permissions. Exercise remote filesystem write restrictions with both restricted-token and MXC sandboxes, including rejection when native MXC is unavailable.

GitOrigin-RevId: 266211377bcb138a0dc75861e9ff2225fa37a53d
This commit is contained in:
iceweasel-oai
2026-09-15 16:40:06 +00:00
committed by copyberry
parent a9d2564bcb
commit d4e11a9b97
32 changed files with 256 additions and 150 deletions
@@ -4931,7 +4931,7 @@ impl ThreadRequestProcessor {
serde_json::json!("unelevated"),
);
}
WindowsSandboxLevel::Disabled | WindowsSandboxLevel::Mxc => {}
WindowsSandboxLevel::Disabled => {}
}
}
let request_overrides = if cli_overrides.is_empty() {
@@ -303,9 +303,7 @@ fn determine_windows_sandbox_readiness_from_state(
) -> WindowsSandboxReadinessResponse {
let status = match windows_sandbox_level {
WindowsSandboxLevel::Disabled => WindowsSandboxReadiness::NotConfigured,
WindowsSandboxLevel::RestrictedToken | WindowsSandboxLevel::Mxc => {
WindowsSandboxReadiness::Ready
}
WindowsSandboxLevel::RestrictedToken => WindowsSandboxReadiness::Ready,
WindowsSandboxLevel::Elevated => {
if sandbox_setup_is_complete {
WindowsSandboxReadiness::Ready
@@ -25,7 +25,7 @@ pub fn prepare_windows_sandbox_config(
let selected_mode = configured_mode.or(match feature_level {
WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated),
WindowsSandboxLevel::RestrictedToken => Some(WindowsSandboxModeToml::Unelevated),
WindowsSandboxLevel::Disabled | WindowsSandboxLevel::Mxc => None,
WindowsSandboxLevel::Disabled => None,
});
apply_requirement_constrained_value("windows.sandbox", selected_mode, constraint, warnings)?;
let effective_mode = *constraint.get();
-1
View File
@@ -79,7 +79,6 @@ fn permission_profile_sandbox_tag(
}
if cfg!(target_os = "windows") {
match windows_sandbox_level {
WindowsSandboxLevel::Mxc => return "windows_mxc",
WindowsSandboxLevel::Elevated => return "windows_elevated",
WindowsSandboxLevel::Disabled | WindowsSandboxLevel::RestrictedToken => {}
}
+5 -1
View File
@@ -8970,7 +8970,11 @@ async fn capability_discovery_uses_environment_permission_profile() {
workspace_roots: environment.workspace_roots().to_vec(),
user_home_dir: environment.user_home_dir.clone(),
temporary_directories: environment.temporary_directories.clone(),
windows_sandbox_level: WindowsSandboxLevel::Elevated,
windows_sandbox_selection: if cfg!(windows) {
codex_file_system::WindowsSandboxSelection::Elevated
} else {
codex_file_system::WindowsSandboxSelection::Disabled
},
windows_sandbox_private_desktop: false,
windows_sandbox_proxy_settings_mode: None,
use_legacy_landlock: true,
+2 -2
View File
@@ -9,7 +9,7 @@ use crate::exec_policy::AllowPrefixRules;
use crate::shell_snapshot::ShellSnapshot;
use crate::shell_snapshot::ShellSnapshotFile;
use crate::shell_snapshot::ShellSnapshotSandbox;
use crate::tools::sandboxing::executor_windows_sandbox_level;
use crate::tools::sandboxing::executor_windows_sandbox_selection;
use arc_swap::ArcSwap;
use codex_core_plugins::PluginCommandAttribution;
use codex_core_plugins::ResolvedPluginMetricsOperation;
@@ -227,7 +227,7 @@ impl TurnEnvironment {
workspace_roots: self.workspace_roots().to_vec(),
user_home_dir: self.user_home_dir.clone(),
temporary_directories: self.temporary_directories.clone(),
windows_sandbox_level: executor_windows_sandbox_level(
windows_sandbox_selection: executor_windows_sandbox_selection(
config.windows_sandbox_level,
self.cwd(),
),
@@ -13,7 +13,7 @@ use crate::tools::sandboxing::Sandboxable;
use crate::tools::sandboxing::ToolCtx;
use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::executor_windows_sandbox_level;
use crate::tools::sandboxing::executor_windows_sandbox_selection;
use codex_apply_patch::AppliedPatchDelta;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ApplyPatchOptions;
@@ -102,7 +102,7 @@ impl ApplyPatchRuntime {
workspace_roots: attempt.workspace_roots.to_vec(),
user_home_dir: req.turn_environment.user_home_dir.clone(),
temporary_directories: None,
windows_sandbox_level: executor_windows_sandbox_level(
windows_sandbox_selection: executor_windows_sandbox_selection(
attempt.windows_sandbox_level,
attempt.sandbox_cwd,
),
@@ -276,8 +276,12 @@ async fn file_system_sandbox_context_preserves_executor_workspace_permissions()
Some(codex_utils_path_uri::PathUri::from_abs_path(&path))
);
assert_eq!(
sandbox.windows_sandbox_level,
WindowsSandboxLevel::RestrictedToken
sandbox.windows_sandbox_selection,
if cfg!(windows) {
codex_file_system::WindowsSandboxSelection::RestrictedToken
} else {
codex_file_system::WindowsSandboxSelection::Disabled
}
);
assert_eq!(sandbox.windows_sandbox_private_desktop, true);
assert_eq!(sandbox.use_legacy_landlock, true);
@@ -349,7 +353,7 @@ async fn file_system_sandbox_context_respects_sandbox_request() {
workspace_roots: vec![cwd],
user_home_dir: Some(user_home_dir),
temporary_directories: None,
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
windows_sandbox_selection: codex_file_system::WindowsSandboxSelection::RestrictedToken,
windows_sandbox_private_desktop: false,
windows_sandbox_proxy_settings_mode: None,
use_legacy_landlock: false,
+13 -1
View File
@@ -13,6 +13,7 @@ use crate::state::SessionServices;
use crate::tools::hook_names::HookToolName;
use crate::tools::network_approval::NetworkApprovalSpec;
use codex_file_system::FileSystemSandboxContext;
use codex_file_system::WindowsSandboxSelection;
use codex_network_proxy::NetworkProxy;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::config_types::WindowsSandboxLevel;
@@ -417,6 +418,17 @@ pub(crate) fn executor_windows_sandbox_level(
}
}
pub(crate) fn executor_windows_sandbox_selection(
windows_sandbox_level: WindowsSandboxLevel,
cwd: &PathUri,
) -> WindowsSandboxSelection {
if cwd.infer_path_convention() == Some(PathConvention::Windows) {
executor_windows_sandbox_level(windows_sandbox_level, cwd).into()
} else {
WindowsSandboxSelection::Disabled
}
}
impl<'a> SandboxAttempt<'a> {
/// Whether this attempt bypasses sandboxing required by its ambient policy.
/// Use the requested policy, not the controller's wrapper, for remote executors.
@@ -513,7 +525,7 @@ impl<'a> SandboxAttempt<'a> {
workspace_roots: self.workspace_roots.to_vec(),
user_home_dir: None,
temporary_directories: None,
windows_sandbox_level: executor_windows_sandbox_level(
windows_sandbox_selection: executor_windows_sandbox_selection(
self.windows_sandbox_level,
self.sandbox_cwd,
),
+3 -3
View File
@@ -373,10 +373,10 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
workspace_roots: vec![cwd_uri.clone()],
user_home_dir: None,
temporary_directories: None,
windows_sandbox_level: if cfg!(windows) {
codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken
windows_sandbox_selection: if cfg!(windows) {
codex_file_system::WindowsSandboxSelection::RestrictedToken
} else {
codex_protocol::config_types::WindowsSandboxLevel::Disabled
codex_file_system::WindowsSandboxSelection::Disabled
},
windows_sandbox_private_desktop: false,
windows_sandbox_proxy_settings_mode: None,
@@ -27,7 +27,7 @@ use crate::exec_env::inject_session_env;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::shell::ShellType;
use crate::tools::sandboxing::executor_windows_sandbox_level;
use crate::tools::sandboxing::executor_windows_sandbox_selection;
impl Session {
pub(crate) fn prewarm_shell_snapshots(
@@ -97,7 +97,7 @@ impl Session {
workspace_roots: environment.workspace_roots().to_vec(),
user_home_dir: None,
temporary_directories: None,
windows_sandbox_level: executor_windows_sandbox_level(
windows_sandbox_selection: executor_windows_sandbox_selection(
environment.config().windows_sandbox_level,
environment.cwd(),
),
@@ -1,7 +1,7 @@
//! Reviews terminal input against retained launch permissions and current policy.
//! Permission snapshots stay host-owned; neither an earlier approval nor a policy
//! change alters the sandbox of an already-running process. Native launches keep
//! their configured Windows sandbox level; executor launches use executor defaults.
//! their configured Windows sandbox selection; executor launches use executor defaults.
use super::ProcessEntry;
use super::UnifiedExecContext;
@@ -56,7 +56,7 @@ impl TerminalPolicy {
if matches!(sandbox_source, TerminalSandboxSource::Native) {
// The filesystem helper applies executor defaults, but native process
// launches honor Disabled. Preserve it so later enablement is detected.
sandbox.windows_sandbox_level = environment.config().windows_sandbox_level;
sandbox.windows_sandbox_selection = environment.config().windows_sandbox_level.into();
}
Self {
sandbox,
@@ -920,6 +920,7 @@ mod tests {
use super::ProcessSandboxType;
use super::ShellInfo;
use codex_file_system::FileSystemSandboxContext;
use codex_file_system::WindowsSandboxSelection;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkProxyAuditMetadata;
use codex_network_proxy::NetworkProxyConfig;
@@ -1274,9 +1275,11 @@ mod tests {
let mut sandbox =
FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, cwd.clone());
sandbox.user_home_dir = Some(cwd.clone());
sandbox.windows_sandbox_selection = WindowsSandboxSelection::Mxc;
let serialized = serde_json::to_value(&sandbox).expect("serialize sandbox");
assert_eq!(serialized["windowsSandboxLevel"], "mxc");
assert_eq!(
serialized["userHomeDir"],
serde_json::json!(cwd.to_string())
@@ -78,9 +78,7 @@ async fn discover_root(
#[cfg(target_os = "windows")]
if sandbox.is_some_and(|context| {
context.should_run_in_sandbox()
&& context.windows_sandbox_level
== codex_protocol::config_types::WindowsSandboxLevel::Disabled
context.should_run_in_sandbox() && !context.windows_sandbox_is_requested()
}) {
discovery.error = Some("filesystem sandbox is unavailable on this executor".to_string());
return discovery;
+8 -7
View File
@@ -3,6 +3,7 @@ use std::collections::HashMap;
use std::time::Duration;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
@@ -16,7 +17,6 @@ use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_utils_absolute_path::AbsolutePathBuf;
#[cfg(not(target_os = "linux"))]
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
@@ -144,10 +144,10 @@ impl FileSystemSandboxRunner {
let sandbox_manager = sandbox_manager.with_allowed_symlinked_codex_home(
self.runtime_paths.allowed_symlinked_codex_home.clone(),
);
let sandbox = sandbox_manager.select_initial(
let (sandbox, windows_sandbox_level) = crate::sandbox_selection::select_sandbox(
&sandbox_manager,
permission_profile,
SandboxablePreference::Require,
sandbox_context.windows_sandbox_level,
sandbox_context,
/*has_managed_network_requirements*/ false,
);
if sandbox == SandboxType::None {
@@ -182,7 +182,8 @@ impl FileSystemSandboxRunner {
self.runtime_paths.codex_linux_sandbox_exe.as_deref()
},
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_level: windows_sandbox_level
.unwrap_or(WindowsSandboxLevel::Disabled),
windows_sandbox_private_desktop: sandbox_context
.windows_sandbox_private_desktop,
},
@@ -721,8 +722,8 @@ mod tests {
"filesystem sandbox cannot be enforced on this executor"
);
crate::FileSystemSandboxContext {
windows_sandbox_level:
codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken,
windows_sandbox_selection:
codex_file_system::WindowsSandboxSelection::RestrictedToken,
..sandbox_context
}
};
+2
View File
@@ -36,6 +36,7 @@ mod resolved_capability;
mod rpc;
mod rpc_server_requests;
mod runtime_paths;
mod sandbox_selection;
mod sandboxed_file_open;
mod sandboxed_file_system;
mod server;
@@ -86,6 +87,7 @@ pub use codex_file_system::WalkEntryKind;
pub use codex_file_system::WalkError;
pub use codex_file_system::WalkOptions;
pub use codex_file_system::WalkOutcome;
pub use codex_file_system::WindowsSandboxSelection;
pub use codex_file_system::WriteFileOptions;
pub use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR;
+17 -16
View File
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::process_telemetry::ProcessTelemetry;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_file_system::WindowsSandboxSelection;
use codex_network_proxy::CUSTOM_CA_ENV_KEYS;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::ManagedProxyRouting;
@@ -22,7 +23,6 @@ use codex_sandboxing::SandboxDirectSpawnTransformRequest;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
use codex_sandboxing::WindowsSandboxProxySettingsMode;
use codex_sandboxing::WindowsSandboxSpawnRequest;
@@ -39,6 +39,7 @@ use crate::ExecServerRuntimePaths;
use crate::protocol::ExecParams;
use crate::rpc::internal_error;
use crate::rpc::invalid_params;
use crate::sandbox_selection::select_sandbox;
pub(crate) struct PreparedExecRequest {
pub(crate) command: Vec<String>,
@@ -53,7 +54,7 @@ pub(crate) struct PreparedExecRequest {
struct PreparedWindowsSandboxRequest {
permission_profile: PermissionProfile,
workspace_roots: Vec<AbsolutePathBuf>,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
windows_sandbox_level: WindowsSandboxLevel,
proxy_enforced: bool,
network_proxy_restricting_sid: Option<String>,
proxy_settings_mode: WindowsSandboxProxySettingsMode,
@@ -87,7 +88,7 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
telemetry: &ProcessTelemetry,
) -> Result<PreparedExecRequest, JSONRPCErrorError> {
if let Some(sandbox) = params.sandbox.as_ref()
&& sandbox.windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Mxc
&& sandbox.windows_sandbox_selection == WindowsSandboxSelection::Mxc
{
if params.arg0.is_some() || sandbox.windows_sandbox_private_desktop {
return Err(invalid_params(
@@ -120,11 +121,9 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
prepare_managed_network(
params.managed_network.as_ref(),
network_proxy,
if params
.sandbox
.as_ref()
.is_some_and(|sandbox| sandbox.windows_sandbox_level == WindowsSandboxLevel::Mxc)
{
if params.sandbox.as_ref().is_some_and(|sandbox| {
sandbox.windows_sandbox_selection == WindowsSandboxSelection::Mxc
}) {
ManagedProxyRouting::DedicatedListeners
} else {
ManagedProxyRouting::SharedIngress
@@ -213,10 +212,10 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
#[cfg(target_os = "macos")]
let sandbox_manager = sandbox_manager
.with_allowed_symlinked_codex_home(runtime_paths.allowed_symlinked_codex_home.clone());
let sandbox = sandbox_manager.select_initial(
let (sandbox, windows_sandbox_level) = select_sandbox(
&sandbox_manager,
&permissions,
SandboxablePreference::Require,
sandbox_context.windows_sandbox_level,
sandbox_context,
params.enforce_managed_network,
);
if sandbox == SandboxType::None {
@@ -272,7 +271,7 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
runtime_paths.codex_linux_sandbox_exe.as_deref()
},
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_level: windows_sandbox_level.unwrap_or(WindowsSandboxLevel::Disabled),
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
},
};
@@ -284,10 +283,12 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
}
.map_err(|err| invalid_params(format!("failed to prepare process sandbox: {err}")))?;
let windows_sandbox = if sandbox == SandboxType::WindowsRestrictedToken {
let windows_sandbox_level = windows_sandbox_level.ok_or_else(|| {
invalid_params("restricted token sandbox requires a sandbox level".to_string())
})?;
request.arg0 = params.arg0.clone();
let proxy_enforced = params.enforce_managed_network;
let use_elevated =
windows_sandbox_uses_elevated_backend(sandbox_context.windows_sandbox_level);
let use_elevated = windows_sandbox_uses_elevated_backend(windows_sandbox_level);
let filesystem_overrides = if use_elevated {
resolve_windows_elevated_filesystem_overrides(
sandbox,
@@ -300,14 +301,14 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
sandbox,
&permissions,
&native_sandbox_policy_cwd,
sandbox_context.windows_sandbox_level,
windows_sandbox_level,
)
}
.map_err(|err| invalid_params(format!("failed to prepare process sandbox: {err}")))?;
Some(PreparedWindowsSandboxRequest {
permission_profile: permissions,
workspace_roots: native_workspace_roots,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_level,
proxy_enforced,
network_proxy_restricting_sid,
proxy_settings_mode: windows_sandbox_proxy_settings_mode,
@@ -4,6 +4,8 @@ use std::sync::Arc;
use std::time::Duration;
use codex_exec_server_protocol::JSONRPCErrorError;
#[cfg(unix)]
use codex_file_system::WindowsSandboxSelection;
#[cfg(target_os = "macos")]
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkPolicyAuditObserver;
@@ -135,8 +137,7 @@ async fn sandbox_request_wraps_native_argv_on_executor() {
);
let mut params = params;
params.sandbox.as_mut().unwrap().windows_sandbox_level =
codex_protocol::config_types::WindowsSandboxLevel::Mxc;
params.sandbox.as_mut().unwrap().windows_sandbox_selection = WindowsSandboxSelection::Mxc;
let error = prepare_exec_request(
&params,
HashMap::new(),
@@ -620,7 +621,7 @@ async fn managed_network_honors_windows_sandbox_level(windows_sandbox_level: Win
permissions.clone(),
cwd_uri.clone(),
);
sandbox.windows_sandbox_level = windows_sandbox_level;
sandbox.windows_sandbox_selection = windows_sandbox_level.into();
sandbox.windows_sandbox_proxy_settings_mode =
Some(codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve);
let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig {
@@ -0,0 +1,30 @@
//! Resolves an executor sandbox context to a concrete local sandbox implementation.
use codex_file_system::FileSystemSandboxContext;
use codex_file_system::WindowsSandboxSelection;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
pub(crate) fn select_sandbox(
manager: &SandboxManager,
permission_profile: &PermissionProfile,
sandbox_context: &FileSystemSandboxContext,
has_managed_network_requirements: bool,
) -> (SandboxType, Option<WindowsSandboxLevel>) {
let windows_sandbox_level = match sandbox_context.windows_sandbox_selection {
WindowsSandboxSelection::Disabled => WindowsSandboxLevel::Disabled,
WindowsSandboxSelection::RestrictedToken => WindowsSandboxLevel::RestrictedToken,
WindowsSandboxSelection::Elevated => WindowsSandboxLevel::Elevated,
WindowsSandboxSelection::Mxc => return (SandboxType::WindowsMxc, None),
};
let sandbox_type = manager.select_initial(
permission_profile,
SandboxablePreference::Require,
windows_sandbox_level,
has_managed_network_requirements,
);
(sandbox_type, Some(windows_sandbox_level))
}
@@ -3,7 +3,6 @@ use std::io;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_protocol::config_types::WindowsSandboxLevel;
use crate::CapabilityRootsDiscoverParams;
use crate::CapabilityRootsDiscoverResponse;
@@ -81,8 +80,7 @@ impl FileSystemHandler {
.and_then(|root| root.sandbox.as_ref())
.filter(|sandbox| {
sandbox.should_run_in_sandbox()
&& (!cfg!(target_os = "windows")
|| sandbox.windows_sandbox_level != WindowsSandboxLevel::Disabled)
&& (!cfg!(target_os = "windows") || sandbox.windows_sandbox_is_requested())
&& params
.roots
.iter()
@@ -12,20 +12,15 @@ use codex_exec_server::CapabilityRootsDiscoverResponse;
use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::InitializeParams;
use codex_exec_server::InitializeResponse;
use codex_exec_server::WindowsSandboxSelection;
use codex_exec_server_protocol::CapabilityRootDiscoverRequest;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCResponse;
#[cfg(unix)]
use codex_protocol::models::PermissionProfile;
#[cfg(unix)]
use codex_protocol::permissions::FileSystemAccessMode;
#[cfg(unix)]
use codex_protocol::permissions::FileSystemSandboxEntry;
#[cfg(unix)]
use codex_protocol::permissions::FileSystemSandboxPolicy;
#[cfg(unix)]
use codex_protocol::permissions::NetworkSandboxPolicy;
#[cfg(unix)]
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use common::exec_server::exec_server;
@@ -188,10 +183,11 @@ async fn discovers_cursor_plugin_without_reading_default_mcp_for_inline_servers(
Ok(())
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn sandboxed_discovery_batches_roots_without_combining_different_permissions()
-> anyhow::Result<()> {
#[cfg(windows)]
crate::skip_if_mxc_unavailable!(Ok(()));
let workspace = tempfile::tempdir()?;
let first_root = workspace.path().join("first");
let second_root = workspace.path().join("second");
@@ -216,6 +212,7 @@ async fn sandboxed_discovery_batches_roots_without_combining_different_permissio
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted),
workspace_uri,
);
let shared_sandbox = with_native_sandbox(shared_sandbox);
#[cfg(target_os = "linux")]
let fake_bwrap_directory = tempfile::tempdir()?;
@@ -338,6 +335,7 @@ async fn sandboxed_discovery_batches_roots_without_combining_different_permissio
),
first_uri.clone(),
);
let first_only_sandbox = with_native_sandbox(first_only_sandbox);
let read_second_root = FileSystemSandboxEntry::new(
AbsolutePathBuf::from_absolute_path(&second_root)?.into(),
FileSystemAccessMode::Read,
@@ -350,6 +348,7 @@ async fn sandboxed_discovery_batches_roots_without_combining_different_permissio
),
second_uri.clone(),
);
let second_only_sandbox = with_native_sandbox(second_only_sandbox);
let response = discover_roots(
&mut server,
vec![
@@ -546,3 +545,10 @@ fn write_file(path: &std::path::Path, contents: &str) -> anyhow::Result<()> {
std::fs::write(path, contents)?;
Ok(())
}
fn with_native_sandbox(mut sandbox: FileSystemSandboxContext) -> FileSystemSandboxContext {
if cfg!(windows) {
sandbox.windows_sandbox_selection = WindowsSandboxSelection::Mxc;
}
sandbox
}
+10
View File
@@ -32,6 +32,16 @@ pub(crate) const SYSTEM_PROXY_URL_ENV: &str = "CODEX_EXEC_SERVER_TEST_SYSTEM_PRO
const CODEX_WINDOWS_SANDBOX_ARG1: &str = "--run-as-windows-sandbox";
const DELAYED_OUTPUT_AFTER_EXIT_CHILD_ARG: &str = "--codex-test-delayed-output-after-exit-child";
#[macro_export]
macro_rules! skip_if_mxc_unavailable {
($return_value:expr $(,)?) => {{
if !codex_sandboxing::windows_mxc_available() {
eprintln!("skipping test: native MXC is unavailable on this host");
return $return_value;
}
}};
}
#[ctor]
pub static TEST_BINARY_DISPATCH_GUARD: Option<TestBinaryDispatchGuard> = {
let guard = configure_test_binary_dispatch("codex-exec-server-tests", |exe_name, argv1| {
+18 -16
View File
@@ -27,6 +27,8 @@ use codex_exec_server::ShellInfo;
#[cfg(unix)]
use codex_exec_server::ShellSnapshotRequest;
use codex_exec_server::StartedExecProcess;
#[cfg(any(unix, windows))]
use codex_exec_server::WindowsSandboxSelection;
use codex_exec_server::WriteStatus;
#[cfg(unix)]
use codex_network_proxy::NetworkProxyConfig;
@@ -36,7 +38,6 @@ use codex_network_proxy::RemoteNetworkProxyConfig;
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
#[cfg(unix)]
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
use codex_protocol::config_types::WindowsSandboxLevel;
#[cfg(unix)]
use codex_protocol::models::PermissionProfile;
#[cfg(unix)]
@@ -104,10 +105,6 @@ async fn create_process_context(use_remote: bool) -> Result<ProcessContext> {
}
}
fn selected_windows_sandbox_available(windows_sandbox_level: WindowsSandboxLevel) -> bool {
windows_sandbox_level != WindowsSandboxLevel::Mxc || codex_sandboxing::windows_mxc_available()
}
#[cfg(target_os = "macos")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn codex_home_symlink_opt_out_respects_host_config_and_scope() -> Result<()> {
@@ -1295,13 +1292,11 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
}
async fn assert_remote_windows_sandbox_process_write(
windows_sandbox_level: WindowsSandboxLevel,
expected_sandbox_type: codex_sandboxing::SandboxType,
tty: bool,
) -> Result<()> {
if !selected_windows_sandbox_available(windows_sandbox_level) {
eprintln!("skipping MXC enforcement test: native MXC is unavailable on this host");
return Ok(());
if expected_sandbox_type == codex_sandboxing::SandboxType::WindowsMxc {
crate::skip_if_mxc_unavailable!(Ok(()));
}
let context = create_process_context(/*use_remote*/ true).await?;
let workspace = TempDir::new()?;
@@ -1311,7 +1306,19 @@ async fn assert_remote_windows_sandbox_process_write(
SandboxPolicy::new_read_only_policy(),
cwd.clone(),
)?;
sandbox.windows_sandbox_level = windows_sandbox_level;
match expected_sandbox_type {
codex_sandboxing::SandboxType::WindowsRestrictedToken => {
sandbox.windows_sandbox_selection = WindowsSandboxSelection::RestrictedToken;
}
codex_sandboxing::SandboxType::WindowsMxc => {
sandbox.windows_sandbox_selection = WindowsSandboxSelection::Mxc;
}
codex_sandboxing::SandboxType::None
| codex_sandboxing::SandboxType::MacosSeatbelt
| codex_sandboxing::SandboxType::LinuxSeccomp => {
anyhow::bail!("expected a Windows sandbox type")
}
}
let session = match context
.backend
@@ -1767,19 +1774,16 @@ async fn exec_process_write_then_read_without_tty(use_remote: bool) -> Result<()
}
#[test_case(
WindowsSandboxLevel::RestrictedToken,
codex_sandboxing::SandboxType::WindowsRestrictedToken,
false;
"restricted_token"
)]
#[test_case(
WindowsSandboxLevel::Mxc,
codex_sandboxing::SandboxType::WindowsMxc,
false;
"mxc_pipe"
)]
#[test_case(
WindowsSandboxLevel::Mxc,
codex_sandboxing::SandboxType::WindowsMxc,
true;
"mxc_conpty"
@@ -1788,12 +1792,10 @@ async fn exec_process_write_then_read_without_tty(use_remote: bool) -> Result<()
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial_test::serial(remote_exec_server)]
async fn remote_windows_sandbox_process_accepts_process_write(
windows_sandbox_level: WindowsSandboxLevel,
expected_sandbox_type: codex_sandboxing::SandboxType,
tty: bool,
) -> Result<()> {
assert_remote_windows_sandbox_process_write(windows_sandbox_level, expected_sandbox_type, tty)
.await
assert_remote_windows_sandbox_process_write(expected_sandbox_type, tty).await
}
#[test_case(false ; "local")]
@@ -14,10 +14,7 @@ use pretty_assertions::assert_eq;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial_test::serial(remote_exec_server)]
async fn mxc_tmpdir_uses_command_environment_over_rpc() -> Result<()> {
if !selected_windows_sandbox_available(WindowsSandboxLevel::Mxc) {
eprintln!("skipping MXC enforcement test: native MXC is unavailable on this host");
return Ok(());
}
crate::skip_if_mxc_unavailable!(Ok(()));
let root = TempDir::new()?;
let command_temp = root.path().join("command temp");
let server_temp = root.path().join("server temp");
@@ -53,7 +50,7 @@ async fn mxc_tmpdir_uses_command_environment_over_rpc() -> Result<()> {
PermissionProfile::from_runtime_permissions(&fs, NetworkSandboxPolicy::Restricted),
cwd.clone(),
);
sandbox.windows_sandbox_level = WindowsSandboxLevel::Mxc;
sandbox.windows_sandbox_selection = codex_exec_server::WindowsSandboxSelection::Mxc;
let command_temp = command_temp.to_string_lossy().into_owned();
let started = environment
.get_exec_backend()
@@ -7,7 +7,7 @@ use codex_exec_server::ExecServerRuntimePaths;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::LocalFileSystem;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_exec_server::WindowsSandboxSelection;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
@@ -141,7 +141,7 @@ pub(crate) fn workspace_write_sandbox(
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted),
PathUri::from_abs_path(&writable_root),
);
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
sandbox.windows_sandbox_selection = WindowsSandboxSelection::RestrictedToken;
sandbox
}
@@ -163,7 +163,7 @@ fn sandbox_context(mut entries: Vec<FileSystemSandboxEntry>) -> FileSystemSandbo
),
);
if cfg!(windows) {
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
sandbox.windows_sandbox_selection = WindowsSandboxSelection::RestrictedToken;
}
sandbox
}
@@ -20,9 +20,10 @@ use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::GetMetadataOptions;
use codex_exec_server::ReadFileOptions;
use codex_exec_server::RemoveOptions;
use codex_exec_server::WindowsSandboxSelection;
use codex_exec_server::WriteFileOptions;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::SandboxType;
use codex_utils_path_uri::PathUri;
use futures::TryStreamExt;
use pretty_assertions::assert_eq;
@@ -346,8 +347,12 @@ async fn file_system_no_follow_operations_reject_named_pipes(
Ok(())
}
#[test_case(SandboxType::WindowsRestrictedToken; "restricted_token")]
#[test_case(SandboxType::WindowsMxc; "mxc")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_remote_fs_helper_respects_windows_sandbox_write_policy() -> Result<()> {
async fn file_system_remote_fs_helper_respects_windows_sandbox_write_policy(
sandbox_type: SandboxType,
) -> Result<()> {
let context = create_file_system_context(FileSystemImplementation::Remote).await?;
let file_system = context.file_system;
let tmp = tempfile::TempDir::new()?;
@@ -355,7 +360,39 @@ async fn file_system_remote_fs_helper_respects_windows_sandbox_write_policy() ->
std::fs::create_dir_all(&readonly_dir)?;
let mut sandbox = read_only_sandbox_for_cwd(readonly_dir.clone())?;
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
match sandbox_type {
SandboxType::WindowsRestrictedToken => {
sandbox.windows_sandbox_selection = WindowsSandboxSelection::RestrictedToken;
}
SandboxType::WindowsMxc => {
sandbox.windows_sandbox_selection = WindowsSandboxSelection::Mxc;
}
SandboxType::None | SandboxType::MacosSeatbelt | SandboxType::LinuxSeccomp => {
anyhow::bail!("expected a Windows sandbox type")
}
}
let blocked_file = readonly_dir.join("blocked.txt");
if sandbox_type == SandboxType::WindowsMxc && !codex_sandboxing::windows_mxc_available() {
let error = file_system
.write_file(
&PathUri::from_host_native_path(&blocked_file)?,
b"blocked".to_vec(),
WriteFileOptions::default(),
Some(&sandbox),
)
.await
.expect_err("unavailable MXC must fail closed");
assert_eq!(
(error.kind(), error.to_string()),
(
std::io::ErrorKind::InvalidInput,
"failed to prepare fs sandbox: failed to prepare MXC sandbox: native MXC is unavailable on this executor".to_owned(),
)
);
assert!(!blocked_file.exists());
return Ok(());
}
let readable_file = readonly_dir.join("readable.txt");
std::fs::write(&readable_file, b"readable")?;
@@ -369,12 +406,13 @@ async fn file_system_remote_fs_helper_respects_windows_sandbox_write_policy() ->
// Some local Windows hosts cannot create restricted tokens. Reaching that
// error still proves the remote fs helper went through the Windows sandbox
// launcher; before the wrapper fix this read would have run unsandboxed.
if is_unsupported_restricted_token_host(&read_result) {
if sandbox_type == SandboxType::WindowsRestrictedToken
&& is_unsupported_restricted_token_host(&read_result)
{
return Ok(());
}
assert_eq!(read_result?, b"readable");
let blocked_file = readonly_dir.join("blocked.txt");
let error = file_system
.write_file(
&PathUri::from_host_native_path(&blocked_file)?,
@@ -450,7 +488,7 @@ async fn file_system_private_desktop_survives_helper_exits_and_separates_permiss
}
let mut readonly = read_only_sandbox_for_cwd(tmp.path().to_path_buf())?;
readonly.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
readonly.windows_sandbox_selection = WindowsSandboxSelection::RestrictedToken;
readonly.windows_sandbox_private_desktop = true;
assert_eq!(
file_system
+1 -4
View File
@@ -310,10 +310,7 @@ async fn read_bounded_text(
};
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox)
&& path.infer_path_convention() == Some(PathConvention::Windows)
&& sandbox.is_some_and(|context| {
context.windows_sandbox_level
== codex_protocol::config_types::WindowsSandboxLevel::Disabled
})
&& sandbox.is_some_and(|context| !context.windows_sandbox_is_requested())
{
return Err(SkillProviderError::new(
"executor skill resource requires an unavailable filesystem sandbox",
@@ -30,6 +30,7 @@ use codex_exec_server::WalkEntry;
use codex_exec_server::WalkEntryKind;
use codex_exec_server::WalkOptions;
use codex_exec_server::WalkOutcome;
use codex_exec_server::WindowsSandboxSelection;
use codex_exec_server::WriteFileOptions;
use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::capabilities::SelectedCapabilityRoot;
@@ -301,12 +302,12 @@ async fn skill_loading_and_reads_use_the_supplied_executor_file_system() {
}
#[tokio::test]
async fn windows_executor_skill_read_rejects_disabled_sandbox_on_any_orchestrator() {
async fn windows_executor_skill_read_requires_a_requested_sandbox() {
let provider = ExecutorSkillProvider::new_with_restriction_product(
Arc::new(EnvironmentManager::default_for_tests()),
/*restriction_product*/ None,
);
let sandbox = FileSystemSandboxContext::from_permission_profile(
let mut sandbox = FileSystemSandboxContext::from_permission_profile(
PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(Vec::new()),
NetworkSandboxPolicy::Restricted,
@@ -317,6 +318,25 @@ async fn windows_executor_skill_read_rejects_disabled_sandbox_on_any_orchestrato
"local",
PathUri::parse("file:///C:/skill/SKILL.md").expect("Windows resource URI"),
);
let error = provider
.read(SkillReadRequest {
_lifetime: PhantomData,
authority: SkillAuthority::new(SkillSourceKind::Executor, "windows-root"),
package: SkillPackageId("skill://windows-root/C:/skill".into()),
resource: resource.clone(),
resolved_executor_roots: Vec::new(),
sandbox: Some(sandbox.clone()),
host_snapshot: None,
mcp_resources: None,
})
.await
.expect_err("disabled Windows sandbox must fail closed");
assert_eq!(
error.message,
"executor skill resource requires an unavailable filesystem sandbox"
);
sandbox.windows_sandbox_selection = WindowsSandboxSelection::Mxc;
let error = provider
.read(SkillReadRequest {
_lifetime: PhantomData,
@@ -329,11 +349,12 @@ async fn windows_executor_skill_read_rejects_disabled_sandbox_on_any_orchestrato
mcp_resources: None,
})
.await
.expect_err("disabled Windows sandbox must fail closed");
assert_eq!(
error.message,
"executor skill resource requires an unavailable filesystem sandbox"
.expect_err("synthetic Windows resource should not be readable");
assert!(
error
.message
.starts_with("failed to read executor skill resource"),
"{error:?}"
);
}
+32 -2
View File
@@ -325,6 +325,30 @@ impl TryFrom<ExecPermissionProfile> for PermissionProfile {
}
}
/// Windows sandbox choice encoded in executor RPCs.
///
/// The serialized field retains its legacy `windowsSandboxLevel` name for compatibility, but MXC
/// is a sandbox implementation rather than a RestrictedToken level.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WindowsSandboxSelection {
#[default]
Disabled,
RestrictedToken,
Elevated,
Mxc,
}
impl From<WindowsSandboxLevel> for WindowsSandboxSelection {
fn from(level: WindowsSandboxLevel) -> Self {
match level {
WindowsSandboxLevel::Disabled => Self::Disabled,
WindowsSandboxLevel::RestrictedToken => Self::RestrictedToken,
WindowsSandboxLevel::Elevated => Self::Elevated,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileSystemSandboxContext {
@@ -339,7 +363,8 @@ pub struct FileSystemSandboxContext {
/// Executor-local default directories used to resolve `:tmpdir` policy entries.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temporary_directories: Option<Vec<PathUri>>,
pub windows_sandbox_level: WindowsSandboxLevel,
#[serde(rename = "windowsSandboxLevel")]
pub windows_sandbox_selection: WindowsSandboxSelection,
#[serde(default)]
pub windows_sandbox_private_desktop: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -385,7 +410,7 @@ impl FileSystemSandboxContext {
workspace_roots,
user_home_dir: None,
temporary_directories: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_selection: WindowsSandboxSelection::Disabled,
windows_sandbox_private_desktop: false,
windows_sandbox_proxy_settings_mode: None,
use_legacy_landlock: false,
@@ -402,6 +427,11 @@ impl FileSystemSandboxContext {
&& !file_system_policy.has_full_disk_write_access()
}
/// Whether this context selects either supported Windows sandbox implementation.
pub fn windows_sandbox_is_requested(&self) -> bool {
self.windows_sandbox_selection != WindowsSandboxSelection::Disabled
}
/// Borrows the executor-owned paths needed to interpret filesystem policy entries.
///
/// Returns `None` after the cwd has been omitted from a transport context.
-1
View File
@@ -299,7 +299,6 @@ pub enum WindowsSandboxLevel {
Disabled,
RestrictedToken,
Elevated,
Mxc,
}
/// Controls whether a Windows sandbox launch reconciles persistent proxy settings or preserves
-3
View File
@@ -333,9 +333,6 @@ impl SandboxManager {
if !self.should_sandbox(permission_profile, pref, has_managed_network_requirements) {
return SandboxType::None;
}
if cfg!(windows) && windows_sandbox_level == WindowsSandboxLevel::Mxc {
return SandboxType::WindowsMxc;
}
get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
.unwrap_or(SandboxType::None)
}
-42
View File
@@ -25,48 +25,6 @@ use pretty_assertions::assert_eq;
use std::collections::HashMap;
use tempfile::TempDir;
#[test]
fn mxc_selection_preserves_unsandboxed_requests_and_other_platforms() {
let manager = SandboxManager::new();
let platform_sandbox = if cfg!(windows) {
SandboxType::WindowsMxc
} else {
get_platform_sandbox(/*windows_sandbox_enabled*/ false).unwrap_or(SandboxType::None)
};
for (permissions, preference, expected) in [
(
PermissionProfile::read_only(),
SandboxablePreference::Auto,
platform_sandbox,
),
(
PermissionProfile::Disabled,
SandboxablePreference::Require,
platform_sandbox,
),
(
PermissionProfile::read_only(),
SandboxablePreference::Forbid,
SandboxType::None,
),
(
PermissionProfile::Disabled,
SandboxablePreference::Auto,
SandboxType::None,
),
] {
assert_eq!(
manager.select_initial(
&permissions,
preference,
WindowsSandboxLevel::Mxc,
/*has_managed_network_requirements*/ false,
),
expected,
);
}
}
#[test]
fn danger_full_access_defaults_to_no_sandbox_without_network_requirements() {
let manager = SandboxManager::new();