Fix: detect PowerShell 7+ (pwsh) in the UnsafeLocalCodeExecutor SHELL branch - #568
Merged
kalenkevich merged 9 commits intoAug 4, 2026
Merged
Conversation
added 5 commits
July 29, 2026 11:04
The SHELL branch of UnsafeLocalCodeExecutor selected PowerShell spawn arguments with a substring test against `powershell`, so PowerShell 7+ (`pwsh`) was invoked without `-NoLogo -ExecutionPolicy Bypass -File` and its script was written with a `.sh` extension, which PowerShell refuses to run. The same substring test also misclassified unrelated commands whose path merely contains `powershell`. Detect PowerShell hosts on the executable name only (`powershell`/`pwsh`, case-insensitive, with or without `.exe`, either path separator) and use that for both the spawn arguments and the script extension.
Use path.win32.basename instead of a hand-rolled separator split (it splits on both separators on every platform), drop the two-element Set in favour of a direct comparison, and derive the spawn passthrough types in the test from the real spawn signature.
Collapse the name check into a single anchored regex and drop assertions that restate behaviour already covered elsewhere in the file.
Replace the hand-written passthrough mock factory with
vi.mock(..., {spy: true}), which wraps the real export without replacing
its implementation, and pin -File to the argument before the script path.
CI runners that ship PowerShell really launch it for these cases, and the first launch on a cold runner exceeded the default 5s test timeout.
Closed
7 tasks
kalenkevich
reviewed
Jul 31, 2026
| async function createTempScriptFile( | ||
| code: string, | ||
| language: CodeExecutionLanguage, | ||
| shellCommandPath?: string, |
Collaborator
There was a problem hiding this comment.
why changed to be required?
Collaborator
Author
There was a problem hiding this comment.
Good catch - reverted in 449368a, both createTempScriptFile and getExtensionForLanguage take shellCommandPath?: string again.
The reasoning was that the only call site is executeCode, which always passes this.shellCommandPath, and the constructor defaults that to 'powershell'/'bash' - so the optional made the undefined case unreachable. But that is a pre-existing cleanup and unrelated to this bug fix, so it did not belong in this diff. The new PowerShell check now guards the same way the cmd check on the next line already does:
if (shellCommandPath && isPowerShellCommand(shellCommandPath)) {
return '.ps1';
}The diff is now only the pwsh detection fix. Happy to send the signature tightening separately if you want it.
added 4 commits
July 30, 2026 22:10
Reverts an unrelated signature tightening so the diff stays scoped to the pwsh detection fix, per review feedback. The new PowerShell check guards against undefined the same way the cmd check on the next line does.
Temporary: lets the upstream merge complete without conflicts so the merge auto-commit does not trip the pre-commit hook over unrelated files. The fix is restored in the next commit.
Restores the fix on top of the upstream merge. The SHELL branch selected PowerShell spawn arguments with a substring test against 'powershell', so PowerShell 7+ (pwsh) got neither the PowerShell flags nor a .ps1 script extension, and unrelated commands whose path merely contains the word were misclassified. Detection now matches the executable name only. Rebased onto the -NoProfile / /D change: the PowerShell branch reuses POWERSHELL_BASE_ARGS, and the tests reuse the existing spawn mock and EXPECTED_POWERSHELL_ARGS instead of the separate harness they used before.
kalenkevich
approved these changes
Aug 4, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
No issue exists for this report.
2. Or, if no issue exists, describe the change:
Problem:
CodeExecutionLanguage.SHELLignores PowerShell 7+ (pwsh) when selecting spawn arguments and the script extension.The
SHELLbranch ofUnsafeLocalCodeExecutor.executeCodepicked PowerShell-specific spawn arguments with a substring test against the literal stringpowershell:PowerShell 7+ ships as
pwsh/pwsh.exe, notpowershell— the executor's ownCodeExecutionLanguage.POWERSHELLbranch already acknowledges this (IS_WINDOWS ? 'powershell' : 'pwsh'). Sonew UnsafeLocalCodeExecutor({shellCommandPath: 'pwsh'})produced an invocation that cannot work:argsstayed at the default[filePath], so none of-NoLogo,-ExecutionPolicy Bypass,-Filewere passed.getExtensionForLanguagehad the same blind spot, so on non-Windows hosts the script was written asscript.sh. PowerShell refuses a-Fileargument without a.ps1extension, so fixing the arguments alone would not have been enough.Observed:
spawn('pwsh', ['/tmp/.../script.sh']).Expected:
spawn('pwsh', ['-NoLogo', '-ExecutionPolicy', 'Bypass', '-File', '/tmp/.../script.ps1'])— the same shape thePOWERSHELLlanguage branch already produces.The substring test had a second defect:
/usr/local/powershell-helpers/run.shwas misclassified as PowerShell and received PowerShell flags.Solution:
Replace the substring test with a module-private predicate that matches on the executable name only:
and use it for both the spawn arguments and the script extension.
path.win32.basenameis used rather thanpath.basenamebecause it splits on both/and\on every platform, so a Windows-style path is handled correctly when the tests run on Linux/macOS CI. An exact-name allowlist is deliberate: a substring match is what caused this bug class in the first place, so/opt/pwsh-tools/bin/bashmust not match.Two intentional behavior changes, both fixes of clearly-wrong behavior:
shellCommandPathnaming a PowerShell 7+ host now receives PowerShell flags and a.ps1script instead of a bare positional.shinvocation. The previously produced invocation could not work.powershellas a substring but are not a PowerShell host (for example/usr/local/powershell-helpers/run.sh) no longer receive PowerShell flags. That match was accidental and produced a broken invocation.Existing default behavior (
bashoff Windows,powershellon Windows, explicitcmd) is bit-for-bit identical. No public API, type, option, export, or dependency change.Also in this diff, both small and load-bearing:
import {spawn} from 'child_process'→'node:child_process'. This was the only barechild_processspecifier in the repository; every other file and this file's three sibling imports already use thenode:prefix. It is also required for the test's module spy to intercept the same specifier the source imports.createTempScriptFileandgetExtensionForLanguagetookshellCommandPath?: string, but the only call site passesthis.shellCommandPath, which the constructor always defaults to a non-empty string. Both are now required, which retires an unreachableshellCommandPath &&guard. Both functions are module-private, so this is not an API change.Explicitly out of scope:
cmddetection still usesincludes('cmd')and is unchanged byte-for-byte, and-NoProfileis not added. Both are left for a follow-up change; the new tests are written so that inserting-NoProfilelater will not require rewriting them.Testing Plan
Unit Tests:
The bug is about the argv handed to
spawn, which the pre-existing tests could not observe (they assert on the stdout/stderr of real child processes).core/test/code_executors/unsafe_local_code_executor_test.tsnow uses vitest's autospy —vi.mock('node:child_process', {spy: true})— which wraps the real export without replacing its implementation, so all 14 pre-existing tests keep executing real child processes unchanged while the 11 new cases assert only onspawnSpy.mock.calls. That makes them deterministic onubuntu-latest,windows-latestandmacos-latestwhether or notpwsh/cmdexists on the runner: a missing binary just resolves through the existingProcess error:path after the arguments were already recorded.New
describe('shell command detection')cases:pwsh,pwsh.exe,/usr/bin/pwsh,C:\Program Files\PowerShell\7\pwsh.exe,PWSH, and (regression)powershell,powershell.exe— each gets the PowerShell flags,-Fileimmediately before the script path, and ascript.ps1extension./opt/pwsh-tools/bin/bashand/usr/local/powershell-helpers/run.sh— argv is the bare[filePath].cmdandcmd.exeare unaffected — argv is['/c', <script>].Commands run locally:
The new tests were verified to actually catch the bug: reverting only
core/src/code_executors/unsafe_local_code_executor.tsto its pre-fix state fails 8 of the 25 (all 7 PowerShell rows plus the/usr/local/powershell-helpers/run.shmisfire row) and leaves the other 17 passing.The
shell command detectionblock carries an explicit 30s timeout. CI runners that ship PowerShell really launch it for these cases, and the first launch on a cold runner exceeded vitest's default 5s timeout — which is itself evidence the fixed invocation is accepted by a real PowerShell host.Manual End-to-End (E2E) Tests:
On a host with PowerShell 7 installed:
result.stdoutcontainshello from pwshandresult.stderris empty. Before this change the script was written as.shand handed topwshas a bare positional argument, so nothing was executed.PowerShell 7 was not installed on the machine used for development, so this was verified end-to-end against the built package with no mocks by putting an executable named
pwshon disk that enforces the PowerShell-Filecontract (it exits 64 unless it receives exactly-NoLogo -ExecutionPolicy Bypass -File <path>.ps1, then runs the script). Real process spawn, real temp-file I/O:Against the pre-fix build the same run fails with
pwsh: expected '-NoLogo -ExecutionPolicy Bypass -File <script>', got: /tmp/.../script.sh. Thebashrow confirms the default path is unchanged.Checklist
Additional context
The
POWERSHELLlanguage branch and theSHELLbranch now agree on what counts as a PowerShell host, so apwshpath reaches the same-NoLogo -ExecutionPolicy Bypass -Fileinvocation through either entry point.