From 44d70994e72a09e3358aef5c7fec0e5f7f32bb30 Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Tue, 7 Apr 2026 10:39:14 -0700 Subject: [PATCH] Force no-profile PowerShell in Windows sandbox --- codex-rs/core/src/memories/usage.rs | 1 + codex-rs/core/src/tools/handlers/shell.rs | 30 +++++++++++++-- .../core/src/tools/handlers/shell_tests.rs | 29 ++++++++++++++ .../core/src/tools/handlers/unified_exec.rs | 12 +++++- .../src/tools/handlers/unified_exec_tests.rs | 38 +++++++++++++++++++ 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/memories/usage.rs b/codex-rs/core/src/memories/usage.rs index b98a009822..5f70753de9 100644 --- a/codex-rs/core/src/memories/usage.rs +++ b/codex-rs/core/src/memories/usage.rs @@ -104,6 +104,7 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec Vec { + fn base_command( + shell: &Shell, + command: &str, + use_login_shell: bool, + windows_sandbox_level: WindowsSandboxLevel, + ) -> Vec { + let use_login_shell = if windows_sandbox_level != WindowsSandboxLevel::Disabled + && matches!(shell.shell_type, ShellType::PowerShell) + { + false + } else { + use_login_shell + }; shell.derive_exec_args(command, use_login_shell) } @@ -146,7 +160,12 @@ impl ShellCommandHandler { ) -> Result { let shell = session.user_shell(); let use_login_shell = Self::resolve_use_login_shell(params.login, allow_login_shell)?; - let command = Self::base_command(shell.as_ref(), ¶ms.command, use_login_shell); + let command = Self::base_command( + shell.as_ref(), + ¶ms.command, + use_login_shell, + turn_context.windows_sandbox_level, + ); Ok(ExecParams { command, @@ -303,7 +322,12 @@ impl ToolHandler for ShellCommandHandler { Err(_) => return true, }; let shell = invocation.session.user_shell(); - let command = Self::base_command(shell.as_ref(), ¶ms.command, use_login_shell); + let command = Self::base_command( + shell.as_ref(), + ¶ms.command, + use_login_shell, + invocation.turn.windows_sandbox_level, + ); !is_known_safe_command(&command) }) .unwrap_or(true) diff --git a/codex-rs/core/src/tools/handlers/shell_tests.rs b/codex-rs/core/src/tools/handlers/shell_tests.rs index fee47c2147..2e99e8c707 100644 --- a/codex-rs/core/src/tools/handlers/shell_tests.rs +++ b/codex-rs/core/src/tools/handlers/shell_tests.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use std::sync::Arc; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::ShellCommandToolCallParams; use pretty_assertions::assert_eq; @@ -138,6 +139,7 @@ fn shell_command_handler_respects_explicit_login_flag() { &shell, "echo login shell", /*use_login_shell*/ true, + WindowsSandboxLevel::Disabled, ); assert_eq!( login_command, @@ -148,6 +150,7 @@ fn shell_command_handler_respects_explicit_login_flag() { &shell, "echo non login shell", /*use_login_shell*/ false, + WindowsSandboxLevel::Disabled, ); assert_eq!( non_login_command, @@ -155,6 +158,32 @@ fn shell_command_handler_respects_explicit_login_flag() { ); } +#[test] +fn shell_command_handler_forces_non_login_powershell_inside_windows_sandbox() { + let shell = Shell { + shell_type: ShellType::PowerShell, + shell_path: PathBuf::from("powershell.exe"), + shell_snapshot: crate::shell::empty_shell_snapshot_receiver(), + }; + + let command = ShellCommandHandler::base_command( + &shell, + "Get-ChildItem", + /*use_login_shell*/ true, + WindowsSandboxLevel::RestrictedToken, + ); + + assert_eq!( + command, + vec![ + "powershell.exe".to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "Get-ChildItem".to_string(), + ] + ); +} + #[tokio::test] async fn shell_command_handler_defaults_to_non_login_when_disallowed() { let (session, turn_context) = make_session_and_context().await; diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 4486cd7917..0986b14e1a 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -2,6 +2,7 @@ use crate::function_tool::FunctionCallError; use crate::maybe_emit_implicit_skill_invocation; use crate::sandboxing::SandboxPermissions; use crate::shell::Shell; +use crate::shell::ShellType; use crate::shell::get_shell_by_model_provided_path; use crate::tools::context::ExecCommandToolOutput; use crate::tools::context::ToolInvocation; @@ -25,6 +26,7 @@ use crate::unified_exec::WriteStdinRequest; use codex_features::Feature; use codex_otel::SessionTelemetry; use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::TerminalInteractionEvent; @@ -113,6 +115,7 @@ impl ToolHandler for UnifiedExecHandler { invocation.session.user_shell(), &invocation.turn.tools_config.unified_exec_shell_mode, invocation.turn.tools_config.allow_login_shell, + invocation.turn.windows_sandbox_level, ) { Ok(command) => command, Err(_) => return true, @@ -198,6 +201,7 @@ impl ToolHandler for UnifiedExecHandler { session.user_shell(), &turn.tools_config.unified_exec_shell_mode, turn.tools_config.allow_login_shell, + turn.windows_sandbox_level, ) .map_err(FunctionCallError::RespondToModel)?; let command_for_display = codex_shell_command::parse_command::shlex_join(&command); @@ -377,8 +381,9 @@ pub(crate) fn get_command( session_shell: Arc, shell_mode: &UnifiedExecShellMode, allow_login_shell: bool, + windows_sandbox_level: WindowsSandboxLevel, ) -> Result, String> { - let use_login_shell = match args.login { + let mut use_login_shell = match args.login { Some(true) if !allow_login_shell => { return Err( "login shell is disabled by config; omit `login` or set it to false.".to_string(), @@ -396,6 +401,11 @@ pub(crate) fn get_command( shell }); let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref()); + if windows_sandbox_level != WindowsSandboxLevel::Disabled + && matches!(shell.shell_type, ShellType::PowerShell) + { + use_login_shell = false; + } Ok(shell.derive_exec_args(&args.cmd, use_login_shell)) } UnifiedExecShellMode::ZshFork(zsh_fork_config) => Ok(vec![ diff --git a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs index 7c40eeff1d..03940ddd98 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs @@ -33,6 +33,7 @@ fn test_get_command_uses_default_shell_when_unspecified() -> anyhow::Result<()> Arc::new(default_user_shell()), &UnifiedExecShellMode::Direct, /*allow_login_shell*/ true, + WindowsSandboxLevel::Disabled, ) .map_err(anyhow::Error::msg)?; @@ -54,6 +55,7 @@ fn test_get_command_respects_explicit_bash_shell() -> anyhow::Result<()> { Arc::new(default_user_shell()), &UnifiedExecShellMode::Direct, /*allow_login_shell*/ true, + WindowsSandboxLevel::Disabled, ) .map_err(anyhow::Error::msg)?; @@ -80,6 +82,7 @@ fn test_get_command_respects_explicit_powershell_shell() -> anyhow::Result<()> { Arc::new(default_user_shell()), &UnifiedExecShellMode::Direct, /*allow_login_shell*/ true, + WindowsSandboxLevel::Disabled, ) .map_err(anyhow::Error::msg)?; @@ -87,6 +90,38 @@ fn test_get_command_respects_explicit_powershell_shell() -> anyhow::Result<()> { Ok(()) } +#[test] +fn test_get_command_forces_non_login_powershell_inside_windows_sandbox() -> anyhow::Result<()> { + let json = r#"{"cmd": "Get-ChildItem", "login": true}"#; + + let args: ExecCommandArgs = parse_arguments(json)?; + let shell = Shell { + shell_type: ShellType::PowerShell, + shell_path: PathBuf::from("powershell.exe"), + shell_snapshot: crate::shell::empty_shell_snapshot_receiver(), + }; + + let command = get_command( + &args, + Arc::new(shell), + &UnifiedExecShellMode::Direct, + /*allow_login_shell*/ true, + WindowsSandboxLevel::Elevated, + ) + .map_err(anyhow::Error::msg)?; + + assert_eq!( + command, + vec![ + "powershell.exe".to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "Get-ChildItem".to_string(), + ] + ); + Ok(()) +} + #[test] fn test_get_command_respects_explicit_cmd_shell() -> anyhow::Result<()> { let json = r#"{"cmd": "echo hello", "shell": "cmd"}"#; @@ -100,6 +135,7 @@ fn test_get_command_respects_explicit_cmd_shell() -> anyhow::Result<()> { Arc::new(default_user_shell()), &UnifiedExecShellMode::Direct, /*allow_login_shell*/ true, + WindowsSandboxLevel::Disabled, ) .map_err(anyhow::Error::msg)?; @@ -117,6 +153,7 @@ fn test_get_command_rejects_explicit_login_when_disallowed() -> anyhow::Result<( Arc::new(default_user_shell()), &UnifiedExecShellMode::Direct, /*allow_login_shell*/ false, + WindowsSandboxLevel::Disabled, ) .expect_err("explicit login should be rejected"); @@ -150,6 +187,7 @@ fn test_get_command_ignores_explicit_shell_in_zsh_fork_mode() -> anyhow::Result< Arc::new(default_user_shell()), &shell_mode, /*allow_login_shell*/ true, + WindowsSandboxLevel::Disabled, ) .map_err(anyhow::Error::msg)?;