feat(windows) start powershell in utf-8 mode (#7902)

## Summary
Adds a FeatureFlag to enforce UTF8 encoding in powershell, particularly
Windows Powershell v5. This should help address issues like #7290.

Notably, this PR does not include the ability to parse `apply_patch`
invocations within UTF8 shell commands (calls to the freeform tool
should not be impacted). I am leaving this out of scope for now. We
should address before this feature becomes Stable, but those cases are
not the default behavior at this time so we're okay for experimentation
phase. We should continue cleaning up the `apply_patch::invocation`
logic and then can handle it more cleanly.

## Testing
- [x] Adds additional testing
This commit is contained in:
Dylan Hurd
2025-12-22 09:36:44 -08:00
committed by GitHub
Unverified
parent b24b7884c7
commit 33e1d0844a
6 changed files with 251 additions and 3 deletions
+8
View File
@@ -91,6 +91,8 @@ pub enum Feature {
Tui2,
/// Enable discovery and injection of skills.
Skills,
/// Enforce UTF8 output in Powershell.
PowershellUtf8,
}
impl Feature {
@@ -386,6 +388,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::Experimental,
default_enabled: true,
},
FeatureSpec {
id: Feature::PowershellUtf8,
key: "powershell_utf8",
stage: Stage::Experimental,
default_enabled: false,
},
FeatureSpec {
id: Feature::Tui2,
key: "tui2",
+29 -2
View File
@@ -8,6 +8,30 @@ use crate::shell::detect_shell_type;
const POWERSHELL_FLAGS: &[&str] = &["-nologo", "-noprofile", "-command", "-c"];
/// Prefixed command for powershell shell calls to force UTF-8 console output.
pub(crate) const UTF8_OUTPUT_PREFIX: &str =
"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;\n";
pub(crate) fn prefix_powershell_script_with_utf8(command: &[String]) -> Vec<String> {
let Some((_, script)) = extract_powershell_command(command) else {
return command.to_vec();
};
let trimmed = script.trim_start();
let script = if trimmed.starts_with(UTF8_OUTPUT_PREFIX) {
script.to_string()
} else {
format!("{UTF8_OUTPUT_PREFIX}{script}")
};
let mut command: Vec<String> = command[..(command.len() - 1)]
.iter()
.map(std::string::ToString::to_string)
.collect();
command.push(script);
command
}
/// Extract the PowerShell script body from an invocation such as:
///
/// - ["pwsh", "-NoProfile", "-Command", "Get-ChildItem -Recurse | Select-String foo"]
@@ -22,7 +46,10 @@ pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
}
let shell = &command[0];
if detect_shell_type(&PathBuf::from(shell)) != Some(ShellType::PowerShell) {
if !matches!(
detect_shell_type(&PathBuf::from(shell)),
Some(ShellType::PowerShell)
) {
return None;
}
@@ -36,7 +63,7 @@ pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
}
if flag.eq_ignore_ascii_case("-Command") || flag.eq_ignore_ascii_case("-c") {
let script = &command[i + 1];
return Some((shell, script.as_str()));
return Some((shell, script));
}
i += 1;
}
+10
View File
@@ -5,8 +5,11 @@ Executes shell requests under the orchestrator: asks for approval when needed,
builds a CommandSpec, and runs it under the current SandboxAttempt.
*/
use crate::exec::ExecToolCallOutput;
use crate::features::Feature;
use crate::powershell::prefix_powershell_script_with_utf8;
use crate::sandboxing::SandboxPermissions;
use crate::sandboxing::execute_env;
use crate::shell::ShellType;
use crate::tools::runtimes::build_command_spec;
use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot;
use crate::tools::sandboxing::Approvable;
@@ -144,6 +147,13 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
let base_command = &req.command;
let session_shell = ctx.session.user_shell();
let command = maybe_wrap_shell_lc_with_snapshot(base_command, session_shell.as_ref());
let command = if matches!(session_shell.shell_type, ShellType::PowerShell)
&& ctx.session.features().enabled(Feature::PowershellUtf8)
{
prefix_powershell_script_with_utf8(&command)
} else {
command
};
let spec = build_command_spec(
&command,
@@ -7,7 +7,10 @@ the session manager to spawn PTYs once an ExecEnv is prepared.
use crate::error::CodexErr;
use crate::error::SandboxErr;
use crate::exec::ExecExpiration;
use crate::features::Feature;
use crate::powershell::prefix_powershell_script_with_utf8;
use crate::sandboxing::SandboxPermissions;
use crate::shell::ShellType;
use crate::tools::runtimes::build_command_spec;
use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot;
use crate::tools::sandboxing::Approvable;
@@ -165,6 +168,13 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecSession> for UnifiedExecRunt
let base_command = &req.command;
let session_shell = ctx.session.user_shell();
let command = maybe_wrap_shell_lc_with_snapshot(base_command, session_shell.as_ref());
let command = if matches!(session_shell.shell_type, ShellType::PowerShell)
&& ctx.session.features().enabled(Feature::PowershellUtf8)
{
prefix_powershell_script_with_utf8(&command)
} else {
command
};
let spec = build_command_spec(
&command,