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 => 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): Promise { 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')}`) }