2026-03-24 21:23:51 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2026 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
|
|
|
import { createSandboxManager } from './sandboxManagerFactory.js';
|
|
|
|
|
import { ShellExecutionService } from './shellExecutionService.js';
|
|
|
|
|
import { getSecureSanitizationConfig } from './environmentSanitization.js';
|
|
|
|
|
import {
|
|
|
|
|
type SandboxedCommand,
|
|
|
|
|
NoopSandboxManager,
|
|
|
|
|
LocalSandboxManager,
|
|
|
|
|
} from './sandboxManager.js';
|
|
|
|
|
import { execFile, execSync } from 'node:child_process';
|
|
|
|
|
import { promisify } from 'node:util';
|
|
|
|
|
import os from 'node:os';
|
|
|
|
|
import fs from 'node:fs';
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
import http from 'node:http';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Abstracts platform-specific shell commands for integration testing.
|
|
|
|
|
*/
|
|
|
|
|
const Platform = {
|
|
|
|
|
isWindows: os.platform() === 'win32',
|
|
|
|
|
|
|
|
|
|
/** Returns a command to create an empty file. */
|
|
|
|
|
touch(filePath: string) {
|
|
|
|
|
return this.isWindows
|
|
|
|
|
? { command: 'cmd.exe', args: ['/c', `type nul > "${filePath}"`] }
|
|
|
|
|
: { command: 'touch', args: [filePath] };
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Returns a command to read a file's content. */
|
|
|
|
|
cat(filePath: string) {
|
|
|
|
|
return this.isWindows
|
|
|
|
|
? { command: 'cmd.exe', args: ['/c', `type "${filePath}"`] }
|
|
|
|
|
: { command: 'cat', args: [filePath] };
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Returns a command to echo a string. */
|
|
|
|
|
echo(text: string) {
|
|
|
|
|
return this.isWindows
|
|
|
|
|
? { command: 'cmd.exe', args: ['/c', `echo ${text}`] }
|
|
|
|
|
: { command: 'echo', args: [text] };
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Returns a command to perform a network request. */
|
|
|
|
|
curl(url: string) {
|
|
|
|
|
return this.isWindows
|
|
|
|
|
? {
|
|
|
|
|
command: 'powershell.exe',
|
|
|
|
|
args: ['-Command', `Invoke-WebRequest -Uri ${url} -TimeoutSec 1`],
|
|
|
|
|
}
|
|
|
|
|
: { command: 'curl', args: ['-s', '--connect-timeout', '1', url] };
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Returns a command that checks if the current terminal is interactive. */
|
|
|
|
|
isPty() {
|
|
|
|
|
return this.isWindows
|
|
|
|
|
? 'cmd.exe /c echo True'
|
|
|
|
|
: 'bash -c "if [ -t 1 ]; then echo True; else echo False; fi"';
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Returns a path that is strictly outside the workspace and likely blocked. */
|
|
|
|
|
getExternalBlockedPath() {
|
|
|
|
|
return this.isWindows
|
|
|
|
|
? 'C:\\Windows\\System32\\drivers\\etc\\hosts'
|
|
|
|
|
: '/Users/Shared/.gemini_test_blocked';
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function runCommand(command: SandboxedCommand) {
|
|
|
|
|
try {
|
|
|
|
|
const { stdout, stderr } = await promisify(execFile)(
|
|
|
|
|
command.program,
|
|
|
|
|
command.args,
|
|
|
|
|
{
|
|
|
|
|
cwd: command.cwd,
|
|
|
|
|
env: command.env,
|
|
|
|
|
encoding: 'utf-8',
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return { status: 0, stdout, stderr };
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
const err = error as { code?: number; stdout?: string; stderr?: string };
|
|
|
|
|
return {
|
|
|
|
|
status: err.code ?? 1,
|
|
|
|
|
stdout: err.stdout ?? '',
|
|
|
|
|
stderr: err.stderr ?? '',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines if the system has the necessary binaries to run the sandbox.
|
2026-03-25 10:29:46 -04:00
|
|
|
* Throws an error if a supported platform is missing its required tools.
|
2026-03-24 21:23:51 -04:00
|
|
|
*/
|
2026-03-25 10:29:46 -04:00
|
|
|
function ensureSandboxAvailable(): boolean {
|
|
|
|
|
const platform = os.platform();
|
|
|
|
|
|
|
|
|
|
if (platform === 'win32') {
|
2026-03-24 21:23:51 -04:00
|
|
|
// Windows sandboxing relies on icacls, which is a core system utility and
|
|
|
|
|
// always available.
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 10:29:46 -04:00
|
|
|
if (platform === 'darwin') {
|
|
|
|
|
if (fs.existsSync('/usr/bin/sandbox-exec')) {
|
2026-03-26 13:04:44 -07:00
|
|
|
try {
|
|
|
|
|
execSync('sandbox-exec -p "(version 1)(allow default)" echo test', {
|
|
|
|
|
stdio: 'ignore',
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
console.warn(
|
|
|
|
|
'sandbox-exec is present but cannot be used (likely running inside a sandbox already). Skipping sandbox tests.',
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-03-25 10:29:46 -04:00
|
|
|
}
|
|
|
|
|
throw new Error(
|
|
|
|
|
'Sandboxing tests on macOS require /usr/bin/sandbox-exec to be present.',
|
|
|
|
|
);
|
2026-03-24 21:23:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-25 10:29:46 -04:00
|
|
|
if (platform === 'linux') {
|
2026-03-24 21:23:51 -04:00
|
|
|
try {
|
|
|
|
|
execSync('which bwrap', { stdio: 'ignore' });
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
2026-03-25 10:29:46 -04:00
|
|
|
throw new Error(
|
|
|
|
|
'Sandboxing tests on Linux require bubblewrap (bwrap) to be installed.',
|
|
|
|
|
);
|
2026-03-24 21:23:51 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('SandboxManager Integration', () => {
|
|
|
|
|
const workspace = process.cwd();
|
2026-03-27 12:57:26 -04:00
|
|
|
const manager = createSandboxManager({ enabled: true }, { workspace });
|
2026-03-24 21:23:51 -04:00
|
|
|
|
|
|
|
|
// Skip if we are on an unsupported platform or if it's a NoopSandboxManager
|
|
|
|
|
const shouldSkip =
|
|
|
|
|
manager instanceof NoopSandboxManager ||
|
|
|
|
|
manager instanceof LocalSandboxManager ||
|
2026-03-25 10:29:46 -04:00
|
|
|
!ensureSandboxAvailable();
|
2026-03-24 21:23:51 -04:00
|
|
|
|
|
|
|
|
describe.skipIf(shouldSkip)('Cross-platform Sandbox Behavior', () => {
|
|
|
|
|
describe('Basic Execution', () => {
|
|
|
|
|
it('executes commands within the workspace', async () => {
|
|
|
|
|
const { command, args } = Platform.echo('sandbox test');
|
|
|
|
|
const sandboxed = await manager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: workspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).toBe(0);
|
|
|
|
|
expect(result.stdout.trim()).toBe('sandbox test');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('supports interactive pseudo-terminals (node-pty)', async () => {
|
|
|
|
|
const handle = await ShellExecutionService.execute(
|
|
|
|
|
Platform.isPty(),
|
|
|
|
|
workspace,
|
|
|
|
|
() => {},
|
|
|
|
|
new AbortController().signal,
|
|
|
|
|
true,
|
|
|
|
|
{
|
|
|
|
|
sanitizationConfig: getSecureSanitizationConfig(),
|
|
|
|
|
sandboxManager: manager,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await handle.result;
|
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
|
expect(result.output).toContain('True');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('File System Access', () => {
|
|
|
|
|
it('blocks access outside the workspace', async () => {
|
|
|
|
|
const blockedPath = Platform.getExternalBlockedPath();
|
|
|
|
|
const { command, args } = Platform.touch(blockedPath);
|
|
|
|
|
|
|
|
|
|
const sandboxed = await manager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: workspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).not.toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('grants access to explicitly allowed paths', async () => {
|
|
|
|
|
const allowedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'allowed-'));
|
|
|
|
|
const testFile = path.join(allowedDir, 'test.txt');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const { command, args } = Platform.touch(testFile);
|
|
|
|
|
const sandboxed = await manager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: workspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
policy: { allowedPaths: [allowedDir] },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).toBe(0);
|
|
|
|
|
expect(fs.existsSync(testFile)).toBe(true);
|
|
|
|
|
} finally {
|
|
|
|
|
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
|
|
|
fs.rmSync(allowedDir, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('blocks access to forbidden paths within the workspace', async () => {
|
|
|
|
|
const tempWorkspace = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), 'workspace-'),
|
|
|
|
|
);
|
|
|
|
|
const forbiddenDir = path.join(tempWorkspace, 'forbidden');
|
|
|
|
|
const testFile = path.join(forbiddenDir, 'test.txt');
|
|
|
|
|
fs.mkdirSync(forbiddenDir);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const osManager = createSandboxManager(
|
|
|
|
|
{ enabled: true },
|
2026-03-27 12:57:26 -04:00
|
|
|
{ workspace: tempWorkspace, forbiddenPaths: [forbiddenDir] },
|
2026-03-24 21:23:51 -04:00
|
|
|
);
|
|
|
|
|
const { command, args } = Platform.touch(testFile);
|
|
|
|
|
|
|
|
|
|
const sandboxed = await osManager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: tempWorkspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).not.toBe(0);
|
|
|
|
|
} finally {
|
|
|
|
|
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('blocks access to files inside forbidden directories recursively', async () => {
|
|
|
|
|
const tempWorkspace = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), 'workspace-'),
|
|
|
|
|
);
|
|
|
|
|
const forbiddenDir = path.join(tempWorkspace, 'forbidden');
|
|
|
|
|
const nestedDir = path.join(forbiddenDir, 'nested');
|
|
|
|
|
const nestedFile = path.join(nestedDir, 'test.txt');
|
|
|
|
|
|
|
|
|
|
fs.mkdirSync(nestedDir, { recursive: true });
|
|
|
|
|
fs.writeFileSync(nestedFile, 'secret');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const osManager = createSandboxManager(
|
|
|
|
|
{ enabled: true },
|
2026-03-27 12:57:26 -04:00
|
|
|
{ workspace: tempWorkspace, forbiddenPaths: [forbiddenDir] },
|
2026-03-24 21:23:51 -04:00
|
|
|
);
|
|
|
|
|
const { command, args } = Platform.cat(nestedFile);
|
|
|
|
|
|
|
|
|
|
const sandboxed = await osManager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: tempWorkspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).not.toBe(0);
|
|
|
|
|
} finally {
|
|
|
|
|
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('prioritizes forbiddenPaths over allowedPaths', async () => {
|
|
|
|
|
const tempWorkspace = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), 'workspace-'),
|
|
|
|
|
);
|
|
|
|
|
const conflictDir = path.join(tempWorkspace, 'conflict');
|
|
|
|
|
const testFile = path.join(conflictDir, 'test.txt');
|
|
|
|
|
fs.mkdirSync(conflictDir);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const osManager = createSandboxManager(
|
|
|
|
|
{ enabled: true },
|
2026-03-27 12:57:26 -04:00
|
|
|
{ workspace: tempWorkspace, forbiddenPaths: [conflictDir] },
|
2026-03-24 21:23:51 -04:00
|
|
|
);
|
|
|
|
|
const { command, args } = Platform.touch(testFile);
|
|
|
|
|
|
|
|
|
|
const sandboxed = await osManager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: tempWorkspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
policy: {
|
|
|
|
|
allowedPaths: [conflictDir],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).not.toBe(0);
|
|
|
|
|
} finally {
|
|
|
|
|
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('gracefully ignores non-existent paths in allowedPaths and forbiddenPaths', async () => {
|
|
|
|
|
const tempWorkspace = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), 'workspace-'),
|
|
|
|
|
);
|
|
|
|
|
const nonExistentPath = path.join(tempWorkspace, 'does-not-exist');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const osManager = createSandboxManager(
|
|
|
|
|
{ enabled: true },
|
2026-03-27 12:57:26 -04:00
|
|
|
{ workspace: tempWorkspace, forbiddenPaths: [nonExistentPath] },
|
2026-03-24 21:23:51 -04:00
|
|
|
);
|
|
|
|
|
const { command, args } = Platform.echo('survived');
|
|
|
|
|
const sandboxed = await osManager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: tempWorkspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
policy: {
|
|
|
|
|
allowedPaths: [nonExistentPath],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).toBe(0);
|
|
|
|
|
expect(result.stdout.trim()).toBe('survived');
|
|
|
|
|
} finally {
|
|
|
|
|
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('prevents creation of non-existent forbidden paths', async () => {
|
|
|
|
|
// Windows icacls cannot explicitly protect paths that have not yet been created.
|
|
|
|
|
if (Platform.isWindows) return;
|
|
|
|
|
|
|
|
|
|
const tempWorkspace = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), 'workspace-'),
|
|
|
|
|
);
|
|
|
|
|
const nonExistentFile = path.join(tempWorkspace, 'never-created.txt');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const osManager = createSandboxManager(
|
|
|
|
|
{ enabled: true },
|
2026-03-27 12:57:26 -04:00
|
|
|
{ workspace: tempWorkspace, forbiddenPaths: [nonExistentFile] },
|
2026-03-24 21:23:51 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// We use touch to attempt creation of the file
|
|
|
|
|
const { command: cmdTouch, args: argsTouch } =
|
|
|
|
|
Platform.touch(nonExistentFile);
|
|
|
|
|
|
|
|
|
|
const sandboxedCmd = await osManager.prepareCommand({
|
|
|
|
|
command: cmdTouch,
|
|
|
|
|
args: argsTouch,
|
|
|
|
|
cwd: tempWorkspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Execute the command, we expect it to fail (permission denied or read-only file system)
|
|
|
|
|
const result = await runCommand(sandboxedCmd);
|
|
|
|
|
|
|
|
|
|
expect(result.status).not.toBe(0);
|
|
|
|
|
expect(fs.existsSync(nonExistentFile)).toBe(false);
|
|
|
|
|
} finally {
|
|
|
|
|
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('blocks access to both a symlink and its target when the symlink is forbidden', async () => {
|
|
|
|
|
if (Platform.isWindows) return;
|
|
|
|
|
|
|
|
|
|
const tempWorkspace = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), 'workspace-'),
|
|
|
|
|
);
|
|
|
|
|
const targetFile = path.join(tempWorkspace, 'target.txt');
|
|
|
|
|
const symlinkFile = path.join(tempWorkspace, 'link.txt');
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(targetFile, 'secret data');
|
|
|
|
|
fs.symlinkSync(targetFile, symlinkFile);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const osManager = createSandboxManager(
|
|
|
|
|
{ enabled: true },
|
2026-03-27 12:57:26 -04:00
|
|
|
{ workspace: tempWorkspace, forbiddenPaths: [symlinkFile] },
|
2026-03-24 21:23:51 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Attempt to read the target file directly
|
|
|
|
|
const { command: cmdTarget, args: argsTarget } =
|
|
|
|
|
Platform.cat(targetFile);
|
|
|
|
|
const commandTarget = await osManager.prepareCommand({
|
|
|
|
|
command: cmdTarget,
|
|
|
|
|
args: argsTarget,
|
|
|
|
|
cwd: tempWorkspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
const resultTarget = await runCommand(commandTarget);
|
|
|
|
|
expect(resultTarget.status).not.toBe(0);
|
|
|
|
|
|
|
|
|
|
// Attempt to read via the symlink
|
|
|
|
|
const { command: cmdLink, args: argsLink } =
|
|
|
|
|
Platform.cat(symlinkFile);
|
|
|
|
|
const commandLink = await osManager.prepareCommand({
|
|
|
|
|
command: cmdLink,
|
|
|
|
|
args: argsLink,
|
|
|
|
|
cwd: tempWorkspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
const resultLink = await runCommand(commandLink);
|
|
|
|
|
expect(resultLink.status).not.toBe(0);
|
|
|
|
|
} finally {
|
|
|
|
|
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Network Access', () => {
|
|
|
|
|
let server: http.Server;
|
|
|
|
|
let url: string;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
server = http.createServer((_, res) => {
|
|
|
|
|
res.setHeader('Connection', 'close');
|
|
|
|
|
res.writeHead(200);
|
|
|
|
|
res.end('ok');
|
|
|
|
|
});
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
|
server.on('error', reject);
|
|
|
|
|
server.listen(0, '127.0.0.1', () => {
|
|
|
|
|
const addr = server.address() as import('net').AddressInfo;
|
|
|
|
|
url = `http://127.0.0.1:${addr.port}`;
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
if (server) await new Promise<void>((res) => server.close(() => res()));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('blocks network access by default', async () => {
|
|
|
|
|
const { command, args } = Platform.curl(url);
|
|
|
|
|
const sandboxed = await manager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: workspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).not.toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('grants network access when explicitly allowed', async () => {
|
|
|
|
|
const { command, args } = Platform.curl(url);
|
|
|
|
|
const sandboxed = await manager.prepareCommand({
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
cwd: workspace,
|
|
|
|
|
env: process.env,
|
|
|
|
|
policy: { networkAccess: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await runCommand(sandboxed);
|
|
|
|
|
expect(result.status).toBe(0);
|
|
|
|
|
if (!Platform.isWindows) {
|
|
|
|
|
expect(result.stdout.trim()).toBe('ok');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|