[codex] Respect Windows sandbox backend in exec policy (#26307)

## Why

Windows managed filesystem permissions can now be backed by a real
Windows sandbox. `exec-policy` was still treating the managed read-only
policy shape as if there were never a sandbox backend, so benign
unmatched commands such as PowerShell directory listings could be
rejected with `blocked by policy` even when `windows.sandbox` was
enabled.

The inverse case still needs to stay conservative: when the Windows
sandbox backend is disabled, managed filesystem restrictions are only
configuration intent, not an enforced filesystem boundary. That applies
to writable-root restricted profiles too, not just read-only profiles.

## What Changed

- Thread the effective `WindowsSandboxLevel` into exec-policy approval
decisions for shell, unified exec, and intercepted shell exec paths.
- Treat managed restricted filesystem profiles as lacking sandbox
protection only on Windows when `WindowsSandboxLevel::Disabled`.
- Exclude full-disk-write profiles from that no-backend path because
they do not rely on filesystem sandbox enforcement.
- Remove the cwd-sensitive read-only heuristic and the now-stale cwd
plumbing from exec-policy approval contexts.
- Add Windows coverage for both enabled-sandbox and disabled-backend
behavior, including a writable-root managed profile.

## Validation

- Added/updated `exec_policy` coverage for managed filesystem
restrictions, full-disk-write exclusion, enabled Windows sandbox
behavior, and disabled-backend read-only/writable-root behavior.
- `just test -p codex-core exec_policy` — 100 passed, 10 leaky
- Empirical local `codex exec` probe with `--sandbox read-only -c
'windows.sandbox="unelevated"'`: PowerShell directory listing completed
successfully.
- Disabled-backend control with Windows sandbox cleared: the same
command was rejected with `blocked by policy`.
This commit is contained in:
iceweasel-oai
2026-06-05 11:20:52 -07:00
committed by GitHub
Unverified
parent 679a944dbc
commit 82b15b65e2
9 changed files with 229 additions and 74 deletions
+16 -18
View File
@@ -20,6 +20,7 @@ use codex_execpolicy::RuleMatch;
use codex_execpolicy::blocking_append_allow_prefix_rule;
use codex_execpolicy::blocking_append_network_rule;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::protocol::AskForApproval;
@@ -120,7 +121,7 @@ pub(crate) enum ExecPolicyCommandOrigin {
pub(crate) struct UnmatchedCommandContext<'a> {
pub(crate) approval_policy: AskForApproval,
pub(crate) permission_profile: &'a PermissionProfile,
pub(crate) sandbox_cwd: &'a Path,
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) sandbox_permissions: SandboxPermissions,
pub(crate) used_complex_parsing: bool,
pub(crate) command_origin: ExecPolicyCommandOrigin,
@@ -240,7 +241,7 @@ pub(crate) struct ExecApprovalRequest<'a> {
pub(crate) command: &'a [String],
pub(crate) approval_policy: AskForApproval,
pub(crate) permission_profile: PermissionProfile,
pub(crate) sandbox_cwd: &'a Path,
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) sandbox_permissions: SandboxPermissions,
pub(crate) prefix_rule: Option<Vec<String>>,
}
@@ -274,7 +275,7 @@ impl ExecPolicyManager {
command,
approval_policy,
permission_profile,
sandbox_cwd,
windows_sandbox_level,
sandbox_permissions,
prefix_rule,
} = req;
@@ -294,7 +295,7 @@ impl ExecPolicyManager {
UnmatchedCommandContext {
approval_policy,
permission_profile: &permission_profile,
sandbox_cwd,
windows_sandbox_level,
sandbox_permissions,
used_complex_parsing,
command_origin,
@@ -631,7 +632,7 @@ pub(crate) fn render_decision_for_unmatched_command(
let UnmatchedCommandContext {
approval_policy,
permission_profile,
sandbox_cwd,
windows_sandbox_level,
sandbox_permissions,
used_complex_parsing,
command_origin,
@@ -645,15 +646,18 @@ pub(crate) fn render_decision_for_unmatched_command(
}
};
// On Windows, ReadOnly sandbox is not a real sandbox, so special-case it
// here.
let environment_lacks_sandbox_protections =
cfg!(windows) && profile_is_managed_read_only(permission_profile, sandbox_cwd);
// When the Windows sandbox backend is disabled, managed filesystem
// restrictions are only a policy shape; there is no platform sandbox to
// enforce the boundary. Keep that legacy case conservative while still
// relying on the real Windows sandbox when it is enabled.
let windows_managed_fs_restrictions_without_sandbox_backend = cfg!(windows)
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
&& profile_has_managed_filesystem_restrictions(permission_profile);
if is_known_safe
&& !used_complex_parsing
&& (approval_policy == AskForApproval::UnlessTrusted
|| environment_lacks_sandbox_protections)
|| windows_managed_fs_restrictions_without_sandbox_backend)
{
return Decision::Allow;
}
@@ -671,7 +675,7 @@ pub(crate) fn render_decision_for_unmatched_command(
codex_shell_command::is_dangerous_command::is_dangerous_powershell_words(command)
}
};
if command_is_dangerous || environment_lacks_sandbox_protections {
if command_is_dangerous || windows_managed_fs_restrictions_without_sandbox_backend {
return match approval_policy {
AskForApproval::Never => {
let sandbox_is_explicitly_disabled = matches!(
@@ -740,10 +744,7 @@ pub(crate) fn render_decision_for_unmatched_command(
}
}
fn profile_is_managed_read_only(
permission_profile: &PermissionProfile,
sandbox_cwd: &Path,
) -> bool {
fn profile_has_managed_filesystem_restrictions(permission_profile: &PermissionProfile) -> bool {
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
matches!(permission_profile, PermissionProfile::Managed { .. })
&& matches!(
@@ -751,9 +752,6 @@ fn profile_is_managed_read_only(
FileSystemSandboxKind::Restricted
)
&& !file_system_sandbox_policy.has_full_disk_write_access()
&& file_system_sandbox_policy
.get_writable_roots_with_cwd(sandbox_cwd)
.is_empty()
}
fn default_policy_path(codex_home: &Path) -> PathBuf {
+41 -23
View File
@@ -15,6 +15,7 @@ use codex_config::Sourced;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
@@ -1088,7 +1089,7 @@ fn unmatched_granular_policy_still_prompts_for_restricted_sandbox_escalation() {
mcp_elicitations: true,
}),
permission_profile: &PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
used_complex_parsing: false,
command_origin: ExecPolicyCommandOrigin::Generic,
@@ -1108,7 +1109,7 @@ fn unmatched_on_request_uses_permission_profile_file_system_policy_for_escalatio
UnmatchedCommandContext {
approval_policy: AskForApproval::OnRequest,
permission_profile: &PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
used_complex_parsing: false,
command_origin: ExecPolicyCommandOrigin::Generic,
@@ -1128,7 +1129,7 @@ fn known_safe_on_request_still_prompts_for_restricted_sandbox_escalation() {
UnmatchedCommandContext {
approval_policy: AskForApproval::OnRequest,
permission_profile: &PermissionProfile::workspace_write(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
sandbox_permissions: SandboxPermissions::RequireEscalated,
used_complex_parsing: false,
command_origin: ExecPolicyCommandOrigin::Generic,
@@ -1138,7 +1139,7 @@ fn known_safe_on_request_still_prompts_for_restricted_sandbox_escalation() {
}
#[test]
fn managed_cwd_write_profile_is_not_read_only() {
fn managed_cwd_write_profile_has_filesystem_restrictions() {
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
@@ -1158,14 +1159,13 @@ fn managed_cwd_write_profile_is_not_read_only() {
NetworkSandboxPolicy::Restricted,
);
assert!(!profile_is_managed_read_only(
&permission_profile,
Path::new("/tmp/project")
assert!(profile_has_managed_filesystem_restrictions(
&permission_profile
));
}
#[test]
fn managed_unresolvable_write_profile_is_still_read_only() {
fn managed_unresolvable_write_profile_has_filesystem_restrictions() {
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
@@ -1188,9 +1188,27 @@ fn managed_unresolvable_write_profile_is_still_read_only() {
NetworkSandboxPolicy::Restricted,
);
assert!(profile_is_managed_read_only(
&permission_profile,
Path::new("/tmp/project")
assert!(profile_has_managed_filesystem_restrictions(
&permission_profile
));
}
#[test]
fn managed_full_disk_write_profile_has_no_filesystem_restrictions() {
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
}]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
assert!(!profile_has_managed_filesystem_restrictions(
&permission_profile
));
}
@@ -1317,7 +1335,7 @@ async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision()
mcp_elicitations: true,
}),
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: None,
})
@@ -1354,7 +1372,7 @@ async fn mixed_rule_and_sandbox_prompt_rejects_when_granular_rules_are_disabled(
mcp_elicitations: true,
}),
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: None,
})
@@ -1378,7 +1396,7 @@ async fn exec_approval_requirement_falls_back_to_heuristics() {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
@@ -1403,7 +1421,7 @@ async fn empty_bash_lc_script_falls_back_to_original_command() {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
@@ -1432,7 +1450,7 @@ async fn whitespace_bash_lc_script_falls_back_to_original_command() {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
@@ -1461,7 +1479,7 @@ async fn request_rule_uses_prefix_rule() {
command: &command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
})
@@ -1493,7 +1511,7 @@ async fn request_rule_falls_back_when_prefix_rule_does_not_approve_all_commands(
command: &command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::Disabled,
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
})
@@ -1532,7 +1550,7 @@ async fn heuristics_apply_when_other_commands_match_policy() {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: PermissionProfile::Disabled,
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
@@ -2013,7 +2031,7 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
command: &sneaky_command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: permissions,
prefix_rule: None,
})
@@ -2037,7 +2055,7 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
command: &dangerous_command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: permissions,
prefix_rule: None,
})
@@ -2057,7 +2075,7 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
command: &dangerous_command,
approval_policy: AskForApproval::Never,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: permissions,
prefix_rule: None,
})
@@ -2152,7 +2170,7 @@ async fn exec_approval_requirement_for_command(
command: &command,
approval_policy,
permission_profile,
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
sandbox_permissions,
prefix_rule,
})
+85 -2
View File
@@ -1,6 +1,5 @@
use super::*;
use pretty_assertions::assert_eq;
use std::path::Path;
#[tokio::test]
async fn evaluates_powershell_inner_commands_against_prompt_rules() {
@@ -79,7 +78,7 @@ fn unmatched_safe_powershell_words_are_allowed() {
UnmatchedCommandContext {
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: &PermissionProfile::read_only(),
sandbox_cwd: Path::new("/tmp"),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
used_complex_parsing: false,
command_origin: ExecPolicyCommandOrigin::PowerShell,
@@ -88,6 +87,90 @@ fn unmatched_safe_powershell_words_are_allowed() {
);
}
#[test]
fn read_only_windows_sandbox_runs_unmatched_commands_under_sandbox() {
let command = vec!["cmd.exe".to_string(), "/c".to_string(), "dir".to_string()];
for windows_sandbox_level in [
WindowsSandboxLevel::RestrictedToken,
WindowsSandboxLevel::Elevated,
] {
assert_eq!(
Decision::Allow,
render_decision_for_unmatched_command(
&command,
UnmatchedCommandContext {
approval_policy: AskForApproval::Never,
permission_profile: &PermissionProfile::read_only(),
windows_sandbox_level,
sandbox_permissions: SandboxPermissions::UseDefault,
used_complex_parsing: false,
command_origin: ExecPolicyCommandOrigin::Generic,
},
)
);
}
}
#[test]
fn read_only_windows_policy_without_sandbox_backend_still_requires_approval() {
let command = vec!["cmd.exe".to_string(), "/c".to_string(), "dir".to_string()];
assert_eq!(
Decision::Forbidden,
render_decision_for_unmatched_command(
&command,
UnmatchedCommandContext {
approval_policy: AskForApproval::Never,
permission_profile: &PermissionProfile::read_only(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
used_complex_parsing: false,
command_origin: ExecPolicyCommandOrigin::Generic,
},
),
"command is forbidden because approval policy is never and there is no Windows sandbox to rely on"
);
}
#[test]
fn writable_windows_policy_without_sandbox_backend_still_requires_approval() {
let command = vec!["cmd.exe".to_string(), "/c".to_string(), "dir".to_string()];
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
},
]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
assert_eq!(
Decision::Forbidden,
render_decision_for_unmatched_command(
&command,
UnmatchedCommandContext {
approval_policy: AskForApproval::Never,
permission_profile: &permission_profile,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
used_complex_parsing: false,
command_origin: ExecPolicyCommandOrigin::Generic,
},
)
);
}
#[tokio::test]
async fn unmatched_dangerous_powershell_inner_commands_require_approval() {
let inner_command = vec![
+1 -2
View File
@@ -10881,8 +10881,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
command: &command,
approval_policy: turn_context.approval_policy.value(),
permission_profile: turn_context.permission_profile(),
#[allow(deprecated)]
sandbox_cwd: turn_context.cwd.as_path(),
windows_sandbox_level: turn_context.windows_sandbox_level,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
+1 -2
View File
@@ -167,8 +167,7 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
command: &exec_params.command,
approval_policy: turn.approval_policy.value(),
permission_profile: turn.permission_profile(),
#[allow(deprecated)]
sandbox_cwd: turn.cwd.as_path(),
windows_sandbox_level: turn.windows_sandbox_level,
sandbox_permissions: if effective_additional_permissions.permissions_preapproved {
codex_protocol::models::SandboxPermissions::UseDefault
} else {
@@ -65,7 +65,6 @@ use codex_shell_escalation::ShellCommandExecutor;
use codex_shell_escalation::Stopwatch;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -224,7 +223,6 @@ pub(super) async fn try_run_zsh_fork(
approval_policy: ctx.turn.approval_policy.value(),
permission_profile: command_executor.permission_profile.clone(),
file_system_sandbox_policy: command_executor.file_system_sandbox_policy.clone(),
sandbox_policy_cwd: command_executor.sandbox_policy_cwd.clone(),
sandbox_permissions: req.sandbox_permissions,
approval_sandbox_permissions,
prompt_permissions: req.additional_permissions.clone(),
@@ -297,7 +295,6 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
approval_policy: ctx.turn.approval_policy.value(),
permission_profile: exec_request.permission_profile.clone(),
file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(),
sandbox_policy_cwd: exec_request.windows_sandbox_policy_cwd.clone(),
sandbox_permissions: req.sandbox_permissions,
approval_sandbox_permissions: approval_sandbox_permissions(
req.sandbox_permissions,
@@ -332,7 +329,6 @@ struct CoreShellActionProvider {
approval_policy: AskForApproval,
permission_profile: PermissionProfile,
file_system_sandbox_policy: FileSystemSandboxPolicy,
sandbox_policy_cwd: AbsolutePathBuf,
sandbox_permissions: SandboxPermissions,
approval_sandbox_permissions: SandboxPermissions,
prompt_permissions: Option<AdditionalPermissionProfile>,
@@ -622,7 +618,7 @@ impl EscalationPolicy for CoreShellActionProvider {
InterceptedExecPolicyContext {
approval_policy: self.approval_policy,
permission_profile: self.permission_profile.clone(),
sandbox_cwd: self.sandbox_policy_cwd.as_path(),
windows_sandbox_level: self.turn.windows_sandbox_level,
sandbox_permissions: self.approval_sandbox_permissions,
enable_shell_wrapper_parsing:
ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING,
@@ -671,12 +667,12 @@ fn evaluate_intercepted_exec_policy(
policy: &Policy,
program: &AbsolutePathBuf,
argv: &[String],
context: InterceptedExecPolicyContext<'_>,
context: InterceptedExecPolicyContext,
) -> Evaluation {
let InterceptedExecPolicyContext {
approval_policy,
permission_profile,
sandbox_cwd,
windows_sandbox_level,
sandbox_permissions,
enable_shell_wrapper_parsing,
} = context;
@@ -703,7 +699,7 @@ fn evaluate_intercepted_exec_policy(
crate::exec_policy::UnmatchedCommandContext {
approval_policy,
permission_profile: &permission_profile,
sandbox_cwd,
windows_sandbox_level,
sandbox_permissions,
used_complex_parsing,
command_origin: crate::exec_policy::ExecPolicyCommandOrigin::Generic,
@@ -721,10 +717,10 @@ fn evaluate_intercepted_exec_policy(
}
#[derive(Clone)]
struct InterceptedExecPolicyContext<'a> {
struct InterceptedExecPolicyContext {
approval_policy: AskForApproval,
permission_profile: PermissionProfile,
sandbox_cwd: &'a Path,
windows_sandbox_level: WindowsSandboxLevel,
sandbox_permissions: SandboxPermissions,
enable_shell_wrapper_parsing: bool,
}
@@ -16,6 +16,7 @@ use codex_execpolicy::PolicyParser;
use codex_execpolicy::RuleMatch;
use codex_hooks::Hooks;
use codex_hooks::HooksConfig;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
@@ -456,7 +457,6 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
sandbox_policy_cwd: workdir.clone(),
sandbox_permissions: SandboxPermissions::RequireEscalated,
approval_sandbox_permissions: SandboxPermissions::RequireEscalated,
prompt_permissions: None,
@@ -508,7 +508,6 @@ fn evaluate_intercepted_exec_policy_uses_wrapper_command_when_shell_wrapper_pars
parser.parse("test.rules", policy_src).unwrap();
let policy = parser.build();
let program = AbsolutePathBuf::try_from(host_absolute_path(&["bin", "zsh"])).unwrap();
let sandbox_cwd = test_sandbox_cwd();
let enable_intercepted_exec_policy_shell_wrapper_parsing = false;
let evaluation = evaluate_intercepted_exec_policy(
@@ -522,7 +521,7 @@ fn evaluate_intercepted_exec_policy_uses_wrapper_command_when_shell_wrapper_pars
InterceptedExecPolicyContext {
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: sandbox_cwd.as_path(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
enable_shell_wrapper_parsing: enable_intercepted_exec_policy_shell_wrapper_parsing,
},
@@ -560,7 +559,6 @@ fn evaluate_intercepted_exec_policy_matches_inner_shell_commands_when_enabled()
parser.parse("test.rules", policy_src).unwrap();
let policy = parser.build();
let program = AbsolutePathBuf::try_from(host_absolute_path(&["bin", "bash"])).unwrap();
let sandbox_cwd = test_sandbox_cwd();
let enable_intercepted_exec_policy_shell_wrapper_parsing = true;
let evaluation = evaluate_intercepted_exec_policy(
@@ -574,7 +572,7 @@ fn evaluate_intercepted_exec_policy_matches_inner_shell_commands_when_enabled()
InterceptedExecPolicyContext {
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: sandbox_cwd.as_path(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
enable_shell_wrapper_parsing: enable_intercepted_exec_policy_shell_wrapper_parsing,
},
@@ -608,7 +606,6 @@ host_executable(name = "git", paths = ["{git_path_literal}"])
parser.parse("test.rules", &policy_src).unwrap();
let policy = parser.build();
let program = AbsolutePathBuf::try_from(git_path).unwrap();
let sandbox_cwd = test_sandbox_cwd();
let evaluation = evaluate_intercepted_exec_policy(
&policy,
@@ -617,7 +614,7 @@ host_executable(name = "git", paths = ["{git_path_literal}"])
InterceptedExecPolicyContext {
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: sandbox_cwd.as_path(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
enable_shell_wrapper_parsing: false,
},
@@ -670,7 +667,6 @@ prefix_rule(pattern = ["{cat_path_literal}"], decision = "allow")
approval_policy: AskForApproval::OnRequest,
permission_profile,
file_system_sandbox_policy,
sandbox_policy_cwd: workdir.clone(),
sandbox_permissions: SandboxPermissions::UseDefault,
approval_sandbox_permissions: SandboxPermissions::UseDefault,
prompt_permissions: None,
@@ -713,7 +709,6 @@ async fn denied_reads_keep_granular_sandbox_rejection_for_escalation() -> anyhow
}),
permission_profile,
file_system_sandbox_policy,
sandbox_policy_cwd: workdir.clone(),
sandbox_permissions: SandboxPermissions::RequireEscalated,
approval_sandbox_permissions: SandboxPermissions::RequireEscalated,
prompt_permissions: None,
@@ -744,7 +739,6 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default(
let argv = ["printf".to_string(), "hello".to_string()];
let approval_policy = AskForApproval::OnRequest;
let permission_profile = PermissionProfile::workspace_write();
let sandbox_cwd = test_sandbox_cwd();
let preapproved = evaluate_intercepted_exec_policy(
&policy,
@@ -753,7 +747,7 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default(
InterceptedExecPolicyContext {
approval_policy,
permission_profile: permission_profile.clone(),
sandbox_cwd: sandbox_cwd.as_path(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: super::approval_sandbox_permissions(
SandboxPermissions::WithAdditionalPermissions,
/*additional_permissions_preapproved*/ true,
@@ -768,7 +762,7 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default(
InterceptedExecPolicyContext {
approval_policy,
permission_profile,
sandbox_cwd: sandbox_cwd.as_path(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
enable_shell_wrapper_parsing: false,
},
@@ -793,7 +787,6 @@ host_executable(name = "git", paths = ["{allowed_git_literal}"])
parser.parse("test.rules", &policy_src).unwrap();
let policy = parser.build();
let program = AbsolutePathBuf::try_from(other_git.clone()).unwrap();
let sandbox_cwd = test_sandbox_cwd();
let evaluation = evaluate_intercepted_exec_policy(
&policy,
@@ -802,7 +795,7 @@ host_executable(name = "git", paths = ["{allowed_git_literal}"])
InterceptedExecPolicyContext {
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
sandbox_cwd: sandbox_cwd.as_path(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
enable_shell_wrapper_parsing: false,
},
@@ -1019,9 +1019,7 @@ impl UnifiedExecProcessManager {
command: &request.command,
approval_policy: context.turn.approval_policy.value(),
permission_profile: context.turn.permission_profile(),
// The process cwd may be model-controlled. Policy resolution
// stays anchored to the selected turn environment cwd instead.
sandbox_cwd: request.sandbox_cwd.as_path(),
windows_sandbox_level: context.turn.windows_sandbox_level,
sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault
} else {
+71
View File
@@ -87,6 +87,77 @@ fn assert_no_matched_rules_invariant(output_item: &Value) {
);
}
#[cfg(windows)]
#[tokio::test]
async fn unified_exec_disabled_windows_sandbox_rejects_managed_read_only_command() -> Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex().with_config(|config| {
config
.features
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
config
.features
.disable(Feature::WindowsSandbox)
.expect("test config should allow feature update");
config
.features
.disable(Feature::WindowsSandboxElevated)
.expect("test config should allow feature update");
config.set_windows_sandbox_enabled(false);
config.set_windows_elevated_sandbox_enabled(false);
});
let test = builder.build(&server).await?;
let call_id = "unified-exec-disabled-windows-sandbox-read-only";
let args = json!({
"cmd": "cmd.exe /c dir",
"yield_time_ms": 1_000,
});
mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-disabled-windows-sandbox-1"),
ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?),
ev_completed("resp-disabled-windows-sandbox-1"),
]),
)
.await;
let results_mock = mount_sse_once(
&server,
sse(vec![
ev_assistant_message("msg-disabled-windows-sandbox-1", "done"),
ev_completed("resp-disabled-windows-sandbox-2"),
]),
)
.await;
submit_user_turn(
&test,
"run unified exec with disabled Windows sandbox",
AskForApproval::Never,
PermissionProfile::read_only(),
None,
)
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
let output_item = results_mock.single_request().function_call_output(call_id);
let Some(output) = output_item.get("output").and_then(Value::as_str) else {
panic!("function_call_output should include string output payload: {output_item:?}");
};
assert!(
output.contains("cmd.exe /c dir") && output.contains("rejected: blocked by policy"),
"unexpected output: {output}",
);
Ok(())
}
#[tokio::test]
async fn execpolicy_blocks_shell_invocation() -> Result<()> {
let mut builder = test_codex().with_config(|config| {