feat: complete WorkBuddy parity and Windows release workflow

This commit is contained in:
xiaohan.yuan committed 2026-10-18 12:45:21 +08:00
1 parent 143516b3a8
commit d5512df549
434 files changed
+54977 -1854

No files matched your search

+191
View File
@@ -0,0 +1,191 @@
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<void>(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<void>(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')}`)
}