Merge 88514764af into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-05-22 10:40:18 -07:00
committed by GitHub
34 changed files with 857 additions and 863 deletions
@@ -76,10 +76,8 @@ impl WindowsSandboxRequestProcessor {
Ok(config) => {
let setup_request = WindowsSandboxSetupRequest {
mode,
policy: config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
policy_cwd: config.cwd.to_path_buf(),
permission_profile: config.permissions.effective_permission_profile(),
workspace_roots: config.effective_workspace_roots(),
command_cwd,
env_map: std::env::vars().collect(),
codex_home: config.codex_home.to_path_buf(),
+14 -22
View File
@@ -197,10 +197,11 @@ async fn run_command_under_sandbox(
// In practice, this should be `std::env::current_dir()` because this CLI
// does not support `--cwd`, but let's use the config value for consistency.
let cwd = config.cwd.clone();
// For now, we always use the same cwd for both the command and the
// sandbox policy. In the future, we could add a CLI option to set them
// separately.
// Non-Windows sandbox launchers still materialize profile-relative entries
// against the command cwd.
let sandbox_policy_cwd = cwd.clone();
#[cfg(target_os = "windows")]
let workspace_roots = config.effective_workspace_roots();
let env = create_env(
&config.permissions.shell_environment_policy,
@@ -211,7 +212,7 @@ async fn run_command_under_sandbox(
if let SandboxType::Windows = sandbox_type {
#[cfg(target_os = "windows")]
{
run_command_under_windows_session(&config, command, cwd, sandbox_policy_cwd, env).await;
run_command_under_windows_session(&config, command, cwd, workspace_roots, env).await;
}
#[cfg(not(target_os = "windows"))]
{
@@ -338,24 +339,15 @@ async fn run_command_under_windows_session(
config: &Config,
command: Vec<String>,
cwd: AbsolutePathBuf,
sandbox_policy_cwd: AbsolutePathBuf,
workspace_roots: Vec<AbsolutePathBuf>,
env: std::collections::HashMap<String, String>,
) -> ! {
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_windows_sandbox::spawn_windows_sandbox_session_elevated;
use codex_windows_sandbox::spawn_windows_sandbox_session_elevated_for_permission_profile;
use codex_windows_sandbox::spawn_windows_sandbox_session_legacy;
let sandbox_policy = config
.permissions
.legacy_sandbox_policy(sandbox_policy_cwd.as_path());
let policy_str = match serde_json::to_string(&sandbox_policy) {
Ok(policy_str) => policy_str,
Err(err) => {
eprintln!("windows sandbox failed to serialize policy: {err}");
std::process::exit(1);
}
};
let permission_profile = config.permissions.effective_permission_profile();
let use_elevated = matches!(
WindowsSandboxLevel::from_config(config),
@@ -363,9 +355,9 @@ async fn run_command_under_windows_session(
);
let spawned = if use_elevated {
spawn_windows_sandbox_session_elevated(
policy_str.as_str(),
sandbox_policy_cwd.as_path(),
spawn_windows_sandbox_session_elevated_for_permission_profile(
&permission_profile,
workspace_roots.as_slice(),
config.codex_home.as_path(),
command,
cwd.as_path(),
@@ -383,8 +375,8 @@ async fn run_command_under_windows_session(
.await
} else {
spawn_windows_sandbox_session_legacy(
policy_str.as_str(),
sandbox_policy_cwd.as_path(),
&permission_profile,
workspace_roots.as_slice(),
config.codex_home.as_path(),
command,
cwd.as_path(),
@@ -1020,7 +1012,7 @@ mod tests {
}
#[tokio::test]
async fn debug_sandbox_uses_explicit_profile_cwd() -> anyhow::Result<()> {
async fn debug_sandbox_uses_explicit_cwd() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
+21 -15
View File
@@ -387,7 +387,13 @@ pub fn build_exec_request(
.map(|request| {
let windows_sandbox_policy_cwd = AbsolutePathBuf::try_from(sandbox_cwd.to_path_buf())
.unwrap_or_else(|_| request.cwd.clone());
ExecRequest::from_sandbox_exec_request(request, options, windows_sandbox_policy_cwd)
let windows_sandbox_workspace_roots = vec![windows_sandbox_policy_cwd.clone()];
ExecRequest::from_sandbox_exec_request(
request,
options,
windows_sandbox_policy_cwd,
windows_sandbox_workspace_roots,
)
})
.map_err(CodexErr::from)?;
let use_windows_elevated_backend = windows_sandbox_uses_elevated_backend(
@@ -423,7 +429,6 @@ pub(crate) async fn execute_exec_request(
stdout_stream: Option<StdoutStream>,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> Result<ExecToolCallOutput> {
let sandbox_policy = exec_request.compatibility_sandbox_policy();
let ExecRequest {
command,
cwd,
@@ -434,6 +439,7 @@ pub(crate) async fn execute_exec_request(
capture_policy,
sandbox,
windows_sandbox_policy_cwd,
windows_sandbox_workspace_roots,
windows_sandbox_level,
windows_sandbox_private_desktop,
permission_profile,
@@ -464,9 +470,9 @@ pub(crate) async fn execute_exec_request(
stdout_stream,
after_spawn,
sandbox,
&sandbox_policy,
&permission_profile,
&windows_sandbox_policy_cwd,
&windows_sandbox_workspace_roots,
windows_sandbox_filesystem_overrides.as_ref(),
)
.await;
@@ -481,9 +487,10 @@ async fn get_raw_output_result(
stdout_stream: Option<StdoutStream>,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
#[cfg_attr(not(windows), allow(unused_variables))] sandbox: SandboxType,
#[cfg_attr(not(windows), allow(unused_variables))] sandbox_policy: &SandboxPolicy,
#[cfg_attr(not(windows), allow(unused_variables))] permission_profile: &PermissionProfile,
#[cfg_attr(not(windows), allow(unused_variables))] windows_sandbox_policy_cwd: &AbsolutePathBuf,
#[cfg_attr(not(windows), allow(unused_variables))]
windows_sandbox_workspace_roots: &[AbsolutePathBuf],
#[cfg_attr(not(windows), allow(unused_variables))] windows_sandbox_filesystem_overrides: Option<
&WindowsSandboxFilesystemOverrides,
>,
@@ -492,9 +499,9 @@ async fn get_raw_output_result(
if sandbox == SandboxType::WindowsRestrictedToken {
return exec_windows_sandbox(
params,
sandbox_policy,
permission_profile,
windows_sandbox_policy_cwd,
windows_sandbox_workspace_roots,
windows_sandbox_filesystem_overrides,
)
.await;
@@ -572,9 +579,9 @@ fn record_windows_sandbox_spawn_failure(
#[cfg(target_os = "windows")]
async fn exec_windows_sandbox(
params: ExecParams,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
windows_sandbox_policy_cwd: &AbsolutePathBuf,
windows_sandbox_workspace_roots: &[AbsolutePathBuf],
windows_sandbox_filesystem_overrides: Option<&WindowsSandboxFilesystemOverrides>,
) -> Result<RawExecToolCallOutput> {
use crate::config::find_codex_home;
@@ -604,12 +611,11 @@ async fn exec_windows_sandbox(
None
};
let policy_str = serde_json::to_string(sandbox_policy).map_err(|err| {
CodexErr::Io(io::Error::other(format!(
"failed to serialize Windows sandbox policy: {err}"
)))
})?;
let sandbox_cwd = windows_sandbox_policy_cwd.clone();
let workspace_roots = if windows_sandbox_workspace_roots.is_empty() {
vec![windows_sandbox_policy_cwd.clone()]
} else {
windows_sandbox_workspace_roots.to_vec()
};
let permission_profile = permission_profile.clone();
let codex_home = find_codex_home().map_err(|err| {
CodexErr::Io(io::Error::other(format!(
@@ -637,7 +643,7 @@ async fn exec_windows_sandbox(
run_windows_sandbox_capture_for_permission_profile_elevated(
codex_windows_sandbox::ElevatedSandboxProfileCaptureRequest {
permission_profile: &permission_profile,
permission_profile_cwd: &sandbox_cwd,
workspace_roots: workspace_roots.as_slice(),
codex_home: codex_home.as_ref(),
command,
cwd: &cwd,
@@ -655,8 +661,8 @@ async fn exec_windows_sandbox(
)
} else {
run_windows_sandbox_capture_with_filesystem_overrides(
policy_str.as_str(),
&sandbox_cwd,
&permission_profile,
workspace_roots.as_slice(),
codex_home.as_ref(),
command,
&cwd,
+5
View File
@@ -52,6 +52,7 @@ pub struct ExecRequest {
pub capture_policy: ExecCapturePolicy,
pub sandbox: SandboxType,
pub windows_sandbox_policy_cwd: AbsolutePathBuf,
pub windows_sandbox_workspace_roots: Vec<AbsolutePathBuf>,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
pub permission_profile: PermissionProfile,
@@ -77,6 +78,7 @@ impl ExecRequest {
arg0: Option<String>,
) -> Self {
let windows_sandbox_policy_cwd = cwd.clone();
let windows_sandbox_workspace_roots = vec![cwd.clone()];
let (file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
Self {
@@ -89,6 +91,7 @@ impl ExecRequest {
capture_policy,
sandbox,
windows_sandbox_policy_cwd,
windows_sandbox_workspace_roots,
windows_sandbox_level,
windows_sandbox_private_desktop,
permission_profile,
@@ -112,6 +115,7 @@ impl ExecRequest {
request: SandboxExecRequest,
options: ExecOptions,
windows_sandbox_policy_cwd: AbsolutePathBuf,
windows_sandbox_workspace_roots: Vec<AbsolutePathBuf>,
) -> Self {
let SandboxExecRequest {
command,
@@ -150,6 +154,7 @@ impl ExecRequest {
capture_policy,
sandbox,
windows_sandbox_policy_cwd,
windows_sandbox_workspace_roots,
windows_sandbox_level,
windows_sandbox_private_desktop,
permission_profile,
+1
View File
@@ -192,6 +192,7 @@ pub(crate) async fn execute_user_shell_command(
capture_policy: ExecCapturePolicy::ShellTool,
sandbox: SandboxType::None,
windows_sandbox_policy_cwd: cwd.clone(),
windows_sandbox_workspace_roots: turn_context.config.effective_workspace_roots(),
windows_sandbox_level: turn_context.windows_sandbox_level,
windows_sandbox_private_desktop: turn_context
.config
+4
View File
@@ -85,6 +85,7 @@ impl ToolOrchestrator {
enforce_managed_network: attempt.enforce_managed_network,
manager: attempt.manager,
sandbox_cwd: attempt.sandbox_cwd,
workspace_roots: attempt.workspace_roots,
codex_linux_sandbox_exe: attempt.codex_linux_sandbox_exe,
use_legacy_landlock: attempt.use_legacy_landlock,
windows_sandbox_level: attempt.windows_sandbox_level,
@@ -236,12 +237,14 @@ impl ToolOrchestrator {
let use_legacy_landlock = turn_ctx.features.use_legacy_landlock();
#[allow(deprecated)]
let sandbox_cwd = tool.sandbox_cwd(req).unwrap_or(&turn_ctx.cwd);
let workspace_roots = turn_ctx.config.effective_workspace_roots();
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
permissions: &turn_ctx.permission_profile,
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd,
workspace_roots: workspace_roots.as_slice(),
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
@@ -360,6 +363,7 @@ impl ToolOrchestrator {
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd,
workspace_roots: workspace_roots.as_slice(),
codex_linux_sandbox_exe: None,
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
@@ -212,6 +212,7 @@ async fn file_system_sandbox_context_uses_active_attempt() {
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &path,
workspace_roots: std::slice::from_ref(&path),
codex_linux_sandbox_exe: None,
use_legacy_landlock: true,
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
@@ -265,6 +266,7 @@ async fn no_sandbox_attempt_has_no_file_system_context() {
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &path,
workspace_roots: std::slice::from_ref(&path),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -111,6 +111,7 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow::
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &cwd,
workspace_roots: std::slice::from_ref(&cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -139,6 +139,7 @@ pub(super) async fn try_run_zsh_fork(
capture_policy: _capture_policy,
sandbox,
windows_sandbox_policy_cwd: sandbox_policy_cwd,
windows_sandbox_workspace_roots: _windows_sandbox_workspace_roots,
windows_sandbox_level,
windows_sandbox_private_desktop: _windows_sandbox_private_desktop,
permission_profile,
@@ -794,6 +795,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
capture_policy: ExecCapturePolicy::ShellTool,
sandbox: self.sandbox,
windows_sandbox_policy_cwd: self.sandbox_policy_cwd.clone(),
windows_sandbox_workspace_roots: vec![self.sandbox_policy_cwd.clone()],
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: false,
permission_profile: self.permission_profile.clone(),
@@ -931,6 +933,7 @@ impl CoreShellCommandExecutor {
exec_request,
options,
self.sandbox_policy_cwd.clone(),
vec![self.sandbox_policy_cwd.clone()],
);
if let Some(network) = exec_request.network.as_ref() {
network.apply_to_env(&mut exec_request.env);
+2
View File
@@ -383,6 +383,7 @@ pub(crate) struct SandboxAttempt<'a> {
pub enforce_managed_network: bool,
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a AbsolutePathBuf,
pub(crate) workspace_roots: &'a [AbsolutePathBuf],
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
@@ -422,6 +423,7 @@ impl<'a> SandboxAttempt<'a> {
request,
options,
windows_sandbox_policy_cwd,
self.workspace_roots.to_vec(),
)
})
}
@@ -866,12 +866,6 @@ impl UnifiedExecProcessManager {
#[cfg(target_os = "windows")]
if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken {
let sandbox_policy = request.compatibility_sandbox_policy();
let policy_json = serde_json::to_string(&sandbox_policy).map_err(|err| {
UnifiedExecError::create_process(format!(
"failed to serialize Windows sandbox policy: {err}"
))
})?;
let codex_home = crate::config::find_codex_home().map_err(|err| {
UnifiedExecError::create_process(format!(
"windows sandbox: failed to resolve codex_home: {err}"
@@ -903,7 +897,7 @@ impl UnifiedExecProcessManager {
codex_protocol::config_types::WindowsSandboxLevel::Elevated => {
codex_windows_sandbox::spawn_windows_sandbox_session_elevated_for_permission_profile(
&request.permission_profile,
request.windows_sandbox_policy_cwd.as_path(),
request.windows_sandbox_workspace_roots.as_slice(),
codex_home.as_ref(),
request.command.clone(),
request.cwd.as_path(),
@@ -923,8 +917,8 @@ impl UnifiedExecProcessManager {
codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken
| codex_protocol::config_types::WindowsSandboxLevel::Disabled => {
codex_windows_sandbox::spawn_windows_sandbox_session_legacy(
policy_json.as_str(),
request.windows_sandbox_policy_cwd.as_path(),
&request.permission_profile,
request.windows_sandbox_workspace_roots.as_slice(),
codex_home.as_ref(),
request.command.clone(),
request.cwd.as_path(),
@@ -100,7 +100,8 @@ fn exec_server_params_use_env_policy_overlay_contract() {
expiration: crate::exec::ExecExpiration::DefaultTimeout,
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
sandbox: codex_sandboxing::SandboxType::None,
windows_sandbox_policy_cwd: cwd,
windows_sandbox_policy_cwd: cwd.clone(),
windows_sandbox_workspace_roots: vec![cwd],
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
permission_profile,
+30 -28
View File
@@ -8,7 +8,8 @@ use codex_features::FeaturesToml;
use codex_login::default_client::originator;
use codex_otel::sanitize_metric_tag_value;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
@@ -145,16 +146,17 @@ pub fn elevated_setup_failure_metric_name(_err: &anyhow::Error) -> &'static str
#[cfg(target_os = "windows")]
pub fn run_elevated_setup(
policy: &SandboxPolicy,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> anyhow::Result<()> {
let permissions =
codex_windows_sandbox::ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
policy, policy_cwd,
);
codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)?;
codex_windows_sandbox::run_elevated_setup(
codex_windows_sandbox::SandboxSetupRequest {
permissions: &permissions,
@@ -169,8 +171,8 @@ pub fn run_elevated_setup(
#[cfg(not(target_os = "windows"))]
pub fn run_elevated_setup(
_policy: &SandboxPolicy,
_policy_cwd: &Path,
_permission_profile: &PermissionProfile,
_workspace_roots: &[AbsolutePathBuf],
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
@@ -180,15 +182,15 @@ pub fn run_elevated_setup(
#[cfg(target_os = "windows")]
pub fn run_legacy_setup_preflight(
policy: &SandboxPolicy,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_windows_sandbox_legacy_preflight(
policy,
policy_cwd,
permission_profile,
workspace_roots,
codex_home,
command_cwd,
env_map,
@@ -197,16 +199,16 @@ pub fn run_legacy_setup_preflight(
#[cfg(target_os = "windows")]
pub fn run_setup_refresh_with_extra_read_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
extra_read_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_setup_refresh_with_extra_read_roots(
policy,
policy_cwd,
permission_profile,
workspace_roots,
command_cwd,
env_map,
codex_home,
@@ -217,8 +219,8 @@ pub fn run_setup_refresh_with_extra_read_roots(
#[cfg(not(target_os = "windows"))]
pub fn run_legacy_setup_preflight(
_policy: &SandboxPolicy,
_policy_cwd: &Path,
_permission_profile: &PermissionProfile,
_workspace_roots: &[AbsolutePathBuf],
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
@@ -228,8 +230,8 @@ pub fn run_legacy_setup_preflight(
#[cfg(not(target_os = "windows"))]
pub fn run_setup_refresh_with_extra_read_roots(
_policy: &SandboxPolicy,
_policy_cwd: &Path,
_permission_profile: &PermissionProfile,
_workspace_roots: &[AbsolutePathBuf],
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
@@ -247,8 +249,8 @@ pub enum WindowsSandboxSetupMode {
#[derive(Debug, Clone)]
pub struct WindowsSandboxSetupRequest {
pub mode: WindowsSandboxSetupMode,
pub policy: SandboxPolicy,
pub policy_cwd: PathBuf,
pub permission_profile: PermissionProfile,
pub workspace_roots: Vec<AbsolutePathBuf>,
pub command_cwd: PathBuf,
pub env_map: HashMap<String, String>,
pub codex_home: PathBuf,
@@ -285,8 +287,8 @@ async fn run_windows_sandbox_setup_and_persist(
request: WindowsSandboxSetupRequest,
) -> anyhow::Result<()> {
let mode = request.mode;
let policy = request.policy;
let policy_cwd = request.policy_cwd;
let permission_profile = request.permission_profile;
let workspace_roots = request.workspace_roots;
let command_cwd = request.command_cwd;
let env_map = request.env_map;
let codex_home = request.codex_home;
@@ -297,8 +299,8 @@ async fn run_windows_sandbox_setup_and_persist(
WindowsSandboxSetupMode::Elevated => {
if !sandbox_setup_is_complete(setup_codex_home.as_path()) {
run_elevated_setup(
&policy,
policy_cwd.as_path(),
&permission_profile,
workspace_roots.as_slice(),
command_cwd.as_path(),
&env_map,
setup_codex_home.as_path(),
@@ -307,8 +309,8 @@ async fn run_windows_sandbox_setup_and_persist(
}
WindowsSandboxSetupMode::Unelevated => {
run_legacy_setup_preflight(
&policy,
policy_cwd.as_path(),
&permission_profile,
workspace_roots.as_slice(),
command_cwd.as_path(),
&env_map,
setup_codex_home.as_path(),
@@ -1,13 +1,14 @@
use crate::windows_sandbox::run_setup_refresh_with_extra_read_roots;
use anyhow::Result;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
pub fn grant_read_root_non_elevated(
policy: &SandboxPolicy,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
@@ -25,8 +26,8 @@ pub fn grant_read_root_non_elevated(
let canonical_root = dunce::canonicalize(read_root)?;
run_setup_refresh_with_extra_read_roots(
policy,
policy_cwd,
permission_profile,
workspace_roots,
command_cwd,
env_map,
codex_home,
@@ -1,19 +1,25 @@
use super::grant_read_root_non_elevated;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
fn policy() -> SandboxPolicy {
SandboxPolicy::new_workspace_write_policy()
fn permission_profile() -> PermissionProfile {
PermissionProfile::workspace_write()
}
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
#[test]
fn rejects_relative_path() {
let tmp = TempDir::new().expect("tempdir");
let workspace_roots = workspace_roots_for(tmp.path());
let err = grant_read_root_non_elevated(
&policy(),
tmp.path(),
&permission_profile(),
workspace_roots.as_slice(),
tmp.path(),
&HashMap::new(),
tmp.path(),
@@ -27,9 +33,10 @@ fn rejects_relative_path() {
fn rejects_missing_path() {
let tmp = TempDir::new().expect("tempdir");
let missing = tmp.path().join("does-not-exist");
let workspace_roots = workspace_roots_for(tmp.path());
let err = grant_read_root_non_elevated(
&policy(),
tmp.path(),
&permission_profile(),
workspace_roots.as_slice(),
tmp.path(),
&HashMap::new(),
tmp.path(),
@@ -44,9 +51,10 @@ fn rejects_file_path() {
let tmp = TempDir::new().expect("tempdir");
let file_path = tmp.path().join("file.txt");
std::fs::write(&file_path, "hello").expect("write file");
let workspace_roots = workspace_roots_for(tmp.path());
let err = grant_read_root_non_elevated(
&policy(),
tmp.path(),
&permission_profile(),
workspace_roots.as_slice(),
tmp.path(),
&HashMap::new(),
tmp.path(),
+2
View File
@@ -1040,11 +1040,13 @@ See the Codex keymap documentation for supported actions and examples."
.unwrap_or(false);
if should_check {
let cwd = app.config.cwd.clone();
let workspace_roots = app.config.effective_workspace_roots();
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
let tx = app.app_event_tx.clone();
let logs_base_dir = app.config.codex_home.clone();
Self::spawn_world_writable_scan(
cwd,
workspace_roots,
env_map,
logs_base_dir,
startup_permission_profile,
+21 -9
View File
@@ -8,6 +8,12 @@ use super::*;
#[cfg(target_os = "windows")]
use codex_utils_approval_presets::ApprovalPreset;
#[cfg(target_os = "windows")]
pub(super) struct WindowsSetupPermissions {
pub(super) permission_profile: PermissionProfile,
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
}
impl App {
pub(super) async fn rebuild_config_for_cwd(&self, cwd: PathBuf) -> Result<Config> {
let mut overrides = self.harness_overrides.clone();
@@ -45,19 +51,25 @@ impl App {
}
#[cfg(target_os = "windows")]
pub(super) async fn permission_profile_for_windows_setup(
pub(super) async fn windows_setup_permissions(
&self,
preset: &ApprovalPreset,
profile_selection: Option<&PermissionProfileSelection>,
) -> Result<PermissionProfile> {
) -> Result<WindowsSetupPermissions> {
match profile_selection {
Some(selection) => Ok(self
.rebuild_config_for_permission_profile(selection.profile_id.as_str())
.await?
.permissions
.permission_profile()
.clone()),
None => Ok(preset.permission_profile.clone()),
Some(selection) => {
let selected_config = self
.rebuild_config_for_permission_profile(selection.profile_id.as_str())
.await?;
Ok(WindowsSetupPermissions {
permission_profile: selected_config.permissions.permission_profile().clone(),
workspace_roots: selected_config.effective_workspace_roots(),
})
}
None => Ok(WindowsSetupPermissions {
permission_profile: preset.permission_profile.clone(),
workspace_roots: self.config.effective_workspace_roots(),
}),
}
}
+22 -51
View File
@@ -887,11 +887,11 @@ impl App {
} => {
#[cfg(target_os = "windows")]
{
let permission_profile = match self
.permission_profile_for_windows_setup(&preset, profile_selection.as_ref())
let setup_permissions = match self
.windows_setup_permissions(&preset, profile_selection.as_ref())
.await
{
Ok(permission_profile) => permission_profile,
Ok(setup_permissions) => setup_permissions,
Err(err) => {
tracing::warn!(
error = %err,
@@ -903,8 +903,9 @@ impl App {
return Ok(AppRunControl::Continue);
}
};
let policy_cwd = self.config.cwd.clone();
let command_cwd = policy_cwd.clone();
let permission_profile = setup_permissions.permission_profile;
let workspace_roots = setup_permissions.workspace_roots;
let command_cwd = self.config.cwd.clone();
let env_map: std::collections::HashMap<String, String> =
std::env::vars().collect();
let codex_home = self.config.codex_home.clone();
@@ -926,25 +927,10 @@ impl App {
self.chat_widget.show_windows_sandbox_setup_status();
self.windows_sandbox.setup_started_at = Some(Instant::now());
let session_telemetry = self.session_telemetry.clone();
let Ok(policy) = permission_profile
.to_legacy_sandbox_policy(policy_cwd.as_path())
.inspect_err(|err| {
tracing::error!(
%err,
"approval preset permissions cannot be projected for elevated Windows sandbox setup"
);
})
else {
tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt {
preset,
profile_selection,
});
return Ok(AppRunControl::Continue);
};
tokio::task::spawn_blocking(move || {
let result = crate::legacy_core::windows_sandbox::run_elevated_setup(
&policy,
policy_cwd.as_path(),
&permission_profile,
workspace_roots.as_slice(),
command_cwd.as_path(),
&env_map,
codex_home.as_path(),
@@ -1011,11 +997,11 @@ impl App {
} => {
#[cfg(target_os = "windows")]
{
let permission_profile = match self
.permission_profile_for_windows_setup(&preset, profile_selection.as_ref())
let setup_permissions = match self
.windows_setup_permissions(&preset, profile_selection.as_ref())
.await
{
Ok(permission_profile) => permission_profile,
Ok(setup_permissions) => setup_permissions,
Err(err) => {
tracing::warn!(
error = %err,
@@ -1027,8 +1013,9 @@ impl App {
return Ok(AppRunControl::Continue);
}
};
let policy_cwd = self.config.cwd.clone();
let command_cwd = policy_cwd.clone();
let permission_profile = setup_permissions.permission_profile;
let workspace_roots = setup_permissions.workspace_roots;
let command_cwd = self.config.cwd.clone();
let env_map: std::collections::HashMap<String, String> =
std::env::vars().collect();
let codex_home = self.config.codex_home.clone();
@@ -1036,26 +1023,11 @@ impl App {
let session_telemetry = self.session_telemetry.clone();
self.chat_widget.show_windows_sandbox_setup_status();
let Ok(policy) = permission_profile
.to_legacy_sandbox_policy(policy_cwd.as_path())
.inspect_err(|err| {
tracing::error!(
%err,
"approval preset permissions cannot be projected for legacy Windows sandbox setup"
);
})
else {
tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt {
preset,
profile_selection,
});
return Ok(AppRunControl::Continue);
};
tokio::task::spawn_blocking(move || {
if let Err(err) =
crate::legacy_core::windows_sandbox::run_legacy_setup_preflight(
&policy,
policy_cwd.as_path(),
&permission_profile,
workspace_roots.as_slice(),
command_cwd.as_path(),
&env_map,
codex_home.as_path(),
@@ -1092,11 +1064,8 @@ impl App {
/*hint*/ None,
));
let policy = self
.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path());
let policy_cwd = self.config.cwd.clone();
let permission_profile = self.config.permissions.effective_permission_profile();
let workspace_roots = self.config.effective_workspace_roots();
let command_cwd = self.config.cwd.clone();
let env_map: std::collections::HashMap<String, String> =
std::env::vars().collect();
@@ -1106,8 +1075,8 @@ impl App {
tokio::task::spawn_blocking(move || {
let requested_path = PathBuf::from(path);
let event = match crate::legacy_core::grant_read_root_non_elevated(
&policy,
policy_cwd.as_path(),
&permission_profile,
workspace_roots.as_slice(),
command_cwd.as_path(),
&env_map,
codex_home.as_path(),
@@ -1535,6 +1504,7 @@ impl App {
&& !self.chat_widget.world_writable_warning_hidden();
if should_check {
let cwd = self.config.cwd.clone();
let workspace_roots = self.config.effective_workspace_roots();
let env_map: std::collections::HashMap<String, String> =
std::env::vars().collect();
let tx = self.app_event_tx.clone();
@@ -1543,6 +1513,7 @@ impl App {
self.config.permissions.effective_permission_profile();
Self::spawn_world_writable_scan(
cwd,
workspace_roots,
env_map,
logs_base_dir,
permission_profile,
+15 -9
View File
@@ -16,25 +16,31 @@ impl App {
#[cfg(target_os = "windows")]
pub(super) fn spawn_world_writable_scan(
cwd: AbsolutePathBuf,
workspace_roots: Vec<AbsolutePathBuf>,
env_map: std::collections::HashMap<String, String>,
logs_base_dir: AbsolutePathBuf,
permission_profile: PermissionProfile,
tx: AppEventSender,
) {
let Ok(sandbox_policy) = permission_profile.to_legacy_sandbox_policy(cwd.as_path()) else {
send_world_writable_scan_failed(&tx);
let Ok(permissions) =
codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
workspace_roots.as_slice(),
)
else {
return;
};
tokio::task::spawn_blocking(move || {
let logs_base_dir_path = logs_base_dir.as_path();
let result = codex_windows_sandbox::apply_world_writable_scan_and_denies(
logs_base_dir_path,
cwd.as_path(),
&env_map,
&sandbox_policy,
Some(logs_base_dir_path),
);
let result =
codex_windows_sandbox::apply_world_writable_scan_and_denies_for_permissions(
logs_base_dir_path,
cwd.as_path(),
&env_map,
&permissions,
Some(logs_base_dir_path),
);
if result.is_err() {
// Scan failed: warn without examples.
send_world_writable_scan_failed(&tx);
@@ -14,13 +14,22 @@ impl ChatWidget {
return None;
}
let cwd = self.config.cwd.clone();
let workspace_roots = self.config.effective_workspace_roots();
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
let policy = self.config.legacy_sandbox_policy();
match codex_windows_sandbox::apply_world_writable_scan_and_denies(
let permission_profile = self.config.permissions.effective_permission_profile();
let Ok(permissions) =
codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
workspace_roots.as_slice(),
)
else {
return None;
};
match codex_windows_sandbox::apply_world_writable_scan_and_denies_for_permissions(
self.config.codex_home.as_path(),
cwd.as_path(),
&env_map,
&policy,
&permissions,
Some(self.config.codex_home.as_path()),
) {
Ok(_) => None,
+139 -71
View File
@@ -44,19 +44,41 @@ pub(crate) fn compute_allow_paths_for_permissions(
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::fs;
use tempfile::TempDir;
fn workspace_write_profile(
writable_roots: &[AbsolutePathBuf],
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
) -> PermissionProfile {
PermissionProfile::workspace_write_with(
writable_roots,
NetworkSandboxPolicy::Restricted,
exclude_tmpdir_env_var,
exclude_slash_tmp,
)
}
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
fn compute_allow_paths(
policy: &SandboxPolicy,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
) -> AllowDenyPaths {
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)
.expect("managed permission profile");
compute_allow_paths_for_permissions(&permissions, command_cwd, env_map)
}
@@ -68,14 +90,20 @@ mod tests {
let _ = fs::create_dir_all(&command_cwd);
let _ = fs::create_dir_all(&extra_root);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let writable_roots = vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()];
let permission_profile = workspace_write_profile(
&writable_roots,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&HashMap::new(),
);
assert!(
paths
@@ -91,25 +119,30 @@ mod tests {
}
#[test]
fn uses_policy_cwd_for_legacy_workspace_root() {
fn uses_runtime_workspace_roots_for_workspace_root() {
let tmp = TempDir::new().expect("tempdir");
let policy_cwd = tmp.path().join("workspace");
let command_cwd = policy_cwd.join("subdir");
let workspace_root = tmp.path().join("workspace");
let command_cwd = workspace_root.join("subdir");
fs::create_dir_all(&command_cwd).expect("create command cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(workspace_root.as_path());
let paths = compute_allow_paths(&policy, &policy_cwd, &command_cwd, &HashMap::new());
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&HashMap::new(),
);
assert!(
paths
.allow
.contains(&dunce::canonicalize(&policy_cwd).unwrap())
.contains(&dunce::canonicalize(&workspace_root).unwrap())
);
assert!(
!paths
@@ -127,17 +160,22 @@ mod tests {
let _ = fs::create_dir_all(&command_cwd);
let _ = fs::create_dir_all(&temp_dir);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ false,
);
let mut env_map = HashMap::new();
env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string());
env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string());
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map);
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&env_map,
);
assert!(
paths
@@ -160,17 +198,22 @@ mod tests {
let _ = fs::create_dir_all(&command_cwd);
let _ = fs::create_dir_all(&temp_dir);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
);
let mut env_map = HashMap::new();
env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string());
env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string());
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map);
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&env_map,
);
let expected_allow: HashSet<PathBuf> = [
dunce::canonicalize(&command_cwd).unwrap(),
@@ -189,14 +232,19 @@ mod tests {
let command_cwd = tmp.path().join("workspace");
let _ = fs::create_dir_all(&command_cwd);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ false,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&HashMap::new(),
);
let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
.into_iter()
.collect();
@@ -212,14 +260,19 @@ mod tests {
let git_dir = command_cwd.join(".git");
let _ = fs::create_dir_all(&git_dir);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ false,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&HashMap::new(),
);
let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
.into_iter()
.collect();
@@ -239,14 +292,19 @@ mod tests {
let _ = fs::create_dir_all(&command_cwd);
let _ = fs::write(&git_file, "gitdir: .git/worktrees/example");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ false,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&HashMap::new(),
);
let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
.into_iter()
.collect();
@@ -267,14 +325,19 @@ mod tests {
let _ = fs::create_dir_all(&codex_dir);
let _ = fs::create_dir_all(&agents_dir);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ false,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&HashMap::new(),
);
let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
.into_iter()
.collect();
@@ -295,14 +358,19 @@ mod tests {
let command_cwd = tmp.path().join("workspace");
let _ = fs::create_dir_all(&command_cwd);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ false,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
let paths = compute_allow_paths(
&permission_profile,
workspace_roots.as_slice(),
&command_cwd,
&HashMap::new(),
);
assert_eq!(paths.allow.len(), 1);
assert!(
paths.deny.is_empty(),
-19
View File
@@ -7,7 +7,6 @@ use crate::cap::workspace_write_root_contains_path;
use crate::logging::debug_log;
use crate::logging::log_note;
use crate::path_normalization::canonical_path_key;
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup::effective_write_roots_for_permissions;
use crate::token::LocalSid;
@@ -218,24 +217,6 @@ pub fn audit_everyone_writable(
Ok(Vec::new())
}
pub fn apply_world_writable_scan_and_denies(
codex_home: &Path,
cwd: &Path,
env_map: &std::collections::HashMap<String, String>,
sandbox_policy: &SandboxPolicy,
logs_base_dir: Option<&Path>,
) -> Result<()> {
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(sandbox_policy, cwd);
apply_world_writable_scan_and_denies_for_permissions(
codex_home,
cwd,
env_map,
&permissions,
logs_base_dir,
)
}
pub fn apply_world_writable_scan_and_denies_for_permissions(
codex_home: &Path,
cwd: &Path,
@@ -238,7 +238,8 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
hide_current_user_profile_dir(req.codex_home.as_path());
let token_mode = token_mode_for_permission_profile(
&req.permission_profile,
&req.permission_profile_cwd,
&req.workspace_roots,
&req.cwd,
&req.env,
)
.context("resolve permission profile token mode")?;
@@ -11,6 +11,7 @@ use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
@@ -25,7 +26,7 @@ use std::path::PathBuf;
const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;
/// Protocol version shared by the parent process and elevated command runner.
pub const IPC_PROTOCOL_VERSION: u8 = 2;
pub const IPC_PROTOCOL_VERSION: u8 = 3;
/// Length-prefixed, JSON-encoded frame.
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -60,7 +61,7 @@ pub struct SpawnRequest {
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub permission_profile: PermissionProfile,
pub permission_profile_cwd: PathBuf,
pub workspace_roots: Vec<AbsolutePathBuf>,
pub codex_home: PathBuf,
pub real_codex_home: PathBuf,
pub cap_sids: Vec<String>,
@@ -197,6 +198,9 @@ mod tests {
#[test]
fn spawn_request_serializes_permission_profile() {
let workspace_roots = vec![
AbsolutePathBuf::from_absolute_path("workspace").expect("absolute workspace root"),
];
let msg = FramedMessage {
version: IPC_PROTOCOL_VERSION,
message: Message::SpawnRequest {
@@ -205,7 +209,7 @@ mod tests {
cwd: PathBuf::from(r"C:\workspace"),
env: HashMap::new(),
permission_profile: PermissionProfile::read_only(),
permission_profile_cwd: PathBuf::from(r"C:\workspace"),
workspace_roots: workspace_roots.clone(),
codex_home: PathBuf::from(r"C:\codex"),
real_codex_home: PathBuf::from(r"C:\Users\codex"),
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
@@ -222,15 +226,13 @@ mod tests {
assert_eq!("managed", encoded["payload"]["permission_profile"]["type"]);
assert_eq!(None, encoded["payload"].get("policy_json_or_preset"));
assert_eq!(None, encoded["payload"].get("sandbox_policy_cwd"));
assert_eq!(None, encoded["payload"].get("permission_profile_cwd"));
let decoded: FramedMessage = serde_json::from_value(encoded).expect("deserialize");
let Message::SpawnRequest { payload } = decoded.message else {
panic!("unexpected message");
};
assert_eq!(PermissionProfile::read_only(), payload.permission_profile);
assert_eq!(
PathBuf::from(r"C:\workspace"),
payload.permission_profile_cwd
);
assert_eq!(workspace_roots, payload.workspace_roots);
}
}
@@ -4,26 +4,9 @@ use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
pub struct ElevatedSandboxCaptureRequest<'a> {
pub policy_json_or_preset: &'a str,
pub sandbox_policy_cwd: &'a Path,
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 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],
}
pub struct ElevatedSandboxProfileCaptureRequest<'a> {
pub permission_profile: &'a PermissionProfile,
pub permission_profile_cwd: &'a Path,
pub workspace_roots: &'a [AbsolutePathBuf],
pub codex_home: &'a Path,
pub command: Vec<String>,
pub cwd: &'a Path,
@@ -39,7 +22,6 @@ pub struct ElevatedSandboxProfileCaptureRequest<'a> {
}
mod windows_impl {
use super::ElevatedSandboxCaptureRequest;
use super::ElevatedSandboxProfileCaptureRequest;
use crate::acl::allow_null_device;
use crate::cap::load_or_create_cap_sids;
@@ -56,7 +38,6 @@ mod windows_impl {
use crate::logging::log_failure;
use crate::logging::log_start;
use crate::logging::log_success;
use crate::policy::parse_policy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::spawn_runner_transport;
use crate::sandbox_utils::ensure_codex_home_exists;
@@ -64,7 +45,6 @@ mod windows_impl {
use crate::setup::effective_write_roots_for_permissions;
use crate::token::LocalSid;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
@@ -77,7 +57,7 @@ mod windows_impl {
) -> Result<CaptureResult> {
let ElevatedSandboxProfileCaptureRequest {
permission_profile,
permission_profile_cwd,
workspace_roots,
codex_home,
command,
cwd,
@@ -91,10 +71,11 @@ mod windows_impl {
deny_read_paths_override,
deny_write_paths_override,
} = request;
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd(
permission_profile,
permission_profile_cwd,
)?;
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)
@@ -159,7 +140,7 @@ mod windows_impl {
cwd: cwd.to_path_buf(),
env: env_map.clone(),
permission_profile: permission_profile.clone(),
permission_profile_cwd: permission_profile_cwd.to_path_buf(),
workspace_roots: workspace_roots.to_vec(),
codex_home: sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids,
@@ -218,87 +199,13 @@ mod windows_impl {
})
})()
}
/// Legacy policy-string adapter for callers that have not moved to permission profiles yet.
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
request: ElevatedSandboxCaptureRequest<'_>,
) -> Result<CaptureResult> {
let ElevatedSandboxCaptureRequest {
policy_json_or_preset,
sandbox_policy_cwd,
codex_home,
command,
cwd,
env_map,
timeout_ms,
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 policy = parse_policy(policy_json_or_preset)?;
let permission_profile =
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd);
run_windows_sandbox_capture_for_permission_profile(ElevatedSandboxProfileCaptureRequest {
permission_profile: &permission_profile,
permission_profile_cwd: sandbox_policy_cwd,
codex_home,
command,
cwd,
env_map,
timeout_ms,
use_private_desktop,
proxy_enforced,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
deny_read_paths_override,
deny_write_paths_override,
})
}
#[cfg(test)]
mod tests {
use crate::policy::SandboxPolicy;
fn workspace_policy(network_access: bool) -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}
}
#[test]
fn applies_network_block_when_access_is_disabled() {
assert!(!workspace_policy(/*network_access*/ false).has_full_network_access());
}
#[test]
fn skips_network_block_when_access_is_allowed() {
assert!(workspace_policy(/*network_access*/ true).has_full_network_access());
}
#[test]
fn applies_network_block_for_read_only() {
assert!(!SandboxPolicy::new_read_only_policy().has_full_network_access());
}
}
}
#[cfg(target_os = "windows")]
pub use windows_impl::run_windows_sandbox_capture;
#[cfg(target_os = "windows")]
pub use windows_impl::run_windows_sandbox_capture_for_permission_profile;
#[cfg(not(target_os = "windows"))]
mod stub {
use super::ElevatedSandboxCaptureRequest;
use super::ElevatedSandboxProfileCaptureRequest;
use anyhow::Result;
use anyhow::bail;
@@ -311,14 +218,6 @@ mod stub {
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(
_request: ElevatedSandboxCaptureRequest<'_>,
) -> Result<CaptureResult> {
bail!("Windows sandbox is only available on Windows")
}
/// 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(
@@ -328,7 +227,5 @@ mod stub {
}
}
#[cfg(not(target_os = "windows"))]
pub use stub::run_windows_sandbox_capture;
#[cfg(not(target_os = "windows"))]
pub use stub::run_windows_sandbox_capture_for_permission_profile;
+76 -83
View File
@@ -34,8 +34,6 @@ mod logging;
#[cfg(target_os = "windows")]
mod path_normalization;
#[cfg(target_os = "windows")]
mod policy;
#[cfg(target_os = "windows")]
mod process;
#[cfg(target_os = "windows")]
mod resolved_permissions;
@@ -106,8 +104,6 @@ pub use acl::fetch_dacl_handle;
#[cfg(target_os = "windows")]
pub use acl::path_mask_allows;
#[cfg(target_os = "windows")]
pub use audit::apply_world_writable_scan_and_denies;
#[cfg(target_os = "windows")]
pub use audit::apply_world_writable_scan_and_denies_for_permissions;
#[cfg(target_os = "windows")]
pub use cap::load_or_create_cap_sids;
@@ -137,12 +133,8 @@ pub use dpapi::protect as dpapi_protect;
#[cfg(target_os = "windows")]
pub use dpapi::unprotect as dpapi_unprotect;
#[cfg(target_os = "windows")]
pub use elevated_impl::ElevatedSandboxCaptureRequest;
#[cfg(target_os = "windows")]
pub use elevated_impl::ElevatedSandboxProfileCaptureRequest;
#[cfg(target_os = "windows")]
pub use elevated_impl::run_windows_sandbox_capture as run_windows_sandbox_capture_elevated;
#[cfg(target_os = "windows")]
pub use elevated_impl::run_windows_sandbox_capture_for_permission_profile as run_windows_sandbox_capture_for_permission_profile_elevated;
#[cfg(target_os = "windows")]
pub use helper_materialization::resolve_current_exe_for_launch;
@@ -189,10 +181,6 @@ pub use logging::log_note;
#[cfg(target_os = "windows")]
pub use path_normalization::canonicalize_path;
#[cfg(target_os = "windows")]
pub use policy::SandboxPolicy;
#[cfg(target_os = "windows")]
pub use policy::parse_policy;
#[cfg(target_os = "windows")]
pub use process::PipeSpawnHandles;
#[cfg(target_os = "windows")]
pub use process::StderrMode;
@@ -260,8 +248,6 @@ pub use token::create_workspace_write_token_with_caps_from;
#[cfg(target_os = "windows")]
pub use token::get_current_token_for_restriction;
#[cfg(target_os = "windows")]
pub use unified_exec::spawn_windows_sandbox_session_elevated;
#[cfg(target_os = "windows")]
pub use unified_exec::spawn_windows_sandbox_session_elevated_for_permission_profile;
#[cfg(target_os = "windows")]
pub use unified_exec::spawn_windows_sandbox_session_legacy;
@@ -289,8 +275,6 @@ pub use workspace_acl::is_command_cwd_root;
#[cfg(not(target_os = "windows"))]
pub use stub::CaptureResult;
#[cfg(not(target_os = "windows"))]
pub use stub::apply_world_writable_scan_and_denies;
#[cfg(not(target_os = "windows"))]
pub use stub::run_windows_sandbox_capture;
#[cfg(not(target_os = "windows"))]
pub use stub::run_windows_sandbox_legacy_preflight;
@@ -299,9 +283,7 @@ pub use stub::run_windows_sandbox_legacy_preflight;
mod windows_impl {
use super::logging::log_failure;
use super::logging::log_success;
use super::policy::SandboxPolicy;
use super::process::create_process_as_user;
use super::resolved_permissions::ResolvedWindowsSandboxPermissions;
use super::sandbox_utils::ensure_codex_home_exists;
use super::spawn_prep::LegacyAclSids;
use super::spawn_prep::SpawnPrepOptions;
@@ -312,6 +294,7 @@ mod windows_impl {
use super::spawn_prep::prepare_legacy_spawn_context;
use super::spawn_prep::root_capability_sids;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::io;
@@ -366,8 +349,8 @@ mod windows_impl {
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -376,8 +359,8 @@ mod windows_impl {
use_private_desktop: bool,
) -> Result<CaptureResult> {
run_windows_sandbox_capture_with_filesystem_overrides(
policy_json_or_preset,
sandbox_policy_cwd,
permission_profile,
workspace_roots,
codex_home,
command,
cwd,
@@ -391,8 +374,8 @@ mod windows_impl {
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture_with_filesystem_overrides(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -411,8 +394,8 @@ mod windows_impl {
.map(AbsolutePathBuf::to_path_buf)
.collect::<Vec<_>>();
let common = prepare_legacy_spawn_context(
policy_json_or_preset,
sandbox_policy_cwd,
permission_profile,
workspace_roots,
codex_home,
cwd,
&mut env_map,
@@ -422,12 +405,11 @@ mod windows_impl {
add_git_safe_directory: false,
},
)?;
let policy = common.policy;
let permissions = common.permissions;
let current_dir = common.current_dir;
let logs_base_dir = common.logs_base_dir.as_deref();
let uses_write_capabilities = common.uses_write_capabilities;
if !policy.has_full_disk_read_access() {
if !permissions.has_full_disk_read_access() {
anyhow::bail!(
"Restricted read-only access requires the elevated Windows sandbox backend"
);
@@ -437,14 +419,14 @@ mod windows_impl {
if !additional_deny_read_paths.is_empty() {
anyhow::bail!("deny-read overrides require the elevated Windows sandbox backend");
}
let capability_roots = legacy_session_capability_roots(
&policy,
sandbox_policy_cwd,
&current_dir,
&env_map,
let capability_roots =
legacy_session_capability_roots(&permissions, &current_dir, &env_map, codex_home);
let security = prepare_legacy_session_security(
uses_write_capabilities,
codex_home,
);
let security = prepare_legacy_session_security(&policy, codex_home, cwd, capability_roots)?;
cwd,
capability_roots,
)?;
allow_null_device_for_workspace_write(uses_write_capabilities);
apply_legacy_session_acl_rules(
&permissions,
@@ -591,32 +573,29 @@ mod windows_impl {
}
pub fn run_windows_sandbox_legacy_preflight(
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Result<()> {
let is_workspace_write = matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. });
if !is_workspace_write {
let Ok(permissions) = super::resolved_permissions::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
) else {
return Ok(());
};
if !permissions.uses_write_capabilities_for_cwd(cwd, env_map) {
return Ok(());
}
ensure_codex_home_exists(codex_home)?;
let current_dir = cwd.to_path_buf();
let capability_roots = legacy_session_capability_roots(
sandbox_policy,
sandbox_policy_cwd,
&current_dir,
env_map,
codex_home,
);
let capability_roots =
legacy_session_capability_roots(&permissions, &current_dir, env_map, codex_home);
let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?;
apply_legacy_session_acl_rules(
&ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
sandbox_policy,
sandbox_policy_cwd,
),
&permissions,
codex_home,
&current_dir,
env_map,
@@ -634,43 +613,66 @@ mod windows_impl {
#[cfg(test)]
mod tests {
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use std::collections::HashMap;
use std::path::Path;
fn workspace_policy(network_access: bool) -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}
fn workspace_profile(network_policy: NetworkSandboxPolicy) -> PermissionProfile {
PermissionProfile::workspace_write_with(
&[],
network_policy,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
)
}
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new("."))
.should_apply_network_block()
fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool {
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
&[],
)
.expect("managed permissions")
.should_apply_network_block()
}
#[test]
fn applies_network_block_when_access_is_disabled() {
assert!(should_apply_network_block(&workspace_policy(
/*network_access*/ false
assert!(should_apply_network_block(&workspace_profile(
NetworkSandboxPolicy::Restricted
)));
}
#[test]
fn skips_network_block_when_access_is_allowed() {
assert!(!should_apply_network_block(&workspace_policy(
/*network_access*/ true
assert!(!should_apply_network_block(&workspace_profile(
NetworkSandboxPolicy::Enabled
)));
}
#[test]
fn applies_network_block_for_read_only() {
assert!(should_apply_network_block(
&SandboxPolicy::new_read_only_policy()
));
assert!(should_apply_network_block(&PermissionProfile::read_only()));
}
#[test]
fn legacy_preflight_skips_profiles_without_managed_filesystem_permissions() {
for permission_profile in [
PermissionProfile::Disabled,
PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
},
] {
super::run_windows_sandbox_legacy_preflight(
&permission_profile,
&[],
Path::new("."),
Path::new("."),
&HashMap::new(),
)
.expect("unsupported profiles do not need ACL preflight");
}
}
}
}
@@ -679,7 +681,8 @@ mod windows_impl {
mod stub {
use anyhow::Result;
use anyhow::bail;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
@@ -693,8 +696,8 @@ mod stub {
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
_policy_json_or_preset: &str,
_sandbox_policy_cwd: &Path,
_permission_profile: &PermissionProfile,
_workspace_roots: &[AbsolutePathBuf],
_codex_home: &Path,
_command: Vec<String>,
_cwd: &Path,
@@ -705,19 +708,9 @@ mod stub {
bail!("Windows sandbox is only available on Windows")
}
pub fn apply_world_writable_scan_and_denies(
_codex_home: &Path,
_cwd: &Path,
_env_map: &HashMap<String, String>,
_sandbox_policy: &SandboxPolicy,
_logs_base_dir: Option<&Path>,
) -> Result<()> {
bail!("Windows sandbox is only available on Windows")
}
pub fn run_windows_sandbox_legacy_preflight(
_sandbox_policy: &SandboxPolicy,
_sandbox_policy_cwd: &Path,
_permission_profile: &PermissionProfile,
_workspace_roots: &[AbsolutePathBuf],
_codex_home: &Path,
_cwd: &Path,
_env_map: &HashMap<String, String>,
-61
View File
@@ -1,61 +0,0 @@
use anyhow::Result;
pub use codex_protocol::protocol::SandboxPolicy;
pub fn parse_policy(value: &str) -> Result<SandboxPolicy> {
match value {
"read-only" => Ok(SandboxPolicy::new_read_only_policy()),
"workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()),
"danger-full-access" | "external-sandbox" => {
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
}
other => {
let parsed: SandboxPolicy = serde_json::from_str(other)?;
if matches!(
parsed,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
anyhow::bail!(
"DangerFullAccess and ExternalSandbox are not supported for sandboxing"
);
}
Ok(parsed)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn rejects_external_sandbox_preset() {
let err = parse_policy("external-sandbox").unwrap_err();
assert!(
err.to_string()
.contains("DangerFullAccess and ExternalSandbox are not supported")
);
}
#[test]
fn rejects_external_sandbox_json() {
let payload =
serde_json::to_string(&codex_protocol::protocol::SandboxPolicy::ExternalSandbox {
network_access: codex_protocol::protocol::NetworkAccess::Enabled,
})
.unwrap();
let err = parse_policy(&payload).unwrap_err();
assert!(
err.to_string()
.contains("DangerFullAccess and ExternalSandbox are not supported")
);
}
#[test]
fn parses_read_only_policy() {
assert_eq!(
parse_policy("read-only").unwrap(),
SandboxPolicy::new_read_only_policy()
);
}
}
@@ -5,7 +5,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
@@ -38,13 +37,15 @@ pub enum WindowsSandboxTokenMode {
/// Chooses the restricted-token family needed for a managed permission profile.
pub fn token_mode_for_permission_profile(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Result<WindowsSandboxTokenMode> {
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd(
permission_profile,
cwd,
)?;
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)?;
if permissions.file_system.has_full_disk_write_access() {
anyhow::bail!(
"permission profile requests full-disk filesystem writes, which cannot be enforced by the Windows sandbox"
@@ -58,14 +59,6 @@ pub fn token_mode_for_permission_profile(
}
impl ResolvedWindowsSandboxPermissions {
pub fn from_legacy_policy_for_cwd(policy: &SandboxPolicy, cwd: &Path) -> Self {
Self {
file_system: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(policy, cwd)
.materialize_project_roots_with_cwd(cwd),
network: NetworkSandboxPolicy::from(policy),
}
}
pub fn try_from_permission_profile(permission_profile: &PermissionProfile) -> Result<Self> {
if !matches!(permission_profile, PermissionProfile::Managed { .. }) {
anyhow::bail!(
@@ -85,15 +78,15 @@ impl ResolvedWindowsSandboxPermissions {
}
/// Resolves a managed permission profile and binds symbolic `:workspace_roots`
/// entries to the permission root supplied by the caller.
pub fn try_from_permission_profile_for_cwd(
/// entries to the workspace roots supplied by the caller.
pub fn try_from_permission_profile_for_workspace_roots(
permission_profile: &PermissionProfile,
cwd: &Path,
workspace_roots: &[AbsolutePathBuf],
) -> Result<Self> {
let mut permissions = Self::try_from_permission_profile(permission_profile)?;
permissions.file_system = permissions
.file_system
.materialize_project_roots_with_cwd(cwd);
.materialize_project_roots_with_workspace_roots(workspace_roots);
Ok(permissions)
}
@@ -211,9 +204,14 @@ mod tests {
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::project_roots_glob_pattern;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
#[test]
fn permission_profile_workspace_write_uses_windows_temp_env_vars() {
let tmp = TempDir::new().expect("tempdir");
@@ -247,38 +245,10 @@ mod tests {
}
#[test]
fn legacy_workspace_root_stays_bound_to_policy_cwd() {
fn permission_profile_workspace_root_uses_runtime_workspace_roots() {
let tmp = TempDir::new().expect("tempdir");
let policy_cwd = tmp.path().join("workspace");
let command_cwd = policy_cwd.join("subdir");
std::fs::create_dir_all(&command_cwd).expect("create command cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, &policy_cwd);
let roots = permissions
.writable_roots_for_cwd(&command_cwd, &HashMap::new())
.into_iter()
.map(|root| root.root)
.collect::<Vec<_>>();
assert_eq!(
roots,
vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")]
);
}
#[test]
fn permission_profile_workspace_root_stays_bound_to_profile_cwd() {
let tmp = TempDir::new().expect("tempdir");
let profile_cwd = tmp.path().join("workspace");
let command_cwd = profile_cwd.join("subdir");
let workspace_root = tmp.path().join("workspace");
let command_cwd = workspace_root.join("subdir");
std::fs::create_dir_all(&command_cwd).expect("create command cwd");
let permission_profile = PermissionProfile::Managed {
@@ -293,11 +263,13 @@ mod tests {
},
network: NetworkSandboxPolicy::Restricted,
};
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd(
&permission_profile,
&profile_cwd,
)
.expect("managed permission profile");
let workspace_roots = workspace_roots_for(workspace_root.as_path());
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
workspace_roots.as_slice(),
)
.expect("managed permission profile");
let roots = permissions
.writable_roots_for_cwd(&command_cwd, &HashMap::new())
@@ -307,7 +279,101 @@ mod tests {
assert_eq!(
roots,
vec![dunce::canonicalize(&profile_cwd).expect("canonical profile cwd")]
vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")]
);
}
#[test]
fn permission_profile_workspace_roots_expand_all_runtime_workspace_roots() {
let tmp = TempDir::new().expect("tempdir");
let first = AbsolutePathBuf::from_absolute_path(tmp.path().join("first"))
.expect("absolute first root");
let second = AbsolutePathBuf::from_absolute_path(tmp.path().join("second"))
.expect("absolute second root");
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(Some(".git".into())),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
},
access: FileSystemAccessMode::Deny,
},
],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
&[first.clone(), second.clone()],
)
.expect("managed permission profile");
assert_eq!(
permissions.file_system,
FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: first.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: second.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: first.join(".git"),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: second.join(".git"),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(
"**/*.env",
first.as_path(),
)
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(
"**/*.env",
second.as_path(),
)
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::Deny,
},
])
);
}
@@ -316,9 +382,11 @@ mod tests {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let workspace_roots = workspace_roots_for(cwd.as_path());
let token_mode = token_mode_for_permission_profile(
&PermissionProfile::read_only(),
workspace_roots.as_slice(),
&cwd,
&HashMap::new(),
)
@@ -332,9 +400,11 @@ mod tests {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let workspace_roots = workspace_roots_for(cwd.as_path());
let token_mode = token_mode_for_permission_profile(
&PermissionProfile::workspace_write(),
workspace_roots.as_slice(),
&cwd,
&HashMap::new(),
)
@@ -390,9 +460,15 @@ mod tests {
},
network: NetworkSandboxPolicy::Restricted,
};
let workspace_roots = workspace_roots_for(cwd.as_path());
let err = token_mode_for_permission_profile(&permission_profile, &cwd, &HashMap::new())
.expect_err("full disk writes should not resolve to a token mode");
let err = token_mode_for_permission_profile(
&permission_profile,
workspace_roots.as_slice(),
&cwd,
&HashMap::new(),
)
.expect_err("full disk writes should not resolve to a token mode");
assert!(
err.to_string()
+137 -77
View File
@@ -17,7 +17,6 @@ use crate::helper_materialization::helper_bin_dir;
use crate::logging::log_note;
use crate::path_normalization::canonical_path_key;
use crate::path_normalization::canonicalize_path;
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup_error::SetupErrorCode;
use crate::setup_error::SetupFailure;
@@ -30,6 +29,8 @@ use anyhow::Result;
use anyhow::anyhow;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::GetLastError;
@@ -104,21 +105,21 @@ pub struct SetupRootOverrides {
}
pub fn run_setup_refresh(
policy: &SandboxPolicy,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
proxy_enforced: bool,
) -> Result<()> {
if matches!(
policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
let Ok(permissions) =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)
else {
return Ok(());
}
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
};
run_setup_refresh_inner(
SandboxSetupRequest {
permissions: &permissions,
@@ -139,22 +140,22 @@ pub fn run_setup_refresh_with_overrides(
}
pub fn run_setup_refresh_with_extra_read_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
extra_read_roots: Vec<PathBuf>,
proxy_enforced: bool,
) -> Result<()> {
if matches!(
policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
let Ok(permissions) =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)
else {
return Ok(());
}
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
};
let mut read_roots = gather_read_roots(command_cwd, &permissions, env_map, codex_home);
read_roots.extend(extra_read_roots);
run_setup_refresh_inner(
@@ -1020,13 +1021,15 @@ mod tests {
use crate::helper_materialization::BIN_DIRNAME;
use crate::helper_materialization::RESOURCES_DIRNAME;
use crate::helper_materialization::helper_bin_dir;
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use tempfile::TempDir;
@@ -1038,10 +1041,67 @@ mod tests {
}
fn permissions_for(
policy: &SandboxPolicy,
policy_cwd: &std::path::Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
) -> ResolvedWindowsSandboxPermissions {
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd)
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)
.expect("managed permission profile")
}
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
fn workspace_write_profile(
writable_roots: &[AbsolutePathBuf],
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
) -> PermissionProfile {
PermissionProfile::workspace_write_with(
writable_roots,
NetworkSandboxPolicy::Restricted,
exclude_tmpdir_env_var,
exclude_slash_tmp,
)
}
#[test]
fn setup_refresh_skips_profiles_without_managed_filesystem_permissions() {
let tmp = TempDir::new().expect("tempdir");
let command_cwd = tmp.path().join("workspace");
let codex_home = tmp.path().join("codex-home");
fs::create_dir_all(&command_cwd).expect("create workspace");
for permission_profile in [
PermissionProfile::Disabled,
PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
},
] {
super::run_setup_refresh(
&permission_profile,
command_cwd.as_path(),
command_cwd.as_path(),
&HashMap::new(),
codex_home.as_path(),
/*proxy_enforced*/ false,
)
.expect("unsupported profiles do not need setup refresh");
super::run_setup_refresh_with_extra_read_roots(
&permission_profile,
command_cwd.as_path(),
command_cwd.as_path(),
&HashMap::new(),
codex_home.as_path(),
vec![command_cwd.clone()],
/*proxy_enforced*/ false,
)
.expect("unsupported profiles do not need setup refresh");
}
}
#[test]
@@ -1361,8 +1421,9 @@ mod tests {
let codex_home = tmp.path().join("codex-home");
let command_cwd = tmp.path().join("workspace");
fs::create_dir_all(&command_cwd).expect("create workspace");
let policy = SandboxPolicy::new_read_only_policy();
let permissions = permissions_for(&policy, &command_cwd);
let permission_profile = PermissionProfile::read_only();
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let roots = gather_read_roots(&command_cwd, &permissions, &HashMap::new(), &codex_home);
let expected =
@@ -1379,16 +1440,16 @@ mod tests {
let writable_root = tmp.path().join("extra-write-root");
fs::create_dir_all(&command_cwd).expect("create workspace");
fs::create_dir_all(&writable_root).expect("create writable root");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![
AbsolutePathBuf::from_absolute_path(&writable_root)
.expect("absolute writable root"),
],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions = permissions_for(&policy, &command_cwd);
let writable_roots = vec![
AbsolutePathBuf::from_absolute_path(&writable_root).expect("absolute writable root"),
];
let permission_profile = workspace_write_profile(
&writable_roots,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let roots = gather_read_roots(&command_cwd, &permissions, &HashMap::new(), &codex_home);
let expected_writable =
@@ -1401,16 +1462,15 @@ mod tests {
fn build_payload_roots_preserves_helper_roots_when_read_override_is_provided() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let policy_cwd = tmp.path().join("policy-cwd");
let workspace_root = tmp.path().join("workspace-root");
let command_cwd = tmp.path().join("workspace");
let readable_root = tmp.path().join("docs");
fs::create_dir_all(&policy_cwd).expect("create policy cwd");
fs::create_dir_all(&workspace_root).expect("create workspace root");
fs::create_dir_all(&command_cwd).expect("create workspace");
fs::create_dir_all(&readable_root).expect("create readable root");
let policy = SandboxPolicy::ReadOnly {
network_access: false,
};
let permissions = permissions_for(&policy, &policy_cwd);
let permission_profile = PermissionProfile::read_only();
let workspace_roots = workspace_roots_for(workspace_root.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let (read_roots, write_roots) = build_payload_roots(
&super::SandboxSetupRequest {
@@ -1449,16 +1509,15 @@ mod tests {
fn build_payload_roots_replaces_full_read_policy_when_read_override_is_provided() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let policy_cwd = tmp.path().join("policy-cwd");
let workspace_root = tmp.path().join("workspace-root");
let command_cwd = tmp.path().join("workspace");
let readable_root = tmp.path().join("docs");
fs::create_dir_all(&policy_cwd).expect("create policy cwd");
fs::create_dir_all(&workspace_root).expect("create workspace root");
fs::create_dir_all(&command_cwd).expect("create workspace");
fs::create_dir_all(&readable_root).expect("create readable root");
let policy = SandboxPolicy::ReadOnly {
network_access: false,
};
let permissions = permissions_for(&policy, &policy_cwd);
let permission_profile = PermissionProfile::read_only();
let workspace_roots = workspace_roots_for(workspace_root.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let (read_roots, write_roots) = build_payload_roots(
&super::SandboxSetupRequest {
@@ -1504,13 +1563,13 @@ mod tests {
fs::create_dir_all(&command_cwd).expect("create workspace");
fs::create_dir_all(&extra_root).expect("create extra root");
fs::create_dir_all(&sandbox_root).expect("create sandbox root");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions = permissions_for(&policy, &command_cwd);
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let override_roots = vec![
command_cwd.clone(),
extra_root.clone(),
@@ -1553,21 +1612,21 @@ mod tests {
}
#[test]
fn effective_write_roots_use_policy_cwd_for_legacy_workspace_root() {
fn effective_write_roots_use_runtime_workspace_roots_for_workspace_root() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let policy_cwd = tmp.path().join("workspace");
let command_cwd = policy_cwd.join("subdir");
let workspace_root = tmp.path().join("workspace");
let command_cwd = workspace_root.join("subdir");
fs::create_dir_all(&codex_home).expect("create codex home");
fs::create_dir_all(&command_cwd).expect("create command cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions = permissions_for(&policy, &policy_cwd);
let permission_profile = workspace_write_profile(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(workspace_root.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let effective_write_roots = super::effective_write_roots_for_setup(
&permissions,
@@ -1579,7 +1638,7 @@ mod tests {
assert_eq!(
effective_write_roots,
vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")]
vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")]
);
}
@@ -1594,16 +1653,16 @@ mod tests {
let explicit_deny = tmp.path().join("explicit-deny");
fs::create_dir_all(&command_git).expect("create command .git");
fs::create_dir_all(&extra_codex).expect("create extra .codex");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![
AbsolutePathBuf::from_absolute_path(&extra_write_root)
.expect("absolute writable root"),
],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions = permissions_for(&policy, &command_cwd);
let writable_roots = vec![
AbsolutePathBuf::from_absolute_path(&extra_write_root).expect("absolute writable root"),
];
let permission_profile = workspace_write_profile(
&writable_roots,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let request = super::SandboxSetupRequest {
permissions: &permissions,
command_cwd: &command_cwd,
@@ -1633,8 +1692,9 @@ mod tests {
let codex_home = tmp.path().join("codex-home");
let command_cwd = tmp.path().join("workspace");
fs::create_dir_all(&command_cwd).expect("create workspace");
let policy = SandboxPolicy::new_read_only_policy();
let permissions = permissions_for(&policy, &command_cwd);
let permission_profile = PermissionProfile::read_only();
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let roots = gather_full_read_roots_for_permissions(
&command_cwd,
+113 -97
View File
@@ -17,8 +17,6 @@ use crate::identity::SandboxCreds;
use crate::identity::require_logon_sandbox_creds;
use crate::logging::log_start;
use crate::path_normalization::canonicalize_path;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::sandbox_utils::ensure_codex_home_exists;
use crate::sandbox_utils::inject_git_safe_directory;
@@ -33,6 +31,8 @@ use crate::workspace_acl::protect_workspace_agents_dir;
use crate::workspace_acl::protect_workspace_codex_dir;
use anyhow::Context;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::ffi::c_void;
use std::path::Path;
@@ -41,7 +41,6 @@ use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::HANDLE;
pub(crate) struct SpawnContext {
pub(crate) policy: SandboxPolicy,
pub(crate) permissions: ResolvedWindowsSandboxPermissions,
pub(crate) current_dir: PathBuf,
pub(crate) logs_base_dir: Option<PathBuf>,
@@ -81,21 +80,19 @@ pub(crate) struct LegacyAclSids<'a> {
}
fn prepare_spawn_context_common(
policy_json_or_preset: &str,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
cwd: &Path,
env_map: &mut HashMap<String, String>,
command: &[String],
options: SpawnPrepOptions,
) -> Result<SpawnContext> {
let policy = parse_policy(policy_json_or_preset)?;
if matches!(
&policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
}
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)?;
normalize_null_device_env(env_map);
ensure_non_interactive_pager(env_map);
@@ -112,12 +109,9 @@ fn prepare_spawn_context_common(
let logs_base_dir = Some(sandbox_base);
log_start(command, logs_base_dir.as_deref());
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, policy_cwd);
let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);
Ok(SpawnContext {
policy,
permissions,
current_dir: cwd.to_path_buf(),
logs_base_dir,
@@ -126,8 +120,8 @@ fn prepare_spawn_context_common(
}
pub(crate) fn prepare_legacy_spawn_context(
policy_json_or_preset: &str,
policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
cwd: &Path,
env_map: &mut HashMap<String, String>,
@@ -135,8 +129,8 @@ pub(crate) fn prepare_legacy_spawn_context(
options: SpawnPrepOptions,
) -> Result<SpawnContext> {
let common = prepare_spawn_context_common(
policy_json_or_preset,
policy_cwd,
permission_profile,
workspace_roots,
codex_home,
cwd,
env_map,
@@ -150,38 +144,31 @@ pub(crate) fn prepare_legacy_spawn_context(
}
pub(crate) fn prepare_legacy_session_security(
policy: &SandboxPolicy,
uses_write_capabilities: bool,
codex_home: &Path,
cwd: &Path,
capability_roots: impl IntoIterator<Item = PathBuf>,
) -> Result<LegacySessionSecurity> {
let caps = load_or_create_cap_sids(codex_home)?;
let (h_token, readonly_sid, readonly_sid_str, write_root_sids) = unsafe {
match policy {
SandboxPolicy::ReadOnly { .. } => {
let psid = LocalSid::from_string(&caps.readonly)?;
let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?;
(h_token, Some(psid), Some(caps.readonly), Vec::new())
}
SandboxPolicy::WorkspaceWrite { .. } => {
let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?;
if write_root_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
let base = get_current_token_for_restriction()?;
let cap_ptrs: Vec<*mut c_void> = write_root_sids
.iter()
.map(|root| root.sid.as_ptr())
.collect();
let h_token =
create_workspace_write_token_with_caps_from(base, cap_ptrs.as_slice());
CloseHandle(base);
let h_token = h_token?;
(h_token, None, None, write_root_sids)
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
unreachable!("dangerous policies rejected before legacy session prep")
if uses_write_capabilities {
let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?;
if write_root_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
let base = get_current_token_for_restriction()?;
let cap_ptrs: Vec<*mut c_void> = write_root_sids
.iter()
.map(|root| root.sid.as_ptr())
.collect();
let h_token = create_workspace_write_token_with_caps_from(base, cap_ptrs.as_slice());
CloseHandle(base);
let h_token = h_token?;
(h_token, None, None, write_root_sids)
} else {
let psid = LocalSid::from_string(&caps.readonly)?;
let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?;
(h_token, Some(psid), Some(caps.readonly), Vec::new())
}
};
@@ -194,21 +181,18 @@ pub(crate) fn prepare_legacy_session_security(
}
pub(crate) fn legacy_session_capability_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
permissions: &ResolvedWindowsSandboxPermissions,
current_dir: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> Vec<PathBuf> {
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
let allow_paths = compute_allow_paths_for_permissions(&permissions, current_dir, env_map)
let allow_paths = compute_allow_paths_for_permissions(permissions, current_dir, env_map)
.allow
.into_iter()
.collect::<Vec<_>>();
if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) {
effective_write_roots_for_permissions(
&permissions,
permissions,
current_dir,
env_map,
codex_home,
@@ -459,7 +443,6 @@ pub(crate) fn prepare_elevated_spawn_context_for_permissions(
#[cfg(test)]
mod tests {
use super::SandboxPolicy;
use super::SpawnPrepOptions;
use super::deny_root_capabilities_for_path;
use super::legacy_session_capability_roots;
@@ -469,34 +452,56 @@ mod tests {
use crate::cap::load_or_create_cap_sids;
use crate::cap::workspace_write_cap_sid_for_root;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new("."))
.should_apply_network_block()
fn workspace_profile(
network_policy: NetworkSandboxPolicy,
writable_roots: &[AbsolutePathBuf],
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
) -> PermissionProfile {
PermissionProfile::workspace_write_with(
writable_roots,
network_policy,
exclude_tmpdir_env_var,
exclude_slash_tmp,
)
}
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool {
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
&[],
)
.expect("managed permission profile")
.should_apply_network_block()
}
#[test]
fn no_network_env_rewrite_applies_for_workspace_write() {
assert!(should_apply_network_block(
&SandboxPolicy::new_workspace_write_policy(),
&PermissionProfile::workspace_write()
));
}
#[test]
fn no_network_env_rewrite_skips_when_network_access_is_allowed() {
assert!(!should_apply_network_block(
&SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
));
assert!(!should_apply_network_block(&workspace_profile(
NetworkSandboxPolicy::Enabled,
&[],
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
)));
}
#[test]
@@ -504,10 +509,11 @@ mod tests {
let codex_home = TempDir::new().expect("tempdir");
let cwd = TempDir::new().expect("tempdir");
let mut env_map = HashMap::new();
let workspace_roots = workspace_roots_for(cwd.path());
let _context = prepare_legacy_spawn_context(
"workspace-write",
cwd.path(),
&PermissionProfile::workspace_write(),
workspace_roots.as_slice(),
codex_home.path(),
cwd.path(),
&mut env_map,
@@ -534,10 +540,11 @@ mod tests {
"HTTP_PROXY".to_string(),
"http://user.proxy:8080".to_string(),
)]);
let workspace_roots = workspace_roots_for(cwd.path());
let context = prepare_spawn_context_common(
"workspace-write",
cwd.path(),
&PermissionProfile::workspace_write(),
workspace_roots.as_slice(),
codex_home.path(),
cwd.path(),
&mut env_map,
@@ -548,7 +555,7 @@ mod tests {
},
)
.expect("preserve existing env prep");
assert_eq!(context.policy, SandboxPolicy::new_workspace_write_policy());
assert!(context.uses_write_capabilities);
assert_eq!(env_map.get("SBX_NONET_ACTIVE"), None);
assert_eq!(
@@ -558,24 +565,30 @@ mod tests {
}
#[test]
fn legacy_session_capability_roots_use_policy_cwd_for_workspace_root() {
fn legacy_session_capability_roots_use_runtime_workspace_roots_for_workspace_root() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let policy_cwd = tmp.path().join("workspace");
let command_cwd = policy_cwd.join("subdir");
let workspace_root = tmp.path().join("workspace");
let command_cwd = workspace_root.join("subdir");
std::fs::create_dir_all(&codex_home).expect("create codex home");
std::fs::create_dir_all(&command_cwd).expect("create command cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permission_profile = workspace_profile(
NetworkSandboxPolicy::Restricted,
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(workspace_root.as_path());
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
workspace_roots.as_slice(),
)
.expect("managed permission profile");
let roots = legacy_session_capability_roots(
&policy,
&policy_cwd,
&permissions,
&command_cwd,
&HashMap::new(),
&codex_home,
@@ -583,7 +596,7 @@ mod tests {
assert_eq!(
roots,
vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")]
vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")]
);
}
@@ -672,24 +685,27 @@ mod tests {
std::fs::create_dir_all(&active_root).expect("create active root");
std::fs::create_dir_all(&sandbox_root).expect("create sandbox root");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![
AbsolutePathBuf::try_from(active_root.as_path()).expect("active root"),
AbsolutePathBuf::try_from(codex_home.as_path()).expect("codex home"),
AbsolutePathBuf::try_from(sandbox_root.as_path()).expect("sandbox root"),
],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let roots = legacy_session_capability_roots(
&policy,
&workspace,
&workspace,
&HashMap::new(),
&codex_home,
let writable_roots = vec![
AbsolutePathBuf::try_from(active_root.as_path()).expect("active root"),
AbsolutePathBuf::try_from(codex_home.as_path()).expect("codex home"),
AbsolutePathBuf::try_from(sandbox_root.as_path()).expect("sandbox root"),
];
let permission_profile = workspace_profile(
NetworkSandboxPolicy::Restricted,
&writable_roots,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(workspace.as_path());
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
workspace_roots.as_slice(),
)
.expect("managed permission profile");
let roots =
legacy_session_capability_roots(&permissions, &workspace, &HashMap::new(), &codex_home);
assert!(roots.contains(&dunce::canonicalize(&workspace).expect("workspace")));
assert!(roots.contains(&dunce::canonicalize(&active_root).expect("active root")));
@@ -8,7 +8,6 @@ use crate::ipc_framed::FramedMessage;
use crate::ipc_framed::IPC_PROTOCOL_VERSION;
use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::policy::parse_policy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::spawn_runner_transport;
use crate::spawn_prep::prepare_elevated_spawn_context_for_permissions;
@@ -27,7 +26,7 @@ use tokio::sync::oneshot;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
permission_profile: &PermissionProfile,
permission_profile_cwd: &Path,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -50,10 +49,11 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
.iter()
.map(AbsolutePathBuf::to_path_buf)
.collect::<Vec<_>>();
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd(
permission_profile,
permission_profile_cwd,
)?;
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)?;
let elevated = prepare_elevated_spawn_context_for_permissions(
permissions,
codex_home,
@@ -72,7 +72,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
cwd: cwd.to_path_buf(),
env: env_map.clone(),
permission_profile: permission_profile.clone(),
permission_profile_cwd: permission_profile_cwd.to_path_buf(),
workspace_roots: workspace_roots.to_vec(),
codex_home: elevated.sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids: elevated.cap_sids.clone(),
@@ -145,44 +145,3 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
stdin_open,
))
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn spawn_windows_sandbox_session_elevated(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
read_roots_override: Option<&[PathBuf]>,
read_roots_include_platform_defaults: bool,
write_roots_override: Option<&[PathBuf]>,
deny_read_paths_override: &[AbsolutePathBuf],
deny_write_paths_override: &[AbsolutePathBuf],
tty: bool,
stdin_open: bool,
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
let policy = parse_policy(policy_json_or_preset)?;
let permission_profile =
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd);
spawn_windows_sandbox_session_elevated_for_permission_profile(
&permission_profile,
sandbox_policy_cwd,
codex_home,
command,
cwd,
env_map,
timeout_ms,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
deny_read_paths_override,
deny_write_paths_override,
tty,
stdin_open,
use_private_desktop,
)
.await
}
@@ -17,6 +17,7 @@ use crate::spawn_prep::legacy_session_capability_roots;
use crate::spawn_prep::prepare_legacy_session_security;
use crate::spawn_prep::prepare_legacy_spawn_context;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
@@ -269,8 +270,8 @@ fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize)
#[allow(clippy::too_many_arguments)]
pub(crate) async fn spawn_windows_sandbox_session_legacy(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -283,8 +284,8 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
let common = prepare_legacy_spawn_context(
policy_json_or_preset,
sandbox_policy_cwd,
permission_profile,
workspace_roots,
codex_home,
cwd,
&mut env_map,
@@ -294,7 +295,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
add_git_safe_directory: false,
},
)?;
if !common.policy.has_full_disk_read_access() {
if !common.permissions.has_full_disk_read_access() {
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
}
// WRITE_RESTRICTED tokens consult restricting SIDs only for writes, so this
@@ -307,14 +308,17 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
.map(AbsolutePathBuf::to_path_buf)
.collect::<Vec<_>>();
let capability_roots = legacy_session_capability_roots(
&common.policy,
sandbox_policy_cwd,
&common.permissions,
&common.current_dir,
&env_map,
codex_home,
);
let security =
prepare_legacy_session_security(&common.policy, codex_home, cwd, capability_roots)?;
let security = prepare_legacy_session_security(
common.uses_write_capabilities,
codex_home,
cwd,
capability_roots,
)?;
allow_null_device_for_workspace_write(common.uses_write_capabilities);
apply_legacy_session_acl_rules(
@@ -19,8 +19,8 @@ use std::path::PathBuf;
#[allow(clippy::too_many_arguments)]
pub async fn spawn_windows_sandbox_session_legacy(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -33,8 +33,8 @@ pub async fn spawn_windows_sandbox_session_legacy(
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
backends::legacy::spawn_windows_sandbox_session_legacy(
policy_json_or_preset,
sandbox_policy_cwd,
permission_profile,
workspace_roots,
codex_home,
command,
cwd,
@@ -52,7 +52,7 @@ pub async fn spawn_windows_sandbox_session_legacy(
#[allow(clippy::too_many_arguments)]
pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
permission_profile: &PermissionProfile,
permission_profile_cwd: &Path,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -69,45 +69,7 @@ pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
) -> Result<SpawnedProcess> {
backends::elevated::spawn_windows_sandbox_session_elevated_for_permission_profile(
permission_profile,
permission_profile_cwd,
codex_home,
command,
cwd,
env_map,
timeout_ms,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
deny_read_paths_override,
deny_write_paths_override,
tty,
stdin_open,
use_private_desktop,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn spawn_windows_sandbox_session_elevated(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
read_roots_override: Option<&[PathBuf]>,
read_roots_include_platform_defaults: bool,
write_roots_override: Option<&[PathBuf]>,
deny_read_paths_override: &[AbsolutePathBuf],
deny_write_paths_override: &[AbsolutePathBuf],
tty: bool,
stdin_open: bool,
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
backends::elevated::spawn_windows_sandbox_session_elevated(
policy_json_or_preset,
sandbox_policy_cwd,
workspace_roots,
codex_home,
command,
cwd,
@@ -5,6 +5,7 @@ use crate::ipc_framed::Message;
use crate::ipc_framed::decode_bytes;
use crate::ipc_framed::read_frame;
use crate::run_windows_sandbox_capture;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::ProcessDriver;
use pretty_assertions::assert_eq;
@@ -75,6 +76,14 @@ fn sandbox_log(codex_home: &Path) -> String {
.unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display()))
}
fn workspace_write_profile() -> PermissionProfile {
PermissionProfile::workspace_write()
}
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec<Message> {
let deadline = Instant::now() + Duration::from_secs(2);
loop {
@@ -149,9 +158,10 @@ fn legacy_non_tty_cmd_emits_output() {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-non-tty-cmd");
println!("cmd codex_home={}", codex_home.path().display());
let permission_profile = workspace_write_profile();
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
@@ -189,9 +199,10 @@ fn legacy_non_tty_cmd_rejects_deny_read_overrides() {
let secret_path =
AbsolutePathBuf::from_absolute_path(cwd.join("legacy-non-tty-deny-read-secret.env"))
.expect("absolute deny-read fixture path");
let permission_profile = workspace_write_profile();
let err = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
@@ -228,9 +239,10 @@ fn legacy_non_tty_powershell_emits_output() {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-non-tty-pwsh");
println!("pwsh codex_home={}", codex_home.path().display());
let permission_profile = workspace_write_profile();
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
pwsh.display().to_string(),
@@ -413,9 +425,10 @@ fn legacy_capture_powershell_emits_output() {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-capture-pwsh");
println!("capture pwsh codex_home={}", codex_home.path().display());
let permission_profile = workspace_write_profile();
let result = run_windows_sandbox_capture(
"workspace-write",
cwd.as_path(),
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
pwsh.display().to_string(),
@@ -452,9 +465,10 @@ fn legacy_tty_powershell_emits_output_and_accepts_input() {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-tty-pwsh");
println!("tty pwsh codex_home={}", codex_home.path().display());
let permission_profile = workspace_write_profile();
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
pwsh.display().to_string(),
@@ -505,9 +519,10 @@ fn legacy_tty_cmd_emits_output_and_accepts_input() {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-tty-cmd");
println!("tty cmd codex_home={}", codex_home.path().display());
let permission_profile = workspace_write_profile();
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
@@ -558,9 +573,10 @@ fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() {
"tty cmd default desktop codex_home={}",
codex_home.path().display()
);
let permission_profile = workspace_write_profile();
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),