From e7eb7ae0b860f377f2c7f6af950ec467e1b258cd Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Tue, 19 May 2026 09:45:56 -0700 Subject: [PATCH] powershell: normalize literal comma-array args --- .../core/src/exec_policy_windows_tests.rs | 28 ++++++++ .../src/command_safety/powershell_parser.ps1 | 69 ++++++++++++++----- codex-rs/shell-command/src/powershell.rs | 26 +++++++ 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/codex-rs/core/src/exec_policy_windows_tests.rs b/codex-rs/core/src/exec_policy_windows_tests.rs index c1552f7e12..8f49c4998b 100644 --- a/codex-rs/core/src/exec_policy_windows_tests.rs +++ b/codex-rs/core/src/exec_policy_windows_tests.rs @@ -70,6 +70,34 @@ fn commands_for_exec_policy_parses_powershell_shell_wrapper() { ); } +#[test] +fn commands_for_exec_policy_normalizes_powershell_literal_comma_arrays() { + let command = vec![ + "powershell.exe".to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "gh issue view 18861 --repo openai/codex --json number,title,state".to_string(), + ]; + + assert_eq!( + commands_for_exec_policy(&command), + ExecPolicyCommands { + commands: vec![vec![ + "gh".to_string(), + "issue".to_string(), + "view".to_string(), + "18861".to_string(), + "-repo".to_string(), + "openai/codex".to_string(), + "-json".to_string(), + "number,title,state".to_string(), + ]], + used_complex_parsing: false, + command_origin: ExecPolicyCommandOrigin::PowerShell, + } + ); +} + #[test] fn unmatched_safe_powershell_words_are_allowed() { let command = vec!["Get-Content".to_string(), "Cargo.toml".to_string()]; diff --git a/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 b/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 index 9f19f172eb..391738e375 100644 --- a/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 +++ b/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 @@ -88,24 +88,47 @@ function Write-Response { $stdout.WriteLine(($Response | ConvertTo-Json -Compress -Depth 3)) } +function Convert-LiteralCommandValue { + param($expression) + + if ($expression -is [System.Management.Automation.Language.StringConstantExpressionAst]) { + return $expression.Value + } + + if ($expression -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) { + if ($expression.NestedExpressions.Count -gt 0) { + return $null + } + return $expression.Value + } + + if ($expression -is [System.Management.Automation.Language.ConstantExpressionAst]) { + return $expression.Value.ToString() + } + + if ($expression.GetType().FullName -eq 'System.Management.Automation.Language.ArrayLiteralAst') { + $parts = [System.Collections.Generic.List[string]]::new() + foreach ($element in $expression.Elements) { + $part = Convert-LiteralCommandValue $element + if ($part -eq $null) { + return $null + } + $parts.Add($part) + } + return [string]::Join(',', $parts) + } + + return $null +} + function Convert-CommandElement { param($element) # Accept only literal-ish command elements. Variable expansion, subexpressions, splats, # and other dynamic forms return $null so the whole request becomes unsupported. - if ($element -is [System.Management.Automation.Language.StringConstantExpressionAst]) { - return @($element.Value) - } - - if ($element -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) { - if ($element.NestedExpressions.Count -gt 0) { - return $null - } - return @($element.Value) - } - - if ($element -is [System.Management.Automation.Language.ConstantExpressionAst]) { - return @($element.Value.ToString()) + $literalValue = Convert-LiteralCommandValue $element + if ($literalValue -ne $null) { + return @($literalValue) } if ($element -is [System.Management.Automation.Language.CommandParameterAst]) { @@ -113,17 +136,25 @@ function Convert-CommandElement { return @('-' + $element.ParameterName) } - if ($element.Argument -is [System.Management.Automation.Language.StringConstantExpressionAst]) { - return @('-' + $element.ParameterName, $element.Argument.Value) - } - - if ($element.Argument -is [System.Management.Automation.Language.ConstantExpressionAst]) { - return @('-' + $element.ParameterName, $element.Argument.Value.ToString()) + $argumentValue = Convert-LiteralCommandValue $element.Argument + if ($argumentValue -ne $null) { + return @('-' + $element.ParameterName, $argumentValue) } return $null } + if ($element -is [System.Management.Automation.Language.CommandExpressionAst]) { + if ($element.Redirections.Count -gt 0) { + return $null + } + + $expressionValue = Convert-LiteralCommandValue $element.Expression + if ($expressionValue -ne $null) { + return @($expressionValue) + } + } + return $null } diff --git a/codex-rs/shell-command/src/powershell.rs b/codex-rs/shell-command/src/powershell.rs index f7b294c0f8..4feb7ce104 100644 --- a/codex-rs/shell-command/src/powershell.rs +++ b/codex-rs/shell-command/src/powershell.rs @@ -235,4 +235,30 @@ mod tests { ] ); } + + #[cfg(windows)] + #[test] + fn parses_literal_comma_separated_argument_as_one_word() { + let commands = parse_powershell_command_into_plain_commands(&[ + "powershell.exe".to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "gh issue view 18861 --repo openai/codex --json number,title,state".to_string(), + ]) + .expect("parse"); + + assert_eq!( + commands, + vec![vec![ + "gh".to_string(), + "issue".to_string(), + "view".to_string(), + "18861".to_string(), + "-repo".to_string(), + "openai/codex".to_string(), + "-json".to_string(), + "number,title,state".to_string(), + ]] + ); + } }