mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
Adds tsconfig.json (strict) over product code, fixes its remaining 90 diagnostics (type-only, interface corrections, and explicit narrowing), and adds a typecheck job to the required free-tests aggregate running bun run typecheck, the test-code ratchet (identity -> count baseline, fails on new, repeated, or unlocked fixed diagnostics), and the lib/cso format check. Reuses fixes from #2447 where they still applied.
2171 lines
87 KiB
TypeScript
2171 lines
87 KiB
TypeScript
import * as fs from 'node:fs';
|
|
import { randomBytes } from 'node:crypto';
|
|
import { spawn } from 'node:child_process';
|
|
import { basename, dirname, join } from 'node:path';
|
|
import {
|
|
AssertionWitnessBinding,
|
|
AssertionWitnessReceipt,
|
|
CsoError,
|
|
PreparationProof,
|
|
RepairBundle,
|
|
RepairReviewArtifact,
|
|
SnapshotManifest,
|
|
VerificationManifest,
|
|
VerificationObservation,
|
|
VerificationRequest,
|
|
canonical,
|
|
relativePath,
|
|
sha256,
|
|
snapshotPathHandleId,
|
|
snapshotPathId,
|
|
validateVerificationObservation,
|
|
validateVerificationRequest,
|
|
} from './contracts';
|
|
import { QualifiedRuntime } from './runtime-catalog';
|
|
import { inspectPreparation, type CsoStack } from './preparation';
|
|
import { containedFile } from './snapshot';
|
|
import { DockerEndpoint, DockerGroup } from './docker';
|
|
import type { PreparedDatabaseContract } from './preparation-executor';
|
|
import { redact, sanitizeHelperForJson } from './process';
|
|
import { secureDirectory, writeJsonExclusive } from './state';
|
|
import {
|
|
AssertionWitnessHandle,
|
|
AssertionWitnessSession,
|
|
WitnessedVerificationResult,
|
|
assertionWitnessPairHash,
|
|
testExecutionPassed,
|
|
validateStoredAssertionWitnessReceipt,
|
|
witnessObservationHash,
|
|
} from './witness';
|
|
export { testExecutionPassed } from './witness';
|
|
|
|
export interface VerificationExecutor {
|
|
observe(
|
|
source: string,
|
|
phase: 'before' | 'after',
|
|
request: VerificationRequest,
|
|
runtime: QualifiedRuntime,
|
|
verifier: QualifiedRuntime,
|
|
work: string,
|
|
control: string,
|
|
execution?: { environment: Record<string, string>; database?: PreparedDatabaseContract },
|
|
testEvidence?: { minimumPassingTests: number[] },
|
|
witness?: AssertionWitnessHandle,
|
|
): Promise<VerificationObservation | WitnessedVerificationResult>;
|
|
}
|
|
export interface FailedVerificationAttempt {
|
|
schemaVersion: 3;
|
|
artifactKind: 'repair_candidate';
|
|
id: string;
|
|
runId: string;
|
|
findingId: string;
|
|
createdAt: string;
|
|
bundleIssued: false;
|
|
runtime: { image: string; platform: string; profile: string };
|
|
policyHash: string;
|
|
requestHash: string;
|
|
harnessHash: string;
|
|
sourceHash: string;
|
|
request: VerificationRequest;
|
|
patchHash: string;
|
|
testToolchain: 'runtime' | 'project';
|
|
testCompletionAssurance: 'self_reported';
|
|
preparationHash?: string;
|
|
before: VerificationObservation;
|
|
after?: VerificationObservation;
|
|
reproduction: 'blocked' | 'inconclusive' | 'disproved' | 'reproduced';
|
|
repair: 'failed' | 'proposed';
|
|
failure: { code: string; message: string };
|
|
}
|
|
export class VerificationAttemptError extends CsoError {
|
|
constructor(
|
|
public causeError: CsoError,
|
|
public attempt: FailedVerificationAttempt,
|
|
) {
|
|
super(causeError.code, `${causeError.message}; before-phase evidence retained as attempt ${attempt.id}`);
|
|
this.name = 'VerificationAttemptError';
|
|
}
|
|
}
|
|
async function attemptGuard(
|
|
runDir: string,
|
|
work: string,
|
|
watchdog: string,
|
|
deadline: number,
|
|
): Promise<() => Promise<void>> {
|
|
const control = secureDirectory(join(runDir, 'supervision', basename(work)));
|
|
secureDirectory(work);
|
|
const ready = join(control, 'attempt.ready'),
|
|
terminal = join(control, 'attempt.terminal'),
|
|
stopped = join(control, 'attempt.stopped');
|
|
const child = spawn(
|
|
watchdog,
|
|
[
|
|
'--attempt-owner',
|
|
String(process.pid),
|
|
'--deadline',
|
|
String(Math.ceil(deadline / 1000)),
|
|
'--control-dir',
|
|
control,
|
|
'--work-root',
|
|
work,
|
|
'--run-root',
|
|
runDir,
|
|
],
|
|
{ cwd: control, env: { PATH: '/usr/bin:/bin' }, detached: true, stdio: 'ignore' },
|
|
);
|
|
let failed = false;
|
|
child.once('error', () => {
|
|
failed = true;
|
|
});
|
|
child.unref();
|
|
for (let i = 0; i < 100 && !failed && !fs.existsSync(ready); i++)
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
let alive = false;
|
|
try {
|
|
if (child.pid) {
|
|
process.kill(child.pid, 0);
|
|
alive = true;
|
|
}
|
|
} catch {}
|
|
if (failed || !alive || !fs.existsSync(ready)) {
|
|
try {
|
|
if (child.pid) process.kill(child.pid, 'SIGKILL');
|
|
} catch {}
|
|
fs.rmSync(work, { recursive: true, force: true });
|
|
throw new CsoError('ISOLATION_FAILED', 'Attempt execution-copy watchdog failed its startup handshake');
|
|
}
|
|
return async () => {
|
|
fs.rmSync(work, { recursive: true, force: true });
|
|
fs.writeFileSync(terminal, 'normal cleanup complete\n', { mode: 0o600, flag: 'wx' });
|
|
for (let i = 0; i < 100 && !fs.existsSync(stopped); i++)
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
if (!fs.existsSync(stopped))
|
|
throw new CsoError('ISOLATION_FAILED', 'Attempt watchdog did not acknowledge execution-copy cleanup');
|
|
};
|
|
}
|
|
function allFiles(root: string, at = root, ignore: (path: string) => boolean = () => false): string[] {
|
|
const out: string[] = [];
|
|
for (const entry of fs.readdirSync(at, { withFileTypes: true })) {
|
|
const p = join(at, entry.name),
|
|
relative = p.slice(root.length + 1).replaceAll('\\', '/');
|
|
if (ignore(relative)) continue;
|
|
if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile()))
|
|
throw new CsoError('UNSAFE_PATH', 'Execution copy contains a special file');
|
|
if (entry.isDirectory()) out.push(...allFiles(root, p, ignore));
|
|
else out.push(relative);
|
|
}
|
|
return out.sort();
|
|
}
|
|
export function treeHash(root: string, predicate: (path: string) => boolean = () => true): string {
|
|
return sha256(
|
|
canonical(
|
|
allFiles(root)
|
|
.filter(predicate)
|
|
.map((path) => {
|
|
const file = containedFile(root, path),
|
|
before = fs.lstatSync(file);
|
|
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1)
|
|
throw new CsoError('UNSAFE_PATH', 'Execution copy contains a special or hard-linked file');
|
|
const body = fs.readFileSync(file),
|
|
after = fs.lstatSync(file);
|
|
if (
|
|
before.ino !== after.ino ||
|
|
before.dev !== after.dev ||
|
|
before.size !== after.size ||
|
|
before.mtimeMs !== after.mtimeMs ||
|
|
before.ctimeMs !== after.ctimeMs
|
|
)
|
|
throw new CsoError('SNAPSHOT_RACE', `Execution copy changed while hashing: ${path}`);
|
|
return [path, sha256(body), before.mode & 0o777];
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
const DEPENDENCY =
|
|
/(?:^|\/)(?:package(?:-lock)?\.json|npm-shrinkwrap\.json|bun\.lock|uv\.lock|requirements[^/]*\.txt|pyproject\.toml|setup\.(?:py|cfg)|Gemfile(?:\.lock)?|[^/]+\.gemspec)$/;
|
|
const CONFIG = /(?:^|\/)(?:config\/.+|\.env|Dockerfile|Procfile|.*\.(?:toml|ya?ml|json))$/;
|
|
export function fileEffect(path: string): 'source' | 'configuration' | 'dependency' {
|
|
return DEPENDENCY.test(path) ? 'dependency' : CONFIG.test(path) ? 'configuration' : 'source';
|
|
}
|
|
export function patchHash(request: Pick<VerificationRequest, 'changes'>): string {
|
|
return sha256(canonical(request.changes));
|
|
}
|
|
export function resolveVerificationRequestPaths(
|
|
manifest: SnapshotManifest,
|
|
request: VerificationRequest,
|
|
): VerificationRequest {
|
|
const resolve = (reference: string): string => {
|
|
const id = snapshotPathHandleId(reference);
|
|
if (!id) return relativePath(reference);
|
|
const entry = manifest.entries.find((item) => item.pathId === id);
|
|
if (entry) return entry.path;
|
|
const deleted = manifest.deletedPaths?.find((item) => item.pathId === id);
|
|
if (deleted) return deleted.path;
|
|
const changed = manifest.changedPaths?.find((path) => snapshotPathId(manifest.root, path) === id);
|
|
if (changed) return changed;
|
|
throw new CsoError(
|
|
'INVALID_SCHEMA',
|
|
`Verification path handle is outside the retained snapshot: ${reference}`,
|
|
);
|
|
};
|
|
const argument = (value: string): string => {
|
|
const direct = snapshotPathHandleId(value);
|
|
if (direct) return resolve(value);
|
|
if (value.startsWith('./') && snapshotPathHandleId(value.slice(2))) return `./${resolve(value.slice(2))}`;
|
|
return value;
|
|
};
|
|
const command = (value: VerificationRequest['start']): VerificationRequest['start'] => ({
|
|
...value,
|
|
args: value.args.map(argument),
|
|
});
|
|
return {
|
|
...request,
|
|
start: command(request.start),
|
|
existingTests: request.existingTests.map(command),
|
|
boundaryFiles: request.boundaryFiles.map(resolve),
|
|
testFiles: request.testFiles.map(resolve),
|
|
changes: request.changes.map((change) => ({ ...change, path: resolve(change.path) })),
|
|
};
|
|
}
|
|
export function reviewRequestHash(request: VerificationRequest): string {
|
|
const { artifactId, ...review } = request.review;
|
|
return sha256(canonical({ ...request, review }));
|
|
}
|
|
export function reviewArtifactIdentity(artifact: RepairReviewArtifact): string {
|
|
const { id, ...bound } = artifact;
|
|
return sha256(canonical(bound)).slice(0, 32);
|
|
}
|
|
export function makeReviewArtifact(
|
|
runId: string,
|
|
request: VerificationRequest,
|
|
producer: string,
|
|
): RepairReviewArtifact {
|
|
if (!producer || producer.length > 200 || producer === request.review.reviewer)
|
|
throw new CsoError(
|
|
'INVALID_SCHEMA',
|
|
'Repair producer and independent reviewer identities must be distinct',
|
|
);
|
|
if (!request.review.independent)
|
|
throw new CsoError('INVALID_SCHEMA', 'Repair review must be explicitly independent');
|
|
const artifact: RepairReviewArtifact = {
|
|
schemaVersion: 3,
|
|
id: '',
|
|
runId,
|
|
findingId: request.findingId,
|
|
createdAt: new Date().toISOString(),
|
|
producer,
|
|
reviewer: request.review.reviewer,
|
|
assurance: 'self_attested',
|
|
requestHash: reviewRequestHash(request),
|
|
patchHash: patchHash(request),
|
|
rootCauseRepaired: request.review.rootCauseRepaired,
|
|
featurePreserved: request.review.featurePreserved,
|
|
boundaryMocks: request.review.boundaryMocks,
|
|
rationale: request.review.rationale,
|
|
};
|
|
artifact.id = reviewArtifactIdentity(artifact);
|
|
return artifact;
|
|
}
|
|
export function validateReviewArtifact(
|
|
value: unknown,
|
|
runId: string,
|
|
request: VerificationRequest,
|
|
): RepairReviewArtifact {
|
|
const artifact = value as RepairReviewArtifact;
|
|
if (
|
|
!artifact ||
|
|
artifact.schemaVersion !== 3 ||
|
|
artifact.assurance !== 'self_attested' ||
|
|
artifact.runId !== runId ||
|
|
artifact.findingId !== request.findingId ||
|
|
artifact.id !== request.review.artifactId ||
|
|
reviewArtifactIdentity(artifact) !== artifact.id ||
|
|
artifact.requestHash !== reviewRequestHash(request) ||
|
|
artifact.patchHash !== patchHash(request) ||
|
|
artifact.reviewer !== request.review.reviewer ||
|
|
artifact.producer === artifact.reviewer ||
|
|
artifact.rootCauseRepaired !== request.review.rootCauseRepaired ||
|
|
artifact.featurePreserved !== request.review.featurePreserved ||
|
|
artifact.boundaryMocks !== request.review.boundaryMocks ||
|
|
artifact.rationale !== request.review.rationale
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Self-attested repair-review artifact does not bind this request',
|
|
);
|
|
return artifact;
|
|
}
|
|
export function verificationIdentity(manifest: VerificationManifest): string {
|
|
const { id, ...bound } = manifest;
|
|
return sha256(canonical(bound)).slice(0, 32);
|
|
}
|
|
export function verificationHarnessHash(request: VerificationRequest, sourceRoot: string): string {
|
|
const testInputs = request.testFiles.map((path) => {
|
|
const file = containedFile(sourceRoot, path);
|
|
if (!fs.existsSync(file))
|
|
throw new CsoError('INCOMPATIBLE_INPUT', `Immutable existing-test input is missing: ${path}`);
|
|
const stat = fs.lstatSync(file);
|
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
|
|
throw new CsoError('UNSAFE_PATH', `Immutable existing-test input is unsafe: ${path}`);
|
|
return [path, sha256(fs.readFileSync(file)), stat.mode & 0o777];
|
|
});
|
|
return sha256(
|
|
canonical({
|
|
port: request.port,
|
|
start: request.start,
|
|
legitimate: request.legitimate,
|
|
security: request.security,
|
|
existingTests: request.existingTests,
|
|
testInputs,
|
|
}),
|
|
);
|
|
}
|
|
export interface CanonicalTestPlan {
|
|
commands: VerificationRequest['existingTests'];
|
|
files: string[];
|
|
kind: string;
|
|
toolchain: 'runtime' | 'project';
|
|
minimumPassingTests: number[];
|
|
signature: string;
|
|
}
|
|
export interface CanonicalStartPlan {
|
|
command: VerificationRequest['start'];
|
|
kind: string;
|
|
signature: string;
|
|
entrypointFiles: string[];
|
|
}
|
|
const TEST_TREE = /(?:^|\/)(?:test|tests|__tests__|spec|fixtures|__fixtures__|testdata)(?:\/|$)/;
|
|
const NODE_TEST_CONFIG =
|
|
/(?:^|\/)(?:(?:jest|vitest|vite|playwright|cypress|karma|babel|ava|webpack)\.(?:config|conf)\.[^/]+|(?:jest|vitest|playwright|cypress|babel|ava|webpack)\.config\.[^/]+|\.mocharc(?:\.[^/]+)?|\.babelrc(?:\.[^/]+)?|tsconfig(?:\.[^/]+)?\.json|bunfig\.toml)$/;
|
|
const BUN_RUNTIME_POLICY_ARGS = ['--no-install', '--config=/opt/cso/no-auto-install.toml'];
|
|
// -S prevents dependency-provided .pth startup code from running before the
|
|
// trusted bootstrap. Add the venv's fixed Linux purelib directory directly,
|
|
// without processing .pth files, and import each runner before application cwd.
|
|
const PYTHON_PURELIB =
|
|
"os.path.join(os.path.dirname(os.path.dirname(sys.executable)),'lib',f'python{sys.version_info.major}.{sys.version_info.minor}','site-packages')";
|
|
const PYTEST_BOOTSTRAP = `import os,sys;sys.path.append(${PYTHON_PURELIB});import pytest;sys.path.insert(0,os.getcwd());raise SystemExit(pytest.main(sys.argv[1:]))`;
|
|
const UNITTEST_BOOTSTRAP = `import os,sys,unittest;sys.path.append(${PYTHON_PURELIB});sys.path.insert(0,os.getcwd());unittest.main(module=None,argv=['unittest',*sys.argv[1:]])`;
|
|
const DJANGO_BOOTSTRAP = `import os,sys,runpy;sys.path.append(${PYTHON_PURELIB});import django;sys.path.insert(0,os.getcwd());sys.argv=['manage.py',*sys.argv[1:]];runpy.run_path('manage.py',run_name='__main__')`;
|
|
const FLASK_BOOTSTRAP = `import os,sys;sys.path.append(${PYTHON_PURELIB});from flask.cli import main as _cso_main;sys.path.insert(0,os.getcwd());sys.argv=['flask',*sys.argv[1:]];_cso_main()`;
|
|
const UVICORN_BOOTSTRAP = `import os,sys;sys.path.append(${PYTHON_PURELIB});from uvicorn.main import main as _cso_main;sys.path.insert(0,os.getcwd());sys.argv=['uvicorn',*sys.argv[1:]];_cso_main()`;
|
|
function fileText(root: string, path: string): string {
|
|
try {
|
|
return fs.readFileSync(containedFile(root, path), 'utf8');
|
|
} catch {
|
|
throw new CsoError('MISSING_INPUT', `Canonical test input is missing or unreadable: ${path}`);
|
|
}
|
|
}
|
|
function packageTestProjection(root: string, paths: string[]): unknown[] {
|
|
return paths
|
|
.filter((path) => /(?:^|\/)package\.json$/.test(path) && !TEST_TREE.test(path))
|
|
.map((path) => {
|
|
let value: Record<string, any>;
|
|
try {
|
|
value = JSON.parse(fileText(root, path));
|
|
} catch {
|
|
throw new CsoError('MISSING_INPUT', `Canonical package test configuration is invalid: ${path}`);
|
|
}
|
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
throw new CsoError('MISSING_INPUT', `Canonical package test configuration is invalid: ${path}`);
|
|
return {
|
|
path,
|
|
type: value.type ?? null,
|
|
workspaces: value.workspaces ?? null,
|
|
scripts: value.scripts ?? null,
|
|
jest: value.jest ?? null,
|
|
vitest: value.vitest ?? null,
|
|
mocha: value.mocha ?? null,
|
|
ava: value.ava ?? null,
|
|
nyc: value.nyc ?? null,
|
|
};
|
|
});
|
|
}
|
|
function withoutJsCommentsAndStrings(value: string): string {
|
|
return value.replace(
|
|
/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g,
|
|
(match) => ' '.repeat(match.length),
|
|
);
|
|
}
|
|
function assertJavascriptTestRegistrations(root: string, tests: string[]): number {
|
|
const code = tests.map((path) => withoutJsCommentsAndStrings(fileText(root, path))).join('\n');
|
|
if (/\b(?:fdescribe|fit)\s*\(|\b(?:describe|test|it)\s*\.\s*(?:only|concurrent\s*\.\s*only)\b/.test(code))
|
|
throw new CsoError('MISSING_INPUT', 'Canonical JavaScript tests cannot certify a focused-only suite');
|
|
const registrations = [
|
|
...code.matchAll(/\b(?:test|it)\s*(?:\.\s*(?:concurrent|each)\s*(?:\([^)]*\))?)?\s*\(/g),
|
|
].length;
|
|
if (!registrations)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical JavaScript tests need static evidence of at least one non-skipped test or it registration',
|
|
);
|
|
return registrations;
|
|
}
|
|
function boundedTestPaths(tests: string[]): string[] {
|
|
if (!tests.length) throw new CsoError('MISSING_INPUT', 'No canonical project test sources were found');
|
|
if (tests.length > 1000)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical project test suite exceeds the 1,000-file verification limit',
|
|
);
|
|
const paths = tests.map((path) => `./${path}`);
|
|
if (paths.reduce((bytes, path) => bytes + Buffer.byteLength(path) + 1, 0) > 128 * 1024)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical project test paths exceed the bounded direct-runner argument limit',
|
|
);
|
|
return paths;
|
|
}
|
|
function directPackageTest(
|
|
root: string,
|
|
stack: 'node' | 'bun',
|
|
manifest: Record<string, any>,
|
|
tests: string[],
|
|
configuration: string[],
|
|
): {
|
|
command: VerificationRequest['existingTests'][number];
|
|
kind: string;
|
|
runner: string;
|
|
toolchain: 'runtime' | 'project';
|
|
minimumPassingTests: number;
|
|
} {
|
|
const scripts = manifest.scripts;
|
|
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts))
|
|
throw new CsoError('MISSING_INPUT', `Canonical ${stack} package test scripts are missing or invalid`);
|
|
const script = scripts.test;
|
|
if (
|
|
typeof script !== 'string' ||
|
|
!script.trim() ||
|
|
script.length > 4096 ||
|
|
/no test specified|^\s*(?:true|:|exit\s+0)\s*$/i.test(script)
|
|
)
|
|
throw new CsoError('MISSING_INPUT', `Canonical ${stack} test script is missing or a placeholder`);
|
|
for (const hook of ['pretest', 'posttest']) {
|
|
const value = scripts[hook];
|
|
if (value !== undefined && typeof value !== 'string')
|
|
throw new CsoError('MISSING_INPUT', `Canonical ${stack} ${hook} lifecycle hook is invalid`);
|
|
if (typeof value === 'string' && value.trim())
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
`Canonical ${stack} tests cannot certify through package lifecycle hooks; remove ${hook} or run the direct standard runner`,
|
|
);
|
|
}
|
|
if (/[;&|><`$()\\\r\n]/.test(script))
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
`Canonical ${stack} tests require one recognized direct standard runner; local wrappers and shell composition are not admitted`,
|
|
);
|
|
const words = script.trim().split(/\s+/),
|
|
standard = ['jest', 'vitest', 'mocha', 'ava'],
|
|
paths = boundedTestPaths(tests),
|
|
minimumPassingTests = assertJavascriptTestRegistrations(root, tests);
|
|
if (canonical(words) === canonical(['node', '--test'])) {
|
|
return {
|
|
command: { executable: '/usr/local/bin/node', args: ['--test', '--test-reporter=tap', ...paths] },
|
|
kind: 'direct node --test with TAP count evidence',
|
|
runner: 'node',
|
|
toolchain: 'runtime',
|
|
minimumPassingTests,
|
|
};
|
|
}
|
|
if (canonical(words) === canonical(['bun', 'test'])) {
|
|
if (stack !== 'bun')
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical Node verification cannot depend on the Bun test runtime',
|
|
);
|
|
return {
|
|
command: { executable: '/usr/local/bin/bun', args: [...BUN_RUNTIME_POLICY_ARGS, 'test', ...paths] },
|
|
kind: 'direct bun test with automatic installation disabled',
|
|
runner: 'bun',
|
|
toolchain: 'runtime',
|
|
minimumPassingTests,
|
|
};
|
|
}
|
|
const runner =
|
|
words.length === 1 && standard.includes(words[0])
|
|
? words[0]
|
|
: words.length === 3 && words[0] === 'npx' && words[1] === '--no-install' && standard.includes(words[2])
|
|
? words[2]
|
|
: undefined;
|
|
if (!runner)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
`Canonical ${stack} tests require one recognized direct standard runner; local wrappers and shell composition are not admitted`,
|
|
);
|
|
const control = canonical({
|
|
embedded: manifest[runner] ?? null,
|
|
files: configuration
|
|
.filter((path) => NODE_TEST_CONFIG.test(path))
|
|
.map((path) => [path, fileText(root, path)]),
|
|
});
|
|
if (
|
|
/(?:collectOnly|dryRun|passWithNoTests|testNamePattern|\b(?:grep|fgrep|match)\b|--(?:collect-only|dry-run|grep|fgrep|match|passWithNoTests))/i.test(
|
|
control,
|
|
)
|
|
)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
`Canonical ${runner} configuration cannot focus, skip execution, or allow an empty suite`,
|
|
);
|
|
const args =
|
|
runner === 'jest'
|
|
? ['--runTestsByPath', '--passWithNoTests=false', '--json', ...paths]
|
|
: runner === 'vitest'
|
|
? ['run', '--passWithNoTests=false', '--reporter=verbose', ...paths]
|
|
: runner === 'mocha'
|
|
? ['--fail-zero', '--no-dry-run', '--forbid-only', '--reporter', 'json', ...paths]
|
|
: ['--tap', ...paths];
|
|
return {
|
|
command: { executable: `/work/node_modules/.bin/${runner}`, args },
|
|
kind: `direct local ${runner}`,
|
|
runner,
|
|
toolchain: 'project',
|
|
minimumPassingTests,
|
|
};
|
|
}
|
|
function directPackageStartEntrypoint(root: string, stack: 'node' | 'bun', script: string): string {
|
|
if (/[;&|><`$()\\\r\n]/.test(script))
|
|
throw new CsoError('MISSING_INPUT', `Canonical ${stack} startup cannot use shell composition`);
|
|
const words = script.trim().split(/\s+/),
|
|
runner = words.shift();
|
|
if (
|
|
runner !== stack ||
|
|
words.length !== 1 ||
|
|
!/^[A-Za-z0-9_./-]+\.(?:[cm]?[jt]s|jsx|tsx)$/.test(words[0]) ||
|
|
words[0].startsWith('/') ||
|
|
words[0].split('/').includes('..')
|
|
)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
`Canonical ${stack} package startup requires one direct contained ${stack} entrypoint`,
|
|
);
|
|
const path = relativePath(words[0]);
|
|
if (!executableSource(root, path))
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
`Canonical ${stack} package startup entrypoint is missing or unsafe: ${path}`,
|
|
);
|
|
return path;
|
|
}
|
|
function pyprojectTestProjection(root: string, path: string): unknown {
|
|
try {
|
|
const value = Bun.TOML.parse(fileText(root, path)) as Record<string, any>;
|
|
return {
|
|
tool: { pytest: value?.tool?.pytest ?? null, coverage: value?.tool?.coverage ?? null },
|
|
projectScripts: value?.project?.scripts ?? null,
|
|
};
|
|
} catch {
|
|
throw new CsoError('MISSING_INPUT', 'Canonical Python test configuration is invalid: pyproject.toml');
|
|
}
|
|
}
|
|
export function canonicalTestPlan(sourceRoot: string, stack: CsoStack): CanonicalTestPlan {
|
|
const generated = (path: string) => {
|
|
const first = path.split('/')[0];
|
|
return stack === 'node'
|
|
? first === 'node_modules' || first === '.cso-npm-cache'
|
|
: stack === 'bun'
|
|
? first === 'node_modules' || first === '.cso-bun-cache'
|
|
: stack === 'python'
|
|
? first === '.venv' || first === '.cso-uv-cache' || path === '.gstack-cso-public-requirements.txt'
|
|
: path.startsWith('vendor/bundle/') || first === '.cso-bundle' || first === '.cso-gems';
|
|
};
|
|
const all = allFiles(sourceRoot, sourceRoot, generated);
|
|
let tests: string[] = [],
|
|
configuration: string[] = [],
|
|
commands: VerificationRequest['existingTests'] = [],
|
|
kind = '',
|
|
toolchain: 'runtime' | 'project' = 'runtime',
|
|
minimumPassingTests: number[] = [],
|
|
runnerEvidence: unknown = {};
|
|
if (stack === 'node' || stack === 'bun') {
|
|
let manifest: Record<string, any>;
|
|
try {
|
|
manifest = JSON.parse(fileText(sourceRoot, 'package.json'));
|
|
} catch {
|
|
throw new CsoError('MISSING_INPUT', `Canonical ${stack} package.json is missing or invalid`);
|
|
}
|
|
tests = all.filter((path) =>
|
|
/(?:^|\/)(?:test|tests|__tests__)\/.*\.(?:[cm]?js|tsx?|jsx)$|\.(?:test|spec)\.(?:[cm]?js|tsx?|jsx)$/.test(
|
|
path,
|
|
),
|
|
);
|
|
configuration = all.filter((path) => TEST_TREE.test(path) || NODE_TEST_CONFIG.test(path));
|
|
const direct = directPackageTest(sourceRoot, stack, manifest, tests, configuration);
|
|
commands = [direct.command];
|
|
kind = direct.kind;
|
|
toolchain = direct.toolchain;
|
|
minimumPassingTests = [direct.minimumPassingTests];
|
|
runnerEvidence = {
|
|
runner: direct.runner,
|
|
declaredScript: manifest.scripts.test,
|
|
packages: packageTestProjection(sourceRoot, all),
|
|
minimumPassingTests: direct.minimumPassingTests,
|
|
};
|
|
} else if (stack === 'python') {
|
|
tests = all.filter((path) =>
|
|
/(?:^|\/)(?:test|tests)\/.*\.py$|(?:^|\/)test_[^/]+\.py$|_test\.py$/.test(path),
|
|
);
|
|
configuration = all.filter(
|
|
(path) =>
|
|
TEST_TREE.test(path) ||
|
|
/(?:^|\/)(?:conftest\.py|\.?pytest\.ini|\.?pytest\.toml|setup\.cfg|tox\.ini|noxfile\.py)$/.test(path),
|
|
);
|
|
const bodies = tests.map((path) => fileText(sourceRoot, path)),
|
|
configBodies = configuration.map((path) => fileText(sourceRoot, path)),
|
|
pyproject = all.includes('pyproject.toml')
|
|
? pyprojectTestProjection(sourceRoot, 'pyproject.toml')
|
|
: null;
|
|
const pytestEvidence =
|
|
all.some((path) => /(?:^|\/)(?:conftest\.py|\.?pytest\.ini|\.?pytest\.toml)$/.test(path)) ||
|
|
[...bodies, ...configBodies].some(
|
|
(body) =>
|
|
/(?:^|\n)\s*(?:import pytest|from pytest\b|@pytest\.)/m.test(body) ||
|
|
/(?:^|\n)(?:async\s+)?def test_[A-Za-z0-9_]*\s*\(/m.test(body),
|
|
);
|
|
const unittestEvidence = bodies.some((body) =>
|
|
/(?:^|\n)\s*(?:import unittest|from unittest\b)|unittest\.TestCase|TestCase\s*\)/m.test(body),
|
|
);
|
|
if (!pytestEvidence && !unittestEvidence)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Python test runner is ambiguous; declare pytest evidence or a unittest suite',
|
|
);
|
|
const usePytest = pytestEvidence,
|
|
paths = boundedTestPaths(tests),
|
|
pytestControl = [...configBodies, canonical(pyproject)].join('\n');
|
|
if (
|
|
usePytest &&
|
|
/(?:--collect-only|\s--co\b|--setup-(?:only|plan)|--fixtures(?:-per-test)?|--no-summary|\baddopts[^\n]*(?:\s-k\b|\s-m\b|--ignore\b|--deselect\b|(?:^|\s)-q{2,}\b))/i.test(
|
|
pytestControl,
|
|
)
|
|
)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical pytest configuration cannot collect only, focus, deselect, suppress its count, or skip test execution',
|
|
);
|
|
commands = [
|
|
{
|
|
executable: '/work/.venv/bin/python',
|
|
args: usePytest
|
|
? ['-I', '-S', '-c', PYTEST_BOOTSTRAP, '-q', '--color=no', '--', ...paths]
|
|
: ['-I', '-S', '-c', UNITTEST_BOOTSTRAP, ...paths],
|
|
},
|
|
];
|
|
kind = usePytest
|
|
? 'isolated prepared pytest with explicit files, positive summary, and no .pth startup'
|
|
: 'isolated standard-library unittest with explicit files and no .pth startup';
|
|
toolchain = usePytest ? 'project' : 'runtime';
|
|
minimumPassingTests = [1];
|
|
runnerEvidence = {
|
|
runner: usePytest ? 'pytest' : 'unittest',
|
|
bootstrap: usePytest ? PYTEST_BOOTSTRAP : UNITTEST_BOOTSTRAP,
|
|
siteInitialization: false,
|
|
reporter: usePytest ? 'quiet-positive-summary-no-color' : 'unittest-summary',
|
|
pyproject,
|
|
};
|
|
} else {
|
|
const specs = all.filter((path) => /(?:^|\/)spec\/.*_spec\.rb$/.test(path)),
|
|
rails = all.filter((path) => /(?:^|\/)test\/.*_test\.rb$/.test(path));
|
|
tests = [...specs, ...rails];
|
|
configuration = all.filter((path) => TEST_TREE.test(path) || /(?:^|\/)\.rspec(?:-local)?$/.test(path));
|
|
const rspecControl = configuration
|
|
.filter((path) => /(?:^|\/)\.rspec(?:-local)?$/.test(path))
|
|
.map((path) => fileText(sourceRoot, path))
|
|
.join('\n');
|
|
if (/--(?:dry-run|tag|example|pattern|exclude-pattern|only-failures|next-failure)\b/.test(rspecControl))
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical RSpec configuration cannot dry-run, focus, filter, or select only prior failures',
|
|
);
|
|
commands = [
|
|
...(specs.length
|
|
? [
|
|
{
|
|
executable: '/usr/local/bin/bundle',
|
|
args: ['exec', 'rspec', '--format', 'json', '--', ...boundedTestPaths(specs)],
|
|
},
|
|
]
|
|
: []),
|
|
...(rails.length
|
|
? [
|
|
{
|
|
executable: '/usr/local/bin/bundle',
|
|
args: ['exec', 'rails', 'test', '--no-color', ...boundedTestPaths(rails)],
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
kind = commands.map((command) => command.args.join(' ')).join(' + ');
|
|
toolchain = 'project';
|
|
minimumPassingTests = commands.map(() => 1);
|
|
runnerEvidence = {
|
|
rspec: specs.length > 0,
|
|
minitest: rails.length > 0,
|
|
reporters: specs.length
|
|
? ['rspec-json', ...(rails.length ? ['rails-summary-no-color'] : [])]
|
|
: ['rails-summary-no-color'],
|
|
};
|
|
}
|
|
tests = [...new Set(tests)].sort();
|
|
configuration = [...new Set(configuration)].sort();
|
|
if (!tests.length)
|
|
throw new CsoError('MISSING_INPUT', `No canonical ${stack} project test sources were found`);
|
|
const selected = [...new Set([...configuration, ...tests])].sort();
|
|
if (selected.length > 1000)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical project test suite exceeds the 1,000-file verification limit',
|
|
);
|
|
if (
|
|
minimumPassingTests.length !== commands.length ||
|
|
minimumPassingTests.some((value) => !Number.isInteger(value) || value < 1)
|
|
)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical test plan could not derive a positive execution-count floor',
|
|
);
|
|
return {
|
|
commands,
|
|
files: selected,
|
|
kind,
|
|
toolchain,
|
|
minimumPassingTests,
|
|
signature: sha256(
|
|
canonical({ stack, runnerEvidence, commands, files: selected, toolchain, minimumPassingTests }),
|
|
),
|
|
};
|
|
}
|
|
export function assertCanonicalTestPlan(
|
|
request: VerificationRequest,
|
|
sourceRoot: string,
|
|
stack: CsoStack,
|
|
): CanonicalTestPlan {
|
|
const plan = canonicalTestPlan(sourceRoot, stack);
|
|
if (
|
|
canonical(request.existingTests) !== canonical(plan.commands) ||
|
|
canonical([...request.testFiles].sort()) !== canonical(plan.files)
|
|
)
|
|
throw new CsoError(
|
|
'INVALID_SCHEMA',
|
|
`Existing tests must use the helper-derived full ${plan.kind} suite and its immutable inputs`,
|
|
);
|
|
return plan;
|
|
}
|
|
function executableSource(root: string, path: string): boolean {
|
|
try {
|
|
const file = containedFile(root, relativePath(path)),
|
|
stat = fs.lstatSync(file);
|
|
return stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
function rejectPythonFrameworkShadows(root: string, framework: string, names: string[]): void {
|
|
for (const name of names)
|
|
for (const candidate of [`${name}.py`, name]) {
|
|
const file = containedFile(root, candidate);
|
|
if (fs.existsSync(file))
|
|
throw new CsoError(
|
|
'PREREQUISITE',
|
|
`Canonical ${framework} startup rejects root import shadow: ${candidate}`,
|
|
);
|
|
}
|
|
}
|
|
export function canonicalStartPlan(sourceRoot: string, stack: CsoStack, port: number): CanonicalStartPlan {
|
|
if (!Number.isInteger(port) || port < 1024 || port > 65535)
|
|
throw new CsoError(
|
|
'INVALID_SCHEMA',
|
|
'Canonical application start needs a loopback port from 1024 to 65535',
|
|
);
|
|
let command: VerificationRequest['start'],
|
|
kind: string,
|
|
entrypointFiles: string[] = [],
|
|
evidence: unknown;
|
|
if (stack === 'node' || stack === 'bun') {
|
|
let manifest: Record<string, any>;
|
|
try {
|
|
manifest = JSON.parse(fileText(sourceRoot, 'package.json'));
|
|
} catch {
|
|
throw new CsoError('MISSING_INPUT', `Canonical ${stack} package.json is missing or invalid`);
|
|
}
|
|
const start = manifest?.scripts?.start;
|
|
if (typeof start === 'string' && start.trim() && !/no start|^\s*(?:true|:|exit\s+0)\s*$/i.test(start)) {
|
|
for (const hook of ['prestart', 'poststart']) {
|
|
const value = manifest?.scripts?.[hook];
|
|
if (value !== undefined && typeof value !== 'string')
|
|
throw new CsoError('MISSING_INPUT', `Canonical ${stack} ${hook} lifecycle hook is invalid`);
|
|
if (typeof value === 'string' && value.trim())
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
`Canonical ${stack} startup cannot certify through package lifecycle hooks; remove ${hook} or run the direct entrypoint`,
|
|
);
|
|
}
|
|
const entry = directPackageStartEntrypoint(sourceRoot, stack, start);
|
|
command = {
|
|
executable: stack === 'node' ? '/usr/local/bin/node' : '/usr/local/bin/bun',
|
|
args: stack === 'node' ? [entry] : [...BUN_RUNTIME_POLICY_ARGS, entry],
|
|
};
|
|
kind = `direct ${stack} package start`;
|
|
evidence = { script: start, entry };
|
|
entrypointFiles = ['package.json', entry];
|
|
} else {
|
|
const declared =
|
|
typeof manifest?.main === 'string' && manifest.main.length < 4096 ? manifest.main : undefined,
|
|
candidates = [
|
|
...(declared ? [declared] : []),
|
|
...'server.js,app.js,index.js,server.mjs,app.mjs,index.mjs'.split(','),
|
|
].filter((value, index, all) => all.indexOf(value) === index && executableSource(sourceRoot, value));
|
|
if (candidates.length !== 1)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
`Canonical ${stack} startup is ambiguous; declare one non-placeholder start script or one conventional main entrypoint`,
|
|
);
|
|
const entry = relativePath(candidates[0]);
|
|
command = {
|
|
executable: stack === 'node' ? '/usr/local/bin/node' : '/usr/local/bin/bun',
|
|
args: stack === 'node' ? [entry] : [...BUN_RUNTIME_POLICY_ARGS, entry],
|
|
};
|
|
kind = `${stack} ${entry}`;
|
|
evidence = { entry };
|
|
entrypointFiles = [entry];
|
|
}
|
|
} else if (stack === 'rails') {
|
|
entrypointFiles = ['config/application.rb', 'config/environment.rb'].filter((path) =>
|
|
executableSource(sourceRoot, path),
|
|
);
|
|
if (entrypointFiles.length !== 2)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical Rails startup requires config/application.rb and config/environment.rb',
|
|
);
|
|
command = {
|
|
executable: '/usr/local/bin/bundle',
|
|
args: ['exec', 'rails', 'server', '-b', '127.0.0.1', '-p', String(port)],
|
|
};
|
|
kind = 'Rails loopback server';
|
|
evidence = { entrypointFiles };
|
|
} else {
|
|
const preparation = inspectPreparation(sourceRoot, 'python');
|
|
if (preparation.status !== 'ready')
|
|
throw new CsoError(
|
|
'PREREQUISITE',
|
|
preparation.prerequisites.map((item) => item.message).join('; ') ||
|
|
'Python dependency metadata is incomplete',
|
|
);
|
|
const dependencies = new Set(
|
|
preparation.inputs.map((input) => input.name.toLowerCase().replaceAll('_', '-')),
|
|
),
|
|
choices: Array<{
|
|
kind: string;
|
|
command: VerificationRequest['start'];
|
|
files: string[];
|
|
evidence: unknown;
|
|
}> = [];
|
|
if (dependencies.has('django') && executableSource(sourceRoot, 'manage.py')) {
|
|
rejectPythonFrameworkShadows(sourceRoot, 'Django', ['django']);
|
|
choices.push({
|
|
kind: 'isolated Django loopback server without .pth startup',
|
|
command: {
|
|
executable: '/work/.venv/bin/python',
|
|
args: ['-I', '-S', '-c', DJANGO_BOOTSTRAP, 'runserver', `127.0.0.1:${port}`, '--noreload'],
|
|
},
|
|
files: ['manage.py'],
|
|
evidence: {
|
|
framework: 'django',
|
|
bootstrap: DJANGO_BOOTSTRAP,
|
|
siteInitialization: false,
|
|
rootImportShadowsRejected: ['django.py', 'django/'],
|
|
},
|
|
});
|
|
}
|
|
for (const file of ['app.py', 'application.py', 'wsgi.py'])
|
|
if (dependencies.has('flask') && executableSource(sourceRoot, file)) {
|
|
const body = fileText(sourceRoot, file),
|
|
match = body.match(/(?:^|\n)\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*Flask\s*\(/m);
|
|
if (match) {
|
|
rejectPythonFrameworkShadows(sourceRoot, 'Flask', ['flask']);
|
|
choices.push({
|
|
kind: 'isolated Flask loopback server without .pth startup',
|
|
command: {
|
|
executable: '/work/.venv/bin/python',
|
|
args: [
|
|
'-I',
|
|
'-S',
|
|
'-c',
|
|
FLASK_BOOTSTRAP,
|
|
'--app',
|
|
`${file.replace(/\.py$/, '')}:${match[1]}`,
|
|
'run',
|
|
'--host',
|
|
'127.0.0.1',
|
|
'--port',
|
|
String(port),
|
|
],
|
|
},
|
|
files: [file],
|
|
evidence: {
|
|
framework: 'flask',
|
|
module: file,
|
|
symbol: match[1],
|
|
bootstrap: FLASK_BOOTSTRAP,
|
|
siteInitialization: false,
|
|
rootImportShadowsRejected: ['flask.py', 'flask/'],
|
|
},
|
|
});
|
|
}
|
|
}
|
|
for (const file of ['main.py', 'app.py', 'server.py'])
|
|
if (dependencies.has('fastapi') && dependencies.has('uvicorn') && executableSource(sourceRoot, file)) {
|
|
const body = fileText(sourceRoot, file),
|
|
match = body.match(/(?:^|\n)\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*FastAPI\s*\(/m);
|
|
if (match) {
|
|
rejectPythonFrameworkShadows(sourceRoot, 'FastAPI/Uvicorn', ['fastapi', 'uvicorn']);
|
|
choices.push({
|
|
kind: 'isolated FastAPI loopback server without .pth startup',
|
|
command: {
|
|
executable: '/work/.venv/bin/python',
|
|
args: [
|
|
'-I',
|
|
'-S',
|
|
'-c',
|
|
UVICORN_BOOTSTRAP,
|
|
`${file.replace(/\.py$/, '')}:${match[1]}`,
|
|
'--app-dir',
|
|
'/work',
|
|
'--host',
|
|
'127.0.0.1',
|
|
'--port',
|
|
String(port),
|
|
],
|
|
},
|
|
files: [file],
|
|
evidence: {
|
|
framework: 'fastapi',
|
|
module: file,
|
|
symbol: match[1],
|
|
bootstrap: UVICORN_BOOTSTRAP,
|
|
siteInitialization: false,
|
|
rootImportShadowsRejected: ['fastapi.py', 'fastapi/', 'uvicorn.py', 'uvicorn/'],
|
|
},
|
|
});
|
|
}
|
|
}
|
|
if (preparation.inputs.length === 0) {
|
|
const direct = ['app.py', 'application.py', 'server.py', 'main.py'].filter((file) =>
|
|
executableSource(sourceRoot, file),
|
|
);
|
|
if (direct.length === 1) {
|
|
const entry = direct[0];
|
|
choices.push({
|
|
kind: 'isolated standard-library Python application',
|
|
command: { executable: '/usr/local/bin/python', args: ['-I', entry] },
|
|
files: [entry],
|
|
evidence: { framework: 'standard-library', entry, isolatedMode: true, dependencyClosure: 'empty' },
|
|
});
|
|
}
|
|
}
|
|
const unique = choices.filter(
|
|
(choice, index) =>
|
|
choices.findIndex((other) => canonical(other.command) === canonical(choice.command)) === index,
|
|
);
|
|
if (unique.length !== 1)
|
|
throw new CsoError(
|
|
'MISSING_INPUT',
|
|
'Canonical Python startup is unavailable or ambiguous; use one supported Django, Flask, or FastAPI entrypoint with locked runtime dependencies',
|
|
);
|
|
({ command, kind, evidence } = unique[0]);
|
|
entrypointFiles = unique[0].files;
|
|
}
|
|
const entrypointEvidence = entrypointFiles.map((path) => {
|
|
const file = containedFile(sourceRoot, path),
|
|
stat = fs.lstatSync(file);
|
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
|
|
throw new CsoError('UNSAFE_PATH', `Canonical startup input is unsafe: ${path}`);
|
|
return { path, sha256: sha256(fs.readFileSync(file)), mode: stat.mode & 0o777 };
|
|
});
|
|
return {
|
|
command,
|
|
kind,
|
|
entrypointFiles,
|
|
signature: sha256(canonical({ stack, kind, evidence, command, entrypointEvidence })),
|
|
};
|
|
}
|
|
export function assertCanonicalStartPlan(
|
|
request: VerificationRequest,
|
|
sourceRoot: string,
|
|
stack: CsoStack,
|
|
): CanonicalStartPlan {
|
|
const plan = canonicalStartPlan(sourceRoot, stack, request.port);
|
|
if (canonical(request.start) !== canonical(plan.command))
|
|
throw new CsoError(
|
|
'INVALID_SCHEMA',
|
|
`Application start must use the helper-derived ${plan.kind} command`,
|
|
);
|
|
return plan;
|
|
}
|
|
export function preparePatchedSource(snapshot: string, target: string, request: VerificationRequest): void {
|
|
secureDirectory(target);
|
|
fs.cpSync(snapshot, target, {
|
|
recursive: true,
|
|
errorOnExist: false,
|
|
force: true,
|
|
preserveTimestamps: false,
|
|
});
|
|
for (const change of request.changes) {
|
|
const path = relativePath(change.path),
|
|
file = containedFile(target, path),
|
|
exists = fs.existsSync(file);
|
|
if (change.beforeSha256 === null && exists)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', `Expected new patch path already exists: ${path}`);
|
|
if (change.beforeSha256 !== null && (!exists || sha256(fs.readFileSync(file)) !== change.beforeSha256))
|
|
throw new CsoError('INCOMPATIBLE_INPUT', `Patch preimage does not match: ${path}`);
|
|
const derived = fileEffect(path);
|
|
if (change.effect !== derived)
|
|
throw new CsoError('INVALID_SCHEMA', `${path} must be declared as ${derived}, not ${change.effect}`);
|
|
if (change.after === null) {
|
|
fs.unlinkSync(file);
|
|
continue;
|
|
}
|
|
const safe = redact(change.after);
|
|
if (safe !== change.after)
|
|
throw new CsoError(
|
|
'REDACTION_FAILED',
|
|
`Patch content for ${path} contains material that cannot enter a repair bundle`,
|
|
);
|
|
const mode = exists ? fs.statSync(file).mode & 0o777 : 0o600;
|
|
secureDirectory(dirname(file));
|
|
fs.writeFileSync(file, change.after, { mode });
|
|
}
|
|
}
|
|
export function certify(params: {
|
|
runId: string;
|
|
manifest: SnapshotManifest;
|
|
request: VerificationRequest;
|
|
identityRequest?: VerificationRequest;
|
|
runtime: QualifiedRuntime;
|
|
verifier: QualifiedRuntime;
|
|
before: VerificationObservation;
|
|
after: VerificationObservation;
|
|
beforeRoot: string;
|
|
afterRoot: string;
|
|
policyHash: string;
|
|
auditPolicyHash?: string;
|
|
archives: string[];
|
|
dependencyClosures?: { before: unknown; after: unknown };
|
|
preparation?: { before: PreparationProof; after: PreparationProof };
|
|
reviewArtifact?: RepairReviewArtifact;
|
|
startPlanHash?: string;
|
|
testPlanHash?: string;
|
|
testToolchain: 'runtime' | 'project';
|
|
minimumPassingTests?: number[];
|
|
witness?: { before: AssertionWitnessReceipt; after: AssertionWitnessReceipt };
|
|
}): { manifest: VerificationManifest; bundle?: RepairBundle } {
|
|
const { request } = params,
|
|
identityRequest = params.identityRequest ?? request,
|
|
pHash = patchHash(identityRequest),
|
|
harnessHash = verificationHarnessHash(request, params.beforeRoot),
|
|
afterHarnessHash = verificationHarnessHash(request, params.afterRoot),
|
|
fixturesHash = sha256(canonical(identityRequest.fixtures));
|
|
if (afterHarnessHash !== harnessHash)
|
|
throw new CsoError('ASSERTION_FAILED', 'Repair changed immutable existing-test inputs');
|
|
if (!['runtime', 'project'].includes(params.testToolchain))
|
|
throw new CsoError(
|
|
'INVALID_SCHEMA',
|
|
'Verification test toolchain must be helper-derived as runtime or project',
|
|
);
|
|
if (identityRequest.review.reviewedPatchHash !== pHash)
|
|
throw new CsoError('INVALID_SCHEMA', `Independent review binds the wrong patch hash; expected ${pHash}`);
|
|
const sourceAfter = treeHash(params.afterRoot),
|
|
dependenciesBefore = treeHash(params.beforeRoot, (p) => DEPENDENCY.test(p)),
|
|
dependenciesAfter = treeHash(params.afterRoot, (p) => DEPENDENCY.test(p)),
|
|
configurationBefore = treeHash(params.beforeRoot, (p) => CONFIG.test(p) && !DEPENDENCY.test(p)),
|
|
configurationAfter = treeHash(params.afterRoot, (p) => CONFIG.test(p) && !DEPENDENCY.test(p));
|
|
const mechanical =
|
|
params.before.booted &&
|
|
params.before.legitimate &&
|
|
params.before.security === 'intended_failure' &&
|
|
params.before.existingTests &&
|
|
params.after.booted &&
|
|
params.after.legitimate &&
|
|
params.after.security === 'pass' &&
|
|
params.after.existingTests;
|
|
const reviewGate =
|
|
identityRequest.review.independent &&
|
|
identityRequest.review.rootCauseRepaired &&
|
|
identityRequest.review.featurePreserved &&
|
|
!identityRequest.review.boundaryMocks;
|
|
// Application code shares the project test process and can forge reporter
|
|
// output or terminate the runner. The signed receipt authenticates the
|
|
// separate verifier assertions; project-test completion stays self-reported.
|
|
const testCompletionAssurance = 'self_reported' as const;
|
|
const reviewAssurance = params.reviewArtifact?.assurance ?? 'self_attested';
|
|
const inconclusive =
|
|
!params.before.booted ||
|
|
!params.before.legitimate ||
|
|
params.before.security === 'inconclusive' ||
|
|
!params.after.booted ||
|
|
params.after.security === 'inconclusive';
|
|
if (
|
|
params.preparation &&
|
|
(canonical(params.preparation.before.transformations) !==
|
|
canonical(params.preparation.after.transformations) ||
|
|
params.preparation.before.databaseHash !== params.preparation.after.databaseHash)
|
|
)
|
|
throw new CsoError('ASSERTION_FAILED', 'Repair changed the synthetic preparation or database boundary');
|
|
if (mechanical && reviewGate && params.testToolchain === 'project') {
|
|
if (request.changes.some((change) => change.effect === 'dependency'))
|
|
throw new CsoError(
|
|
'PREREQUISITE',
|
|
'Runtime-tested dependency repairs require a test runner pinned in the qualified runtime; project-installed test toolchains may change with the repair',
|
|
);
|
|
if (!params.preparation)
|
|
throw new CsoError(
|
|
'ASSERTION_FAILED',
|
|
'Project-installed test toolchains require before/after prepared dependency proofs',
|
|
);
|
|
if (params.preparation.before.preparedDependencyHash !== params.preparation.after.preparedDependencyHash)
|
|
throw new CsoError(
|
|
'ASSERTION_FAILED',
|
|
'Project-installed test toolchain bytes changed between source phases',
|
|
);
|
|
}
|
|
const transformations = params.manifest.entries.filter((e) => e.transformation),
|
|
transformationsHash = sha256(canonical(transformations)),
|
|
archivesHash = sha256(canonical([...params.archives].sort())),
|
|
requestHash = sha256(canonical(identityRequest)),
|
|
preparationHash = params.preparation ? sha256(canonical(params.preparation)) : undefined,
|
|
startPlanHash = params.startPlanHash ?? sha256(canonical(request.start)),
|
|
testPlanHash =
|
|
params.testPlanHash ??
|
|
sha256(canonical({ commands: request.existingTests, files: [...request.testFiles].sort() })),
|
|
auditPolicyHash = params.auditPolicyHash ?? sha256(canonical({})),
|
|
assertionHash = sha256(
|
|
canonical({ legitimate: identityRequest.legitimate, security: identityRequest.security }),
|
|
),
|
|
minimumPassingTests = params.minimumPassingTests ?? request.existingTests.map(() => 1),
|
|
runner = {
|
|
testToolchain: params.testToolchain,
|
|
startPlanHash,
|
|
testPlanHash,
|
|
commandsHash: sha256(canonical(request.existingTests)),
|
|
minimumPassingTestsHash: sha256(canonical(minimumPassingTests)),
|
|
};
|
|
let assertionAssurance: VerificationManifest['assertionAssurance'], witnessHash: string | undefined;
|
|
if (params.witness) {
|
|
const beforeReceipt = validateStoredAssertionWitnessReceipt(params.witness.before),
|
|
afterReceipt = validateStoredAssertionWitnessReceipt(params.witness.after),
|
|
stable = (binding: AssertionWitnessBinding) => {
|
|
const { nonce: _, issuedAt: __, expiresAt: ___, ...value } = binding;
|
|
return value;
|
|
},
|
|
expected = (
|
|
phase: 'before' | 'after',
|
|
sourceHash: string,
|
|
dependencyHash: string,
|
|
configurationHash: string,
|
|
) => ({
|
|
schemaVersion: 1,
|
|
protocol: 'gstack-cso-assertion-witness-v1',
|
|
phase,
|
|
runId: params.runId,
|
|
findingId: identityRequest.findingId,
|
|
policyHash: params.policyHash,
|
|
auditPolicyHash,
|
|
runtime: {
|
|
image: params.runtime.image,
|
|
verifierImage: params.verifier.image,
|
|
platform: params.runtime.platform,
|
|
profile: params.runtime.id,
|
|
},
|
|
runner,
|
|
sourceHash,
|
|
dependencyHash,
|
|
configurationHash,
|
|
requestHash,
|
|
patchHash: pHash,
|
|
harnessHash,
|
|
assertionHash,
|
|
fixturesHash,
|
|
});
|
|
if (
|
|
canonical(stable(beforeReceipt.binding)) !==
|
|
canonical(
|
|
expected('before', params.manifest.executionHash, dependenciesBefore, configurationBefore),
|
|
) ||
|
|
canonical(stable(afterReceipt.binding)) !==
|
|
canonical(expected('after', sourceAfter, dependenciesAfter, configurationAfter)) ||
|
|
beforeReceipt.publicKey !== afterReceipt.publicKey ||
|
|
beforeReceipt.keyId !== afterReceipt.keyId ||
|
|
beforeReceipt.binding.nonce === afterReceipt.binding.nonce
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Authenticated assertion witness receipts do not bind this verification',
|
|
);
|
|
const beforeObservation = {
|
|
...params.before,
|
|
existingTests: beforeReceipt.diagnosticTestsPassed,
|
|
inputHash: harnessHash,
|
|
},
|
|
afterObservation = {
|
|
...params.after,
|
|
existingTests: afterReceipt.diagnosticTestsPassed,
|
|
inputHash: harnessHash,
|
|
};
|
|
if (
|
|
beforeReceipt.observationHash !== witnessObservationHash(beforeObservation) ||
|
|
afterReceipt.observationHash !== witnessObservationHash(afterObservation) ||
|
|
params.before.existingTests !== beforeReceipt.diagnosticTestsPassed ||
|
|
params.after.existingTests !== afterReceipt.diagnosticTestsPassed ||
|
|
!beforeReceipt.externalAssertionsPassed ||
|
|
!afterReceipt.externalAssertionsPassed
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Authenticated assertion witness receipts do not match the verifier observations',
|
|
);
|
|
const stableExecutions = (receipt: AssertionWitnessReceipt) =>
|
|
receipt.executions.map(({ outputHash: _, ...execution }) => execution);
|
|
if (canonical(stableExecutions(beforeReceipt)) !== canonical(stableExecutions(afterReceipt)))
|
|
throw new CsoError('ASSERTION_FAILED', 'Repair changed the existing-test execution count or outcome');
|
|
assertionAssurance = 'authenticated_out_of_process';
|
|
witnessHash = assertionWitnessPairHash({ before: beforeReceipt, after: afterReceipt });
|
|
}
|
|
const passed = mechanical && reviewGate && assertionAssurance === 'authenticated_out_of_process',
|
|
preservationUnattested = mechanical && reviewGate && !passed,
|
|
createdAt = new Date().toISOString(),
|
|
verification: VerificationManifest = {
|
|
version: 3,
|
|
id: '',
|
|
runId: params.runId,
|
|
findingId: identityRequest.findingId,
|
|
createdAt,
|
|
helperAbi: 3,
|
|
runtime: { image: params.runtime.image, platform: params.runtime.platform, profile: params.runtime.id },
|
|
testToolchain: params.testToolchain,
|
|
policyHash: params.policyHash,
|
|
auditPolicyHash,
|
|
harnessHash,
|
|
requestHash,
|
|
startPlanHash,
|
|
testPlanHash,
|
|
fixturesHash,
|
|
patchHash: pHash,
|
|
originalSourceHash: params.manifest.originalHash,
|
|
transformationsHash,
|
|
archivesHash,
|
|
...(preparationHash ? { preparationHash } : {}),
|
|
beforeSourceHash: params.manifest.executionHash,
|
|
afterSourceHash: sourceAfter,
|
|
beforeDependencies: dependenciesBefore,
|
|
afterDependencies: dependenciesAfter,
|
|
beforeConfiguration: configurationBefore,
|
|
afterConfiguration: configurationAfter,
|
|
before: { ...params.before, inputHash: harnessHash },
|
|
after: { ...params.after, inputHash: harnessHash },
|
|
review: identityRequest.review,
|
|
reviewAssurance,
|
|
...(assertionAssurance ? { assertionAssurance } : {}),
|
|
testCompletionAssurance,
|
|
...(witnessHash ? { witnessHash } : {}),
|
|
result: passed ? 'runtime_tested' : inconclusive || preservationUnattested ? 'inconclusive' : 'failed',
|
|
};
|
|
const id = verificationIdentity(verification);
|
|
verification.id = id;
|
|
if (!['runtime_tested', 'tested'].includes(verification.result)) return { manifest: verification };
|
|
const bundle: RepairBundle = {
|
|
schemaVersion: 3,
|
|
runId: params.runId,
|
|
id,
|
|
createdAt,
|
|
expiresAt: new Date(Date.parse(createdAt) + 30 * 86400_000).toISOString(),
|
|
requiredInputs: {
|
|
sourceHash: params.manifest.executionHash,
|
|
originalHash: params.manifest.originalHash,
|
|
runtimeImage: params.runtime.image,
|
|
platform: params.runtime.platform,
|
|
archives: params.archives,
|
|
...(params.dependencyClosures ? { dependencyClosures: params.dependencyClosures } : {}),
|
|
},
|
|
request: identityRequest,
|
|
verification,
|
|
transformations,
|
|
...(params.preparation ? { preparation: params.preparation } : {}),
|
|
...(params.reviewArtifact ? { reviewArtifact: params.reviewArtifact } : {}),
|
|
witness: params.witness!,
|
|
};
|
|
return { manifest: verification, bundle };
|
|
}
|
|
|
|
export function validateRepairBundle(
|
|
value: unknown,
|
|
id: string,
|
|
sourceRoot?: string,
|
|
sourceManifest?: SnapshotManifest,
|
|
): RepairBundle {
|
|
const bundle = value as RepairBundle;
|
|
if (
|
|
!bundle ||
|
|
bundle.schemaVersion !== 3 ||
|
|
bundle.id !== id ||
|
|
bundle.runId !== bundle.verification?.runId ||
|
|
bundle.verification?.id !== id ||
|
|
verificationIdentity(bundle.verification) !== id
|
|
)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle identity or verification provenance is invalid');
|
|
const request = validateVerificationRequest(bundle.request),
|
|
verification = bundle.verification,
|
|
required = bundle.requiredInputs;
|
|
const createdAtMs = Date.parse(bundle.createdAt);
|
|
if (
|
|
!Number.isFinite(createdAtMs) ||
|
|
new Date(createdAtMs).toISOString() !== bundle.createdAt ||
|
|
bundle.createdAt !== verification.createdAt ||
|
|
bundle.expiresAt !== new Date(createdAtMs + 30 * 86400_000).toISOString()
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle retention timestamps do not match their authenticated verification time',
|
|
);
|
|
if (
|
|
!['runtime_tested', 'tested'].includes(verification.result) ||
|
|
!['self_attested', 'host_verified'].includes(verification.reviewAssurance) ||
|
|
verification.assertionAssurance !== 'authenticated_out_of_process' ||
|
|
(verification.result === 'tested' &&
|
|
verification.testCompletionAssurance !== 'authenticated_out_of_process') ||
|
|
(verification.result === 'runtime_tested' && verification.testCompletionAssurance !== 'self_reported')
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle lacks the assurance required by its repair label',
|
|
);
|
|
if (!['runtime', 'project'].includes(verification.testToolchain))
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle test toolchain provenance is invalid');
|
|
if (
|
|
request.findingId !== verification.findingId ||
|
|
request.runtimeProfile !== verification.runtime.profile ||
|
|
sha256(canonical(request)) !== verification.requestHash ||
|
|
canonical(request.review) !== canonical(verification.review) ||
|
|
patchHash(request) !== verification.patchHash ||
|
|
![verification.requestHash, verification.startPlanHash, verification.testPlanHash].every((value) =>
|
|
/^[a-f0-9]{64}$/.test(value),
|
|
) ||
|
|
sha256(canonical(request.fixtures)) !== verification.fixturesHash ||
|
|
required.sourceHash !== verification.beforeSourceHash ||
|
|
required.originalHash !== verification.originalSourceHash ||
|
|
required.runtimeImage !== verification.runtime.image ||
|
|
required.platform !== verification.runtime.platform ||
|
|
sha256(canonical([...(required.archives ?? [])].sort())) !== verification.archivesHash ||
|
|
sha256(canonical(bundle.transformations ?? [])) !== verification.transformationsHash
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle request, harness, or contents do not match their authenticated manifest',
|
|
);
|
|
if (!bundle.witness || !verification.witnessHash)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle omitted its authenticated external assertion witness',
|
|
);
|
|
const beforeWitness = validateStoredAssertionWitnessReceipt(bundle.witness.before),
|
|
afterWitness = validateStoredAssertionWitnessReceipt(bundle.witness.after),
|
|
stable = (binding: AssertionWitnessBinding) => {
|
|
const { nonce: _, issuedAt: __, expiresAt: ___, ...rest } = binding;
|
|
return rest;
|
|
},
|
|
assertionHash = sha256(canonical({ legitimate: request.legitimate, security: request.security }));
|
|
if (
|
|
assertionWitnessPairHash({ before: beforeWitness, after: afterWitness }) !== verification.witnessHash ||
|
|
beforeWitness.publicKey !== afterWitness.publicKey ||
|
|
beforeWitness.keyId !== afterWitness.keyId ||
|
|
beforeWitness.binding.nonce === afterWitness.binding.nonce
|
|
)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle assertion witness identity is invalid');
|
|
for (const [phase, receipt, observation, sourceHash, dependencyHash, configurationHash] of [
|
|
[
|
|
'before',
|
|
beforeWitness,
|
|
verification.before,
|
|
verification.beforeSourceHash,
|
|
verification.beforeDependencies,
|
|
verification.beforeConfiguration,
|
|
],
|
|
[
|
|
'after',
|
|
afterWitness,
|
|
verification.after,
|
|
verification.afterSourceHash,
|
|
verification.afterDependencies,
|
|
verification.afterConfiguration,
|
|
],
|
|
] as const) {
|
|
const binding = stable(receipt.binding);
|
|
if (
|
|
binding.phase !== phase ||
|
|
binding.runId !== verification.runId ||
|
|
binding.findingId !== verification.findingId ||
|
|
binding.policyHash !== verification.policyHash ||
|
|
binding.auditPolicyHash !== verification.auditPolicyHash ||
|
|
binding.runtime.image !== verification.runtime.image ||
|
|
binding.runtime.verifierImage !== verification.runtime.image ||
|
|
binding.runtime.platform !== verification.runtime.platform ||
|
|
binding.runtime.profile !== verification.runtime.profile ||
|
|
binding.runner.testToolchain !== verification.testToolchain ||
|
|
binding.runner.startPlanHash !== verification.startPlanHash ||
|
|
binding.runner.testPlanHash !== verification.testPlanHash ||
|
|
binding.sourceHash !== sourceHash ||
|
|
binding.dependencyHash !== dependencyHash ||
|
|
binding.configurationHash !== configurationHash ||
|
|
binding.requestHash !== verification.requestHash ||
|
|
binding.patchHash !== verification.patchHash ||
|
|
binding.harnessHash !== verification.harnessHash ||
|
|
binding.assertionHash !== assertionHash ||
|
|
binding.fixturesHash !== verification.fixturesHash ||
|
|
receipt.observationHash !== witnessObservationHash(observation) ||
|
|
receipt.diagnosticTestsPassed !== observation.existingTests ||
|
|
!receipt.externalAssertionsPassed
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle assertion witness does not bind its verification manifest',
|
|
);
|
|
}
|
|
if (canonical(beforeWitness.binding.runner) !== canonical(afterWitness.binding.runner))
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle changed the witnessed runner between phases');
|
|
if (required.dependencyClosures) {
|
|
const hashes = new Set<string>();
|
|
for (const phase of ['before', 'after'] as const) {
|
|
const closure = required.dependencyClosures[phase] as any;
|
|
if (
|
|
!closure ||
|
|
typeof closure !== 'object' ||
|
|
Array.isArray(closure) ||
|
|
!Array.isArray(closure.archives) ||
|
|
!/^([a-f0-9]{64})$/.test(closure.closureHash ?? '')
|
|
)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle dependency closure is malformed');
|
|
const { closureHash, ...body } = closure;
|
|
if (sha256(canonical(body)) !== closureHash)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle dependency closure identity is invalid');
|
|
for (const archive of closure.archives) {
|
|
if (!archive || typeof archive !== 'object' || !/^[a-f0-9]{64}$/.test(archive.sha256 ?? ''))
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle dependency archive provenance is malformed',
|
|
);
|
|
hashes.add(archive.sha256);
|
|
}
|
|
}
|
|
if (canonical([...hashes].sort()) !== canonical([...(required.archives ?? [])].sort()))
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle archive hashes do not match its dependency closures',
|
|
);
|
|
}
|
|
if (required.dependencyClosures && !bundle.preparation)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle omitted prepared-source invariance proofs');
|
|
if (verification.testToolchain === 'project' && !bundle.preparation)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle omitted project test-toolchain preparation proofs',
|
|
);
|
|
if (bundle.preparation) {
|
|
if (
|
|
!verification.preparationHash ||
|
|
sha256(canonical(bundle.preparation)) !== verification.preparationHash ||
|
|
canonical(bundle.preparation.before?.transformations) !==
|
|
canonical(bundle.preparation.after?.transformations) ||
|
|
bundle.preparation.before?.databaseHash !== bundle.preparation.after?.databaseHash
|
|
)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle preparation proof is invalid');
|
|
for (const phase of ['before', 'after'] as const) {
|
|
const proof = bundle.preparation[phase] as PreparationProof,
|
|
closure = (required.dependencyClosures as any)?.[phase];
|
|
if (
|
|
!proof ||
|
|
proof.schemaVersion !== 1 ||
|
|
![
|
|
proof.dependencyClosureHash,
|
|
proof.configurationHash,
|
|
proof.sourceProjectionHash,
|
|
proof.preparedManifestHash,
|
|
proof.preparedDependencyHash,
|
|
proof.receiptHash,
|
|
proof.executionEnvironmentHash,
|
|
proof.databaseHash,
|
|
].every((value) => /^[a-f0-9]{64}$/.test(value)) ||
|
|
!Array.isArray(proof.transformations) ||
|
|
proof.transformations.some(
|
|
(item) =>
|
|
!item ||
|
|
typeof item.path !== 'string' ||
|
|
!/^[a-f0-9]{64}$/.test(item.sha256) ||
|
|
!Number.isInteger(item.mode) ||
|
|
typeof item.reason !== 'string',
|
|
) ||
|
|
(closure && proof.dependencyClosureHash !== closure.closureHash)
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle preparation proof does not bind its dependency closure',
|
|
);
|
|
}
|
|
}
|
|
if (
|
|
verification.testToolchain === 'project' &&
|
|
bundle.preparation!.before.preparedDependencyHash !== bundle.preparation!.after.preparedDependencyHash
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle project test toolchain changed between source phases',
|
|
);
|
|
if (request.review.artifactId) {
|
|
if (!bundle.reviewArtifact)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle omitted its independent review artifact');
|
|
validateReviewArtifact(bundle.reviewArtifact, bundle.runId, request);
|
|
}
|
|
validateVerificationObservation(verification.before);
|
|
validateVerificationObservation(verification.after);
|
|
if (sourceRoot) {
|
|
const references = [
|
|
...request.boundaryFiles,
|
|
...request.testFiles,
|
|
...request.changes.map((change) => change.path),
|
|
...request.start.args,
|
|
...request.existingTests.flatMap((command) => command.args),
|
|
],
|
|
hasHandles = references.some((reference) =>
|
|
Boolean(
|
|
snapshotPathHandleId(reference) ||
|
|
(reference.startsWith('./') && snapshotPathHandleId(reference.slice(2))),
|
|
),
|
|
);
|
|
if (hasHandles && !sourceManifest)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Repair bundle path handles require the matching snapshot manifest for source validation',
|
|
);
|
|
const executionRequest = sourceManifest
|
|
? resolveVerificationRequestPaths(sourceManifest, request)
|
|
: request;
|
|
if (verificationHarnessHash(executionRequest, sourceRoot) !== verification.harnessHash)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle harness does not match supplied source inputs');
|
|
const commandsHash = sha256(canonical(executionRequest.existingTests)),
|
|
commandHashes = executionRequest.existingTests.map((command) => sha256(canonical(command)));
|
|
if (
|
|
beforeWitness.binding.runner.commandsHash !== commandsHash ||
|
|
canonical(beforeWitness.executions.map((item) => item.commandHash)) !== canonical(commandHashes) ||
|
|
canonical(afterWitness.executions.map((item) => item.commandHash)) !== canonical(commandHashes)
|
|
)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Repair bundle witnessed a different test runner command set');
|
|
}
|
|
return { ...bundle, request };
|
|
}
|
|
|
|
export class DockerVerificationExecutor implements VerificationExecutor {
|
|
private attemptDeadline: number;
|
|
private executionStarted = false;
|
|
constructor(
|
|
private endpoint: DockerEndpoint,
|
|
private watchdogPath: string,
|
|
private runExecutionDeadline = Date.now() + 300_000,
|
|
private onExecutionStarted?: () => void | Promise<void>,
|
|
) {
|
|
this.attemptDeadline = Math.min(Date.now() + 300_000, runExecutionDeadline);
|
|
}
|
|
async observe(
|
|
source: string,
|
|
phase: 'before' | 'after',
|
|
request: VerificationRequest,
|
|
runtime: QualifiedRuntime,
|
|
verifier: QualifiedRuntime,
|
|
work: string,
|
|
control: string,
|
|
execution?: { environment: Record<string, string>; database?: PreparedDatabaseContract },
|
|
testEvidence?: { minimumPassingTests: number[] },
|
|
witness?: AssertionWitnessHandle,
|
|
): Promise<VerificationObservation | WitnessedVerificationResult> {
|
|
const phaseDir = secureDirectory(join(work, phase)),
|
|
phaseControl = secureDirectory(join(control, phase)),
|
|
policy = secureDirectory(join(phaseDir, 'policy')),
|
|
policyFile = join(policy, 'verification.json'),
|
|
fixtures = secureDirectory(join(phaseDir, 'fixtures'));
|
|
const verifierPolicy = JSON.stringify({
|
|
phase,
|
|
port: request.port,
|
|
legitimate: request.legitimate,
|
|
security: request.security,
|
|
});
|
|
if (redact(verifierPolicy) !== verifierPolicy)
|
|
throw new CsoError('REDACTION_FAILED', 'Verification harness contains secret-bearing data');
|
|
fs.writeFileSync(policyFile, verifierPolicy, { mode: 0o600 });
|
|
for (const [path, body] of Object.entries(request.fixtures)) {
|
|
const file = containedFile(fixtures, path);
|
|
secureDirectory(dirname(file));
|
|
fs.writeFileSync(file, body, { mode: 0o600 });
|
|
}
|
|
const deadline = this.attemptDeadline;
|
|
if (deadline <= Date.now())
|
|
throw new CsoError('DEADLINE', 'No execution time remains before the reporting reserve');
|
|
let group: DockerGroup | undefined;
|
|
try {
|
|
group = await DockerGroup.create(
|
|
this.endpoint,
|
|
`${request.findingId.slice(0, 12)}-${phase}-${Date.now()}-${randomBytes(6).toString('hex')}`,
|
|
phaseControl,
|
|
deadline,
|
|
verifier.image,
|
|
this.watchdogPath,
|
|
);
|
|
if (!this.executionStarted) {
|
|
this.executionStarted = true;
|
|
await this.onExecutionStarted?.();
|
|
}
|
|
const supplied = execution?.environment ?? {},
|
|
allowed = new Set([
|
|
'PATH',
|
|
'VIRTUAL_ENV',
|
|
'PYTHONNOUSERSITE',
|
|
'BUNDLE_PATH',
|
|
'BUNDLE_FROZEN',
|
|
'BUNDLE_DEPLOYMENT',
|
|
'BUNDLE_DISABLE_SHARED_GEMS',
|
|
'BUNDLE_IGNORE_CONFIG',
|
|
'BUNDLE_ALLOW_OFFLINE_INSTALL',
|
|
'BUNDLE_CACHE_PATH',
|
|
'BUNDLE_USER_HOME',
|
|
'GEM_HOME',
|
|
'GEM_PATH',
|
|
]);
|
|
if (
|
|
Object.entries(supplied).some(
|
|
([key, value]) => !allowed.has(key) || typeof value !== 'string' || value.includes('\0'),
|
|
)
|
|
)
|
|
throw new CsoError('ISOLATION_FAILED', 'Prepared execution environment exceeded its fixed allowlist');
|
|
const env = {
|
|
...supplied,
|
|
PORT: String(request.port),
|
|
HOST: '127.0.0.1',
|
|
NODE_ENV: 'test',
|
|
RAILS_ENV: 'test',
|
|
RACK_ENV: 'test',
|
|
PYTHONUNBUFFERED: '1',
|
|
CI: '1',
|
|
SECRET_KEY_BASE: 'cso-synthetic-test-key',
|
|
CSO_FIXTURES: '/fixtures',
|
|
};
|
|
const database = execution?.database;
|
|
if (database && runtime.stack !== 'rails')
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Prepared database contract can only execute with Rails');
|
|
if (runtime.stack === 'rails' && !database)
|
|
throw new CsoError('PREREQUISITE', 'Rails verification omitted its prepared database contract');
|
|
if (database?.adapter === 'postgresql') {
|
|
const databaseFile = join(policy, 'postgresql.databases'),
|
|
names = database.connections.map((name) => `cso_${name}`);
|
|
if (!names.length || names.some((name) => !/^cso_[A-Za-z_][A-Za-z0-9_]{0,47}$/.test(name)))
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Prepared PostgreSQL connection names are invalid');
|
|
fs.writeFileSync(databaseFile, names.join('\n') + '\n', { mode: 0o444, flag: 'wx' });
|
|
const postgres = await group.createContainer({
|
|
role: 'postgres',
|
|
image: database.sidecar.image,
|
|
command: ['/opt/cso/run-postgresql', '/policy/postgresql.databases'],
|
|
postgresDatabasePolicy: databaseFile,
|
|
});
|
|
await group.start(postgres);
|
|
let ready = false;
|
|
for (let attempt = 0; attempt < 100 && !ready; attempt++) {
|
|
const checked = await group.execCapture(postgres, [
|
|
'/opt/cso/postgresql-ready',
|
|
'/policy/postgresql.databases',
|
|
]);
|
|
ready = checked.code === 0;
|
|
if (!ready) await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
if (!ready)
|
|
throw new CsoError(
|
|
'TOOL_FAILED',
|
|
'Disposable PostgreSQL did not create and accept connections for every declared Rails database',
|
|
);
|
|
}
|
|
const cleanCommand = (command: string[]) => [
|
|
'/usr/bin/env',
|
|
'-i',
|
|
...Object.entries(env)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([key, value]) => `${key}=${value}`),
|
|
...command,
|
|
];
|
|
const dbPrepare = async (id: string) => {
|
|
const result = await group!.execCapture(
|
|
id,
|
|
cleanCommand(['/usr/local/bin/bundle', 'exec', 'rails', 'db:prepare']),
|
|
{ workdir: '/work' },
|
|
);
|
|
if (result.code !== 0)
|
|
throw new CsoError(
|
|
'TOOL_FAILED',
|
|
'Rails database preparation failed in the isolated test environment',
|
|
);
|
|
};
|
|
let app: string;
|
|
if (runtime.stack === 'rails') {
|
|
app = await group.createContainer({
|
|
role: 'app',
|
|
image: runtime.image,
|
|
source,
|
|
env,
|
|
command: ['/opt/cso/run-app', '/bin/sleep', '2147483647'],
|
|
readonlyDirectories: [{ host: fixtures, container: '/fixtures' }],
|
|
});
|
|
await group.start(app);
|
|
await dbPrepare(app);
|
|
await group.execDetached(app, [request.start.executable, ...request.start.args]);
|
|
} else {
|
|
app = await group.createContainer({
|
|
role: 'app',
|
|
image: runtime.image,
|
|
source,
|
|
env,
|
|
command: ['/opt/cso/run-app', request.start.executable, ...request.start.args],
|
|
readonlyDirectories: [{ host: fixtures, container: '/fixtures' }],
|
|
});
|
|
await group.start(app);
|
|
}
|
|
const verifierId = await group.createContainer({
|
|
role: 'verifier',
|
|
image: verifier.image,
|
|
command: ['/opt/cso/verifier', '/policy/verification.json'],
|
|
readonlyFiles: [{ host: policyFile, container: '/policy/verification.json' }],
|
|
});
|
|
const result = await group.startAttach(verifierId);
|
|
let observation: VerificationObservation;
|
|
try {
|
|
observation = validateVerificationObservation(JSON.parse(result.output.trim()));
|
|
} catch {
|
|
observation = {
|
|
booted: false,
|
|
legitimate: false,
|
|
security: 'inconclusive',
|
|
existingTests: false,
|
|
output: 'verifier returned invalid bounded output',
|
|
inputHash: '',
|
|
};
|
|
}
|
|
await group.removeContainer(verifierId);
|
|
await group.removeContainer(app);
|
|
const minimumPassingTests = testEvidence?.minimumPassingTests ?? request.existingTests.map(() => 1);
|
|
if (
|
|
minimumPassingTests.length !== request.existingTests.length ||
|
|
minimumPassingTests.some((value) => !Number.isInteger(value) || value < 1)
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Helper-derived test execution-count floors do not match the canonical test commands',
|
|
);
|
|
let existingTests = true;
|
|
const executions: Array<{
|
|
command: VerificationRequest['existingTests'][number];
|
|
code: number;
|
|
output: string;
|
|
minimumPassingTests: number;
|
|
}> = [];
|
|
for (const [index, test] of request.existingTests.entries()) {
|
|
const rails = runtime.stack === 'rails';
|
|
const testId = await group.createContainer({
|
|
role: 'tests',
|
|
image: runtime.image,
|
|
source,
|
|
env,
|
|
command: rails
|
|
? ['/opt/cso/run-app', '/bin/sleep', '2147483647']
|
|
: ['/opt/cso/run-app', test.executable, ...test.args],
|
|
readonlyDirectories: [{ host: fixtures, container: '/fixtures' }],
|
|
});
|
|
let testResult: { code: number; output: string };
|
|
if (rails) {
|
|
await group.start(testId);
|
|
await dbPrepare(testId);
|
|
const result = await group.execCapture(testId, cleanCommand([test.executable, ...test.args]), {
|
|
workdir: '/work',
|
|
});
|
|
testResult = { code: result.code, output: result.stdout + result.stderr };
|
|
} else testResult = await group.startAttach(testId);
|
|
executions.push({
|
|
command: test,
|
|
code: testResult.code,
|
|
output: testResult.output,
|
|
minimumPassingTests: minimumPassingTests[index],
|
|
});
|
|
if (!testExecutionPassed(test, testResult.code, testResult.output, minimumPassingTests[index]))
|
|
existingTests = false;
|
|
await group.removeContainer(testId);
|
|
}
|
|
if (witness) {
|
|
if (observation.existingTests)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'External verifier attempted to assert project test completion',
|
|
);
|
|
const receipt = await witness.attest(observation, executions),
|
|
witnessed = {
|
|
...observation,
|
|
existingTests: receipt.diagnosticTestsPassed,
|
|
inputHash: witness.binding.harnessHash,
|
|
};
|
|
return { observation: witnessed, witness: receipt };
|
|
}
|
|
observation.existingTests = existingTests;
|
|
return observation;
|
|
} finally {
|
|
if (group) await group.cleanup();
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function verifyRepair(params: {
|
|
runId: string;
|
|
runDir: string;
|
|
manifest: SnapshotManifest;
|
|
rawRequest: unknown;
|
|
runtime: QualifiedRuntime;
|
|
verifier: QualifiedRuntime;
|
|
policyHash: string;
|
|
auditPolicyHash?: string;
|
|
archives: string[];
|
|
dependencyClosures?: { before: unknown; after: unknown };
|
|
preparation?: { before: PreparationProof; after: PreparationProof };
|
|
reviewArtifact?: RepairReviewArtifact;
|
|
executor: VerificationExecutor;
|
|
persist?: boolean;
|
|
watchdogPath?: string;
|
|
attemptDeadline?: number;
|
|
}): Promise<{ manifest: VerificationManifest; bundle: RepairBundle }> {
|
|
const identityRequest = validateVerificationRequest(params.rawRequest),
|
|
request = resolveVerificationRequestPaths(params.manifest, identityRequest),
|
|
snapshot = join(params.runDir, 'snapshot'),
|
|
work = join(
|
|
params.runDir,
|
|
'verification',
|
|
`${request.findingId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
|
|
),
|
|
after = join(work, 'sources', 'after'),
|
|
observations = join(work, 'observations'),
|
|
groupControls = secureDirectory(join(params.runDir, 'supervision', basename(work), 'docker-groups'));
|
|
if (canonical(sanitizeHelperForJson(identityRequest)) !== canonical(identityRequest))
|
|
throw new CsoError(
|
|
'REDACTION_FAILED',
|
|
'Verification request contains sensitive material that cannot enter a replayable bundle',
|
|
);
|
|
if (!['node', 'bun', 'python', 'rails'].includes(params.runtime.stack))
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Canonical project tests require an application runtime');
|
|
const stack = params.runtime.stack as CsoStack,
|
|
beforeTestPlan = assertCanonicalTestPlan(request, snapshot, stack),
|
|
beforeStartPlan = assertCanonicalStartPlan(request, snapshot, stack);
|
|
if (
|
|
beforeTestPlan.toolchain === 'project' &&
|
|
request.changes.some((change) => change.effect === 'dependency')
|
|
)
|
|
throw new CsoError(
|
|
'PREREQUISITE',
|
|
'Runtime-tested dependency repairs require a test runner pinned in the qualified runtime; project-installed test toolchains may change with the repair',
|
|
);
|
|
for (const boundary of [
|
|
...new Set([...request.boundaryFiles, ...request.testFiles, ...beforeStartPlan.entrypointFiles]),
|
|
]) {
|
|
const e = params.manifest.entries.find((x) => x.path === boundary);
|
|
if (!e || e.transformation)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
`Snapshot transformation changes or withholds a verification input: ${boundary}`,
|
|
);
|
|
}
|
|
for (const change of request.changes) {
|
|
const path = relativePath(change.path),
|
|
entry = params.manifest.entries.find((x) => x.path === path);
|
|
containedFile(snapshot, path);
|
|
if (change.beforeSha256 === null) {
|
|
if (entry)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
`Declared new repair path already exists in the snapshot: ${path}`,
|
|
);
|
|
} else if (!entry || entry.transformation)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
`Snapshot transformation changes or withholds a repair input: ${path}`,
|
|
);
|
|
}
|
|
let guardedCleanup: (() => Promise<void>) | undefined;
|
|
if (params.watchdogPath) {
|
|
const deadline = Math.min(params.attemptDeadline ?? Date.now() + 300_000, Date.now() + 300_000);
|
|
guardedCleanup = await attemptGuard(params.runDir, work, params.watchdogPath, deadline);
|
|
}
|
|
secureDirectory(observations);
|
|
let certified: ReturnType<typeof certify> | undefined,
|
|
beforeObs: VerificationObservation | undefined,
|
|
afterObs: VerificationObservation | undefined,
|
|
beforeWitness: AssertionWitnessReceipt | undefined,
|
|
afterWitness: AssertionWitnessReceipt | undefined,
|
|
failure: unknown,
|
|
missingExternalWitness = false;
|
|
try {
|
|
preparePatchedSource(snapshot, after, request);
|
|
const afterTestPlan = canonicalTestPlan(after, stack),
|
|
afterStartPlan = canonicalStartPlan(after, stack, request.port);
|
|
if (canonical(afterTestPlan) !== canonical(beforeTestPlan))
|
|
throw new CsoError(
|
|
'ASSERTION_FAILED',
|
|
'Repair changed the canonical project test suite, runner configuration, or discovered test inputs',
|
|
);
|
|
const startShape = (plan: CanonicalStartPlan) => ({
|
|
command: plan.command,
|
|
kind: plan.kind,
|
|
entrypointFiles: plan.entrypointFiles,
|
|
}),
|
|
changedPaths = new Set(request.changes.map((change) => change.path));
|
|
if (
|
|
canonical(startShape(afterStartPlan)) !== canonical(startShape(beforeStartPlan)) ||
|
|
(afterStartPlan.signature !== beforeStartPlan.signature &&
|
|
!beforeStartPlan.entrypointFiles.some((path) => changedPaths.has(path)))
|
|
)
|
|
throw new CsoError(
|
|
'ASSERTION_FAILED',
|
|
'Repair changed the helper-derived application startup plan outside its declared patch',
|
|
);
|
|
const beforeInvariant = treeHash(snapshot),
|
|
afterInvariant = treeHash(after);
|
|
if (beforeInvariant !== params.manifest.executionHash)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Retained source does not match the snapshot identity bound to this verification',
|
|
);
|
|
const testEvidence = { minimumPassingTests: beforeTestPlan.minimumPassingTests },
|
|
auditPolicyHash = params.auditPolicyHash ?? sha256(canonical({})),
|
|
requestHash = sha256(canonical(identityRequest)),
|
|
pHash = patchHash(identityRequest),
|
|
harnessHash = verificationHarnessHash(request, snapshot),
|
|
assertionHash = sha256(
|
|
canonical({ legitimate: identityRequest.legitimate, security: identityRequest.security }),
|
|
),
|
|
runner = {
|
|
testToolchain: beforeTestPlan.toolchain,
|
|
startPlanHash: beforeStartPlan.signature,
|
|
testPlanHash: beforeTestPlan.signature,
|
|
commandsHash: sha256(canonical(request.existingTests)),
|
|
minimumPassingTestsHash: sha256(canonical(beforeTestPlan.minimumPassingTests)),
|
|
},
|
|
session = new AssertionWitnessSession(
|
|
observations,
|
|
Math.min(params.attemptDeadline ?? Date.now() + 300_000, Date.now() + 300_000),
|
|
),
|
|
stable = (
|
|
phase: 'before' | 'after',
|
|
root: string,
|
|
sourceHash: string,
|
|
): Omit<
|
|
AssertionWitnessBinding,
|
|
'schemaVersion' | 'protocol' | 'nonce' | 'issuedAt' | 'expiresAt'
|
|
> => ({
|
|
phase,
|
|
runId: params.runId,
|
|
findingId: identityRequest.findingId,
|
|
policyHash: params.policyHash,
|
|
auditPolicyHash,
|
|
runtime: {
|
|
image: params.runtime.image,
|
|
verifierImage: params.verifier.image,
|
|
platform: params.runtime.platform,
|
|
profile: params.runtime.id,
|
|
},
|
|
runner,
|
|
sourceHash,
|
|
dependencyHash: treeHash(root, (p) => DEPENDENCY.test(p)),
|
|
configurationHash: treeHash(root, (p) => CONFIG.test(p) && !DEPENDENCY.test(p)),
|
|
requestHash,
|
|
patchHash: pHash,
|
|
harnessHash,
|
|
assertionHash,
|
|
fixturesHash: sha256(canonical(identityRequest.fixtures)),
|
|
}),
|
|
beforeHandle = session.handle(stable('before', snapshot, beforeInvariant));
|
|
const rawBefore = await params.executor.observe(
|
|
snapshot,
|
|
'before',
|
|
request,
|
|
params.runtime,
|
|
params.verifier,
|
|
observations,
|
|
groupControls,
|
|
undefined,
|
|
testEvidence,
|
|
beforeHandle,
|
|
),
|
|
observedBefore =
|
|
'observation' in (rawBefore as any)
|
|
? validateVerificationObservation((rawBefore as WitnessedVerificationResult).observation)
|
|
: validateVerificationObservation(rawBefore as VerificationObservation);
|
|
if ('observation' in (rawBefore as any))
|
|
beforeWitness = beforeHandle.validate(
|
|
(rawBefore as WitnessedVerificationResult).witness,
|
|
observedBefore,
|
|
);
|
|
if (treeHash(snapshot) !== beforeInvariant)
|
|
throw new CsoError('ASSERTION_FAILED', 'Verification mutated the retained source snapshot');
|
|
beforeObs = observedBefore;
|
|
const afterHandle = session.handle(stable('after', after, afterInvariant)),
|
|
rawAfter = await params.executor.observe(
|
|
after,
|
|
'after',
|
|
request,
|
|
params.runtime,
|
|
params.verifier,
|
|
observations,
|
|
groupControls,
|
|
undefined,
|
|
testEvidence,
|
|
afterHandle,
|
|
),
|
|
observedAfter =
|
|
'observation' in (rawAfter as any)
|
|
? validateVerificationObservation((rawAfter as WitnessedVerificationResult).observation)
|
|
: validateVerificationObservation(rawAfter as VerificationObservation);
|
|
if ('observation' in (rawAfter as any))
|
|
afterWitness = afterHandle.validate((rawAfter as WitnessedVerificationResult).witness, observedAfter);
|
|
if (treeHash(after) !== afterInvariant)
|
|
throw new CsoError('ASSERTION_FAILED', 'Verification mutated the pristine patched source');
|
|
afterObs = observedAfter;
|
|
certified = certify({
|
|
...params,
|
|
request,
|
|
identityRequest,
|
|
before: beforeObs,
|
|
after: afterObs,
|
|
beforeRoot: snapshot,
|
|
afterRoot: after,
|
|
startPlanHash: beforeStartPlan.signature,
|
|
testPlanHash: beforeTestPlan.signature,
|
|
testToolchain: beforeTestPlan.toolchain,
|
|
minimumPassingTests: beforeTestPlan.minimumPassingTests,
|
|
...(beforeWitness && afterWitness ? { witness: { before: beforeWitness, after: afterWitness } } : {}),
|
|
});
|
|
} catch (error) {
|
|
failure = error;
|
|
}
|
|
try {
|
|
if (guardedCleanup) await guardedCleanup();
|
|
else fs.rmSync(work, { recursive: true, force: true });
|
|
} catch (error) {
|
|
failure = error;
|
|
certified = undefined;
|
|
missingExternalWitness = false;
|
|
}
|
|
if (!failure && certified && !certified.bundle) {
|
|
const manifest = certified.manifest,
|
|
review = manifest.review;
|
|
missingExternalWitness =
|
|
!manifest.assertionAssurance &&
|
|
manifest.testCompletionAssurance === 'self_reported' &&
|
|
manifest.result === 'inconclusive' &&
|
|
manifest.before.booted &&
|
|
manifest.before.legitimate &&
|
|
manifest.before.security === 'intended_failure' &&
|
|
manifest.before.existingTests &&
|
|
manifest.after.booted &&
|
|
manifest.after.legitimate &&
|
|
manifest.after.security === 'pass' &&
|
|
manifest.after.existingTests &&
|
|
review.independent &&
|
|
review.rootCauseRepaired &&
|
|
review.featurePreserved &&
|
|
!review.boundaryMocks;
|
|
failure = missingExternalWitness
|
|
? new CsoError(
|
|
'PREREQUISITE',
|
|
'Repair verification retained self-reported project-test diagnostics but requires a helper-authenticated out-of-process external assertion witness before a runtime-tested bundle can be issued',
|
|
)
|
|
: new CsoError(
|
|
'ASSERTION_FAILED',
|
|
manifest.result === 'inconclusive'
|
|
? 'Repair verification was inconclusive; no repair bundle was issued'
|
|
: 'Repair failed one or more required boot, control, security, existing-test, or review assertions; no repair bundle was issued',
|
|
);
|
|
}
|
|
if (failure) {
|
|
if (beforeObs) {
|
|
const cause =
|
|
failure instanceof CsoError
|
|
? failure
|
|
: new CsoError('ASSERTION_FAILED', 'Repair validation failed after the before-phase observation');
|
|
const reproduction = !beforeObs.booted
|
|
? 'blocked'
|
|
: !beforeObs.legitimate
|
|
? 'inconclusive'
|
|
: beforeObs.security === 'intended_failure'
|
|
? 'reproduced'
|
|
: beforeObs.security === 'pass'
|
|
? 'disproved'
|
|
: 'inconclusive',
|
|
harnessHash = verificationHarnessHash(request, snapshot);
|
|
const missingWitness =
|
|
missingExternalWitness &&
|
|
cause.code === 'PREREQUISITE' &&
|
|
reproduction === 'reproduced' &&
|
|
afterObs?.booted === true &&
|
|
afterObs.legitimate === true &&
|
|
afterObs.security === 'pass' &&
|
|
beforeObs.existingTests &&
|
|
afterObs.existingTests;
|
|
const raw = {
|
|
schemaVersion: 3 as const,
|
|
artifactKind: 'repair_candidate' as const,
|
|
runId: params.runId,
|
|
findingId: identityRequest.findingId,
|
|
createdAt: new Date().toISOString(),
|
|
bundleIssued: false as const,
|
|
runtime: {
|
|
image: params.runtime.image,
|
|
platform: params.runtime.platform,
|
|
profile: params.runtime.id,
|
|
},
|
|
policyHash: params.policyHash,
|
|
requestHash: sha256(canonical(identityRequest)),
|
|
harnessHash,
|
|
sourceHash: params.manifest.executionHash,
|
|
request: identityRequest,
|
|
patchHash: patchHash(identityRequest),
|
|
testToolchain: beforeTestPlan.toolchain,
|
|
testCompletionAssurance: 'self_reported' as const,
|
|
...(params.preparation ? { preparationHash: sha256(canonical(params.preparation)) } : {}),
|
|
before: { ...beforeObs, inputHash: harnessHash },
|
|
...(afterObs ? { after: { ...afterObs, inputHash: harnessHash } } : {}),
|
|
reproduction,
|
|
repair: missingWitness ? ('proposed' as const) : ('failed' as const),
|
|
failure: { code: cause.code, message: cause.message },
|
|
},
|
|
safe = sanitizeHelperForJson(raw) as Omit<FailedVerificationAttempt, 'id'>,
|
|
id = sha256(canonical(safe)).slice(0, 32),
|
|
attempt: FailedVerificationAttempt = { ...safe, id };
|
|
validateVerificationObservation(attempt.before);
|
|
if (attempt.after) validateVerificationObservation(attempt.after);
|
|
if (params.persist !== false)
|
|
writeJsonExclusive(join(params.runDir, 'verification-attempts', `${id}.json`), attempt);
|
|
throw new VerificationAttemptError(cause, attempt);
|
|
}
|
|
throw failure;
|
|
}
|
|
if (!certified?.bundle)
|
|
throw new CsoError('ASSERTION_FAILED', 'Verification ended without a certifiable result');
|
|
if (params.persist !== false) {
|
|
const persistable = sanitizeHelperForJson(certified.bundle);
|
|
if (canonical(persistable) !== canonical(certified.bundle))
|
|
throw new CsoError(
|
|
'REDACTION_FAILED',
|
|
'Repair bundle provenance contains material that cannot be persisted without changing its identity',
|
|
);
|
|
validateRepairBundle(persistable, certified.bundle.id, snapshot, params.manifest);
|
|
writeJsonExclusive(join(params.runDir, 'bundles', `${certified.bundle.id}.json`), persistable);
|
|
}
|
|
return { manifest: certified.manifest, bundle: certified.bundle };
|
|
}
|