#!/usr/bin/env node import { createHash, randomBytes } from "node:crypto"; import { closeSync, constants as fsConstants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync } from "node:fs"; import { access, lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; import { buildSafeEnvironment, collectRepositoryProvenance, 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 = /^p11-[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 ZIP_FILES = ["manifest.json", "workspace.yaml", "contract.env.example", "README.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 (lstatSync(resolved).isFile()) return resolved; } catch {} } throw new Error(`required executable not found: ${name}`); } function resolveExecutables(repositoryRoot) { const repo = canonicalRoot(repositoryRoot); const thtPath = join(repo, "harness", ".venv", "bin", "tht"); if (!existsSync(thtPath)) throw new Error("required executable not found: tht"); return { gitPath: resolveSystemExecutable("git"), pythonPath: resolveSystemExecutable("python3"), thtPath: realpathSync(thtPath) }; } const TOPOLOGY = [ "remote.git", "author", "installation/registry", "installation/data", "installation/runtime", "fixture-secrets", "fixtures/descriptors", "fixtures/requests", "requests", "responses", "exports/raw", "exports/extracted", "rendered", "logs", ]; export const CHECK_IDS = Object.freeze([ "preflight", "clean_state", "ownership", "catalog_bootstrap", "catalog_only_listing", "bootstrap_create_once", "api_curator_boundary", "curator_descriptor_update", "content_only_revision", "docs_only_reconciliation", "same_revision_git_objects", "snapshot_and_export", "runtime_render_determinism", "tht_config_check", "negative_catalog_layout_cases", "negative_schema_context_cases", "no_p2_scope_artifacts", "secret_scan", "cleanup_confinement", ]); 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 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); } function canonicalRoot(repositoryRoot) { return realpathSync(repositoryRoot); } export function canonicalIntegrationBase(repositoryRoot = defaultRepositoryRoot) { return join(canonicalRoot(repositoryRoot), ".artifacts", "p11-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 exactOwnedResources(run) { return [ run.root, join(run.root, "remote.git"), join(run.root, "author"), join(run.root, "installation", "registry"), join(run.root, "installation", "data"), join(run.root, "installation", "runtime"), ]; } function initialListeners(pid) { return [{ name: "primary", kind: "fastify", host: "127.0.0.1", requestedPort: 0, pid, state: "not_started" }]; } function ownershipValue(run, listeners = run.listeners) { return { schemaVersion: 1, kind: "p11-acceptance", runId: run.runId, runNonce: run.nonce, root: run.root, repositoryRoot: run.repositoryRoot, startedAt: run.startedAt, pid: run.pid, listeners, resources: exactOwnedResources(run), }; } async function writeOwnership(run, listenerUpdate) { const listeners = listenerUpdate ? run.listeners.map((listener) => listener.name === listenerUpdate.name ? listenerUpdate : listener) : run.listeners; await atomicWrite(join(run.root, "ownership.json"), `${JSON.stringify(ownershipValue(run, listeners), null, 2)}\n`); run.listeners = listeners; } 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 ?? `p11-${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, listeners: initialListeners(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"); const listener = value.listeners?.[0]; const validListener = Array.isArray(value.listeners) && value.listeners.length === 1 && listener?.name === "primary" && listener.kind === "fastify" && listener.host === "127.0.0.1" && listener.requestedPort === 0 && listener.pid === process.pid && ["not_started", "listening", "closed", "close_failed"].includes(listener.state) && (listener.state === "not_started" ? !("actualPort" in listener) : Number.isInteger(listener.actualPort) && listener.actualPort >= 1 && listener.actualPort <= 65535); if (value.schemaVersion !== 1 || value.kind !== "p11-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 ?? "") || !validListener || JSON.stringify(value.resources) !== JSON.stringify(exactOwnedResources(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, nonce: expectedNonce, startedAt: value.startedAt, pid: process.pid, }, 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 sanitizeForEvidence(value, forbiddenValues = []) { const forbidden = forbiddenValues.filter((item) => typeof item === "string" && item.length > 0); const redactString = (input) => forbidden.reduce((text, secret) => text.split(secret).join("[REDACTED]"), input); if (typeof value === "string") return redactString(value); if (Array.isArray(value)) return value.map((item) => sanitizeForEvidence(item, forbiddenValues)); if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeForEvidence(item, forbiddenValues)])); return value; } async function fileArtifact(root, relativePath) { const bytes = await readFile(join(root, relativePath)); return { path: relativePath.split(sep).join("/"), sha256: sha256(bytes) }; } async function evidence(run, relativePath, value, forbiddenValues = []) { await atomicWrite(join(run.root, relativePath), `${JSON.stringify(sanitizeForEvidence(value, forbiddenValues), null, 2)}\n`); return await fileArtifact(run.root, relativePath); } async function writeJson(path, value) { await atomicWrite(path, `${JSON.stringify(value, null, 2)}\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); if (entry.isDirectory()) await visit(path); else if (entry.isFile()) files.push({ path, rel: relative(root, path).split(sep).join("/") }); } } if (existsSync(root)) await visit(root); return files.sort((a, b) => a.rel.localeCompare(b.rel)); } async function snapshotDigest(root) { const result = {}; for (const file of await walkFiles(root)) result[file.rel] = sha256(await readFile(file.path)); return result; } function assertByteIdentical(left, right, label) { if (JSON.stringify(left) !== JSON.stringify(right)) throw new Error(`${label} changed unexpectedly`); } async function writeReportFiles({ run, report }) { validateReport(report); await writeJson(join(run.root, "report.json"), report); const lines = [ `# P1.1 acceptance report`, "", `Run ID: ${report.runId}`, `Overall: ${report.overall}`, "", ...report.checks.map((check) => `- ${check.id}: ${check.status}`), "", `report.json sha256: ${sha256(await readFile(join(run.root, "report.json")))}`, `P1.1 automated integration: ${report.overall}`, "P1.1 manual acceptance: PENDING", ]; await atomicWrite(join(run.root, "report.md"), `${lines.join("\n")}\n`); } export function validateReport(report) { if (!report || typeof report !== "object" || Array.isArray(report)) throw new Error("report is malformed"); if (report.schemaVersion !== 1 || !RUN_ID.test(report.runId ?? "") || !ISO_UTC.test(report.startedAt ?? "") || !ISO_UTC.test(report.finishedAt ?? "") || report.command !== "p11-acceptance integration --keep") throw new Error("report identity is invalid"); if (report.overall !== deriveOverall(report.checks ?? [])) throw new Error("report overall is not derived"); if (!Array.isArray(report.checks) || report.checks.length !== CHECK_IDS.length) throw new Error("report checks are incomplete"); const ids = report.checks.map((check) => check.id); if (JSON.stringify(ids) !== JSON.stringify(CHECK_IDS)) throw new Error("report checks are not exact"); const artifactPaths = new Set(); for (const check of report.checks) { if (!["PASS", "FAIL"].includes(check.status) || !ISO_UTC.test(check.startedAt ?? "") || !ISO_UTC.test(check.finishedAt ?? "")) { throw new Error("report check metadata is invalid"); } if (!Array.isArray(check.commands) || check.commands.some((command) => typeof command !== "string" || !/^[A-Za-z0-9._+-]+$/.test(command))) { throw new Error("report command is invalid"); } if (!Array.isArray(check.artifacts)) throw new Error("report artifacts are invalid"); for (const artifact of check.artifacts) { if (typeof artifact.path !== "string" || artifact.path.startsWith("/") || artifact.path.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(artifact.path)) { throw new Error("report artifact path is invalid"); } if (!HEX64.test(artifact.sha256 ?? "")) throw new Error("report artifact hash is invalid"); if (artifactPaths.has(artifact.path)) throw new Error("report artifact path is duplicated"); artifactPaths.add(artifact.path); } } } async function execCommand(executable, argv, { cwd, env, timeoutMs = 30_000, stdin } = {}) { if (!Array.isArray(argv) || argv.some((value) => typeof value !== "string")) throw new Error("command argv must be a string array"); const result = await execFileAsync(executable, argv, { cwd, env, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, encoding: "utf8", ...(stdin === undefined ? {} : { input: stdin }), }); return { code: 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" }; } async function git(ctx, argv, options = {}) { return await execCommand(ctx.executables.gitPath, argv, { ...options, env: ctx.env }); } async function tht(ctx, argv, options = {}) { try { return await execCommand(ctx.executables.thtPath, argv, { ...options, env: ctx.env }); } catch (error) { if (typeof error?.code === "number") return { code: error.code, stdout: error.stdout ?? "", stderr: error.stderr ?? "" }; throw error; } } function namespace(id) { return id.toUpperCase().replaceAll("-", "_"); } function baseWorkspace(id, evidenceSource) { return { workspace: { schema_version: 4, id, name: `P1.1 ${id}`, description: `Catalog entry for ${id}`, language: "en" }, dwh: { engine: "postgres", database: "postgres", schema: "public", supported_transports: ["postgres_direct"] }, evidence: { source: evidenceSource, policy: { max_chunk_chars: 4000, retain_published_generations: 3 } }, }; } function descriptors() { return [ baseWorkspace("p11-filesystem", { type: "filesystem", uri: "p11-filesystem/evidence", patterns: ["**/*.md"], max_bytes: 10485760 }), baseWorkspace("p11-http", { type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "signed_urls_file", connect_timeout_ms: 1250, read_timeout_ms: 30001, max_bytes: 12345, max_redirects: 2, allow_private_hosts: false, max_cache_bytes: 67890 }), baseWorkspace("p11-s3", { type: "s3", uri: "s3://p11-evidence/published/", endpoint_url: "https://s3.example.test/", region: "eu-west-1", credentials: "static_files", trusted_endpoint: true, allow_private_endpoint: false, allow_insecure_endpoint: false, max_bytes: 12345, max_objects: 33, max_pages: 4, page_size: 5 }), ]; } 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 setupSecrets(ctx) { const secretDir = join(ctx.run.root, "fixture-secrets"); const values = { dwh: `DWH-${randomBytes(12).toString("hex")}`, signed: `SIGNED-${randomBytes(12).toString("hex")}`, access: `ACCESS-${randomBytes(12).toString("hex")}`, secret: `SECRET-${randomBytes(12).toString("hex")}`, session: `SESSION-${randomBytes(12).toString("hex")}`, rejected: `REJECTED-${randomBytes(12).toString("hex")}`, }; ctx.forbiddenValues = Object.values(values); ctx.secretValues = values; const paths = { dwh: join(secretDir, "dwh-password"), signed: join(secretDir, "evidence-signed-urls.json"), access: join(secretDir, "evidence-access"), secret: join(secretDir, "evidence-secret"), session: join(secretDir, "evidence-session"), }; await atomicWrite(paths.dwh, scalarSecretBytes(values.dwh)); await atomicWrite(paths.signed, JSON.stringify([`https://evidence.example.test/guide.md?token=${values.signed}`])); await atomicWrite(paths.access, scalarSecretBytes(values.access)); await atomicWrite(paths.secret, scalarSecretBytes(values.secret)); await atomicWrite(paths.session, scalarSecretBytes(values.session)); const env = {}; for (const workspace of ctx.descriptors) { const prefix = `THT_WS_${namespace(workspace.workspace.id)}`; Object.assign(env, { [`${prefix}_DWH_TRANSPORT`]: "postgres_direct", [`${prefix}_DWH_HOST`]: "dwh.invalid", [`${prefix}_DWH_PORT`]: "5432", [`${prefix}_DWH_USER`]: "reader", [`${prefix}_DWH_PASSWORD_FILE`]: paths.dwh, }); } Object.assign(env, { THT_WS_P11_HTTP_EVIDENCE_SIGNED_URLS_FILE: paths.signed, THT_WS_P11_S3_EVIDENCE_ACCESS_KEY_FILE: paths.access, THT_WS_P11_S3_EVIDENCE_SECRET_KEY_FILE: paths.secret, THT_WS_P11_S3_EVIDENCE_SESSION_TOKEN_FILE: paths.session, }); Object.assign(ctx.env, env); await atomicWrite(join(ctx.run.root, "installation", "bindings.env"), `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n")}\n`); await atomicWrite(join(ctx.run.root, "installation", "runtime", "base.yaml"), "{}\n"); } function catalog(entries = ctxDescriptors) { return { schema_version: 1, workspaces: entries.map(({ workspace }) => ({ id: workspace.id, name: workspace.name, description: workspace.description })) }; } const ctxDescriptors = descriptors(); async function initializeGit(ctx) { const author = join(ctx.run.root, "author"); await git(ctx, ["init", "--bare", "--initial-branch=main", join(ctx.run.root, "remote.git")], { cwd: ctx.run.root }); await git(ctx, ["clone", join(ctx.run.root, "remote.git"), author], { cwd: ctx.run.root }); await git(ctx, ["config", "user.name", "P1 Fixture Curator"], { cwd: author }); await git(ctx, ["config", "user.email", "p1-curator@example.invalid"], { cwd: author }); const catalogBytes = `${JSON.stringify({ schema_version: 1, workspaces: [ ...catalog(ctx.descriptors).workspaces, { id: "p11-pending", name: "P1.1 pending", description: "Catalog-only slot awaiting bootstrap" }, ], }, null, 2)}\n`; await atomicWrite(join(author, "thoth-workspaces.yaml"), catalogBytes, 0o644); const evidenceRoot = join(author, "p11-filesystem", "evidence"); await mkdir(join(evidenceRoot, "domain"), { recursive: true }); await atomicWrite(join(evidenceRoot, "guide.md"), "# P1.1 curated Evidence\n", 0o644); await atomicWrite(join(evidenceRoot, "domain", "table.md"), "# Curated table\n", 0o644); await git(ctx, ["add", "thoth-workspaces.yaml"], { cwd: author }); await git(ctx, ["add", "p11-filesystem/evidence/guide.md"], { cwd: author }); await git(ctx, ["add", "-A", "p11-filesystem/evidence"], { cwd: author }); await git(ctx, ["commit", "-m", "Bootstrap curated P1 content"], { cwd: author }); await git(ctx, ["push", "origin", "main"], { cwd: author }); ctx.bootstrapCommit = (await git(ctx, ["rev-parse", "HEAD"], { cwd: author })).stdout.trim(); ctx.catalogBlobBefore = (await git(ctx, ["rev-parse", `HEAD:thoth-workspaces.yaml`], { cwd: author })).stdout.trim(); ctx.evidenceTreeBefore = (await git(ctx, ["rev-parse", `HEAD:p11-filesystem/evidence`], { cwd: author })).stdout.trim(); } async function loadProductionBackend() { const [{ loadConfig }, { buildApp }, { WorkspaceRegistry }, { ThtRunner }] = await Promise.all([ import("../dist/config.js"), import("../dist/app.js"), import("../dist/workspaces/registry.js"), import("../dist/tht/tht-runner.js"), ]); return { loadConfig, buildApp, WorkspaceRegistry, ThtRunner }; } async function startBackend(ctx) { const { loadConfig, buildApp, WorkspaceRegistry, ThtRunner } = await loadProductionBackend(); const config = loadConfig(ctx.env); const registry = new WorkspaceRegistry(config.workspaceRegistry); const thtRunner = new ThtRunner({ thtBin: config.thtBin, harnessDir: config.harnessDir, configPath: join(ctx.run.root, "installation", "runtime", "base.yaml"), dataRoot: config.dataRoot, runtimeSnapshotRoot: join(config.workspaceRegistry.root, "snapshots", "runtime"), secretRoots: config.workspaceRegistry.secretRoots, secretsFile: config.secretsFile, secretFiles: config.secretFiles, semanticRuntime: { internalQdrantUrl: config.internalQdrantUrl, internalEmbeddingUrl: config.internalEmbeddingUrl, internalEmbeddingModel: config.internalEmbeddingModel, internalEmbeddingDimensions: config.internalEmbeddingDimensions, }, }); const app = buildApp(config, { thtRunner, workspaceRegistry: registry }); const address = await app.listen({ host: "127.0.0.1", port: 0 }); const baseUrl = `http://127.0.0.1:${new URL(address).port}`; ctx.registry = registry; ctx.thtRunner = thtRunner; ctx.app = app; ctx.baseUrl = baseUrl; await writeOwnership(ctx.run, { name: "primary", kind: "fastify", host: "127.0.0.1", requestedPort: 0, actualPort: Number(new URL(address).port), pid: process.pid, state: "listening", }); } async function stopBackend(ctx) { if (ctx.app) { await ctx.app.close().catch(() => {}); await writeOwnership(ctx.run, { name: "primary", kind: "fastify", host: "127.0.0.1", requestedPort: 0, actualPort: Number(new URL(ctx.baseUrl).port), pid: process.pid, state: "closed", }).catch(() => {}); } } async function request(ctx, id, method, path, body, binary = false, safeInput) { const requestSummary = safeInput === undefined ? { method, path, ...(body === undefined ? {} : { body: sanitizeForEvidence(body, ctx.forbiddenValues) }) } : { method, path, input: safeInput }; await evidence(ctx.run, `requests/${id}.json`, requestSummary, ctx.forbiddenValues); const response = await fetch(`${ctx.baseUrl}${path}`, { method, headers: body === undefined ? {} : { "content-type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), signal: AbortSignal.timeout(15_000), }); if (binary) { const bytes = Buffer.from(await response.arrayBuffer()); await atomicWrite(join(ctx.run.root, `exports/raw/${id}.zip`), bytes); await evidence(ctx.run, `responses/${id}.json`, { status: response.status, bytes: bytes.length, contentType: response.headers.get("content-type") }); return { status: response.status, bytes }; } const text = await response.text(); let parsed; try { parsed = text ? JSON.parse(text) : null; } catch { parsed = { invalidJson: true, raw: text }; } await evidence(ctx.run, `responses/${id}.json`, { status: response.status, body: sanitizeForEvidence(parsed, ctx.forbiddenValues) }, ctx.forbiddenValues); return { status: response.status, body: parsed }; } async function extractZip(ctx, id, bytes) { const yauzl = (await import("yauzl")).default; const output = join(ctx.run.root, "exports", "extracted", id); await mkdir(output, { recursive: true }); const files = await new Promise((resolvePromise, reject) => { yauzl.fromBuffer(bytes, { lazyEntries: true, strictFileNames: true, validateEntrySizes: true }, (error, zip) => { if (error || !zip) return reject(error ?? new Error("zip open failed")); const collected = new Map(); zip.on("error", reject); zip.on("entry", (entry) => { if (!ZIP_FILES.includes(entry.fileName) || entry.fileName.includes("..") || entry.fileName.startsWith("/") || entry.fileName.endsWith("/")) return reject(new Error("unsafe export entry")); zip.openReadStream(entry, (streamError, stream) => { if (streamError || !stream) return reject(streamError ?? new Error("zip stream failed")); const chunks = []; stream.on("data", (chunk) => chunks.push(chunk)); stream.on("error", reject); stream.on("end", async () => { const buffer = Buffer.concat(chunks); collected.set(entry.fileName, buffer); await atomicWrite(join(output, entry.fileName), buffer); zip.readEntry(); }); }); }); zip.on("end", () => resolvePromise(collected)); zip.readEntry(); }); }); assert(files.size === ZIP_FILES.length, "export bundle entry mismatch"); return JSON.parse(files.get("manifest.json").toString("utf8")); } function checkResult(id, startedAt, status, artifacts = [], commands = [], error) { return { id, status, startedAt, finishedAt: nowIso(), artifacts, commands, ...(error ? { error } : {}) }; } async function executeChecks({ checks }) { const results = []; let stopped = false; for (const scenario of checks) { const startedAt = nowIso(); if (stopped) { results.push(checkResult(scenario.id, startedAt, "FAIL", [], [], "Not executed after earlier failure.")); continue; } try { const output = await scenario.run(); results.push(checkResult(scenario.id, startedAt, "PASS", output.artifacts ?? [], output.commands ?? [])); } catch (error) { const partial = error?.acceptancePartial ?? {}; results.push(checkResult(scenario.id, startedAt, "FAIL", partial.artifacts ?? [], partial.commands ?? [], "Acceptance scenario failed safely.")); stopped = true; } } return results; } async function registryState(ctx) { const statePath = join(ctx.run.root, "installation", "registry", "state", "active.json"); const active = JSON.parse(await readFile(statePath, "utf8")); return { head: active.head, revisions: active.revisions.map((revision) => ({ id: revision.id, commit: revision.commit, blob: revision.blob })), catalog: active.catalog ?? null, }; } function safeErrorEnvelope(response, code, status) { assert(response.status === status, `expected ${status}`); assert(response.body?.code === code, `expected error code ${code}`); assert(Object.keys(response.body).sort().join(",") === "code,message", "error envelope is not exact"); } async function productionChecks(ctx) { const check = async (id, value, commands = []) => ({ commands, artifacts: [await evidence(ctx.run, `logs/${id}.json`, value, ctx.forbiddenValues)] }); return [ { id: "preflight", run: async () => check("preflight", { node: process.version, repositoryHead: ctx.provenance.head, repositoryTree: ctx.provenance.tree, clean: ctx.provenance.clean, thtExecutable: true }) }, { id: "clean_state", run: async () => check("clean_state", { runId: ctx.run.runId, reused: false }) }, { id: "ownership", run: async () => { await readAndValidateOwnership({ repositoryRoot: ctx.repositoryRoot, runRoot: ctx.run.root, expectedNonce: ctx.run.nonce }); return await check("ownership", { valid: true }); } }, { id: "catalog_bootstrap", run: async () => { await initializeGit(ctx); for (const workspace of ctx.descriptors) await atomicWrite(join(ctx.run.root, "fixtures", "descriptors", `${workspace.workspace.id}.json`), `${JSON.stringify(workspace, null, 2)}\n`); return { commands: ["git"], artifacts: [ await evidence(ctx.run, "logs/catalog-bootstrap.json", { bootstrapCommit: ctx.bootstrapCommit, catalogOnly: true }), await fileArtifact(ctx.run.root, "author/thoth-workspaces.yaml"), await fileArtifact(ctx.run.root, "author/p11-filesystem/evidence/guide.md"), ], }; } }, { id: "catalog_only_listing", run: async () => { await startBackend(ctx); const status = await request(ctx, "registry-status", "GET", "/workspace-registry/status"); assert(status.status === 200 && status.body.head === ctx.bootstrapCommit, "status head mismatch"); const listed = await request(ctx, "workspace-list-initial", "GET", "/workspaces"); assert(listed.status === 200 && listed.body.length === 4, "catalog listing failed"); assert(listed.body.every((entry) => entry.configurationState === "configuration_required"), "catalog entries were not configuration_required"); ctx.baseCommit = status.body.head; return await check("catalog_only_listing", { head: status.body.head, ids: listed.body.map((entry) => entry.id), allConfigurationRequired: true }); } }, { id: "bootstrap_create_once", run: async () => { let base = ctx.baseCommit; ctx.bootstrapResponses = {}; for (const workspace of ctx.descriptors) { const validated = await request(ctx, `validate-${workspace.workspace.id}`, "POST", "/workspaces/validate", { workspace }); assert(validated.status === 200, `validate failed ${workspace.workspace.id}`); const published = await request(ctx, `publish-${workspace.workspace.id}`, "POST", "/workspaces/publish", { action: "create", workspace, baseCommit: base }); assert(published.status === 200 && HEX40.test(published.body.revision.commit), `publish failed ${workspace.workspace.id}`); ctx.bootstrapResponses[workspace.workspace.id] = published.body; base = published.body.revision.commit; } ctx.publishHead = base; const listed = await request(ctx, "workspace-list-ready", "GET", "/workspaces"); assert(listed.body.filter((entry) => entry.configurationState === "ready").length === 3, "bootstrap did not activate all published entries"); assert(listed.body.find((entry) => entry.id === "p11-pending")?.configurationState === "configuration_required", "pending slot was not left unconfigured"); return await check("bootstrap_create_once", { head: base, readyIds: listed.body.filter((entry) => entry.configurationState === "ready").map((entry) => entry.id) }); } }, { id: "api_curator_boundary", run: async () => { const author = join(ctx.run.root, "author"); const catalogAfter = (await git(ctx, ["rev-parse", `HEAD:thoth-workspaces.yaml`], { cwd: author })).stdout.trim(); const evidenceAfter = (await git(ctx, ["rev-parse", `HEAD:p11-filesystem/evidence`], { cwd: author })).stdout.trim(); assert(catalogAfter === ctx.catalogBlobBefore, "catalog blob changed during bootstrap"); assert(evidenceAfter === ctx.evidenceTreeBefore, "evidence tree changed during bootstrap"); ctx.apiBoundaryState = await registryState(ctx); return await check("api_curator_boundary", { catalogUnchanged: true, evidenceUnchanged: true, state: ctx.apiBoundaryState }, ["git"]); } }, { id: "curator_descriptor_update", run: async () => { const author = join(ctx.run.root, "author"); await git(ctx, ["fetch", "origin", "main"], { cwd: author }); await git(ctx, ["reset", "--hard", "origin/main"], { cwd: author }); const workspace = structuredClone(ctx.descriptors[0]); workspace.workspace.name = "P1.1 Curated Filesystem"; workspace.workspace.description = "Curator updated descriptor and catalog metadata"; ctx.curatedWorkspace = workspace; const updatedCatalog = catalog([workspace, ctx.descriptors[1], ctx.descriptors[2]]); await atomicWrite(join(author, "thoth-workspaces.yaml"), `${JSON.stringify(updatedCatalog, null, 2)}\n`, 0o644); await atomicWrite(join(author, "p11-filesystem", "workspace.yaml"), `${(await import("yaml")).stringify(workspace)}`, 0o644); await git(ctx, ["add", "thoth-workspaces.yaml"], { cwd: author }); await git(ctx, ["add", "--", "p11-filesystem/workspace.yaml"], { cwd: author }); await git(ctx, ["commit", "-m", "Publish workspace p1-filesystem"], { cwd: author }); await git(ctx, ["push", "origin", "main"], { cwd: author }); ctx.curatorCommit = (await git(ctx, ["rev-parse", "HEAD"], { cwd: author })).stdout.trim(); ctx.curatorDescriptorBlob = (await git(ctx, ["rev-parse", `HEAD:p11-filesystem/workspace.yaml`], { cwd: author })).stdout.trim(); const pulled = await request(ctx, "pull-after-curator-update", "POST", "/workspace-registry/pull"); assert(pulled.status === 200 && HEX40.test(pulled.body.head), "pull after curator update failed"); ctx.docsFollowupHead = pulled.body.head; const read = await request(ctx, "read-after-curator-update", "GET", "/workspaces/p11-filesystem"); assert(read.status === 200 && read.body.workspace.workspace.name === workspace.workspace.name, "curator update did not activate"); assert(read.body.revision.blob === ctx.curatorDescriptorBlob, "api rewrote curator descriptor bytes"); return await check("curator_descriptor_update", { curatorCommit: ctx.curatorCommit, activeHead: ctx.docsFollowupHead, descriptorBlob: ctx.curatorDescriptorBlob }, ["git"]); } }, { id: "content_only_revision", run: async () => { const author = join(ctx.run.root, "author"); await git(ctx, ["fetch", "origin", "main"], { cwd: author }); await git(ctx, ["reset", "--hard", "origin/main"], { cwd: author }); await atomicWrite(join(author, "p11-filesystem", "evidence", "guide.md"), "# P1.1 curated Evidence v2\n", 0o644); await git(ctx, ["add", "p11-filesystem/evidence/guide.md"], { cwd: author }); await git(ctx, ["commit", "-m", "Update curated Evidence only"], { cwd: author }); await git(ctx, ["push", "origin", "main"], { cwd: author }); ctx.contentCommit = (await git(ctx, ["rev-parse", "HEAD"], { cwd: author })).stdout.trim(); const pulled = await request(ctx, "pull-after-content-update", "POST", "/workspace-registry/pull"); assert(pulled.status === 200 && pulled.body.head === ctx.contentCommit, "content pull head mismatch"); const read = await request(ctx, "read-after-content-update", "GET", "/workspaces/p11-filesystem"); assert(read.body.revision.commit === ctx.contentCommit, "content commit did not activate"); assert(read.body.revision.blob === ctx.curatorDescriptorBlob, "descriptor blob changed on content-only update"); ctx.currentRead = read.body; return await check("content_only_revision", { commit: ctx.contentCommit, descriptorBlobUnchanged: true }, ["git"]); } }, { id: "docs_only_reconciliation", run: async () => { const repo = join(ctx.run.root, "installation", "registry", "repo"); const diff = (await git(ctx, ["show", "--name-only", "--format=", ctx.docsFollowupHead], { cwd: repo })).stdout.trim().split(/\n+/).filter(Boolean); assert(diff.length > 0 && diff.every((path) => path.startsWith("workspace-docs/")), "docs follow-up touched non-doc paths"); const finalDescriptor = (await git(ctx, ["rev-parse", `${ctx.docsFollowupHead}:p11-filesystem/workspace.yaml`], { cwd: repo })).stdout.trim(); assert(finalDescriptor === ctx.curatorDescriptorBlob, "docs follow-up rewrote descriptor"); return await check("docs_only_reconciliation", { head: ctx.docsFollowupHead, files: diff, descriptorBlobPreserved: true }, ["git"]); } }, { id: "same_revision_git_objects", run: async () => { const repo = join(ctx.run.root, "installation", "registry", "repo"); const revision = ctx.currentRead.revision; const manifestPath = join(dirname(revision.snapshotPath), "snapshot.json"); const manifest = JSON.parse(await readFile(manifestPath, "utf8")); const catalogBlob = (await git(ctx, ["rev-parse", `${revision.commit}:thoth-workspaces.yaml`], { cwd: repo })).stdout.trim(); const descriptorBlob = (await git(ctx, ["rev-parse", `${revision.commit}:p11-filesystem/workspace.yaml`], { cwd: repo })).stdout.trim(); const evidenceTree = (await git(ctx, ["rev-parse", `${revision.commit}:p11-filesystem/evidence`], { cwd: repo })).stdout.trim(); assert(manifest.head === revision.commit, "snapshot manifest head mismatch"); assert(descriptorBlob === revision.blob, "descriptor blob mismatch"); ctx.snapshotManifest = manifest; return { commands: ["git"], artifacts: [ await evidence(ctx.run, "logs/same-revision-git-objects.json", { commit: revision.commit, catalogBlob, descriptorBlob, evidenceTree, snapshotHead: manifest.head }), await fileArtifact(ctx.run.root, relative(ctx.run.root, revision.snapshotPath)), await fileArtifact(ctx.run.root, relative(ctx.run.root, manifestPath)), ], }; } }, { id: "snapshot_and_export", run: async () => { ctx.exportManifests = {}; const artifacts = []; for (const workspace of ctx.descriptors) { const id = workspace.workspace.id; const exported = await request(ctx, `export-${id}`, "GET", `/workspaces/${id}/export`, undefined, true); assert(exported.status === 200, `export failed ${id}`); ctx.exportManifests[id] = await extractZip(ctx, id, exported.bytes); artifacts.push(await fileArtifact(ctx.run.root, `exports/raw/export-${id}.zip`)); for (const name of ZIP_FILES) artifacts.push(await fileArtifact(ctx.run.root, `exports/extracted/${id}/${name}`)); } return { commands: [], artifacts: [await evidence(ctx.run, "logs/snapshot-and-export.json", { exported: Object.keys(ctx.exportManifests), files: ZIP_FILES }), ...artifacts] }; } }, { id: "runtime_render_determinism", run: async () => { const YAML = await import("yaml"); ctx.configChecks = []; const artifacts = []; for (const workspace of ctx.descriptors) { const revision = (await request(ctx, `read-render-${workspace.workspace.id}`, "GET", `/workspaces/${workspace.workspace.id}`)).body.revision; const renders = []; for (let n = 1; n <= 2; n += 1) { const lease = ctx.thtRunner.acquireWorkspaceRuntime(revision.snapshotPath); try { const bytes = await readFile(lease.path); renders.push(bytes); await atomicWrite(join(ctx.run.root, "rendered", `${workspace.workspace.id}-${n}.yaml`), bytes); const checked = await tht(ctx, ["config", "check", "-c", lease.path], { cwd: ctx.env.THT_HARNESS_DIR, timeoutMs: 30_000 }); ctx.configChecks.push({ id: workspace.workspace.id, observation: n, code: checked.code }); } finally { lease.release(); } artifacts.push(await fileArtifact(ctx.run.root, `rendered/${workspace.workspace.id}-${n}.yaml`)); } assert(renders[0].equals(renders[1]), `render was nondeterministic ${workspace.workspace.id}`); const rendered = YAML.parse(renders[0].toString("utf8")); assert(rendered.runtime_identity.workspace_revision === revision.commit, `runtime identity mismatch ${workspace.workspace.id}`); } return { commands: ["tht"], artifacts: [await evidence(ctx.run, "logs/runtime-render-determinism.json", { deterministic: true, checks: ctx.configChecks }), ...artifacts] }; } }, { id: "tht_config_check", run: async () => { assert(ctx.configChecks.length === ctx.descriptors.length * 2 && ctx.configChecks.every((item) => item.code === 0), "tht config checks failed"); return await check("tht-config-check", ctx.configChecks, ["tht"]); } }, { id: "negative_catalog_layout_cases", run: async () => { const baseline = await registryState(ctx); const author = join(ctx.run.root, "author"); const current = (await request(ctx, "current-list-before-negatives", "GET", "/workspaces")).body; const secondCreate = await request(ctx, "second-create", "POST", "/workspaces/publish", { action: "create", workspace: ctx.descriptors[0], baseCommit: baseline.head }); safeErrorEnvelope(secondCreate, "workspace_curator_owned", 409); const update = await request(ctx, "legacy-update", "POST", "/workspaces/publish", { action: "update", workspace: ctx.descriptors[0], baseCommit: baseline.head, baseBlob: ctx.curatorDescriptorBlob }); safeErrorEnvelope(update, "workspace_curator_owned", 409); const deletion = await request(ctx, "legacy-delete", "POST", "/workspaces/publish", { action: "delete", id: "p11-filesystem", baseCommit: baseline.head, baseBlob: ctx.curatorDescriptorBlob }); safeErrorEnvelope(deletion, "workspace_curator_owned", 409); const unknown = structuredClone(ctx.descriptors[0]); unknown.workspace.id = "p11-unknown"; const unknownPublish = await request(ctx, "unknown-catalog-id", "POST", "/workspaces/publish", { action: "create", workspace: unknown, baseCommit: baseline.head }); safeErrorEnvelope(unknownPublish, "workspace_invalid", 400); const mismatch = structuredClone(ctx.descriptors[0]); mismatch.workspace.id = "p11-pending"; mismatch.workspace.name = "Mismatched pending name"; mismatch.semantic_index.vector_store.collection = "p11-pending"; const mismatchPublish = await request(ctx, "catalog-metadata-mismatch", "POST", "/workspaces/publish", { action: "create", workspace: mismatch, baseCommit: baseline.head }); safeErrorEnvelope(mismatchPublish, "workspace_invalid", 400); const after = await registryState(ctx); assertByteIdentical(after, baseline, "registry state after curator-owned refusals"); assert(JSON.stringify((await request(ctx, "current-list-after-negatives", "GET", "/workspaces")).body) === JSON.stringify(current), "workspace listing mutated after negative cases"); await git(ctx, ["fetch", "origin", "main"], { cwd: author }); await git(ctx, ["reset", "--hard", "origin/main"], { cwd: author }); await mkdir(join(author, "workspaces"), { recursive: true }); await atomicWrite(join(author, "workspaces", "legacy.yaml"), "workspace: bad\n", 0o644); await git(ctx, ["add", "--", "workspaces/legacy.yaml"], { cwd: author }); await git(ctx, ["commit", "-m", "Invalid contextual Evidence state"], { cwd: author }); await git(ctx, ["push", "origin", "HEAD:main"], { cwd: author }); const rejectedPull = await request(ctx, "invalid-layout-pull", "POST", "/workspace-registry/pull"); safeErrorEnvelope(rejectedPull, "workspace_invalid", 400); const afterInvalidPull = await registryState(ctx); assertByteIdentical(afterInvalidPull, baseline, "registry state after invalid pull"); return await check("negative_catalog_layout_cases", { secondCreate: true, update: true, delete: true, unknownCatalogId: true, metadataMismatch: true, oldLayoutRejected: true }, ["git"]); } }, { id: "negative_schema_context_cases", run: async () => { const base = structuredClone(ctx.descriptors[0]); const cases = [ ["invalid-uri", (workspace) => { workspace.evidence.source.uri = "/etc/passwd"; }, "evidence.source.uri"], ["invalid-secret-field", (workspace) => { workspace.evidence.source.password = ctx.secretValues.rejected; }, "evidence.source.password"], ["missing-evidence-tree", (workspace) => { workspace.workspace.id = "p11-pending"; workspace.workspace.name = "P1.1 pending"; workspace.workspace.description = "Catalog-only slot awaiting bootstrap"; workspace.semantic_index.vector_store.collection = "p11-pending"; workspace.evidence.source.uri = "p11-pending/evidence"; }, "evidence.source.uri"], ]; const outcomes = []; for (const [id, mutate, field] of cases) { const workspace = structuredClone(base); mutate(workspace); const endpoint = id === "missing-evidence-tree" ? "/workspaces/publish" : "/workspaces/validate"; const payload = id === "missing-evidence-tree" ? { action: "create", workspace, baseCommit: ctx.publishHead } : { workspace }; const response = await request(ctx, `negative-schema-${id}`, "POST", endpoint, payload, false, { case: id, expectedInputField: field }); safeErrorEnvelope(response, "workspace_invalid", 400); outcomes.push({ case: id, status: response.status, field }); } return await check("negative_schema_context_cases", outcomes); } }, { id: "no_p2_scope_artifacts", run: async () => { const forbidden = ["artifacts/evidence", "materialized", "qdrant", "embedding", "ACTIVE", "retention"]; const present = forbidden.filter((path) => existsSync(join(ctx.run.root, path))); assert(present.length === 0, "p2 scope artifacts present"); return await check("no_p2_scope_artifacts", { absent: forbidden }); } }, { id: "secret_scan", run: async () => { const findings = await scanSecrets({ runRoot: ctx.run.root, forbiddenValues: ctx.forbiddenValues, expectedGitRepositories: ["remote.git", "author"] }); assert(findings.length === 0, "secret scan found leaked secret material"); return await check("secret_scan", { findings: 0 }); } }, { id: "cleanup_confinement", run: async () => { const parent = canonicalIntegrationBase(ctx.repositoryRoot); const siblings = (await readdir(parent)).filter((name) => name !== ctx.run.runId); return await check("cleanup_confinement", { listenerState: ctx.run.listeners[0].state, siblingCount: siblings.length }); } }, ]; } async function setupContext({ repositoryRoot = defaultRepositoryRoot, env = process.env } = {}) { const run = await createOwnedRun({ repositoryRoot }); const provenance = await collectRepositoryProvenance({ repositoryRoot }); const executables = resolveExecutables(repositoryRoot); const harnessDir = realpathSync(join(repositoryRoot, "harness")); const ownedHome = join(run.root, "installation", "runtime", "acceptance-home"); const ownedTmp = join(run.root, "installation", "runtime", "tmp"); await mkdir(ownedHome, { recursive: true, mode: 0o700 }); await mkdir(ownedTmp, { recursive: true, mode: 0o700 }); const executablePath = [...new Set([dirname(executables.gitPath), dirname(executables.pythonPath), dirname(executables.thtPath)])].join(":"); const fixtureEnv = { PATH: executablePath, HOME: ownedHome, TMPDIR: ownedTmp, HOST: "127.0.0.1", PORT: "0", AUTH_MODE: "none", THT_BIN: executables.thtPath, THT_HARNESS_DIR: harnessDir, THT_DATA_ROOT: join(run.root, "installation", "data"), SETTINGS_FILE: join(run.root, "installation", "data", "settings.json"), MAINTENANCE_STATE_FILE: join(run.root, "installation", "data", "maintenance.json"), THT_WORKSPACE_REGISTRY_ROOT: join(run.root, "installation", "registry"), THT_WORKSPACE_GIT_REMOTE: join(run.root, "remote.git"), THT_WORKSPACE_GIT_BRANCH: "main", THT_WORKSPACE_GIT_AUTHOR_NAME: "P1 API Publisher", THT_WORKSPACE_GIT_AUTHOR_EMAIL: "p1-api@example.invalid", THT_WORKSPACE_INSTALLATION_ID: "p11-acceptance", THT_WORKSPACE_SECRET_ROOTS: join(run.root, "fixture-secrets"), THT_HOME: join(run.root, "installation", "runtime", "tht-home"), PYTHONDONTWRITEBYTECODE: "1", PYTHONNOUSERSITE: "1", }; const ctx = { run, repositoryRoot: canonicalRoot(repositoryRoot), provenance, executables, descriptors: descriptors(), env: buildSafeEnvironment({ ambient: env, fixture: fixtureEnv }), forbiddenValues: [], }; await createTopology(run); await setupSecrets(ctx); return ctx; } export async function runIntegration({ repositoryRoot = defaultRepositoryRoot, keep = false, env = process.env, announce } = {}) { const ctx = await setupContext({ repositoryRoot, env }); const priorEnv = {}; for (const [key, value] of Object.entries(ctx.env)) { priorEnv[key] = process.env[key]; process.env[key] = value; } let success = false; try { const checks = await productionChecks(ctx); const results = await executeChecks({ checks }); const report = { schemaVersion: 1, runId: ctx.run.runId, startedAt: ctx.run.startedAt, finishedAt: nowIso(), command: "p11-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 { await stopBackend(ctx).catch(() => {}); for (const [key, value] of Object.entries(ctx.env)) { if (priorEnv[key] === undefined) delete process.env[key]; else process.env[key] = priorEnv[key]; } } } 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: p11-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; } }