|
|
|
@@ -15,19 +15,20 @@ import { fileURLToPath } from "node:url";
|
|
|
|
|
import { promisify } from "node:util";
|
|
|
|
|
|
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
|
let commandEventSink;
|
|
|
|
|
const RUN_ID = /^p1-[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 SAFE_RELATIVE = /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\\)[A-Za-z0-9._/-]+$/;
|
|
|
|
|
const COMMAND = /^[A-Za-z0-9._+-]+$/;
|
|
|
|
|
const CHECK_IDS = [
|
|
|
|
|
export const CHECK_IDS = Object.freeze([
|
|
|
|
|
"preflight", "clean_state", "ownership", "local_git_bootstrap",
|
|
|
|
|
"http_validate_publish_pull_read_export", "same_revision_git_objects",
|
|
|
|
|
"content_only_revision", "snapshot_and_docs", "runtime_render_determinism",
|
|
|
|
|
"tht_config_check", "negative_schema_cases", "negative_context_case",
|
|
|
|
|
"no_p1_scope_artifacts", "secret_scan", "cleanup_confinement",
|
|
|
|
|
];
|
|
|
|
|
]);
|
|
|
|
|
const TOPOLOGY = [
|
|
|
|
|
"remote.git", "author", "installation/registry", "installation/data", "installation/runtime",
|
|
|
|
|
"fixture-secrets", "fixtures/descriptors", "fixtures/requests", "requests", "responses",
|
|
|
|
@@ -168,6 +169,11 @@ export async function runCommand(options) {
|
|
|
|
|
const child = execFile(executable, argv, { cwd, env, timeout: timeoutMs, maxBuffer: maxOutputBytes, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
|
|
|
const code = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
|
|
|
|
|
const result = { code, stdout: stdout ?? "", stderr: stderr ?? "" };
|
|
|
|
|
if (commandEventSink) commandEventSink.push({
|
|
|
|
|
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",
|
|
|
|
|
});
|
|
|
|
|
if (error) Object.assign(error, { result });
|
|
|
|
|
error ? reject(error) : resolvePromise(result);
|
|
|
|
|
});
|
|
|
|
@@ -193,10 +199,13 @@ function forbiddenKey(value) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
export function deriveOverall(checks) { return checks.length > 0 && checks.every(({ status }) => status === "PASS") ? "PASS" : "FAIL"; }
|
|
|
|
|
function hasExactCheckIds(checks) {
|
|
|
|
|
return checks.length === CHECK_IDS.length && checks.every(({ id }, index) => id === CHECK_IDS[index]);
|
|
|
|
|
}
|
|
|
|
|
export function validateReport(report) {
|
|
|
|
|
if (!report || report.schemaVersion !== 1 || !RUN_ID.test(report.runId ?? "") || !ISO_UTC.test(report.startedAt ?? "")
|
|
|
|
|
|| !ISO_UTC.test(report.finishedAt ?? "") || typeof report.command !== "string" || forbiddenKey(report)
|
|
|
|
|
|| !Array.isArray(report.checks) || report.checks.length === 0) throw new Error("report is invalid");
|
|
|
|
|
|| !Array.isArray(report.checks) || !hasExactCheckIds(report.checks)) throw new Error("report is invalid");
|
|
|
|
|
const ids = 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)
|
|
|
|
@@ -237,12 +246,18 @@ async function gitObjectFindings(runRoot, forbiddenValues) {
|
|
|
|
|
if (!existsSync(directory)) continue;
|
|
|
|
|
const args = basename(directory) === "remote.git" ? ["--git-dir", directory] : ["-C", directory];
|
|
|
|
|
let objects;
|
|
|
|
|
try { objects = (await git([...args, "rev-list", "--objects", "--all"])).stdout.trim().split("\n").filter(Boolean); } catch { continue; }
|
|
|
|
|
try {
|
|
|
|
|
objects = (await git([...args, "rev-list", "--objects", "--all"])).stdout.trim().split("\n").filter(Boolean);
|
|
|
|
|
} catch { throw new Error(`Git secret scan failed closed during enumeration: ${basename(directory)}`); }
|
|
|
|
|
for (const line of objects) {
|
|
|
|
|
const oid = line.split(" ", 1)[0];
|
|
|
|
|
const type = (await git([...args, "cat-file", "-t", oid])).stdout.trim();
|
|
|
|
|
if (type !== "blob") continue;
|
|
|
|
|
const bytes = Buffer.from((await git([...args, "cat-file", "blob", oid], { maxOutputBytes: 16 * 1024 * 1024 })).stdout);
|
|
|
|
|
let type; let bytes;
|
|
|
|
|
try {
|
|
|
|
|
type = (await git([...args, "cat-file", "-t", oid])).stdout.trim();
|
|
|
|
|
if (!/^(blob|tree|commit|tag)$/.test(type)) throw new Error("invalid object type");
|
|
|
|
|
if (type !== "blob") continue;
|
|
|
|
|
bytes = Buffer.from((await git([...args, "cat-file", "blob", oid], { maxOutputBytes: 16 * 1024 * 1024 })).stdout);
|
|
|
|
|
} catch { throw new Error(`Git secret scan failed closed during object inspection: ${basename(directory)}:${oid}`); }
|
|
|
|
|
if (containsAny(bytes, forbiddenValues)) findings.push({ path: `git-object:${basename(directory)}:${oid}` });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -256,6 +271,26 @@ export async function scanSecrets({ runRoot, forbiddenValues, virtualFiles = []
|
|
|
|
|
findings.push(...await gitObjectFindings(runRoot, values));
|
|
|
|
|
return findings;
|
|
|
|
|
}
|
|
|
|
|
export function negativeRequestEvidence(caseLabel, expectedInputField) {
|
|
|
|
|
if (!/^[a-z0-9-]+$/.test(caseLabel) || !/^[a-z_]+(?:\.[a-z_]+)*$/.test(expectedInputField)) throw new Error("unsafe negative-case evidence");
|
|
|
|
|
return { case: caseLabel, expectedInputField };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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" });
|
|
|
|
|
throw new Error("external fetch prohibited");
|
|
|
|
|
}
|
|
|
|
|
return await fetchImplementation(input, init);
|
|
|
|
|
};
|
|
|
|
|
return { fetch: guardedFetch, externalAttempts };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sanitizeForEvidence(value, forbiddenValues = []) {
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
let safe = value;
|
|
|
|
@@ -272,23 +307,27 @@ async function evidence(run, path, value, forbiddenValues = []) {
|
|
|
|
|
return await fileArtifact(run.root, path);
|
|
|
|
|
}
|
|
|
|
|
export async function executeChecks({ checks, failAt, recorder } = {}) {
|
|
|
|
|
if (!Array.isArray(checks) || !hasExactCheckIds(checks)) throw new Error("scenarios must match the exact ordered check set");
|
|
|
|
|
if (failAt !== undefined && !CHECK_IDS.includes(failAt)) throw new Error("failure hook must name an exact check");
|
|
|
|
|
const results = [];
|
|
|
|
|
const ids = new Set();
|
|
|
|
|
let stopped = false;
|
|
|
|
|
for (const scenario of checks) {
|
|
|
|
|
if (ids.has(scenario.id)) throw new Error("duplicate scenario id");
|
|
|
|
|
ids.add(scenario.id);
|
|
|
|
|
const startedAt = nowIso();
|
|
|
|
|
let result;
|
|
|
|
|
try {
|
|
|
|
|
const output = await scenario.run();
|
|
|
|
|
if (scenario.id === failAt) throw new Error("injected acceptance failure");
|
|
|
|
|
result = { id: scenario.id, status: "PASS", startedAt, finishedAt: nowIso(), commands: output.commands ?? [], artifacts: output.artifacts ?? [] };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: "Acceptance scenario failed safely." };
|
|
|
|
|
if (stopped) {
|
|
|
|
|
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: "Not executed after earlier failure." };
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
const output = await scenario.run();
|
|
|
|
|
if (scenario.id === failAt) throw new Error("injected acceptance failure");
|
|
|
|
|
result = { id: scenario.id, status: "PASS", startedAt, finishedAt: nowIso(), commands: output.commands ?? [], artifacts: output.artifacts ?? [] };
|
|
|
|
|
} catch {
|
|
|
|
|
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: "Acceptance scenario failed safely." };
|
|
|
|
|
stopped = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
results.push(result);
|
|
|
|
|
if (recorder) await recorder(result);
|
|
|
|
|
if (result.status === "FAIL") break;
|
|
|
|
|
}
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
@@ -352,8 +391,9 @@ async function setupSecrets(ctx) {
|
|
|
|
|
THT_WS_P1_S3_EVIDENCE_SESSION_TOKEN_FILE: paths.session,
|
|
|
|
|
});
|
|
|
|
|
Object.assign(ctx.env, env);
|
|
|
|
|
env.THT_WORKSPACE_SECRET_ROOTS = secretDir;
|
|
|
|
|
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", "base.yaml"), "{}\n");
|
|
|
|
|
await atomicWrite(join(ctx.run.root, "installation", "runtime", "base.yaml"), "{}\n");
|
|
|
|
|
}
|
|
|
|
|
async function initializeGit(ctx) {
|
|
|
|
|
await git(["init", "--bare", "--initial-branch=main", join(ctx.run.root, "remote.git")], { cwd: ctx.run.root });
|
|
|
|
@@ -381,7 +421,7 @@ async function startBackend(ctx) {
|
|
|
|
|
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", "base.yaml"),
|
|
|
|
|
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,
|
|
|
|
@@ -390,18 +430,28 @@ async function startBackend(ctx) {
|
|
|
|
|
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" });
|
|
|
|
|
}
|
|
|
|
|
async function request(ctx, id, method, path, body, binary = false) {
|
|
|
|
|
const requestSummary = { method, path, ...(body === undefined ? {} : { body: sanitizeForEvidence(body, ctx.forbiddenValues) }) };
|
|
|
|
|
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) {
|
|
|
|
|
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);
|
|
|
|
|
const response = await fetch(`${ctx.baseUrl}${path}`, {
|
|
|
|
|
ctx.httpRequests.push({ method, path });
|
|
|
|
|
const response = await ctx.guardedFetch(`${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 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 });
|
|
|
|
|
return { status: response.status, bytes };
|
|
|
|
|
}
|
|
|
|
@@ -444,11 +494,22 @@ async function snapshotDigest(path) {
|
|
|
|
|
for (const file of files) result[file.rel] = sha256(await readFile(file.path));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
const SAFE_AMBIENT_ENV = Object.freeze(["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "TZ", "NODE_EXTRA_CA_CERTS"]);
|
|
|
|
|
export function buildSafeEnvironment({ ambient = process.env, fixture = {} } = {}) {
|
|
|
|
|
const safe = {};
|
|
|
|
|
for (const key of SAFE_AMBIENT_ENV) if (typeof ambient[key] === "string") safe[key] = ambient[key];
|
|
|
|
|
for (const [key, value] of Object.entries(fixture)) {
|
|
|
|
|
if (typeof value !== "string") throw new Error(`fixture environment value must be a string: ${key}`);
|
|
|
|
|
safe[key] = value;
|
|
|
|
|
}
|
|
|
|
|
return safe;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function setupContext(run, repositoryRoot, env) {
|
|
|
|
|
const thtBin = realpathSync(env.THT_BIN ?? join(repositoryRoot, "harness", ".venv", "bin", "tht"));
|
|
|
|
|
const harnessDir = realpathSync(join(repositoryRoot, "harness"));
|
|
|
|
|
const ctx = { run, repositoryRoot, descriptors: descriptors(), forbiddenValues: [], env: {
|
|
|
|
|
...env, HOST: "127.0.0.1", PORT: "0", AUTH_MODE: "none", THT_BIN: thtBin,
|
|
|
|
|
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"),
|
|
|
|
|
SETTINGS_FILE: join(run.root, "installation", "data", "settings.json"),
|
|
|
|
|
MAINTENANCE_STATE_FILE: join(run.root, "installation", "data", "maintenance.json"),
|
|
|
|
@@ -457,11 +518,69 @@ 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"),
|
|
|
|
|
} };
|
|
|
|
|
};
|
|
|
|
|
const ctx = {
|
|
|
|
|
run, repositoryRoot, descriptors: descriptors(), 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;
|
|
|
|
|
return ctx;
|
|
|
|
|
}
|
|
|
|
|
async function registryState(ctx) {
|
|
|
|
|
const root = join(ctx.run.root, "installation", "registry");
|
|
|
|
|
const bytes = async (path) => sha256(await readFile(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")),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
function assertByteIdentical(left, right, label) {
|
|
|
|
|
assert(JSON.stringify(left) === JSON.stringify(right), `${label} state changed`);
|
|
|
|
|
}
|
|
|
|
|
async function currentSnapshotManifest(ctx, commit) {
|
|
|
|
|
const path = join(ctx.run.root, "installation", "registry", "snapshots", commit, "snapshot.json");
|
|
|
|
|
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
|
|
|
assert(manifest.head === commit, "snapshot manifest head mismatch");
|
|
|
|
|
return { path, manifest };
|
|
|
|
|
}
|
|
|
|
|
function revisionFromManifest(manifest, id) {
|
|
|
|
|
const revision = manifest.revisions.find((candidate) => candidate.id === id);
|
|
|
|
|
assert(revision, `manifest revision absent ${id}`);
|
|
|
|
|
return revision;
|
|
|
|
|
}
|
|
|
|
|
function renderedRoot(parsed) { return parsed.evidence.sources[0].root; }
|
|
|
|
|
function assertRuntimeContract(ctx, id, parsed, revision) {
|
|
|
|
|
assert(parsed.runtime_identity.workspace_id === id, "runtime workspace identity mismatch");
|
|
|
|
|
assert(parsed.runtime_identity.workspace_revision === revision.commit, "runtime revision mismatch");
|
|
|
|
|
assert(parsed.runtime_identity.source_identity === `workspace://${id}`, "runtime source identity mismatch");
|
|
|
|
|
assert(parsed.vector.max_chunk_chars === 4000 && parsed.vector.retain_published_generations === 3, "runtime policy mismatch");
|
|
|
|
|
const source = parsed.evidence.sources[0];
|
|
|
|
|
if (id === "p1-filesystem") {
|
|
|
|
|
const exactRoot = join(dirname(revision.snapshotPath), "workspace-content", id, "evidence");
|
|
|
|
|
assert(source.type === "filesystem" && source.root === exactRoot, "filesystem root mismatch");
|
|
|
|
|
assert(JSON.stringify(source.patterns) === JSON.stringify(["**/*.md"]) && source.max_bytes === 10485760, "filesystem source contract mismatch");
|
|
|
|
|
assert(!existsSync(source.root), "filesystem Evidence root was materialized");
|
|
|
|
|
} else if (id === "p1-http") {
|
|
|
|
|
assert(source.type === "http", "HTTP source type mismatch");
|
|
|
|
|
assert(JSON.stringify(source.provenance_urls) === JSON.stringify(["https://evidence.example.test/guide.md"]), "HTTP provenance mismatch");
|
|
|
|
|
assert(source.signed_urls_file === join(ctx.run.root, "fixture-secrets", "evidence-signed-urls.json"), "HTTP binding mismatch");
|
|
|
|
|
assert(source.connect_timeout === 1.25 && source.read_timeout === 30.001 && source.max_bytes === 12345
|
|
|
|
|
&& source.max_redirects === 2 && source.allow_private_hosts === false && source.max_cache_bytes === 67890, "HTTP limits mismatch");
|
|
|
|
|
} else if (id === "p1-s3") {
|
|
|
|
|
assert(source.type === "s3" && source.bucket === "p1-evidence" && source.prefix === "published/", "S3 identity mismatch");
|
|
|
|
|
assert(source.endpoint_url === "https://s3.example.test/" && source.region === "eu-west-1", "S3 endpoint mismatch");
|
|
|
|
|
assert(source.access_key_file === join(ctx.run.root, "fixture-secrets", "evidence-access")
|
|
|
|
|
&& source.secret_key_file === join(ctx.run.root, "fixture-secrets", "evidence-secret")
|
|
|
|
|
&& source.session_token_file === join(ctx.run.root, "fixture-secrets", "evidence-session"), "S3 bindings mismatch");
|
|
|
|
|
assert(source.trusted_endpoint === true && source.allow_private_endpoint === false && source.allow_insecure_endpoint === false
|
|
|
|
|
&& source.max_bytes === 12345 && source.max_objects === 33 && source.max_pages === 4 && source.page_size === 5, "S3 limits mismatch");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function productionChecks(ctx) {
|
|
|
|
|
const log = async (id, value) => ({ commands: [], artifacts: [await evidence(ctx.run, `logs/${id}.json`, value, ctx.forbiddenValues)] });
|
|
|
|
|
return [
|
|
|
|
@@ -497,24 +616,32 @@ function productionChecks(ctx) {
|
|
|
|
|
base = published.body.revision.commit;
|
|
|
|
|
}
|
|
|
|
|
ctx.publicationHead = base;
|
|
|
|
|
const pulled = await request(ctx, "registry-pull", "POST", "/workspace-registry/pull"); assert(pulled.status === 200 && pulled.body.head === base, "pull failed");
|
|
|
|
|
const listed = await request(ctx, "workspace-list", "GET", "/workspaces"); assert(listed.status === 200 && listed.body.length === 3, "list failed");
|
|
|
|
|
ctx.reads = {};
|
|
|
|
|
const pulled = await request(ctx, "registry-pull", "POST", "/workspace-registry/pull");
|
|
|
|
|
assert(pulled.status === 200 && pulled.body.head === base, "pull failed");
|
|
|
|
|
const listed = await request(ctx, "workspace-list", "GET", "/workspaces");
|
|
|
|
|
assert(listed.status === 200 && listed.body.length === 3, "list failed");
|
|
|
|
|
ctx.reads = {}; ctx.exportManifests = {};
|
|
|
|
|
const artifacts = [];
|
|
|
|
|
for (const workspace of ctx.descriptors) {
|
|
|
|
|
const id = workspace.workspace.id; const read = await request(ctx, `read-${id}`, "GET", `/workspaces/${id}`);
|
|
|
|
|
assert(read.status === 200, `read failed ${id}`); ctx.reads[id] = read.body;
|
|
|
|
|
const exported = await request(ctx, `export-${id}`, "GET", `/workspaces/${id}/export`, undefined, true);
|
|
|
|
|
assert(exported.status === 200, `export failed ${id}`); await extractZip(ctx, id, exported.bytes);
|
|
|
|
|
assert(exported.status === 200, `export failed ${id}`); ctx.exportManifests[id] = await extractZip(ctx, id, exported.bytes);
|
|
|
|
|
artifacts.push(await fileArtifact(ctx.run.root, exportArchiveEvidencePath(`export-${id}`)));
|
|
|
|
|
for (const name of ZIP_FILES) artifacts.push(await fileArtifact(ctx.run.root, `exports/extracted/${id}/${name}`));
|
|
|
|
|
}
|
|
|
|
|
return await log("http_flow", { workspaceIds: Object.keys(ctx.reads), head: base, realListener: true, fetch: true });
|
|
|
|
|
artifacts.unshift(await evidence(ctx.run, "logs/http-flow.json", { workspaceIds: Object.keys(ctx.reads), head: base, realListener: true, fetch: true }));
|
|
|
|
|
return { commands: [], artifacts };
|
|
|
|
|
} },
|
|
|
|
|
{ id: "same_revision_git_objects", run: async () => {
|
|
|
|
|
const read = await request(ctx, "read-filesystem-identity", "GET", "/workspaces/p1-filesystem");
|
|
|
|
|
const revision = read.body.revision; ctx.oldRevision = revision; ctx.oldSnapshotDigest = await snapshotDigest(dirname(revision.snapshotPath));
|
|
|
|
|
const revision = ctx.reads["p1-filesystem"].revision;
|
|
|
|
|
ctx.oldRevision = revision; ctx.oldSnapshotDigest = await snapshotDigest(dirname(revision.snapshotPath));
|
|
|
|
|
const checkoutHead = (await git(["rev-parse", "HEAD"], { cwd: join(ctx.run.root, "installation", "registry", "repo") })).stdout.trim();
|
|
|
|
|
const manifest = JSON.parse(await readFile(join(dirname(revision.snapshotPath), "snapshot.json"), "utf8"));
|
|
|
|
|
const manifestPath = join(dirname(revision.snapshotPath), "snapshot.json");
|
|
|
|
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
|
|
|
const lease = ctx.thtRunner.acquireWorkspaceRuntime(revision.snapshotPath); let rendered;
|
|
|
|
|
try { rendered = (await import("yaml")).parse(await readFile(lease.path, "utf8")); } finally { lease.release(); }
|
|
|
|
|
ctx.oldFilesystemRoot = renderedRoot(rendered);
|
|
|
|
|
const identities = [revision.commit, checkoutHead, manifest.head, rendered.runtime_identity.workspace_revision];
|
|
|
|
|
assert(new Set(identities).size === 1, "revision identities diverged");
|
|
|
|
|
const repo = join(ctx.run.root, "installation", "registry", "repo");
|
|
|
|
@@ -523,7 +650,10 @@ function productionChecks(ctx) {
|
|
|
|
|
const type = (await git(["cat-file", "-t", `${revision.commit}:workspace-content/p1-filesystem/evidence`], { cwd: repo })).stdout.trim();
|
|
|
|
|
assert(type === "tree", "Evidence object is not a tree");
|
|
|
|
|
assert(!existsSync(join(dirname(revision.snapshotPath), "workspace-content")), "snapshot materialized workspace-content");
|
|
|
|
|
return { commands: ["git"], artifacts: [await evidence(ctx.run, "logs/git-object-proof.json", { commit: revision.commit, checkoutHead, manifestHead: manifest.head, runtimeRevision: rendered.runtime_identity.workspace_revision, evidenceType: type })] };
|
|
|
|
|
return { commands: ["git"], artifacts: [
|
|
|
|
|
await evidence(ctx.run, "logs/git-object-proof.json", { commit: revision.commit, checkoutHead, manifestHead: manifest.head, runtimeRevision: rendered.runtime_identity.workspace_revision, filesystemRoot: ctx.oldFilesystemRoot, evidenceType: type }),
|
|
|
|
|
await fileArtifact(ctx.run.root, relative(ctx.run.root, manifestPath)), await fileArtifact(ctx.run.root, relative(ctx.run.root, revision.snapshotPath)),
|
|
|
|
|
] };
|
|
|
|
|
} },
|
|
|
|
|
{ id: "content_only_revision", run: async () => {
|
|
|
|
|
const author = join(ctx.run.root, "author");
|
|
|
|
@@ -532,34 +662,52 @@ function productionChecks(ctx) {
|
|
|
|
|
await writeFile(join(author, "workspace-content", "p1-filesystem", "evidence", "guide.md"), "# P1 curated Evidence v2\n");
|
|
|
|
|
await git(["add", "workspace-content/p1-filesystem/evidence/guide.md"], { cwd: author }); await git(["commit", "-m", "Update curated Evidence only"], { cwd: author }); await git(["push", "origin", "main"], { cwd: author });
|
|
|
|
|
ctx.contentCommit = (await git(["rev-parse", "HEAD"], { cwd: author })).stdout.trim();
|
|
|
|
|
const pulled = await request(ctx, "content-only-pull", "POST", "/workspace-registry/pull"); assert(pulled.status === 200 && pulled.body.head === ctx.contentCommit, "content pull failed");
|
|
|
|
|
const current = (await request(ctx, "read-filesystem-content", "GET", "/workspaces/p1-filesystem")).body.revision;
|
|
|
|
|
const pulled = await request(ctx, "content-only-pull", "POST", "/workspace-registry/pull");
|
|
|
|
|
assert(pulled.status === 200 && pulled.body.head === ctx.contentCommit, "content pull failed");
|
|
|
|
|
const currentRead = (await request(ctx, "read-filesystem-content", "GET", "/workspaces/p1-filesystem")).body;
|
|
|
|
|
const current = currentRead.revision;
|
|
|
|
|
const descriptorAfter = (await git(["rev-parse", "HEAD:workspaces/p1-filesystem.yaml"], { cwd: author })).stdout.trim();
|
|
|
|
|
assert(current.commit === ctx.contentCommit && current.blob === ctx.oldRevision.blob && descriptorAfter === descriptorBefore, "content revision identity failed");
|
|
|
|
|
assert(JSON.stringify(await snapshotDigest(dirname(ctx.oldRevision.snapshotPath))) === JSON.stringify(ctx.oldSnapshotDigest), "old snapshot changed");
|
|
|
|
|
assertByteIdentical(await snapshotDigest(dirname(ctx.oldRevision.snapshotPath)), ctx.oldSnapshotDigest, "old snapshot");
|
|
|
|
|
const lease = ctx.thtRunner.acquireWorkspaceRuntime(current.snapshotPath); let rendered;
|
|
|
|
|
try { rendered = (await import("yaml")).parse(await readFile(lease.path, "utf8")); } finally { lease.release(); }
|
|
|
|
|
const newRoot = renderedRoot(rendered);
|
|
|
|
|
const expectedOldRoot = join(dirname(ctx.oldRevision.snapshotPath), "workspace-content", "p1-filesystem", "evidence");
|
|
|
|
|
const expectedNewRoot = join(dirname(current.snapshotPath), "workspace-content", "p1-filesystem", "evidence");
|
|
|
|
|
assert(ctx.oldFilesystemRoot === expectedOldRoot, "old filesystem root was not old commit-addressed root");
|
|
|
|
|
assert(newRoot === expectedNewRoot && newRoot !== ctx.oldFilesystemRoot, "new filesystem root did not change exactly with commit");
|
|
|
|
|
ctx.currentRevision = current;
|
|
|
|
|
return { commands: ["git"], artifacts: [await evidence(ctx.run, "logs/content-only-revision.json", { oldCommit: ctx.oldRevision.commit, newCommit: current.commit, descriptorBlob: current.blob, oldSnapshotImmutable: true })] };
|
|
|
|
|
const currentSnapshot = await currentSnapshotManifest(ctx, current.commit); ctx.currentManifest = currentSnapshot.manifest;
|
|
|
|
|
return { commands: ["git"], artifacts: [
|
|
|
|
|
await evidence(ctx.run, "logs/content-only-revision.json", { oldCommit: ctx.oldRevision.commit, newCommit: current.commit, descriptorBlob: current.blob, oldFilesystemRoot: ctx.oldFilesystemRoot, newFilesystemRoot: newRoot, oldSnapshotImmutable: true }),
|
|
|
|
|
await fileArtifact(ctx.run.root, relative(ctx.run.root, currentSnapshot.path)), await fileArtifact(ctx.run.root, relative(ctx.run.root, current.snapshotPath)),
|
|
|
|
|
] };
|
|
|
|
|
} },
|
|
|
|
|
{ id: "snapshot_and_docs", run: async () => {
|
|
|
|
|
const artifacts = [];
|
|
|
|
|
for (const id of ctx.descriptors.map((item) => item.workspace.id)) {
|
|
|
|
|
const extracted = join(ctx.run.root, "exports", "extracted", id);
|
|
|
|
|
for (const name of ZIP_FILES) assert((await lstat(join(extracted, name))).isFile(), `missing extracted ${name}`);
|
|
|
|
|
const read = (await request(ctx, `read-${id}-snapshot`, "GET", `/workspaces/${id}`)).body;
|
|
|
|
|
for (const suffix of [".yaml", ".env.example", ".md", "snapshot.json"]) {
|
|
|
|
|
const file = suffix === "snapshot.json" ? join(dirname(read.revision.snapshotPath), suffix) : join(dirname(read.revision.snapshotPath), `${id}${suffix}`);
|
|
|
|
|
assert(existsSync(file), `snapshot artifact absent ${file}`);
|
|
|
|
|
for (const name of ZIP_FILES) {
|
|
|
|
|
assert((await lstat(join(extracted, name))).isFile(), `missing extracted ${name}`);
|
|
|
|
|
artifacts.push(await fileArtifact(ctx.run.root, `exports/extracted/${id}/${name}`));
|
|
|
|
|
}
|
|
|
|
|
const revision = revisionFromManifest(ctx.currentManifest, id);
|
|
|
|
|
for (const suffix of [".yaml", ".env.example", ".md", "snapshot.json"]) {
|
|
|
|
|
const file = suffix === "snapshot.json" ? join(dirname(revision.snapshotPath), suffix) : join(dirname(revision.snapshotPath), `${id}${suffix}`);
|
|
|
|
|
assert(existsSync(file), `snapshot artifact absent ${file}`);
|
|
|
|
|
artifacts.push(await fileArtifact(ctx.run.root, relative(ctx.run.root, file)));
|
|
|
|
|
}
|
|
|
|
|
assert(!existsSync(join(dirname(revision.snapshotPath), "workspace-content")), "snapshot materialized source tree");
|
|
|
|
|
}
|
|
|
|
|
return await log("snapshot_and_docs", { exactBundleFiles: ZIP_FILES, generatedDocs: true, immutableSnapshots: true });
|
|
|
|
|
artifacts.unshift(await evidence(ctx.run, "logs/snapshot-and-docs.json", { exactBundleFiles: ZIP_FILES, generatedDocs: true, immutableSnapshots: true, derivedFromManifest: true }));
|
|
|
|
|
return { commands: [], artifacts };
|
|
|
|
|
} },
|
|
|
|
|
{ id: "runtime_render_determinism", run: async () => {
|
|
|
|
|
ctx.configChecks = [];
|
|
|
|
|
const YAML = await import("yaml");
|
|
|
|
|
ctx.configChecks = []; const artifacts = []; const YAML = await import("yaml");
|
|
|
|
|
for (const id of ctx.descriptors.map((item) => item.workspace.id)) {
|
|
|
|
|
const read = (await request(ctx, `read-${id}-runtime`, "GET", `/workspaces/${id}`)).body;
|
|
|
|
|
const bytes = [];
|
|
|
|
|
const revision = revisionFromManifest(ctx.currentManifest, id); const bytes = [];
|
|
|
|
|
for (let n = 1; n <= 2; n += 1) {
|
|
|
|
|
const lease = ctx.thtRunner.acquireWorkspaceRuntime(read.revision.snapshotPath);
|
|
|
|
|
const lease = ctx.thtRunner.acquireWorkspaceRuntime(revision.snapshotPath);
|
|
|
|
|
try {
|
|
|
|
|
const contents = await readFile(lease.path); bytes.push(contents);
|
|
|
|
|
await atomicWrite(join(ctx.run.root, "rendered", `${id}-${n}.yaml`), contents);
|
|
|
|
@@ -568,19 +716,19 @@ function productionChecks(ctx) {
|
|
|
|
|
} finally { lease.release(); }
|
|
|
|
|
const runtimeDir = join(ctx.run.root, "installation", "registry", "snapshots", "runtime");
|
|
|
|
|
if (existsSync(runtimeDir)) assert((await readdir(runtimeDir)).length === 0, "runtime lease leaked");
|
|
|
|
|
artifacts.push(await fileArtifact(ctx.run.root, `rendered/${id}-${n}.yaml`));
|
|
|
|
|
}
|
|
|
|
|
assert(bytes[0].equals(bytes[1]), `render nondeterministic ${id}`);
|
|
|
|
|
const parsed = YAML.parse(bytes[0].toString("utf8"));
|
|
|
|
|
assert(parsed.runtime_identity.workspace_id === id && parsed.runtime_identity.workspace_revision === read.revision.commit, "render identity mismatch");
|
|
|
|
|
assertRuntimeContract(ctx, id, YAML.parse(bytes[0].toString("utf8")), revision);
|
|
|
|
|
}
|
|
|
|
|
return { commands: ["tht"], artifacts: [await evidence(ctx.run, "logs/runtime-render.json", { deterministic: true, released: true, workspaces: ctx.descriptors.map((item) => item.workspace.id) })] };
|
|
|
|
|
artifacts.unshift(await evidence(ctx.run, "logs/runtime-render.json", { deterministic: true, released: true, fullSourcePolicyAssertions: true, rootsUnmaterialized: true, workspaces: ctx.descriptors.map((item) => item.workspace.id) }));
|
|
|
|
|
return { commands: ["tht"], artifacts };
|
|
|
|
|
} },
|
|
|
|
|
{ id: "tht_config_check", run: async () => {
|
|
|
|
|
assert(ctx.configChecks.length === 6 && ctx.configChecks.every(({ code }) => code === 0), "tht config checks incomplete");
|
|
|
|
|
return { commands: ["tht"], artifacts: [await evidence(ctx.run, "logs/tht-config-check.json", ctx.configChecks)] };
|
|
|
|
|
} },
|
|
|
|
|
{ id: "negative_schema_cases", run: async () => {
|
|
|
|
|
const baselineHead = (await git(["rev-parse", "HEAD"], { cwd: join(ctx.run.root, "installation", "registry", "repo") })).stdout.trim();
|
|
|
|
|
const base = structuredClone(ctx.descriptors[0]);
|
|
|
|
|
const cases = [
|
|
|
|
|
["absolute", (w) => { w.evidence.source.uri = "/tmp/evidence"; }, "evidence.source.uri"],
|
|
|
|
@@ -588,96 +736,182 @@ function productionChecks(ctx) {
|
|
|
|
|
["backslash", (w) => { w.evidence.source.uri = "workspace-content\\p1-filesystem\\evidence"; }, "evidence.source.uri"],
|
|
|
|
|
["cross-workspace", (w) => { w.evidence.source.uri = "workspace-content/other/evidence"; }, "evidence.source.uri"],
|
|
|
|
|
["unsupported-source", (w) => { w.evidence.source.type = "ftp"; w.evidence.source.uri = "ftp://example.test/file"; }, "evidence.source.type"],
|
|
|
|
|
["unsupported-protocol", (w) => { w.evidence.source = { type: "http", uris: ["ftp://evidence.example.test/file"], authentication: "none" }; }, "evidence.source.uris"],
|
|
|
|
|
["credential-field", (w) => { w.evidence.source.password = ctx.secretValues.rejected; }, "evidence.source.password"],
|
|
|
|
|
["http-userinfo-query", (w) => { w.evidence.source = { type: "http", uris: [`https://user:${ctx.secretValues.rejected}@evidence.example.test/guide?x=${ctx.secretValues.rejected}`], authentication: "none" }; }, "evidence.source.uris"],
|
|
|
|
|
["malformed-policy", (w) => { w.evidence.policy.max_chunk_chars = 0; }, "evidence.policy.max_chunk_chars"],
|
|
|
|
|
["malformed-limit", (w) => { w.evidence.source.max_bytes = 0; }, "evidence.source.max_bytes"],
|
|
|
|
|
];
|
|
|
|
|
const outcomes = [];
|
|
|
|
|
for (const [id, mutate, field] of cases) {
|
|
|
|
|
const workspace = structuredClone(base); mutate(workspace);
|
|
|
|
|
await evidence(ctx.run, `fixtures/requests/negative-${id}.json`, { case: id, expectedField: field, rawCredentialPersisted: false });
|
|
|
|
|
const response = await request(ctx, `negative-${id}`, "POST", "/workspaces/validate", { workspace });
|
|
|
|
|
assert(response.status === 400 && response.body.code === "workspace_invalid", `negative accepted ${id}`);
|
|
|
|
|
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);
|
|
|
|
|
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}`);
|
|
|
|
|
assert(JSON.stringify(response.body).includes(ctx.secretValues.rejected) === false, `negative leaked ${id}`);
|
|
|
|
|
const currentHead = (await git(["rev-parse", "HEAD"], { cwd: join(ctx.run.root, "installation", "registry", "repo") })).stdout.trim();
|
|
|
|
|
assert(currentHead === baselineHead, `negative mutated head ${id}`); outcomes.push({ id, status: 400, code: "workspace_invalid", field });
|
|
|
|
|
assertByteIdentical(await registryState(ctx), before, `negative ${id}`);
|
|
|
|
|
outcomes.push({ case: id, status: response.status, code: response.body.code, expectedInputField: field, genericSafeEnvelope: true, stateByteIdentical: true });
|
|
|
|
|
}
|
|
|
|
|
return { commands: ["git"], artifacts: [await evidence(ctx.run, "logs/negative-schema.json", outcomes, ctx.forbiddenValues)] };
|
|
|
|
|
return { commands: ["git"], artifacts: [await evidence(ctx.run, "logs/negative-schema.json", outcomes)] };
|
|
|
|
|
} },
|
|
|
|
|
{ id: "negative_context_case", run: async () => {
|
|
|
|
|
const { WorkspaceRegistry } = await loadProductionBackend(); const author = join(ctx.run.root, "author");
|
|
|
|
|
await git(["checkout", "-b", "invalid-context", ctx.contentCommit], { cwd: author }); await git(["push", "-u", "origin", "invalid-context"], { cwd: author });
|
|
|
|
|
const isolatedRoot = join(ctx.run.root, "installation", "registry-context");
|
|
|
|
|
const registry = new WorkspaceRegistry({ ...ctx.registryConfig, root: isolatedRoot, branch: "invalid-context" });
|
|
|
|
|
await registry.bootstrap(); const before = await registry.read("p1-filesystem");
|
|
|
|
|
const author = join(ctx.run.root, "author");
|
|
|
|
|
const missing = baseWorkspace("missing-context", { type: "filesystem", uri: "workspace-content/missing-context/evidence", patterns: ["**/*.md"], max_bytes: 100 });
|
|
|
|
|
await assertRejectsCode(() => registry.publish({ action: "create", workspace: missing, baseCommit: ctx.contentCommit }), "workspace_invalid");
|
|
|
|
|
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", "invalid-context"], { cwd: author });
|
|
|
|
|
await assertRejectsCode(() => registry.pull(), "workspace_invalid"); const after = await registry.read("p1-filesystem");
|
|
|
|
|
assert(after.revision.commit === before.revision.commit, "isolated active snapshot changed");
|
|
|
|
|
const primary = (await request(ctx, "primary-after-context", "GET", "/workspaces/p1-filesystem")).body;
|
|
|
|
|
assert(primary.revision.commit === ctx.contentCommit, "primary state changed");
|
|
|
|
|
return { commands: ["git"], artifacts: [await evidence(ctx.run, "logs/negative-context.json", { missingCreateRejected: true, invalidPullRejected: true, lastValidCommit: after.revision.commit, primaryCommit: primary.revision.commit })] };
|
|
|
|
|
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 })] };
|
|
|
|
|
} },
|
|
|
|
|
{ id: "no_p1_scope_artifacts", run: async () => {
|
|
|
|
|
const forbidden = ["artifacts/evidence", "corpus/ACTIVE", "embeddings", "qdrant-records", "preprocessing-invocation"];
|
|
|
|
|
const present = (await walkFiles(ctx.run.root)).map(({ rel }) => rel).filter((path) => forbidden.some((part) => path.includes(part)));
|
|
|
|
|
assert(present.length === 0, "P6 scope artifact created"); return await log("no-p1-scope-artifacts", { absent: forbidden });
|
|
|
|
|
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 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 });
|
|
|
|
|
} },
|
|
|
|
|
{ id: "secret_scan", run: async () => {
|
|
|
|
|
const findings = await scanSecrets({ runRoot: ctx.run.root, forbiddenValues: ctx.forbiddenValues }); assert(findings.length === 0, "secret canary found outside exclusion");
|
|
|
|
|
return await log("secret-scan", { scanned: true, excluded: "fixture-secrets", findings: [] });
|
|
|
|
|
const findings = await scanSecrets({ runRoot: ctx.run.root, forbiddenValues: ctx.forbiddenValues });
|
|
|
|
|
assert(findings.length === 0, "secret canary found outside exclusion");
|
|
|
|
|
return await log("secret-scan", { scanned: true, gitEnumerationFailClosed: true, excluded: "fixture-secrets", findings: [] });
|
|
|
|
|
} },
|
|
|
|
|
{ id: "cleanup_confinement", run: async () => {
|
|
|
|
|
const fakeRepo = join(ctx.run.root, "fixtures", "cleanup-repository"); await mkdir(join(fakeRepo, ".artifacts", "p1-integration"), { recursive: true });
|
|
|
|
|
const synthetic = await createOwnedRun({ repositoryRoot: fakeRepo }); const sibling = join(fakeRepo, ".artifacts", "p1-integration", `p1-${"e".repeat(32)}`); await mkdir(sibling); await writeFile(join(sibling, "sentinel"), "foreign");
|
|
|
|
|
const fakeRepo = join(ctx.run.root, "installation", "runtime", "cleanup-test");
|
|
|
|
|
await mkdir(join(fakeRepo, ".artifacts", "p1-integration"), { recursive: true });
|
|
|
|
|
const synthetic = await createOwnedRun({ repositoryRoot: fakeRepo });
|
|
|
|
|
const sibling = join(fakeRepo, ".artifacts", "p1-integration", `p1-${"e".repeat(32)}`);
|
|
|
|
|
await mkdir(sibling); await writeFile(join(sibling, "sentinel"), "foreign");
|
|
|
|
|
await cleanupOwnedRun({ repositoryRoot: fakeRepo, runRoot: synthetic.root, expectedNonce: synthetic.nonce });
|
|
|
|
|
assert(await readFile(join(sibling, "sentinel"), "utf8") === "foreign", "cleanup removed sibling");
|
|
|
|
|
return await log("cleanup-confinement", { ownedRemoved: true, siblingPreserved: true });
|
|
|
|
|
await rm(fakeRepo, { recursive: true });
|
|
|
|
|
assert(!existsSync(fakeRepo), "cleanup test resource remained");
|
|
|
|
|
return await log("cleanup-confinement", { ownedRemoved: true, siblingPreservedDuringAssertion: true, testResourceRemoved: true });
|
|
|
|
|
} },
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function assertRejectsCode(fn, code) {
|
|
|
|
|
try { await fn(); } catch (error) { if (error?.code === code) return; throw error; }
|
|
|
|
|
throw new Error(`expected ${code}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function runIntegration({ repositoryRoot = defaultRepositoryRoot, keep = false, env = process.env, failAt = env.P1_ACCEPTANCE_FAIL_AT, checks } = {}) {
|
|
|
|
|
function replaceProcessEnvironment(values) {
|
|
|
|
|
for (const key of Object.keys(process.env)) delete process.env[key];
|
|
|
|
|
Object.assign(process.env, values);
|
|
|
|
|
}
|
|
|
|
|
function failedCheck(id, startedAt, error) {
|
|
|
|
|
return { id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error };
|
|
|
|
|
}
|
|
|
|
|
function completeFailedResults(results, firstError = "Acceptance setup failed safely.") {
|
|
|
|
|
const completed = [...results];
|
|
|
|
|
for (let index = completed.length; index < CHECK_IDS.length; index += 1) {
|
|
|
|
|
completed.push(failedCheck(CHECK_IDS[index], nowIso(), index === 0 ? firstError : "Not executed after earlier failure."));
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
try {
|
|
|
|
|
run = await createOwnedRun({ repositoryRoot });
|
|
|
|
|
if (checks === undefined) { ctx = await setupContext(run, repositoryRoot, env); checks = productionChecks(ctx); }
|
|
|
|
|
commandEventSink = [];
|
|
|
|
|
if (checks === undefined) {
|
|
|
|
|
ctx = await setup(run, repositoryRoot, env);
|
|
|
|
|
if (!ctx) throw new Error("acceptance setup returned no context");
|
|
|
|
|
replaceProcessEnvironment(ctx.env);
|
|
|
|
|
checks = productionChecks(ctx);
|
|
|
|
|
}
|
|
|
|
|
results = await executeChecks({ checks, failAt });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
fatal = error;
|
|
|
|
|
if (run && results.length === 0) results = [{ id: "preflight", status: "FAIL", startedAt: run.startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: "Acceptance setup failed safely." }];
|
|
|
|
|
if (run) results = completeFailedResults(results);
|
|
|
|
|
} finally {
|
|
|
|
|
if (ctx?.app) {
|
|
|
|
|
await ctx.app.close().catch(() => {});
|
|
|
|
|
await writeOwnership(run, { ...run.listener, state: "closed" }).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
for (const key of Object.keys(process.env)) if (!(key in savedEnv)) delete process.env[key];
|
|
|
|
|
Object.assign(process.env, savedEnv);
|
|
|
|
|
if (ctx?.originalFetch) globalThis.fetch = ctx.originalFetch;
|
|
|
|
|
replaceProcessEnvironment(savedEnv);
|
|
|
|
|
}
|
|
|
|
|
if (!run) throw fatal;
|
|
|
|
|
const success = !fatal && results.length === checks.length && results.every(({ status }) => status === "PASS");
|
|
|
|
|
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)}\n`); let mdBytes = Buffer.from(renderReportMarkdown(report));
|
|
|
|
|
let jsonBytes = Buffer.from(`${JSON.stringify(report, null, 2)}
|
|
|
|
|
`); let mdBytes = Buffer.from(renderReportMarkdown(report));
|
|
|
|
|
if (ctx?.forbiddenValues) {
|
|
|
|
|
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: ctx.forbiddenValues, virtualFiles: [{ path: "report.json", bytes: jsonBytes }, { path: "report.md", bytes: mdBytes }] });
|
|
|
|
|
if (findings.length) {
|
|
|
|
|
report.overall = "FAIL";
|
|
|
|
|
const secret = report.checks.find(({ id }) => id === "secret_scan"); if (secret) secret.status = "FAIL";
|
|
|
|
|
else report.checks.push({ id: "secret_scan", status: "FAIL", startedAt: nowIso(), finishedAt: nowIso(), commands: [], artifacts: [], error: "Secret scan found protected content." });
|
|
|
|
|
jsonBytes = Buffer.from(`${JSON.stringify(report, null, 2)}\n`); mdBytes = Buffer.from(renderReportMarkdown(report));
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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.";
|
|
|
|
|
}
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
await atomicWrite(join(run.root, "report.json"), jsonBytes); await atomicWrite(join(run.root, "report.md"), mdBytes);
|
|
|
|
|
const finalSuccess = report.overall === "PASS";
|
|
|
|
|
if (announce) await announce({ report, runRoot: run.root, keep });
|
|
|
|
|
const removed = await finalizeOwnedRun({ run, success: finalSuccess, keep });
|
|
|
|
|
return { exitCode: finalSuccess ? 0 : 1, runRoot: run.root, retained: !removed, report };
|
|
|
|
|
}
|
|
|
|
@@ -687,8 +921,14 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
|
|
console.error("usage: p1-acceptance integration [--keep]"); return 2;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const result = await runIntegration({ repositoryRoot: defaultRepositoryRoot, keep: argv.includes("--keep"), env });
|
|
|
|
|
if (result.retained) console.log(result.runRoot);
|
|
|
|
|
const result = await runIntegration({
|
|
|
|
|
repositoryRoot: defaultRepositoryRoot, keep: argv.includes("--keep"), env,
|
|
|
|
|
announce: async ({ report, runRoot, keep }) => {
|
|
|
|
|
console.log(`automated integration: ${report.overall}`);
|
|
|
|
|
console.log("manual acceptance: PENDING");
|
|
|
|
|
if (keep || report.overall !== "PASS") console.log(runRoot);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return result.exitCode;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("P1 acceptance failed before owning a reportable run."); return 1;
|
|
|
|
|