Compare commits

...

1 Commits

Author SHA1 Message Date
David Wiesen
44d70994e7 Force no-profile PowerShell in Windows sandbox 2026-04-07 10:39:14 -07:00
5 changed files with 106 additions and 4 deletions

View File

@@ -104,6 +104,7 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
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()?;
Some((command, invocation.turn.resolve_path(params.workdir)))

View File

@@ -12,6 +12,7 @@ use crate::exec_policy::ExecApprovalRequest;
use crate::function_tool::FunctionCallError;
use crate::maybe_emit_implicit_skill_invocation;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
@@ -35,6 +36,7 @@ use crate::tools::runtimes::shell::ShellRuntime;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use crate::tools::sandboxing::ToolCtx;
use codex_features::Feature;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::ExecCommandSource;
use codex_shell_command::is_safe_command::is_known_safe_command;
@@ -133,7 +135,19 @@ impl ShellCommandHandler {
Ok(login.unwrap_or(allow_login_shell))
}
fn base_command(shell: &Shell, command: &str, use_login_shell: bool) -> Vec<String> {
fn base_command(
shell: &Shell,
command: &str,
use_login_shell: bool,
windows_sandbox_level: WindowsSandboxLevel,
) -> Vec<String> {
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<ExecParams, FunctionCallError> {
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(), &params.command, use_login_shell);
let command = Self::base_command(
shell.as_ref(),
&params.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(), &params.command, use_login_shell);
let command = Self::base_command(
shell.as_ref(),
&params.command,
use_login_shell,
invocation.turn.windows_sandbox_level,
);
!is_known_safe_command(&command)
})
.unwrap_or(true)

View File

@@ -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;

View File

@@ -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>,
shell_mode: &UnifiedExecShellMode,
allow_login_shell: bool,
windows_sandbox_level: WindowsSandboxLevel,
) -> Result<Vec<String>, 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![

View File

@@ -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)?;