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

+163
View File
@@ -0,0 +1,163 @@
import assert from 'node:assert/strict'
import { promises as fs } from 'node:fs'
import { join, resolve } from 'node:path'
import { configureSandbox } from '../src/main/sandbox/configuration'
import { NativeWorkspaceWriter } from '../src/main/sandbox/filesystem-writer'
import { digest } from '../src/main/sandbox/transactions'
import { commitChanges, restoreTransaction } from '../src/main/sandbox/transactions'
import { executeSandboxCommand } from '../src/main/sandbox/service'
import { executeTool } from '../src/main/agent/tools'
import { execFileSync, type ChildProcess } from 'node:child_process'
import { randomUUID } from 'node:crypto'
if (process.platform !== 'win32') throw new Error('Native write integration requires Windows')
await fs.mkdir(resolve('.tmp'), { recursive: true })
const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-write-'))
const workspace = join(fixture, 'workspace'), stateRoot = join(fixture, 'state')
await fs.mkdir(workspace)
configureSandbox({ stateRoot, resourceDirectory: resolve('resources/sandbox/win32-x64') })
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}`) }
try {
await fs.writeFile(join(workspace, 'sample.txt'), 'before')
const writer = await NativeWorkspaceWriter.create(workspace)
try {
const before = await writer.inspect('sample.txt', true)
check('native transaction reads exact bytes under a pinned root', before.bytes?.toString() === 'before')
await writer.replace('sample.txt', digest(Buffer.from('before')), Buffer.from('after'))
check('replacement succeeds while original target remains pinned', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'after')
await assert.rejects(writer.replace('sample.txt', digest(Buffer.from('before')), Buffer.from('wrong')), /changed/)
check('stale expected content cannot overwrite the target', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'after')
} finally { await writer.close() }
await fs.mkdir(join(workspace, 'nested'))
await fs.writeFile(join(workspace, 'nested/data.bin'), Buffer.from([0, 255, 1, 127]))
const outside = join(fixture, 'outside'); await fs.mkdir(outside)
await fs.writeFile(join(outside, 'data.bin'), 'OUTSIDE_UNCHANGED')
const pinned = await NativeWorkspaceWriter.create(workspace)
try {
await pinned.inspect('nested/data.bin', true)
const moved = await fs.rename(join(workspace, 'nested'), join(workspace, 'moved')).then(() => true, () => false)
check('pinned ancestor cannot be renamed into a junction attack', !moved)
const rootMoved = await fs.rename(workspace, join(fixture, 'replaced-root')).then(() => true, () => false)
check('workspace root remains pinned for the whole transaction session', !rootMoved)
await pinned.replace('nested/data.bin', digest(Buffer.from([0, 255, 1, 127])), Buffer.alloc(0))
check('nested binary-to-empty replacement preserves byte semantics', (await fs.stat(join(workspace, 'nested/data.bin'))).size === 0)
await pinned.replace('created.bin', null, Buffer.from([0, 128, 255]))
await assert.rejects(pinned.replace('created.bin', null, Buffer.from('overwrite')), /changed/)
check('creation refuses to overwrite a target that already exists', (await fs.readFile(join(workspace, 'created.bin'))).equals(Buffer.from([0, 128, 255])))
for (const path of ['../outside/data.bin', '.env', 'stream.txt:ads', '.ycode-sandbox-owned.tmp']) await assert.rejects(pinned.replace(path, null, Buffer.from('blocked')))
check('outside, sensitive, ADS and reserved transaction paths are denied', await fs.readFile(join(outside, 'data.bin'), 'utf8') === 'OUTSIDE_UNCHANGED')
await fs.symlink(outside, join(workspace, 'junction'), 'junction')
await assert.rejects(pinned.replace('junction/data.bin', digest(Buffer.from('OUTSIDE_UNCHANGED')), Buffer.from('blocked')))
check('native relative opens reject a junction target', await fs.readFile(join(outside, 'data.bin'), 'utf8') === 'OUTSIDE_UNCHANGED')
await fs.unlink(join(workspace, 'junction'))
await fs.link(join(outside, 'data.bin'), join(workspace, 'hardlink.bin'))
await assert.rejects(pinned.replace('hardlink.bin', digest(Buffer.from('OUTSIDE_UNCHANGED')), Buffer.from('blocked')), /hard link/)
check('native replacement rejects hard-linked files', await fs.readFile(join(outside, 'data.bin'), 'utf8') === 'OUTSIDE_UNCHANGED')
await fs.unlink(join(workspace, 'hardlink.bin'))
const busy = await fs.open(join(workspace, 'sample.txt'), 'r+')
try { await assert.rejects(pinned.replace('sample.txt', digest(Buffer.from('after')), Buffer.from('blocked')), /win32=32/) }
finally { await busy.close() }
check('existing writable handles fail closed without relaxed sharing', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'after')
await fs.chmod(join(workspace, 'sample.txt'), 0o444)
try { await assert.rejects(pinned.replace('sample.txt', digest(Buffer.from('after')), Buffer.from('blocked')), /read-only/) }
finally { await fs.chmod(join(workspace, 'sample.txt'), 0o666) }
check('read-only targets are rejected before handoff', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'after')
await pinned.replace('created.bin', digest(Buffer.from([0, 128, 255])), null)
check('native deletion removes only the verified file object', !await fs.stat(join(workspace, 'created.bin')).catch(() => null))
} finally { await pinned.close() }
await fs.rename(join(workspace, 'nested'), join(workspace, 'released'))
check('session close releases ancestor pins', (await fs.stat(join(workspace, 'released'))).isDirectory())
const context = { workspaceRoot: workspace, sessionId: 'write-session', runId: 'write-run', toolCallId: 'write-call', approve: async () => true }
await fs.copyFile(resolve('resources/sandbox/win32-x64/ycode-sandbox.exe'), join(workspace, 'runner-copy.exe'))
const hostOnly = await executeSandboxCommand('runner-copy.exe --workspace-write "%CD%" -', context)
check('sandbox descendants cannot invoke the host transaction entry point', !hostOnly.ok && /host-only/.test(hostOnly.data.stderr))
await fs.unlink(join(workspace, 'runner-copy.exe'))
const tool = await executeTool('write_file', { path: 'implicit/a/b/file.txt', content: 'implicit-content' }, context)
const transactionId = (tool.data as { transactionId?: string })?.transactionId
check('default file tool commits through the native transaction backend', tool.ok && !!transactionId && await fs.readFile(join(workspace, 'implicit/a/b/file.txt'), 'utf8') === 'implicit-content')
const journal = JSON.parse(await fs.readFile(join(stateRoot, 'transactions', transactionId!, 'manifest.json'), 'utf8'))
check('journal binds root identity and records implicit parents and mutation intent', journal.version === 3 && journal.workspaceIdentity && journal.directories.length === 3 && journal.mutations.length === 1)
await restoreTransaction(transactionId!, workspace)
check('recovery removes implicitly created parents as well as file bytes', !await fs.stat(join(workspace, 'implicit')).catch(() => null))
let attempts = 0
const guardedId = await commitChanges(workspace, [{ path: 'released/data.bin', beforeHash: digest(Buffer.alloc(0)), after: Buffer.from('guarded') }], context, undefined, {
beforeApply: async () => {
for (let i = 0; i < 20; i++) if (!await fs.rename(join(workspace, 'released'), join(workspace, 'attack-moved')).then(() => true, () => false)) attempts++
}, check: async () => undefined
})
check('real commit rejects repeated ancestor replacement attempts after preflight', attempts === 20 && await fs.readFile(join(workspace, 'released/data.bin'), 'utf8') === 'guarded' && await fs.readFile(join(outside, 'data.bin'), 'utf8') === 'OUTSIDE_UNCHANGED')
await restoreTransaction(guardedId, workspace)
await fs.mkdir(join(workspace, 'delete-tree')); await fs.writeFile(join(workspace, 'delete-tree/original.txt'), 'original')
await assert.rejects(commitChanges(workspace, [{ path: 'delete-tree/original.txt', beforeHash: digest(Buffer.from('original')), after: null }], context, undefined,
{ beforeApply: async () => { await fs.writeFile(join(workspace, 'delete-tree/external.txt'), 'external') }, check: async () => undefined }, [{ path: 'delete-tree', before: true, after: false }]))
check('late directory additions stop deletion and compensate earlier file removals', await fs.readFile(join(workspace, 'delete-tree/original.txt'), 'utf8') === 'original' && await fs.readFile(join(workspace, 'delete-tree/external.txt'), 'utf8') === 'external')
await assert.rejects(commitChanges(workspace, [{ path: 'sample.txt', beforeHash: digest(Buffer.from('after')), after: Buffer.from('approved') }], context, undefined,
{ beforeApply: async () => { await fs.writeFile(join(workspace, 'sample.txt'), 'external-edit') }, check: async () => undefined }))
check('post-preflight external file edits survive a rejected commit', await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'external-edit')
for (const route of ['command', 'file-tool']) {
const targetRoot = join(fixture, `approval-${route}`), movedRoot = `${targetRoot}-old`
await fs.mkdir(targetRoot); await fs.writeFile(join(targetRoot, 'sample.txt'), 'before-approval')
const approve = async (): Promise<boolean> => { await fs.rename(targetRoot, movedRoot); await fs.mkdir(targetRoot); await fs.writeFile(join(targetRoot, 'sample.txt'), 'before-approval'); return true }
if (route === 'command') await assert.rejects(executeSandboxCommand('echo approved>sample.txt', { ...context, workspaceRoot: targetRoot, approve }), /identity changed|被替换/)
else assert.equal((await executeTool('write_file', { path: 'sample.txt', content: 'approved' }, { ...context, workspaceRoot: targetRoot, approve })).ok, false)
check(`${route} approval cannot authorize a replacement root with identical contents`, await fs.readFile(join(targetRoot, 'sample.txt'), 'utf8') === 'before-approval' && await fs.readFile(join(movedRoot, 'sample.txt'), 'utf8') === 'before-approval')
}
const rootRecovery = join(fixture, 'root-recovery'); await fs.mkdir(rootRecovery); await fs.writeFile(join(rootRecovery, 'sample.txt'), 'before')
const rootTransaction = await commitChanges(rootRecovery, [{ path: 'sample.txt', beforeHash: digest(Buffer.from('before')), after: Buffer.from('after') }])
await fs.rename(rootRecovery, `${rootRecovery}-old`); await fs.mkdir(rootRecovery); await fs.writeFile(join(rootRecovery, 'sample.txt'), 'after')
await assert.rejects(restoreTransaction(rootTransaction, rootRecovery), /identity changed/)
check('recovery refuses a same-path replacement root with matching file hashes', await fs.readFile(join(rootRecovery, 'sample.txt'), 'utf8') === 'after')
await fs.writeFile(join(workspace, 'first.txt'), 'first-before'); await fs.writeFile(join(workspace, 'second.txt'), 'second-before')
let phase = 0
const killed = await commitChanges(workspace, [
{ path: 'first.txt', beforeHash: digest(Buffer.from('first-before')), after: Buffer.from('first-after') },
{ path: 'second.txt', beforeHash: digest(Buffer.from('second-before')), after: Buffer.from('second-after') }
], context, undefined, { beforeApply: async () => undefined, check: async () => {
if (++phase !== 2) return
const query = `Get-CimInstance Win32_Process -Filter 'ParentProcessId=${process.pid}' | Where-Object { $_.Name -eq 'ycode-sandbox.exe' } | Select-Object -ExpandProperty ProcessId`
const pids = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', query], { encoding: 'utf8', windowsHide: true }).trim().split(/\s+/).map(Number).filter(Number.isSafeInteger)
assert.equal(pids.length, 1); process.kill(pids[0]); await new Promise(done => setTimeout(done, 40))
} }).then(() => null, error => error as Error & { transactionId?: string; recoveryRequired?: boolean })
check('native backend death reports recovery-required after a partial batch', killed?.recoveryRequired && !!killed.transactionId && await fs.readFile(join(workspace, 'first.txt'), 'utf8') === 'first-after')
await restoreTransaction(killed!.transactionId!, workspace)
check('a new native session recovers a batch after actual backend death', await fs.readFile(join(workspace, 'first.txt'), 'utf8') === 'first-before' && await fs.readFile(join(workspace, 'second.txt'), 'utf8') === 'second-before')
const handoff = await commitChanges(workspace, [{ path: 'first.txt', beforeHash: digest(Buffer.from('first-before')), after: Buffer.from('handoff-after') }])
const manifestPath = join(stateRoot, 'transactions', handoff, 'manifest.json'), manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'))
const intent = manifest.mutations[0], oldScratch = join(workspace, `.ycode-sandbox-${intent.nonce}-old.tmp`), newScratch = join(workspace, `.ycode-sandbox-${intent.nonce}-new.tmp`)
await fs.unlink(join(workspace, 'first.txt')); await fs.writeFile(oldScratch, 'first-before'); await fs.writeFile(newScratch, 'handoff-after')
manifest.status = 'applying'; manifest.mutations = []; await fs.writeFile(manifestPath, JSON.stringify(manifest))
await restoreTransaction(handoff, workspace)
check('interrupted-handoff disk fixture restores absent basename and cleans verified scratch', await fs.readFile(join(workspace, 'first.txt'), 'utf8') === 'first-before' && !await fs.stat(oldScratch).catch(() => null) && !await fs.stat(newScratch).catch(() => null))
const blocked = await NativeWorkspaceWriter.create(workspace)
try {
const nonce = randomUUID().replaceAll('-', ''), scratch = join(workspace, `.ycode-sandbox-${nonce}-old.tmp`)
await fs.writeFile(scratch, 'foreign-content')
await assert.rejects(blocked.cleanup('first.txt', nonce, digest(Buffer.from('first-before')), null), /changed externally/)
check('scratch cleanup refuses unrelated or externally changed contents', await fs.readFile(scratch, 'utf8') === 'foreign-content')
} finally { await blocked.close() }
const dying = await NativeWorkspaceWriter.create(workspace)
const child = Reflect.get(dying, 'child') as ChildProcess
child.kill(); await new Promise(done => setTimeout(done, 30))
await assert.rejects(dying.replace('first.txt', digest(Buffer.from('first-before')), Buffer.from('never')))
await dying.close()
check('dead native channel cannot fall back to an ordinary host write', await fs.readFile(join(workspace, 'first.txt'), 'utf8') === 'first-before')
const cliRoot = join(fixture, 'CLI work \u4e2d\u6587 ^'); await fs.mkdir(cliRoot); await fs.writeFile(join(cliRoot, 'sample.txt'), 'cli-before')
const cliId = await commitChanges(cliRoot, [{ path: 'sample.txt', beforeHash: digest(Buffer.from('cli-before')), after: Buffer.from('cli-after') }])
execFileSync(process.execPath, ['scripts/sandbox-maintain.mjs', 'restore', cliId, '--workspace', cliRoot, '--state-root', stateRoot], { windowsHide: true, timeout: 30_000, stdio: 'pipe' })
check('actual maintenance CLI restores v3 at a space Unicode caret path', await fs.readFile(join(cliRoot, 'sample.txt'), 'utf8') === 'cli-before')
for (const version of [1, 2]) {
const id = await commitChanges(cliRoot, [{ path: 'sample.txt', beforeHash: digest(Buffer.from('cli-before')), after: Buffer.from('legacy-after') }])
const path = join(stateRoot, 'transactions', id, 'manifest.json'), legacy = JSON.parse(await fs.readFile(path, 'utf8'))
legacy.version = version; delete legacy.workspaceIdentity; delete legacy.mutationJournal; delete legacy.mutations
if (version === 1) delete legacy.directories
await fs.writeFile(path, JSON.stringify(legacy))
await restoreTransaction(id, cliRoot)
const upgraded = JSON.parse(await fs.readFile(path, 'utf8'))
check(`legacy v${version} recovery uses native backend and upgrades root identity`, await fs.readFile(join(cliRoot, 'sample.txt'), 'utf8') === 'cli-before' && upgraded.version === 3 && !!upgraded.workspaceIdentity)
}
console.log(`SANDBOX_WRITE_PASS ${checks.length}/${checks.length}`)
} finally {
await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ fixture, checks }, null, 2) + '\n')
console.log(`SANDBOX_WRITE_REPORT ${join(fixture, 'report.json')}`)
}