Files
stack/scripts/gate-history.mjs
T
2026-08-01 00:17:50 -05:00

360 lines
13 KiB
JavaScript

import { existsSync } from 'node:fs';
import { createHash, randomUUID } from 'node:crypto';
import { access, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
function git(root, args, { allowFailure = false } = {}) {
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
if (result.status !== 0 && !allowFailure) {
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout).trim()}`);
}
return result;
}
export async function listProspectiveCommits(root, activationCommit, head = 'HEAD') {
const result = git(root, [
'rev-list',
'--first-parent',
'--reverse',
`${activationCommit}..${head}`,
]);
return result.stdout.trim() ? result.stdout.trim().split('\n') : [];
}
export async function readManifestAtCommit(root, commit) {
const result = git(root, ['show', `${commit}:gates/gates.manifest.json`]);
return JSON.parse(result.stdout);
}
async function snapshotAuthoritativeTree(root) {
const snapshot = new Map();
async function walk(current) {
for (const child of await readdir(current, { withFileTypes: true })) {
if (['.git', '.home', 'node_modules'].includes(child.name)) continue;
const absolute = path.join(current, child.name);
const relative = path.relative(root, absolute).split(path.sep).join('/');
const stats = await lstat(absolute);
if (stats.isDirectory()) {
await walk(absolute);
} else if (stats.isSymbolicLink()) {
snapshot.set(relative, `symlink:${stats.mode}:${await readlink(absolute)}`);
} else if (stats.isFile()) {
const digest = createHash('sha256')
.update(await readFile(absolute))
.digest('hex');
snapshot.set(relative, `file:${stats.mode}:${digest}`);
}
}
}
await walk(root);
return snapshot;
}
async function authoritativeTreeChanges(root, snapshot) {
const changes = [];
for (const [relative, expected] of snapshot) {
const absolute = path.join(root, relative);
let actual;
try {
const stats = await lstat(absolute);
if (stats.isSymbolicLink()) {
actual = `symlink:${stats.mode}:${await readlink(absolute)}`;
} else if (stats.isFile()) {
const digest = createHash('sha256')
.update(await readFile(absolute))
.digest('hex');
actual = `file:${stats.mode}:${digest}`;
} else {
actual = `other:${stats.mode}`;
}
} catch (error) {
if (error.code !== 'ENOENT') throw error;
actual = 'missing';
}
if (actual !== expected) changes.push(relative);
}
return changes;
}
function bubblewrap(root, command, args, { storePath, timeout = 300_000 } = {}) {
const sandboxArgs = [
'--unshare-net',
'--unshare-pid',
'--unshare-ipc',
'--unshare-uts',
'--die-with-parent',
'--new-session',
'--clearenv',
];
for (const systemPath of ['/usr', '/bin', '/lib', '/lib64', '/etc']) {
if (existsSync(systemPath)) sandboxArgs.push('--ro-bind', systemPath, systemPath);
}
sandboxArgs.push('--dev', '/dev', '--proc', '/proc', '--tmpfs', '/tmp', '--bind', root, '/work');
if (storePath) sandboxArgs.push('--ro-bind', storePath, '/pnpm-store');
const corepackHome = path.join(process.env.HOME ?? '', '.cache', 'node', 'corepack');
if (existsSync(corepackHome)) sandboxArgs.push('--ro-bind', corepackHome, '/corepack');
sandboxArgs.push(
'--chdir',
'/work',
'--setenv',
'HOME',
'/work/.home',
'--setenv',
'PATH',
'/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
'--setenv',
'LANG',
'C.UTF-8',
'--setenv',
'CI',
'true',
);
if (storePath) sandboxArgs.push('--setenv', 'NPM_CONFIG_STORE_DIR', '/pnpm-store');
if (existsSync(corepackHome)) sandboxArgs.push('--setenv', 'COREPACK_HOME', '/corepack');
const enteredMarker = `__MOSAIC_BWRAP_ENTERED_${randomUUID()}__`;
sandboxArgs.push(
'/bin/sh',
'-c',
'printf "%s\\n" "$1"; shift; exec "$@"',
'mosaic-bwrap-entry',
enteredMarker,
command,
...args,
);
const result = spawnSync('bwrap', sandboxArgs, { encoding: 'utf8', timeout });
const sandboxEntered = result.stdout?.includes(enteredMarker) === true;
return {
...result,
stdout: (result.stdout ?? '').replace(`${enteredMarker}\n`, ''),
sandboxLauncher: 'bwrap',
sandboxEntered,
};
}
export async function replayCommit(root, commit) {
const replayRoot = await mkdtemp(
path.join(path.dirname(root), `.gate-history-${commit.slice(0, 12)}-`),
);
const archive = `${replayRoot}.tar`;
try {
git(root, ['archive', '--format=tar', `--output=${archive}`, commit]);
const extract = spawnSync('tar', ['-xf', archive, '-C', replayRoot], { encoding: 'utf8' });
if (extract.status !== 0) {
return { status: extract.status, stdout: extract.stdout, stderr: extract.stderr };
}
const authoritativeSnapshot = await snapshotAuthoritativeTree(replayRoot);
await mkdir(path.join(replayRoot, '.home'), { recursive: true });
let storePath;
try {
await access(path.join(replayRoot, 'package.json'));
const init = spawnSync('git', ['init', '--quiet', replayRoot], { encoding: 'utf8' });
if (init.status !== 0) return init;
const store = spawnSync('pnpm', ['store', 'path'], { encoding: 'utf8' });
if (store.status !== 0) return store;
storePath = store.stdout.trim();
const install = bubblewrap(
replayRoot,
'pnpm',
['install', '--frozen-lockfile', '--offline'],
{ storePath, timeout: 600_000 },
);
if (install.status !== 0 || install.error || install.signal) {
return {
...install,
stderr: `historical frozen dependency install failed: ${install.error?.message || install.stderr || install.stdout || ''}`,
};
}
const authoritativeChanges = await authoritativeTreeChanges(
replayRoot,
authoritativeSnapshot,
);
if (authoritativeChanges.length > 0) {
return {
status: 1,
stdout: '',
stderr: `authoritative archived file changed during historical install: ${authoritativeChanges.join(', ')}`,
};
}
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
return bubblewrap(
replayRoot,
process.execPath,
[
'/work/scripts/gate-verify.mjs',
'--root',
'/work',
'--manifest',
'gates/gates.manifest.json',
'--skip-history',
],
{ storePath },
);
} finally {
await rm(archive, { force: true });
await rm(replayRoot, { recursive: true, force: true });
}
}
export function assessProviderEvidence(commit, pipelines) {
const matches = pipelines.filter((candidate) => candidate.commit === commit);
if (matches.length === 0) {
return {
state: 'absent',
detail:
'no retained provider record was supplied; retention expiry and never-ran are not inferred',
};
}
const pipelineStates = new Set(['success', 'failure', 'error', 'pending', 'running', 'queued']);
const stepStates = new Set([
'success',
'failure',
'error',
'pending',
'running',
'queued',
'skipped',
]);
const numbers = matches.map((candidate) => candidate.number);
const malformed = matches.some(
(candidate) =>
typeof candidate.commit !== 'string' ||
candidate.commit.length === 0 ||
!Number.isInteger(candidate.number) ||
!pipelineStates.has(candidate.status) ||
!Array.isArray(candidate.steps) ||
candidate.steps.some(
(step) =>
typeof step?.name !== 'string' ||
typeof step?.status !== 'string' ||
!stepStates.has(step.status),
),
);
if (malformed || new Set(numbers).size !== numbers.length) {
return {
state: 'terminal-failure',
detail: 'provider records are malformed or have ambiguous pipeline numbers',
};
}
const pipeline = [...matches].sort((left, right) => right.number - left.number)[0];
const gateSteps = (pipeline.steps ?? []).filter((step) => step.name === 'gate-verify');
if (gateSteps.length !== 1) {
return {
state: 'terminal-failure',
detail: `provider record has ambiguous gate-verify step count ${gateSteps.length}`,
};
}
const [gateStep] = gateSteps;
if (pipeline.status === 'success' && gateStep.status === 'success') {
return { state: 'terminal-success', detail: 'pipeline and gate-verify step succeeded' };
}
if (['pending', 'running', 'queued'].includes(pipeline.status)) {
return { state: 'current-running', detail: `pipeline is ${pipeline.status}` };
}
return {
state: 'terminal-failure',
detail: `pipeline=${pipeline.status ?? 'unknown'}, gate-verify=${gateStep?.status ?? 'absent'}`,
};
}
async function loadProviderEvidence() {
const evidenceFile = process.env.GATE_PROVIDER_EVIDENCE_FILE;
if (!evidenceFile) return [];
const parsed = JSON.parse(await readFile(evidenceFile, 'utf8'));
if (!Array.isArray(parsed)) throw new Error('provider evidence file must contain a JSON array');
return parsed;
}
function isMainCommit(root, head) {
if (process.env.CI_COMMIT_BRANCH === 'main') return true;
const result = git(root, ['merge-base', '--is-ancestor', head, 'refs/remotes/origin/main'], {
allowFailure: true,
});
return result.status === 0;
}
export async function verifyHistory({ root, manifest }) {
const failures = [];
const observations = [];
const head = git(root, ['rev-parse', 'HEAD']).stdout.trim();
if (!manifest.activationCommit) {
failures.push('history activationCommit is missing');
return { failures, observations };
}
const activationCheck = git(
root,
['merge-base', '--is-ancestor', manifest.activationCommit, head],
{ allowFailure: true },
);
if (activationCheck.status !== 0) {
failures.push(
`history activation commit ${manifest.activationCommit} is not an ancestor of ${head}`,
);
return { failures, observations };
}
const onMain = isMainCommit(root, head);
// RM-02 execution boundary (RM-60, cross-reference RM-59), kept adjacent in both directions:
// DOES: run every registered current-tree gate and declared inerting mutation on PR CI,
// unprivileged and fail-closed.
// DOES NOT: execute a commit's own verifier in an isolated PR replay. PR-controlled code would
// otherwise need the namespace capability intended to contain that same code. That external
// trust boundary must be runner/provider-owned before any PR executable or config is evaluated.
observations.push(
`RM-02 EXECUTION BOUNDARY ${head}: DOES: verify the current tree and declared inerting mutations on every PR, unprivileged and fail-closed; DOES NOT: execute isolated per-commit verifier replay in repository-controlled CI; owner RM-60, cross-reference RM-59`,
);
if (!onMain) {
observations.push(
`PROVIDER ASSERTION DEFERRED ${head}: commit is not yet on main; retained provider evidence starts after merge and no replay success is inferred`,
);
}
const pipelines = onMain ? await loadProviderEvidence() : [];
const commits = await listProspectiveCommits(root, manifest.activationCommit, head);
for (const commit of commits) {
let commitManifest;
try {
commitManifest = await readManifestAtCommit(root, commit);
} catch (error) {
failures.push(`${commit}: own-tree registry cannot be read: ${error.message}`);
continue;
}
if (commitManifest.schemaVersion !== manifest.schemaVersion) {
failures.push(`${commit}: own-tree registry schema is not supported`);
continue;
}
const evidence = assessProviderEvidence(commit, pipelines);
if (commit === head) {
observations.push(
`CURRENT TREE EVALUATED ${commit}: all registered cases ran from this checkout; provider evidence=${evidence.state} (${evidence.detail})`,
);
continue;
}
observations.push(
`INTERMEDIATE REPLAY DEFERRED ${commit}: isolated own-tree execution is not performed by repository-controlled CI; owner RM-60, cross-reference RM-59; no success is inferred`,
);
if (!onMain) {
observations.push(
`PROVIDER EVIDENCE ${commit}: DEFERRED until the commit is on main; no success is inferred`,
);
continue;
}
observations.push(
`POST-MERGE DETECTION BOUNDARY ${commit}: protected isolated replay awaits RM-60; when available, a failure requires quarantine/revert and is detection, not pre-merge prevention`,
);
if (evidence.state === 'terminal-failure') {
failures.push(
`${commit}: retained provider evidence is not terminal-success (${evidence.detail})`,
);
} else if (evidence.state === 'terminal-success') {
observations.push(`PROVIDER EVIDENCE ${commit}: terminal-success (${evidence.detail})`);
} else {
observations.push(
`PROVIDER EVIDENCE ${commit}: ${evidence.state.toUpperCase()} (${evidence.detail}); no merge-time success is inferred`,
);
}
}
return { failures, observations };
}