diff --git a/src/sandbox/linux-sandbox-utils.ts b/src/sandbox/linux-sandbox-utils.ts index 24f7e3c..875e108 100644 --- a/src/sandbox/linux-sandbox-utils.ts +++ b/src/sandbox/linux-sandbox-utils.ts @@ -44,6 +44,9 @@ export interface LinuxSandboxParams { allowAllUnixSockets?: boolean binShell?: string ripgrepConfig?: { command: string; args?: string[] } + env?: Record + preCommand?: string + skipGitConfigProtection?: boolean } // Track generated seccomp filters for cleanup on process exit @@ -280,6 +283,7 @@ function buildSandboxCommand( userCommand: string, seccompFilterPath: string | undefined, shell?: string, + preCommand?: string, ): string { // Default to bash for backward compatibility const shellPath = shell || 'bash' @@ -289,12 +293,17 @@ function buildSandboxCommand( 'trap "kill %1 %2 2>/dev/null; exit" EXIT', ] + // If preCommand is provided, run it after socat setup but before user command + // This is useful for initialization tasks like setting up certificates + const preCommandScript = preCommand ? [preCommand] : [] + // If seccomp filter is provided, use apply-seccomp to apply it if (seccompFilterPath) { // apply-seccomp approach: // 1. Outer bwrap/bash: starts socat processes (can use Unix sockets) - // 2. apply-seccomp: applies seccomp filter and execs user command - // 3. User command runs with seccomp active (Unix sockets blocked) + // 2. preCommand: runs initialization tasks (if provided) + // 3. apply-seccomp: applies seccomp filter and execs user command + // 4. User command runs with seccomp active (Unix sockets blocked) // // apply-seccomp is a simple C program that: // - Sets PR_SET_NO_NEW_PRIVS @@ -318,12 +327,17 @@ function buildSandboxCommand( userCommand, ]) - const innerScript = [...socatCommands, applySeccompCmd].join('\n') + const innerScript = [ + ...socatCommands, + ...preCommandScript, + applySeccompCmd, + ].join('\n') return `${shellPath} -c ${shellquote.quote([innerScript])}` } else { // No seccomp filter - run user command directly const innerScript = [ ...socatCommands, + ...preCommandScript, `eval ${shellquote.quote([userCommand])}`, ].join('\n') @@ -338,6 +352,7 @@ async function generateFilesystemArgs( readConfig: FsReadRestrictionConfig | undefined, writeConfig: FsWriteRestrictionConfig | undefined, ripgrepConfig: { command: string; args?: string[] } = { command: 'rg' }, + skipGitConfigProtection = false, ): Promise { const args: string[] = [] // fs already imported @@ -378,7 +393,10 @@ async function generateFilesystemArgs( // Deny writes within allowed paths (user-specified + mandatory denies) const denyPaths = [ ...(writeConfig.denyWithinAllow || []), - ...(await getMandatoryDenyWithinAllow(ripgrepConfig)), + ...(await getMandatoryDenyWithinAllow( + ripgrepConfig, + skipGitConfigProtection, + )), ] for (const pathPattern of denyPaths) { @@ -511,6 +529,9 @@ export async function wrapCommandWithSandboxLinux( allowAllUnixSockets, binShell, ripgrepConfig = { command: 'rg' }, + env: customEnv, + preCommand, + skipGitConfigProtection = false, } = params // Determine if we have restrictions to apply @@ -601,6 +622,7 @@ export async function wrapCommandWithSandboxLinux( const proxyEnv = generateProxyEnvVars( 3128, // Internal HTTP listener port 1080, // Internal SOCKS listener port + customEnv, // Custom environment variables from config ) bwrapArgs.push( ...proxyEnv.flatMap((env: string) => { @@ -627,6 +649,12 @@ export async function wrapCommandWithSandboxLinux( String(socksProxyPort), ) } + } else if (customEnv) { + // No network restrictions, but custom env vars are provided + // Add them directly without proxy env vars + for (const [key, value] of Object.entries(customEnv)) { + bwrapArgs.push('--setenv', key, value) + } } // ========== FILESYSTEM RESTRICTIONS ========== @@ -634,6 +662,7 @@ export async function wrapCommandWithSandboxLinux( readConfig, writeConfig, ripgrepConfig, + skipGitConfigProtection, ) bwrapArgs.push(...fsArgs) @@ -677,6 +706,7 @@ export async function wrapCommandWithSandboxLinux( command, seccompFilterPath, shell, + preCommand, ) bwrapArgs.push(sandboxCommand) } else if (seccompFilterPath) { @@ -690,16 +720,21 @@ export async function wrapCommandWithSandboxLinux( ) } + // If preCommand is provided, prepend it to the user command + const commandWithPre = preCommand ? `${preCommand}\n${command}` : command const applySeccompCmd = shellquote.quote([ applySeccompBinary, seccompFilterPath, shell, '-c', - command, + commandWithPre, ]) bwrapArgs.push(applySeccompCmd) } else { - bwrapArgs.push(command) + // No network restrictions and no seccomp - run command directly + // If preCommand is provided, prepend it to the user command + const commandWithPre = preCommand ? `${preCommand}\n${command}` : command + bwrapArgs.push(commandWithPre) } // Build the outer bwrap command diff --git a/src/sandbox/sandbox-config.ts b/src/sandbox/sandbox-config.ts index 2675276..c622730 100644 --- a/src/sandbox/sandbox-config.ts +++ b/src/sandbox/sandbox-config.ts @@ -155,6 +155,27 @@ export const SandboxRuntimeConfigSchema = z.object({ ripgrep: RipgrepConfigSchema.optional().describe( 'Custom ripgrep configuration (default: { command: "rg" })', ), + env: z + .record(z.string(), z.string()) + .optional() + .describe('Custom environment variables to set inside the sandbox'), + preCommand: z + .string() + .optional() + .describe( + 'Shell command to run inside the sandbox before the main command. ' + + 'Runs after network bridges are established but before the user command. ' + + 'Use for initialization tasks.', + ), + skipGitConfigProtection: z + .boolean() + .optional() + .describe( + 'Skip the mandatory protection that blocks writes to .git/config and .git/hooks. ' + + 'WARNING: Only enable this when using an external security proxy ' + + 'that already provides protection against git config exploits (core.fsmonitor, etc.). ' + + 'Without this protection, malicious code could achieve arbitrary code execution via git.', + ), }) // Export inferred types diff --git a/src/sandbox/sandbox-manager.ts b/src/sandbox/sandbox-manager.ts index bea9895..d69bed9 100644 --- a/src/sandbox/sandbox-manager.ts +++ b/src/sandbox/sandbox-manager.ts @@ -438,6 +438,18 @@ function getRipgrepConfig(): { command: string; args?: string[] } { return config?.ripgrep ?? { command: 'rg' } } +function getEnv(): Record | undefined { + return config?.env +} + +function getPreCommand(): string | undefined { + return config?.preCommand +} + +function getSkipGitConfigProtection(): boolean { + return config?.skipGitConfigProtection ?? false +} + function getProxyPort(): number | undefined { return managerContext?.httpProxyPort } @@ -495,13 +507,19 @@ async function wrapWithSandbox( customConfig?.filesystem?.denyRead ?? config?.filesystem.denyRead ?? [], } - // Check if network proxy is needed based on allowed domains + // Check if network proxy is needed based on allowed domains or external proxy config // Unix sockets are local IPC and don't require the network proxy const allowedDomains = customConfig?.network?.allowedDomains ?? config?.network.allowedDomains ?? [] - const needsNetworkProxy = allowedDomains.length > 0 + // Network sandboxing is needed if: + // 1. There are allowed domains (sandbox-runtime enforces allowlist), OR + // 2. External proxy ports are configured (external proxy enforces allowlist) + const hasExternalProxy = + config?.network.httpProxyPort !== undefined || + config?.network.socksProxyPort !== undefined + const needsNetworkProxy = allowedDomains.length > 0 || hasExternalProxy // Wait for network initialization only if proxy is actually needed if (needsNetworkProxy) { @@ -539,6 +557,9 @@ async function wrapWithSandbox( allowAllUnixSockets: getAllowAllUnixSockets(), binShell, ripgrepConfig: getRipgrepConfig(), + env: getEnv(), + preCommand: getPreCommand(), + skipGitConfigProtection: getSkipGitConfigProtection(), }) default: @@ -818,6 +839,8 @@ export interface ISandboxManager { getAllowLocalBinding(): boolean | undefined getIgnoreViolations(): Record | undefined getEnableWeakerNestedSandbox(): boolean | undefined + getEnv(): Record | undefined + getPreCommand(): string | undefined getProxyPort(): number | undefined getSocksProxyPort(): number | undefined getLinuxHttpSocketPath(): string | undefined @@ -856,6 +879,8 @@ export const SandboxManager: ISandboxManager = { getAllowLocalBinding, getIgnoreViolations, getEnableWeakerNestedSandbox, + getEnv, + getPreCommand, getProxyPort, getSocksProxyPort, getLinuxHttpSocketPath, diff --git a/src/sandbox/sandbox-utils.ts b/src/sandbox/sandbox-utils.ts index ac13b9c..9d9c056 100644 --- a/src/sandbox/sandbox-utils.ts +++ b/src/sandbox/sandbox-utils.ts @@ -151,9 +151,11 @@ export function getDefaultWritePaths(): string[] { * This uses ripgrep to scan the filesystem for dangerous files and directories * Returns absolute paths that must be blocked from writes * @param ripgrepConfig Ripgrep configuration (command and optional args) + * @param skipGitConfigProtection If true, skip blocking .git/config and .git/hooks */ export async function getMandatoryDenyWithinAllow( ripgrepConfig: { command: string; args?: string[] } = { command: 'rg' }, + skipGitConfigProtection = false, ): Promise { const denyPaths: string[] = [] const cwd = process.cwd() @@ -271,52 +273,55 @@ export async function getMandatoryDenyWithinAllow( // Special handling for dangerous .git paths // We block specific paths within .git that can be used for code execution - const dangerousGitPaths = [ - '.git/hooks', // Block all hook files to prevent code execution via git hooks - '.git/config', // Block config file to prevent dangerous config options like core.fsmonitor - ] - - for (const gitPath of dangerousGitPaths) { - // Add the path in the current working directory - const absoluteGitPath = path.resolve(cwd, gitPath) - denyPaths.push(absoluteGitPath) - - // Also find .git directories in subdirectories and block their hooks/config - // This handles nested repositories (case-insensitive) - try { - // Find all .git directories by looking for .git/HEAD files (case-insensitive) - const gitHeadFiles = await ripGrep( - [ - '--files', - '--hidden', - '--iglob', - '**/.git/HEAD', - '-g', - '!**/node_modules/**', - ], - cwd, - abortController.signal, - ripgrepConfig, - ) + // This can be skipped when using an external security proxy that handles these exploits + if (!skipGitConfigProtection) { + const dangerousGitPaths = [ + '.git/hooks', // Block all hook files to prevent code execution via git hooks + '.git/config', // Block config file to prevent dangerous config options like core.fsmonitor + ] + + for (const gitPath of dangerousGitPaths) { + // Add the path in the current working directory + const absoluteGitPath = path.resolve(cwd, gitPath) + denyPaths.push(absoluteGitPath) + + // Also find .git directories in subdirectories and block their hooks/config + // This handles nested repositories (case-insensitive) + try { + // Find all .git directories by looking for .git/HEAD files (case-insensitive) + const gitHeadFiles = await ripGrep( + [ + '--files', + '--hidden', + '--iglob', + '**/.git/HEAD', + '-g', + '!**/node_modules/**', + ], + cwd, + abortController.signal, + ripgrepConfig, + ) - for (const gitHeadFile of gitHeadFiles) { - // Get the .git directory path - const gitDir = path.dirname(gitHeadFile) - - // Add the dangerous path within this .git directory - if (gitPath === '.git/hooks') { - const hooksPath = path.join(gitDir, 'hooks') - denyPaths.push(hooksPath) - } else if (gitPath === '.git/config') { - const configPath = path.join(gitDir, 'config') - denyPaths.push(configPath) + for (const gitHeadFile of gitHeadFiles) { + // Get the .git directory path + const gitDir = path.dirname(gitHeadFile) + + // Add the dangerous path within this .git directory + if (gitPath === '.git/hooks') { + const hooksPath = path.join(gitDir, 'hooks') + denyPaths.push(hooksPath) + } else if (gitPath === '.git/config') { + const configPath = path.join(gitDir, 'config') + denyPaths.push(configPath) + } } + } catch (error) { + // If ripgrep fails, we cannot safely determine all .git repositories + throw new Error( + `Failed to scan for .git directories: ${error instanceof Error ? error.message : String(error)}`, + ) } - } catch (error) { - // If ripgrep fails, we cannot safely determine all .git repositories - throw new Error( - `Failed to scan for .git directories: ${error instanceof Error ? error.message : String(error)}`, - ) } } @@ -330,11 +335,17 @@ export async function getMandatoryDenyWithinAllow( export function generateProxyEnvVars( httpProxyPort?: number, socksProxyPort?: number, + customEnv?: Record, ): string[] { const envVars: string[] = [`SANDBOX_RUNTIME=1`, `TMPDIR=/tmp/claude`] - // If no proxy ports provided, return minimal env vars + // If no proxy ports provided, return minimal env vars (plus custom env) if (!httpProxyPort && !socksProxyPort) { + if (customEnv) { + for (const [key, value] of Object.entries(customEnv)) { + envVars.push(`${key}=${value}`) + } + } return envVars } @@ -422,6 +433,13 @@ export function generateProxyEnvVars( // Most HTTP clients do not support SOCKS URLs in these variables and will fail, and we want // to avoid overriding the client otherwise respecting the ALL_PROXY env var which points to SOCKS. + // Add custom environment variables (these can override the defaults above) + if (customEnv) { + for (const [key, value] of Object.entries(customEnv)) { + envVars.push(`${key}=${value}`) + } + } + return envVars } diff --git a/test/configurable-proxy-ports.test.ts b/test/configurable-proxy-ports.test.ts index be47e76..69f78fb 100644 --- a/test/configurable-proxy-ports.test.ts +++ b/test/configurable-proxy-ports.test.ts @@ -1,10 +1,11 @@ -import { describe, it, expect, beforeAll, afterAll } from 'bun:test' +import { describe, it, expect, afterAll } from 'bun:test' import { spawnSync } from 'node:child_process' import * as http from 'node:http' import * as net from 'node:net' import { SandboxManager } from '../src/sandbox/sandbox-manager.js' import type { SandboxRuntimeConfig } from '../src/sandbox/sandbox-config.js' import { getPlatform } from '../src/utils/platform.js' +import { generateProxyEnvVars } from '../src/sandbox/sandbox-utils.js' /** * Integration tests for configurable proxy ports feature @@ -310,18 +311,22 @@ describe('Configurable Proxy Ports Integration Tests', () => { const { port, hostname } = new URL(`http://${req.url}`) // Connect to target (allow everything - no filtering) - const serverSocket = net.connect(parseInt(port) || 80, hostname, () => { - clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n') - serverSocket.write(head) - serverSocket.pipe(clientSocket) - clientSocket.pipe(serverSocket) - }) - - serverSocket.on('error', (err) => { + const serverSocket = net.connect( + parseInt(port) || 80, + hostname, + () => { + clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n') + serverSocket.write(head) + serverSocket.pipe(clientSocket) + clientSocket.pipe(serverSocket) + }, + ) + + serverSocket.on('error', _err => { clientSocket.end() }) - clientSocket.on('error', (err) => { + clientSocket.on('error', _err => { serverSocket.end() }) }) @@ -337,12 +342,12 @@ describe('Configurable Proxy Ports Integration Tests', () => { headers: req.headers, } - const proxyReq = http.request(options, (proxyRes) => { + const proxyReq = http.request(options, proxyRes => { res.writeHead(proxyRes.statusCode!, proxyRes.headers) proxyRes.pipe(res) }) - proxyReq.on('error', (err) => { + proxyReq.on('error', _err => { res.writeHead(502) res.end('Bad Gateway') }) @@ -356,7 +361,9 @@ describe('Configurable Proxy Ports Integration Tests', () => { const addr = externalProxyServer!.address() if (addr && typeof addr === 'object') { externalProxyPort = addr.port - console.log(`External allow-all proxy started on port ${externalProxyPort}`) + console.log( + `External allow-all proxy started on port ${externalProxyPort}`, + ) resolve() } else { reject(new Error('Failed to get proxy address')) @@ -387,7 +394,7 @@ describe('Configurable Proxy Ports Integration Tests', () => { // Try to access example.com (in allowlist) // This verifies that requests are routed through the external proxy const command = await SandboxManager.wrapWithSandbox( - 'curl -s --max-time 5 http://example.com' + 'curl -s --max-time 5 http://example.com', ) const result = spawnSync(command, { @@ -404,14 +411,15 @@ describe('Configurable Proxy Ports Integration Tests', () => { expect(output).not.toContain('blocked by network allowlist') console.log('✓ Request to example.com succeeded through external proxy') - console.log('✓ This verifies SRT used the external proxy on the configured port') - + console.log( + '✓ This verifies SRT used the external proxy on the configured port', + ) } finally { // Clean up await SandboxManager.reset() if (externalProxyServer) { - await new Promise((resolve) => { + await new Promise(resolve => { externalProxyServer!.close(() => { console.log('External proxy server closed') resolve() @@ -422,3 +430,288 @@ describe('Configurable Proxy Ports Integration Tests', () => { }) }) }) + +/** + * Tests for custom environment variables feature + */ +describe('Custom Environment Variables Tests', () => { + afterAll(async () => { + await SandboxManager.reset() + }) + + describe('generateProxyEnvVars with customEnv', () => { + it('should include custom env vars when customEnv is provided', () => { + const customEnv = { + SSL_CERT_FILE: '/tmp/ca-bundle.crt', + MY_CUSTOM_VAR: 'my-value', + } + + const envVars = generateProxyEnvVars(3128, 1080, customEnv) + + // Should include standard proxy env vars + expect(envVars).toContain('HTTP_PROXY=http://localhost:3128') + expect(envVars).toContain('HTTPS_PROXY=http://localhost:3128') + + // Should include custom env vars + expect(envVars).toContain('SSL_CERT_FILE=/tmp/ca-bundle.crt') + expect(envVars).toContain('MY_CUSTOM_VAR=my-value') + }) + + it('should include custom env vars even without proxy ports', () => { + const customEnv = { + SSL_CERT_FILE: '/tmp/ca-bundle.crt', + } + + const envVars = generateProxyEnvVars(undefined, undefined, customEnv) + + // Should include minimal env vars + expect(envVars).toContain('SANDBOX_RUNTIME=1') + expect(envVars).toContain('TMPDIR=/tmp/claude') + + // Should include custom env vars + expect(envVars).toContain('SSL_CERT_FILE=/tmp/ca-bundle.crt') + + // Should NOT include proxy env vars + expect(envVars.some(v => v.startsWith('HTTP_PROXY='))).toBe(false) + }) + + it('should allow custom env vars to override defaults', () => { + const customEnv = { + TMPDIR: '/my/custom/tmp', // Override the default + } + + const envVars = generateProxyEnvVars(3128, 1080, customEnv) + + // Custom TMPDIR should appear after the default, effectively overriding it + const tmpDirEntries = envVars.filter(v => v.startsWith('TMPDIR=')) + expect(tmpDirEntries.length).toBe(2) + // The custom one should come last (later entries override earlier in most shells) + expect(tmpDirEntries[tmpDirEntries.length - 1]).toBe( + 'TMPDIR=/my/custom/tmp', + ) + }) + }) + + describe('SandboxManager env config', () => { + it('should store and retrieve env config', async () => { + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: [], + deniedDomains: [], + }, + filesystem: { + denyRead: [], + allowWrite: ['/tmp'], + denyWrite: [], + }, + env: { + SSL_CERT_FILE: '/tmp/ca-bundle.crt', + NODE_EXTRA_CA_CERTS: '/tmp/ca-bundle.crt', + }, + } + + await SandboxManager.initialize(config) + + const env = SandboxManager.getEnv() + expect(env).toBeDefined() + expect(env?.SSL_CERT_FILE).toBe('/tmp/ca-bundle.crt') + expect(env?.NODE_EXTRA_CA_CERTS).toBe('/tmp/ca-bundle.crt') + + await SandboxManager.reset() + }) + + it('should return undefined when env is not configured', async () => { + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: [], + deniedDomains: [], + }, + filesystem: { + denyRead: [], + allowWrite: ['/tmp'], + denyWrite: [], + }, + // No env configured + } + + await SandboxManager.initialize(config) + + const env = SandboxManager.getEnv() + expect(env).toBeUndefined() + + await SandboxManager.reset() + }) + }) +}) + +/** + * Tests for preCommand feature + */ +describe('PreCommand Tests', () => { + afterAll(async () => { + await SandboxManager.reset() + }) + + describe('SandboxManager preCommand config', () => { + it('should store and retrieve preCommand config', async () => { + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: [], + deniedDomains: [], + }, + filesystem: { + denyRead: [], + allowWrite: ['/tmp'], + denyWrite: [], + }, + preCommand: 'echo "Initializing sandbox"', + } + + await SandboxManager.initialize(config) + + const preCommand = SandboxManager.getPreCommand() + expect(preCommand).toBe('echo "Initializing sandbox"') + + await SandboxManager.reset() + }) + + it('should return undefined when preCommand is not configured', async () => { + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: [], + deniedDomains: [], + }, + filesystem: { + denyRead: [], + allowWrite: ['/tmp'], + denyWrite: [], + }, + // No preCommand configured + } + + await SandboxManager.initialize(config) + + const preCommand = SandboxManager.getPreCommand() + expect(preCommand).toBeUndefined() + + await SandboxManager.reset() + }) + }) + + describe('preCommand execution in sandbox (Linux only)', () => { + it('should execute preCommand before main command', async () => { + // Skip if not on Linux + if (getPlatform() !== 'linux') { + console.log('Skipping preCommand execution test on non-Linux platform') + return + } + + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: ['example.com'], // Need network to trigger full sandbox wrapping + deniedDomains: [], + }, + filesystem: { + denyRead: [], + allowWrite: ['/tmp'], + denyWrite: [], + }, + preCommand: 'echo "PRE_COMMAND_EXECUTED" > /tmp/precommand-test.txt', + enableWeakerNestedSandbox: true, // Needed for containerized test environments + } + + await SandboxManager.initialize(config) + + // Wrap a command that reads the file created by preCommand + const command = await SandboxManager.wrapWithSandbox( + 'cat /tmp/precommand-test.txt', + ) + + const result = spawnSync(command, { + shell: true, + encoding: 'utf8', + timeout: 10000, + }) + + // The preCommand should have created the file, and main command should read it + expect(result.stdout).toContain('PRE_COMMAND_EXECUTED') + + await SandboxManager.reset() + }) + + it('should fail if preCommand fails', async () => { + // Skip if not on Linux + if (getPlatform() !== 'linux') { + console.log('Skipping preCommand failure test on non-Linux platform') + return + } + + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: ['example.com'], + deniedDomains: [], + }, + filesystem: { + denyRead: [], + allowWrite: ['/tmp'], + denyWrite: [], + }, + preCommand: 'exit 1', // This should cause the sandbox to fail + enableWeakerNestedSandbox: true, // Needed for containerized test environments + } + + await SandboxManager.initialize(config) + + const command = await SandboxManager.wrapWithSandbox( + 'echo "Should not reach here"', + ) + + const result = spawnSync(command, { + shell: true, + encoding: 'utf8', + timeout: 10000, + }) + + // The command should fail because preCommand failed + expect(result.status).not.toBe(0) + + await SandboxManager.reset() + }) + }) +}) + +/** + * Tests for combined env and preCommand features + */ +describe('Combined env and preCommand Tests', () => { + afterAll(async () => { + await SandboxManager.reset() + }) + + describe('Using both env and preCommand together', () => { + it('should support both env and preCommand in config', async () => { + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: [], + deniedDomains: [], + }, + filesystem: { + denyRead: [], + allowWrite: ['/tmp'], + denyWrite: [], + }, + env: { + MY_VAR: 'my-value', + }, + preCommand: 'echo "Setup complete"', + } + + await SandboxManager.initialize(config) + + expect(SandboxManager.getEnv()).toEqual({ MY_VAR: 'my-value' }) + expect(SandboxManager.getPreCommand()).toBe('echo "Setup complete"') + + await SandboxManager.reset() + }) + }) +})