From 3fae2503b3b622b4aa3fc7483852a80914d72bbd Mon Sep 17 00:00:00 2001 From: Chris Bookholt Date: Wed, 1 Jul 2026 05:21:41 -0700 Subject: [PATCH] Expose trusted PowerShell parser selection by flavor --- .../shell-command/src/command_safety/mod.rs | 2 + .../src/command_safety/powershell_parser.rs | 45 ++++++++++++++-- codex-rs/shell-command/src/powershell.rs | 53 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/codex-rs/shell-command/src/command_safety/mod.rs b/codex-rs/shell-command/src/command_safety/mod.rs index 24046cdad5..8794a86b77 100644 --- a/codex-rs/shell-command/src/command_safety/mod.rs +++ b/codex-rs/shell-command/src/command_safety/mod.rs @@ -4,8 +4,10 @@ pub mod is_dangerous_command; pub mod is_safe_command; #[cfg(windows)] pub(crate) mod windows_safe_commands; +pub(crate) use powershell_parser::TrustedPowerShellFlavor; #[cfg(all(test, windows))] pub(crate) use powershell_parser::trusted_standard_pwsh_invocation_path; #[cfg(all(test, windows))] pub(crate) use powershell_parser::trusted_windows_powershell_invocation_path; pub(crate) use powershell_parser::try_parse_powershell_ast_commands; +pub(crate) use powershell_parser::try_parse_powershell_ast_commands_with_trusted_flavor; diff --git a/codex-rs/shell-command/src/command_safety/powershell_parser.rs b/codex-rs/shell-command/src/command_safety/powershell_parser.rs index 7ddf8ee688..ba05cfc472 100644 --- a/codex-rs/shell-command/src/command_safety/powershell_parser.rs +++ b/codex-rs/shell-command/src/command_safety/powershell_parser.rs @@ -55,6 +55,46 @@ pub(crate) fn try_parse_powershell_ast_commands( } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum TrustedPowerShellFlavor { + WindowsPowerShell, + PowerShell7, +} + +/// Parses a script with the authoritative machine-wide parser for the requested flavor. +/// +/// Unlike [`try_parse_powershell_ast_commands`], parser selection here is independent of a +/// runtime command's executable spelling. This lets callers inspect an untrusted runtime wrapper +/// without ever spawning that wrapper before approval. +pub(crate) fn try_parse_powershell_ast_commands_with_trusted_flavor( + flavor: TrustedPowerShellFlavor, + script: &str, +) -> Option>> { + #[cfg(windows)] + { + let parser_executable = match flavor { + TrustedPowerShellFlavor::WindowsPowerShell => trusted_powershell_invocation_path( + TrustedPowerShellRoot::System, + WINDOWS_POWERSHELL_SUFFIX, + ), + TrustedPowerShellFlavor::PowerShell7 => trusted_powershell_invocation_path( + TrustedPowerShellRoot::ProgramFiles, + WINDOWS_PWSH_SUFFIX, + ), + }?; + match parse_with_powershell_ast(&parser_executable, script) { + PowershellParseOutcome::Commands(commands) => Some(commands), + PowershellParseOutcome::Unsupported | PowershellParseOutcome::Failed => None, + } + } + + #[cfg(not(windows))] + { + let _ = (flavor, script); + None + } +} + /// Selects the host-side parser only when the command itself names that same trusted binary. /// /// The parser runs before the command approval and sandbox boundaries. Bare executable names and @@ -94,7 +134,7 @@ pub(crate) fn trusted_standard_pwsh_invocation_path() -> Option { trusted_powershell_invocation_path(TrustedPowerShellRoot::ProgramFiles, WINDOWS_PWSH_SUFFIX) } -#[cfg(all(test, windows))] +#[cfg(windows)] fn trusted_powershell_invocation_path( root_kind: TrustedPowerShellRoot, suffix: &str, @@ -102,8 +142,7 @@ fn trusted_powershell_invocation_path( let root = trusted_windows_root(root_kind).ok()?; let invocation_path = join_windows_path(&root, suffix); let invocation_path_str = invocation_path.to_str()?; - trusted_powershell_parser_executable(invocation_path_str)?; - Some(invocation_path) + trusted_powershell_parser_executable(invocation_path_str) } #[cfg(any(test, windows))] diff --git a/codex-rs/shell-command/src/powershell.rs b/codex-rs/shell-command/src/powershell.rs index 8f74f584b7..7a8d4af2d0 100644 --- a/codex-rs/shell-command/src/powershell.rs +++ b/codex-rs/shell-command/src/powershell.rs @@ -2,7 +2,9 @@ use std::path::PathBuf; use codex_utils_absolute_path::AbsolutePathBuf; +use crate::command_safety::TrustedPowerShellFlavor; use crate::command_safety::try_parse_powershell_ast_commands; +use crate::command_safety::try_parse_powershell_ast_commands_with_trusted_flavor; use crate::shell_detect::ShellType; use crate::shell_detect::detect_shell_type; @@ -82,6 +84,28 @@ pub fn parse_powershell_command_into_plain_commands( try_parse_powershell_ast_commands(executable, script) } +/// Selects which protected machine-wide PowerShell installation parses a script. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PowerShellFlavor { + WindowsPowerShell, + PowerShell7, +} + +/// Parses a PowerShell script without using the runtime wrapper as the parser executable. +/// +/// The selected parser comes from the authoritative Windows System or Program Files known folder. +/// On unsupported hosts, or when the protected parser is unavailable, this fails closed. +pub fn parse_powershell_script_with_trusted_parser( + flavor: PowerShellFlavor, + script: &str, +) -> Option>> { + let trusted_flavor = match flavor { + PowerShellFlavor::WindowsPowerShell => TrustedPowerShellFlavor::WindowsPowerShell, + PowerShellFlavor::PowerShell7 => TrustedPowerShellFlavor::PowerShell7, + }; + try_parse_powershell_ast_commands_with_trusted_flavor(trusted_flavor, script) +} + /// This function attempts to find a powershell.exe executable on the system. pub fn try_find_powershell_executable_blocking() -> Option { try_find_powershellish_executable_in_path(&["powershell.exe"]) @@ -152,10 +176,14 @@ fn is_powershellish_executable_available(powershell_or_pwsh_exe: &std::path::Pat #[cfg(test)] mod tests { + #[cfg(windows)] + use super::PowerShellFlavor; use super::UTF8_OUTPUT_PREFIX; use super::extract_powershell_command; #[cfg(windows)] use super::parse_powershell_command_into_plain_commands; + #[cfg(windows)] + use super::parse_powershell_script_with_trusted_parser; use super::prefix_powershell_script_with_utf8; #[cfg(windows)] @@ -277,4 +305,29 @@ mod tests { ] ); } + + #[cfg(windows)] + #[test] + fn parses_with_authoritative_parser_selected_independently_of_runtime_wrapper() { + assert_eq!( + parse_powershell_script_with_trusted_parser( + PowerShellFlavor::WindowsPowerShell, + "Write-Output windows", + ), + Some(vec![vec![ + "Write-Output".to_string(), + "windows".to_string(), + ]]), + ); + + if crate::command_safety::trusted_standard_pwsh_invocation_path().is_some() { + assert_eq!( + parse_powershell_script_with_trusted_parser( + PowerShellFlavor::PowerShell7, + "Write-Output core", + ), + Some(vec![vec!["Write-Output".to_string(), "core".to_string(),]]), + ); + } + } }