fix: complete P1 integration audit trail
This commit is contained in:
+514
-148
@@ -8,6 +8,7 @@ import {
|
||||
access, chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, symlink, writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { Socket, isIP } from "node:net";
|
||||
import {
|
||||
basename, dirname, isAbsolute, join, relative, resolve, sep,
|
||||
} from "node:path";
|
||||
@@ -16,6 +17,7 @@ import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
let commandEventSink;
|
||||
let activeCommandCheckId;
|
||||
const RUN_ID = /^p1-[0-9a-f]{32}$/;
|
||||
const HEX40 = /^[0-9a-f]{40}$/;
|
||||
const HEX64 = /^[0-9a-f]{64}$/;
|
||||
@@ -86,16 +88,40 @@ async function atomicWrite(path, bytes, mode = 0o600) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function ownership(run, listener = run.listener) {
|
||||
function exactOwnedResources(run) {
|
||||
const contextual = join(run.root, "installation", "runtime", "contextual");
|
||||
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"),
|
||||
contextual,
|
||||
join(contextual, "remote.git"),
|
||||
join(contextual, "author"),
|
||||
join(contextual, "registry"),
|
||||
join(contextual, "data"),
|
||||
join(contextual, "runtime"),
|
||||
];
|
||||
}
|
||||
function initialListeners(pid) {
|
||||
return ["primary", "contextual"].map((name) => ({
|
||||
name, kind: "fastify", host: "127.0.0.1", requestedPort: 0, pid, state: "not_started",
|
||||
}));
|
||||
}
|
||||
function ownership(run, listeners = run.listeners) {
|
||||
return {
|
||||
schemaVersion: 1, runId: run.runId, runNonce: run.nonce, root: run.root,
|
||||
repositoryRoot: run.repositoryRoot, startedAt: run.startedAt, pid: run.pid,
|
||||
listener,
|
||||
resources: [run.root, { kind: "fastify", host: "127.0.0.1", requestedPort: 0, pid: run.pid }],
|
||||
listeners,
|
||||
resources: exactOwnedResources(run),
|
||||
};
|
||||
}
|
||||
async function writeOwnership(run, listener = run.listener) {
|
||||
run.listener = listener;
|
||||
async function writeOwnership(run, listenerUpdate) {
|
||||
if (listenerUpdate) {
|
||||
run.listeners = run.listeners.map((listener) => listener.name === listenerUpdate.name ? listenerUpdate : listener);
|
||||
}
|
||||
await atomicWrite(join(run.root, "ownership.json"), `${JSON.stringify(ownership(run), null, 2)}\n`);
|
||||
}
|
||||
export async function createOwnedRun({ repositoryRoot, runId, nonce, now, pid } = {}) {
|
||||
@@ -110,7 +136,7 @@ export async function createOwnedRun({ repositoryRoot, runId, nonce, now, pid }
|
||||
const run = {
|
||||
repositoryRoot: repo, root, runId: id, nonce: nonce ?? randomBytes(32).toString("hex"),
|
||||
startedAt: now ?? nowIso(), pid: pid ?? process.pid,
|
||||
listener: { kind: "fastify", host: "127.0.0.1", requestedPort: 0, pid: pid ?? process.pid, state: "not_started" },
|
||||
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 });
|
||||
@@ -119,12 +145,19 @@ export async function createOwnedRun({ repositoryRoot, runId, nonce, now, pid }
|
||||
}
|
||||
function strictOwnership(value, run, expectedNonce) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("ownership is malformed");
|
||||
const expected = ownership(run, value.listener);
|
||||
const validListeners = Array.isArray(value.listeners) && value.listeners.length === 2
|
||||
&& value.listeners.every((listener, index) => {
|
||||
const expectedName = ["primary", "contextual"][index];
|
||||
const common = listener?.name === expectedName && listener.kind === "fastify" && listener.host === "127.0.0.1"
|
||||
&& listener.requestedPort === 0 && listener.pid === process.pid && ["not_started", "listening", "closed"].includes(listener.state);
|
||||
return common && (listener.state === "not_started"
|
||||
? !("actualPort" in listener)
|
||||
: Number.isInteger(listener.actualPort) && listener.actualPort >= 1 && listener.actualPort <= 65535);
|
||||
});
|
||||
if (value.schemaVersion !== 1 || value.runId !== run.runId || value.runNonce !== expectedNonce
|
||||
|| value.root !== run.root || value.repositoryRoot !== run.repositoryRoot || value.pid !== process.pid
|
||||
|| !ISO_UTC.test(value.startedAt ?? "") || !value.listener || value.listener.kind !== "fastify"
|
||||
|| value.listener.host !== "127.0.0.1" || value.listener.requestedPort !== 0 || value.listener.pid !== process.pid
|
||||
|| JSON.stringify(value.resources) !== JSON.stringify(expected.resources)) throw new Error("ownership identity mismatch");
|
||||
|| !ISO_UTC.test(value.startedAt ?? "") || !validListeners
|
||||
|| JSON.stringify(value.resources) !== JSON.stringify(exactOwnedResources(run))) throw new Error("ownership identity mismatch");
|
||||
return value;
|
||||
}
|
||||
export async function readAndValidateOwnership({ repositoryRoot, runRoot, expectedNonce }) {
|
||||
@@ -141,9 +174,10 @@ export async function readAndValidateOwnership({ repositoryRoot, runRoot, expect
|
||||
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, listener: value.listener,
|
||||
startedAt: value.startedAt, pid: process.pid,
|
||||
}, expectedNonce);
|
||||
}
|
||||
|
||||
export async function cleanupOwnedRun({ repositoryRoot, runRoot, expectedNonce }) {
|
||||
const value = await readAndValidateOwnership({ repositoryRoot, runRoot, expectedNonce });
|
||||
const base = canonicalIntegrationBase(repositoryRoot);
|
||||
@@ -163,6 +197,8 @@ export async function runCommand(options) {
|
||||
for (const key of Object.keys(options)) if (!allowed.has(key)) throw new Error(`unsupported command option ${key}`);
|
||||
const { executable, argv, cwd, env, timeoutMs = 30_000, stdin, maxOutputBytes = MAX_OUTPUT } = options;
|
||||
if (typeof executable !== "string" || executable.length === 0 || /[;&|`$><\n\r]/.test(executable)) throw new Error("command executable is invalid");
|
||||
const allowlistedExecutable = executable === "git" || (isAbsolute(executable) && basename(executable) === "tht");
|
||||
if (!allowlistedExecutable) throw new Error("command executable is not allowlisted");
|
||||
if (!Array.isArray(argv) || argv.some((value) => typeof value !== "string")) throw new Error("command argv must be a string array");
|
||||
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000) throw new Error("command timeout is invalid");
|
||||
return await new Promise((resolvePromise, reject) => {
|
||||
@@ -173,6 +209,7 @@ export async function runCommand(options) {
|
||||
executable: basename(executable),
|
||||
argvLabels: argv.map((value) => isAbsolute(value) || value.includes(sep) ? "[path]" : /^[a-z]+:\/\//i.test(value) ? "[url]" : value.length > 80 ? "[value]" : value),
|
||||
outcome: error ? "FAIL" : "PASS",
|
||||
...(activeCommandCheckId ? { checkId: activeCommandCheckId } : {}),
|
||||
});
|
||||
if (error) Object.assign(error, { result });
|
||||
error ? reject(error) : resolvePromise(result);
|
||||
@@ -207,6 +244,7 @@ export function validateReport(report) {
|
||||
|| !ISO_UTC.test(report.finishedAt ?? "") || typeof report.command !== "string" || forbiddenKey(report)
|
||||
|| !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) || !["PASS", "FAIL"].includes(check.status)
|
||||
|| !ISO_UTC.test(check.startedAt ?? "") || !ISO_UTC.test(check.finishedAt ?? "")
|
||||
@@ -216,6 +254,10 @@ export function validateReport(report) {
|
||||
return !HEX64.test(sha256 ?? "");
|
||||
})) throw new Error("report check is invalid");
|
||||
ids.add(check.id);
|
||||
for (const artifact of check.artifacts) {
|
||||
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;
|
||||
@@ -240,10 +282,11 @@ async function walkFiles(root, current = root, out = []) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
async function gitObjectFindings(runRoot, forbiddenValues) {
|
||||
async function gitObjectFindings(runRoot, forbiddenValues, expectedGitRepositories) {
|
||||
const findings = [];
|
||||
for (const directory of [join(runRoot, "remote.git"), join(runRoot, "author")]) {
|
||||
if (!existsSync(directory)) continue;
|
||||
for (const rel of expectedGitRepositories) {
|
||||
const directory = join(runRoot, rel);
|
||||
if (!existsSync(directory)) throw new Error(`Git secret scan failed closed: missing expected Git repository: ${rel}`);
|
||||
const args = basename(directory) === "remote.git" ? ["--git-dir", directory] : ["-C", directory];
|
||||
let objects;
|
||||
try {
|
||||
@@ -263,12 +306,12 @@ async function gitObjectFindings(runRoot, forbiddenValues) {
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
export async function scanSecrets({ runRoot, forbiddenValues, virtualFiles = [] }) {
|
||||
export async function scanSecrets({ runRoot, forbiddenValues, virtualFiles = [], expectedGitRepositories = ["remote.git", "author"] }) {
|
||||
const values = forbiddenValues.filter((value) => typeof value === "string" && value.length >= 8);
|
||||
const findings = [];
|
||||
for (const file of await walkFiles(runRoot)) if (containsAny(await readFile(file.path), values)) findings.push({ path: file.rel });
|
||||
for (const file of virtualFiles) if (containsAny(Buffer.from(file.bytes), values)) findings.push({ path: file.path });
|
||||
findings.push(...await gitObjectFindings(runRoot, values));
|
||||
findings.push(...await gitObjectFindings(runRoot, values, expectedGitRepositories));
|
||||
return findings;
|
||||
}
|
||||
export function negativeRequestEvidence(caseLabel, expectedInputField) {
|
||||
@@ -276,14 +319,19 @@ export function negativeRequestEvidence(caseLabel, expectedInputField) {
|
||||
return { case: caseLabel, expectedInputField };
|
||||
}
|
||||
|
||||
function loopbackOrigin(value) {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port) throw new Error("owned API must be loopback HTTP");
|
||||
return url.origin;
|
||||
}
|
||||
|
||||
export function installExternalFetchGuard(ownedBaseUrl, fetchImplementation = globalThis.fetch) {
|
||||
const owned = new URL(ownedBaseUrl);
|
||||
if (owned.protocol !== "http:" || owned.hostname !== "127.0.0.1" || !owned.port) throw new Error("owned API must be loopback HTTP");
|
||||
const ownedOrigin = loopbackOrigin(ownedBaseUrl);
|
||||
const externalAttempts = [];
|
||||
const guardedFetch = async (input, init) => {
|
||||
const candidate = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
||||
if (candidate.origin !== owned.origin) {
|
||||
externalAttempts.push({ protocol: candidate.protocol, loopback: candidate.hostname === "127.0.0.1" });
|
||||
if (candidate.origin !== ownedOrigin) {
|
||||
externalAttempts.push({ transport: "fetch", protocol: candidate.protocol, loopback: candidate.hostname === "127.0.0.1" });
|
||||
throw new Error("external fetch prohibited");
|
||||
}
|
||||
return await fetchImplementation(input, init);
|
||||
@@ -291,6 +339,57 @@ export function installExternalFetchGuard(ownedBaseUrl, fetchImplementation = gl
|
||||
return { fetch: guardedFetch, externalAttempts };
|
||||
}
|
||||
|
||||
function socketDestination(args) {
|
||||
const first = Array.isArray(args[0]) ? args[0][0] : args[0];
|
||||
if (typeof first === "object" && first !== null) {
|
||||
if (first.path !== undefined) return { path: String(first.path) };
|
||||
return { host: String(first.host ?? first.hostname ?? "localhost"), port: Number(first.port) };
|
||||
}
|
||||
if (typeof first === "number") return { host: typeof args[1] === "string" ? args[1] : "localhost", port: first };
|
||||
return { path: String(first) };
|
||||
}
|
||||
|
||||
export function installNetworkGuard(fetchImplementation = globalThis.fetch) {
|
||||
if (typeof fetchImplementation !== "function") throw new Error("global fetch is unavailable");
|
||||
const ownedOrigins = new Set();
|
||||
const externalAttempts = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalConnect = Socket.prototype.connect;
|
||||
const isOwned = (host, port) => {
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) return false;
|
||||
const normalized = host === "localhost" || host === "::1" ? "127.0.0.1" : host;
|
||||
return isIP(normalized) !== 0 && normalized === "127.0.0.1" && ownedOrigins.has(`http://127.0.0.1:${port}`);
|
||||
};
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const candidate = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
||||
if (!ownedOrigins.has(candidate.origin)) {
|
||||
externalAttempts.push({ transport: "fetch", protocol: candidate.protocol, loopback: candidate.hostname === "127.0.0.1" });
|
||||
throw new Error("external network connection prohibited");
|
||||
}
|
||||
return await fetchImplementation(input, init);
|
||||
};
|
||||
Socket.prototype.connect = function guardedSocketConnect(...args) {
|
||||
const destination = socketDestination(args);
|
||||
if (!("host" in destination) || !isOwned(destination.host, destination.port)) {
|
||||
externalAttempts.push({ transport: "socket", loopback: destination.host === "127.0.0.1" });
|
||||
throw new Error("external network connection prohibited");
|
||||
}
|
||||
return originalConnect.apply(this, args);
|
||||
};
|
||||
let restored = false;
|
||||
return {
|
||||
externalAttempts,
|
||||
addOwnedOrigin(value) { ownedOrigins.add(loopbackOrigin(value)); },
|
||||
hasOwnedOrigin(value) { return ownedOrigins.has(loopbackOrigin(value)); },
|
||||
restore() {
|
||||
if (restored) return;
|
||||
restored = true;
|
||||
globalThis.fetch = originalFetch;
|
||||
Socket.prototype.connect = originalConnect;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeForEvidence(value, forbiddenValues = []) {
|
||||
if (typeof value === "string") {
|
||||
let safe = value;
|
||||
@@ -319,10 +418,15 @@ export async function executeChecks({ checks, failAt, recorder } = {}) {
|
||||
} else {
|
||||
try {
|
||||
const output = await scenario.run();
|
||||
if (scenario.id === failAt) throw new Error("injected acceptance failure");
|
||||
if (scenario.id === failAt) {
|
||||
const injected = new Error("injected acceptance failure");
|
||||
injected.acceptancePartial = { commands: output.commands ?? [], artifacts: output.artifacts ?? [] };
|
||||
throw injected;
|
||||
}
|
||||
result = { id: scenario.id, status: "PASS", startedAt, finishedAt: nowIso(), commands: output.commands ?? [], artifacts: output.artifacts ?? [] };
|
||||
} catch {
|
||||
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: "Acceptance scenario failed safely." };
|
||||
} catch (error) {
|
||||
const partial = error?.acceptancePartial ?? {};
|
||||
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: partial.commands ?? [], artifacts: partial.artifacts ?? [], error: "Acceptance scenario failed safely." };
|
||||
stopped = true;
|
||||
}
|
||||
}
|
||||
@@ -365,6 +469,8 @@ async function setupSecrets(ctx) {
|
||||
session: `SESSION-${randomBytes(16).toString("hex")}`,
|
||||
rejected: `REJECTED-${randomBytes(16).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"),
|
||||
@@ -374,8 +480,6 @@ async function setupSecrets(ctx) {
|
||||
await atomicWrite(paths.access, scalarSecretBytes(values.access));
|
||||
await atomicWrite(paths.secret, scalarSecretBytes(values.secret));
|
||||
await atomicWrite(paths.session, scalarSecretBytes(values.session));
|
||||
ctx.forbiddenValues = Object.values(values);
|
||||
ctx.secretValues = values;
|
||||
const env = {};
|
||||
for (const workspace of ctx.descriptors) {
|
||||
const ns = namespace(workspace.workspace.id); const prefix = `THT_WS_${ns}`;
|
||||
@@ -415,50 +519,78 @@ async function loadProductionBackend() {
|
||||
]);
|
||||
return { loadConfig, buildApp, WorkspaceRegistry, ThtRunner };
|
||||
}
|
||||
async function startBackend(ctx) {
|
||||
async function startProductionBackend(ctx, { name, env, runtimeConfigPath }) {
|
||||
const { loadConfig, buildApp, WorkspaceRegistry, ThtRunner } = await loadProductionBackend();
|
||||
const config = loadConfig(ctx.env);
|
||||
ctx.registryConfig = config.workspaceRegistry;
|
||||
ctx.registry = new WorkspaceRegistry(ctx.registryConfig);
|
||||
ctx.thtRunner = new ThtRunner({
|
||||
thtBin: config.thtBin, harnessDir: config.harnessDir, configPath: join(ctx.run.root, "installation", "runtime", "base.yaml"),
|
||||
const config = loadConfig(env);
|
||||
const registry = new WorkspaceRegistry(config.workspaceRegistry);
|
||||
const thtRunner = new ThtRunner({
|
||||
thtBin: config.thtBin, harnessDir: config.harnessDir, configPath: runtimeConfigPath,
|
||||
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 },
|
||||
});
|
||||
ctx.app = buildApp(config, { thtRunner: ctx.thtRunner, workspaceRegistry: ctx.registry });
|
||||
const address = await ctx.app.listen({ host: "127.0.0.1", port: 0 });
|
||||
const url = new URL(address); ctx.baseUrl = `http://127.0.0.1:${url.port}`;
|
||||
const fetchGuard = installExternalFetchGuard(ctx.baseUrl, ctx.originalFetch);
|
||||
ctx.guardedFetch = fetchGuard.fetch; ctx.externalAttempts = fetchGuard.externalAttempts;
|
||||
globalThis.fetch = ctx.guardedFetch;
|
||||
await writeOwnership(ctx.run, { kind: "fastify", host: "127.0.0.1", requestedPort: 0, actualPort: Number(url.port), pid: process.pid, state: "listening" });
|
||||
const app = buildApp(config, { thtRunner, workspaceRegistry: registry });
|
||||
let address;
|
||||
try {
|
||||
address = await app.listen({ host: "127.0.0.1", port: 0 });
|
||||
} catch (error) {
|
||||
await app.close().catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
const url = new URL(address);
|
||||
const baseUrl = `http://127.0.0.1:${url.port}`;
|
||||
ctx.networkGuard.addOwnedOrigin(baseUrl);
|
||||
const service = { name, app, baseUrl, registry, thtRunner, config };
|
||||
ctx.services.push(service);
|
||||
await writeOwnership(ctx.run, {
|
||||
name, kind: "fastify", host: "127.0.0.1", requestedPort: 0,
|
||||
actualPort: Number(url.port), pid: process.pid, state: "listening",
|
||||
});
|
||||
return service;
|
||||
}
|
||||
|
||||
async function startBackend(ctx) {
|
||||
const service = await startProductionBackend(ctx, {
|
||||
name: "primary", env: ctx.env,
|
||||
runtimeConfigPath: join(ctx.run.root, "installation", "runtime", "base.yaml"),
|
||||
});
|
||||
ctx.registryConfig = service.config.workspaceRegistry;
|
||||
ctx.registry = service.registry;
|
||||
ctx.thtRunner = service.thtRunner;
|
||||
ctx.app = service.app;
|
||||
ctx.baseUrl = service.baseUrl;
|
||||
}
|
||||
|
||||
export function exportArchiveEvidencePath(requestId) {
|
||||
if (!/^export-[a-z0-9-]+$/.test(requestId)) throw new Error("invalid export request id");
|
||||
return `exports/raw/${requestId}.zip`;
|
||||
}
|
||||
async function request(ctx, id, method, path, body, binary = false, requestEvidence) {
|
||||
function trackArtifact(ctx, artifact) {
|
||||
if (ctx.activeArtifacts && !ctx.activeArtifacts.some(({ path }) => path === artifact.path)) ctx.activeArtifacts.push(artifact);
|
||||
return artifact;
|
||||
}
|
||||
|
||||
async function request(ctx, id, method, path, body, binary = false, requestEvidence, baseUrl = ctx.baseUrl) {
|
||||
const requestSummary = requestEvidence === undefined
|
||||
? { method, path, ...(body === undefined ? {} : { body: sanitizeForEvidence(body, ctx.forbiddenValues) }) }
|
||||
: { method, path, input: requestEvidence };
|
||||
await evidence(ctx.run, `requests/${id}.json`, requestSummary, ctx.forbiddenValues);
|
||||
trackArtifact(ctx, await evidence(ctx.run, `requests/${id}.json`, requestSummary, ctx.forbiddenValues));
|
||||
ctx.httpRequests.push({ method, path });
|
||||
const response = await ctx.guardedFetch(`${ctx.baseUrl}${path}`, {
|
||||
const response = await globalThis.fetch(`${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, exportArchiveEvidencePath(id)), bytes);
|
||||
await evidence(ctx.run, `responses/${id}.json`, { status: response.status, contentType: response.headers.get("content-type"), bytes: bytes.length });
|
||||
trackArtifact(ctx, await evidence(ctx.run, `responses/${id}.json`, { status: response.status, contentType: response.headers.get("content-type"), bytes: bytes.length }));
|
||||
return { status: response.status, bytes };
|
||||
}
|
||||
const text = await response.text(); let parsed;
|
||||
try { parsed = text ? JSON.parse(text) : null; } catch { parsed = { invalidJson: true }; }
|
||||
const safe = sanitizeForEvidence(parsed, ctx.forbiddenValues);
|
||||
await evidence(ctx.run, `responses/${id}.json`, { status: response.status, body: safe }, ctx.forbiddenValues);
|
||||
trackArtifact(ctx, await evidence(ctx.run, `responses/${id}.json`, { status: response.status, body: safe }, ctx.forbiddenValues));
|
||||
return { status: response.status, body: parsed };
|
||||
}
|
||||
async function extractZip(ctx, id, bytes) {
|
||||
@@ -489,6 +621,12 @@ async function extractZip(ctx, id, bytes) {
|
||||
return manifest;
|
||||
}
|
||||
function assert(condition, message) { if (!condition) throw new Error(message); }
|
||||
function assertGenericWorkspaceInvalid(response, label) {
|
||||
assert(response.status === 400 && response.body?.code === "workspace_invalid", `${label} was not rejected through HTTP`);
|
||||
assert(Object.keys(response.body).sort().join(",") === "code,message", `${label} response envelope was not exact`);
|
||||
assert(response.body.message === "Workspace request or bundle is invalid.", `${label} response message was not generic`);
|
||||
assert(!/fatal:|stderr|git command|rev-parse|ls-tree/i.test(JSON.stringify(response.body)), `${label} exposed Git stderr`);
|
||||
}
|
||||
async function snapshotDigest(path) {
|
||||
const files = await walkFiles(path); const result = {};
|
||||
for (const file of files) result[file.rel] = sha256(await readFile(file.path));
|
||||
@@ -505,9 +643,10 @@ export function buildSafeEnvironment({ ambient = process.env, fixture = {} } = {
|
||||
return safe;
|
||||
}
|
||||
|
||||
async function setupContext(run, repositoryRoot, env) {
|
||||
async function setupContext(run, repositoryRoot, env, ctx = {}) {
|
||||
const thtBin = realpathSync(env.THT_BIN ?? join(repositoryRoot, "harness", ".venv", "bin", "tht"));
|
||||
const harnessDir = realpathSync(join(repositoryRoot, "harness"));
|
||||
const gitTracePath = join(run.root, "logs", "production-git-trace.jsonl");
|
||||
const fixtureEnv = {
|
||||
HOST: "127.0.0.1", PORT: "0", AUTH_MODE: "none", THT_BIN: thtBin,
|
||||
THT_HARNESS_DIR: harnessDir, THT_DATA_ROOT: join(run.root, "installation", "data"),
|
||||
@@ -518,26 +657,73 @@ async function setupContext(run, repositoryRoot, env) {
|
||||
THT_WORKSPACE_GIT_AUTHOR_NAME: "P1 API Publisher", THT_WORKSPACE_GIT_AUTHOR_EMAIL: "p1-api@example.invalid",
|
||||
THT_WORKSPACE_INSTALLATION_ID: "p1-acceptance", THT_WORKSPACE_SECRET_ROOTS: join(run.root, "fixture-secrets"),
|
||||
THT_HOME: join(run.root, "installation", "runtime", "tht-home"),
|
||||
GIT_TRACE2_EVENT: gitTracePath,
|
||||
};
|
||||
const ctx = {
|
||||
run, repositoryRoot, descriptors: descriptors(), forbiddenValues: [],
|
||||
Object.assign(ctx, {
|
||||
run, repositoryRoot, descriptors: descriptors(), forbiddenValues: ctx.forbiddenValues ?? [],
|
||||
env: buildSafeEnvironment({ ambient: env, fixture: fixtureEnv }),
|
||||
originalFetch: globalThis.fetch, httpRequests: [], externalAttempts: [], prohibitedInvocations: [],
|
||||
};
|
||||
await createTopology(run); await setupSecrets(ctx);
|
||||
for (const [name, value] of Object.entries(ctx.env)) process.env[name] = value;
|
||||
httpRequests: [], services: [], gitTracePath,
|
||||
expectedGitRepositories: ["remote.git", "author"],
|
||||
});
|
||||
await createTopology(run);
|
||||
await setupSecrets(ctx);
|
||||
return ctx;
|
||||
}
|
||||
async function registryState(ctx) {
|
||||
const root = join(ctx.run.root, "installation", "registry");
|
||||
const bytes = async (path) => sha256(await readFile(path));
|
||||
|
||||
async function treeHash(path, excludedPrefixes = []) {
|
||||
const digest = await snapshotDigest(path);
|
||||
for (const key of Object.keys(digest)) if (excludedPrefixes.some((prefix) => key === prefix || key.startsWith(`${prefix}/`))) delete digest[key];
|
||||
return sha256(JSON.stringify(digest));
|
||||
}
|
||||
async function checkoutSemanticState(path) {
|
||||
const head = await git(["rev-parse", "HEAD"], { cwd: path });
|
||||
const branch = await git(["symbolic-ref", "--short", "HEAD"], { cwd: path });
|
||||
const statusResult = await git(["status", "--porcelain=v1"], { cwd: path });
|
||||
const indexTree = await git(["write-tree"], { cwd: path });
|
||||
const refs = await git(["show-ref"], { cwd: path });
|
||||
return {
|
||||
active: await bytes(join(root, "state", "active.json")),
|
||||
snapshots: sha256(JSON.stringify(await snapshotDigest(join(root, "snapshots")))),
|
||||
checkoutHead: await bytes(join(root, "repo", ".git", "refs", "heads", "main")),
|
||||
remoteHead: await bytes(join(ctx.run.root, "remote.git", "refs", "heads", "main")),
|
||||
head: head.stdout.trim(), branch: branch.stdout.trim(), status: statusResult.stdout,
|
||||
indexTree: indexTree.stdout.trim(), refs: sha256(refs.stdout),
|
||||
worktree: await treeHash(path, [".git"]),
|
||||
};
|
||||
}
|
||||
async function bareSemanticState(path, branch) {
|
||||
const [head, tree, refs] = await Promise.all([
|
||||
git(["--git-dir", path, "rev-parse", `refs/heads/${branch}`]),
|
||||
git(["--git-dir", path, "rev-parse", `refs/heads/${branch}^{tree}`]),
|
||||
git(["--git-dir", path, "show-ref"]),
|
||||
]);
|
||||
return { head: head.stdout.trim(), tree: tree.stdout.trim(), refs: sha256(refs.stdout) };
|
||||
}
|
||||
async function primarySemanticState(ctx) {
|
||||
return {
|
||||
remote: await bareSemanticState(join(ctx.run.root, "remote.git"), "main"),
|
||||
author: await checkoutSemanticState(join(ctx.run.root, "author")),
|
||||
checkout: await checkoutSemanticState(join(ctx.run.root, "installation", "registry", "repo")),
|
||||
active: await treeHash(join(ctx.run.root, "installation", "registry", "state")),
|
||||
snapshots: await treeHash(join(ctx.run.root, "installation", "registry", "snapshots")),
|
||||
data: await treeHash(join(ctx.run.root, "installation", "data")),
|
||||
runtime: await treeHash(join(ctx.run.root, "installation", "runtime"), ["contextual"]),
|
||||
};
|
||||
}
|
||||
async function registryState(ctx) {
|
||||
return await primarySemanticState(ctx);
|
||||
}
|
||||
async function contextualSemanticState(root) {
|
||||
return {
|
||||
remote: await bareSemanticState(join(root, "remote.git"), "invalid-context"),
|
||||
author: await checkoutSemanticState(join(root, "author")),
|
||||
checkout: await checkoutSemanticState(join(root, "registry", "repo")),
|
||||
active: await treeHash(join(root, "registry", "state")),
|
||||
snapshots: await treeHash(join(root, "registry", "snapshots")),
|
||||
data: await treeHash(join(root, "data")),
|
||||
runtime: await treeHash(join(root, "runtime")),
|
||||
};
|
||||
}
|
||||
async function invariantArtifact(ctx, path, value) {
|
||||
return trackArtifact(ctx, await evidence(ctx.run, path, value, ctx.forbiddenValues));
|
||||
}
|
||||
|
||||
function assertByteIdentical(left, right, label) {
|
||||
assert(JSON.stringify(left) === JSON.stringify(right), `${label} state changed`);
|
||||
}
|
||||
@@ -581,9 +767,79 @@ function assertRuntimeContract(ctx, id, parsed, revision) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function traceSize(path) {
|
||||
try { return (await stat(path)).size; } catch (error) { if (error.code === "ENOENT") return 0; throw error; }
|
||||
}
|
||||
function uniqueArtifacts(artifacts) {
|
||||
const seen = new Set();
|
||||
return artifacts.filter((artifact) => !seen.has(artifact.path) && seen.add(artifact.path));
|
||||
}
|
||||
async function commandsObservedForCheck(ctx, checkId, traceBefore) {
|
||||
const commands = new Set((commandEventSink ?? []).filter((event) => event.checkId === checkId).map((event) => event.executable));
|
||||
if (await traceSize(ctx.gitTracePath) > traceBefore) commands.add("git");
|
||||
return [...commands].sort();
|
||||
}
|
||||
function wrapProductionCheck(ctx, scenario) {
|
||||
return {
|
||||
id: scenario.id,
|
||||
run: async () => {
|
||||
ctx.activeArtifacts = [];
|
||||
activeCommandCheckId = scenario.id;
|
||||
const traceBefore = await traceSize(ctx.gitTracePath);
|
||||
try {
|
||||
const output = await scenario.run();
|
||||
return {
|
||||
commands: await commandsObservedForCheck(ctx, scenario.id, traceBefore),
|
||||
artifacts: uniqueArtifacts([...(output.artifacts ?? []), ...ctx.activeArtifacts]),
|
||||
};
|
||||
} catch (error) {
|
||||
error.acceptancePartial = {
|
||||
commands: await commandsObservedForCheck(ctx, scenario.id, traceBefore),
|
||||
artifacts: uniqueArtifacts(ctx.activeArtifacts),
|
||||
};
|
||||
throw error;
|
||||
} finally {
|
||||
activeCommandCheckId = undefined;
|
||||
ctx.activeArtifacts = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function assertProductionGitTrace(ctx) {
|
||||
const text = await readFile(ctx.gitTracePath, "utf8");
|
||||
const events = text.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
||||
const productionStarts = events.filter((event) => event.event === "start" && Array.isArray(event.argv)
|
||||
&& event.argv.some((arg) => typeof arg === "string" && arg.startsWith("core.hooksPath=")));
|
||||
const publication = productionStarts.some(({ argv }) => argv.includes("commit") && argv.some((arg) => /^Publish workspace /.test(arg)));
|
||||
const pull = productionStarts.some(({ argv }) => argv.includes("fetch"));
|
||||
assert(publication && pull, "production Git publication/pull operations absent from Trace2 evidence");
|
||||
return { eventCount: events.length, productionStartCount: productionStarts.length, publication, pull };
|
||||
}
|
||||
|
||||
async function assertNoP1ScopeEntrypoints(ctx) {
|
||||
const workspacesRoot = join(ctx.repositoryRoot, "backend", "dist", "workspaces");
|
||||
const modules = (await readdir(workspacesRoot)).filter((name) => name.endsWith(".js"));
|
||||
const forbiddenModuleNames = modules.filter((name) => /evidence[-_.]?(?:adapter|acquisition|preprocess)|(?:acquisition|preprocess)[-_.]?evidence/i.test(name));
|
||||
const forbiddenExports = [];
|
||||
for (const name of modules) {
|
||||
const source = await readFile(join(workspacesRoot, name), "utf8");
|
||||
if (/export\s+(?:class|function|const)\s+(?:acquire|preprocess)Evidence|export\s+(?:class|function|const)\s+Evidence(?:Adapter|Acquisition|Preprocessor)/.test(source)) forbiddenExports.push(name);
|
||||
}
|
||||
const routeSurfaces = ctx.services.map(({ name, app }) => ({ name, routes: app.printRoutes({ commonPrefix: false }) }));
|
||||
const forbiddenRoutes = routeSurfaces.filter(({ routes }) => /\/(?:evidence|acquisition|preprocess)(?:\W|$)/i.test(routes));
|
||||
const packageJson = JSON.parse(await readFile(join(ctx.repositoryRoot, "backend", "package.json"), "utf8"));
|
||||
const entrypointBytes = JSON.stringify({ main: packageJson.main, bin: packageJson.bin, exports: packageJson.exports, scripts: packageJson.scripts });
|
||||
const forbiddenPackageEntrypoints = /(?:acquire|preprocess)Evidence|Evidence(?:Adapter|Acquisition|Preprocessor)/i.test(entrypointBytes);
|
||||
assert(forbiddenModuleNames.length === 0 && forbiddenExports.length === 0 && forbiddenRoutes.length === 0 && !forbiddenPackageEntrypoints,
|
||||
"prohibited P1 adapter/acquisition/preprocessing entrypoint surface present");
|
||||
return { moduleFilesAudited: modules.sort(), routeAppsAudited: routeSurfaces.map(({ name }) => name), packageEntrypointsAudited: true };
|
||||
}
|
||||
|
||||
function productionChecks(ctx) {
|
||||
const log = async (id, value) => ({ commands: [], artifacts: [await evidence(ctx.run, `logs/${id}.json`, value, ctx.forbiddenValues)] });
|
||||
return [
|
||||
const scenarios = [
|
||||
{ id: "preflight", run: async () => {
|
||||
const gitVersion = await git(["--version"]); await access(ctx.env.THT_BIN, fsConstants.X_OK);
|
||||
return await log("preflight", { git: gitVersion.stdout.trim(), node: process.version, thtExecutable: true });
|
||||
@@ -598,9 +854,14 @@ function productionChecks(ctx) {
|
||||
} },
|
||||
{ id: "local_git_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`);
|
||||
const descriptorArtifacts = [];
|
||||
for (const workspace of ctx.descriptors) {
|
||||
const path = `fixtures/descriptors/${workspace.workspace.id}.json`;
|
||||
await atomicWrite(join(ctx.run.root, path), `${JSON.stringify(workspace, null, 2)}\n`);
|
||||
descriptorArtifacts.push(await fileArtifact(ctx.run.root, path));
|
||||
}
|
||||
assert(!existsSync(join(ctx.run.root, "author", "workspaces")), "fixture authored a descriptor");
|
||||
return { commands: ["git"], artifacts: [await evidence(ctx.run, "logs/local_git_bootstrap.json", { bootstrapCommit: ctx.bootstrapCommit, descriptorEmpty: true })] };
|
||||
return { artifacts: [await evidence(ctx.run, "logs/local_git_bootstrap.json", { bootstrapCommit: ctx.bootstrapCommit, descriptorEmpty: true }), ...descriptorArtifacts] };
|
||||
} },
|
||||
{ id: "http_validate_publish_pull_read_export", run: async () => {
|
||||
await startBackend(ctx);
|
||||
@@ -746,11 +1007,9 @@ function productionChecks(ctx) {
|
||||
for (const [id, mutate, field] of cases) {
|
||||
const before = await registryState(ctx); const workspace = structuredClone(base); mutate(workspace);
|
||||
const safeInput = negativeRequestEvidence(id, field);
|
||||
await evidence(ctx.run, `fixtures/requests/negative-${id}.json`, safeInput);
|
||||
trackArtifact(ctx, await evidence(ctx.run, `fixtures/requests/negative-${id}.json`, safeInput));
|
||||
const response = await request(ctx, `negative-${id}`, "POST", "/workspaces/validate", { workspace }, false, safeInput);
|
||||
assert(response.status === 400 && response.body?.code === "workspace_invalid", `negative accepted ${id}`);
|
||||
assert(Object.keys(response.body).sort().join(",") === "code,message", `negative response envelope unsafe ${id}`);
|
||||
assert(response.body.message === "Workspace request or bundle is invalid.", `negative response message unsafe ${id}`);
|
||||
assertGenericWorkspaceInvalid(response, `negative ${id}`);
|
||||
assert(JSON.stringify(response.body).includes(ctx.secretValues.rejected) === false, `negative leaked ${id}`);
|
||||
assertByteIdentical(await registryState(ctx), before, `negative ${id}`);
|
||||
outcomes.push({ case: id, status: response.status, code: response.body.code, expectedInputField: field, genericSafeEnvelope: true, stateByteIdentical: true });
|
||||
@@ -758,43 +1017,108 @@ function productionChecks(ctx) {
|
||||
return { commands: ["git"], artifacts: [await evidence(ctx.run, "logs/negative-schema.json", outcomes)] };
|
||||
} },
|
||||
{ id: "negative_context_case", run: async () => {
|
||||
const author = join(ctx.run.root, "author");
|
||||
const contextual = join(ctx.run.root, "installation", "runtime", "contextual");
|
||||
const contextualRemote = join(contextual, "remote.git");
|
||||
const contextualAuthor = join(contextual, "author");
|
||||
const primaryBefore = await primarySemanticState(ctx);
|
||||
assert(primaryBefore.remote.head === ctx.contentCommit, "primary main was not last-valid before contextual scenario");
|
||||
|
||||
await mkdir(contextual, { recursive: true });
|
||||
await git(["clone", "--bare", join(ctx.run.root, "remote.git"), contextualRemote], { cwd: contextual });
|
||||
await git(["clone", contextualRemote, contextualAuthor], { cwd: contextual });
|
||||
await git(["config", "user.name", "P1 Context Curator"], { cwd: contextualAuthor });
|
||||
await git(["config", "user.email", "p1-context@example.invalid"], { cwd: contextualAuthor });
|
||||
await git(["checkout", "-b", "invalid-context"], { cwd: contextualAuthor });
|
||||
await git(["push", "-u", "origin", "invalid-context"], { cwd: contextualAuthor });
|
||||
await mkdir(join(contextual, "runtime"), { recursive: true });
|
||||
await mkdir(join(contextual, "data"), { recursive: true });
|
||||
await atomicWrite(join(contextual, "runtime", "base.yaml"), "{}\n");
|
||||
const contextualEnv = buildSafeEnvironment({ ambient: ctx.env, fixture: {
|
||||
...ctx.env,
|
||||
THT_DATA_ROOT: join(contextual, "data"),
|
||||
SETTINGS_FILE: join(contextual, "data", "settings.json"),
|
||||
MAINTENANCE_STATE_FILE: join(contextual, "data", "maintenance.json"),
|
||||
THT_WORKSPACE_REGISTRY_ROOT: join(contextual, "registry"),
|
||||
THT_WORKSPACE_GIT_REMOTE: contextualRemote,
|
||||
THT_WORKSPACE_GIT_BRANCH: "invalid-context",
|
||||
THT_WORKSPACE_INSTALLATION_ID: "p1-contextual-acceptance",
|
||||
THT_HOME: join(contextual, "runtime", "tht-home"),
|
||||
} });
|
||||
const contextualService = await startProductionBackend(ctx, {
|
||||
name: "contextual", env: contextualEnv, runtimeConfigPath: join(contextual, "runtime", "base.yaml"),
|
||||
});
|
||||
ctx.expectedGitRepositories.push(
|
||||
"installation/runtime/contextual/remote.git",
|
||||
"installation/runtime/contextual/author",
|
||||
);
|
||||
const contextualStatus = await request(ctx, "context-registry-status", "GET", "/workspace-registry/status", undefined, false, undefined, contextualService.baseUrl);
|
||||
assert(contextualStatus.status === 200 && contextualStatus.body.head === ctx.contentCommit, "contextual registry bootstrap failed");
|
||||
const contextualBaselinePull = await request(ctx, "context-registry-baseline-pull", "POST", "/workspace-registry/pull", undefined, false, undefined, contextualService.baseUrl);
|
||||
assert(contextualBaselinePull.status === 200 && contextualBaselinePull.body.head === ctx.contentCommit, "contextual baseline pull failed");
|
||||
const primaryAfterSetup = await primarySemanticState(ctx);
|
||||
await invariantArtifact(ctx, "logs/negative-context-setup-state.json", { before: primaryBefore, after: primaryAfterSetup });
|
||||
assertByteIdentical(primaryAfterSetup, primaryBefore, "primary state during contextual setup");
|
||||
|
||||
const missing = baseWorkspace("missing-context", { type: "filesystem", uri: "workspace-content/missing-context/evidence", patterns: ["**/*.md"], max_bytes: 100 });
|
||||
const beforePublish = await registryState(ctx);
|
||||
const rejectedPublish = await request(ctx, "context-missing-publish", "POST", "/workspaces/publish", { action: "create", workspace: missing, baseCommit: ctx.contentCommit });
|
||||
assert(rejectedPublish.status === 400 && rejectedPublish.body?.code === "workspace_invalid", "context publish was not rejected through HTTP");
|
||||
assertByteIdentical(await registryState(ctx), beforePublish, "failed contextual publish");
|
||||
await rm(join(author, "workspace-content", "p1-filesystem", "evidence"), { recursive: true });
|
||||
await git(["add", "-A", "workspace-content/p1-filesystem/evidence"], { cwd: author });
|
||||
await git(["commit", "-m", "Invalid contextual Evidence state"], { cwd: author });
|
||||
await git(["push", "origin", "main"], { cwd: author });
|
||||
const invalidRemoteCommit = (await git(["rev-parse", "HEAD"], { cwd: author })).stdout.trim();
|
||||
const remoteBeforePull = sha256(await readFile(join(ctx.run.root, "remote.git", "refs", "heads", "main")));
|
||||
const activeBeforePull = sha256(await readFile(join(ctx.run.root, "installation", "registry", "state", "active.json")));
|
||||
const snapshotsBeforePull = sha256(JSON.stringify(await snapshotDigest(join(ctx.run.root, "installation", "registry", "snapshots"))));
|
||||
const rejectedPull = await request(ctx, "context-invalid-pull", "POST", "/workspace-registry/pull");
|
||||
assert(rejectedPull.status === 400 && rejectedPull.body?.code === "workspace_invalid", "invalid pull was not rejected through HTTP");
|
||||
const remoteAfterPull = sha256(await readFile(join(ctx.run.root, "remote.git", "refs", "heads", "main")));
|
||||
const activeAfterPull = sha256(await readFile(join(ctx.run.root, "installation", "registry", "state", "active.json")));
|
||||
const snapshotsAfterPull = sha256(JSON.stringify(await snapshotDigest(join(ctx.run.root, "installation", "registry", "snapshots"))));
|
||||
const checkoutAfterPull = (await git(["rev-parse", "HEAD"], { cwd: join(ctx.run.root, "installation", "registry", "repo") })).stdout.trim();
|
||||
assert(remoteAfterPull === remoteBeforePull, "pull mutated fixture-author remote");
|
||||
assert(activeAfterPull === activeBeforePull && snapshotsAfterPull === snapshotsBeforePull, "invalid pull changed last-valid active snapshots");
|
||||
assert(checkoutAfterPull === invalidRemoteCommit, "invalid checkout did not advance as expected");
|
||||
return { commands: ["git"], artifacts: [await evidence(ctx.run, "logs/negative-context.json", { realHttp: true, missingPublishStateByteIdentical: true, invalidRemoteCommit, checkoutAdvancedInvalid: true, remoteUnchangedByRequest: true, lastValidCommit: ctx.contentCommit, activeAndSnapshotsByteIdentical: true })] };
|
||||
const missingFixture = "fixtures/descriptors/missing-context.json";
|
||||
await atomicWrite(join(ctx.run.root, missingFixture), `${JSON.stringify(missing, null, 2)}\n`);
|
||||
trackArtifact(ctx, await fileArtifact(ctx.run.root, missingFixture));
|
||||
const missingBefore = { primary: await primarySemanticState(ctx), secondary: await contextualSemanticState(contextual) };
|
||||
await invariantArtifact(ctx, "logs/negative-context-missing-before.json", missingBefore);
|
||||
const rejectedPublish = await request(ctx, "context-missing-publish", "POST", "/workspaces/publish", { action: "create", workspace: missing, baseCommit: ctx.contentCommit }, false, undefined, contextualService.baseUrl);
|
||||
const missingAfter = { primary: await primarySemanticState(ctx), secondary: await contextualSemanticState(contextual) };
|
||||
await invariantArtifact(ctx, "logs/negative-context-missing-after.json", missingAfter);
|
||||
assertGenericWorkspaceInvalid(rejectedPublish, "context publish");
|
||||
assertByteIdentical(missingAfter.secondary, missingBefore.secondary, "failed contextual publish full semantic state");
|
||||
assertByteIdentical(missingAfter.primary, primaryBefore, "primary state after contextual publish");
|
||||
|
||||
await rm(join(contextualAuthor, "workspace-content", "p1-filesystem", "evidence"), { recursive: true });
|
||||
await git(["add", "-A", "workspace-content/p1-filesystem/evidence"], { cwd: contextualAuthor });
|
||||
await git(["commit", "-m", "Invalid contextual Evidence state"], { cwd: contextualAuthor });
|
||||
await git(["push", "origin", "invalid-context"], { cwd: contextualAuthor });
|
||||
const invalidRemoteCommit = (await git(["rev-parse", "HEAD"], { cwd: contextualAuthor })).stdout.trim();
|
||||
const invalidBefore = { primary: await primarySemanticState(ctx), secondary: await contextualSemanticState(contextual) };
|
||||
await invariantArtifact(ctx, "logs/negative-context-invalid-before.json", invalidBefore);
|
||||
const rejectedPull = await request(ctx, "context-invalid-pull", "POST", "/workspace-registry/pull", undefined, false, undefined, contextualService.baseUrl);
|
||||
const invalidAfter = { primary: await primarySemanticState(ctx), secondary: await contextualSemanticState(contextual) };
|
||||
await invariantArtifact(ctx, "logs/negative-context-invalid-after.json", invalidAfter);
|
||||
assertGenericWorkspaceInvalid(rejectedPull, "context pull");
|
||||
assertByteIdentical(invalidAfter.primary, primaryBefore, "primary state after contextual pull");
|
||||
assertByteIdentical(invalidAfter.secondary.remote, invalidBefore.secondary.remote, "pull mutated contextual fixture remote");
|
||||
assertByteIdentical(invalidAfter.secondary.author, invalidBefore.secondary.author, "pull mutated contextual fixture author");
|
||||
for (const key of ["active", "snapshots", "data", "runtime"]) {
|
||||
assert(invalidAfter.secondary[key] === invalidBefore.secondary[key], `invalid pull changed last-valid ${key}`);
|
||||
}
|
||||
assert(invalidBefore.secondary.checkout.head === ctx.contentCommit, "contextual checkout was not last-valid before invalid pull");
|
||||
assert(invalidAfter.secondary.checkout.head === invalidRemoteCommit, "invalid checkout did not advance as explicitly allowed");
|
||||
assert(invalidAfter.secondary.checkout.refs !== invalidBefore.secondary.checkout.refs, "invalid checkout refs did not advance as explicitly allowed");
|
||||
assert(invalidAfter.secondary.checkout.branch === "invalid-context" && invalidAfter.secondary.checkout.status === "", "invalid checkout branch/status mismatch");
|
||||
assert(invalidAfter.secondary.checkout.indexTree === invalidAfter.secondary.remote.tree, "invalid checkout index did not match invalid remote tree");
|
||||
assert(invalidAfter.primary.remote.head === ctx.contentCommit, "contextual scenario mutated primary main");
|
||||
return { artifacts: [await evidence(ctx.run, "logs/negative-context.json", {
|
||||
realSecondaryHttp: true, secondaryBranch: "invalid-context", primaryFullSemanticStateByteIdentical: true,
|
||||
primaryMainUnchanged: true, missingPublishSecondaryFullSemanticStateByteIdentical: true,
|
||||
invalidRemoteCommit, checkoutHeadIndexRefsAdvancedExplicitlyAllowed: true, remoteUnchangedByRequest: true,
|
||||
lastValidActiveSnapshotsDataRuntimeByteIdentical: true, exactGenericEnvelopesNoStderr: true,
|
||||
gitTransportTelemetryExcluded: [".git/logs", ".git/FETCH_HEAD", ".git/ORIG_HEAD", ".git/objects"],
|
||||
}, ctx.forbiddenValues)] };
|
||||
} },
|
||||
{ id: "no_p1_scope_artifacts", run: async () => {
|
||||
const forbidden = ["artifacts/evidence", "corpus/ACTIVE", "embedding-output", "qdrant-records", "preprocessing-invocation"];
|
||||
const files = (await walkFiles(ctx.run.root)).map(({ rel }) => rel);
|
||||
const present = files.filter((path) => forbidden.some((part) => path.includes(part)));
|
||||
const prohibitedRoutes = ctx.httpRequests.filter(({ path }) => /\/test$|\/evidence|preprocess|acquire/i.test(path));
|
||||
const prohibitedRoutesCalled = ctx.httpRequests.filter(({ path }) => /\/evidence|\/acquisition|\/preprocess/i.test(path));
|
||||
const prohibitedCommands = (commandEventSink ?? []).filter(({ argvLabels }) => argvLabels.some((label) => /preprocess|acquire.*evidence|embedding|qdrant/i.test(label)));
|
||||
const productionWorkspaceModules = await readdir(join(ctx.repositoryRoot, "backend", "dist", "workspaces"));
|
||||
const adapterConstructorModules = productionWorkspaceModules.filter((name) => /adapter|acquisition/i.test(name));
|
||||
assert(present.length === 0 && prohibitedRoutes.length === 0 && prohibitedCommands.length === 0 && ctx.prohibitedInvocations.length === 0, "prohibited P1 scope operation observed");
|
||||
assert(adapterConstructorModules.length === 0, "unexpected P1 adapter constructor surface present");
|
||||
assert(ctx.externalAttempts.length === 0, "external fetch attempted");
|
||||
return await log("no-p1-scope-artifacts", { absentArtifacts: forbidden, prohibitedRouteInvocations: 0, prohibitedCommandInvocations: prohibitedCommands.length, evidenceAcquisitionInvocations: 0, preprocessingInvocations: 0, adapterConstructorModules, globalFetchGuardInstalled: globalThis.fetch === ctx.guardedFetch, externalFetchAttempts: ctx.externalAttempts.length, ownedLoopbackOnly: true });
|
||||
const surfaceAudit = await assertNoP1ScopeEntrypoints(ctx);
|
||||
const gitTraceProof = await assertProductionGitTrace(ctx);
|
||||
assert(present.length === 0 && prohibitedRoutesCalled.length === 0 && prohibitedCommands.length === 0, "prohibited P1 scope operation observed");
|
||||
assert(ctx.networkGuard.externalAttempts.length === 0, "external network connection attempted");
|
||||
assert(ctx.services.length === 2 && ctx.services.every(({ baseUrl }) => ctx.networkGuard.hasOwnedOrigin(baseUrl)), "listener was not an owned loopback origin");
|
||||
return await log("no-p1-scope-artifacts", {
|
||||
absentArtifacts: forbidden, moduleEntrypointSurfaceAbsent: true, routeEntrypointSurfaceAbsent: true,
|
||||
...surfaceAudit, productionGitTrace: gitTraceProof, networkGuardInstalledBeforeProduction: true, externalNetworkAttempts: [],
|
||||
ownedLoopbackOrigins: ctx.services.map(({ name }) => name),
|
||||
});
|
||||
} },
|
||||
{ id: "secret_scan", run: async () => {
|
||||
const findings = await scanSecrets({ runRoot: ctx.run.root, forbiddenValues: ctx.forbiddenValues });
|
||||
@@ -814,6 +1138,7 @@ function productionChecks(ctx) {
|
||||
return await log("cleanup-confinement", { ownedRemoved: true, siblingPreservedDuringAssertion: true, testResourceRemoved: true });
|
||||
} },
|
||||
];
|
||||
return scenarios.map((scenario) => wrapProductionCheck(ctx, scenario));
|
||||
}
|
||||
|
||||
async function assertRejectsCode(fn, code) {
|
||||
@@ -836,80 +1161,121 @@ function completeFailedResults(results, firstError = "Acceptance setup failed sa
|
||||
return completed;
|
||||
}
|
||||
|
||||
export async function runIntegration({ repositoryRoot = defaultRepositoryRoot, keep = false, env = process.env, failAt = env.P1_ACCEPTANCE_FAIL_AT, checks, setup = setupContext, announce } = {}) {
|
||||
const savedEnv = { ...process.env }; let run; let ctx; let results = []; let fatal;
|
||||
function deduplicateResultArtifacts(results) {
|
||||
const seen = new Set();
|
||||
for (const result of results) result.artifacts = result.artifacts.filter(({ path }) => !seen.has(path) && seen.add(path));
|
||||
return results;
|
||||
}
|
||||
function minimalFailClosedReport(run, keep) {
|
||||
const checks = CHECK_IDS.map((id, index) => failedCheck(
|
||||
id, nowIso(), index === 0 ? "Acceptance audit failed closed." : "Not executed after fail-closed audit.",
|
||||
));
|
||||
return {
|
||||
schemaVersion: 1, runId: run.runId, startedAt: run.startedAt, finishedAt: nowIso(),
|
||||
command: `p1-acceptance integration${keep ? " --keep" : ""}`, overall: "FAIL", checks,
|
||||
};
|
||||
}
|
||||
function reportBytes(report) {
|
||||
validateReport(report);
|
||||
return {
|
||||
json: Buffer.from(`${JSON.stringify(report, null, 2)}
|
||||
`),
|
||||
markdown: Buffer.from(renderReportMarkdown(report)),
|
||||
};
|
||||
}
|
||||
function attachResultArtifact(results, checkId, artifact) {
|
||||
const result = results.find(({ id }) => id === checkId);
|
||||
if (!result) throw new Error("trace artifact owner is absent");
|
||||
result.artifacts.push(artifact);
|
||||
}
|
||||
|
||||
export async function runIntegration({ repositoryRoot = defaultRepositoryRoot, keep = false, env = process.env, failAt = env.P1_ACCEPTANCE_FAIL_AT, checks, setup, announce } = {}) {
|
||||
const savedEnv = { ...process.env };
|
||||
let run; let ctx; let results = []; let fatal;
|
||||
try {
|
||||
run = await createOwnedRun({ repositoryRoot });
|
||||
ctx = {
|
||||
run, repositoryRoot, forbiddenValues: [], expectedGitRepositories: [], services: [],
|
||||
originalFetch: globalThis.fetch,
|
||||
};
|
||||
commandEventSink = [];
|
||||
const selectedSetup = setup ?? (checks === undefined ? setupContext : undefined);
|
||||
if (selectedSetup) {
|
||||
const configured = await selectedSetup(run, repositoryRoot, env, ctx);
|
||||
if (configured && configured !== ctx) Object.assign(ctx, configured);
|
||||
}
|
||||
if (checks === undefined) {
|
||||
ctx = await setup(run, repositoryRoot, env);
|
||||
if (!ctx) throw new Error("acceptance setup returned no context");
|
||||
if (!ctx.env) throw new Error("acceptance setup returned no environment");
|
||||
replaceProcessEnvironment(ctx.env);
|
||||
ctx.networkGuard = installNetworkGuard(ctx.originalFetch);
|
||||
checks = productionChecks(ctx);
|
||||
} else if (ctx.env) {
|
||||
replaceProcessEnvironment(ctx.env);
|
||||
}
|
||||
results = await executeChecks({ checks, failAt });
|
||||
} catch (error) {
|
||||
fatal = error;
|
||||
if (run) results = completeFailedResults(results);
|
||||
} finally {
|
||||
if (ctx?.app) {
|
||||
await ctx.app.close().catch(() => {});
|
||||
await writeOwnership(run, { ...run.listener, state: "closed" }).catch(() => {});
|
||||
if (ctx?.services) {
|
||||
for (const service of [...ctx.services].reverse()) {
|
||||
await service.app.close().catch(() => {});
|
||||
await writeOwnership(run, {
|
||||
name: service.name, kind: "fastify", host: "127.0.0.1", requestedPort: 0,
|
||||
actualPort: Number(new URL(service.baseUrl).port), pid: process.pid, state: "closed",
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
if (ctx?.originalFetch) globalThis.fetch = ctx.originalFetch;
|
||||
ctx?.networkGuard?.restore();
|
||||
replaceProcessEnvironment(savedEnv);
|
||||
}
|
||||
if (!run) throw fatal;
|
||||
results = completeFailedResults(results);
|
||||
const success = !fatal && results.every(({ status }) => status === "PASS");
|
||||
const report = {
|
||||
schemaVersion: 1, runId: run.runId, startedAt: run.startedAt, finishedAt: nowIso(),
|
||||
command: `p1-acceptance integration${keep ? " --keep" : ""}`, overall: success ? "PASS" : "FAIL", checks: results,
|
||||
};
|
||||
validateReport(report);
|
||||
let jsonBytes = Buffer.from(`${JSON.stringify(report, null, 2)}
|
||||
`); let mdBytes = Buffer.from(renderReportMarkdown(report));
|
||||
if (ctx?.forbiddenValues) {
|
||||
let scanFailed = false;
|
||||
try {
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: ctx.forbiddenValues, virtualFiles: [{ path: "report.json", bytes: jsonBytes }, { path: "report.md", bytes: mdBytes }] });
|
||||
scanFailed = findings.length > 0;
|
||||
} catch { scanFailed = true; }
|
||||
if (scanFailed) {
|
||||
const secret = report.checks.find(({ id }) => id === "secret_scan");
|
||||
secret.status = "FAIL";
|
||||
secret.commands = [];
|
||||
secret.artifacts = [];
|
||||
secret.error = "Secret scan failed closed.";
|
||||
report.overall = deriveOverall(report.checks);
|
||||
validateReport(report);
|
||||
jsonBytes = Buffer.from(`${JSON.stringify(report, null, 2)}
|
||||
`); mdBytes = Buffer.from(renderReportMarkdown(report));
|
||||
}
|
||||
}
|
||||
results = deduplicateResultArtifacts(completeFailedResults(results));
|
||||
|
||||
let auditFailed = false;
|
||||
const commandEvents = commandEventSink ?? [];
|
||||
commandEventSink = undefined;
|
||||
let commandArtifact = await evidence(run, "logs/command-events.json", { eventCount: commandEvents.length, events: commandEvents });
|
||||
if (ctx?.forbiddenValues && containsAny(await readFile(join(run.root, commandArtifact.path)), ctx.forbiddenValues)) {
|
||||
commandArtifact = await evidence(run, "logs/command-events.json", { eventCount: commandEvents.length, eventsRedactedAfterFailClosedScan: true });
|
||||
const secret = report.checks.find(({ id }) => id === "secret_scan");
|
||||
secret.status = "FAIL"; secret.commands = []; secret.artifacts = []; secret.error = "Secret scan failed closed.";
|
||||
activeCommandCheckId = undefined;
|
||||
try {
|
||||
const commandArtifact = await evidence(run, "logs/command-events.json", { eventCount: commandEvents.length, events: commandEvents }, ctx.forbiddenValues);
|
||||
attachResultArtifact(results, "preflight", commandArtifact);
|
||||
if (ctx.gitTracePath) {
|
||||
const gitTraceBytes = await readFile(ctx.gitTracePath);
|
||||
for (const line of gitTraceBytes.toString("utf8").split("\n").filter(Boolean)) JSON.parse(line);
|
||||
attachResultArtifact(results, "no_p1_scope_artifacts", await fileArtifact(run.root, relative(run.root, ctx.gitTracePath)));
|
||||
}
|
||||
} catch {
|
||||
auditFailed = true;
|
||||
}
|
||||
const evidenceCheck = report.checks.find(({ status }) => status === "PASS") ?? report.checks[0];
|
||||
evidenceCheck.artifacts.push(commandArtifact);
|
||||
report.overall = deriveOverall(report.checks);
|
||||
validateReport(report);
|
||||
jsonBytes = Buffer.from(`${JSON.stringify(report, null, 2)}
|
||||
`); mdBytes = Buffer.from(renderReportMarkdown(report));
|
||||
if (ctx?.forbiddenValues && (containsAny(jsonBytes, ctx.forbiddenValues) || containsAny(mdBytes, ctx.forbiddenValues))) {
|
||||
const secret = report.checks.find(({ id }) => id === "secret_scan");
|
||||
secret.status = "FAIL"; secret.commands = []; secret.artifacts = []; secret.error = "Secret scan failed closed.";
|
||||
report.overall = deriveOverall(report.checks);
|
||||
validateReport(report);
|
||||
jsonBytes = Buffer.from(`${JSON.stringify(report, null, 2)}
|
||||
`); mdBytes = Buffer.from(renderReportMarkdown(report));
|
||||
deduplicateResultArtifacts(results);
|
||||
|
||||
let report = {
|
||||
schemaVersion: 1, runId: run.runId, startedAt: run.startedAt, finishedAt: nowIso(),
|
||||
command: `p1-acceptance integration${keep ? " --keep" : ""}`,
|
||||
overall: !fatal && deriveOverall(results) === "PASS" ? "PASS" : "FAIL", checks: results,
|
||||
};
|
||||
let bytes;
|
||||
try {
|
||||
bytes = reportBytes(report);
|
||||
const findings = await scanSecrets({
|
||||
runRoot: run.root,
|
||||
forbiddenValues: ctx.forbiddenValues,
|
||||
expectedGitRepositories: ctx.expectedGitRepositories,
|
||||
virtualFiles: [{ path: "report.json", bytes: bytes.json }, { path: "report.md", bytes: bytes.markdown }],
|
||||
});
|
||||
if (findings.length > 0) auditFailed = true;
|
||||
} catch {
|
||||
auditFailed = true;
|
||||
}
|
||||
await atomicWrite(join(run.root, "report.json"), jsonBytes); await atomicWrite(join(run.root, "report.md"), mdBytes);
|
||||
if (auditFailed) {
|
||||
report = minimalFailClosedReport(run, keep);
|
||||
bytes = reportBytes(report);
|
||||
if (containsAny(bytes.json, ctx.forbiddenValues) || containsAny(bytes.markdown, ctx.forbiddenValues)) {
|
||||
throw new Error("sanitized fail-closed report unexpectedly contains a forbidden value");
|
||||
}
|
||||
}
|
||||
await atomicWrite(join(run.root, "report.json"), bytes.json);
|
||||
await atomicWrite(join(run.root, "report.md"), bytes.markdown);
|
||||
const finalSuccess = report.overall === "PASS";
|
||||
if (announce) await announce({ report, runRoot: run.root, keep });
|
||||
const removed = await finalizeOwnedRun({ run, success: finalSuccess, keep });
|
||||
|
||||
@@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createServer, connect } from "node:net";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
CHECK_IDS,
|
||||
buildSafeEnvironment,
|
||||
installExternalFetchGuard,
|
||||
installNetworkGuard,
|
||||
negativeRequestEvidence,
|
||||
cleanupOwnedRun,
|
||||
createOwnedRun,
|
||||
@@ -164,6 +166,28 @@ test("injected failure executes once, retains a complete ordered diagnostic repo
|
||||
assert.equal(report.checks[4].error, "Not executed after earlier failure.");
|
||||
});
|
||||
|
||||
test("failed scenario retains partial request and response evidence with observed commands", async () => {
|
||||
const partial = {
|
||||
commands: ["git"],
|
||||
artifacts: [
|
||||
{ path: "requests/partial.json", sha256: "a".repeat(64) },
|
||||
{ path: "responses/partial.json", sha256: "b".repeat(64) },
|
||||
],
|
||||
};
|
||||
const checks = exactScenarios(async (id) => {
|
||||
if (id === CHECK_IDS[4]) {
|
||||
const error = new Error("HTTP scenario failed after response persistence");
|
||||
error.acceptancePartial = partial;
|
||||
throw error;
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const results = await executeChecks({ checks });
|
||||
assert.deepEqual(results[4].commands, partial.commands);
|
||||
assert.deepEqual(results[4].artifacts, partial.artifacts);
|
||||
assert.equal(results[4].error, "Acceptance scenario failed safely.");
|
||||
});
|
||||
|
||||
test("executeChecks never repeats or executes after first failure but emits the exact check set", async () => {
|
||||
const calls = new Map();
|
||||
const result = await executeChecks({
|
||||
@@ -212,7 +236,7 @@ test("secret scanner excludes only the direct fixture-secrets subtree", async ()
|
||||
await mkdir(dirname(join(run.root, path)), { recursive: true });
|
||||
await writeFile(join(run.root, path), `prefix ${canary} suffix`);
|
||||
}
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: [canary] });
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: [canary], expectedGitRepositories: [] });
|
||||
assert.deepEqual(new Set(findings.map((finding) => finding.path)), new Set(paths));
|
||||
});
|
||||
|
||||
@@ -230,7 +254,7 @@ test("secret scanner examines reachable Git blobs, not just loose file bytes", a
|
||||
await execFileAsync("git", ["commit", "-m", "secret blob"], { cwd: gitRoot });
|
||||
await execFileAsync("git", ["rm", "secret.txt"], { cwd: gitRoot });
|
||||
await execFileAsync("git", ["commit", "-m", "remove worktree copy"], { cwd: gitRoot });
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: [canary] });
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: [canary], expectedGitRepositories: ["author"] });
|
||||
assert.equal(findings.some((finding) => finding.path.startsWith("git-object:")), true);
|
||||
});
|
||||
|
||||
@@ -250,12 +274,15 @@ test("command helper accepts only executable plus separate argv", async () => {
|
||||
await assert.rejects(runCommand({ executable: "/bin/echo", argv: "hello" }));
|
||||
await assert.rejects(runCommand({ executable: "/bin/echo", argv: [], shell: true }));
|
||||
await assert.rejects(runCommand({ executable: "git status; rm -rf /", argv: [] }));
|
||||
await assert.rejects(runCommand({ executable: "/tmp/git", argv: ["--version"] }), /command executable is not allowlisted/);
|
||||
await assert.rejects(runCommand({ executable: "tht", argv: ["config", "check"] }), /command executable is not allowlisted/);
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const executable = join(repositoryRoot, "executable with spaces");
|
||||
await writeFile(executable, "#!/bin/sh\nprintf '%s' \"$1\"\n", { mode: 0o700 });
|
||||
await chmod(executable, 0o700);
|
||||
const result = await runCommand({ executable, argv: ["literal;not-a-shell"] });
|
||||
assert.equal(result.stdout, "literal;not-a-shell");
|
||||
await assert.rejects(runCommand({ executable, argv: ["literal;not-a-shell"] }), /command executable is not allowlisted/);
|
||||
const result = await runCommand({ executable: "git", argv: ["--version"] });
|
||||
assert.match(result.stdout, /^git version /);
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
@@ -320,3 +347,90 @@ test("public wrapper replaces ambient environment before invoking the runner", a
|
||||
assert.match(wrapper, /P1_ACCEPTANCE_FAIL_AT/);
|
||||
assert.doesNotMatch(wrapper, /export THT_BIN/);
|
||||
});
|
||||
|
||||
|
||||
test("network guard is installed globally, rejects non-loopback sockets, and permits one owned listener", async () => {
|
||||
const server = createServer((socket) => socket.end("ok"));
|
||||
await new Promise((resolvePromise, reject) => server.listen(0, "127.0.0.1", (error) => error ? reject(error) : resolvePromise()));
|
||||
const address = server.address();
|
||||
assert(address && typeof address === "object");
|
||||
const guard = installNetworkGuard();
|
||||
try {
|
||||
guard.addOwnedOrigin(`http://127.0.0.1:${address.port}`);
|
||||
const contents = await new Promise((resolvePromise, reject) => {
|
||||
const socket = connect({ host: "127.0.0.1", port: address.port });
|
||||
let value = "";
|
||||
socket.setEncoding("utf8");
|
||||
socket.on("data", (chunk) => { value += chunk; });
|
||||
socket.on("end", () => resolvePromise(value));
|
||||
socket.on("error", reject);
|
||||
});
|
||||
assert.equal(contents, "ok");
|
||||
assert.throws(() => connect({ host: "example.com", port: 80 }), /external network connection prohibited/);
|
||||
await assert.rejects(globalThis.fetch("https://example.com/"), /external network connection prohibited/);
|
||||
assert.equal(guard.externalAttempts.length, 2);
|
||||
} finally {
|
||||
guard.restore();
|
||||
await new Promise((resolvePromise) => server.close(resolvePromise));
|
||||
}
|
||||
});
|
||||
|
||||
test("report validation rejects duplicate artifact paths across checks", () => {
|
||||
const report = validReport();
|
||||
report.checks[1].artifacts[0].path = report.checks[0].artifacts[0].path;
|
||||
assert.throws(() => validateReport(report), /report artifact path is duplicated/);
|
||||
});
|
||||
|
||||
test("virtual report leakage yields a minimal sanitized exact-15 FAIL report", async () => {
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const canary = "VIRTUAL-CANARY-12345678";
|
||||
const checks = exactScenarios(async (id) => ({
|
||||
commands: [],
|
||||
artifacts: id === CHECK_IDS[0] ? [{ path: `logs/${canary}.json`, sha256: "a".repeat(64) }] : [],
|
||||
}));
|
||||
const result = await runIntegration({
|
||||
repositoryRoot,
|
||||
checks,
|
||||
setup: async (_run, _repositoryRoot, _env, ctx) => {
|
||||
ctx.forbiddenValues = [canary];
|
||||
return ctx;
|
||||
},
|
||||
});
|
||||
assert.equal(result.exitCode, 1);
|
||||
const bytes = await readFile(join(result.runRoot, "report.json"));
|
||||
assert.equal(bytes.includes(Buffer.from(canary)), false);
|
||||
const report = JSON.parse(bytes);
|
||||
assert.deepEqual(report.checks.map(({ id }) => id), CHECK_IDS);
|
||||
assert(report.checks.every(({ status, commands, artifacts }) => status === "FAIL" && commands.length === 0 && artifacts.length === 0));
|
||||
});
|
||||
|
||||
test("partial setup preserves forbidden values and never writes secret-bearing report bytes", async () => {
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const canary = "PARTIAL-SETUP-CANARY-12345678";
|
||||
const result = await runIntegration({
|
||||
repositoryRoot,
|
||||
setup: async (run, _repositoryRoot, _env, ctx) => {
|
||||
ctx.forbiddenValues = [canary];
|
||||
await mkdir(join(run.root, "logs"), { recursive: true });
|
||||
await writeFile(join(run.root, "logs", "partial-setup.log"), canary);
|
||||
throw new Error(`unsafe ${canary}`);
|
||||
},
|
||||
});
|
||||
assert.equal(result.exitCode, 1);
|
||||
const bytes = await readFile(join(result.runRoot, "report.json"));
|
||||
assert.equal(bytes.includes(Buffer.from(canary)), false);
|
||||
const report = JSON.parse(bytes);
|
||||
assert.equal(report.checks.length, 15);
|
||||
assert(report.checks.every(({ status }) => status === "FAIL"));
|
||||
});
|
||||
|
||||
test("secret scan fails closed when either expected Git repository is missing", async () => {
|
||||
for (const missing of ["remote.git", "author"]) {
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const run = await createOwnedRun({ repositoryRoot });
|
||||
const present = missing === "remote.git" ? "author" : "remote.git";
|
||||
await mkdir(join(run.root, present));
|
||||
await execFileAsync("git", present === "remote.git" ? ["init", "--bare", join(run.root, present)] : ["init", join(run.root, present)]);
|
||||
await assert.rejects(scanSecrets({ runRoot: run.root, forbiddenValues: ["CANARY-value-123"] }), new RegExp(`missing expected Git repository: ${missing.replace(".", "\\.")}`));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user