diff --git a/core/src/code_executors/unsafe_local_code_executor.ts b/core/src/code_executors/unsafe_local_code_executor.ts index 97061719a..56da44a93 100644 --- a/core/src/code_executors/unsafe_local_code_executor.ts +++ b/core/src/code_executors/unsafe_local_code_executor.ts @@ -38,6 +38,14 @@ const POWERSHELL_BASE_ARGS = [ */ const CMD_BASE_ARGS = ['/D', '/c'] as const; +/** + * Whether `commandPath` names Windows PowerShell (`powershell`) or PowerShell + * 7+ (`pwsh`). `path.win32` splits on both separators on every platform. + */ +function isPowerShellCommand(commandPath: string): boolean { + return /^(powershell|pwsh)(\.exe)?$/i.test(path.win32.basename(commandPath)); +} + /** * Options for UnsafeLocalCodeExecutor. */ @@ -56,6 +64,10 @@ export interface UnsafeLocalCodeExecutorOptions { pythonCommandPath?: string; /** * The command to run Shell code. Default is `bash`. + * + * When it names `powershell` or `pwsh` (with or without `.exe`) the script + * is written as `.ps1` and run through PowerShell rather than as a bare + * shell script. */ shellCommandPath?: string; } @@ -100,6 +112,9 @@ function getExtensionForLanguage( } if (language === CodeExecutionLanguage.SHELL) { + if (shellCommandPath && isPowerShellCommand(shellCommandPath)) { + return '.ps1'; + } if (IS_WINDOWS) { if (shellCommandPath && shellCommandPath.toLowerCase().includes('cmd')) { return '.bat'; @@ -189,7 +204,7 @@ export class UnsafeLocalCodeExecutor extends BaseCodeExecutor { command = this.pythonCommandPath; } else if (language === CodeExecutionLanguage.SHELL) { command = this.shellCommandPath; - if (this.shellCommandPath.toLowerCase().includes('powershell')) { + if (isPowerShellCommand(this.shellCommandPath)) { args = [...POWERSHELL_BASE_ARGS, filePath]; } else if (this.shellCommandPath.toLowerCase().includes('cmd')) { args = [...CMD_BASE_ARGS, filePath]; diff --git a/core/test/code_executors/unsafe_local_code_executor_test.ts b/core/test/code_executors/unsafe_local_code_executor_test.ts index 82a0d2d01..341913ece 100644 --- a/core/test/code_executors/unsafe_local_code_executor_test.ts +++ b/core/test/code_executors/unsafe_local_code_executor_test.ts @@ -445,5 +445,49 @@ describe('UnsafeLocalCodeExecutor', () => { expect.anything(), ); }); + + describe('shell command detection', () => { + async function runShellCode(shellCommandPath: string) { + await new UnsafeLocalCodeExecutor({shellCommandPath}).executeCode({ + invocationContext, + codeExecutionInput: { + code: 'echo "test"', + language: CodeExecutionLanguage.SHELL, + inputFiles: [], + }, + }); + } + + it.each([ + 'pwsh', + 'pwsh.exe', + '/usr/bin/pwsh', + 'C:\\Program Files\\PowerShell\\7\\pwsh.exe', + 'PWSH', + 'powershell', + 'powershell.exe', + ])('runs a .ps1 script through PowerShell for %s', async (shell) => { + await runShellCode(shell); + + expect(spawnMock).toHaveBeenCalledWith( + shell, + EXPECTED_POWERSHELL_ARGS, + expect.anything(), + ); + }); + + it.each([ + '/opt/pwsh-tools/bin/bash', + '/usr/local/powershell-helpers/run.sh', + ])('does not treat %s as PowerShell', async (shell) => { + await runShellCode(shell); + + expect(spawnMock).toHaveBeenCalledWith( + shell, + [expect.stringMatching(/script\.(sh|ps1)$/)], + expect.anything(), + ); + }); + }); }); });