130 lines
8.6 KiB
JavaScript
130 lines
8.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { digestJson, sha256 } from "../lib/canonical.mjs";
|
|
import { snapshotZones } from "../lib/fs-evidence.mjs";
|
|
import { selfTestObserver } from "../lib/observer.mjs";
|
|
import { executableDigest, loadReceiptValidator, SUITE_IDS, writeCanonicalReceipt } from "../lib/receipt.mjs";
|
|
import { installCandidate, isolatedZones, systemIdentity } from "../lib/runtime.mjs";
|
|
import {
|
|
packageSuite, prepareRuntimeCache, suite, verifyAdapters, verifyEntrypoints,
|
|
verifyFuzz, verifyHostile, verifyLegacy, verifyMcp, verifyProperty,
|
|
} from "../lib/suites.mjs";
|
|
import { inspectPackageTarball } from "../lib/tarball.mjs";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const verifierRoot = path.resolve(here, "..");
|
|
const verificationRoot = path.resolve(verifierRoot, "..");
|
|
const baselineRoot = path.join(verificationRoot, "baseline", "v1");
|
|
|
|
function args(values) {
|
|
const out = {};
|
|
for (let i = 2; i < values.length; i += 2) {
|
|
if (!values[i].startsWith("--") || values[i + 1] === undefined) throw new Error(`Invalid argument: ${values[i]}`);
|
|
out[values[i].slice(2)] = values[i + 1];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function required(value, name, pattern = /./) {
|
|
if (!value || !pattern.test(value)) throw new Error(`--${name} is required or invalid`);
|
|
return value;
|
|
}
|
|
|
|
function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); }
|
|
function failedSuite(id, error) {
|
|
const evidence = { code: error.code || "AAS_VERIFIER_INTERNAL", detailDigest: digestJson({ name: error.name, message: error.message }) };
|
|
return { id, status: "failed", executions: 0, failures: 1, evidenceSha256: digestJson(evidence), evidence };
|
|
}
|
|
|
|
function transactionSuite(file, manifest, validator) {
|
|
if (!file || !fs.existsSync(file)) throw Object.assign(new Error("OS-level transaction evidence is required"), { code: "AAS_VERIFIER_TRANSACTION_EVIDENCE_MISSING" });
|
|
const value = readJson(file);
|
|
if (!validator(value)) throw Object.assign(new Error("Transaction evidence schema failed"), { code: "AAS_VERIFIER_TRANSACTION_EVIDENCE_SCHEMA" });
|
|
const fault = new Set(value.faultBoundaryClasses || []);
|
|
const race = new Set(value.raceClasses || []);
|
|
for (const item of manifest.faultBoundaryClasses) if (!fault.has(item)) throw Object.assign(new Error(`Fault boundary not covered: ${item}`), { code: "AAS_VERIFIER_FAULT_BOUNDARY_MISSING" });
|
|
for (const item of manifest.raceClasses) if (!race.has(item)) throw Object.assign(new Error(`Race class not covered: ${item}`), { code: "AAS_VERIFIER_RACE_CLASS_MISSING" });
|
|
if (value.testMode === true || value.mocked === true || value.productionBinary !== true) throw Object.assign(new Error("Transaction evidence is not production black-box evidence"), { code: "AAS_VERIFIER_TRANSACTION_NOT_BLACK_BOX" });
|
|
if (value.partialStates !== 0 || value.unmanagedMutations !== 0 || value.hardPolicyViolations !== 0) throw Object.assign(new Error("Transaction safety invariant failed"), { code: "AAS_VERIFIER_TRANSACTION_INVARIANT" });
|
|
return suite("transaction", value, value.executions || 0);
|
|
}
|
|
|
|
const options = args(process.argv);
|
|
const tarball = path.resolve(required(options.tarball, "tarball"));
|
|
const candidateCommit = required(options["candidate-commit"], "candidate-commit", /^[a-f0-9]{40}$/);
|
|
const verifierCommit = required(options["verifier-commit"], "verifier-commit", /^[a-f0-9]{40}$/);
|
|
const jobId = required(options["job-id"], "job-id");
|
|
const output = path.resolve(required(options.out, "out"));
|
|
const workRoot = path.resolve(required(options["work-root"], "work-root"));
|
|
const runtimeMatrix = readJson(path.join(baselineRoot, "runtime-matrix.json"));
|
|
const job = runtimeMatrix.jobs.find((entry) => entry.id === jobId);
|
|
if (!job) throw new Error(`Unknown job: ${jobId}`);
|
|
if (process.platform !== job.os.replace("macos", "darwin").replace("windows", "win32")) throw new Error(`Job/platform mismatch: ${jobId}/${process.platform}`);
|
|
if (process.arch !== job.architecture || process.version !== `v${job.nodePatch}`) throw new Error(`Frozen runtime identity mismatch: ${process.arch}/${process.version}`);
|
|
|
|
fs.mkdirSync(workRoot, { recursive: true, mode: 0o700 });
|
|
const evidenceDir = path.join(workRoot, "observer-evidence");
|
|
const zones = isolatedZones(path.join(workRoot, "zones"));
|
|
const zoneBefore = snapshotZones(zones);
|
|
const manifest = readJson(path.join(baselineRoot, "verifier-manifest.json"));
|
|
const freeze = readJson(path.join(baselineRoot, "freeze-manifest.json"));
|
|
const budgets = readJson(path.join(baselineRoot, "budgets.json"));
|
|
const hostileManifest = readJson(path.join(baselineRoot, "hostile", "manifest.json"));
|
|
const transactionValidator = loadReceiptValidator(path.join(verificationRoot, "schemas", "product-transaction-evidence.schema.json"));
|
|
const inspection = inspectPackageTarball(tarball);
|
|
const runtime = await installCandidate(tarball, path.join(workRoot, "candidate-install"));
|
|
const tarballBytes = fs.readFileSync(tarball);
|
|
const observer = await selfTestObserver({ cwd: zones.tmp, env: process.env, zones, evidenceDir });
|
|
const runtimePromotion = await prepareRuntimeCache(runtime, tarballBytes, inspection.sha512, zones.cache);
|
|
|
|
const suites = [];
|
|
const failures = [];
|
|
const run = async (id, action) => {
|
|
try { suites.push(await action()); }
|
|
catch (error) {
|
|
suites.push(failedSuite(id, error));
|
|
failures.push({ code: /^AAS_VERIFIER_/.test(error.code || "") ? error.code : "AAS_VERIFIER_INTERNAL", suite: id, detailDigest: digestJson({ name: error.name, message: error.message }) });
|
|
}
|
|
};
|
|
await run("package", () => packageSuite(inspection, runtime));
|
|
await run("entrypoints", () => verifyEntrypoints(runtime, zones));
|
|
await run("mcp", () => verifyMcp(runtime, zones, evidenceDir));
|
|
await run("property", () => verifyProperty(runtime, budgets, runtimeMatrix.jobs.indexOf(job), verifierRoot));
|
|
await run("fuzz", () => verifyFuzz(runtime, budgets, runtimeMatrix.jobs.indexOf(job), verifierRoot));
|
|
await run("hostile", () => verifyHostile(runtime, zones, evidenceDir, hostileManifest, path.join(baselineRoot, "hostile"), verifierRoot));
|
|
await run("legacy", () => verifyLegacy(runtime, zones, verifierRoot, path.join(baselineRoot, "legacy", "14.6.0")));
|
|
await run("transaction", () => transactionSuite(options["transaction-evidence"], manifest, transactionValidator));
|
|
await run("adapters", () => verifyAdapters(runtime, zones, path.join(baselineRoot, "host-adapters"), inspection.sha512, runtimePromotion.identity.closureDigest));
|
|
for (const id of SUITE_IDS) if (!suites.some((entry) => entry.id === id)) suites.push(failedSuite(id, Object.assign(new Error("Suite did not execute"), { code: "AAS_VERIFIER_SUITE_MISSING" })));
|
|
|
|
const mcp = suites.find((entry) => entry.id === "mcp");
|
|
const canonicalSha = mcp?.evidence?.canonicalResponseDigest || digestJson(null);
|
|
const { jobId: _jobId, ...identity } = systemIdentity(job);
|
|
const mcpBefore = mcp?.evidence?.before || Object.fromEntries(Object.keys(zones).map((name) => [name, zoneBefore[name].digest]));
|
|
const mcpAfter = mcp?.evidence?.after || mcpBefore;
|
|
const receipt = {
|
|
schemaVersion: 1,
|
|
receiptVersion: "1.0.0",
|
|
status: failures.length ? "failed" : "passed",
|
|
job: { id: job.id, workflowRunId: process.env.GITHUB_RUN_ID || "1", workflowRunAttempt: process.env.GITHUB_RUN_ATTEMPT || "1" },
|
|
candidate: {
|
|
commit: candidateCommit, package: runtime.manifest.name, version: runtime.manifest.version,
|
|
tarballBytes: inspection.bytes, tarballSha256: inspection.sha256, tarballSha512: inspection.sha512,
|
|
packManifestSha256: digestJson(inspection.entries), installTreeSha256: runtime.treeDigest,
|
|
},
|
|
verifier: { version: "1.0.0", commit: verifierCommit, rootDigest: freeze.rootDigest, contractDigest: digestJson(manifest), owner: "aas-v1-independent-verifier" },
|
|
environment: { ...identity, nodeExecutableSha256: executableDigest() },
|
|
observer: { contractVersion: observer.contractVersion, backend: observer.backend, selfTestDigest: observer.selfTestDigest, networkSentinels: observer.observedNetworkSentinels, writeSentinels: observer.observedWriteSentinels, overflow: false, ambiguousLineage: false },
|
|
zones: Object.fromEntries(Object.keys(zones).map((name) => [name, { beforeSha256: mcpBefore[name], afterSha256: mcpAfter[name], persistentWriteCount: 0 }])),
|
|
suites: suites.sort((a, b) => SUITE_IDS.indexOf(a.id) - SUITE_IDS.indexOf(b.id)),
|
|
canonicalPayload: { sha256: canonicalSha, excludedFields: ["timestamp", "correlationId", "localizedMessage", "diagnostics"], sampleCount: 60 },
|
|
failures,
|
|
};
|
|
writeCanonicalReceipt(output, receipt);
|
|
process.stdout.write(`${JSON.stringify({ ok: failures.length === 0, output, failures })}\n`);
|
|
if (failures.length) process.exit(1);
|