mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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:
committed by
GitHub
Unverified
parent
679a944dbc
commit
82b15b65e2
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user