Files
ThothII/backend/scripts/p5-acceptance.mjs
T

1354 lines
58 KiB
JavaScript

#!/usr/bin/env node
import { createHash, randomBytes } from "node:crypto";
import { execFile, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { createServer } from "node:http";
import { closeSync, constants as fsConstants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
import { access, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import net from "node:net";
import process from "node:process";
import { stringify as yamlStringify } from "yaml";
import { buildSafeEnvironment, deriveOverall, scanSecrets } from "./p1-acceptance.mjs";
const execFileAsync = promisify(execFile);
const modulePath = fileURLToPath(import.meta.url);
const defaultRepositoryRoot = realpathSync(resolve(dirname(modulePath), "../.."));
const RUN_ID = /^p5-[0-9a-f]{32}$/;
const HEX32 = /^[0-9a-f]{32}$/;
const HEX40 = /^[0-9a-f]{40}$/;
const HEX64 = /^[0-9a-f]{64}$/;
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const COMMAND = /^[a-z0-9][a-z0-9-]*$/;
const CHECK_RESULT_STATUS = new Set(["PASS", "FAIL"]);
const CHECK_IDS = Object.freeze([
"preflight",
"clean_state",
"ownership",
"activation_sync",
"accept_happy_path",
"revision_isolation",
"accept_negatives",
"continuation_gate",
"secret_scan",
"cleanup_confinement",
]);
const TOPOLOGY = [
"remote.git",
"author",
"installation",
"installation/data",
"installation/data/sessions",
"installation/registry",
"installation/pi-state",
"fixture-secrets",
"fixtures",
"fixtures/logs",
"logs",
];
const MAX_REPORT_JSON_BYTES = 64 * 1024;
const MAX_REPORT_MD_BYTES = 32 * 1024;
const MAX_STDIO_BYTES = 512 * 1024;
const MAX_SECRET_SCAN_VIRTUAL_BYTES = 256 * 1024;
function nowIso() { return new Date().toISOString(); }
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
function assert(condition, message) { if (!condition) throw new Error(message); }
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
function canonicalRoot(repositoryRoot = defaultRepositoryRoot) {
return realpathSync(repositoryRoot);
}
export function canonicalIntegrationBase(repositoryRoot = defaultRepositoryRoot) {
return join(canonicalRoot(repositoryRoot), ".artifacts", "p5-integration");
}
export function validateRunRoot(repositoryRoot, runRoot, runId) {
if (!RUN_ID.test(runId)) throw new Error("invalid owned run id");
const base = canonicalIntegrationBase(repositoryRoot);
const lexical = resolve(runRoot);
if (dirname(lexical) !== base || basename(lexical) !== runId) throw new Error("run root is not a direct integration child");
return lexical;
}
function validateNoSymlinkAncestors(repositoryRoot, target) {
const repo = canonicalRoot(repositoryRoot);
const rel = relative(repo, target);
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("path leaves repository");
let cursor = repo;
for (const part of rel.split(sep).filter(Boolean)) {
cursor = join(cursor, part);
if (!existsSync(cursor)) break;
const entry = lstatSync(cursor);
if (entry.isSymbolicLink()) throw new Error("owned path ancestor is a symlink");
}
}
async function atomicWrite(path, bytes, mode = 0o600) {
await mkdir(dirname(path), { recursive: true });
const staging = join(dirname(path), `.${basename(path)}.${randomBytes(12).toString("hex")}.tmp`);
let handle;
try {
handle = await open(staging, "wx", mode);
await handle.writeFile(bytes);
await handle.sync();
await handle.close();
handle = undefined;
await rename(staging, path);
const directory = openSync(dirname(path), fsConstants.O_RDONLY);
try { fsyncSync(directory); } finally { closeSync(directory); }
} catch (error) {
if (handle) await handle.close().catch(() => {});
await rm(staging, { force: true }).catch(() => {});
throw error;
}
}
function initialResources(run) {
return [
run.root,
join(run.root, "remote.git"),
join(run.root, "author"),
join(run.root, "installation"),
join(run.root, "installation", "registry"),
join(run.root, "installation", "data"),
join(run.root, "fixture-secrets"),
];
}
function ownershipValue(run) {
return {
schemaVersion: 1,
kind: "p5-acceptance",
runId: run.runId,
runNonce: run.nonce,
root: run.root,
repositoryRoot: run.repositoryRoot,
startedAt: run.startedAt,
pid: run.pid,
resources: initialResources(run),
};
}
async function writeOwnership(run) {
await atomicWrite(join(run.root, "ownership.json"), `${JSON.stringify(ownershipValue(run), null, 2)}\n`);
}
export async function createOwnedRun({ repositoryRoot = defaultRepositoryRoot, runId, nonce, now, pid } = {}) {
const repo = canonicalRoot(repositoryRoot);
const base = canonicalIntegrationBase(repo);
validateNoSymlinkAncestors(repo, base);
await mkdir(join(repo, ".artifacts"), { mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST") throw error; });
await mkdir(base, { mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST") throw error; });
const id = runId ?? `p5-${randomBytes(16).toString("hex")}`;
const root = validateRunRoot(repo, join(base, id), id);
const run = {
repositoryRoot: repo,
root,
runId: id,
nonce: nonce ?? randomBytes(32).toString("hex"),
startedAt: now ?? nowIso(),
pid: pid ?? process.pid,
};
if (!HEX64.test(run.nonce) || !ISO_UTC.test(run.startedAt)) throw new Error("invalid ownership identity");
await mkdir(root, { mode: 0o700 });
await writeOwnership(run);
return run;
}
function strictOwnership(value, run, expectedNonce) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("ownership is malformed");
if (value.schemaVersion !== 1 || value.kind !== "p5-acceptance" || value.runId !== run.runId || value.runNonce !== expectedNonce
|| value.root !== run.root || value.repositoryRoot !== run.repositoryRoot || value.pid !== process.pid
|| !ISO_UTC.test(value.startedAt ?? "")
|| JSON.stringify(value.resources) !== JSON.stringify(initialResources(run))) throw new Error("ownership identity mismatch");
return value;
}
export async function readAndValidateOwnership({ repositoryRoot = defaultRepositoryRoot, runRoot, expectedNonce }) {
const repo = canonicalRoot(repositoryRoot);
const id = basename(resolve(runRoot));
const lexical = validateRunRoot(repo, runRoot, id);
const rootEntry = await lstat(lexical);
if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) throw new Error("owned run root is not a directory");
const ownershipPath = join(lexical, "ownership.json");
const ownershipEntry = await lstat(ownershipPath);
if (!ownershipEntry.isFile() || ownershipEntry.isSymbolicLink()) throw new Error("ownership file is unsafe");
let value;
try { value = JSON.parse(await readFile(ownershipPath, "utf8")); } catch { throw new Error("ownership is malformed"); }
return strictOwnership(value, { repositoryRoot: repo, root: lexical, runId: id }, expectedNonce);
}
export async function cleanupOwnedRun({ repositoryRoot = defaultRepositoryRoot, runRoot, expectedNonce }) {
const value = await readAndValidateOwnership({ repositoryRoot, runRoot, expectedNonce });
const base = canonicalIntegrationBase(repositoryRoot);
const tombstone = join(base, `.deleting-${value.runId}-${expectedNonce.slice(0, 16)}`);
await rename(runRoot, tombstone);
await rm(tombstone, { recursive: true, force: false });
}
async function finalizeOwnedRun({ run, success, keep }) {
if (!success || keep) return false;
await cleanupOwnedRun({ repositoryRoot: run.repositoryRoot, runRoot: run.root, expectedNonce: run.nonce });
return true;
}
function safeArtifactPath(path) {
if (typeof path !== "string" || path.length === 0 || path.length > 255 || path.startsWith("/") || path.includes("..") || path.includes("\\") || /[\0\r\n]/.test(path)) {
throw new Error("report artifact path is invalid");
}
return path;
}
function hasExactCheckIds(checks) {
return checks.length === CHECK_IDS.length && checks.every(({ id }, index) => id === CHECK_IDS[index]);
}
export function validateReport(report) {
if (!report || report.schemaVersion !== 1 || !RUN_ID.test(report.runId ?? "") || !ISO_UTC.test(report.startedAt ?? "")
|| !ISO_UTC.test(report.finishedAt ?? "") || typeof report.command !== "string"
|| !Array.isArray(report.checks) || !hasExactCheckIds(report.checks)) throw new Error("report is invalid");
const ids = new Set();
const artifactPaths = new Set();
for (const check of report.checks) {
if (!check || !/^[a-z0-9_]+$/.test(check.id ?? "") || ids.has(check.id) || !CHECK_RESULT_STATUS.has(check.status)
|| !ISO_UTC.test(check.startedAt ?? "") || !ISO_UTC.test(check.finishedAt ?? "")
|| !Array.isArray(check.commands) || check.commands.some((name) => !COMMAND.test(name))
|| !Array.isArray(check.artifacts)) throw new Error("report check is invalid");
ids.add(check.id);
for (const artifact of check.artifacts) {
safeArtifactPath(artifact.path);
if (!HEX64.test(artifact.sha256 ?? "")) throw new Error("report check is invalid");
if (artifactPaths.has(artifact.path)) throw new Error("report artifact path is duplicated");
artifactPaths.add(artifact.path);
}
}
if (report.overall !== deriveOverall(report.checks)) throw new Error("report overall is not derived");
return report;
}
function renderReportMarkdown(report) {
validateReport(report);
const rows = report.checks.map((check) => `| ${check.id} | ${check.status} |`).join("\n");
return [
"# P5 acceptance report",
"",
`Run: \`${report.runId}\``,
"",
"| Check | Status |",
"|---|---|",
rows,
"",
`P5 automated integration: ${report.overall}`,
"P5 manual acceptance: PENDING",
"",
].join("\n");
}
async function walkFiles(root) {
const files = [];
async function visit(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
const rel = relative(root, path).split(sep).join("/");
if (entry.isSymbolicLink()) throw new Error(`unsafe file tree: ${rel}`);
if (entry.isDirectory()) await visit(path);
else if (entry.isFile()) files.push({ path, rel });
}
}
if (existsSync(root)) await visit(root);
files.sort((a, b) => a.rel.localeCompare(b.rel));
return files;
}
async function snapshotDigest(root, excludedPrefixes = []) {
const result = {};
for (const file of await walkFiles(root)) {
if (excludedPrefixes.some((prefix) => file.rel === prefix || file.rel.startsWith(`${prefix}/`))) continue;
result[file.rel] = sha256(await readFile(file.path));
}
return result;
}
async function fileArtifact(root, relativePath) {
const bytes = await readFile(join(root, relativePath));
return { path: relativePath.split(sep).join("/"), sha256: sha256(bytes) };
}
async function writeJson(path, value) {
await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`);
}
async function writeReportFiles({ run, report }) {
validateReport(report);
const reportJsonPath = join(run.root, "report.json");
const reportMdPath = join(run.root, "report.md");
const reportMd = renderReportMarkdown(report);
if (Buffer.byteLength(JSON.stringify(report)) > MAX_REPORT_JSON_BYTES) throw new Error("report.json exceeds bound");
if (Buffer.byteLength(reportMd) > MAX_REPORT_MD_BYTES) throw new Error("report.md exceeds bound");
await writeJson(reportJsonPath, report);
await atomicWrite(reportMdPath, reportMd, 0o600);
return {
reportJson: await fileArtifact(run.root, "report.json"),
reportMd: await fileArtifact(run.root, "report.md"),
};
}
function resolveSystemExecutable(name) {
for (const candidate of [`/usr/bin/${name}`, `/bin/${name}`, `/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`]) {
try {
const resolved = realpathSync(candidate);
if (statSync(resolved).isFile()) return resolved;
} catch {}
}
throw new Error(`required executable not found: ${name}`);
}
function scalarSecretBytes(value) {
if (typeof value !== "string" || value.length === 0 || /\s|\0/.test(value)) throw new Error("scalar fixture secret is invalid");
return Buffer.from(value);
}
async function manifestFiles(root, paths) {
const files = [];
const visit = async (absolute, rel) => {
const entry = await lstat(absolute);
if (entry.isSymbolicLink()) throw new Error(`provenance path is a symlink: ${rel}`);
if (entry.isDirectory()) {
for (const child of (await readdir(absolute, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
await visit(join(absolute, child.name), rel ? `${rel}/${child.name}` : child.name);
}
} else if (entry.isFile()) {
const bytes = await readFile(absolute);
files.push({ path: rel, bytes: bytes.length, sha256: sha256(bytes) });
} else throw new Error(`provenance path is not a regular file: ${rel}`);
};
for (const path of paths) await visit(join(root, path), path);
files.sort((a, b) => a.path.localeCompare(b.path));
return { files, manifestSha256: sha256(JSON.stringify(files)) };
}
async function collectRepositoryProvenance({ repositoryRoot, gitPath = resolveSystemExecutable("git") }) {
const repo = canonicalRoot(repositoryRoot);
const safeEnv = buildSafeEnvironment({ ambient: {}, fixture: { PATH: `${dirname(gitPath)}:/usr/bin:/bin`, HOME: repo, TMPDIR: join(repo, ".artifacts") } });
const run = async (argv) => await execFileAsync(gitPath, ["-C", repo, ...argv], { env: safeEnv, maxBuffer: MAX_STDIO_BYTES });
const beforeHead = (await run(["rev-parse", "HEAD"]).catch((error) => { throw error; })).stdout.trim();
const beforeTree = (await run(["rev-parse", "HEAD^{tree}"])).stdout.trim();
const beforeStatus = (await run(["status", "--porcelain=v1", "--untracked-files=no"])).stdout;
if (!HEX40.test(beforeHead) || !HEX40.test(beforeTree) || beforeStatus !== "") throw new Error("repository is not clean at exact HEAD");
const backendRoot = join(repo, "backend");
const backendSource = await manifestFiles(backendRoot, [
"src",
"scripts/p5-acceptance.mjs",
"package.json",
"package-lock.json",
"tsconfig.json",
]);
const backendDist = existsSync(join(backendRoot, "dist")) ? await manifestFiles(backendRoot, ["dist"]) : { files: [], manifestSha256: sha256("[]") };
const afterHead = (await run(["rev-parse", "HEAD"]).catch((error) => { throw error; })).stdout.trim();
const afterTree = (await run(["rev-parse", "HEAD^{tree}"])).stdout.trim();
const afterStatus = (await run(["status", "--porcelain=v1", "--untracked-files=no"])).stdout;
if (afterHead !== beforeHead || afterTree !== beforeTree || afterStatus !== beforeStatus) throw new Error("repository provenance changed during binding");
return { schemaVersion: 1, head: beforeHead, tree: beforeTree, clean: true, backendSource, backendDist };
}
async function createTopology(run) {
for (const path of TOPOLOGY) await mkdir(join(run.root, path), { recursive: true, mode: path === "fixture-secrets" ? 0o700 : 0o755 });
}
async function allocatePort() {
const server = net.createServer();
await new Promise((resolve, reject) => server.listen(0, "127.0.0.1", resolve).on("error", reject));
const port = server.address().port;
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
return port;
}
function installationProjectName(installationPath) {
return `thothii-${sha256(installationPath).slice(0, 12)}`;
}
function baseWorkspace(id, { dwhBaseUrl, evidenceSource }) {
return {
workspace: { schema_version: 4, id, name: `P2 ${id}`, language: "en" },
dwh: { engine: "postgres", database: "warehouse", schema: "dw", supported_transports: ["rest_api"] },
diagnostics: {
dwh_rest: { method: "POST", path: "/rpc/ping", auth: "x-api-key", response: { database: "database", schema: "schema" } },
},
...(evidenceSource ? { evidence: { source: evidenceSource, policy: { max_chunk_chars: 4000, retain_published_generations: 3 } } } : {}),
};
}
function namespace(id) { return id.toUpperCase().replaceAll("-", "_"); }
function descriptorYaml(obj) {
return yamlStringify(obj, { lineWidth: 0, sortMapEntries: false });
}
async function setupSecrets(ctx) {
const secretDir = join(ctx.run.root, "fixture-secrets");
const values = {
dwhToken: `P2-DWH-${randomBytes(16).toString("hex")}`,
signedToken: `P2-SIGNED-${randomBytes(16).toString("hex")}`,
bundle: `P2-BUNDLE-${randomBytes(16).toString("hex")}`,
};
ctx.forbiddenValues = Object.values(values);
ctx.secretValues = values;
const paths = {
dwh: join(secretDir, "p2-dwh-api-key"),
filesystemDwh: join(secretDir, "p2-filesystem-api-key"),
signed: join(secretDir, "p2-dwh-evidence-signed-urls.json"),
bundle: join(secretDir, "thothii.secrets"),
};
await atomicWrite(paths.dwh, scalarSecretBytes(values.dwhToken));
await atomicWrite(paths.filesystemDwh, scalarSecretBytes(values.dwhToken));
await atomicWrite(paths.bundle, scalarSecretBytes(values.bundle));
ctx.secretPaths = paths;
}
async function setupFixtures(ctx) {
ctx.fixturePorts = {
dwh: await allocatePort(),
evidence: await allocatePort(),
embedding: await allocatePort(),
qdrant: await allocatePort(),
};
const dwhBaseUrl = `http://host.docker.internal:${ctx.fixturePorts.dwh}`;
const evidenceProvenance = `http://host.docker.internal:${ctx.fixturePorts.evidence}/p2-dwh/guide.md`;
ctx.workspaceObjects = {
dwh: baseWorkspace("p2-dwh", {
dwhBaseUrl,
evidenceSource: {
type: "http",
uris: [evidenceProvenance],
authentication: "signed_urls_file",
connect_timeout_ms: 1250,
read_timeout_ms: 30001,
max_bytes: 65536,
max_redirects: 2,
allow_private_hosts: true,
max_cache_bytes: 65536,
},
}),
filesystem: baseWorkspace("p2-filesystem", {
dwhBaseUrl,
evidenceSource: {
type: "filesystem",
uri: "p2-filesystem/evidence",
patterns: ["**/*.md"],
max_bytes: 1048576,
},
}),
};
const signedUrl = `${evidenceProvenance}?token=${ctx.secretValues.signedToken}`;
await atomicWrite(ctx.secretPaths.signed, `${JSON.stringify([signedUrl], null, 2)}\n`);
ctx.curatedAnnotations = {
"p2-dwh": "tables: {}\n",
"p2-filesystem": "tables: {}\n",
};
ctx.evidenceState = {
content: "# P2 Evidence\n\nFirst generation.\n",
token: ctx.secretValues.signedToken,
};
ctx.dwhState = {
tables: {
patients: {
comment: "Patients",
rows: [
{ patient_id: "p1", name: "Alice" },
{ patient_id: "p2", name: "Bob" },
],
},
visits: {
comment: "Visits",
rows: [
{ id: "v1", patient_id: "p1", note: "checkup" },
{ id: "v2", patient_id: "p2", note: "xray" },
],
},
labs: {
comment: "Labs",
rows: [
{ id: "l1", patient_id: "p1", code: "hemoglobin" },
{ id: "l2", patient_id: "p2", code: "glucose" },
],
},
},
token: ctx.secretValues.dwhToken,
};
}
function inferColumnType(value) {
return typeof value === "number" ? "integer" : "text";
}
function topValues(rows, column, limit) {
const counts = new Map();
for (const row of rows) {
const value = row[column];
if (value === undefined || value === null || value === "") continue;
counts.set(String(value), (counts.get(String(value)) ?? 0) + 1);
}
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([value]) => ({ value }));
}
async function startHttpServer({ port, handler }) {
const server = createServer(async (req, res) => {
try {
await handler(req, res);
} catch {
res.statusCode = 500;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ error: "fixture failed" }));
}
});
await new Promise((resolve, reject) => server.listen(port, "127.0.0.1", () => resolve()).on("error", reject));
return server;
}
async function startServers(ctx) {
const dwhServer = await startHttpServer({
port: ctx.fixturePorts.dwh,
handler: async (req, res) => {
const body = await new Promise((resolve) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
const json = body.length === 0 ? {} : JSON.parse(body);
if (req.headers["x-api-key"] !== ctx.dwhState.token) {
res.statusCode = 401;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ message: "unauthorized" }));
return;
}
const send = (payload) => {
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(payload));
};
const url = new URL(req.url, `http://127.0.0.1:${ctx.fixturePorts.dwh}`);
if (req.method !== "POST" || !url.pathname.startsWith("/rpc/")) {
res.statusCode = 404;
res.end(JSON.stringify({ message: "not found" }));
return;
}
const fn = url.pathname.slice("/rpc/".length);
const schemaName = json.schema_name ?? "dw";
if (schemaName !== "dw") {
send([]);
return;
}
if (fn === "ping") {
send({ db_connected: true, schema_accessible: true, database: "warehouse", schema: "dw" });
return;
}
const table = typeof json.table_name === "string" ? json.table_name : "";
const tableData = ctx.dwhState.tables[table];
if (fn === "list_tables") {
send(Object.entries(ctx.dwhState.tables).map(([name, info]) => ({ table: name, type: "TABLE", comment: info.comment, rows: info.rows.length })));
return;
}
if (!tableData) {
send([]);
return;
}
if (fn === "table_columns") {
const first = tableData.rows[0] ?? {};
send(Object.keys(first).map((column) => ({
column,
type: inferColumnType(first[column]),
nullable: false,
// Only the referenced table marks `patient_id` as primary, so the SQL miner sees a
// PK/non-PK pair while the same-name heuristic still discovers joins from the others.
pk: column === "id" || (table === "patients" && column === "patient_id"),
default: null,
})));
return;
}
if (fn === "table_comments") {
send(Object.keys(tableData.rows[0] ?? {}).map((column) => ({ object: "COLUMN", name: column, comment: `${table}.${column}` })));
return;
}
if (fn === "table_foreign_keys") {
send([]);
return;
}
if (fn === "top_values") {
send(topValues(tableData.rows, json.column_name, Number(json.max_values ?? 10)));
return;
}
if (fn === "column_stats") {
send({});
return;
}
if (fn === "run_query") {
send([]);
return;
}
if (fn === "explain_query") {
send([{ line: "Seq Scan" }]);
return;
}
res.statusCode = 404;
res.end(JSON.stringify({ message: "unknown rpc" }));
},
});
const evidenceServer = await startHttpServer({
port: ctx.fixturePorts.evidence,
handler: async (req, res) => {
const url = new URL(req.url, `http://127.0.0.1:${ctx.fixturePorts.evidence}`);
if (url.pathname !== "/p2-dwh/guide.md" || url.searchParams.get("token") !== ctx.evidenceState.token) {
res.statusCode = 403;
res.end("forbidden");
return;
}
res.statusCode = 200;
res.setHeader("content-type", "text/markdown; charset=utf-8");
res.end(ctx.evidenceState.content);
},
});
const embeddingServer = await startHttpServer({
port: ctx.fixturePorts.embedding,
handler: async (req, res) => {
const body = await new Promise((resolve) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
const url = new URL(req.url, `http://127.0.0.1:${ctx.fixturePorts.embedding}`);
if (req.method !== "POST" || url.pathname !== "/api/embed") {
res.statusCode = 404;
res.end(JSON.stringify({ error: "not found" }));
return;
}
const payload = JSON.parse(body || "{}");
const inputs = Array.isArray(payload.input) ? payload.input : [];
const embeddings = inputs.map((text) => {
const seed = sha256(String(text));
return Array.from({ length: 1024 }, (_, index) => {
const offset = (index * 2) % seed.length;
const value = Number.parseInt(seed.slice(offset, offset + 2), 16);
return (value / 255) - 0.5;
});
});
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ model: payload.model, embeddings }));
},
});
ctx.servers = [dwhServer, evidenceServer, embeddingServer];
}
async function stopServers(ctx) {
for (const server of ctx.servers ?? []) {
await new Promise((resolve) => server.close(() => resolve()));
}
ctx.servers = [];
}
async function git(ctx, args, cwd = join(ctx.run.root, "author")) {
return await runCommand({ executable: ctx.executables.gitPath, argv: args, cwd, env: ctx.execEnv });
}
async function initializeGitAndRegistry(ctx) {
const author = join(ctx.run.root, "author");
await git(ctx, ["init", "--bare", "--initial-branch=main", join(ctx.run.root, "remote.git")], ctx.run.root);
await git(ctx, ["clone", join(ctx.run.root, "remote.git"), author], ctx.run.root);
await git(ctx, ["config", "user.name", "P2 Fixture Curator"], author);
await git(ctx, ["config", "user.email", "p5-curator@example.invalid"], author);
const writeWorkspaces = async () => {
const catalog = {
schema_version: 1,
workspaces: [
{ id: "p2-dwh", name: ctx.workspaceObjects.dwh.workspace.name },
{ id: "p2-filesystem", name: ctx.workspaceObjects.filesystem.workspace.name },
],
};
await writeFile(join(author, "thoth-workspaces.yaml"), yamlStringify(catalog, { lineWidth: 0, sortMapEntries: false }));
for (const [id, workspace] of Object.entries(ctx.workspaceObjects)) {
const pathId = workspace.workspace.id;
await mkdir(join(author, pathId), { recursive: true });
const yaml = descriptorYaml(workspace);
await writeFile(join(author, pathId, "workspace.yaml"), yaml);
const docs = ctx.workspaceModules.renderWorkspaceDocs(workspace);
await mkdir(join(author, "workspace-docs", pathId), { recursive: true });
await writeFile(join(author, "workspace-docs", pathId, "contract.env.example"), docs.envExample);
await writeFile(join(author, "workspace-docs", pathId, "README.md"), docs.markdown);
}
for (const [id, workspace] of Object.entries(ctx.workspaceObjects)) {
const pathId = workspace.workspace.id;
await mkdir(join(author, pathId, "schema"), { recursive: true });
await writeFile(join(author, pathId, "schema", "annotations.yaml"), ctx.curatedAnnotations[pathId]);
}
await mkdir(join(author, "p2-filesystem", "evidence"), { recursive: true });
await writeFile(join(author, "p2-filesystem", "evidence", "guide.md"), "# P2 Filesystem Evidence\n\nCommitted fixture.\n");
};
await writeWorkspaces();
await git(ctx, ["add", "."], author);
await git(ctx, ["commit", "-m", "Bootstrap P2 fixtures"], author);
await git(ctx, ["push", "origin", "main"], author);
ctx.registryCommit = (await git(ctx, ["rev-parse", "HEAD"], author)).stdout.trim();
const registry = new ctx.workspaceModules.WorkspaceRegistry({
root: join(ctx.run.root, "installation", "registry"),
remoteUrl: join(ctx.run.root, "remote.git"),
branch: "main",
gitAuthorName: "P2 Acceptance",
gitAuthorEmail: "p5-acceptance@example.invalid",
installationId: "p5-acceptance",
secretRoots: [join(ctx.run.root, "fixture-secrets")],
maxImportBytes: 16 * 1024 * 1024,
maxImportEntries: 1024,
dataRoot: join(ctx.run.root, "installation", "data"),
});
await registry.bootstrap();
ctx.registry = registry;
}
async function mutateWorkspaceDescriptor(ctx, workspaceId, mutator, commitMessage) {
const author = join(ctx.run.root, "author");
// The registry may have produced docs-only follow-up commits on the remote; the curator
// always rebases onto the latest remote head before committing so the push stays fast-forward.
await git(ctx, ["fetch", "origin", "main"], author);
await git(ctx, ["reset", "--hard", "origin/main"], author);
const workspace = structuredClone(ctx.workspaceObjects[workspaceId === "p2-dwh" ? "dwh" : "filesystem"]);
mutator(workspace);
ctx.workspaceObjects[workspaceId === "p2-dwh" ? "dwh" : "filesystem"] = workspace;
await writeFile(join(author, workspaceId, "workspace.yaml"), descriptorYaml(workspace));
const docs = ctx.workspaceModules.renderWorkspaceDocs(workspace);
const docsDir = join(author, "workspace-docs", workspaceId);
await mkdir(docsDir, { recursive: true, mode: 0o700 });
await writeFile(join(docsDir, "contract.env.example"), docs.envExample);
await writeFile(join(docsDir, "README.md"), docs.markdown);
await git(ctx, ["add", `${workspaceId}/workspace.yaml`, `workspace-docs/${workspaceId}/contract.env.example`, `workspace-docs/${workspaceId}/README.md`], author);
await git(ctx, ["commit", "-m", commitMessage], author);
await git(ctx, ["push", "origin", "main"], author);
await ctx.registry.pull();
ctx.registryCommit = (await git(ctx, ["rev-parse", "HEAD"], author)).stdout.trim();
}
async function mutateWorkspaceAnnotations(ctx, workspaceId, contents, commitMessage) {
const author = join(ctx.run.root, "author");
await git(ctx, ["fetch", "origin", "main"], author);
await git(ctx, ["reset", "--hard", "origin/main"], author);
const annotationsPath = join(author, workspaceId, "schema", "annotations.yaml");
await mkdir(dirname(annotationsPath), { recursive: true });
await writeFile(annotationsPath, contents);
ctx.curatedAnnotations[workspaceId] = contents;
await git(ctx, ["add", `${workspaceId}/schema/annotations.yaml`], author);
await git(ctx, ["commit", "-m", commitMessage], author);
await git(ctx, ["push", "origin", "main"], author);
await ctx.registry.pull();
ctx.registryCommit = (await git(ctx, ["rev-parse", "HEAD"], author)).stdout.trim();
}
async function writeInstallationFiles(ctx) {
const installationDir = join(ctx.run.root, "installation");
const operatorEnvPath = join(installationDir, "operator.env");
const bindingsEnvPath = join(installationDir, "workspace-bindings.env");
const connectorOverridePath = join(installationDir, "connector-secrets.override.yaml");
const fixtureOverridePath = join(installationDir, "fixture.override.yaml");
const installationPath = join(installationDir, "thothii-installation.yaml");
ctx.installationPath = installationPath;
ctx.composeProject = installationProjectName(installationPath);
const qdrantPort = ctx.fixturePorts.qdrant;
const bindings = [
`THT_WS_P2_DWH_DWH_TRANSPORT=rest_api`,
`THT_WS_P2_DWH_DWH_BASE_URL=http://host.docker.internal:${ctx.fixturePorts.dwh}`,
`THT_WS_P2_DWH_DWH_API_KEY_FILE=/run/secrets/p2-dwh-api-key`,
`THT_WS_P2_DWH_EVIDENCE_SIGNED_URLS_FILE=/run/secrets/p2-dwh-evidence-signed-urls`,
`THT_WS_P2_FILESYSTEM_DWH_TRANSPORT=rest_api`,
`THT_WS_P2_FILESYSTEM_DWH_BASE_URL=http://host.docker.internal:${ctx.fixturePorts.dwh}`,
`THT_WS_P2_FILESYSTEM_DWH_API_KEY_FILE=/run/secrets/p2-filesystem-api-key`,
].join("\n") + "\n";
await atomicWrite(bindingsEnvPath, bindings);
const operatorEnv = [
`THT_DATA_ROOT=${join(ctx.run.root, "installation", "data")}`,
`THT_WORKSPACE_REGISTRY_ROOT=${join(ctx.run.root, "installation", "registry")}`,
`THT_PI_STATE_ROOT=${join(ctx.run.root, "installation", "pi-state")}`,
`PI_AUTH_FILE=${join(ctx.run.root, "installation", "pi-auth.json")}`,
`THT_SECRETS_FILE=${ctx.secretPaths.bundle}`,
`THT_WORKSPACE_BINDINGS_ENV_FILE=${bindingsEnvPath}`,
`THT_WORKSPACE_GIT_REMOTE=${join(ctx.run.root, "remote.git")}`,
`THT_WORKSPACE_GIT_BRANCH=main`,
`THT_WORKSPACE_GIT_AUTHOR_NAME=P2 Acceptance`,
`THT_WORKSPACE_GIT_AUTHOR_EMAIL=p5-acceptance@example.invalid`,
`THT_WORKSPACE_INSTALLATION_ID=p5-acceptance`,
`THT_DB_NAME=warehouse`,
`THT_DWH_REST_URL=http://host.docker.internal:${ctx.fixturePorts.dwh}`,
`THT_LLM_URL=http://127.0.0.1:9`,
`THOTH_SERVER_BIND=127.0.0.1`,
`THOTH_HTTP_PORT=18080`,
`THOTH_CORE_HTTP_PORT=18787`,
`THT_WS_P2_DWH_DWH_API_KEY_SOURCE=${ctx.secretPaths.dwh}`,
`THT_WS_P2_DWH_EVIDENCE_SIGNED_URLS_SOURCE=${ctx.secretPaths.signed}`,
`THT_WS_P2_FILESYSTEM_DWH_API_KEY_SOURCE=${ctx.secretPaths.filesystemDwh}`,
`THT_EVIDENCE_PRIVATE_HOST_ALLOWLIST=host.docker.internal`,
].join("\n") + "\n";
await atomicWrite(operatorEnvPath, operatorEnv);
await atomicWrite(join(ctx.run.root, "installation", "pi-auth.json"), JSON.stringify({ fixture: true }));
const embeddingStubPath = join(installationDir, "embedding-stub.py");
await atomicWrite(embeddingStubPath, EMBEDDING_STUB_SOURCE);
const override = {
services: {
core: {
image: ctx.coreImageTag,
extra_hosts: ["host.docker.internal:host-gateway"],
},
"workspace-maintenance": {
image: ctx.coreImageTag,
environment: {
THT_EVIDENCE_PRIVATE_HOST_ALLOWLIST: "host.docker.internal",
},
extra_hosts: ["host.docker.internal:host-gateway"],
},
qdrant: {
ports: [`127.0.0.1:${qdrantPort}:6333`],
restart: "no",
},
// Deterministic Ollama-compatible embedding fixture on the internal allowlisted host
// name `embedding` (http://embedding:11434). Replaces the real Ollama service entirely.
embedding: {
image: ctx.coreImageTag,
entrypoint: ["python3", "/stub.py"],
volumes: [
{ type: "bind", source: embeddingStubPath, target: "/stub.py", read_only: true },
],
healthcheck: { disable: true },
},
},
};
await atomicWrite(fixtureOverridePath, yamlStringify(override, { lineWidth: 0, sortMapEntries: false }));
const generated = await runCommand({
executable: join(ctx.repositoryRoot, "scripts", "generate-connector-secrets-override.sh"),
argv: [
"--bindings-env", bindingsEnvPath,
"--operator-env", operatorEnvPath,
"--output", connectorOverridePath,
"--service", "workspace-maintenance",
"--role", "all",
],
env: ctx.execEnv,
});
if (generated.exitCode !== 0) throw new Error(`connector override generation failed: ${generated.stderr || generated.stdout}`);
const installation = {
profile: "server",
projectDirectory: ctx.repositoryRoot,
envFile: operatorEnvPath,
overrides: [
join(ctx.repositoryRoot, "deploy", "compose.server.yaml"),
fixtureOverridePath,
connectorOverridePath,
],
};
await atomicWrite(installationPath, yamlStringify(installation, { lineWidth: 0, sortMapEntries: false }));
ctx.installation = installation;
}
function thothctlBinaryPath(repositoryRoot) {
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform] ?? "linux";
const arch = { x64: "amd64", arm64: "arm64" }[process.arch] ?? "amd64";
const suffix = platform === "windows" ? ".exe" : "";
const candidates = [
join(repositoryRoot, "dist", "thothctl", `thothctl-${platform}-${arch}${suffix}`),
join(repositoryRoot, "tools", "thothctl", "bin", `thothctl${suffix}`),
];
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
throw new Error("built thothctl binary is unavailable");
}
async function runCommand({ executable, argv = [], cwd, env, input, maxOutputBytes = MAX_STDIO_BYTES }) {
const result = await execFileAsync(executable, argv, {
cwd,
env,
encoding: "utf8",
maxBuffer: maxOutputBytes,
...(input === undefined ? {} : { input }),
}).then(
({ stdout, stderr }) => ({ exitCode: 0, stdout, stderr }),
(error) => ({ exitCode: error.code ?? 1, stdout: error.stdout ?? "", stderr: error.stderr ?? error.message ?? "" }),
);
return result;
}
async function buildCoreImage(ctx) {
const tag = `thothii-core:p2-${ctx.run.runId.slice(3, 15)}`;
ctx.coreImageTag = tag;
const build = await runCommand({
executable: ctx.executables.dockerPath,
argv: ["build", "-f", join(ctx.repositoryRoot, "docker", "core.Dockerfile"), "-t", tag, ctx.repositoryRoot],
env: { ...ctx.execEnv, DOCKER_BUILDKIT: "1" },
maxOutputBytes: 4 * 1024 * 1024,
});
if (build.exitCode !== 0) throw new Error(`core image build failed: ${build.stderr || build.stdout}`);
}
async function buildThothctl(ctx) {
const command = await runCommand({
executable: join(ctx.repositoryRoot, "scripts", "build-thothctl.sh"),
argv: [],
env: { ...ctx.execEnv, THT_THOTHCTL_OUTPUT_DIRECTORY: join(ctx.repositoryRoot, "dist", "thothctl") },
maxOutputBytes: 4 * 1024 * 1024,
});
if (command.exitCode !== 0) throw new Error(`build-thothctl failed: ${command.stderr || command.stdout}`);
ctx.thothctlPath = thothctlBinaryPath(ctx.repositoryRoot);
}
function composeBaseArgs(ctx) {
const args = [
"compose",
"--project-name", ctx.composeProject,
"--project-directory", ctx.installation.projectDirectory,
"--env-file", ctx.installation.envFile,
"-f", join(ctx.repositoryRoot, "compose.yaml"),
];
for (const override of ctx.installation.overrides) args.push("-f", override);
return args;
}
async function dockerCompose(ctx, commandArgs, { allowFailure = false, maxOutputBytes = 2 * 1024 * 1024 } = {}) {
const result = await runCommand({
executable: ctx.executables.dockerPath,
argv: [...composeBaseArgs(ctx), ...commandArgs],
env: ctx.execEnv,
maxOutputBytes,
});
if (!allowFailure && result.exitCode !== 0) throw new Error(`docker compose ${commandArgs.join(" ")} failed: ${result.stderr || result.stdout}`);
return result;
}
async function startQdrant(ctx) {
await dockerCompose(ctx, ["up", "-d", "qdrant", "embedding"]);
for (let attempt = 0; attempt < 60; attempt += 1) {
try {
const response = await fetch(`http://127.0.0.1:${ctx.fixturePorts.qdrant}/collections`);
if (response.ok) return;
} catch {}
await sleep(1000);
}
throw new Error("qdrant did not become ready");
}
async function qdrantJson(ctx, method, path, body) {
const response = await fetch(`http://127.0.0.1:${ctx.fixturePorts.qdrant}${path}`, {
method,
headers: { "content-type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const payload = response.status === 204 ? {} : await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`qdrant request failed: ${method} ${path} ${response.status}`);
return payload;
}
async function preprovisionCollection(ctx, workspaceId) {
await qdrantJson(ctx, "PUT", `/collections/${workspaceId}`, {
vectors: { size: 1024, distance: "Cosine" },
});
for (const field of ["content_hash", "document_id", "kind", "record_key", "record_kind", "vector_generation", "workspace_id", "workspace_revision"]) {
await qdrantJson(ctx, "PUT", `/collections/${workspaceId}/index`, { field_name: field, field_schema: "keyword" });
}
}
async function listCollections(ctx) {
const payload = await qdrantJson(ctx, "GET", "/collections");
const collections = payload.result?.collections ?? [];
return collections.map((item) => item.name).sort();
}
async function dumpQdrantPayloads(ctx, workspaceId) {
const response = await qdrantJson(ctx, "POST", `/collections/${workspaceId}/points/scroll`, { limit: 128, with_payload: true, with_vector: false });
return JSON.stringify(response.result?.points ?? []);
}
async function runThothctlJson(ctx, label, workspaceArgs, expectedExitCode) {
const stdoutPath = join(ctx.run.root, "logs", `${label}.stdout.json`);
const stderrPath = join(ctx.run.root, "logs", `${label}.stderr.txt`);
const result = await runCommand({
executable: ctx.thothctlPath,
argv: ["--installation", ctx.installationPath, ...workspaceArgs, "--json"],
env: ctx.execEnv,
maxOutputBytes: 2 * 1024 * 1024,
});
await atomicWrite(stdoutPath, result.stdout || "");
await atomicWrite(stderrPath, result.stderr || "");
if (expectedExitCode !== undefined && result.exitCode !== expectedExitCode) {
throw new Error(`${label} exit ${result.exitCode} != ${expectedExitCode}`);
}
let payload;
try { payload = JSON.parse(result.stdout); } catch (error) { throw new Error(`${label} returned non-JSON stdout`); }
return { result, payload, artifacts: [await fileArtifact(ctx.run.root, relative(ctx.run.root, stdoutPath)), await fileArtifact(ctx.run.root, relative(ctx.run.root, stderrPath))] };
}
async function runThothctlRaw(ctx, label, workspaceArgs) {
const stdoutPath = join(ctx.run.root, "logs", `${label}.stdout.txt`);
const stderrPath = join(ctx.run.root, "logs", `${label}.stderr.txt`);
const result = await runCommand({
executable: ctx.thothctlPath,
argv: ["--installation", ctx.installationPath, ...workspaceArgs],
env: ctx.execEnv,
maxOutputBytes: 2 * 1024 * 1024,
});
await atomicWrite(stdoutPath, result.stdout || "");
await atomicWrite(stderrPath, result.stderr || "");
return {
result,
artifacts: [
await fileArtifact(ctx.run.root, relative(ctx.run.root, stdoutPath)),
await fileArtifact(ctx.run.root, relative(ctx.run.root, stderrPath)),
],
};
}
async function loadWorkspaceSnapshot(ctx, workspaceId) {
const active = JSON.parse(await readFile(join(ctx.run.root, "installation", "registry", "state", "active.json"), "utf8"));
const revision = active.revisions.find((entry) => entry.id === workspaceId);
const snapshotPath = revision.snapshotPath;
const contents = await readFile(snapshotPath, "utf8");
return { active, revision, contents };
}
async function assertNoCoreFrontendRunning(ctx) {
const ps = await dockerCompose(ctx, ["ps", "--status", "running", "--format", "json"], { allowFailure: true });
if (ps.exitCode !== 0) return [];
const lines = ps.stdout.trim() === "" ? [] : ps.stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
const services = lines.map((item) => item.Service);
if (services.includes("core") || services.includes("frontend") || services.includes("workspace-maintenance")) {
throw new Error("core/frontend/maintenance is unexpectedly running");
}
return services;
}
function sameSet(left, right) {
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
}
const EMBEDDING_STUB_SOURCE = String.raw`import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class _Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(length))
inputs = payload.get("input", [])
if isinstance(inputs, str):
inputs = [inputs]
embeddings = [[0.01] * 1024 for _ in inputs]
body = json.dumps({"model": payload.get("model", "qwen3-embedding:0.6b"), "embeddings": embeddings}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
HTTPServer(("0.0.0.0", 11434), _Handler).serve_forever()
`;
function realUserHome() {
try {
const output = execFileSync("bash", ["-lc", 'printf "%s" ~'], { encoding: "utf8" }).trim();
return output.length > 0 ? output : undefined;
} catch {
return undefined;
}
}
async function setupRealContext({ repositoryRoot = defaultRepositoryRoot, env = process.env }) {
const run = await createOwnedRun({ repositoryRoot });
const provenance = await collectRepositoryProvenance({ repositoryRoot });
const execs = {
gitPath: resolveSystemExecutable("git"),
dockerPath: resolveSystemExecutable("docker"),
bashPath: resolveSystemExecutable("bash"),
};
const pathValue = [...new Set([dirname(execs.gitPath), dirname(execs.dockerPath), "/usr/bin", "/bin", "/opt/homebrew/bin", "/usr/local/bin"])].join(":");
// Docker CLI plugins (buildx) live under the real user's ~/.docker; the wrapper runs with a
// scrubbed environment, so derive the real home from the passwd entry and expose DOCKER_CONFIG.
const realHome = env.P5_REAL_HOME ?? realUserHome();
const execEnv = buildSafeEnvironment({ ambient: env, fixture: {
PATH: pathValue,
HOME: run.root,
TMPDIR: join(run.root, "tmp"),
...(realHome ? { DOCKER_CONFIG: join(realHome, ".docker") } : {}),
} });
const workspaceModules = await import("../dist/workspaces/registry.js").then(async (registryModule) => ({
WorkspaceRegistry: registryModule.WorkspaceRegistry,
...(await import("../dist/workspaces/schema.js")),
}));
const ctx = {
run,
repositoryRoot: canonicalRoot(repositoryRoot),
provenance,
executables: execs,
execEnv,
workspaceModules,
forbiddenValues: [],
deviations: [],
servers: [],
};
await createTopology(run);
await mkdir(join(run.root, "tmp"), { recursive: true, mode: 0o700 });
await setupSecrets(ctx);
await setupFixtures(ctx);
return ctx;
}
async function executeChecksLocal({ checks, failAt } = {}) {
if (!Array.isArray(checks) || !hasExactCheckIds(checks)) throw new Error("scenarios must match the exact ordered check set");
if (failAt !== undefined && !CHECK_IDS.includes(failAt)) throw new Error("failure hook must name an exact check");
const results = [];
let stopped = false;
for (const scenario of checks) {
const startedAt = nowIso();
let result;
if (stopped) {
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: "Not executed after earlier failure." };
} else {
try {
const output = await scenario.run();
if (scenario.id === failAt) throw new Error("injected acceptance failure");
result = { id: scenario.id, status: "PASS", startedAt, finishedAt: nowIso(), commands: output.commands ?? [], artifacts: output.artifacts ?? [] };
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: `Acceptance scenario failed safely: ${detail}` };
stopped = true;
}
}
results.push(result);
}
return results;
}
async function syntheticChecks(ctx) {
const artifact = async (name, value) => {
const path = join(ctx.run.root, "logs", `${name}.json`);
await writeJson(path, value);
return await fileArtifact(ctx.run.root, relative(ctx.run.root, path));
};
return CHECK_IDS.map((id, index) => ({
id,
async run() {
return {
commands: [index === 0 ? "node" : "git"],
artifacts: [await artifact(id, { id, synthetic: true })],
};
},
}));
}
async function realChecks(ctx) {
const state = {};
const syncedRoot = (workspaceId, commit) => join(ctx.run.root, "installation", "data", "sessions", workspaceId, "revisions", commit, "artifacts");
const syncedAnnotations = (workspaceId, commit) => join(syncedRoot(workspaceId, commit), "mschema", "annotations.yaml");
const syncedManifest = (workspaceId, commit) => join(syncedRoot(workspaceId, commit), "mschema", "annotations.ownership.json");
const activeCommit = async (workspaceId) => (await loadWorkspaceSnapshot(ctx, workspaceId)).revision.commit;
return [
{
id: "preflight",
async run() {
await buildThothctl(ctx);
await buildCoreImage(ctx);
await writeInstallationFiles(ctx);
return {
commands: ["docker", "node", "git"],
artifacts: [{ path: "logs/provenance.json", sha256: sha256(JSON.stringify(ctx.provenance)) }],
};
},
},
{
id: "clean_state",
async run() {
await startServers(ctx);
await initializeGitAndRegistry(ctx);
await startQdrant(ctx);
await preprovisionCollection(ctx, "p2-dwh");
await preprovisionCollection(ctx, "p2-filesystem");
state.collectionsBefore = await listCollections(ctx);
state.runningServices = await assertNoCoreFrontendRunning(ctx);
state.initialCommit = await activeCommit("p2-filesystem");
await writeJson(join(ctx.run.root, "logs", "collections-before.json"), state.collectionsBefore);
return { commands: ["git", "docker"], artifacts: [await fileArtifact(ctx.run.root, "logs/collections-before.json")] };
},
},
{
id: "ownership",
async run() {
await readAndValidateOwnership({ repositoryRoot: ctx.repositoryRoot, runRoot: ctx.run.root, expectedNonce: ctx.run.nonce });
const installStat = await stat(ctx.installationPath);
assert(installStat.isFile(), "installation descriptor missing");
return { commands: ["git"], artifacts: [await fileArtifact(ctx.run.root, "ownership.json")] };
},
},
{
id: "activation_sync",
async run() {
// The initial curated blob must have been synchronized on activation with a verified
// ownership manifest at the exact revision-qualified runtime root.
const commit = state.initialCommit;
const annotationsPath = syncedAnnotations("p2-filesystem", commit);
const manifestPath = syncedManifest("p2-filesystem", commit);
assert(readFileSync(annotationsPath, "utf8") === "tables: {}\n", "synced annotations content mismatch");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
assert(manifest.workspace === "p2-filesystem", "ownership manifest workspace mismatch");
assert(manifest.commit === commit, "ownership manifest commit mismatch");
assert(/^[0-9a-f]{40}$/.test(manifest.blobId ?? ""), "ownership manifest blobId missing");
assert(manifest.contentDigest === `sha256:${sha256("tables: {}\n")}`, "ownership manifest digest mismatch");
assert(manifest.destination === annotationsPath, "ownership manifest destination mismatch");
return { commands: [], artifacts: [await fileArtifact(ctx.run.root, relative(ctx.run.root, annotationsPath)), await fileArtifact(ctx.run.root, relative(ctx.run.root, manifestPath))] };
},
},
{
id: "accept_happy_path",
async run() {
// 1) a fresh full run introspects the DWH, mines FK candidates, and blocks for review.
const full = await runThothctlJson(ctx, "preprocess-run-fs-blocked", ["workspace", "preprocess", "run", "--workspace", "p2-filesystem"], 3);
assert(full.payload.code === "manual_review_required", `full run did not block: ${full.payload.code}`);
const digest = full.payload.artifactIdentities?.[0]?.digest;
assert(/^sha256:[0-9a-f]{64}$/.test(digest ?? ""), "candidate digest missing");
state.runId = full.payload.runId;
assert(/^[0-9a-f]{32}$/.test(state.runId ?? ""), "run id missing");
// 2) the operator records the human review with the explicit accept command.
const accepted = await runThothctlJson(ctx, "schema-accept-filesystem", ["workspace", "schema", "accept", "--workspace", "p2-filesystem", "--run", state.runId, "--yes"], 0);
assert(accepted.payload.status === "succeeded" && accepted.payload.code === "ok", "schema accept failed");
assert(Array.isArray(accepted.payload.artifactIdentities), "accept artifact identities missing");
// 3) same-revision resume continues through the FK gate and stops at filesystem Evidence.
const resumed = await runThothctlJson(ctx, "preprocess-run-fs-resume", ["workspace", "preprocess", "run", "--workspace", "p2-filesystem", "--resume", state.runId], 3);
assert(resumed.payload.code === "evidence_materialization_required", `resume code mismatch: ${resumed.payload.code}`);
state.acceptedCommit = state.initialCommit;
return { commands: ["thothctl"], artifacts: [...full.artifacts, ...accepted.artifacts, ...resumed.artifacts] };
},
},
{
id: "revision_isolation",
async run() {
// A new annotations revision writes a distinct immutable runtime root.
await mutateWorkspaceAnnotations(ctx, "p2-filesystem", "tables: {}\n# revision B\n", "Curate reviewed FK annotations (revision B)");
const revisionB = await activeCommit("p2-filesystem");
assert(revisionB !== state.initialCommit, "annotations commit did not change the revision");
assert(existsSync(syncedAnnotations("p2-filesystem", revisionB)), "revision B annotations not synced");
assert(syncedAnnotations("p2-filesystem", revisionB) !== syncedAnnotations("p2-filesystem", state.initialCommit), "revision roots are not isolated");
state.revisionB = revisionB;
return { commands: ["git"], artifacts: [await fileArtifact(ctx.run.root, relative(ctx.run.root, syncedAnnotations("p2-filesystem", revisionB)))] };
},
},
{
id: "accept_negatives",
async run() {
// Grammar: --yes is required (exit 2, host-side, no JSON result).
const missingYes = await runThothctlRaw(ctx, "neg-missing-yes", ["workspace", "schema", "accept", "--workspace", "p2-filesystem", "--run", state.runId]);
assert(missingYes.result.exitCode === 2, `missing --yes exit ${missingYes.result.exitCode} != 2`);
// Unknown run fails closed without recording a review.
const unknown = await runThothctlJson(ctx, "neg-unknown-run", ["workspace", "schema", "accept", "--workspace", "p2-filesystem", "--run", "e".repeat(32), "--yes"], 1);
assert(unknown.payload.code === "annotation_invalid", "unknown run code mismatch");
return { commands: ["thothctl"], artifacts: [...missingYes.artifacts, ...unknown.artifacts] };
},
},
{
id: "continuation_gate",
async run() {
// A run accepted at revision A must not be silently resumed after the annotations revision
// changed to B: the pinned job no longer matches the active revision.
const stale = await runThothctlJson(ctx, "stale-resume-after-revision-change", ["workspace", "preprocess", "run", "--workspace", "p2-filesystem", "--resume", state.runId], 1);
assert(["preprocessing_resume_mismatch", "preprocessing_conflict"].includes(stale.payload.code), `stale resume code mismatch: ${stale.payload.code}`);
return { commands: ["thothctl"], artifacts: [...stale.artifacts] };
},
},
{
id: "secret_scan",
async run() {
const virtualFiles = [];
const qdrantDump = await dumpQdrantPayloads(ctx, "p2-filesystem");
if (Buffer.byteLength(qdrantDump) <= MAX_SECRET_SCAN_VIRTUAL_BYTES) virtualFiles.push({ path: "virtual/qdrant-p2-filesystem.json", bytes: qdrantDump });
const findings = await scanSecrets({
runRoot: ctx.run.root,
forbiddenValues: ctx.forbiddenValues,
virtualFiles,
expectedGitRepositories: ["remote.git", "author"],
});
await writeJson(join(ctx.run.root, "logs", "secret-scan.json"), findings);
if (findings.length > 0) throw new Error(`secret scan found ${findings.length} leak(s)`);
return { commands: ["git"], artifacts: [await fileArtifact(ctx.run.root, "logs/secret-scan.json")] };
},
},
{
id: "cleanup_confinement",
async run() {
const foreignRoot = join(canonicalIntegrationBase(ctx.repositoryRoot), `p5-${"f".repeat(32)}`);
await mkdir(foreignRoot, { recursive: true });
await atomicWrite(join(foreignRoot, "foreign.txt"), "foreign");
assert(readFileSync(join(foreignRoot, "foreign.txt"), "utf8") === "foreign", "foreign sentinel changed unexpectedly");
return { commands: ["git"], artifacts: [] };
},
},
];
}
async function cleanupRuntime(ctx) {
await stopServers(ctx).catch(() => {});
if (ctx.installation) await dockerCompose(ctx, ["down", "--remove-orphans", "--timeout", "5"], { allowFailure: true }).catch(() => {});
if (ctx.coreImageTag) await runCommand({ executable: ctx.executables.dockerPath, argv: ["image", "rm", "-f", ctx.coreImageTag], env: ctx.execEnv, maxOutputBytes: MAX_STDIO_BYTES }).catch(() => {});
}
export async function runIntegration({ repositoryRoot = defaultRepositoryRoot, keep = false, env = process.env, announce } = {}) {
const synthetic = env.P5_ACCEPTANCE_SYNTHETIC === "1";
const failAt = env.P5_ACCEPTANCE_FAIL_AT;
const ctx = synthetic
? { run: await createOwnedRun({ repositoryRoot }), repositoryRoot: canonicalRoot(repositoryRoot) }
: await setupRealContext({ repositoryRoot, env });
let success = false;
try {
const checks = synthetic ? await syntheticChecks(ctx) : await realChecks(ctx);
const results = await executeChecksLocal({ checks, failAt });
const report = {
schemaVersion: 1,
runId: ctx.run.runId,
startedAt: ctx.run.startedAt,
finishedAt: nowIso(),
command: "p5-acceptance integration --keep",
overall: deriveOverall(results),
checks: results,
};
await writeReportFiles({ run: ctx.run, report });
success = report.overall === "PASS";
if (announce) await announce({ report, runRoot: ctx.run.root });
return { exitCode: success ? 0 : 1, runRoot: ctx.run.root, retained: !(await finalizeOwnedRun({ run: ctx.run, success, keep })) };
} finally {
if (!synthetic) await cleanupRuntime(ctx).catch(() => {});
}
}
export async function main(argv = process.argv.slice(2), env = process.env) {
if (argv.length < 1 || argv[0] !== "integration" || argv.length > 2 || (argv[1] && argv[1] !== "--keep")) {
throw new Error("usage: p5-acceptance.mjs integration [--keep]");
}
const result = await runIntegration({ keep: argv.includes("--keep"), env });
return result.exitCode;
}
if (process.argv[1] && realpathSync(process.argv[1]) === modulePath) {
try {
const code = await main();
process.exitCode = code;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
export { CHECK_IDS };