Files
ycode/scripts/sandbox-feasibility.ts
T

92 lines
7.3 KiB
TypeScript

import assert from 'node:assert/strict'
import { promises as fs } from 'node:fs'
import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { release, arch } from 'node:os'
import { join, resolve } from 'node:path'
import { configureSandbox } from '../src/main/sandbox/configuration'
import { executeSandboxCommand } from '../src/main/sandbox/service'
if (process.platform !== 'win32') throw new Error('P0 mechanisms must be tested on Windows')
await fs.mkdir(resolve('.tmp'), { recursive: true })
const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-feasibility-')), workspace = join(fixture, 'workspace')
await fs.mkdir(workspace)
const probe = resolve('resources/sandbox/win32-x64/sandbox-probe.exe'), registryId = randomUUID()
configureSandbox({ stateRoot: join(fixture, 'state'), resourceDirectory: resolve('resources/sandbox/win32-x64') })
await fs.copyFile(probe, join(workspace, 'sandbox-probe.exe'))
const checks: Array<{ label: string; pass: boolean }> = [], observations: Record<string, unknown> = { os: release(), architecture: arch() }
function check(label: string, condition: unknown): void { checks.push({ label, pass: !!condition }); assert.ok(condition, label); console.log(`PASS ${label}`) }
function host(args: string[]): Promise<{ code: number | null; stdout: string }> {
return new Promise((done, reject) => {
const child = spawn(probe, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''
const timeout = setTimeout(() => child.kill(), 8000)
child.stdout.on('data', chunk => { stdout += chunk }); child.stderr.on('data', chunk => { stdout += chunk })
child.once('error', error => { clearTimeout(timeout); reject(error) }); child.once('close', code => { clearTimeout(timeout); done({ code, stdout }) })
})
}
try {
observations.host = JSON.parse((await host(['--host-facts'])).stdout)
const created = await host(['--registry-create', registryId])
check('registry fixture is created only under a UUID-owned test key', created.code === 0)
const before = await host(['--registry-read', registryId])
check('host positive control reads the exact fake registry value', before.code === 0 && before.stdout.includes('1193046'))
const context = { workspaceRoot: workspace, runId: 'p0-mechanisms', toolCallId: 'registry' }
const read = await executeSandboxCommand(`sandbox-probe.exe --registry-read ${registryId}`, context)
const write = await executeSandboxCommand(`sandbox-probe.exe --registry-write ${registryId}`, context)
observations.registry = { read: { ok: read.ok, stdout: read.data.stdout }, write: { ok: write.ok, stdout: write.data.stdout } }
check('AppContainer cannot read the host-owned registry fixture value', !read.ok && !read.data.stdout.includes('1193046'))
const after = await host(['--registry-read', registryId])
check('AppContainer registry writes cannot change the host fixture', after.code === 0 && after.stdout === before.stdout)
check('invalid registry identities are rejected without arbitrary key access', (await host(['--registry-create', '../outside'])).code === 2)
const directory = join(fixture, 'locked'), renamed = join(fixture, 'renamed'), ready = join(fixture, 'ready'), unlock = join(fixture, 'unlock')
await fs.mkdir(directory); await fs.writeFile(join(directory, 'sample.txt'), 'test-only')
const held = host(['--hold-directory', directory, ready, unlock])
for (let attempt = 0; attempt < 100 && !await fs.stat(ready).catch(() => null); attempt++) await new Promise(done => setTimeout(done, 20))
check('native directory handle acquires its test-only object before replacement attempt', !!await fs.stat(ready).catch(() => null))
const replacement = await fs.rename(directory, renamed).then(() => null, error => error as NodeJS.ErrnoException)
observations.directoryReplacement = { error: replacement?.code ?? null }
const blocked = !!replacement && ['EPERM', 'EACCES', 'EBUSY'].includes(replacement.code ?? '')
await fs.writeFile(unlock, 'release')
check('native directory-handle probe exits and releases ownership', (await held).code === 0)
check('no-delete-sharing directory handle blocks replacement while held', blocked)
await fs.rename(directory, renamed)
check('positive control can rename the directory after handle release', await fs.readFile(join(renamed, 'sample.txt'), 'utf8') === 'test-only')
if (process.argv.includes('--probe-pipe')) {
const pipeId = randomUUID(), runs = join(fixture, 'state/runs')
const client = executeSandboxCommand(`sandbox-probe.exe --pipe-client ${pipeId}`, { ...context, toolCallId: 'p0-pipe', timeoutMs: 6000 })
let profile: string | undefined
for (let attempt = 0; attempt < 100 && !profile; attempt++) {
for (const entry of await fs.readdir(runs)) {
const record = await fs.readFile(join(runs, entry, 'request.json'), 'utf8').then(raw => JSON.parse(raw), () => null)
if (record?.identity?.toolCallId === 'p0-pipe') profile = record.profile
}
if (!profile) await new Promise(done => setTimeout(done, 10))
}
assert.ok(profile, 'pipe scenario request must provide a host-owned profile identity')
const server = host(['--pipe-server', pipeId, profile])
const clientResult = await client, serverResult = await server
observations.authenticatedPipe = { client: { ok: clientResult.ok, stdout: clientResult.data.stdout }, server: serverResult }
const available = clientResult.ok && clientResult.data.stdout.includes('BROKER_PONG') && serverResult.code === 0 && serverResult.stdout.includes('PIPE_AUTHENTICATED')
observations.brokerPipeAvailable = available
if (available) check('host-scoped broker pipe accepts and authenticates the actual AppContainer client', true)
else {
check('unavailable broker transport cannot enable host execution or writeback', !clientResult.ok && clientResult.data.isolated && clientResult.data.sandbox === 'windows-appcontainer' && clientResult.data.writeback === 'not-applied' && !clientResult.data.changes.length)
console.log('SANDBOX_PIPE_UNAVAILABLE: experimental transport remains gated; see observations')
if (process.argv.includes('--require-pipe')) throw new Error('Authenticated broker pipe feasibility has not passed')
}
}
console.log(`SANDBOX_FEASIBILITY_PASS ${checks.length}/${checks.length}`)
} finally {
const cleanup = await host(['--registry-delete', registryId]).catch(error => ({ code: -1, stdout: String(error) }))
observations.registryCleanup = cleanup
await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ checks, observations, fixture, limitations: [
'Registry results describe the current AppContainer and fake HKCU key only; no configurable registry policy is implemented.',
'Holding one directory is a candidate primitive, not proof of safe ancestor traversal or a complete file broker.',
'The pipe probe exchanges a fixed echo only; it does not implement operations, policy dispatch, queue bounds or descendant Job membership authentication.',
'Controlled egress, descendant program policy, restricted ConPTY and hard disk quotas require further prototypes.',
'This host permission level and OS are the only tested environment; no original WorkBuddy execution occurred.'
] }, null, 2) + '\n')
console.log(`SANDBOX_FEASIBILITY_REPORT ${join(fixture, 'report.json')}`)
assert.equal(cleanup.code, 0, 'test registry key must be cleaned')
}