mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 20:45:11 +02:00
285 lines
17 KiB
JavaScript
285 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
// The installed tier router owns provisioning and cleanup. This acceptance
|
|
// driver transfers only an npm archive and a fixed verifier into the VM.
|
|
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const http = require('node:http');
|
|
const net = require('node:net');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawn } = require('node:child_process');
|
|
|
|
const NODE_VERSION = '22.18.0';
|
|
const NODE_SHA = '2c12913cba67af77ded8a399df3fd91c2e7f8628c7079da40bb9ff33bf00dfc0';
|
|
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
const quote = text => `'${String(text).replace(/'/g, `'"'"'`)}'`;
|
|
|
|
function command(executable, args, cwd, timeout = 900000) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(executable, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false });
|
|
let stdout = ''; let stderr = ''; let size = 0; let termination = null; let settled = false;
|
|
const stop = reason => {
|
|
if (!termination) termination = reason;
|
|
child.kill('SIGKILL');
|
|
};
|
|
const timer = setTimeout(() => stop('timeout'), timeout);
|
|
const collect = key => chunk => {
|
|
size += chunk.length;
|
|
if (size > 24 * 1024 * 1024) { stop('output-limit'); return; }
|
|
if (key === 'stdout') stdout += chunk; else stderr += chunk;
|
|
};
|
|
child.stdout.on('data', collect('stdout')); child.stderr.on('data', collect('stderr'));
|
|
child.once('error', error => {
|
|
if (settled) return;
|
|
settled = true; clearTimeout(timer); reject(error);
|
|
});
|
|
child.once('close', (code, signal) => {
|
|
if (settled) return;
|
|
settled = true; clearTimeout(timer); resolve({ code, signal, stdout, stderr, termination });
|
|
});
|
|
});
|
|
}
|
|
|
|
function fingerprintSandboxCli(executable) {
|
|
const resolved = fs.realpathSync(executable);
|
|
fs.accessSync(resolved, fs.constants.X_OK);
|
|
const before = fs.statSync(resolved);
|
|
assert.ok(before.isFile() && before.size > 0 && before.size <= 64 * 1024 * 1024,
|
|
'Sandbox CLI must be a bounded executable file');
|
|
const bytes = fs.readFileSync(resolved);
|
|
const after = fs.statSync(resolved);
|
|
assert.equal(after.dev, before.dev, 'Sandbox CLI changed during fingerprinting');
|
|
assert.equal(after.ino, before.ino, 'Sandbox CLI changed during fingerprinting');
|
|
assert.equal(after.size, before.size, 'Sandbox CLI changed during fingerprinting');
|
|
assert.equal(after.mtimeMs, before.mtimeMs, 'Sandbox CLI changed during fingerprinting');
|
|
const executableDigest = digest(bytes);
|
|
const sourceRoot = path.basename(path.dirname(resolved)) === 'sandbox' ? path.dirname(resolved) : null;
|
|
if (!sourceRoot) return { path: resolved, bytes: bytes.length, digest: executableDigest,
|
|
implementation: { root: null, files: 1, bytes: bytes.length, digest: executableDigest } };
|
|
const files = [];
|
|
function visit(directory) {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
const file = path.join(directory, entry.name);
|
|
assert.equal(entry.isSymbolicLink(), false, 'Sandbox CLI implementation must not contain symbolic links');
|
|
if (entry.isDirectory()) visit(file);
|
|
else {
|
|
assert.equal(entry.isFile(), true, 'Sandbox CLI implementation must contain regular files only');
|
|
files.push(file);
|
|
assert.ok(files.length <= 512, 'Sandbox CLI implementation exceeds the file bound');
|
|
}
|
|
}
|
|
}
|
|
visit(sourceRoot);
|
|
const hash = crypto.createHash('sha256'); let total = 0;
|
|
for (const file of files) {
|
|
const content = fs.readFileSync(file);
|
|
total += content.length;
|
|
assert.ok(total <= 32 * 1024 * 1024, 'Sandbox CLI implementation exceeds the byte bound');
|
|
hash.update(path.relative(sourceRoot, file).split(path.sep).join('/')).update('\0').update(content);
|
|
}
|
|
return { path: resolved, bytes: bytes.length, digest: executableDigest,
|
|
implementation: { root: sourceRoot, files: files.length, bytes: total, digest: hash.digest('hex') } };
|
|
}
|
|
|
|
function resolveSandboxCli(commandName = 'ecc-sandbox') {
|
|
const candidates = path.isAbsolute(commandName) ? [commandName]
|
|
: (process.env.PATH || '').split(path.delimiter).filter(directory => path.isAbsolute(directory))
|
|
.map(directory => path.join(directory, commandName));
|
|
const executable = candidates.find(candidate => {
|
|
try { fs.accessSync(candidate, fs.constants.X_OK); return true; } catch { return false; }
|
|
});
|
|
assert.ok(executable, 'Sandbox CLI executable was not found');
|
|
return fingerprintSandboxCli(executable);
|
|
}
|
|
|
|
function verifySandboxCli(binding) {
|
|
const current = fingerprintSandboxCli(binding.path);
|
|
assert.deepEqual(current, binding, 'Sandbox CLI changed after acceptance was staged');
|
|
return current;
|
|
}
|
|
|
|
function validateReport(stdout, { tier, manifest }) {
|
|
try {
|
|
const report = JSON.parse(stdout);
|
|
assert.ok(report && typeof report === 'object' && !Array.isArray(report));
|
|
assert.equal(report.result, 'pass');
|
|
assert.equal(report.backend, tier === 1 ? 'podman' : 'lume');
|
|
assert.equal(report.tier, tier);
|
|
assert.equal(report.execution_mode, 'real');
|
|
const installDiff = report.install_diff;
|
|
assert.ok(installDiff && typeof installDiff === 'object' && !Array.isArray(installDiff));
|
|
for (const key of ['files_added', 'files_changed', 'files_deleted', 'path_changes',
|
|
'services_registered', 'dotfiles_touched']) assert.ok(Array.isArray(installDiff[key]));
|
|
if (tier === 1) assert.equal(installDiff.complete, true);
|
|
else {
|
|
assert.equal(installDiff.method, 'scan');
|
|
assert.equal(installDiff.complete, false);
|
|
assert.ok(report.notes?.includes('VM install diff is a bounded best-effort path scan, not a complete disk diff'));
|
|
}
|
|
assert.equal(report.assertions?.length, manifest.steps.assert.length);
|
|
for (let index = 0; index < manifest.steps.assert.length; index++) {
|
|
assert.deepEqual(report.assertions[index], { cmd: manifest.steps.assert[index], pass: true });
|
|
}
|
|
const assertion = manifest.steps.assert.at(-1);
|
|
const step = report.steps?.findLast(item => item?.cmd === assertion);
|
|
assert.equal(step?.exit, 0);
|
|
assert.equal(typeof step.stdout_tail, 'string');
|
|
const smoke = JSON.parse(step.stdout_tail.trim());
|
|
assert.equal(smoke?.schemaVersion, 'ecc.context-sandbox-smoke.v1');
|
|
assert.equal(smoke.passed, true);
|
|
assert.equal(smoke.os, tier === 1 ? 'linux' : 'darwin');
|
|
assert.equal(smoke.arch, 'arm64');
|
|
assert.equal(smoke.authenticated, false);
|
|
assert.equal(smoke.taskOutcomes, 'unobserved');
|
|
assert.equal(smoke.matrix?.length, 10);
|
|
const layouts = smoke.matrix.map(item => `${item.target}/${item.profile}`).sort();
|
|
assert.deepEqual(layouts, ['claude/full', 'claude/lean', 'codex/full', 'codex/lean',
|
|
'cursor/full', 'cursor/lean', 'opencode/full', 'opencode/lean', 'pi/full', 'pi/lean']);
|
|
return { report, smoke };
|
|
} catch {
|
|
throw new Error('Sandbox acceptance report or final smoke payload is invalid');
|
|
}
|
|
}
|
|
|
|
function manifestFor({ tier, archiveDigest, verifierDigest, url, runName }) {
|
|
assert.ok([1, 2].includes(tier));
|
|
for (const value of [archiveDigest, verifierDigest]) assert.match(value, /^[a-f0-9]{64}$/);
|
|
assert.match(runName, /^[a-z0-9-]+$/);
|
|
const guestRoot = tier === 1 ? `/home/ecc/${runName}` : `/tmp/${runName}`;
|
|
const setup = [`mkdir -m 700 ${quote(guestRoot)}`];
|
|
let runtime = '';
|
|
if (tier === 2) {
|
|
const parsed = new URL(url);
|
|
assert.equal(parsed.protocol, 'http:');
|
|
assert.equal(parsed.username, ''); assert.equal(parsed.password, '');
|
|
assert.equal(net.isIP(parsed.hostname), 4, 'Artifact URL requires an IPv4 address');
|
|
setup.push(`curl -fsS --max-time 120 https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-arm64.tar.gz -o ${quote(`${guestRoot}/node.tgz`)} && test "$(shasum -a 256 ${quote(`${guestRoot}/node.tgz`)} | cut -d ' ' -f 1)" = ${NODE_SHA} && tar -xzf ${quote(`${guestRoot}/node.tgz`)} -C ${quote(guestRoot)}`);
|
|
runtime = `export PATH=${quote(`${guestRoot}/node-v${NODE_VERSION}-darwin-arm64/bin`)}:$PATH; `;
|
|
for (const file of ['package.tgz', 'sandbox-smoke.js']) {
|
|
setup.push(`curl -fsS --max-time 120 ${quote(`${url}/${file}`)} -o ${quote(`${guestRoot}/${file}`)}`);
|
|
}
|
|
} else {
|
|
setup.push(`cp /workspace/source/package.tgz /workspace/source/sandbox-smoke.js ${quote(guestRoot)}/`);
|
|
}
|
|
const check = `const fs=require('fs'),c=require('crypto'); for(const [f,h] of ${JSON.stringify([['package.tgz', archiveDigest], ['sandbox-smoke.js', verifierDigest]])}) {if(c.createHash('sha256').update(fs.readFileSync(f)).digest('hex')!==h)throw Error('Input digest mismatch')}`;
|
|
setup.push(`${runtime}cd ${quote(guestRoot)} && node -e ${quote(check)} && npm install --ignore-scripts --omit=dev --no-audit --no-fund --fetch-timeout=30000 --fetch-retries=1 --prefix consumer ./package.tgz && npm install --ignore-scripts --no-audit --no-fund --fetch-timeout=30000 --fetch-retries=1 --prefix tools @openai/codex@0.154.0 ${quote(`@openai/codex-${tier === 2 ? 'darwin' : 'linux'}-arm64@npm:@openai/codex@0.154.0-${tier === 2 ? 'darwin' : 'linux'}-arm64`)}`);
|
|
const assertion = `${runtime}export PATH=${quote(`${guestRoot}/tools/node_modules/.bin`)}:$PATH; node ${quote(`${guestRoot}/sandbox-smoke.js`)} ${quote(`${guestRoot}/consumer/node_modules/ecc-universal`)} ${quote(guestRoot)}`;
|
|
const manifest = { name: runName, needs: { os: [tier === 1 ? 'linux' : 'macos'], arch: ['arm64'],
|
|
capabilities: ['clean-home', 'pkg-install', 'network:*'], trust: 'first-party', native: tier === 2 },
|
|
resources: { cpu: 2, memory: tier === 1 ? '1GB' : '2GB', timeout: 900 },
|
|
steps: { setup, assert: [assertion] }, report: 'install-diff' };
|
|
for (const step of [...setup, assertion]) assert.ok(step.length <= 8192);
|
|
return manifest;
|
|
}
|
|
|
|
async function serveInputs(files, host) {
|
|
assert.equal(net.isIP(host), 4, 'Artifact host must be an explicit IPv4 address');
|
|
const token = crypto.randomBytes(24).toString('hex');
|
|
const requests = [];
|
|
const server = http.createServer((request, response) => {
|
|
const file = request.url?.startsWith(`/${token}/`) ? request.url.slice(token.length + 2) : '';
|
|
if (request.method !== 'GET' || !Object.hasOwn(files, file) || requests.length >= 12) {
|
|
response.writeHead(404).end(); return;
|
|
}
|
|
const bytes = files[file]; requests.push({ file, bytes: bytes.length, digest: digest(bytes) });
|
|
response.writeHead(200, { 'Content-Length': bytes.length, 'Content-Type': 'application/octet-stream', 'Cache-Control': 'no-store' });
|
|
response.end(bytes);
|
|
});
|
|
server.requestTimeout = 150000; server.headersTimeout = 10000;
|
|
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, host, resolve); });
|
|
return { url: `http://${host}:${server.address().port}/${token}`, requests,
|
|
close: () => new Promise(resolve => { server.close(resolve); server.closeAllConnections(); }) };
|
|
}
|
|
|
|
async function run(options) {
|
|
assert.ok([1, 2].includes(options.tier), 'Choose --tier 1 or --tier 2');
|
|
assert.equal(process.arch, 'arm64', 'This acceptance currently certifies arm64 only');
|
|
const repoRoot = path.resolve(__dirname, '../..');
|
|
if (options.sandboxCli) assert.ok(path.isAbsolute(options.sandboxCli), '--sandbox-cli must be an absolute trusted executable');
|
|
const sandboxBinding = resolveSandboxCli(options.sandboxCli || 'ecc-sandbox');
|
|
const sandboxCli = sandboxBinding.path;
|
|
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-profile-sandbox-'));
|
|
const resultRoot = path.resolve(options.output);
|
|
fs.mkdirSync(resultRoot, { recursive: true, mode: 0o700 });
|
|
const runName = `ecc-profile-tier${options.tier}-${crypto.randomUUID()}`;
|
|
let server;
|
|
const receipt = { schemaVersion: 'ecc.context-sandbox-acceptance.v1', runName, tier: options.tier,
|
|
sourceRevision: (await command('git', ['rev-parse', 'HEAD'], repoRoot, 10000)).stdout.trim(),
|
|
sourceDirty: (await command('git', ['status', '--porcelain'], repoRoot, 10000)).stdout.length > 0,
|
|
sandboxCli, sandboxCliDigest: sandboxBinding.digest,
|
|
sandboxImplementationDigest: sandboxBinding.implementation.digest, reportValidated: false,
|
|
credentialsTransferred: false, artifactServerClosed: false, stageRemoved: false };
|
|
try {
|
|
const packed = await command('npm', ['pack', '--json', '--pack-destination', stage], repoRoot);
|
|
assert.equal(packed.code, 0, packed.stderr);
|
|
const pack = JSON.parse(packed.stdout)[0];
|
|
const archive = fs.readFileSync(path.join(stage, pack.filename));
|
|
assert.ok(archive.length < 64 * 1024 * 1024, 'Package exceeds transfer bound');
|
|
const verifier = fs.readFileSync(path.join(__dirname, 'sandbox-smoke.js'));
|
|
assert.ok(verifier.length < 65536);
|
|
const files = { 'package.tgz': archive, 'sandbox-smoke.js': verifier };
|
|
fs.writeFileSync(path.join(stage, 'package.tgz'), archive, { mode: 0o600 });
|
|
fs.writeFileSync(path.join(stage, 'sandbox-smoke.js'), verifier, { mode: 0o600 });
|
|
receipt.packageDigest = digest(archive); receipt.verifierDigest = digest(verifier);
|
|
if (options.tier === 2) {
|
|
const host = options.artifactHost || Object.values(os.networkInterfaces()).flat()
|
|
.find(address => address.address === '192.168.64.1')?.address;
|
|
assert.ok(host, 'Specify --artifact-host with a host IP reachable from the guest');
|
|
server = await serveInputs(files, host);
|
|
}
|
|
const manifest = manifestFor({ tier: options.tier, archiveDigest: receipt.packageDigest,
|
|
verifierDigest: receipt.verifierDigest, url: server?.url, runName });
|
|
receipt.manifestDigest = digest(Buffer.from(JSON.stringify(manifest)));
|
|
const manifestPath = path.join(stage, 'sandbox.json');
|
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest), { mode: 0o600 });
|
|
fs.copyFileSync(manifestPath, path.join(resultRoot, `${runName}.manifest.json`));
|
|
verifySandboxCli(sandboxBinding);
|
|
const preview = await command(sandboxCli, ['run', manifestPath, '--local-only', '--dry-run'], stage, 30000);
|
|
fs.writeFileSync(path.join(resultRoot, `${runName}.preview.json`), preview.stdout, { mode: 0o600 });
|
|
assert.equal(preview.code, 0, preview.stdout || preview.stderr);
|
|
const routes = JSON.parse(preview.stdout).routes;
|
|
assert.equal(routes?.length, 1, 'Expected exactly one admitted sandbox route');
|
|
assert.equal(routes[0].result, 'routable');
|
|
assert.equal(routes[0].tier, options.tier, 'Router chose a different tier');
|
|
assert.equal(routes[0].backend, options.tier === 1 ? 'podman' : 'lume', 'Router chose a different backend');
|
|
process.stderr.write(`Starting ${runName}; package ${receipt.packageDigest}\n`);
|
|
verifySandboxCli(sandboxBinding);
|
|
const result = await command(sandboxCli, ['run', manifestPath, '--local-only'], stage, 960000);
|
|
receipt.exitCode = result.code; receipt.signal = result.signal;
|
|
fs.writeFileSync(path.join(resultRoot, `${runName}.report.json`), result.stdout, { mode: 0o600 });
|
|
fs.writeFileSync(path.join(resultRoot, `${runName}.stderr.log`), result.stderr, { mode: 0o600 });
|
|
receipt.reportPath = path.join(resultRoot, `${runName}.report.json`);
|
|
assert.equal(result.code, 0, result.stdout || result.stderr);
|
|
verifySandboxCli(sandboxBinding);
|
|
const validated = validateReport(result.stdout, { tier: options.tier, manifest });
|
|
receipt.reportValidated = true;
|
|
receipt.smokeDigest = digest(Buffer.from(JSON.stringify(validated.smoke)));
|
|
if (server) receipt.transfers = server.requests;
|
|
return receipt;
|
|
} finally {
|
|
if (server) { await server.close(); receipt.artifactServerClosed = true; }
|
|
else receipt.artifactServerClosed = true;
|
|
fs.rmSync(stage, { recursive: true, force: true }); receipt.stageRemoved = !fs.existsSync(stage);
|
|
fs.writeFileSync(path.join(resultRoot, `${runName}.driver.json`), JSON.stringify(receipt, null, 2), { mode: 0o600 });
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const args = process.argv.slice(2); const options = {};
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--tier') options.tier = Number(args[++i]);
|
|
else if (args[i] === '--output') options.output = args[++i];
|
|
else if (args[i] === '--artifact-host') options.artifactHost = args[++i];
|
|
else if (args[i] === '--sandbox-cli') options.sandboxCli = args[++i];
|
|
else throw new Error(`Unknown option: ${args[i]}`);
|
|
}
|
|
if (!options.output) throw new Error('--output is required');
|
|
run(options).then(receipt => { process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`); process.exitCode = receipt.exitCode === 0 ? 0 : 1; })
|
|
.catch(error => { process.stderr.write(`${error.stack}\n`); process.exitCode = 1; });
|
|
}
|
|
module.exports = { command, manifestFor, resolveSandboxCli, serveInputs, validateReport,
|
|
verifySandboxCli, run };
|