Compare commits

...

1 Commits

Author SHA1 Message Date
David Wiesen
76a7aa8cc1 Avoid PowerShell profiles in Windows sandbox 2026-04-07 10:39:58 -07:00
7 changed files with 78 additions and 4 deletions

View File

@@ -93,7 +93,11 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
let command = invocation
.session
.user_shell()
.derive_exec_args(&params.command, use_login_shell);
.derive_exec_args_for_windows_sandbox(
&params.command,
use_login_shell,
invocation.turn.windows_sandbox_level,
);
(command, invocation.turn.resolve_path(params.workdir))
}),
"exec_command" => serde_json::from_str::<ExecCommandArgs>(arguments)
@@ -104,6 +108,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

@@ -1,5 +1,6 @@
use crate::shell_detect::detect_shell_type;
use crate::shell_snapshot::ShellSnapshot;
use codex_protocol::config_types::WindowsSandboxLevel;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
@@ -69,6 +70,27 @@ impl Shell {
}
}
/// Like `derive_exec_args`, but disables the PowerShell profile when
/// Windows sandboxing is active. The sandbox account intentionally has a
/// narrower view of per-user tools and modules, so loading the normal user
/// profile can turn otherwise successful commands into profile failures.
pub(crate) fn derive_exec_args_for_windows_sandbox(
&self,
command: &str,
use_login_shell: bool,
windows_sandbox_level: WindowsSandboxLevel,
) -> Vec<String> {
let use_login_shell = if cfg!(target_os = "windows")
&& self.shell_type == ShellType::PowerShell
&& windows_sandbox_level != WindowsSandboxLevel::Disabled
{
false
} else {
use_login_shell
};
self.derive_exec_args(command, use_login_shell)
}
/// Return the shell snapshot if existing.
pub fn shell_snapshot(&self) -> Option<Arc<ShellSnapshot>> {
self.shell_snapshot.borrow().clone()

View File

@@ -146,6 +146,30 @@ fn derive_exec_args() {
);
}
#[test]
fn powershell_windows_sandbox_exec_args_disable_profile() {
let test_powershell_shell = Shell {
shell_type: ShellType::PowerShell,
shell_path: PathBuf::from("pwsh.exe"),
shell_snapshot: empty_shell_snapshot_receiver(),
};
let args = test_powershell_shell.derive_exec_args_for_windows_sandbox(
"echo hello",
/*use_login_shell*/ true,
WindowsSandboxLevel::Elevated,
);
if cfg!(target_os = "windows") {
assert_eq!(
args,
vec!["pwsh.exe", "-NoProfile", "-Command", "echo hello"]
);
} else {
assert_eq!(args, vec!["pwsh.exe", "-Command", "echo hello"]);
}
}
#[tokio::test]
async fn test_current_shell_detects_zsh() {
let shell = Command::new("sh")

View File

@@ -123,7 +123,11 @@ pub(crate) async fn execute_user_shell_command(
// We do not source rc files or otherwise reformat the script.
let use_login_shell = true;
let session_shell = session.user_shell();
let display_command = session_shell.derive_exec_args(&command, use_login_shell);
let display_command = session_shell.derive_exec_args_for_windows_sandbox(
&command,
use_login_shell,
turn_context.windows_sandbox_level,
);
let exec_env_map = create_env(
&turn_context.shell_environment_policy,
Some(session.conversation_id),

View File

@@ -146,7 +146,11 @@ 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 = shell.as_ref().derive_exec_args_for_windows_sandbox(
&params.command,
use_login_shell,
turn_context.windows_sandbox_level,
);
Ok(ExecParams {
command,

View File

@@ -25,6 +25,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 +114,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 +200,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,6 +380,7 @@ 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 {
Some(true) if !allow_login_shell => {
@@ -396,7 +400,11 @@ pub(crate) fn get_command(
shell
});
let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref());
Ok(shell.derive_exec_args(&args.cmd, use_login_shell))
Ok(shell.derive_exec_args_for_windows_sandbox(
&args.cmd,
use_login_shell,
windows_sandbox_level,
))
}
UnifiedExecShellMode::ZshFork(zsh_fork_config) => Ok(vec![
zsh_fork_config.shell_zsh_path.to_string_lossy().to_string(),

View File

@@ -18,6 +18,7 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::config_types::WindowsSandboxLevel;
use tokio::sync::Mutex;
#[test]
@@ -33,6 +34,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 +56,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 +83,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)?;
@@ -100,6 +104,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 +122,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 +156,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)?;