Skip to content
Merged
17 changes: 16 additions & 1 deletion core/src/code_executors/unsafe_local_code_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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;
}
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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];
Expand Down
44 changes: 44 additions & 0 deletions core/test/code_executors/unsafe_local_code_executor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
});
});
});
});
Loading