import assert from 'node:assert/strict' import { promises as fs } from 'node:fs' import { createServer } from 'node:net' import { join, resolve } from 'node:path' import { configureSandbox } from '../src/main/sandbox/configuration' import { executeSandboxCommand, sandboxStatus } from '../src/main/sandbox/service' import { commitChanges, digest, restoreTransaction } from '../src/main/sandbox/transactions' import { executeTool } from '../src/main/agent/tools' import { releaseRunLease } from '../src/main/sandbox/maintenance' import { spawn } from 'node:child_process' import { randomUUID } from 'node:crypto' if (process.platform !== 'win32') throw new Error('Native sandbox regression must run on Windows; unsupported hosts must not report a pass') await fs.mkdir(resolve('.tmp'), { recursive: true }) const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-regression-')) const workspace = join(fixture, 'workspace with spaces'), outside = join(fixture, 'outside.txt') await fs.mkdir(workspace) await fs.writeFile(outside, 'outside-native-fixture') await fs.writeFile(join(workspace, 'sample.txt'), 'before') await fs.writeFile(join(workspace, '.env'), 'DUMMY_SECRET=not-a-real-secret') await fs.copyFile(resolve('resources/sandbox/win32-x64/sandbox-probe.exe'), join(workspace, 'sandbox-probe.exe')) const stateRoot = join(fixture, 'state'), resourceDirectory = resolve('resources/sandbox/win32-x64') configureSandbox({ stateRoot, resourceDirectory }) const checks: Array<{ label: string; pass: boolean }> = [] function check(label: string, condition: unknown): void { checks.push({ label, pass: !!condition }) assert.ok(condition, label) console.log(`PASS ${label}`) } const context = { workspaceRoot: workspace, runId: 'test-run', toolCallId: 'test-tool', approve: async () => true } try { check('native binary and manifest identity are installed', (await sandboxStatus()).state === 'installed') process.env.YCODE_FAKE_SECRET = 'dummy-sensitive-fixture' let result = await executeSandboxCommand('sandbox-probe.exe --info', context) const initialAuditRunId = result.data.runId check('real child is an AppContainer in a Job and has no inherited dummy credential', result.ok && result.data.stdout.includes('"appContainer":true,"job":true,"secretInherited":false')) check('read-only command needs no writeback and native profile is removed', result.data.writeback === 'unchanged' && result.data.profileRemoved) check('temporary drive mapping is removed on normal completion', result.data.driveRemoved) const cleanup = JSON.parse(await fs.readFile(join(stateRoot, 'runs', result.data.runId, 'cleanup.json'), 'utf8')) check('completed scratch copy is removed while audit remains', cleanup.stageRemoved && !(await fs.stat(join(stateRoot, 'runs', result.data.runId, 'stage')).catch(() => null))) result = await executeSandboxCommand('dir /b', context) check('cmd directory enumeration works in mapped scratch drive', result.ok && result.data.stdout.includes('sample.txt')) result = await executeSandboxCommand('powershell.exe -NoProfile -NonInteractive -Command "Get-Content sample.txt; Get-ChildItem -Name"', context) check('PowerShell can read and enumerate the scratch workspace', result.ok && result.data.stdout.includes('before') && result.data.stdout.includes('sample.txt')) delete process.env.YCODE_FAKE_SECRET result = await executeSandboxCommand(`sandbox-probe.exe --read "${outside}"`, context) check('native process cannot read an ordinary host file outside the snapshot', !result.ok && result.data.stdout.includes('READ_DENIED')) result = await executeSandboxCommand(`sandbox-probe.exe --write "${outside}"`, context) check('native process cannot overwrite outside file', !result.ok && result.data.stdout.includes('WRITE_DENIED')) check('outside file remains byte-identical', await fs.readFile(outside, 'utf8') === 'outside-native-fixture') result = await executeSandboxCommand(`sandbox-probe.exe --spawn-write "${outside}"`, context) check('descendant process inherits isolation', !result.ok && result.data.stdout.includes('WRITE_DENIED')) result = await executeSandboxCommand('sandbox-probe.exe --breakaway escaped.txt', context) check('Job denies explicit process breakaway', !result.ok && result.data.stdout.includes('SPAWN_DENIED')) result = await executeSandboxCommand('type .env', context) check('dummy credential file is excluded from command snapshot', !result.ok && !result.data.stdout.includes('DUMMY_SECRET')) let approvals = 0 result = await executeSandboxCommand('echo approved>sample.txt', { ...context, approve: async () => { approvals++; return true } }) check('workspace changes are written back after exactly one approval', result.ok && approvals === 1 && result.data.writeback === 'committed') check('before/after identity and independent transaction are returned', result.data.changes[0]?.operation === 'update' && !!result.data.transactionId) check('approved native write reaches actual workspace', (await fs.readFile(join(workspace, 'sample.txt'), 'utf8')).trim() === 'approved') await restoreTransaction(result.data.transactionId!, workspace) await restoreTransaction(result.data.transactionId!, workspace) check('independent backup restores exact bytes and recovery is idempotent', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'before') result = await executeSandboxCommand('echo rejected>sample.txt', { ...context, approve: async () => false }) check('rejected writeback reports failure without host mutation', !result.ok && result.data.writeback === 'rejected' && await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'before') const conflict = await executeSandboxCommand('echo pending>sample.txt', { ...context, approve: async () => { await fs.writeFile(join(workspace, 'sample.txt'), 'external-change'); return true } }).then(() => null, error => error as Error) check('approval-time external edit causes a conflict', conflict instanceof Error) check('conflict does not overwrite external changes', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'external-change') await fs.writeFile(join(workspace, 'sample.txt'), 'before') result = await executeSandboxCommand('del sample.txt', context) check('delete is an approved backed-up transaction', result.ok && result.data.changes[0]?.operation === 'delete' && !(await fs.stat(join(workspace, 'sample.txt')).catch(() => null))) await restoreTransaction(result.data.transactionId!, workspace) check('deleted file restores from independent backup', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'before') result = await executeSandboxCommand('echo new>created.txt', context) check('new file is recorded as create', result.ok && result.data.changes.some(change => change.operation === 'create')) await restoreTransaction(result.data.transactionId!, workspace) check('restoring creation removes only the unchanged created file', !(await fs.stat(join(workspace, 'created.txt')).catch(() => null))) result = await executeSandboxCommand('echo uncommitted>sample.txt & exit /b 7', context) check('unsuccessful command never writes back', !result.ok && result.data.exitCode === 7 && await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'before') const cancelled = new AbortController() const pending = executeSandboxCommand('sandbox-probe.exe --delay 4000 late.txt', { ...context, signal: cancelled.signal }) setTimeout(() => cancelled.abort(), 700) result = await pending check('cancellation terminates native command and removes profile', !result.ok && result.data.status === 'cancelled' && result.data.profileRemoved) result = await executeSandboxCommand('sandbox-probe.exe --delay 4000 timed-out.txt', { ...context, timeoutMs: 300 }) check('native timeout is enforced', !result.ok && result.data.status === 'timeout' && result.data.profileRemoved) result = await executeSandboxCommand('sandbox-probe.exe --spawn-delayed descendant-late.txt', context) await new Promise(done => setTimeout(done, 1600)) const descendantPath = join(stateRoot, 'runs', result.data.runId, 'stage/workspace/descendant-late.txt') check('background descendants are terminated when the root command exits', !(await fs.stat(descendantPath).catch(() => null))) const orphanId = randomUUID(), orphanDirectory = join(stateRoot, 'runs', orphanId), orphanStage = join(orphanDirectory, 'stage') for (const part of ['workspace', 'tools', 'tmp', 'home/AppData/Roaming', 'home/AppData/Local']) await fs.mkdir(join(orphanStage, part), { recursive: true }) await fs.copyFile(join(workspace, 'sandbox-probe.exe'), join(orphanStage, 'workspace/sandbox-probe.exe')) await fs.writeFile(join(orphanStage, 'command.cmd'), '@echo off\r\nsandbox-probe.exe --delay 5000 crash-late.txt\r\n') const crashed = spawn(join(resourceDirectory, 'ycode-sandbox.exe'), ['--run', `YCode.Run.${orphanId.replaceAll('-', '')}`, orphanStage, join(orphanDirectory, 'result.json'), '10000'], { windowsHide: true, stdio: ['pipe', 'ignore', 'ignore'] }) for (let attempt = 0; attempt < 100 && !await fs.stat(join(orphanDirectory, 'lease.json')).catch(() => null); attempt++) await new Promise(done => setTimeout(done, 20)) check('native leases are persisted outside the child writable area', !!await fs.stat(join(orphanDirectory, 'lease.json')).catch(() => null)) const closed = new Promise(done => crashed.once('close', done)) crashed.kill(); await closed check('maintenance releases crash-orphaned AppContainer and exact drive mapping', await releaseRunLease(join(resourceDirectory, 'ycode-sandbox.exe'), orphanDirectory) === 'released') const parentId = randomUUID(), parentDirectory = join(stateRoot, 'runs', parentId), parentStage = join(parentDirectory, 'stage') for (const part of ['workspace', 'tools', 'tmp', 'home/AppData/Roaming', 'home/AppData/Local']) await fs.mkdir(join(parentStage, part), { recursive: true }) await fs.copyFile(join(workspace, 'sandbox-probe.exe'), join(parentStage, 'workspace/sandbox-probe.exe')) await fs.writeFile(join(parentStage, 'command.cmd'), '@echo off\r\nsandbox-probe.exe --delay 5000 parent-late.txt\r\n') const parentScript = join(fixture, 'parent-exit.cjs') await fs.writeFile(parentScript, `const fs=require('node:fs');const cp=require('node:child_process');const a=process.argv.slice(2);cp.spawn(a[0],a.slice(1),{windowsHide:true,detached:true,stdio:'ignore'});const t=setInterval(()=>{if(fs.existsSync(${JSON.stringify(join(parentDirectory, 'lease.json'))})){clearInterval(t);setTimeout(()=>process.exit(0),250)}},20);setTimeout(()=>process.exit(2),5000).unref();`) const shortParent = spawn(process.execPath, [parentScript, join(resourceDirectory, 'ycode-sandbox.exe'), '--run', `YCode.Run.${parentId.replaceAll('-', '')}`, parentStage, join(parentDirectory, 'result.json'), '10000'], { windowsHide: true, stdio: 'ignore' }) await new Promise(done => shortParent.once('close', done)) for (let attempt = 0; attempt < 250 && !await fs.stat(join(parentDirectory, 'result.json')).catch(() => null); attempt++) await new Promise(done => setTimeout(done, 20)) const parentReport = JSON.parse(await fs.readFile(join(parentDirectory, 'result.json'), 'utf8')) check('launch-parent exit terminates the Job and cleans native resources', parentReport.status === 'parent-exited' && parentReport.profileRemoved && parentReport.driveRemoved) let connections = 0 const server = createServer(socket => { connections++; socket.end('local-test') }) await new Promise(done => server.listen(0, '127.0.0.1', done)) try { const address = server.address(); assert.ok(address && typeof address !== 'string') result = await executeSandboxCommand(`sandbox-probe.exe --tcp ${address.port}`, { ...context, timeoutMs: 4000 }) check('real local TCP connection is denied by capability-free AppContainer', !result.ok && result.data.stdout.includes('TCP_DENIED')) check('local TCP listener observes no sandbox connection', connections === 0) } finally { await new Promise(done => server.close(() => done())) } const defaultContext = { ...context, permissionMode: 'default-sandbox' as const } let tool = await executeTool('read_file', { path: '.env' }, defaultContext) check('host file tool shares the sensitive-file policy', !tool.ok) tool = await executeTool('write_file', { path: '.env', content: 'blocked' }, defaultContext) check('host writes cannot replace a protected credential file', !tool.ok && (await fs.readFile(join(workspace, '.env'), 'utf8')).includes('DUMMY_SECRET')) tool = await executeTool('read_file', { path: outside }, defaultContext) check('host file tool rejects outside reads', !tool.ok) await fs.symlink(join(fixture), join(workspace, 'outside-link'), 'junction') tool = await executeTool('read_file', { path: 'outside-link/outside.txt' }, defaultContext) check('host file tool rejects junction escape', !tool.ok) tool = await executeTool('write_file', { path: 'outside-link/outside.txt', content: 'explicit-full-access' }, { ...defaultContext, permissionMode: 'full-access' }) check('explicit full access preserves existing link-target write behavior', tool.ok && (tool.data as { path: string }).path === outside && await fs.readFile(outside, 'utf8') === 'explicit-full-access') await fs.writeFile(outside, 'outside-native-fixture') await fs.link(outside, join(workspace, 'outside-hardlink.txt')) tool = await executeTool('read_file', { path: 'outside-hardlink.txt' }, defaultContext) check('host file tool rejects multi-link alias', !tool.ok) await fs.unlink(join(workspace, 'outside-hardlink.txt')) await fs.unlink(join(workspace, 'outside-link')) tool = await executeTool('write_file', { path: 'host.txt', content: 'protected-write' }, defaultContext) const data = tool.data as { transactionId?: string } check('host write records a persistent independent transaction', tool.ok && !!data.transactionId) await restoreTransaction(data.transactionId!, workspace) check('host write backup supports recovery', !(await fs.stat(join(workspace, 'host.txt')).catch(() => null))) const signal = new AbortController() tool = await executeTool('write_file', { path: 'cancelled-host.txt', content: 'blocked' }, { ...defaultContext, signal: signal.signal, approve: async () => { signal.abort(); return true } }) check('cancellation during host approval does not write', !tool.ok && !(await fs.stat(join(workspace, 'cancelled-host.txt')).catch(() => null))) tool = await executeTool('execute_command', { command: 'echo host-route' }, defaultContext) check('actual default host command uses native sandbox route', tool.ok && (tool.data as { sandbox?: string }).sandbox === 'windows-appcontainer') tool = await executeTool('execute_command', { command: 'mkdir empty-only' }, defaultContext) check('empty-directory writeback uses an approved directory transaction', tool.ok && (await fs.stat(join(workspace, 'empty-only'))).isDirectory()) await restoreTransaction((tool.data as { transactionId: string }).transactionId, workspace) tool = await executeTool('execute_command', { command: 'echo bypass>.env' }, defaultContext) check('command-generated protected file never reaches the host', !tool.ok && (await fs.readFile(join(workspace, '.env'), 'utf8')).includes('DUMMY_SECRET')) result = await executeSandboxCommand('echo unicode>unicode-\u4e2d\u6587.txt', context) check('UTF-8 script and Unicode file writeback work', result.ok && (await fs.readFile(join(workspace, 'unicode-\u4e2d\u6587.txt'), 'utf8')).trim() === 'unicode') await restoreTransaction(result.data.transactionId!, workspace) result = await executeSandboxCommand('echo committed>audit-fault.txt', { ...context, toolCallId: 'audit-fault', approve: async () => { for (const entry of await fs.readdir(join(stateRoot, 'runs'))) { const record = await fs.readFile(join(stateRoot, 'runs', entry, 'request.json'), 'utf8').then(raw => JSON.parse(raw), () => null) if (record?.identity?.toolCallId === 'audit-fault') await fs.mkdir(join(stateRoot, 'runs', entry, 'audit.json')) } return true } }) check('audit storage failure preserves the actual committed outcome', result.ok && result.data.writeback === 'committed' && !result.data.auditSaved && (await fs.readFile(join(workspace, 'audit-fault.txt'), 'utf8')).trim() === 'committed') await restoreTransaction(result.data.transactionId!, workspace) const transaction = await commitChanges(workspace, [{ path: 'sample.txt', beforeHash: digest(Buffer.from('before')), after: Buffer.from('after') }], context) await fs.writeFile(join(workspace, 'sample.txt'), 'new-external-change') await assert.rejects(restoreTransaction(transaction, workspace), /外部修改/) check('recovery refuses to overwrite subsequent external edit', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'new-external-change') configureSandbox({ stateRoot, resourceDirectory: join(fixture, 'missing-backend') }) tool = await executeTool('execute_command', { command: 'echo unsafe>fallback.txt' }, defaultContext) check('missing native backend fails closed without shell fallback', !tool.ok && !(await fs.stat(join(workspace, 'fallback.txt')).catch(() => null))) const tampered = join(fixture, 'tampered-backend') await fs.mkdir(tampered) await fs.copyFile(join(resourceDirectory, 'ycode-sandbox.exe'), join(tampered, 'ycode-sandbox.exe')) const badManifest = JSON.parse(await fs.readFile(join(resourceDirectory, 'manifest.json'), 'utf8')); badManifest.binarySha256 = '0'.repeat(64) await fs.writeFile(join(tampered, 'manifest.json'), JSON.stringify(badManifest)) configureSandbox({ stateRoot, resourceDirectory: tampered }) check('manifest mismatch marks the backend unavailable', (await sandboxStatus()).state === 'unavailable') tool = await executeTool('execute_command', { command: 'echo unsafe>fallback.txt' }, defaultContext) check('manifest mismatch cannot enable a host fallback', !tool.ok && !(await fs.stat(join(workspace, 'fallback.txt')).catch(() => null))) configureSandbox({ stateRoot, resourceDirectory }) const audit = JSON.parse(await fs.readFile(join(stateRoot, 'runs', initialAuditRunId, 'audit.json'), 'utf8')) check('audit persists identity and excludes raw stdout/stderr', audit.identity.runId === 'test-run' && !Object.hasOwn(audit, 'stdout') && !Object.hasOwn(audit, 'stderr')) console.log(`SANDBOX_REGRESSION_PASS ${checks.length}/${checks.length}`) } finally { delete process.env.YCODE_FAKE_SECRET await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ checks, fixture, limitations: ['Windows AppContainer boundary and YCode host tools; not WorkBuddy equivalence, arbitrary native hooks, registry parity or all possible network protocols.'] }, null, 2) + '\n') console.log(`SANDBOX_REPORT ${join(fixture, 'report.json')}`) }