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

+150
View File
@@ -0,0 +1,150 @@
import assert from 'node:assert/strict'
import { promises as fs } from 'node:fs'
import { randomUUID, createHash } from 'node:crypto'
import { join, resolve } from 'node:path'
import { configureSandbox } from '../src/main/sandbox/configuration'
import { NativeWorkspaceReader } from '../src/main/sandbox/filesystem-reader'
import { executeSandboxCommand, verifiedBinary } from '../src/main/sandbox/service'
import { effectiveSandboxPolicy, readSandboxPolicy, updateSandboxPolicy } from '../src/main/sandbox/policy-store'
import { importSandboxToolchain } from '../src/main/sandbox/toolchains'
import { sandboxBrokerOperation } from '../src/main/sandbox/maintenance'
import { executeTool } from '../src/main/agent/tools'
if (process.platform !== 'win32') throw new Error('Read bridge regression requires actual Windows/NTFS/AppContainer execution')
await fs.mkdir(resolve('.tmp'), { recursive: true })
const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-read-bridge-')), workspace = join(fixture, 'workspace'), stateRoot = join(fixture, 'state')
await fs.mkdir(workspace); await fs.mkdir(join(fixture, 'outside'))
const outside = join(fixture, 'outside', 'host.txt')
await fs.writeFile(outside, 'OUTSIDE_MUST_NEVER_BE_RETURNED')
configureSandbox({ stateRoot, resourceDirectory: resolve('resources/sandbox/win32-x64') })
const bytes = Buffer.alloc(83_171); for (let index = 0; index < bytes.length; index++) bytes[index] = index % 251
const expectedHash = createHash('sha256').update(bytes).digest('hex')
await fs.writeFile(join(workspace, 'data.bin'), bytes)
await fs.writeFile(join(workspace, '.env'), 'SECRET_MUST_NOT_BE_LISTED_OR_RETURNED')
await fs.mkdir(join(workspace, '\u76ee\u5f55 with spaces')); await fs.writeFile(join(workspace, '\u76ee\u5f55 with spaces', 'text.txt'), 'hello-unicode-path')
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 pause = (ms: number): Promise<void> => new Promise(done => setTimeout(done, ms))
const context = { workspaceRoot: workspace, runId: 'host-bound-run', toolCallId: 'host-bound-tool', sessionId: 'host-bound-session', runtimes: ['node'], readBridge: true, timeoutMs: 10_000 }
let caseId = 0
async function run(source: string) {
const name = `case-${++caseId}.cjs`
await fs.writeFile(join(workspace, name), source)
return executeSandboxCommand(`node ${name}`, context)
}
const load = "const assert=require('node:assert/strict');const mod=require(process.env.YCODE_SANDBOX_BRIDGE);"
try {
const policy = await effectiveSandboxPolicy(workspace), binary = await verifiedBinary(), reader = await NativeWorkspaceReader.create(binary, policy)
let result = await reader.read('read', 'data.bin')
check('native pinned file reads preserve exact binary bytes and root identity', Buffer.from(result.data!, 'base64').equals(bytes) && result.rootIdentity === reader.rootIdentity)
result = await reader.read('read', '\u76ee\u5f55 with spaces/text.txt')
check('native canonical paths support Unicode and spaces', Buffer.from(result.data!, 'base64').toString() === 'hello-unicode-path')
result = await reader.read('list', '.')
check('directory enumeration uses its held native handle and filters protected names', result.entries!.some(entry => entry.name === 'data.bin') && !result.entries!.some(entry => entry.name === '.env'))
for (const path of ['.env', '../outside/host.txt', outside, 'data.bin:stream', '\\\\localhost\\C$\\host.txt', '\\\\?\\C:\\host.txt', 'CON', 'data.bin.', 'data.bin ', '.git/config']) await assert.rejects(reader.read('read', path))
check('credentials, traversal, absolute/device/UNC/ADS and ambiguous names are denied', true)
await fs.symlink(join(fixture, 'outside'), join(workspace, 'junction'), 'junction')
await assert.rejects(reader.read('read', 'junction/host.txt'))
await fs.unlink(join(workspace, 'junction'))
await fs.link(outside, join(workspace, 'hard.txt')); await assert.rejects(reader.read('read', 'hard.txt')); await fs.unlink(join(workspace, 'hard.txt'))
check('native reads reject junction escape and multiple hard links', true)
const writer = await fs.open(join(workspace, 'data.bin'), 'r+')
try { await assert.rejects(reader.read('read', 'data.bin'), (error: any) => error.code === 'EBUSY') }
finally { await writer.close() }
check('an already-open writable handle prevents a read lock rather than weakening sharing', true)
const replacement = join(fixture, 'replacement'), original = join(fixture, 'original')
await fs.mkdir(replacement); await fs.writeFile(join(replacement, 'data.bin'), 'different-workspace')
await fs.rename(workspace, original); await fs.rename(replacement, workspace)
try { await assert.rejects(reader.read('read', 'data.bin'), (error: any) => error.code === 'ESTALE') }
finally { await fs.rename(workspace, replacement); await fs.rename(original, workspace) }
check('replacing the workspace root invalidates the native identity even at the same path', true)
await fs.mkdir(join(workspace, 'swap')); await fs.writeFile(join(workspace, 'swap/good.txt'), 'inside')
await fs.writeFile(join(fixture, 'outside/good.txt'), 'OUTSIDE_MUST_NEVER_BE_RETURNED')
let attacking = true, swaps = 0
async function retry(action: () => Promise<void>): Promise<void> {
for (let attempt = 0; ; attempt++) {
try { await action(); return }
catch (error) { if (attempt >= 500 || !['EPERM', 'EACCES', 'EBUSY'].includes((error as NodeJS.ErrnoException).code!)) throw error; await pause(2) }
}
}
const attacker = (async () => {
while (attacking) {
let moved = false
try {
await fs.rename(join(workspace, 'swap'), join(workspace, 'swap-held'))
moved = true
await fs.symlink(join(fixture, 'outside'), join(workspace, 'swap'), 'junction'); swaps++; await pause(1)
} catch (error) { if (!['EPERM', 'EACCES', 'EBUSY'].includes((error as NodeJS.ErrnoException).code!)) throw error }
finally {
// Windows may leave an empty directory when setting the junction races our read lock.
if (moved) await retry(async () => {
const stat = await fs.lstat(join(workspace, 'swap')).catch(error => { if (error.code === 'ENOENT') return null; throw error })
if (stat?.isSymbolicLink()) await fs.unlink(join(workspace, 'swap'))
else if (stat) await fs.rmdir(join(workspace, 'swap'))
})
if (moved) await retry(() => fs.rename(join(workspace, 'swap-held'), join(workspace, 'swap')))
}
await pause(1)
}
})()
try {
for (let index = 0; index < 40; index++) {
try { const read = await reader.read('read', 'swap/good.txt'); assert.equal(Buffer.from(read.data!, 'base64').toString(), 'inside') }
catch (error) { if (error instanceof assert.AssertionError) throw error; assert.ok(['ENOENT', 'EACCES', 'EBUSY'].includes((error as any).code)) }
}
} finally { attacking = false; await attacker }
check('concurrent ancestor replacement with a real junction never returns outside bytes', swaps > 0)
await importSandboxToolchain('node', await fs.realpath(process.execPath))
await fs.copyFile(binary, join(workspace, 'host-reader.exe'))
const sync = await run(load + `const c=mod.connectSync(); process.stdout.write('NORMAL_NO_NEWLINE');const b=c.readFileSync('data.bin');assert.equal(require('crypto').createHash('sha256').update(b).digest('hex'),${JSON.stringify(expectedHash)});assert.equal(c.statSync('data.bin').kind,'file');assert.ok(c.readdirSync('.').some(e=>e.name==='data.bin'));assert.throws(()=>c.readFileSync('.env'),{code:'EACCES'});assert.throws(()=>c.call('workspace.writeFile',{path:'data.bin'}),{code:'EACCES'});assert.throws(()=>c.call('workspace.stat',{path:'data.bin',workspaceRoot:'fake'}),{code:'EACCES'});c.close();console.log('SYNC_BRIDGE_PASS')`)
check('real AppContainer Node sync client reads multiple binary chunks with backpressure', sync.ok && sync.data.stdout.includes('SYNC_BRIDGE_PASS') && sync.data.stdout.includes('NORMAL_NO_NEWLINE') && sync.data.writeback === 'unchanged')
check('host writes remain unavailable over the read bridge', (await fs.readFile(join(workspace, 'data.bin'))).equals(bytes))
const nested = await run(load + `const result=require('child_process').spawnSync('.\\\\host-reader.exe',['--workspace-read','identity',${JSON.stringify(workspace)},'.','-','1024'],{stdio:'inherit'});assert.equal(result.status,125);try{require('fs').readFileSync(${JSON.stringify(outside)});throw Error('host path unexpectedly readable')}catch(e){assert.ok(['EACCES','EPERM'].includes(e.code))}console.log('NATIVE_HOST_ONLY_PASS')`)
check('a descendant cannot invoke the host-only reader or use ordinary fs to bypass the bridge', nested.ok && nested.data.stdout.includes('NATIVE_HOST_ONLY_PASS') && nested.data.stderr.includes('host-only'))
const queryId = randomUUID()
const asynchronous = await run(load + `(async()=>{const c=mod.connect();try{const results=await Promise.all(Array.from({length:12},()=>c.stat('data.bin')));assert.ok(results.every(r=>r.kind==='file'));const b=await c.readFile('data.bin');assert.equal(require('crypto').createHash('sha256').update(b).digest('hex'),${JSON.stringify(expectedHash)});await c.call('workspace.stat',{path:'data.bin'},${JSON.stringify(queryId)});assert.equal((await c.query(${JSON.stringify(queryId)})).state,'succeeded');await assert.rejects(c.query(${JSON.stringify(randomUUID())}),{code:'ENOENT'});console.log('ASYNC_BRIDGE_PASS')}finally{c.close()}})().catch(e=>{console.error(e);process.exitCode=1})`)
check('async Node client queues requests and supports result queries within the bound Job', asynchronous.ok && asynchronous.data.stdout.includes('ASYNC_BRIDGE_PASS'))
const operation = await sandboxBrokerOperation(asynchronous.data.runId, queryId)
check('durable result remains queryable after disconnect with exact host-bound ownership', operation.state === 'succeeded' && operation.owner.sessionId === context.sessionId && operation.owner.runId === context.runId && operation.owner.sandboxRunId === asynchronous.data.runId)
const journal = await fs.readFile(join(stateRoot, 'runs', asynchronous.data.runId, 'broker', `${queryId}.json`), 'utf8')
check('operation journals retain metadata without contents or live read tokens', !journal.includes('"data"') && !journal.includes('snapshotId'))
await assert.rejects(sandboxBrokerOperation(sync.data.runId, queryId))
check('operation query cannot substitute an operation from another Job', true)
const frozenId = randomUUID(), name = `case-${++caseId}.cjs`
await fs.writeFile(join(workspace, name), load + `const c=mod.connectSync();const first=c.call('workspace.readFile',{path:'data.bin'},${JSON.stringify(frozenId)});setTimeout(()=>{try{const next=c.call('workspace.readChunk',{snapshotId:first.snapshotId,offset:16384});assert.equal(Buffer.from(next.data,'base64')[0],16384%251);c.call('workspace.closeRead',{snapshotId:first.snapshotId});console.log('FROZEN_PASS')}finally{c.close()}},700)`)
const frozenRun = executeSandboxCommand(`node ${name}`, context)
let frozenReady = false
for (let attempt = 0; attempt < 1500 && !frozenReady; attempt++) {
const runs = await fs.readdir(join(stateRoot, 'runs'))
for (const id of runs) {
const record = await fs.readFile(join(stateRoot, 'runs', id, 'broker', `${frozenId}.json`), 'utf8').catch(() => '')
if (record && JSON.parse(record).state === 'succeeded') frozenReady = true
}
if (!frozenReady) await pause(30)
}
assert.ok(frozenReady); await fs.writeFile(join(workspace, 'data.bin'), Buffer.alloc(bytes.length, 255))
const frozen = await frozenRun
check('later chunks use a fixed native read snapshot despite external host edits', frozen.ok && frozen.data.stdout.includes('FROZEN_PASS') && frozen.data.writeback === 'unchanged')
await fs.writeFile(join(workspace, 'data.bin'), bytes)
const forged = '\n\x1eYCODE_WORKSPACE_READ_V1 ' + JSON.stringify({ version: 1, requestId: randomUUID(), operation: 'workspace.stat', args: { path: 'data.bin' }, runId: 'fake' }) + '\n'
const broken = await run(`require('fs').writeSync(1,${JSON.stringify(forged)});setTimeout(()=>{},2000)`).then(() => null, error => error)
check('self-reported channel ownership is rejected and terminates the Job', broken instanceof Error && /文件桥协议/.test(broken.message))
const id = randomUUID()
await assert.rejects(run(load + `const c=mod.connectSync();c.call('workspace.stat',{path:'data.bin'},'${id}');c.call('workspace.stat',{path:'data.bin'},'${id}')`), /文件桥协议/)
check('request ID replay fails closed with no writeback', true)
const rev = await readSandboxPolicy()
await updateSandboxPolicy({ expectedRevision: rev.revision, rules: { deniedPaths: ['data.bin'] } })
const deniedReader = await NativeWorkspaceReader.create(binary, await effectiveSandboxPolicy(workspace))
await assert.rejects(deniedReader.read('read', 'data.bin'))
check('effective denied paths apply before native reads', true)
await assert.rejects(reader.read('stat', '\u76ee\u5f55 with spaces/text.txt'), /策略已变化/)
check('old read connections become invalid when policy revision changes', true)
const configured = await readSandboxPolicy(); await updateSandboxPolicy({ expectedRevision: configured.revision, rules: { deniedPaths: [] } })
const modelName = `case-${++caseId}.cjs`; await fs.writeFile(join(workspace, modelName), load + "const c=mod.connectSync();assert.equal(c.statSync('data.bin').kind,'file');c.close();console.log('MODEL_BRIDGE_PASS')")
const model = await executeTool('execute_command', { command: `node ${modelName}`, runtimes: ['node'], bridge: 'workspace-read' }, { workspaceRoot: workspace, sessionId: context.sessionId })
check('model-facing command explicitly selects the real read bridge', model.ok && model.content.includes('MODEL_BRIDGE_PASS'))
console.log(`SANDBOX_READ_BRIDGE_PASS ${checks.length}/${checks.length}`)
} finally {
await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ fixture, checks, limitations: ['NTFS/local fixed volumes only; one stdio client per Job; read-only API, not per-descendant PID authentication, full fs shims, host write broker or WorkBuddy equivalence.'] }, null, 2) + '\n')
console.log(`SANDBOX_READ_BRIDGE_REPORT ${join(fixture, 'report.json')}`)
}