Files
ycode/scripts/sandbox-toolchain-regression.ts
T

104 lines
12 KiB
TypeScript

import assert from 'node:assert/strict'
import { promises as fs } from 'node:fs'
import { join, resolve } from 'node:path'
import { execFileSync } from 'node:child_process'
import { configureSandbox } from '../src/main/sandbox/configuration'
import { importSandboxToolchain, listSandboxToolchains } from '../src/main/sandbox/toolchains'
import { executeSandboxCommand, sandboxStatus } from '../src/main/sandbox/service'
import { restoreTransaction } from '../src/main/sandbox/transactions'
import { importSandboxDependencies } from '../src/main/sandbox/dependency-cache'
if (process.platform !== 'win32') throw new Error('Toolchain regression requires real Windows isolation')
await fs.mkdir(resolve('.tmp'), { recursive: true })
const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-toolchain-'))
const workspace = join(fixture, 'workspace'), stateRoot = join(fixture, 'state'), outside = join(fixture, 'outside.txt')
await fs.mkdir(workspace); await fs.writeFile(outside, 'host-only-fixture')
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}`) }
const context = { workspaceRoot: workspace, sessionId: 'toolchain-session', runId: 'toolchain-run', toolCallId: 'toolchain-call', approve: async () => true }
try {
check('Node/Python start missing instead of inheriting host PATH', (await listSandboxToolchains()).filter(item => item.kind !== 'git').every(item => item.state === 'missing'))
await assert.rejects(executeSandboxCommand('node --version', { ...context, runtimes: ['node'] }), /尚未注册/)
check('requesting a missing runtime fails closed', true)
await assert.rejects(importSandboxToolchain('node', 'relative-node.exe'), /绝对路径/)
const node = await importSandboxToolchain('node', await fs.realpath(process.execPath))
check('explicit Node import persists content-addressed file identities and npm', node.state === 'registered' && !!node.id && !!node.files && node.features!.includes('npm'))
process.env.YCODE_DUMMY_MODEL_TOKEN = 'not-a-real-credential'
let result = await executeSandboxCommand('node -e "console.log(process.version); console.log(process.env.YCODE_DUMMY_MODEL_TOKEN || \'NO_TOKEN\'); console.log(process.env.NPM_CONFIG_OFFLINE)"', { ...context, runtimes: ['node'] })
check('real Node executes inside the native sandbox with a clean environment', result.ok && result.data.stdout.includes(process.version) && result.data.stdout.includes('NO_TOKEN') && !result.data.stdout.includes('not-a-real-credential'))
check('native execution returns the exact staged runtime identity', result.data.toolchains?.[0]?.id === node.id)
await fs.writeFile(join(workspace, 'host-read.cjs'), `try { require('fs').readFileSync(${JSON.stringify(outside)});process.exit(9) } catch(error) { if(!['EACCES','EPERM'].includes(error.code))throw error; console.log('HOST_READ_DENIED') }`)
result = await executeSandboxCommand('node host-read.cjs', { ...context, runtimes: ['node'] })
check('real Node cannot read the ordinary host fixture', result.ok && result.data.stdout.includes('HOST_READ_DENIED'))
await fs.writeFile(join(workspace, 'runtime-ro.cjs'), "const fs=require('fs'),path=require('path');const target=path.join(path.dirname(process.execPath),'node_modules/npm/package.json');fs.readFileSync(target);try {fs.appendFileSync(target,'x');process.exit(9)}catch(error){if(!['EACCES','EPERM'].includes(error.code))throw error;console.log('RUNTIME_READONLY')}")
result = await executeSandboxCommand('node runtime-ro.cjs', { ...context, runtimes: ['node'] })
check('native ACLs prevent modification of the staged interpreter', result.ok && result.data.stdout.includes('RUNTIME_READONLY'))
await fs.writeFile(join(workspace, 'package.json'), JSON.stringify({ name: 'ycode-sandbox-fixture', version: '1.0.0', private: true, scripts: { build: 'node build.cjs', test: 'node test.cjs' } }))
await fs.writeFile(join(workspace, 'build.cjs'), "require('fs').writeFileSync('artifact.txt','built-in-sandbox')")
await fs.writeFile(join(workspace, 'test.cjs'), "require('assert').equal(require('fs').readFileSync('artifact.txt','utf8'),'built-in-sandbox');console.log('NODE_PROJECT_PASS')")
result = await executeSandboxCommand('npm run build && npm test', { ...context, runtimes: ['node'] })
check('npm builds and tests a real offline project in the snapshot', result.ok && result.data.stdout.includes('NODE_PROJECT_PASS') && result.data.writeback === 'committed')
check('npm build artifact reaches the host only through approved transaction', await fs.readFile(join(workspace, 'artifact.txt'), 'utf8') === 'built-in-sandbox' && !!result.data.transactionId)
await restoreTransaction(result.data.transactionId!, workspace)
const pythonSource = execFileSync('where.exe', ['python'], { encoding: 'utf8', windowsHide: true }).trim().split(/\r?\n/)[0]
const python = await importSandboxToolchain('python', await fs.realpath(pythonSource))
check('Python standard library and pip are imported without executing source on the host', python.state === 'registered' && python.features!.includes('pip'))
result = await executeSandboxCommand('python -c "import sys,json,ssl,sqlite3; print(\'PYTHON_STDLIB_PASS\'); print(sys.prefix)"', { ...context, runtimes: ['python'] })
check('real Python loads its private standard library and extension modules', result.ok && result.data.stdout.includes('PYTHON_STDLIB_PASS') && !result.data.stdout.includes(pythonSource))
result = await executeSandboxCommand('python -m pip --version', { ...context, runtimes: ['python'] })
check('pip runs from the controlled Python runtime', result.ok && /pip \d/.test(result.data.stdout))
await fs.writeFile(join(workspace, 'test_fixture.py'), "from pathlib import Path\nimport unittest\nclass Fixture(unittest.TestCase):\n def test_write(self):\n Path('python-artifact.txt').write_text('python-built',encoding='utf-8')\n self.assertEqual(Path('python-artifact.txt').read_text(), 'python-built')\n")
result = await executeSandboxCommand('python -m unittest test_fixture', { ...context, runtimes: ['python'] })
check('Python tests execute and write approved artifacts', result.ok && result.data.writeback === 'committed' && await fs.readFile(join(workspace, 'python-artifact.txt'), 'utf8') === 'python-built')
await restoreTransaction(result.data.transactionId!, workspace)
check('runtime status exposes separate registered Node and Python capabilities', (await sandboxStatus()).toolchains?.filter(item => item.kind !== 'git').every(item => item.state === 'registered'))
await fs.mkdir(join(workspace, 'node_modules/fixture-package'), { recursive: true })
await fs.writeFile(join(workspace, 'node_modules/fixture-package/package.json'), JSON.stringify({ name: 'fixture-package', version: '1.0.0', exports: { require: './index.cjs', import: './esm.mjs' } }))
await fs.writeFile(join(workspace, 'node_modules/fixture-package/index.cjs'), "module.exports={value:'DEPENDENCY_CJS_PASS'}")
await fs.writeFile(join(workspace, 'node_modules/fixture-package/esm.mjs'), "export const value='DEPENDENCY_ESM_PASS'")
await fs.writeFile(join(workspace, 'node_modules/fixture-package/.env'), 'DUMMY_PACKAGE_CREDENTIAL')
const nodeDependencies = await importSandboxDependencies(workspace, 'node')
check('dependency import binds workspace lock inputs and runtime identity while filtering credentials', nodeDependencies.excluded === 1 && nodeDependencies.identity.runtimeId === node.id && nodeDependencies.files === 3)
result = await executeSandboxCommand('node -e "console.log(require(\'fixture-package\').value)" && node --input-type=module -e "import {value} from \'fixture-package\';console.log(value)"', { ...context, runtimes: ['node'], dependencies: ['node'] })
check('controlled node_modules supports real CommonJS and ESM resolution', result.ok && result.data.stdout.includes('DEPENDENCY_CJS_PASS') && result.data.stdout.includes('DEPENDENCY_ESM_PASS') && result.data.writeback === 'unchanged')
check('execution records the exact workspace dependency layer identity', result.data.dependencies?.[0]?.id === nodeDependencies.identity.id)
await fs.writeFile(join(workspace, 'dep-ro.cjs'), "const fs=require('fs'),p=require.resolve('fixture-package');fs.readFileSync(p);try{fs.appendFileSync(p,'x');process.exit(9)}catch(error){if(!['EACCES','EPERM'].includes(error.code))throw error;console.log('DEPENDENCY_READONLY')}")
result = await executeSandboxCommand('node dep-ro.cjs', { ...context, runtimes: ['node'], dependencies: ['node'] })
check('native ACLs enforce dependency-layer immutability', result.ok && result.data.stdout.includes('DEPENDENCY_READONLY'))
await fs.writeFile(join(workspace, 'package-lock.json'), '{}')
await assert.rejects(executeSandboxCommand('node --version', { ...context, runtimes: ['node'], dependencies: ['node'] }), /锁文件已变化/)
check('changed project lock inputs invalidate a cached dependency layer', true)
await fs.unlink(join(workspace, 'package-lock.json'))
const anotherWorkspace = join(fixture, 'another-workspace'); await fs.mkdir(anotherWorkspace)
await assert.rejects(executeSandboxCommand('node --version', { ...context, workspaceRoot: anotherWorkspace, runtimes: ['node'], dependencies: ['node'] }), /ENOENT/)
check('one workspace cannot accidentally select another workspace dependency cache', true)
await fs.mkdir(join(workspace, '.venv/Lib/site-packages/fixture_module'), { recursive: true })
await fs.writeFile(join(workspace, '.venv/Lib/site-packages/fixture_module/__init__.py'), "value='PYTHON_DEPENDENCY_PASS'\n")
const pythonDependencies = await importSandboxDependencies(workspace, 'python')
result = await executeSandboxCommand('python -c "import fixture_module;print(fixture_module.value)"', { ...context, runtimes: ['python'], dependencies: ['python'] })
check('Python imports a workspace-scoped controlled site-packages layer', result.ok && result.data.stdout.includes('PYTHON_DEPENDENCY_PASS') && result.data.dependencies?.[0]?.id === pythonDependencies.identity.id)
const cacheKey = (await fs.readdir(join(stateRoot, 'dependencies')))[0]
const cacheFile = join(stateRoot, 'dependencies', cacheKey, 'objects', nodeDependencies.identity.id, 'files/fixture-package/index.cjs')
const cacheBytes = await fs.readFile(cacheFile)
await fs.appendFile(cacheFile, 'tampered')
await assert.rejects(executeSandboxCommand('node --version', { ...context, runtimes: ['node'], dependencies: ['node'] }), /依赖文件摘要不匹配/)
check('a tampered dependency layer fails closed before execution', true)
await fs.writeFile(cacheFile, cacheBytes)
const alias = JSON.parse(await fs.readFile(join(stateRoot, 'toolchains/node.json'), 'utf8'))
const executable = join(stateRoot, 'toolchains/objects', alias.id, 'files/node.exe')
const bytes = await fs.readFile(executable)
await fs.appendFile(executable, 'tampered')
await assert.rejects(executeSandboxCommand('node --version', { ...context, runtimes: ['node'] }), /摘要不匹配/)
check('a tampered registered interpreter is rejected before native execution', true)
await fs.writeFile(executable, bytes)
await assert.rejects(executeSandboxCommand('echo bad', { ...context, runtimes: ['node', 'node'] }), /选择无效/)
await assert.rejects(executeSandboxCommand('echo bad', { ...context, runtimes: ['../host'] }), /只支持/)
check('runtime IDs cannot be duplicated or used as arbitrary source paths', true)
console.log(`SANDBOX_TOOLCHAIN_PASS ${checks.length}/${checks.length}`)
} finally {
delete process.env.YCODE_DUMMY_MODEL_TOKEN
await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ checks, fixture, limitations: ['This suite covers Node/npm, Python/pip and immutable dependency layers; private Git repository coverage is in test:sandbox:git. Host Git metadata integration, network installs, persistent mutable workspace, pnpm link layouts and full native-addon compatibility remain pending.'] }, null, 2) + '\n')
console.log(`SANDBOX_TOOLCHAIN_REPORT ${join(fixture, 'report.json')}`)
}