[codex] Reject unlowered PowerShell AST regions (#24092)

## Why

On Windows, Codex uses a PowerShell safe-command classifier to decide
whether a command is read-only enough to run without additional
approval. The classifier lowers `EndBlock.Statements` into argv-like
command words and checks those words against a safelist.

PowerShell can execute code stored elsewhere in the AST. Parameter
defaults, named blocks, `using` preambles, and top-level `trap` handlers
are not represented in the lowered statement list. Ignoring those
regions can make a side-effecting script look like a read-only command.

## What

Fail closed whenever a PowerShell script contains executable AST content
that the current lowering does not represent.

## How

- Return `unsupported` for parameter, dynamic-parameter, begin, process,
and clean blocks.
- Return `unsupported` for `using module` and `using assembly`
preambles.
- Return `unsupported` for non-empty `EndBlock.Traps` collections.
- Preserve compatibility with Windows PowerShell 5.1 by looking up
`CleanBlock` dynamically.
- Treat `unsupported` as a failure to prove that the command is safe,
routing it through the normal approval path.
- Add parser-level and end-to-end regressions for parameter blocks,
named blocks, using statements, and trap handlers.

This does not make these PowerShell forms invalid or prevent them from
running. It prevents automatic safe-command approval when the classifier
cannot account for all executable behavior.

## Testing

- `just test -p codex-shell-command`
- Windows CI exercises the parser and end-to-end safe-command
regressions against a real PowerShell installation.

---------

Co-authored-by: viyatb-oai <viyatb@openai.com>
This commit is contained in:
Chris Bookholt
2026-06-22 20:09:20 -07:00
committed by GitHub
Unverified
parent 7c22d376e5
commit 27f22b54ae
3 changed files with 113 additions and 0 deletions
@@ -42,6 +42,21 @@ function Invoke-ParseRequest {
return @{ id = $RequestId; status = 'parse_errors' }
}
# Top-level AST regions and collections outside the end-block statement list
# can execute code that the command lowering below does not inspect.
$cleanBlock = $ast.PSObject.Properties['CleanBlock']
if (
$ast.ParamBlock -ne $null -or
$ast.DynamicParamBlock -ne $null -or
$ast.BeginBlock -ne $null -or
$ast.ProcessBlock -ne $null -or
($cleanBlock -ne $null -and $cleanBlock.Value -ne $null) -or
$ast.UsingStatements.Count -gt 0 -or
$ast.EndBlock.Traps.Count -gt 0
) {
return @{ id = $RequestId; status = 'unsupported' }
}
# PowerShell's stop-parsing marker hands the remaining source text to native
# commands with runtime argument handling that does not match the AST shape we
# flatten below. Keep that form out of the argv-like lowering path entirely.