wip(rm-02): round-4 remediation held at RM-60 boundary
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
import { verifyHistory } from './gate-history.mjs';
|
||||
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'gate-delayed-introduction-'));
|
||||
function git(...args) {
|
||||
const result = spawnSync(
|
||||
'git',
|
||||
['-c', 'user.name=gate-control', '-c', '[email protected]', ...args],
|
||||
{ cwd: root, encoding: 'utf8' },
|
||||
);
|
||||
if (result.status !== 0) throw new Error(result.stderr || result.stdout);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
try {
|
||||
git('init', '-q');
|
||||
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
|
||||
git('add', '.');
|
||||
git('commit', '-m', 'provider target baseline');
|
||||
const baseline = git('rev-parse', 'HEAD');
|
||||
git('update-ref', 'refs/remotes/origin/main', baseline);
|
||||
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
||||
await writeFile(path.join(root, 'scripts', 'preflight.mjs'), 'process.exit(0);\n');
|
||||
git('add', '.');
|
||||
git('commit', '-m', 'gate change before registry');
|
||||
const unregisteredCommit = git('rev-parse', 'HEAD');
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
git('add', '.');
|
||||
git('commit', '-m', 'delayed registry introduction');
|
||||
|
||||
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
||||
process.env.CI_COMMIT_BRANCH = 'feature/delayed-introduction-control';
|
||||
let result;
|
||||
try {
|
||||
result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
|
||||
} finally {
|
||||
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
||||
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
||||
}
|
||||
const detail = result.failures.join('\n');
|
||||
if (detail.includes(unregisteredCommit) && /own-tree registry cannot be read/i.test(detail)) {
|
||||
process.stderr.write(`delayed registry introduction rejected: ${detail}\n`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write('delayed registry introduction was not rejected\n');
|
||||
}
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -20,7 +20,6 @@ try {
|
||||
const result = await verifyRegistry({
|
||||
root,
|
||||
manifest: manifestPath,
|
||||
skipHistory: true,
|
||||
structureOnly: true,
|
||||
fixtureProfile: false,
|
||||
});
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
import { deriveHistoryBoundary } from './gate-history.mjs';
|
||||
|
||||
const candidateKind = process.argv[2];
|
||||
const root = process.cwd();
|
||||
const boundary = deriveHistoryBoundary(root);
|
||||
|
||||
function revParse(revision) {
|
||||
const result = spawnSync('git', ['rev-parse', revision], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.stderr.write(`history boundary control could not resolve ${revision}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
const candidates = {
|
||||
head: revParse('HEAD'),
|
||||
parent: revParse('HEAD^'),
|
||||
introduction: boundary.introductionCommit,
|
||||
};
|
||||
if (!Object.hasOwn(candidates, candidateKind)) {
|
||||
process.stderr.write(`unknown history boundary candidate ${String(candidateKind)}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
const candidate = candidates[candidateKind];
|
||||
if (candidate === boundary.activationCommit) {
|
||||
process.stdout.write(`history boundary candidate ${candidateKind} matched derived activation\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
process.stderr.write(
|
||||
`history boundary candidate ${candidateKind} rejected: derived activation is provider target merge-base\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { assertCurrentTreeObservation } from './gate-verify.mjs';
|
||||
|
||||
const prohibitedReports = [
|
||||
'HISTORY PROVENANCE VERIFIED local-ref',
|
||||
'COMMIT ANCESTRY VERIFIED origin/main',
|
||||
'PROVIDER LINEAGE SUCCESS pipeline-7',
|
||||
];
|
||||
const accepted = prohibitedReports.filter((report) => {
|
||||
try {
|
||||
assertCurrentTreeObservation(report);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const verifier = await readFile(path.join(process.cwd(), 'scripts', 'gate-verify.mjs'), 'utf8');
|
||||
const outputWrites = [...verifier.matchAll(/process\.stdout\.write\s*\(/g)].length;
|
||||
const productionRendererWired =
|
||||
outputWrites === 2 &&
|
||||
/for \(const observation of observations\) \{\s*assertCurrentTreeObservation\(observation\);\s*process\.stdout\.write/s.test(
|
||||
verifier,
|
||||
) &&
|
||||
!/\bconsole\.(?:log|info|debug)\s*\(/.test(verifier);
|
||||
|
||||
if (accepted.length > 0 || !productionRendererWired) {
|
||||
process.stderr.write(
|
||||
`HISTORY_PROVENANCE_FORBIDDEN: closed current-tree observation renderer rejected=${prohibitedReports.length - accepted.length}/${prohibitedReports.length} production-wired=${productionRendererWired}\n`,
|
||||
);
|
||||
process.exit(79);
|
||||
}
|
||||
process.stdout.write('history provenance reporting capability is absent; owner RM-60\n');
|
||||
@@ -1,432 +0,0 @@
|
||||
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 function deriveHistoryBoundary(root, head = 'HEAD') {
|
||||
const targetRef = 'refs/remotes/origin/main';
|
||||
const target = git(root, ['rev-parse', '--verify', targetRef], { allowFailure: true });
|
||||
if (target.status !== 0 || !target.stdout.trim()) {
|
||||
throw new Error(`history boundary cannot be derived: provider target ${targetRef} is absent`);
|
||||
}
|
||||
const introductions = git(root, [
|
||||
'log',
|
||||
'--first-parent',
|
||||
'--diff-filter=A',
|
||||
'--format=%H',
|
||||
'--reverse',
|
||||
head,
|
||||
'--',
|
||||
'gates/gates.manifest.json',
|
||||
])
|
||||
.stdout.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
if (introductions.length === 0) {
|
||||
throw new Error('history boundary cannot be derived: registry introduction is absent');
|
||||
}
|
||||
const introductionCommit = introductions[0];
|
||||
const headOnTarget = git(root, ['merge-base', '--is-ancestor', head, targetRef], {
|
||||
allowFailure: true,
|
||||
});
|
||||
const activation =
|
||||
headOnTarget.status === 0
|
||||
? git(root, ['rev-parse', `${head}^`], { allowFailure: true })
|
||||
: git(root, ['merge-base', head, targetRef], { allowFailure: true });
|
||||
if (activation.status !== 0 || !activation.stdout.trim()) {
|
||||
throw new Error(
|
||||
`history boundary cannot be derived: provider target merge-base for ${head} is unavailable`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
activationCommit: activation.stdout.trim(),
|
||||
introductionCommit,
|
||||
targetRef,
|
||||
};
|
||||
}
|
||||
|
||||
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 validateProviderEvidenceCollection(pipelines) {
|
||||
if (!Array.isArray(pipelines)) return 'provider evidence collection is malformed';
|
||||
const pipelineStates = new Set(['success', 'failure', 'error', 'pending', 'running', 'queued']);
|
||||
const stepStates = new Set([
|
||||
'success',
|
||||
'failure',
|
||||
'error',
|
||||
'pending',
|
||||
'running',
|
||||
'queued',
|
||||
'skipped',
|
||||
]);
|
||||
const malformed = pipelines.some(
|
||||
(candidate) =>
|
||||
!candidate ||
|
||||
typeof candidate !== 'object' ||
|
||||
Array.isArray(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) =>
|
||||
!step ||
|
||||
typeof step !== 'object' ||
|
||||
Array.isArray(step) ||
|
||||
typeof step.name !== 'string' ||
|
||||
typeof step.status !== 'string' ||
|
||||
!stepStates.has(step.status),
|
||||
),
|
||||
);
|
||||
if (malformed) return 'provider records are malformed';
|
||||
const ambiguousGateRecord = pipelines.find(
|
||||
(candidate) => candidate.steps.filter((step) => step.name === 'gate-verify').length !== 1,
|
||||
);
|
||||
if (ambiguousGateRecord) {
|
||||
const count = ambiguousGateRecord.steps.filter((step) => step.name === 'gate-verify').length;
|
||||
return `provider record ${ambiguousGateRecord.number} has ambiguous gate-verify step count ${count}`;
|
||||
}
|
||||
const numbers = pipelines.map((candidate) => candidate.number);
|
||||
if (new Set(numbers).size !== numbers.length) {
|
||||
return 'duplicate pipeline identity across commits in provider evidence collection';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function assessProviderEvidence(commit, pipelines) {
|
||||
const collectionFailure = validateProviderEvidenceCollection(pipelines);
|
||||
if (collectionFailure) {
|
||||
return {
|
||||
state: 'terminal-failure',
|
||||
detail: collectionFailure,
|
||||
};
|
||||
}
|
||||
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 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 (Object.hasOwn(manifest, 'activationCommit')) {
|
||||
failures.push(
|
||||
'author-controlled activationCommit is forbidden; history boundary is derived from the registry introduction',
|
||||
);
|
||||
}
|
||||
let boundary;
|
||||
try {
|
||||
boundary = deriveHistoryBoundary(root, head);
|
||||
} catch (error) {
|
||||
failures.push(error.message);
|
||||
return { failures, observations };
|
||||
}
|
||||
const onMain = isMainCommit(root, head);
|
||||
// RM-02 history bootstrap boundary (Builds 1-2), kept adjacent in both directions:
|
||||
// DOES: anchor feature history to the provider target merge-base, sound against an author who
|
||||
// cannot rewrite main.
|
||||
// DOES NOT: establish integrity when main itself is compromised; Builds 1-2 own that residual.
|
||||
observations.push(
|
||||
`RM-02 HISTORY BOOTSTRAP BOUNDARY ${head}: DOES: anchor the audited range to provider target ${boundary.targetRef} at merge-base ${boundary.activationCommit}, sound against a branch author who cannot rewrite main; DOES NOT: protect against compromise or rewrite of main; residual owner Builds 1-2`,
|
||||
);
|
||||
// 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 collectionFailure = validateProviderEvidenceCollection(pipelines);
|
||||
if (collectionFailure) {
|
||||
failures.push(`provider evidence collection invalid: ${collectionFailure}`);
|
||||
}
|
||||
const commits = await listProspectiveCommits(root, boundary.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 };
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
assessProviderEvidence,
|
||||
deriveHistoryBoundary,
|
||||
listProspectiveCommits,
|
||||
readManifestAtCommit,
|
||||
replayCommit,
|
||||
verifyHistory,
|
||||
} from './gate-history.mjs';
|
||||
|
||||
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `gate-history-${process.pid}`);
|
||||
|
||||
function sandboxUnavailable(result) {
|
||||
if (result.sandboxLauncher !== 'bwrap' || result.sandboxEntered === true) return false;
|
||||
const detail = `${result.stdout ?? ''}${result.stderr ?? ''}${result.error?.message ?? ''}`;
|
||||
const bubblewrapSpawnDenied =
|
||||
['EPERM', 'EACCES', 'ENOENT'].includes(result.error?.code) &&
|
||||
/spawnSync bwrap/i.test(result.error?.message ?? '');
|
||||
if (
|
||||
!bubblewrapSpawnDenied &&
|
||||
!/bwrap:.*(?:Operation not permitted|Creating new namespace failed)/i.test(detail)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
assert.notEqual(result.status, 0, 'sandbox unavailability must remain terminal nonzero');
|
||||
return true;
|
||||
}
|
||||
|
||||
function git(root, ...args) {
|
||||
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
async function commitManifest(root, marker) {
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(root, 'gates', 'gates.manifest.json'),
|
||||
`${JSON.stringify({ schemaVersion: 1, marker })}\n`,
|
||||
);
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', marker);
|
||||
return git(root, 'rev-parse', 'HEAD');
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('sandbox refusal classification requires Bubblewrap provenance', () => {
|
||||
for (const code of ['EPERM', 'EACCES', 'ENOENT']) {
|
||||
assert.equal(
|
||||
sandboxUnavailable({
|
||||
status: null,
|
||||
error: { code, message: `spawnSync bwrap ${code}` },
|
||||
sandboxLauncher: 'bwrap',
|
||||
sandboxEntered: false,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
sandboxUnavailable({
|
||||
status: null,
|
||||
error: { code: 'EPERM', message: 'spawnSync git EPERM' },
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
sandboxUnavailable({ status: 1, stderr: 'historical verifier said bwrap ENOENT' }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
sandboxUnavailable({
|
||||
status: 1,
|
||||
stderr: 'bwrap: Creating new namespace failed: Operation not permitted',
|
||||
sandboxLauncher: 'bwrap',
|
||||
sandboxEntered: false,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
sandboxUnavailable({
|
||||
status: 1,
|
||||
stderr: 'bwrap: Creating new namespace failed: Operation not permitted',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
sandboxUnavailable({
|
||||
status: 1,
|
||||
stderr: 'bwrap: Creating new namespace failed: Operation not permitted',
|
||||
sandboxLauncher: 'bwrap',
|
||||
sandboxEntered: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('prospective history reads each commit own manifest rather than the current tree', async () => {
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
await mkdir(fixtureRoot, { recursive: true });
|
||||
git(fixtureRoot, 'init', '-q');
|
||||
git(fixtureRoot, 'config', 'user.name', 'gate-test');
|
||||
git(fixtureRoot, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(fixtureRoot, 'activation.txt'), 'activation\n');
|
||||
git(fixtureRoot, 'add', '.');
|
||||
git(fixtureRoot, 'commit', '-m', 'activation');
|
||||
const activation = git(fixtureRoot, 'rev-parse', 'HEAD');
|
||||
const first = await commitManifest(fixtureRoot, 'FIRST');
|
||||
const second = await commitManifest(fixtureRoot, 'SECOND');
|
||||
|
||||
assert.deepEqual(await listProspectiveCommits(fixtureRoot, activation, second), [first, second]);
|
||||
assert.equal((await readManifestAtCommit(fixtureRoot, first)).marker, 'FIRST');
|
||||
assert.equal((await readManifestAtCommit(fixtureRoot, second)).marker, 'SECOND');
|
||||
});
|
||||
|
||||
test('historical replay executes each selected commit verifier from that commit tree', async () => {
|
||||
const root = `${fixtureRoot}-replay`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
await writeFile(
|
||||
path.join(root, 'scripts', 'gate-verify.mjs'),
|
||||
"process.stderr.write('OLD TREE INERT\\n'); process.exitCode = 1;\n",
|
||||
);
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'inert historical verifier');
|
||||
const inert = git(root, 'rev-parse', 'HEAD');
|
||||
await writeFile(
|
||||
path.join(root, 'scripts', 'gate-verify.mjs'),
|
||||
"process.stdout.write('NEW TREE VERIFIED\\n');\n",
|
||||
);
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'fixed historical verifier');
|
||||
const fixed = git(root, 'rev-parse', 'HEAD');
|
||||
|
||||
const inertResult = await replayCommit(root, inert);
|
||||
const fixedResult = await replayCommit(root, fixed);
|
||||
if (sandboxUnavailable(inertResult) || sandboxUnavailable(fixedResult)) return;
|
||||
assert.notEqual(inertResult.status, 0);
|
||||
assert.match(inertResult.stderr, /OLD TREE INERT/);
|
||||
assert.equal(fixedResult.status, 0);
|
||||
assert.match(fixedResult.stdout, /NEW TREE VERIFIED/);
|
||||
});
|
||||
|
||||
test('historical install lifecycle cannot replace an authoritative verifier', async () => {
|
||||
const root = `${fixtureRoot}-install-tamper`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
await writeFile(
|
||||
path.join(root, 'scripts', 'gate-verify.mjs'),
|
||||
"process.stderr.write('ORIGINAL VERIFIER RAN\\n'); process.exitCode = 7;\n",
|
||||
);
|
||||
await writeFile(path.join(root, 'forged.mjs'), "process.stdout.write('FORGED SUCCESS\\n');\n");
|
||||
await writeFile(
|
||||
path.join(root, 'package.json'),
|
||||
`${JSON.stringify({
|
||||
name: 'historical-install-tamper',
|
||||
version: '1.0.0',
|
||||
scripts: { postinstall: 'cp forged.mjs scripts/gate-verify.mjs' },
|
||||
})}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, 'pnpm-lock.yaml'),
|
||||
"lockfileVersion: '9.0'\nsettings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\nimporters:\n .: {}\n",
|
||||
);
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'tampering lifecycle fixture');
|
||||
const commit = git(root, 'rev-parse', 'HEAD');
|
||||
|
||||
const result = await replayCommit(root, commit);
|
||||
assert.notEqual(result.status, 0);
|
||||
if (sandboxUnavailable(result)) return;
|
||||
assert.match(result.stderr, /authoritative archived file changed.*scripts\/gate-verify\.mjs/i);
|
||||
assert.doesNotMatch(result.stdout, /FORGED SUCCESS/);
|
||||
});
|
||||
|
||||
test('historical verifier receives no current-process secret environment', async () => {
|
||||
const root = `${fixtureRoot}-secretless`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
await writeFile(
|
||||
path.join(root, 'scripts', 'gate-verify.mjs'),
|
||||
"if (process.env.REPLAY_SENTINEL) { process.stderr.write('SECRET LEAKED\\n'); process.exitCode = 9; } else { process.stdout.write('SECRETLESS\\n'); }\n",
|
||||
);
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'secretless replay fixture');
|
||||
const commit = git(root, 'rev-parse', 'HEAD');
|
||||
|
||||
process.env.REPLAY_SENTINEL = 'must-not-cross-boundary';
|
||||
try {
|
||||
const result = await replayCommit(root, commit);
|
||||
if (sandboxUnavailable(result)) return;
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /SECRETLESS/);
|
||||
assert.doesNotMatch(
|
||||
`${result.stdout}${result.stderr}`,
|
||||
/SECRET LEAKED|must-not-cross-boundary/,
|
||||
);
|
||||
} finally {
|
||||
delete process.env.REPLAY_SENTINEL;
|
||||
}
|
||||
});
|
||||
|
||||
test('historical replay cannot observe a sibling process in the runner PID namespace', async () => {
|
||||
const root = `${fixtureRoot}-pidless`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
|
||||
const sleeper = spawn('sleep', ['30'], {
|
||||
env: { ...process.env, REPLAY_PID_SENTINEL: 'must-not-be-visible' },
|
||||
});
|
||||
try {
|
||||
await writeFile(
|
||||
path.join(root, 'scripts', 'gate-verify.mjs'),
|
||||
`import { existsSync } from 'node:fs';\nif (existsSync('/proc/${sleeper.pid}/environ')) { process.stderr.write('HOST PID VISIBLE\\n'); process.exitCode = 9; } else { process.stdout.write('PIDLESS\\n'); }\n`,
|
||||
);
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'pid-isolated replay fixture');
|
||||
const commit = git(root, 'rev-parse', 'HEAD');
|
||||
const result = await replayCommit(root, commit);
|
||||
if (sandboxUnavailable(result)) return;
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /PIDLESS/);
|
||||
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /HOST PID VISIBLE/);
|
||||
} finally {
|
||||
sleeper.kill('SIGTERM');
|
||||
}
|
||||
});
|
||||
|
||||
test('PR verification states the RM-60 boundary without executing an intermediate verifier', async () => {
|
||||
const root = `${fixtureRoot}-feature`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(root, { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'activation.txt'), 'activation\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'activation');
|
||||
const activation = git(root, 'rev-parse', 'HEAD');
|
||||
git(root, 'update-ref', 'refs/remotes/origin/main', activation);
|
||||
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
await writeFile(
|
||||
path.join(root, 'scripts', 'gate-verify.mjs'),
|
||||
"process.stderr.write('INTERMEDIATE INERT\\n'); process.exitCode = 1;\n",
|
||||
);
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'inert intermediate');
|
||||
await writeFile(
|
||||
path.join(root, 'scripts', 'gate-verify.mjs'),
|
||||
"process.stdout.write('HEAD HEALTHY\\n');\n",
|
||||
);
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'healthy head');
|
||||
|
||||
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
||||
process.env.CI_COMMIT_BRANCH = 'feature/rm-02';
|
||||
try {
|
||||
const result = await verifyHistory({
|
||||
root,
|
||||
manifest: { schemaVersion: 1 },
|
||||
});
|
||||
assert.deepEqual(result.failures, []);
|
||||
assert.ok(
|
||||
result.observations.some((observation) =>
|
||||
/HISTORY BOOTSTRAP BOUNDARY.*DOES:.*provider target.*sound.*cannot rewrite main.*DOES NOT:.*compromise.*main.*Builds 1-2/i.test(
|
||||
observation,
|
||||
),
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
result.observations.some((observation) =>
|
||||
/DOES:.*current tree.*DOES NOT:.*isolated.*RM-60.*RM-59/i.test(observation),
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
result.observations.some((observation) =>
|
||||
/INTERMEDIATE REPLAY DEFERRED.*RM-60.*no success is inferred/i.test(observation),
|
||||
),
|
||||
);
|
||||
assert.ok(result.observations.every((observation) => !/INTERMEDIATE INERT/.test(observation)));
|
||||
} finally {
|
||||
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
||||
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
||||
}
|
||||
});
|
||||
|
||||
test('target merge-base includes gate changes committed before registry introduction', async () => {
|
||||
const root = `${fixtureRoot}-delayed-introduction`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(root, { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'target baseline');
|
||||
const baseline = git(root, 'rev-parse', 'HEAD');
|
||||
git(root, 'update-ref', 'refs/remotes/origin/main', baseline);
|
||||
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
||||
await writeFile(path.join(root, 'scripts', 'preflight.mjs'), 'process.exit(0);\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'gate change before registry');
|
||||
const preRegistryGateChange = git(root, 'rev-parse', 'HEAD');
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'delayed registry introduction');
|
||||
|
||||
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
||||
process.env.CI_COMMIT_BRANCH = 'feature/delayed-introduction';
|
||||
try {
|
||||
const result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
|
||||
assert.match(
|
||||
result.failures.join('\n'),
|
||||
new RegExp(`${preRegistryGateChange}.*own-tree registry cannot be read`, 'i'),
|
||||
);
|
||||
} finally {
|
||||
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
||||
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
||||
}
|
||||
});
|
||||
|
||||
test('derived history boundary includes the registry-introduction commit', async () => {
|
||||
const root = `${fixtureRoot}-derived-boundary`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(root, { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'baseline');
|
||||
const baseline = git(root, 'rev-parse', 'HEAD');
|
||||
git(root, 'update-ref', 'refs/remotes/origin/main', baseline);
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'registry introduction');
|
||||
const introduction = git(root, 'rev-parse', 'HEAD');
|
||||
await writeFile(path.join(root, 'later.txt'), 'later\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'later');
|
||||
const head = git(root, 'rev-parse', 'HEAD');
|
||||
|
||||
assert.deepEqual(deriveHistoryBoundary(root, head), {
|
||||
activationCommit: baseline,
|
||||
introductionCommit: introduction,
|
||||
targetRef: 'refs/remotes/origin/main',
|
||||
});
|
||||
assert.deepEqual(await listProspectiveCommits(root, baseline, head), [introduction, head]);
|
||||
});
|
||||
|
||||
test('author-controlled activation seams cannot omit registry-era history', async () => {
|
||||
const root = `${fixtureRoot}-activation-seam`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'registry introduction');
|
||||
const introduction = git(root, 'rev-parse', 'HEAD');
|
||||
await writeFile(path.join(root, 'one.txt'), 'one\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'one');
|
||||
await writeFile(path.join(root, 'two.txt'), 'two\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'two');
|
||||
const head = git(root, 'rev-parse', 'HEAD');
|
||||
const parent = git(root, 'rev-parse', 'HEAD^');
|
||||
|
||||
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
||||
process.env.CI_COMMIT_BRANCH = 'feature/activation-seam';
|
||||
try {
|
||||
for (const candidate of [head, parent, introduction]) {
|
||||
const result = await verifyHistory({
|
||||
root,
|
||||
manifest: { schemaVersion: 1, activationCommit: candidate },
|
||||
});
|
||||
assert.match(result.failures.join('\n'), /author-controlled activationCommit.*forbidden/i);
|
||||
}
|
||||
} finally {
|
||||
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
||||
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
||||
}
|
||||
});
|
||||
|
||||
test('globally invalid provider evidence fails when HEAD is the only prospective commit', async () => {
|
||||
const root = `${fixtureRoot}-head-only-evidence`;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(root, { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.name', 'gate-test');
|
||||
git(root, 'config', 'user.email', '[email protected]');
|
||||
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'baseline');
|
||||
git(root, 'update-ref', 'refs/remotes/origin/main', git(root, 'rev-parse', 'HEAD'));
|
||||
await mkdir(path.join(root, 'gates'), { recursive: true });
|
||||
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
||||
git(root, 'add', '.');
|
||||
git(root, 'commit', '-m', 'registry introduction');
|
||||
const head = git(root, 'rev-parse', 'HEAD');
|
||||
const evidenceFile = path.join(root, 'provider-evidence.json');
|
||||
await writeFile(
|
||||
evidenceFile,
|
||||
JSON.stringify([
|
||||
{
|
||||
commit: head,
|
||||
number: 7,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
{
|
||||
commit: 'other-subject',
|
||||
number: 7,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
]),
|
||||
);
|
||||
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
||||
const previousEvidence = process.env.GATE_PROVIDER_EVIDENCE_FILE;
|
||||
process.env.CI_COMMIT_BRANCH = 'main';
|
||||
process.env.GATE_PROVIDER_EVIDENCE_FILE = evidenceFile;
|
||||
try {
|
||||
const result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
|
||||
assert.match(result.failures.join('\n'), /duplicate pipeline identity across commits/i);
|
||||
} finally {
|
||||
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
||||
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
||||
if (previousEvidence === undefined) delete process.env.GATE_PROVIDER_EVIDENCE_FILE;
|
||||
else process.env.GATE_PROVIDER_EVIDENCE_FILE = previousEvidence;
|
||||
}
|
||||
});
|
||||
|
||||
test('collection-wide validation rejects ambiguous gate steps on unrelated commits', () => {
|
||||
const records = [
|
||||
{
|
||||
commit: 'target',
|
||||
number: 7,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
{
|
||||
commit: 'unrelated',
|
||||
number: 8,
|
||||
status: 'success',
|
||||
steps: [
|
||||
{ name: 'gate-verify', status: 'success' },
|
||||
{ name: 'gate-verify', status: 'failure' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = assessProviderEvidence('target', records);
|
||||
assert.equal(result.state, 'terminal-failure');
|
||||
assert.match(result.detail, /ambiguous gate-verify step count/i);
|
||||
});
|
||||
|
||||
test('provider evidence rejects non-object collection entries without crashing', () => {
|
||||
for (const record of [null, [], 'text', 42]) {
|
||||
const result = assessProviderEvidence('aaa', [record]);
|
||||
assert.equal(result.state, 'terminal-failure');
|
||||
assert.match(result.detail, /malformed/i);
|
||||
}
|
||||
});
|
||||
|
||||
test('provider evidence rejects duplicate pipeline identity across commits', () => {
|
||||
const records = [
|
||||
{
|
||||
commit: 'aaa',
|
||||
number: 7,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
{
|
||||
commit: 'bbb',
|
||||
number: 7,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
];
|
||||
const result = assessProviderEvidence('aaa', records);
|
||||
assert.equal(result.state, 'terminal-failure');
|
||||
assert.match(result.detail, /duplicate pipeline.*across.*commit|global.*pipeline.*identity/i);
|
||||
});
|
||||
|
||||
test('provider evidence distinguishes retained success, failure, and absent history', () => {
|
||||
const pipelines = [
|
||||
{
|
||||
commit: 'aaa',
|
||||
number: 1,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
{
|
||||
commit: 'bbb',
|
||||
number: 2,
|
||||
status: 'failure',
|
||||
steps: [{ name: 'gate-verify', status: 'failure' }],
|
||||
},
|
||||
];
|
||||
assert.deepEqual(assessProviderEvidence('aaa', pipelines), {
|
||||
state: 'terminal-success',
|
||||
detail: 'pipeline and gate-verify step succeeded',
|
||||
});
|
||||
assert.equal(assessProviderEvidence('bbb', pipelines).state, 'terminal-failure');
|
||||
assert.equal(assessProviderEvidence('ccc', pipelines).state, 'absent');
|
||||
});
|
||||
|
||||
test('duplicate gate-verify steps cannot establish provider success', () => {
|
||||
const result = assessProviderEvidence('aaa', [
|
||||
{
|
||||
commit: 'aaa',
|
||||
number: 7,
|
||||
status: 'success',
|
||||
steps: [
|
||||
{ name: 'gate-verify', status: 'success' },
|
||||
{ name: 'gate-verify', status: 'failure' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert.equal(result.state, 'terminal-failure');
|
||||
assert.match(result.detail, /ambiguous.*gate-verify/i);
|
||||
});
|
||||
|
||||
test('a malformed single provider record cannot establish success', () => {
|
||||
assert.equal(
|
||||
assessProviderEvidence('aaa', [
|
||||
{ commit: 'aaa', status: 'success', steps: [{ name: 'gate-verify', status: 'success' }] },
|
||||
]).state,
|
||||
'terminal-failure',
|
||||
);
|
||||
assert.equal(
|
||||
assessProviderEvidence('bbb', [
|
||||
{
|
||||
commit: 'bbb',
|
||||
number: 1,
|
||||
status: 'surprising',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
]).state,
|
||||
'terminal-failure',
|
||||
);
|
||||
});
|
||||
|
||||
test('provider evidence selects the highest numbered rerun deterministically', () => {
|
||||
const failedThenSucceeded = [
|
||||
{
|
||||
commit: 'aaa',
|
||||
number: 10,
|
||||
status: 'failure',
|
||||
steps: [{ name: 'gate-verify', status: 'failure' }],
|
||||
},
|
||||
{
|
||||
commit: 'aaa',
|
||||
number: 11,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
];
|
||||
const succeededThenFailed = [
|
||||
{
|
||||
commit: 'bbb',
|
||||
number: 21,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
{
|
||||
commit: 'bbb',
|
||||
number: 22,
|
||||
status: 'failure',
|
||||
steps: [{ name: 'gate-verify', status: 'failure' }],
|
||||
},
|
||||
];
|
||||
assert.equal(assessProviderEvidence('aaa', failedThenSucceeded).state, 'terminal-success');
|
||||
assert.equal(assessProviderEvidence('bbb', succeededThenFailed).state, 'terminal-failure');
|
||||
assert.equal(
|
||||
assessProviderEvidence('ccc', [
|
||||
{ commit: 'ccc', status: 'success', steps: [{ name: 'gate-verify', status: 'success' }] },
|
||||
{ commit: 'ccc', status: 'failure', steps: [{ name: 'gate-verify', status: 'failure' }] },
|
||||
]).state,
|
||||
'terminal-failure',
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const root = process.cwd();
|
||||
const removedGateId = 'hook-pre-push';
|
||||
const inventoryEntry = " ['hook-pre-push', '.husky/pre-push'],\n";
|
||||
|
||||
function shrinkManifest(manifest) {
|
||||
const removedGate = manifest.gates.find((gate) => gate.id === removedGateId);
|
||||
assert.ok(removedGate);
|
||||
const removedCaseRefs = new Set(
|
||||
removedGate.cases.map((gateCase) => `${removedGateId}/${gateCase.id}`),
|
||||
);
|
||||
const removedCriterionIds = new Set(
|
||||
manifest.criteria
|
||||
.filter(
|
||||
(criterion) =>
|
||||
criterion.caseRefs.length > 0 &&
|
||||
criterion.caseRefs.every((caseRef) => removedCaseRefs.has(caseRef)),
|
||||
)
|
||||
.map((criterion) => criterion.id),
|
||||
);
|
||||
manifest.gates = manifest.gates.filter((gate) => gate.id !== removedGateId);
|
||||
manifest.criteria = manifest.criteria
|
||||
.filter((criterion) => !removedCriterionIds.has(criterion.id))
|
||||
.map((criterion) => ({
|
||||
...criterion,
|
||||
caseRefs: criterion.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
|
||||
...(criterion.gateRefs
|
||||
? { gateRefs: criterion.gateRefs.filter((gateId) => gateId !== removedGateId) }
|
||||
: {}),
|
||||
}));
|
||||
manifest.proseClaims = manifest.proseClaims.filter(
|
||||
(claim) => !removedCriterionIds.has(claim.criterionId) && !removedCaseRefs.has(claim.caseRef),
|
||||
);
|
||||
manifest.compatibilityScenarios = manifest.compatibilityScenarios
|
||||
.map((scenario) => ({
|
||||
...scenario,
|
||||
caseRefs: scenario.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
|
||||
}))
|
||||
.filter((scenario) => scenario.caseRefs.length > 0);
|
||||
for (const gate of manifest.gates) {
|
||||
for (const gateCase of gate.cases) {
|
||||
gateCase.criterionIds = gateCase.criterionIds.filter(
|
||||
(criterionId) => !removedCriterionIds.has(criterionId),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function attack(mode) {
|
||||
const fixture = await mkdtemp(path.join(os.tmpdir(), `gate-inventory-${mode}-`));
|
||||
try {
|
||||
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(fixture, 'gates'), { recursive: true });
|
||||
const source = await readFile(path.join(root, 'scripts', 'gate-verify.mjs'), 'utf8');
|
||||
if (source.split(inventoryEntry).length - 1 !== 1) {
|
||||
throw new Error('source inventory fixture drifted');
|
||||
}
|
||||
await writeFile(
|
||||
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
||||
mode === 'source-manifest' ? source.replace(inventoryEntry, '') : source,
|
||||
);
|
||||
const baseline = JSON.parse(
|
||||
await readFile(path.join(root, 'gates', 'required-gates.baseline.json'), 'utf8'),
|
||||
);
|
||||
if (mode === 'baseline-manifest') {
|
||||
baseline.gates = baseline.gates.filter((gate) => gate.id !== removedGateId);
|
||||
}
|
||||
await writeFile(
|
||||
path.join(fixture, 'gates', 'required-gates.baseline.json'),
|
||||
`${JSON.stringify(baseline)}\n`,
|
||||
);
|
||||
const manifest = JSON.parse(
|
||||
await readFile(path.join(root, 'gates', 'gates.manifest.json'), 'utf8'),
|
||||
);
|
||||
shrinkManifest(manifest);
|
||||
await writeFile(
|
||||
path.join(fixture, 'gates', 'gates.manifest.json'),
|
||||
`${JSON.stringify(manifest)}\n`,
|
||||
);
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
||||
'--root',
|
||||
fixture,
|
||||
'--manifest',
|
||||
'gates/gates.manifest.json',
|
||||
'--structure-only',
|
||||
],
|
||||
{ cwd: fixture, encoding: 'utf8' },
|
||||
);
|
||||
const combined = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
||||
return (
|
||||
result.status !== 0 &&
|
||||
new RegExp(`(?:baseline|verifier inventory).*${removedGateId}`, 'i').test(combined)
|
||||
);
|
||||
} finally {
|
||||
await rm(fixture, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const sourceManifestRejected = await attack('source-manifest');
|
||||
const baselineManifestRejected = await attack('baseline-manifest');
|
||||
if (sourceManifestRejected && baselineManifestRejected) {
|
||||
process.stderr.write(
|
||||
'INVENTORY_SHRINK_REJECTED: source+manifest and baseline+manifest shrink attacks detected\n',
|
||||
);
|
||||
process.exit(83);
|
||||
}
|
||||
process.stdout.write(
|
||||
`inventory shrink attack escaped: source-manifest=${sourceManifestRejected} baseline-manifest=${baselineManifestRejected}\n`,
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -27,7 +27,6 @@ async function rejectedForEveryGate(mutate, diagnostic) {
|
||||
const result = await verifyRegistry({
|
||||
root,
|
||||
manifest: manifestPath,
|
||||
skipHistory: true,
|
||||
structureOnly: true,
|
||||
fixtureProfile: false,
|
||||
});
|
||||
@@ -41,14 +40,81 @@ async function rejectedForEveryGate(mutate, diagnostic) {
|
||||
|
||||
let rejected;
|
||||
if (mode === 'evidence-subject') {
|
||||
rejected = await rejectedForEveryGate(
|
||||
(gate) => {
|
||||
gate.evidenceSubject = 'different-gate-subject';
|
||||
},
|
||||
(failure, gateId) =>
|
||||
failure.includes(`gate ${gateId}: evidence subject`) &&
|
||||
failure.includes('does not match gate id'),
|
||||
);
|
||||
rejected = true;
|
||||
for (const gateId of expectedGateIds) {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), 'gate-evidence-consumption-'));
|
||||
try {
|
||||
await mkdir(path.join(directory, 'gates'), { recursive: true });
|
||||
const probe = path.join(directory, 'gates', 'probe.sh');
|
||||
await writeFile(probe, '#!/bin/sh\necho EVIDENCE_PROBE >&2\nexit 7\n');
|
||||
await chmod(probe, 0o755);
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
gateRoots: ['gates'],
|
||||
governingClaimFiles: [],
|
||||
coverageBoundary: { included: ['evidence fixture'], excluded: [], trackedBy: 'RM-02' },
|
||||
criteria: [
|
||||
{
|
||||
id: 'EVIDENCE-CONSUMPTION',
|
||||
originalText: 'Consumed evidence stays bound to its gate.',
|
||||
currentText: 'Consumed evidence stays bound to its gate.',
|
||||
claimType: 'integrity',
|
||||
source: 'gate-population-control',
|
||||
meaningChanges: [],
|
||||
caseRefs: [`${gateId}/probe`],
|
||||
},
|
||||
],
|
||||
proseClaims: [],
|
||||
compatibilityScenarios: [],
|
||||
gates: [
|
||||
{
|
||||
id: gateId,
|
||||
source: 'gates/probe.sh',
|
||||
invocation: ['gates/probe.sh'],
|
||||
deployment: { kind: 'none', reason: 'population fixture' },
|
||||
inertMutation: {
|
||||
file: 'gates/probe.sh',
|
||||
find: 'exit 7',
|
||||
replace: 'exit 0',
|
||||
caseId: 'probe',
|
||||
expected: { exitCode: 0 },
|
||||
},
|
||||
cases: [
|
||||
{
|
||||
id: 'probe',
|
||||
criterionIds: ['EVIDENCE-CONSUMPTION'],
|
||||
mustFail: true,
|
||||
required: { exitCode: 7 },
|
||||
actual: { exitCode: 7 },
|
||||
evidence: { subject: 'different-gate-subject' },
|
||||
reasonPattern: 'EVIDENCE_PROBE',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const manifestPath = path.join(directory, 'gates', 'gates.manifest.json');
|
||||
await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
|
||||
const result = await verifyRegistry({
|
||||
root: directory,
|
||||
manifest: manifestPath,
|
||||
structureOnly: false,
|
||||
fixtureProfile: true,
|
||||
});
|
||||
if (
|
||||
!result.failures.some(
|
||||
(failure) =>
|
||||
failure.includes(`gate ${gateId}: consumed evidence subject`) &&
|
||||
failure.includes('does not match gate definition'),
|
||||
)
|
||||
) {
|
||||
rejected = false;
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
} else if (mode === 'type-strict') {
|
||||
rejected = await rejectedForEveryGate(
|
||||
(gate) => {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { assessProviderEvidence } from './gate-history.mjs';
|
||||
|
||||
const records = [
|
||||
{
|
||||
commit: 'subject-a',
|
||||
number: 7,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
{
|
||||
commit: 'subject-b',
|
||||
number: 7,
|
||||
status: 'success',
|
||||
steps: [{ name: 'gate-verify', status: 'success' }],
|
||||
},
|
||||
];
|
||||
const result = assessProviderEvidence('subject-a', records);
|
||||
if (
|
||||
result.state === 'terminal-failure' &&
|
||||
/duplicate pipeline identity across commits/i.test(result.detail)
|
||||
) {
|
||||
process.stderr.write(`${result.detail}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(
|
||||
`cross-commit duplicate was not rejected: ${result.state} (${result.detail})\n`,
|
||||
);
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,233 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import test from 'node:test';
|
||||
|
||||
const root = process.cwd();
|
||||
const verifierPath = path.join(root, 'scripts', 'gate-verify.mjs');
|
||||
const manifestPath = path.join(root, 'gates', 'gates.manifest.json');
|
||||
const requiredGateId = 'hook-pre-push';
|
||||
|
||||
function output(result) {
|
||||
return `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
||||
}
|
||||
|
||||
function shrinkManifest(manifest, removedGateId) {
|
||||
const removedGate = manifest.gates.find((gate) => gate.id === removedGateId);
|
||||
assert.ok(removedGate, `fixture gate ${removedGateId} must exist`);
|
||||
const removedCaseRefs = new Set(
|
||||
removedGate.cases.map((gateCase) => `${removedGateId}/${gateCase.id}`),
|
||||
);
|
||||
const removedCriterionIds = new Set(
|
||||
manifest.criteria
|
||||
.filter(
|
||||
(criterion) =>
|
||||
criterion.caseRefs.length > 0 &&
|
||||
criterion.caseRefs.every((caseRef) => removedCaseRefs.has(caseRef)),
|
||||
)
|
||||
.map((criterion) => criterion.id),
|
||||
);
|
||||
|
||||
manifest.gates = manifest.gates.filter((gate) => gate.id !== removedGateId);
|
||||
manifest.criteria = manifest.criteria
|
||||
.filter((criterion) => !removedCriterionIds.has(criterion.id))
|
||||
.map((criterion) => ({
|
||||
...criterion,
|
||||
caseRefs: criterion.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
|
||||
...(criterion.gateRefs
|
||||
? { gateRefs: criterion.gateRefs.filter((gateId) => gateId !== removedGateId) }
|
||||
: {}),
|
||||
}));
|
||||
manifest.proseClaims = manifest.proseClaims.filter(
|
||||
(claim) => !removedCriterionIds.has(claim.criterionId) && !removedCaseRefs.has(claim.caseRef),
|
||||
);
|
||||
manifest.compatibilityScenarios = manifest.compatibilityScenarios
|
||||
.map((scenario) => ({
|
||||
...scenario,
|
||||
caseRefs: scenario.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
|
||||
}))
|
||||
.filter((scenario) => scenario.caseRefs.length > 0);
|
||||
for (const gate of manifest.gates) {
|
||||
for (const gateCase of gate.cases) {
|
||||
gateCase.criterionIds = gateCase.criterionIds.filter(
|
||||
(criterionId) => !removedCriterionIds.has(criterionId),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('shrinking the verifier inventory and manifest together is rejected by an independent baseline', async () => {
|
||||
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-shrink-both-'));
|
||||
try {
|
||||
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(fixture, 'gates'), { recursive: true });
|
||||
const verifier = await readFile(verifierPath, 'utf8');
|
||||
const inventoryEntry = " ['hook-pre-push', '.husky/pre-push'],\n";
|
||||
assert.equal(verifier.split(inventoryEntry).length - 1, 1, 'source inventory fixture drifted');
|
||||
await writeFile(
|
||||
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
||||
verifier.replace(inventoryEntry, ''),
|
||||
);
|
||||
await copyFile(
|
||||
path.join(root, 'gates', 'required-gates.baseline.json'),
|
||||
path.join(fixture, 'gates', 'required-gates.baseline.json'),
|
||||
);
|
||||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
shrinkManifest(manifest, requiredGateId);
|
||||
await writeFile(
|
||||
path.join(fixture, 'gates', 'gates.manifest.json'),
|
||||
`${JSON.stringify(manifest)}\n`,
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
||||
'--root',
|
||||
fixture,
|
||||
'--manifest',
|
||||
'gates/gates.manifest.json',
|
||||
'--structure-only',
|
||||
],
|
||||
{ cwd: fixture, encoding: 'utf8' },
|
||||
);
|
||||
assert.notEqual(result.status, 0, 'shrinking source anchor and manifest together must go red');
|
||||
assert.match(output(result), /independent required-gate baseline.*hook-pre-push/i);
|
||||
} finally {
|
||||
await rm(fixture, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('shrinking the independent baseline and manifest together is rejected by verifier inventory', async () => {
|
||||
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-shrink-baseline-manifest-'));
|
||||
try {
|
||||
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(fixture, 'gates'), { recursive: true });
|
||||
await copyFile(verifierPath, path.join(fixture, 'scripts', 'gate-verify.mjs'));
|
||||
const baseline = JSON.parse(
|
||||
await readFile(path.join(root, 'gates', 'required-gates.baseline.json'), 'utf8'),
|
||||
);
|
||||
baseline.gates = baseline.gates.filter((gate) => gate.id !== requiredGateId);
|
||||
await writeFile(
|
||||
path.join(fixture, 'gates', 'required-gates.baseline.json'),
|
||||
`${JSON.stringify(baseline)}\n`,
|
||||
);
|
||||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
shrinkManifest(manifest, requiredGateId);
|
||||
await writeFile(
|
||||
path.join(fixture, 'gates', 'gates.manifest.json'),
|
||||
`${JSON.stringify(manifest)}\n`,
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
||||
'--root',
|
||||
fixture,
|
||||
'--manifest',
|
||||
'gates/gates.manifest.json',
|
||||
'--structure-only',
|
||||
],
|
||||
{ cwd: fixture, encoding: 'utf8' },
|
||||
);
|
||||
assert.notEqual(result.status, 0, 'shrinking baseline and manifest together must go red');
|
||||
assert.match(output(result), /verifier inventory.*hook-pre-push.*baseline/i);
|
||||
} finally {
|
||||
await rm(fixture, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('every gate carries an evidence-side subject distinct from its definition', async () => {
|
||||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
for (const gate of manifest.gates) {
|
||||
assert.ok(gate.cases.length > 0, `${gate.id} must have consumable evidence`);
|
||||
for (const gateCase of gate.cases) {
|
||||
assert.equal(
|
||||
gateCase.evidence?.subject,
|
||||
gate.id,
|
||||
`${gate.id}/${gateCase.id} must source its subject from the evidence record`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('evidence population control depends on production result consumption wiring', async () => {
|
||||
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-evidence-consumer-inert-'));
|
||||
try {
|
||||
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
||||
await mkdir(path.join(fixture, 'gates'), { recursive: true });
|
||||
const verifier = await readFile(verifierPath, 'utf8');
|
||||
const consumer = ` const subjectFailure = consumeEvidenceSubject(gate, result.evidence);\n if (subjectFailure) failures.push(subjectFailure);\n`;
|
||||
assert.equal(verifier.split(consumer).length - 1, 1, 'consumer fixture drifted');
|
||||
await writeFile(
|
||||
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
||||
verifier.replace(consumer, ''),
|
||||
);
|
||||
await copyFile(
|
||||
path.join(root, 'scripts', 'gate-population-control.mjs'),
|
||||
path.join(fixture, 'scripts', 'gate-population-control.mjs'),
|
||||
);
|
||||
await copyFile(manifestPath, path.join(fixture, 'gates', 'gates.manifest.json'));
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(fixture, 'scripts', 'gate-population-control.mjs'), 'evidence-subject'],
|
||||
{ cwd: fixture, encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(result.status, 0, output(result));
|
||||
assert.match(output(result), /did not reject every registered gate/i);
|
||||
} finally {
|
||||
await rm(fixture, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('production verification has a closed current-tree observation renderer', async () => {
|
||||
const verifier = await readFile(verifierPath, 'utf8');
|
||||
assert.doesNotMatch(verifier, /from ['"]\.\/gate-history\.mjs['"]/);
|
||||
assert.doesNotMatch(verifier, /\bverifyHistory\s*\(/);
|
||||
assert.doesNotMatch(verifier, /history[-_ ]?provenance/i);
|
||||
const attacks = [
|
||||
[
|
||||
'history wording',
|
||||
' /^META-NEGATIVE-CONTROL /,',
|
||||
' /^HISTORY PROVENANCE VERIFIED /,\n /^META-NEGATIVE-CONTROL /,',
|
||||
],
|
||||
[
|
||||
'renamed ancestry wording',
|
||||
' /^META-NEGATIVE-CONTROL /,',
|
||||
' /^COMMIT ANCESTRY VERIFIED /,\n /^META-NEGATIVE-CONTROL /,',
|
||||
],
|
||||
[
|
||||
'provider lineage wording',
|
||||
' /^META-NEGATIVE-CONTROL /,',
|
||||
' /^PROVIDER LINEAGE SUCCESS /,\n /^META-NEGATIVE-CONTROL /,',
|
||||
],
|
||||
['renderer bypass', ' assertCurrentTreeObservation(observation);\n', ''],
|
||||
];
|
||||
for (const [name, find, replace] of attacks) {
|
||||
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-history-renderer-'));
|
||||
try {
|
||||
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
||||
assert.equal(verifier.split(find).length - 1, 1, `${name}: fixture drifted`);
|
||||
await writeFile(
|
||||
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
||||
verifier.replace(find, replace),
|
||||
);
|
||||
await copyFile(
|
||||
path.join(root, 'scripts', 'gate-history-exclusion-control.mjs'),
|
||||
path.join(fixture, 'scripts', 'gate-history-exclusion-control.mjs'),
|
||||
);
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(fixture, 'scripts', 'gate-history-exclusion-control.mjs')],
|
||||
{ cwd: fixture, encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(result.status, 79, `${name}: ${output(result)}`);
|
||||
assert.match(output(result), /HISTORY_PROVENANCE_FORBIDDEN/);
|
||||
} finally {
|
||||
await rm(fixture, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
+111
-34
@@ -20,8 +20,6 @@ import {
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
import { verifyHistory } from './gate-history.mjs';
|
||||
|
||||
const COPY_SKIP = new Set(['.git', '.mosaic-test-work', '.next', '.turbo', 'coverage', 'dist']);
|
||||
const POPULATION_CRITERION_IDS = new Set([
|
||||
'RM02-EVIDENCE-SUBJECT-BINDING',
|
||||
@@ -42,14 +40,12 @@ function parseArgs(argv) {
|
||||
const options = {
|
||||
root: process.cwd(),
|
||||
manifest: 'gates/gates.manifest.json',
|
||||
skipHistory: false,
|
||||
structureOnly: false,
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const value = argv[index];
|
||||
if (value === '--root') options.root = path.resolve(argv[++index]);
|
||||
else if (value === '--manifest') options.manifest = argv[++index];
|
||||
else if (value === '--skip-history') options.skipHistory = true;
|
||||
else if (value === '--structure-only') options.structureOnly = true;
|
||||
else throw new Error(`unknown option: ${value}`);
|
||||
}
|
||||
@@ -222,7 +218,8 @@ async function runCase(root, gate, gateCase) {
|
||||
expand(value, caseRoot),
|
||||
]),
|
||||
);
|
||||
return runInvocation(caseRoot, gateCase.invocation ?? gate.invocation, environment);
|
||||
const result = runInvocation(caseRoot, gateCase.invocation ?? gate.invocation, environment);
|
||||
return { ...result, evidence: structuredClone(gateCase.evidence) };
|
||||
} finally {
|
||||
if (caseRoot !== root) await rm(caseRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -396,7 +393,36 @@ function validateEnvironment(environment, label, failures) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateClosedSchema(manifest, failures, { fixtureProfile = false } = {}) {
|
||||
function inventoriesEqual(left, right) {
|
||||
return structuredValuesEqual([...left.entries()], [...right.entries()]);
|
||||
}
|
||||
|
||||
export function consumeEvidenceSubject(gate, evidence) {
|
||||
if (evidence?.subject !== gate.id) {
|
||||
return `gate ${gate.id}: consumed evidence subject ${String(evidence?.subject)} does not match gate definition`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const CURRENT_TREE_OBSERVATION_PATTERNS = [
|
||||
/^META-NEGATIVE-CONTROL /,
|
||||
/^DEPLOYMENT-NEGATIVE-CONTROL /,
|
||||
/^DEPLOYED IDENTITY UNAVAILABLE /,
|
||||
/^DEFECT /,
|
||||
/^COMPATIBILITY /,
|
||||
];
|
||||
|
||||
export function assertCurrentTreeObservation(observation) {
|
||||
if (!CURRENT_TREE_OBSERVATION_PATTERNS.some((pattern) => pattern.test(observation))) {
|
||||
throw new Error(`unsupported observation class: ${observation}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateClosedSchema(
|
||||
manifest,
|
||||
failures,
|
||||
{ fixtureProfile = false, requiredGateInventory } = {},
|
||||
) {
|
||||
if (!fixtureProfile) {
|
||||
for (const population of ['criteria', 'gates', 'proseClaims', 'compatibilityScenarios']) {
|
||||
if (!Array.isArray(manifest[population]) || manifest[population].length === 0) {
|
||||
@@ -440,17 +466,11 @@ function validateClosedSchema(manifest, failures, { fixtureProfile = false } = {
|
||||
if (manifest.mergeAssertions !== undefined) {
|
||||
rejectUnknownKeys(
|
||||
manifest.mergeAssertions,
|
||||
new Set([
|
||||
'mode',
|
||||
'providerEvidence',
|
||||
'deferredReplayOwner',
|
||||
'trustDependencies',
|
||||
'postMergeResponse',
|
||||
]),
|
||||
new Set(['mode', 'deferredReplayOwner', 'trustDependencies', 'postMergeResponse']),
|
||||
'mergeAssertions',
|
||||
failures,
|
||||
);
|
||||
for (const key of ['mode', 'providerEvidence', 'deferredReplayOwner', 'postMergeResponse']) {
|
||||
for (const key of ['mode', 'deferredReplayOwner', 'postMergeResponse']) {
|
||||
requireString(manifest.mergeAssertions?.[key], `mergeAssertions.${key}`, failures);
|
||||
}
|
||||
validateStringArray(
|
||||
@@ -564,12 +584,32 @@ function validateClosedSchema(manifest, failures, { fixtureProfile = false } = {
|
||||
}
|
||||
rejectDuplicateIds(manifest.gates, 'gate', failures);
|
||||
if (!fixtureProfile) {
|
||||
for (const [requiredId, requiredSource] of REQUIRED_GATE_INVENTORY) {
|
||||
const registered = (manifest.gates ?? []).find((gate) => gate?.id === requiredId);
|
||||
if (!registered || registered.source !== requiredSource) {
|
||||
failures.push(
|
||||
`gates population is not anchored: required ${requiredId} at ${requiredSource}`,
|
||||
);
|
||||
if (!(requiredGateInventory instanceof Map) || requiredGateInventory.size === 0) {
|
||||
failures.push('independent required-gate baseline is absent or empty');
|
||||
} else {
|
||||
if (!inventoriesEqual(REQUIRED_GATE_INVENTORY, requiredGateInventory)) {
|
||||
for (const [requiredId, requiredSource] of requiredGateInventory) {
|
||||
if (REQUIRED_GATE_INVENTORY.get(requiredId) !== requiredSource) {
|
||||
failures.push(
|
||||
`independent required-gate baseline rejects verifier inventory drift at ${requiredId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const [requiredId, requiredSource] of REQUIRED_GATE_INVENTORY) {
|
||||
if (requiredGateInventory.get(requiredId) !== requiredSource) {
|
||||
failures.push(
|
||||
`verifier inventory ${requiredId} is absent or changed in independent required-gate baseline`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [requiredId, requiredSource] of requiredGateInventory) {
|
||||
const registered = (manifest.gates ?? []).find((gate) => gate?.id === requiredId);
|
||||
if (!registered || registered.source !== requiredSource) {
|
||||
failures.push(
|
||||
`independent required-gate baseline rejects manifest drift at ${requiredId}: required source ${requiredSource}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -584,19 +624,12 @@ function validateClosedSchema(manifest, failures, { fixtureProfile = false } = {
|
||||
'inertMutation',
|
||||
'cases',
|
||||
'discoveryAliases',
|
||||
'evidenceSubject',
|
||||
]),
|
||||
`gate ${gate.id}`,
|
||||
failures,
|
||||
);
|
||||
requireString(gate.id, `gate ${gate.id}.id`, failures);
|
||||
requireString(gate.source, `gate ${gate.id}.source`, failures);
|
||||
requireString(gate.evidenceSubject, `gate ${gate.id}.evidenceSubject`, failures);
|
||||
if (gate.evidenceSubject !== gate.id) {
|
||||
failures.push(
|
||||
`gate ${gate.id}: evidence subject ${String(gate.evidenceSubject)} does not match gate id`,
|
||||
);
|
||||
}
|
||||
rejectDuplicateIds(gate.cases, `case in gate ${gate.id}`, failures);
|
||||
if (!Array.isArray(gate.invocation) || gate.invocation.length === 0) {
|
||||
failures.push(`${gate.id}: exact invocation is missing`);
|
||||
@@ -656,6 +689,7 @@ function validateClosedSchema(manifest, failures, { fixtureProfile = false } = {
|
||||
'invocation',
|
||||
'required',
|
||||
'actual',
|
||||
'evidence',
|
||||
'reasonPattern',
|
||||
'environment',
|
||||
'fixture',
|
||||
@@ -675,6 +709,17 @@ function validateClosedSchema(manifest, failures, { fixtureProfile = false } = {
|
||||
}
|
||||
validateOutcome(gateCase.required, `${gate.id}/${gateCase.id}.required`, failures);
|
||||
validateOutcome(gateCase.actual, `${gate.id}/${gateCase.id}.actual`, failures);
|
||||
rejectUnknownKeys(
|
||||
gateCase.evidence,
|
||||
new Set(['subject']),
|
||||
`${gate.id}/${gateCase.id}.evidence`,
|
||||
failures,
|
||||
);
|
||||
requireString(
|
||||
gateCase.evidence?.subject,
|
||||
`${gate.id}/${gateCase.id}.evidence.subject`,
|
||||
failures,
|
||||
);
|
||||
if (gateCase.invocation !== undefined) {
|
||||
validateStringArray(gateCase.invocation, `${gate.id}/${gateCase.id}.invocation`, failures);
|
||||
}
|
||||
@@ -1056,8 +1101,40 @@ export async function verifyRegistry(options) {
|
||||
const observations = [];
|
||||
const manifestPath = path.resolve(options.root, options.manifest);
|
||||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
let requiredGateInventory;
|
||||
if (!options.fixtureProfile) {
|
||||
const baselinePath = path.resolve(options.root, 'gates/required-gates.baseline.json');
|
||||
try {
|
||||
const baseline = JSON.parse(await readFile(baselinePath, 'utf8'));
|
||||
if (
|
||||
baseline.schemaVersion !== 1 ||
|
||||
!Array.isArray(baseline.gates) ||
|
||||
Object.keys(baseline).some((key) => !['schemaVersion', 'purpose', 'gates'].includes(key)) ||
|
||||
baseline.gates.some(
|
||||
(gate) =>
|
||||
!gate ||
|
||||
typeof gate !== 'object' ||
|
||||
Array.isArray(gate) ||
|
||||
Object.keys(gate).some((key) => !['id', 'source'].includes(key)) ||
|
||||
typeof gate.id !== 'string' ||
|
||||
gate.id.length === 0 ||
|
||||
typeof gate.source !== 'string' ||
|
||||
gate.source.length === 0,
|
||||
)
|
||||
) {
|
||||
failures.push('independent required-gate baseline has unsupported structure');
|
||||
} else {
|
||||
requiredGateInventory = new Map(baseline.gates.map((gate) => [gate.id, gate.source]));
|
||||
if (requiredGateInventory.size !== baseline.gates.length) {
|
||||
failures.push('independent required-gate baseline has duplicate gate ids');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
failures.push(`independent required-gate baseline cannot be read: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
validateStructure(manifest, failures, options);
|
||||
validateStructure(manifest, failures, { ...options, requiredGateInventory });
|
||||
if (options.structureOnly) return { failures, manifest, observations };
|
||||
|
||||
async function collectPhaseFailure(label, action) {
|
||||
@@ -1088,6 +1165,8 @@ export async function verifyRegistry(options) {
|
||||
if (!result) continue;
|
||||
const combined = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
||||
try {
|
||||
const subjectFailure = consumeEvidenceSubject(gate, result.evidence);
|
||||
if (subjectFailure) failures.push(subjectFailure);
|
||||
if (!outcomeMatches(gateCase.actual, result)) {
|
||||
failures.push(
|
||||
`${gate.id}/${gateCase.id}: observed exit ${String(result.status)}${result.signal ? ` signal ${result.signal}` : ''}${result.error ? ` error ${result.error.message}` : ''} or output disagrees with registry actual ${JSON.stringify(gateCase.actual)}`,
|
||||
@@ -1122,13 +1201,11 @@ export async function verifyRegistry(options) {
|
||||
async function main() {
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const { failures, observations, manifest } = await verifyRegistry(options);
|
||||
if (!options.skipHistory && failures.length === 0) {
|
||||
const history = await verifyHistory({ root: options.root, manifest });
|
||||
failures.push(...history.failures);
|
||||
observations.push(...history.observations);
|
||||
const { failures, observations } = await verifyRegistry(options);
|
||||
for (const observation of observations) {
|
||||
assertCurrentTreeObservation(observation);
|
||||
process.stdout.write(`${observation}\n`);
|
||||
}
|
||||
for (const observation of observations) process.stdout.write(`${observation}\n`);
|
||||
if (failures.length > 0) {
|
||||
for (const failure of failures) process.stderr.write(`GATE VERIFY FAILED: ${failure}\n`);
|
||||
process.exitCode = 1;
|
||||
|
||||
@@ -44,7 +44,6 @@ function baseManifest() {
|
||||
{
|
||||
id: 'meta-fixture',
|
||||
source: 'gates/meta-fixture.sh',
|
||||
evidenceSubject: 'meta-fixture',
|
||||
invocation: ['gates/meta-fixture.sh'],
|
||||
deployment: { kind: 'none', reason: 'test fixture only' },
|
||||
inertMutation: {
|
||||
@@ -61,6 +60,7 @@ function baseManifest() {
|
||||
invocation: ['gates/meta-fixture.sh'],
|
||||
required: { exitCode: 7 },
|
||||
actual: { exitCode: 7 },
|
||||
evidence: { subject: 'meta-fixture' },
|
||||
reasonPattern: 'META_REJECT',
|
||||
},
|
||||
],
|
||||
@@ -96,7 +96,6 @@ function verifyProductionStructure(root, extraArgs = []) {
|
||||
root,
|
||||
'--manifest',
|
||||
'gates/gates.manifest.json',
|
||||
'--skip-history',
|
||||
'--structure-only',
|
||||
...extraArgs,
|
||||
],
|
||||
@@ -143,6 +142,10 @@ test('anchored gate inventory and population criteria cannot shrink together', a
|
||||
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
|
||||
);
|
||||
const root = await fixture('shrunken-gate-population');
|
||||
await copyFile(
|
||||
path.join(process.cwd(), 'gates', 'required-gates.baseline.json'),
|
||||
path.join(root, 'gates', 'required-gates.baseline.json'),
|
||||
);
|
||||
source.gates = source.gates.filter((gate) => gate.id !== 'hook-pre-push');
|
||||
for (const criterion of source.criteria) {
|
||||
if (criterion.gateRefs) {
|
||||
@@ -156,7 +159,10 @@ test('anchored gate inventory and population criteria cannot shrink together', a
|
||||
|
||||
const result = verifyProductionStructure(root);
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(output(result), /gates population is not anchored.*hook-pre-push/i);
|
||||
assert.match(
|
||||
output(result),
|
||||
/independent required-gate baseline rejects manifest drift at hook-pre-push/i,
|
||||
);
|
||||
});
|
||||
|
||||
test('general population criteria cannot delete their gateRefs binding', async () => {
|
||||
@@ -648,6 +654,7 @@ test('compatibility scenarios execute referenced conditions as one construction'
|
||||
fixture: { writeFiles: [{ path: 'conditions/second', content: 'present\n' }] },
|
||||
required: { exitCode: 7 },
|
||||
actual: { exitCode: 7 },
|
||||
evidence: { subject: 'meta-fixture' },
|
||||
reasonPattern: 'SECOND_REASON',
|
||||
environment: { SECOND_REASON: 'SECOND_REASON' },
|
||||
});
|
||||
@@ -761,10 +768,6 @@ test('deployment drift meta-control fails if the shared comparator is made inert
|
||||
);
|
||||
assert.notEqual(inertSource, verifierSource, 'shared deployment comparator mutation went stale');
|
||||
await writeFile(path.join(alteredScripts, 'gate-verify.mjs'), inertSource);
|
||||
await copyFile(
|
||||
path.join(process.cwd(), 'scripts', 'gate-history.mjs'),
|
||||
path.join(alteredScripts, 'gate-history.mjs'),
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
@@ -774,7 +777,6 @@ test('deployment drift meta-control fails if the shared comparator is made inert
|
||||
root,
|
||||
'--manifest',
|
||||
'gates/gates.manifest.json',
|
||||
'--skip-history',
|
||||
],
|
||||
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
|
||||
);
|
||||
|
||||
@@ -13,12 +13,8 @@ const expectedTriggers = `when:
|
||||
- event: push
|
||||
branch: main`;
|
||||
const expectedGateStep = ` image: *node_image
|
||||
# Woodpecker's shallow marker makes merge-base reject even present parents;
|
||||
# full history is required for activation ancestry and manifest provenance.
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- apk add --no-cache bubblewrap
|
||||
- if [ -f .git/shallow ]; then git fetch --unshallow --no-tags origin; fi
|
||||
- pnpm gate:verify
|
||||
depends_on:
|
||||
- install
|
||||
@@ -52,7 +48,11 @@ export function assertUnprivilegedGateStep(pipeline) {
|
||||
// Closed textual construction by design: accepting arbitrary YAML syntax here
|
||||
// would require a duplicate-key-preserving parser. Exact equality rejects all
|
||||
// extra keys, quoted/escaped key spellings, aliases, and mapping merges.
|
||||
assert.equal(matches[0][1].trimEnd(), expectedGateStep, 'gate-verify step must match closed unprivileged construction');
|
||||
assert.equal(
|
||||
matches[0][1].trimEnd(),
|
||||
expectedGateStep,
|
||||
'gate-verify step must match closed unprivileged construction',
|
||||
);
|
||||
}
|
||||
|
||||
test('package.json exposes the canonical gate:verify command', async () => {
|
||||
@@ -77,7 +77,10 @@ test('gate wiring rejects privilege syntax, merges, duplicate keys, and trigger
|
||||
' "<<": *privileged-step\n',
|
||||
];
|
||||
for (const addition of additions) {
|
||||
const changed = pipeline.replace(' gate-verify:\n image:', ` gate-verify:\n${addition} image:`);
|
||||
const changed = pipeline.replace(
|
||||
' gate-verify:\n image:',
|
||||
` gate-verify:\n${addition} image:`,
|
||||
);
|
||||
assert.throws(() => assertUnprivilegedGateStep(changed));
|
||||
}
|
||||
const privilegedInstall = pipeline.replace(
|
||||
@@ -85,10 +88,7 @@ test('gate wiring rejects privilege syntax, merges, duplicate keys, and trigger
|
||||
' install:\n privileged: true\n image:',
|
||||
);
|
||||
const duplicate = `${pipeline}\n gate-verify:\n image: *node_image\n`;
|
||||
const noPullRequest = pipeline.replace(
|
||||
' - event: [pull_request, manual]',
|
||||
' - event: manual',
|
||||
);
|
||||
const noPullRequest = pipeline.replace(' - event: [pull_request, manual]', ' - event: manual');
|
||||
const filteredPullRequest = pipeline.replace(
|
||||
' - event: [pull_request, manual]',
|
||||
' - event: [pull_request, manual]\n path: [scripts/**]',
|
||||
|
||||
@@ -19,7 +19,6 @@ if (!root) throw new Error('fixture runner requires --root');
|
||||
const { failures, observations } = await verifyRegistry({
|
||||
root,
|
||||
manifest,
|
||||
skipHistory: true,
|
||||
structureOnly,
|
||||
fixtureProfile: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user