From 60f0920bd35f141eaafaa580c1e0f4ce6fe43481 Mon Sep 17 00:00:00 2001 From: 0thernet Date: Wed, 2 Sep 2026 14:34:43 -0400 Subject: [PATCH 1/2] feat: support reviewed Bombadil toolchain overrides --- README.md | 32 +- dist/tooling/bombadil.js | 580 ++++++++++++++- docs/adoption.md | 4 +- docs/publishing.md | 6 +- docs/verification.md | 46 +- package.json | 2 +- scripts/npm-publish-workflow.test.ts | 6 +- scripts/package-smoke.ts | 32 +- skills/direct/references/install.md | 10 +- src/exports.test.ts | 9 + src/tooling/bombadil-runner.test.ts | 883 +++++++++++++++++++++- src/tooling/bombadil-runner.ts | 1028 +++++++++++++++++++++++++- src/tooling/bombadil.ts | 2 + 13 files changed, 2559 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 5735f73..5421cd3 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ real interface and feature state Pin Direct as a development dependency: ```sh -bun add --dev @hraness/direct@0.7.18 +bun add --dev @hraness/direct@0.7.19 # or -npm install --save-dev @hraness/direct@0.7.18 +npm install --save-dev @hraness/direct@0.7.19 ``` Keep Direct in `devDependencies`. A production entry must not import Direct, @@ -58,7 +58,7 @@ composition. It requires Git and Bun 1.3.14, then downloads the source and its development dependencies: ```sh -git clone --branch v0.7.18 --depth 1 https://github.com/hraness/direct.git +git clone --branch v0.7.19 --depth 1 https://github.com/hraness/direct.git cd direct bun install --frozen-lockfile --ignore-scripts bun run example:direct @@ -132,9 +132,9 @@ claim separate as `direct` evidence. Install Direct's single bundled skill from the public repository: ```sh -npx skills add hraness/direct#v0.7.18 +npx skills add hraness/direct#v0.7.19 # or -bunx skills add hraness/direct#v0.7.18 +bunx skills add hraness/direct#v0.7.19 ``` The skill is invoked as `$direct`. It routes installation, adoption, and @@ -148,7 +148,7 @@ Copy this prompt into Codex, Claude Code, or another coding agent: ```text Use $direct to install hraness/direct from -the npm registry at the exact 0.7.18 version. Follow the repository README, add +the npm registry at the exact 0.7.19 version. Follow the repository README, add `@hraness/direct` to devDependencies only, and verify that the production dependency graph excludes Direct. Do not add a fixture composition until I ask. @@ -164,7 +164,7 @@ Pin the public npm package to an exact immutable version: ```json { "devDependencies": { - "@hraness/direct": "0.7.18" + "@hraness/direct": "0.7.19" } } ``` @@ -186,8 +186,8 @@ quiescence, coverage claims, cleanup, and emitted production boundaries. The package smoke test keeps that future packaged copy byte-identical to the repository skill. -Prefer `npx skills add hraness/direct#v0.7.18` or -`bunx skills add hraness/direct#v0.7.18` for runner discovery. You can also copy +Prefer `npx skills add hraness/direct#v0.7.19` or +`bunx skills add hraness/direct#v0.7.19` for runner discovery. You can also copy or link that one skill directory into a runner's configured location, then invoke `$direct`. Package installation leaves the skill inert: it does not run a `postinstall` hook or edit repository or user configuration. @@ -384,6 +384,20 @@ campaign selector for replay. Matrix upload plans are public-summary only and publish one atomic parent leaf; run a selected campaign directly for bounded access-controlled private diagnostics. +The npm binary remains the default. A consumer that must validate a reviewed +native fix before its next Bombadil release may set `bombadilToolchain` to one +exact repository-confined executable, SHA-256 digest, full source revision, +supported build contract, and version `0.7.2`. Direct rejects partial or mixed +matrix identities, symlinks, non-executable files, executables larger than 64 +MiB, and digest drift while it opens the configured executable. It copies those +attested open-file bytes into +one private task-owned read/execute-only snapshot, then runs both the bounded +version probe and native campaign from that snapshot. Replacing the configured +path after this boundary cannot change the executed bytes. The consumer must +still install the exact `@antithesishq/bombadil@0.7.2` package for specification +imports. Local raw evidence records the reviewed identity; sanitized upload +receipts omit its path and provenance. + Scheduled wrappers should precompute one lowercase UUID and pass it through the runner's `artifactRun` option. Resolve the exact leaf with `resolveDirectBombadilUploadLeaf` and upload only that leaf with `if: always()`. diff --git a/dist/tooling/bombadil.js b/dist/tooling/bombadil.js index bec85ba..7ec08a2 100644 --- a/dist/tooling/bombadil.js +++ b/dist/tooling/bombadil.js @@ -9,8 +9,10 @@ import { readFile, realpath, rename as rename2, + rmdir, rm as rm2, - stat + stat, + unlink } from "fs/promises"; import { extname, isAbsolute, join as join2, relative, resolve } from "path"; import process2 from "process"; @@ -1194,6 +1196,23 @@ async function writeJsonAtomically(path, value) { // src/tooling/bombadil-runner.ts var EXPECTED_BOMBADIL_VERSION = "0.7.2"; +var BOMBADIL_TOOLCHAIN_BUILD_CONTRACTS = new Set([ + "cargo-release-browser-only", + "nix-default-aarch64-darwin" +]); +var BOMBADIL_TOOLCHAIN_CONFIG_KEYS = [ + "buildContract", + "executablePath", + "sha256", + "sourceRevision", + "version" +]; +var BOMBADIL_TOOLCHAIN_ENVIRONMENT_NAMES = [ + "DIRECT_BOMBADIL_BUILD_CONTRACT", + "DIRECT_BOMBADIL_EXECUTABLE_PATH", + "DIRECT_BOMBADIL_EXECUTABLE_SHA256", + "DIRECT_BOMBADIL_SOURCE_REVISION" +]; var DEFAULT_TIME_LIMIT_SECONDS = 20; var MIN_TIME_LIMIT_SECONDS = 12; var MAX_TIME_LIMIT_SECONDS = 300; @@ -1296,6 +1315,7 @@ var MATRIX_SUMMARY_CAMPAIGNS_KEYS = new Set([ "total" ]); var SHA256_PATTERN = /^[0-9a-f]{64}$/u; +var GIT_REVISION_PATTERN = /^[0-9a-f]{40}$/u; var ARTIFACT_EVIDENCE_JSON_LIMITS = Object.freeze({ maxDepth: 8, maxNodes: 2048, @@ -1306,6 +1326,10 @@ var ARTIFACT_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u; var MAX_ARTIFACT_IDENTIFIER_LENGTH = 80; var MAX_MATRIX_CAMPAIGNS = 32; var ARTIFACT_COORDINATION_ENVIRONMENT = "DIRECT_BOMBADIL_RUN_ID"; +var BOMBADIL_PRIVATE_ENVIRONMENT_NAMES = new Set([ + ARTIFACT_COORDINATION_ENVIRONMENT, + ...BOMBADIL_TOOLCHAIN_ENVIRONMENT_NAMES +]); var ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u; var QUERY_PARAMETER_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]*$/u; var PROTOTYPE_PROPERTY_NAMES = new Set(["__proto__", "constructor", "prototype"]); @@ -1323,6 +1347,9 @@ var RANDOM_RUN_OVERHEAD_MS = 30000; var REPLAY_WALL_CLOCK_TIMEOUT_MS = MAX_TIME_LIMIT_SECONDS * 1000 + RANDOM_RUN_OVERHEAD_MS; var PROCESS_TERMINATION_GRACE_MS = 5000; var MIN_PROCESS_OUTPUT_DRAIN_MS = 500; +var BOMBADIL_VERSION_PROBE_TIMEOUT_MS = 5000; +var BOMBADIL_VERSION_OUTPUT_LIMIT = 1024; +var MAX_BOMBADIL_EXECUTABLE_BYTES = 64 * 1024 * 1024; var SERVER_OUTPUT_TIMEOUT_MS = 3000; var ARTIFACT_MONITOR_INTERVAL_MS = 100; var MAX_LIVE_CHROME_RENAME_RETRIES = 4; @@ -1456,6 +1483,7 @@ var DIRECT_OBSERVATION_KEYS = new Set([ "violations", "violationsValid" ]); +var BOMBADIL_EXECUTABLE_ATTESTATION = Symbol("bombadilExecutableAttestation"); var PROCESS_INTERRUPT_SIGNALS = ["SIGINT", "SIGTERM"]; class BombadilArtifactPolicyError extends Error { @@ -1487,6 +1515,12 @@ class BombadilPersistenceError extends AggregateError { this.name = "BombadilPersistenceError"; } } +function retainPrivateBombadilSnapshotAfterWriterSettlementFailure(failure) { + return new BombadilWriterSettlementError("Bombadil writers were not proven absent; the private executable snapshot was retained as protected persistence evidence", new AggregateError([ + failure, + new BombadilPersistenceError("Bombadil private executable snapshot removal was suppressed", [new Error("native process and writer settlement was not proven")]) + ], "Bombadil writer settlement and private snapshot persistence evidence", { cause: failure })); +} function readOptionValue(arguments_, index, option) { const value = arguments_[index + 1]; if (value === undefined || value.startsWith("-")) { @@ -1518,6 +1552,53 @@ function bombadilNativeBinary(repositoryRoot) { } return join2(repositoryRoot, "node_modules", "@antithesishq", "bombadil", "binaries", binary); } +function validateBombadilToolchainConfig(value, repositoryRoot) { + if (value === undefined) + return null; + if (!isRecord2(value)) { + throw new Error("bombadilToolchain must be an exact object"); + } + const keys = Object.keys(value).sort(compareCodeUnits); + if (JSON.stringify(keys) !== JSON.stringify(BOMBADIL_TOOLCHAIN_CONFIG_KEYS)) { + throw new Error(`bombadilToolchain must contain exactly ${BOMBADIL_TOOLCHAIN_CONFIG_KEYS.join(", ")}`); + } + const executablePath = Reflect.get(value, "executablePath"); + const sha256 = Reflect.get(value, "sha256"); + const sourceRevision = Reflect.get(value, "sourceRevision"); + const version = Reflect.get(value, "version"); + const buildContract = Reflect.get(value, "buildContract"); + if (typeof executablePath !== "string" || !isAbsolute(executablePath) || resolve(executablePath) !== executablePath || !isWithin(repositoryRoot, executablePath)) { + throw new Error("bombadilToolchain.executablePath must be an absolute normalized path inside repositoryRoot"); + } + if (typeof sha256 !== "string" || !SHA256_PATTERN.test(sha256)) { + throw new Error("bombadilToolchain.sha256 must be exactly 64 lowercase hexadecimal characters"); + } + if (typeof sourceRevision !== "string" || !GIT_REVISION_PATTERN.test(sourceRevision)) { + throw new Error("bombadilToolchain.sourceRevision must be exactly 40 lowercase hexadecimal characters"); + } + if (version !== EXPECTED_BOMBADIL_VERSION) { + throw new Error(`bombadilToolchain.version must be exactly ${EXPECTED_BOMBADIL_VERSION}`); + } + if (typeof buildContract !== "string" || !BOMBADIL_TOOLCHAIN_BUILD_CONTRACTS.has(buildContract)) { + throw new Error("bombadilToolchain.buildContract is unsupported"); + } + return Object.freeze({ + buildContract, + executablePath, + sha256, + sourceRevision, + version: EXPECTED_BOMBADIL_VERSION + }); +} +function bombadilToolchainIdentity(toolchain) { + return toolchain === null ? "npm-package" : JSON.stringify({ + buildContract: toolchain.buildContract, + executablePath: toolchain.executablePath, + sha256: toolchain.sha256, + sourceRevision: toolchain.sourceRevision, + version: toolchain.version + }); +} function requireLocalRootHttpOrigin(value) { const baseUrl = normalizeRootHttpOrigin(value); const url = new URL(baseUrl); @@ -4230,6 +4311,7 @@ function validateDirectBombadilFuzzConfig(config, baseUrlOverride) { const viewport = validateViewport(config.viewport); const explorationPolicy = validateExplorationPolicy(config.explorationPolicy); const artifactPolicy = validateArtifactPolicy(config.artifactPolicy); + const bombadilToolchain = validateBombadilToolchainConfig(config.bombadilToolchain, repositoryRoot); const startupTimeoutMs = config.server.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS; if (!Number.isSafeInteger(startupTimeoutMs) || startupTimeoutMs < 1000 || startupTimeoutMs > MAX_STARTUP_TIMEOUT_MS) { throw new Error(`server.startupTimeoutMs must be an integer between 1000 and ${String(MAX_STARTUP_TIMEOUT_MS)}`); @@ -4243,7 +4325,8 @@ function validateDirectBombadilFuzzConfig(config, baseUrlOverride) { specificationPath, baseUrl, artifactRoot: join2(repositoryRoot, "artifacts", "direct-bombadil", config.artifactName), - bombadilExecutable: bombadilNativeBinary(repositoryRoot), + bombadilExecutable: bombadilToolchain?.executablePath ?? bombadilNativeBinary(repositoryRoot), + bombadilToolchain, entryPath, explorationPolicy, port, @@ -4303,6 +4386,31 @@ function createDirectBombadilInvocation(options) { wallClockTimeoutMs: options.replayPath === null ? options.timeLimitSeconds * 1000 + RANDOM_RUN_OVERHEAD_MS : REPLAY_WALL_CLOCK_TIMEOUT_MS }; } +function bombadilChildEnvironment() { + return Object.fromEntries(Object.entries({ + ...process2.env, + NO_COLOR: "1" + }).filter(([name]) => !BOMBADIL_PRIVATE_ENVIRONMENT_NAMES.has(name))); +} +async function readBoundedProcessStream(stream, maximumBytes, label) { + const reader = stream.getReader(); + const decoder = new TextDecoder; + let bytes = 0; + let output = ""; + for (;; ) { + const chunk = await reader.read(); + if (chunk.done) + return `${output}${decoder.decode()}`; + bytes += chunk.value.byteLength; + if (bytes > maximumBytes) { + reader.cancel().catch(() => { + return; + }); + throw new Error(`${label} exceeded ${String(maximumBytes)} bytes`); + } + output += decoder.decode(chunk.value, { stream: true }); + } +} function captureStream(stream, maximumLength = LOG_LIMIT) { let stopCapture; const stopped = new Promise((resolveStopped) => { @@ -4332,15 +4440,23 @@ function captureStream(stream, maximumLength = LOG_LIMIT) { })(); return { result, stop: stopCapture }; } -function signalProcessGroup(process_, signal) { - try { - process2.kill(-process_.pid, signal); - return; - } catch (error) { - if (!isRecord2(error) || error.code !== "ESRCH") - throw error; - if (process_.exitCode === null) - process_.kill(signal); +async function signalProcessGroup(process_, signal, timeoutMs) { + const deadline = Date.now() + timeoutMs; + for (;; ) { + try { + process2.kill(-process_.pid, signal); + return; + } catch (error) { + if (isRecord2(error) && error.code === "ESRCH") { + if (process_.exitCode === null) + process_.kill(signal); + return; + } + if (!isRecord2(error) || error.code !== "EPERM" || Date.now() >= deadline) { + throw error; + } + await Bun.sleep(Math.min(10, Math.max(1, deadline - Date.now()))); + } } } function processGroupMayExist(processId) { @@ -4377,13 +4493,66 @@ async function waitForBombadilLeaderExit(process_, timeoutMs) { } async function settleBombadilProcessGroup(options) { try { - signalProcessGroup(options.process, "SIGKILL"); + await signalProcessGroup(options.process, "SIGKILL", options.timeoutMs); await waitForBombadilLeaderExit(options.process, options.timeoutMs); await waitForProcessGroupExit(options.process.pid, options.timeoutMs); } catch (error) { throw new BombadilWriterSettlementError(`Bombadil process group ${String(options.process.pid)} did not settle safely`, error); } } +async function readExactBombadilExecutableVersion(executablePath, repositoryRoot, expectedAttestation, options = {}) { + const maximumOutputBytes = options.maximumOutputBytes ?? BOMBADIL_VERSION_OUTPUT_LIMIT; + const timeoutMs = options.timeoutMs ?? BOMBADIL_VERSION_PROBE_TIMEOUT_MS; + const probeAttestation = await attestBombadilExecutable(executablePath); + assertSameBombadilExecutableAttestation(expectedAttestation, probeAttestation); + const process_ = Bun.spawn([executablePath, "--version"], { + cwd: repositoryRoot, + detached: true, + env: bombadilChildEnvironment(), + stdin: "ignore", + stdout: "pipe", + stderr: "pipe" + }); + const output = Promise.all([ + readBoundedProcessStream(process_.stdout, maximumOutputBytes, "Bombadil --version stdout"), + readBoundedProcessStream(process_.stderr, maximumOutputBytes, "Bombadil --version stderr") + ]); + let timeout; + try { + const timeoutPromise = new Promise((resolveTimeout) => { + timeout = setTimeout(() => resolveTimeout({ kind: "timeout" }), timeoutMs); + }); + const outcome = await Promise.race([ + Promise.all([process_.exited, output]).then(([exitCode, streams]) => ({ + exitCode, + kind: "exited", + stderr: streams[1], + stdout: streams[0] + })), + timeoutPromise + ]); + if (outcome.kind === "timeout") { + throw new Error(`Bombadil --version exceeded its ${String(timeoutMs)}ms wall-clock limit`); + } + if (outcome.exitCode !== 0) { + throw new Error(`Bombadil --version exited with status ${String(outcome.exitCode)}`); + } + if (outcome.stderr !== "") { + throw new Error("Bombadil --version wrote unexpected stderr"); + } + if (!/^bombadil 0\.7\.2(?:\r?\n)?$/u.test(outcome.stdout)) { + throw new Error(`Bombadil executable must report exactly bombadil ${EXPECTED_BOMBADIL_VERSION}`); + } + return EXPECTED_BOMBADIL_VERSION; + } finally { + if (timeout !== undefined) + clearTimeout(timeout); + await settleBombadilProcessGroup({ + process: process_, + timeoutMs: PROCESS_TERMINATION_GRACE_MS + }); + } +} async function establishCleanBombadilArtifactBaseline(options) { let baseline; try { @@ -4443,6 +4612,7 @@ async function runBombadilNativeProcessInternal(invocation, hooks = {}) { if (bombadilAbortRequested(invocation.abortSignal)) { return abortedBombadilProcessResult(); } + const expectedExecutableAttestation = invocation[BOMBADIL_EXECUTABLE_ATTESTATION] ?? await attestBombadilExecutable(invocation.command[0] ?? ""); await establishCleanBombadilArtifactBaseline({ ...hooks.beforeArtifactBaselineInspect === undefined ? {} : { beforeInspect: hooks.beforeArtifactBaselineInspect }, outputPath: invocation.outputPath, @@ -4451,10 +4621,15 @@ async function runBombadilNativeProcessInternal(invocation, hooks = {}) { if (bombadilAbortRequested(invocation.abortSignal)) { return abortedBombadilProcessResult(); } - const childEnvironment = Object.fromEntries(Object.entries({ - ...process2.env, - NO_COLOR: "1" - }).filter(([name]) => name !== ARTIFACT_COORDINATION_ENVIRONMENT)); + const childEnvironment = bombadilChildEnvironment(); + const spawnAttestation = await attestBombadilExecutable(invocation.command[0] ?? ""); + assertSameBombadilExecutableAttestation(expectedExecutableAttestation, spawnAttestation); + if (hooks.afterFinalExecutableAttestation !== undefined) { + await hooks.afterFinalExecutableAttestation(); + } + if (bombadilAbortRequested(invocation.abortSignal)) { + return abortedBombadilProcessResult(); + } const process_ = Bun.spawn([...invocation.command], { cwd: invocation.cwd, detached: true, @@ -4563,6 +4738,7 @@ var defaultDependencies = { createAbortController: () => new AbortController, createRunId: randomUUID2, now: () => new Date, + readBombadilVersion: readExactBombadilVersion, runBombadil: runBombadilNativeProcess, signalController: { forward: (signal) => process2.kill(process2.pid, signal), @@ -4605,6 +4781,317 @@ async function requireRegularFile(path, label) { if (!metadata.isFile()) throw new Error(`${label} must be a regular file`); } +async function resolveBombadilExecutablePath(candidate, repositoryRoot) { + if (!isAbsolute(candidate) || resolve(candidate) !== candidate || !isWithin(repositoryRoot, candidate)) { + throw new Error("The root Bombadil executable must be an absolute normalized path inside repositoryRoot"); + } + let metadata; + try { + metadata = await lstat(candidate); + } catch { + throw new Error("The root Bombadil executable does not exist at its configured path"); + } + if (metadata.isSymbolicLink() || !metadata.isFile()) { + throw new Error("The root Bombadil executable must be a regular nonsymlink file"); + } + if ((metadata.mode & 73) === 0) { + throw new Error("The root Bombadil executable must have an executable mode bit"); + } + const resolved = await resolveExistingRealPath(candidate, "The root Bombadil executable"); + if (resolved !== candidate || !isWithin(repositoryRoot, resolved)) { + throw new Error("The root Bombadil executable must not traverse a symlink or escape repositoryRoot"); + } + return resolved; +} +function sameBombadilExecutableIdentity(first, second) { + return first.dev === second.dev && first.ino === second.ino && first.nlink === second.nlink && first.mode === second.mode && first.size === second.size && first.mtimeNs === second.mtimeNs && first.ctimeNs === second.ctimeNs; +} +async function attestBombadilExecutable(path, options = {}) { + let lexical; + try { + lexical = await lstat(path, { bigint: true }); + } catch { + throw new Error("The root Bombadil executable does not exist at its configured path"); + } + if (lexical.isSymbolicLink() || !lexical.isFile()) { + throw new Error("The root Bombadil executable must be a regular nonsymlink file"); + } + if ((lexical.mode & 0o111n) === 0n) { + throw new Error("The root Bombadil executable must have an executable mode bit"); + } + if (lexical.size > BigInt(MAX_BOMBADIL_EXECUTABLE_BYTES)) { + throw new Error(`The root Bombadil executable exceeds the ${String(MAX_BOMBADIL_EXECUTABLE_BYTES)}-byte attestation limit`); + } + const handle = await open(path, fileSystemConstants.O_RDONLY | fileSystemConstants.O_NOFOLLOW); + let snapshotHandle = null; + try { + if (options.snapshotPath !== undefined) { + snapshotHandle = await open(options.snapshotPath, fileSystemConstants.O_WRONLY | fileSystemConstants.O_CREAT | fileSystemConstants.O_EXCL | fileSystemConstants.O_NOFOLLOW, 384); + } + const before = await handle.stat({ bigint: true }); + if (!before.isFile() || !sameBombadilExecutableIdentity(lexical, before)) { + throw new Error("The root Bombadil executable changed during attestation"); + } + const hash = createHash("sha256"); + const buffer = Buffer.allocUnsafe(1024 * 1024); + let position = 0; + const size = Number(before.size); + while (position < size) { + const length = Math.min(buffer.byteLength, size - position); + const { bytesRead } = await handle.read(buffer, 0, length, position); + if (bytesRead <= 0) { + throw new Error("The root Bombadil executable changed during attestation"); + } + const bytes = buffer.subarray(0, bytesRead); + hash.update(bytes); + if (snapshotHandle !== null) { + let written = 0; + while (written < bytesRead) { + const result = await snapshotHandle.write(bytes, written, bytesRead - written, position + written); + if (result.bytesWritten <= 0) { + throw new Error("The private Bombadil executable snapshot write made no progress"); + } + written += result.bytesWritten; + } + } + position += bytesRead; + } + if (snapshotHandle !== null) { + await snapshotHandle.chmod(320); + await snapshotHandle.sync(); + } + const after = await handle.stat({ bigint: true }); + if (!sameBombadilExecutableIdentity(before, after)) { + throw new Error("The root Bombadil executable changed during attestation"); + } + const finalLexical = await lstat(path, { bigint: true }); + if (!sameBombadilExecutableIdentity(after, finalLexical)) { + throw new Error("The root Bombadil executable changed during attestation"); + } + return Object.freeze({ + device: after.dev, + inode: after.ino, + linkCount: after.nlink, + mode: after.mode, + sha256: hash.digest("hex"), + size + }); + } finally { + try { + await snapshotHandle?.close(); + } finally { + await handle.close(); + } + } +} +async function removePrivateBombadilExecutableSnapshot(snapshot) { + if (snapshot.attestation.linkCount !== 1n) { + throw new Error("The private Bombadil executable snapshot file identity is not exclusive"); + } + const directory = await openPrivateBombadilSnapshotDirectory(snapshot); + let madeWritable = false; + let directoryRemoved = false; + try { + await requireSamePrivateBombadilSnapshotExecutable(snapshot); + await directory.chmod(448); + madeWritable = true; + await requireSameOpenPrivateBombadilSnapshotDirectory(directory, snapshot, 0o700n); + await requireSamePrivateBombadilSnapshotExecutable(snapshot); + await requireSameOpenPrivateBombadilSnapshotDirectory(directory, snapshot, 0o700n); + await requireSameLexicalPrivateBombadilSnapshotExecutable(snapshot); + await unlink(snapshot.executablePath); + await requireMissingPrivateBombadilSnapshotPath(snapshot.executablePath, "executable"); + await requireSameOpenPrivateBombadilSnapshotDirectory(directory, snapshot, 0o700n); + await rmdir(snapshot.directoryPath); + directoryRemoved = true; + await requireMissingPrivateBombadilSnapshotPath(snapshot.directoryPath, "directory"); + } catch (error) { + if (madeWritable && !directoryRemoved) { + try { + const descriptor = await directory.stat({ bigint: true }); + if (descriptor.isDirectory() && descriptor.dev === snapshot.directoryIdentity.device && descriptor.ino === snapshot.directoryIdentity.inode) { + await directory.chmod(Number(snapshot.directoryIdentity.mode & 0o777n)); + } + } catch (resealError) { + throw new AggregateError([error, resealError], "Bombadil snapshot cleanup failed and its exact directory could not be resealed", { cause: error }); + } + } + throw error; + } finally { + await directory.close(); + } +} +function sameBombadilSnapshotDirectoryIdentity(metadata, identity, permissions = identity.mode & 0o777n) { + const expectedMode = identity.mode & ~0o777n | permissions; + return metadata.isDirectory() && !metadata.isSymbolicLink() && metadata.dev === identity.device && metadata.ino === identity.inode && metadata.mode === expectedMode; +} +async function readPrivateBombadilSnapshotDirectoryIdentity(path) { + const metadata = await lstat(path, { bigint: true }); + if (metadata.isSymbolicLink() || !metadata.isDirectory()) { + throw new Error("The private Bombadil executable snapshot path is not a nonsymlink directory"); + } + return Object.freeze({ + device: metadata.dev, + inode: metadata.ino, + mode: metadata.mode + }); +} +async function openPrivateBombadilSnapshotDirectory(snapshot) { + const lexical = await lstat(snapshot.directoryPath, { bigint: true }); + if (!sameBombadilSnapshotDirectoryIdentity(lexical, snapshot.directoryIdentity)) { + throw new Error("The private Bombadil executable snapshot directory identity changed"); + } + const directory = await open(snapshot.directoryPath, fileSystemConstants.O_RDONLY | fileSystemConstants.O_DIRECTORY | fileSystemConstants.O_NOFOLLOW); + try { + const descriptor = await directory.stat({ bigint: true }); + if (!sameBombadilSnapshotDirectoryIdentity(descriptor, snapshot.directoryIdentity)) { + throw new Error("The private Bombadil executable snapshot directory identity changed"); + } + return directory; + } catch (error) { + await directory.close(); + throw error; + } +} +async function requireSameOpenPrivateBombadilSnapshotDirectory(directory, snapshot, permissions) { + const descriptor = await directory.stat({ bigint: true }); + const lexical = await lstat(snapshot.directoryPath, { bigint: true }); + if (!sameBombadilSnapshotDirectoryIdentity(descriptor, snapshot.directoryIdentity, permissions) || !sameBombadilSnapshotDirectoryIdentity(lexical, snapshot.directoryIdentity, permissions)) { + throw new Error("The private Bombadil executable snapshot directory identity changed"); + } +} +async function requireSamePrivateBombadilSnapshotExecutable(snapshot) { + const attestation = await attestBombadilExecutable(snapshot.executablePath); + if (attestation.device !== snapshot.attestation.device || attestation.inode !== snapshot.attestation.inode || attestation.linkCount !== snapshot.attestation.linkCount || attestation.mode !== snapshot.attestation.mode || attestation.size !== snapshot.attestation.size || attestation.sha256 !== snapshot.attestation.sha256) { + throw new Error("The private Bombadil executable snapshot file identity changed"); + } +} +async function requireSameLexicalPrivateBombadilSnapshotExecutable(snapshot) { + const metadata = await lstat(snapshot.executablePath, { bigint: true }); + if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.dev !== snapshot.attestation.device || metadata.ino !== snapshot.attestation.inode || metadata.nlink !== snapshot.attestation.linkCount || metadata.mode !== snapshot.attestation.mode || metadata.size !== BigInt(snapshot.attestation.size)) { + throw new Error("The private Bombadil executable snapshot file identity changed"); + } +} +async function requireMissingPrivateBombadilSnapshotPath(path, label) { + try { + await lstat(path); + } catch (error) { + if (isRecord2(error) && error.code === "ENOENT") + return; + throw error; + } + throw new Error(`The private Bombadil executable snapshot ${label} survived cleanup`); +} +async function sealPartialPrivateBombadilSnapshotForRetention(snapshot) { + const directory = await openPrivateBombadilSnapshotDirectory(snapshot); + try { + await directory.chmod(320); + await requireSameOpenPrivateBombadilSnapshotDirectory(directory, snapshot, 0o500n); + } finally { + await directory.close(); + } +} +async function materializePrivateBombadilExecutableSnapshot(options) { + await createExclusiveDirectory(options.directoryPath, "Private Bombadil executable snapshot directory"); + const executablePath = join2(options.directoryPath, "bombadil"); + let initialDirectoryIdentity = null; + let cleanupSnapshot = null; + try { + initialDirectoryIdentity = await readPrivateBombadilSnapshotDirectoryIdentity(options.directoryPath); + const sourceAttestation = await attestBombadilExecutable(options.sourcePath, { + snapshotPath: executablePath + }); + const attestation = await attestBombadilExecutable(executablePath); + cleanupSnapshot = Object.freeze({ + attestation, + directoryIdentity: initialDirectoryIdentity, + directoryPath: options.directoryPath, + executablePath, + sourceAttestation + }); + if (sourceAttestation.sha256 !== attestation.sha256 || sourceAttestation.size !== attestation.size) { + throw new Error("The private Bombadil executable snapshot does not match reviewed bytes"); + } + if (sourceAttestation.device === attestation.device && sourceAttestation.inode === attestation.inode) { + throw new Error("The private Bombadil executable snapshot must own a distinct file identity"); + } + if ((attestation.mode & 0o777n) !== 0o500n) { + throw new Error("The private Bombadil executable snapshot must be owner-read-execute only"); + } + if (attestation.linkCount !== 1n) { + throw new Error("The private Bombadil executable snapshot must have exactly one hard link"); + } + const directory = await openPrivateBombadilSnapshotDirectory(cleanupSnapshot); + let directoryIdentity; + try { + await directory.chmod(320); + const descriptor = await directory.stat({ bigint: true }); + const lexical = await lstat(options.directoryPath, { bigint: true }); + if (!sameBombadilSnapshotDirectoryIdentity(descriptor, initialDirectoryIdentity, 0o500n) || !sameBombadilSnapshotDirectoryIdentity(lexical, initialDirectoryIdentity, 0o500n)) { + throw new Error("The private Bombadil executable snapshot directory is not sealed"); + } + directoryIdentity = Object.freeze({ + device: descriptor.dev, + inode: descriptor.ino, + mode: descriptor.mode + }); + } finally { + await directory.close(); + } + return Object.freeze({ + attestation, + directoryIdentity, + directoryPath: options.directoryPath, + executablePath, + sourceAttestation + }); + } catch (error) { + if (cleanupSnapshot !== null) { + try { + await removePrivateBombadilExecutableSnapshot(cleanupSnapshot); + } catch (cleanupError) { + throw new BombadilPersistenceError("Bombadil executable snapshot materialization and cleanup both failed", [error, cleanupError]); + } + } else if (initialDirectoryIdentity !== null) { + try { + await sealPartialPrivateBombadilSnapshotForRetention({ + directoryIdentity: initialDirectoryIdentity, + directoryPath: options.directoryPath + }); + } catch (retentionError) { + throw new BombadilPersistenceError("Bombadil snapshot materialization failed and its exact partial evidence could not be sealed", [error, retentionError]); + } + throw new BombadilPersistenceError("Bombadil snapshot materialization failed; its exact partial snapshot was retained as protected persistence evidence", [error]); + } + throw error; + } +} +function validateBombadilExecutableAttestation(attestation, toolchain, observedVersion) { + assertBombadilExecutableAttestationMatchesToolchain(attestation, toolchain); + return Object.freeze(toolchain === null ? { + buildContract: null, + kind: "npm-package", + sha256: attestation.sha256, + sourceRevision: null, + version: observedVersion + } : { + buildContract: toolchain.buildContract, + kind: "reviewed-override", + sha256: attestation.sha256, + sourceRevision: toolchain.sourceRevision, + version: observedVersion + }); +} +function assertBombadilExecutableAttestationMatchesToolchain(attestation, toolchain) { + if (toolchain !== null && attestation.sha256 !== toolchain.sha256) { + throw new Error("The root Bombadil executable SHA-256 does not match bombadilToolchain.sha256"); + } +} +function assertSameBombadilExecutableAttestation(before, after) { + if (before.device !== after.device || before.inode !== after.inode || before.linkCount !== after.linkCount || before.mode !== after.mode || before.size !== after.size || before.sha256 !== after.sha256) { + throw new Error("The root Bombadil executable changed before native process startup"); + } +} async function requireDirectory(path, label) { let metadata; try { @@ -4661,19 +5148,20 @@ async function resolveDirectBombadilRealPaths(config, replayPath) { if (resolvedReplayPath !== null && !resolvedReplayPath.endsWith(".jsonl")) { throw new Error("--replay must resolve to a .jsonl trace inside repositoryRoot"); } + const bombadilExecutable = config.bombadilToolchain === null ? bombadilNativeBinary(repositoryRoot) : await resolveBombadilExecutablePath(config.bombadilToolchain.executablePath, repositoryRoot); return { config: { ...config, repositoryRoot, specificationPath, artifactRoot: join2(repositoryRoot, "artifacts", "direct-bombadil", config.artifactName), - bombadilExecutable: bombadilNativeBinary(repositoryRoot), + bombadilExecutable, server: { ...config.server, cwd: serverCwd } }, replayPath: resolvedReplayPath }; } -async function readExactBombadilVersion(repositoryRoot) { +async function readExactBombadilVersion(repositoryRoot, toolchain, executablePath, expectedExecutableAttestation) { const packagePath = join2(repositoryRoot, "node_modules", "@antithesishq", "bombadil", "package.json"); let input; try { @@ -4684,6 +5172,9 @@ async function readExactBombadilVersion(repositoryRoot) { if (typeof input !== "object" || input === null || Array.isArray(input) || Reflect.get(input, "version") !== EXPECTED_BOMBADIL_VERSION) { throw new Error(`The root Bombadil package must be exactly ${EXPECTED_BOMBADIL_VERSION}`); } + if (toolchain !== null) { + return await readExactBombadilExecutableVersion(executablePath, repositoryRoot, expectedExecutableAttestation); + } return EXPECTED_BOMBADIL_VERSION; } function helpText(defaultBaseUrl) { @@ -4805,6 +5296,10 @@ async function runDirectBombadilFuzzMatrix(campaignsInput, input = process2.argv throw new Error("Bombadil matrices support public-summary uploads only"); } campaigns = validateCampaignMatrix(campaignsInput); + const toolchainIdentities = new Set(campaigns.map((campaign) => bombadilToolchainIdentity(validateBombadilToolchainConfig(campaign.config.bombadilToolchain, resolve(campaign.config.repositoryRoot))))); + if (toolchainIdentities.size !== 1) { + throw new Error("Every Bombadil matrix campaign must use the same toolchain identity"); + } parsed = parseMatrixCampaignArgument(normalizedOptions.arguments); selected = parsed.campaignId === null ? campaigns : campaigns.filter((campaign) => campaign.id === parsed.campaignId); if (selected.length === 0) { @@ -5116,6 +5611,11 @@ async function runDirectBombadilFuzzInternal(config, input = process2.argv.slice }; const serverCommand = validated.server.command.map((argument) => argument === "{port}" ? validated.port : argument); let bombadilVersion = null; + let bombadilExecutableAttestation = null; + let bombadilRuntimeExecutableAttestation = null; + let bombadilRuntimeExecutable = validated.bombadilExecutable; + let bombadilExecutableSnapshot = null; + let bombadilToolchainEvidence = null; let lease = null; let processResult = null; let attestation = null; @@ -5131,8 +5631,21 @@ async function runDirectBombadilFuzzInternal(config, input = process2.argv.slice let writersSettled = true; { try { - await requireRegularFile(validated.bombadilExecutable, "The root Bombadil executable"); - bombadilVersion = await readExactBombadilVersion(validated.repositoryRoot); + if (validated.bombadilToolchain === null) { + bombadilExecutableAttestation = await attestBombadilExecutable(validated.bombadilExecutable); + bombadilRuntimeExecutableAttestation = bombadilExecutableAttestation; + } else { + bombadilExecutableSnapshot = await materializePrivateBombadilExecutableSnapshot({ + directoryPath: join2(artifactRun.runDirectory, ".bombadil-toolchain"), + sourcePath: validated.bombadilExecutable + }); + bombadilExecutableAttestation = bombadilExecutableSnapshot.sourceAttestation; + bombadilRuntimeExecutableAttestation = bombadilExecutableSnapshot.attestation; + bombadilRuntimeExecutable = bombadilExecutableSnapshot.executablePath; + } + assertBombadilExecutableAttestationMatchesToolchain(bombadilExecutableAttestation, validated.bombadilToolchain); + bombadilVersion = await dependencies.readBombadilVersion(validated.repositoryRoot, validated.bombadilToolchain, bombadilRuntimeExecutable, bombadilRuntimeExecutableAttestation); + bombadilToolchainEvidence = validateBombadilExecutableAttestation(bombadilExecutableAttestation, validated.bombadilToolchain, bombadilVersion); throwIfBombadilRunAborted(abortController.signal); try { lease = await dependencies.acquireServer({ @@ -5149,7 +5662,7 @@ async function runDirectBombadilFuzzInternal(config, input = process2.argv.slice cwd: validated.server.cwd, detachedProcessGroup: true, ...validated.server.env === undefined ? {} : { env: validated.server.env }, - omitEnvironment: [ARTIFACT_COORDINATION_ENVIRONMENT] + omitEnvironment: [...BOMBADIL_PRIVATE_ENVIRONMENT_NAMES] }); terminateAbortedOwnedServer(abortController.signal, ownedServer); return ownedServer; @@ -5168,7 +5681,18 @@ async function runDirectBombadilFuzzInternal(config, input = process2.argv.slice } let processFailure = null; try { - processResult = await dependencies.runBombadil(abortableInvocation); + const spawnAttestation = await attestBombadilExecutable(bombadilRuntimeExecutable); + if (bombadilExecutableAttestation === null || bombadilRuntimeExecutableAttestation === null) { + throw new Error("The root Bombadil executable was not attested before server startup"); + } + assertSameBombadilExecutableAttestation(bombadilRuntimeExecutableAttestation, spawnAttestation); + bombadilToolchainEvidence = validateBombadilExecutableAttestation(bombadilExecutableAttestation, validated.bombadilToolchain, bombadilVersion); + const attestedInvocation = { + ...abortableInvocation, + command: [bombadilRuntimeExecutable, ...abortableInvocation.command.slice(1)], + [BOMBADIL_EXECUTABLE_ATTESTATION]: spawnAttestation + }; + processResult = await dependencies.runBombadil(attestedInvocation); } catch (error) { processFailure = error; } @@ -5209,6 +5733,17 @@ async function runDirectBombadilFuzzInternal(config, input = process2.argv.slice failure ??= error; } } + if (bombadilExecutableSnapshot !== null && writersSettled) { + try { + await removePrivateBombadilExecutableSnapshot(bombadilExecutableSnapshot); + bombadilExecutableSnapshot = null; + } catch (error) { + const persistence = new BombadilPersistenceError("Bombadil private executable snapshot could not be removed", [error]); + failure = failure === null ? persistence : combinePersistenceFailure(failure, persistence, "Bombadil private executable snapshot could not be removed"); + } + } else if (bombadilExecutableSnapshot !== null) { + failure = retainPrivateBombadilSnapshotAfterWriterSettlementFailure(failure ?? new Error("writer settlement unavailable")); + } if (writersSettled) { try { try { @@ -5327,6 +5862,7 @@ async function runDirectBombadilFuzzInternal(config, input = process2.argv.slice bombadil: { version: bombadilVersion, executable: validated.bombadilExecutable, + toolchain: bombadilToolchainEvidence, exitCode: processResult?.exitCode ?? null, termination: processResult?.termination ?? null, outputPath, diff --git a/docs/adoption.md b/docs/adoption.md index c1f17f4..63d3a16 100644 --- a/docs/adoption.md +++ b/docs/adoption.md @@ -3,8 +3,8 @@ Add Direct after identifying the product behavior and external boundary under review. Do not begin by designing fixtures around a provider SDK. The repository carries one `$direct` Agent Skill under `skills/direct`. -Install it with `npx skills add hraness/direct#v0.7.18` or -`bunx skills add hraness/direct#v0.7.18`, or copy that directory into the runner's +Install it with `npx skills add hraness/direct#v0.7.19` or +`bunx skills add hraness/direct#v0.7.19`, or copy that directory into the runner's discovery location. Invoke `$direct` for installation, adoption, and verification work. Skill installation is independent from library package installation and does not activate Direct in a product. diff --git a/docs/publishing.md b/docs/publishing.md index 6209d86..a479a3e 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -62,8 +62,8 @@ cookie, one-time password, recovery code, or write token to GitHub. `v`. ```sh - git tag v0.7.18 - git push origin refs/tags/v0.7.18 + git tag v0.7.19 + git push origin refs/tags/v0.7.19 ``` 3. Wait for **Release**. The workflow runs these boundaries in order: @@ -123,7 +123,7 @@ Merge the fix to `main`, then dispatch **Release** from current `main` with the exact existing stable tag: ```sh -gh workflow run release.yml --ref main -f tag=v0.7.18 +gh workflow run release.yml --ref main -f tag=v0.7.19 ``` The recovery path skips npm publication. It accepts only the newest stable diff --git a/docs/verification.md b/docs/verification.md index 931ee08..475671d 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -10,8 +10,8 @@ definition with `parseDefinitionCoverageSnapshot` so a valid but stale catalog cannot be mistaken for the catalog under review. The repository carries one `$direct` Agent Skill under `skills/direct`. -Install it with `npx skills add hraness/direct#v0.7.18` or -`bunx skills add hraness/direct#v0.7.18`, or copy that directory into the runner's +Install it with `npx skills add hraness/direct#v0.7.19` or +`bunx skills add hraness/direct#v0.7.19`, or copy that directory into the runner's discovery location. Invoke `$direct` for the workflow below. The skill is independent from library package installation and structures the audit; it does not turn deterministic evidence into proof of a substituted live system. @@ -492,6 +492,48 @@ await runDirectBombadilFuzz(config, runId === undefined }); ``` +The package-owned native executable is the default. To exercise a reviewed +native fix before a new Bombadil package exists, place the executable in an +ignored, reproducible directory inside the consumer repository and bind its +complete identity explicitly: + +```ts +const config = { + // The remaining campaign fields stay unchanged. + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath: resolve( + repositoryRoot, + "artifacts/direct-bombadil-toolchain//bombadil", + ), + sha256: "<64 lowercase hexadecimal characters>", + sourceRevision: "<40 lowercase hexadecimal characters>", + version: "0.7.2", + }, +} as const; +``` + +`buildContract` accepts `cargo-release-browser-only` for Bombadil's reviewed +release-browser Cargo build or `nix-default-aarch64-darwin` for its reviewed +Apple-silicon Nix default build. This field records reviewed build provenance; +it does not make a native executable compatible with a different host. The +executable path must be absolute, normalized, repository-confined, regular, +non-symlinked, executable, and no larger than 64 MiB. Direct opens and hashes +that file while binding +its lexical and descriptor identities, copies the verified open-file bytes into +a task-owned private read/execute-only snapshot, and attests the snapshot's +distinct identity, digest, size, and mode. Both the bounded version probe and +native campaign execute that sealed snapshot, so a later replacement of the +configured source path cannot change the executed bytes. The snapshot is +removed after the owned process boundary settles and is never public evidence. +Every campaign in one matrix must use the same package default or the same +five-field override. The exact Bombadil npm package remains required because +the browser specification imports its TypeScript surface. Toolchain-related +environment variables are removed from the server and native child +environments. Raw local run evidence records the version, digest, source +revision, and build contract; sanitized public receipts do not carry the local +path or override identity. + `baseUrl` must be an HTTP root origin on `127.0.0.1` or `localhost` with an explicit port. `entryPath` locates the Direct page while `expectedRoute` states the semantic product route published by the active manifest. The diff --git a/package.json b/package.json index e3770f1..c60ddf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hraness/direct", - "version": "0.7.18", + "version": "0.7.19", "description": "A TypeScript harness for deterministic frontend testing and development with repeatable scenarios, local fixtures, and browser verification for coding agents.", "license": "MIT", "type": "module", diff --git a/scripts/npm-publish-workflow.test.ts b/scripts/npm-publish-workflow.test.ts index 2980bf1..46d7993 100644 --- a/scripts/npm-publish-workflow.test.ts +++ b/scripts/npm-publish-workflow.test.ts @@ -88,6 +88,10 @@ const historicalRecoverySources = [ expectedUnpackedBytes: 1_042_886, version: "0.7.14", }, + { + commit: "965fddb5d6ab9b05bc66d4c7df93770de2f427f6", + version: "0.7.18", + }, ] as const; function workflowStepScript(workflow: string, name: string): string { @@ -341,7 +345,7 @@ import { isUtf8ByteLengthAtMost } from "./utf8-byte-boundary.js"; readonly version?: unknown; }; expect(manifest).toEqual(expect.objectContaining({ - version: "0.7.18", + version: "0.7.19", description: "A TypeScript harness for deterministic frontend testing and development with repeatable scenarios, local fixtures, and browser verification for coding agents.", keywords: [ "frontend-development", diff --git a/scripts/package-smoke.ts b/scripts/package-smoke.ts index dfa25e5..f98bb3d 100644 --- a/scripts/package-smoke.ts +++ b/scripts/package-smoke.ts @@ -336,8 +336,26 @@ function selectBombadilFeatureProfile(version: string): BombadilFeatureProfile { return "baseline"; } -function bombadilToolingTypeChecks(profile: BombadilFeatureProfile): string { +function bombadilToolingTypeChecks( + profile: BombadilFeatureProfile, + supportsBombadilToolchainOverride: boolean, +): string { if (profile === "artifact-delivery") { + const toolchainChecks = supportsBombadilToolchainOverride + ? ` + type BombadilToolchainConfig = import("@hraness/direct/tooling/bombadil").DirectBombadilToolchainConfig; + type BombadilConfigToolchain = NonNullable[0]["bombadilToolchain"]>; + const supportedBombadilToolchain: BombadilToolchainConfig & BombadilConfigToolchain = { + buildContract: "cargo-release-browser-only", + executablePath: "/absolute/repository/artifacts/toolchain/bombadil", + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + sourceRevision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + version: "0.7.2", + };` + : ""; + const toolchainReference = supportsBombadilToolchainOverride + ? "supportedBombadilToolchain, " + : ""; return ` type BombadilRunnerArity = Parameters["length"]; type BombadilRunnerInput = Parameters[1]; @@ -353,6 +371,7 @@ function bombadilToolingTypeChecks(profile: BombadilFeatureProfile): string { }, }; const supportedBombadilTupleInput: BombadilRunnerInput = supportedBombadilArguments; + ${toolchainChecks} const supportedBombadilMatrixInput: BombadilMatrixInput = { arguments: supportedBombadilArguments, artifactRun: { @@ -371,7 +390,7 @@ function bombadilToolingTypeChecks(profile: BombadilFeatureProfile): string { }; // @ts-expect-error Public tooling does not expose dependency injection. const unsupportedBombadilRunnerArity: BombadilRunnerArity = 3; - void [supportedBombadilMatrixInput, supportedBombadilRunnerArities, supportedBombadilRunnerInput, supportedBombadilTupleInput, unsupportedBombadilRunnerArity, unsupportedPrivateBombadilMatrixInput]; + void [supportedBombadilMatrixInput, supportedBombadilRunnerArities, supportedBombadilRunnerInput, ${toolchainReference}supportedBombadilTupleInput, unsupportedBombadilRunnerArity, unsupportedPrivateBombadilMatrixInput]; `; } const matrixChecks = profile === "matrix" @@ -566,6 +585,10 @@ const supportsNamedLayoutContracts = Bun.semver.order( packageManifest.version, "0.7.18", ) >= 0; +const supportsBombadilToolchainOverride = Bun.semver.order( + packageManifest.version, + "0.7.19", +) >= 0; const work = await mkdtemp(join(tmpdir(), "hraness-package-smoke-")); try { const packageInput = parsePackageInput(process.argv.slice(2), repository); @@ -616,7 +639,10 @@ try { await writeFile(join(consumer, "runtime-index.ts"), typeImportSource(runtimeImportSpecifiers)); await writeFile( join(consumer, "tooling-index.ts"), - `${typeImportSource(toolingTypeImportSpecifiers)}${bombadilToolingTypeChecks(bombadilFeatureProfile)}`, + `${typeImportSource(toolingTypeImportSpecifiers)}${bombadilToolingTypeChecks( + bombadilFeatureProfile, + supportsBombadilToolchainOverride, + )}`, ); await writeFile(join(consumer, "tsconfig.bundler.json"), typeScriptConfig({ include: "runtime-index.ts", diff --git a/skills/direct/references/install.md b/skills/direct/references/install.md index a886c33..b961994 100644 --- a/skills/direct/references/install.md +++ b/skills/direct/references/install.md @@ -21,9 +21,9 @@ global `direct` CLI. For a new installation, pin the reviewed public release: ```sh -bun add --dev @hraness/direct@0.7.18 +bun add --dev @hraness/direct@0.7.19 # or, in an npm project -npm install --save-dev @hraness/direct@0.7.18 +npm install --save-dev @hraness/direct@0.7.19 ``` The equivalent manifest entry is: @@ -31,7 +31,7 @@ The equivalent manifest entry is: ```json { "devDependencies": { - "@hraness/direct": "0.7.18" + "@hraness/direct": "0.7.19" } } ``` @@ -43,8 +43,8 @@ If the task starts with skill installation rather than a loaded skill, install the single repository skill with either command: ```sh -npx skills add hraness/direct#v0.7.18 -bunx skills add hraness/direct#v0.7.18 +npx skills add hraness/direct#v0.7.19 +bunx skills add hraness/direct#v0.7.19 ``` Restart or reload the agent runner if it does not discover newly installed diff --git a/src/exports.test.ts b/src/exports.test.ts index 4953eed..3587a29 100644 --- a/src/exports.test.ts +++ b/src/exports.test.ts @@ -12,6 +12,7 @@ import type { DirectBombadilFuzzResult, DirectBombadilFuzzRunInput, DirectBombadilMatrixRunInput, + DirectBombadilToolchainConfig, } from "@hraness/direct/tooling/bombadil"; import * as bundleBoundary from "@hraness/direct/tooling/bundle-boundary"; import * as web from "@hraness/direct/web"; @@ -116,6 +117,13 @@ describe("public package exports", () => { }, }; const supportedArgumentTuple = ["--time-limit=12s"] as const; + const supportedToolchain: DirectBombadilToolchainConfig = { + buildContract: "cargo-release-browser-only", + executablePath: "/absolute/repository/artifacts/toolchain/bombadil", + sha256: "a".repeat(64), + sourceRevision: "b".repeat(40), + version: "0.7.2", + }; const supportedTupleInput: DirectBombadilFuzzRunInput = supportedArgumentTuple; const legacyRunResult: DirectBombadilFuzzResult = { artifactDirectory: "/absolute/repository/artifacts/direct-bombadil/package/run", @@ -151,6 +159,7 @@ describe("public package exports", () => { legacyMatrixResult, supportedRunOptions, supportedTupleInput, + supportedToolchain, unsupportedPrivateMatrixOptions, unsupportedRunnerArity, ]; diff --git a/src/tooling/bombadil-runner.test.ts b/src/tooling/bombadil-runner.test.ts index 34aeb78..a03127b 100644 --- a/src/tooling/bombadil-runner.test.ts +++ b/src/tooling/bombadil-runner.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { defineDirect } from "@hraness/direct"; import { createDirectSession } from "@hraness/direct/testing"; import { getEventListeners } from "node:events"; +import { createHash } from "node:crypto"; import { chmod, link, @@ -14,6 +15,7 @@ import { rm, stat, symlink, + truncate, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -30,6 +32,7 @@ import { parseDirectBombadilMatrixReceipt, parseDirectBombadilMatrixSummary, parseDirectBombadilSanitizedRunSummary, + readExactBombadilExecutableVersionForTest, requireMatchingLiveChromeDownloadIdentityForTest, resolveDirectBombadilUploadLeaf, runBombadilNativeProcess, @@ -108,6 +111,7 @@ async function fixture(): Promise<{ await mkdir(join(packageRoot, "binaries"), { recursive: true }); await writeFile(specificationPath, "export const specification = true;\n"); await writeFile(binaryPath, "fixture native executable\n"); + await chmod(binaryPath, 0o755); await writeFile(join(packageRoot, "package.json"), JSON.stringify({ version: "0.7.2" })); return { @@ -478,7 +482,10 @@ function controllableSignals(): { } function dependencies(options: { + readonly afterAcquire?: () => Promise | void; readonly afterTrace?: (invocation: DirectBombadilInvocation) => Promise; + readonly expectedBombadilExecutable?: string; + readonly expectedBombadilExecutableSha256?: string; readonly exitCode?: number; readonly failAcquire?: boolean; readonly noTrace?: boolean; @@ -492,10 +499,12 @@ function dependencies(options: { readonly calls: string[]; readonly overrides: Partial; readonly serverCommands: string[][]; + readonly serverOmittedEnvironment: string[][]; } { const calls: string[] = []; const signals = controllableSignals(); const serverCommands: string[][] = []; + const serverOmittedEnvironment: string[][] = []; const server = fakeServer( calls, options.neverServerOutput === true @@ -509,27 +518,44 @@ function dependencies(options: { return { calls, serverCommands, + serverOmittedEnvironment, overrides: { now: () => dates.shift() ?? new Date("2026-08-26T12:00:01.250Z"), + readBombadilVersion: async () => "0.7.2", spawnServer: (serverOptions) => { calls.push("spawn-server"); serverCommands.push([...serverOptions.command]); + serverOmittedEnvironment.push([...(serverOptions.omitEnvironment ?? [])]); return server; }, - acquireServer: (acquireOptions): Promise => { + acquireServer: async (acquireOptions): Promise => { calls.push("acquire-server"); const started = acquireOptions.startServer(); if (options.failAcquire === true) { - return Promise.reject(new Error("listener ownership unknown")); + throw new Error("listener ownership unknown"); } - return Promise.resolve({ source: "started", server: started }); + await options.afterAcquire?.(); + return { source: "started", server: started }; }, runBombadil: (invocation) => { calls.push("run-bombadil"); - expect(invocation.command[0]).toEndWith( - `node_modules/@antithesishq/bombadil/binaries/${nativeBinaryName()}`, - ); + if (options.expectedBombadilExecutable === undefined) { + expect(invocation.command[0]).toEndWith( + `node_modules/@antithesishq/bombadil/binaries/${nativeBinaryName()}`, + ); + } else { + expect(invocation.command[0]).not.toBe(options.expectedBombadilExecutable); + expect(invocation.command[0]).toEndWith("/.bombadil-toolchain/bombadil"); + } return (async () => { + if (options.expectedBombadilExecutableSha256 !== undefined) { + const runtimeExecutable = invocation.command[0]; + if (runtimeExecutable === undefined) throw new Error("Expected Bombadil executable argv"); + expect(createHash("sha256").update(await readFile(runtimeExecutable)).digest("hex")) + .toBe(options.expectedBombadilExecutableSha256); + expect((await stat(runtimeExecutable)).mode & 0o777).toBe(0o500); + expect((await stat(dirname(runtimeExecutable))).mode & 0o777).toBe(0o500); + } if (options.noTrace === true) { await mkdir(invocation.outputPath, { recursive: true }); } else { @@ -651,6 +677,79 @@ describe("Direct Bombadil configuration and invocation", () => { ); }); + test("accepts one exact repository-confined reviewed native toolchain", async () => { + const { config, repositoryRoot } = await fixture(); + const executablePath = join(repositoryRoot, "artifacts", "toolchain", "bombadil"); + const bytes = Buffer.from("reviewed native executable\n", "utf8"); + await mkdir(dirname(executablePath), { recursive: true }); + await writeFile(executablePath, bytes); + await chmod(executablePath, 0o755); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + const bombadilToolchain = { + buildContract: "cargo-release-browser-only", + executablePath, + sha256, + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + } as const; + const validated = validateDirectBombadilFuzzConfig({ + ...config, + bombadilToolchain, + }); + expect(validated.bombadilExecutable).toBe(executablePath); + expect(validated.bombadilToolchain).toEqual(bombadilToolchain); + expect(Object.isFrozen(validated.bombadilToolchain)).toBeTrue(); + }); + + test("rejects partial, foreign, malformed, and escaping native toolchain identities", async () => { + const { config, repositoryRoot } = await fixture(); + const executablePath = join(repositoryRoot, "artifacts", "toolchain", "bombadil"); + const valid = { + buildContract: "nix-default-aarch64-darwin", + executablePath, + sha256: "a".repeat(64), + sourceRevision: "b".repeat(40), + version: "0.7.2", + } as const; + expect(() => validateDirectBombadilFuzzConfig({ + ...config, + bombadilToolchain: { ...valid, sha256: "A".repeat(64) }, + })).toThrow("64 lowercase hexadecimal"); + expect(() => validateDirectBombadilFuzzConfig({ + ...config, + bombadilToolchain: { ...valid, sourceRevision: "short" }, + })).toThrow("40 lowercase hexadecimal"); + expect(() => validateDirectBombadilFuzzConfig({ + ...config, + bombadilToolchain: { ...valid, executablePath: join(repositoryRoot, "..", "bombadil") }, + })).toThrow("inside repositoryRoot"); + expect(() => validateDirectBombadilFuzzConfig({ + ...config, + bombadilToolchain: { ...valid, version: "0.7.3" as "0.7.2" }, + })).toThrow("exactly 0.7.2"); + expect(() => validateDirectBombadilFuzzConfig({ + ...config, + bombadilToolchain: { + ...valid, + buildContract: "unreviewed" as "cargo-release-browser-only", + }, + })).toThrow("buildContract is unsupported"); + expect(() => validateDirectBombadilFuzzConfig({ + ...config, + bombadilToolchain: { ...valid, extra: "unreviewed" } as never, + })).toThrow("must contain exactly"); + const partial = { + buildContract: valid.buildContract, + executablePath: valid.executablePath, + sourceRevision: valid.sourceRevision, + version: valid.version, + }; + expect(() => validateDirectBombadilFuzzConfig({ + ...config, + bombadilToolchain: partial as never, + })).toThrow("must contain exactly"); + }); + test("orders query and policy artifacts by explicit code units", async () => { const { config } = await fixture(); const validated = validateDirectBombadilFuzzConfig({ @@ -1004,6 +1103,37 @@ describe("Direct Bombadil campaign matrix", () => { }); }); + test("rejects mixed default and reviewed toolchain identities before any campaign runs", async () => { + const { config, repositoryRoot } = await fixture(); + const executablePath = join(repositoryRoot, "reviewed-bombadil"); + const bytes = Buffer.from("reviewed matrix executable\n", "utf8"); + await writeFile(executablePath, bytes); + await chmod(executablePath, 0o755); + const runtime = dependencies({ expectedBombadilExecutable: executablePath }); + const error = await rejection(runDirectBombadilFuzzMatrix([{ + id: "package-default", + config, + }, { + id: "reviewed-override", + config: { + ...config, + artifactName: "fixture-override", + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath, + sha256: createHash("sha256").update(bytes).digest("hex"), + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + }, + }, + }], { + arguments: ["--campaign=package-default"], + artifactRun: artifactRunPlan(repositoryRoot, 41), + }, runtime.overrides)); + expect(error.message).toContain("same toolchain identity"); + expect(runtime.calls).toEqual([]); + }); + test("rejects ambiguous replay, duplicate IDs, and unknown selection", async () => { const { config } = await fixture(); const campaigns = [{ id: "primary", config }] as const; @@ -2295,6 +2425,322 @@ describe("Direct Bombadil exploration summary", () => { }); describe("Direct Bombadil process lifecycle", () => { + test("accepts only an exact bounded reviewed executable version and settles descendants", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-version-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + const processGroupPath = join(directory, "process-group"); + await writeFile(executablePath, [ + "#!/bin/sh", + `[ -z "$DIRECT_BOMBADIL_RUN_ID" ] || exit 91`, + `[ -z "$DIRECT_BOMBADIL_EXECUTABLE_PATH" ] || exit 92`, + `printf '%s' "$$" > ${JSON.stringify(processGroupPath)}`, + "(trap '' TERM; while :; do sleep 1; done) >/dev/null 2>&1 &", + "printf 'bombadil 0.7.2\\n'", + ].join("\n")); + await chmod(executablePath, 0o755); + const previousRunId = process.env.DIRECT_BOMBADIL_RUN_ID; + const previousExecutablePath = process.env.DIRECT_BOMBADIL_EXECUTABLE_PATH; + process.env.DIRECT_BOMBADIL_RUN_ID = "private-run-id"; + process.env.DIRECT_BOMBADIL_EXECUTABLE_PATH = "private-executable"; + try { + expect(await readExactBombadilExecutableVersionForTest( + executablePath, + directory, + )).toBe("0.7.2"); + } finally { + if (previousRunId === undefined) Reflect.deleteProperty(process.env, "DIRECT_BOMBADIL_RUN_ID"); + else process.env.DIRECT_BOMBADIL_RUN_ID = previousRunId; + if (previousExecutablePath === undefined) { + Reflect.deleteProperty(process.env, "DIRECT_BOMBADIL_EXECUTABLE_PATH"); + } else { + process.env.DIRECT_BOMBADIL_EXECUTABLE_PATH = previousExecutablePath; + } + } + await waitForMissingProcessGroup(Number(await readFile(processGroupPath, "utf8"))); + }); + + test("runs the version probe from reviewed snapshot bytes after source replacement", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-version-attestation-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + const replacementPath = join(directory, "replacement"); + const sentinelPath = join(directory, "probed"); + const reviewedBytes = [ + "#!/bin/sh", + `printf reviewed > ${JSON.stringify(sentinelPath)}`, + "printf 'bombadil 0.7.2\\n'", + "", + ].join("\n"); + const replacementBytes = [ + "#!/bin/sh", + `printf unreviewed > ${JSON.stringify(sentinelPath)}`, + "printf 'bombadil 0.7.3\\n'", + "", + ].join("\n"); + await writeFile(executablePath, reviewedBytes); + await writeFile(replacementPath, replacementBytes); + await chmod(executablePath, 0o755); + await chmod(replacementPath, 0o755); + let snapshotPath: string | null = null; + expect(await readExactBombadilExecutableVersionForTest( + executablePath, + directory, + { + afterPrivateSnapshotMaterialized: async (path) => { + snapshotPath = path; + await rename(replacementPath, executablePath); + }, + }, + )).toBe("0.7.2"); + expect(await readFile(sentinelPath, "utf8")).toBe("reviewed"); + expect(await readFile(executablePath, "utf8")).toBe(replacementBytes); + if (snapshotPath === null) throw new Error("Expected a private version snapshot path"); + expect(await Bun.file(snapshotPath).exists()).toBeFalse(); + }); + + test("refuses snapshot cleanup after its directory path is replaced", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-snapshot-directory-swap-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + const retainedDirectory = join(directory, "retained-snapshot"); + const replacementSentinel = join(directory, "replacement-sentinel"); + await writeFile(executablePath, "#!/bin/sh\nprintf 'bombadil 0.7.2\\n'\n"); + await chmod(executablePath, 0o755); + let replacementDirectory: string | null = null; + const error = await rejection(readExactBombadilExecutableVersionForTest( + executablePath, + directory, + { + afterPrivateSnapshotMaterialized: async (snapshotPath) => { + replacementDirectory = dirname(snapshotPath); + await rename(replacementDirectory, retainedDirectory); + await mkdir(replacementDirectory, { mode: 0o700 }); + await writeFile(join(replacementDirectory, "sentinel"), "unrelated replacement\n"); + await chmod(replacementDirectory, 0o711); + await writeFile(replacementSentinel, "replacement installed\n"); + }, + }, + )); + expect(error.message).toContain("snapshot directory identity changed"); + if (replacementDirectory === null) throw new Error("Expected a replacement directory path"); + expect(await readFile(join(replacementDirectory, "sentinel"), "utf8")) + .toBe("unrelated replacement\n"); + expect((await stat(replacementDirectory)).mode & 0o777).toBe(0o711); + expect(await Bun.file(join(retainedDirectory, "bombadil")).exists()).toBeTrue(); + expect((await stat(retainedDirectory)).mode & 0o777).toBe(0o500); + expect(await readFile(replacementSentinel, "utf8")).toBe("replacement installed\n"); + await chmod(retainedDirectory, 0o700); + }); + + test("refuses snapshot cleanup after its executable path is replaced", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-snapshot-file-swap-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + const replacementPath = join(directory, "replacement"); + const executableBytes = "#!/bin/sh\nprintf 'bombadil 0.7.2\\n'\n"; + await writeFile(executablePath, executableBytes); + await writeFile(replacementPath, executableBytes); + await chmod(executablePath, 0o755); + await chmod(replacementPath, 0o500); + let retainedSnapshotPath: string | null = null; + let snapshotPath: string | null = null; + let unrelatedSentinel: string | null = null; + const error = await rejection(readExactBombadilExecutableVersionForTest( + executablePath, + directory, + { + afterPrivateSnapshotMaterialized: async (path) => { + snapshotPath = path; + const snapshotDirectory = dirname(path); + retainedSnapshotPath = join(snapshotDirectory, "reviewed-bombadil"); + unrelatedSentinel = join(snapshotDirectory, "unrelated-sentinel"); + await chmod(snapshotDirectory, 0o700); + await rename(path, retainedSnapshotPath); + await rename(replacementPath, path); + await writeFile(unrelatedSentinel, "preserve me\n"); + await chmod(unrelatedSentinel, 0o400); + await chmod(snapshotDirectory, 0o500); + }, + }, + )); + expect(error.message).toContain("snapshot file identity changed"); + if (snapshotPath === null || retainedSnapshotPath === null || unrelatedSentinel === null) { + throw new Error("Expected retained snapshot evidence paths"); + } + expect(await readFile(snapshotPath, "utf8")).toBe(executableBytes); + expect(await readFile(retainedSnapshotPath, "utf8")).toBe(executableBytes); + expect((await stat(snapshotPath)).ino).not.toBe((await stat(retainedSnapshotPath)).ino); + expect(await readFile(unrelatedSentinel, "utf8")).toBe("preserve me\n"); + expect((await stat(dirname(snapshotPath))).mode & 0o777).toBe(0o500); + await chmod(dirname(snapshotPath), 0o700); + }); + + test("refuses snapshot cleanup when its executable gains another hard link", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-snapshot-hard-link-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + const retainedLink = join(directory, "retained-private-bytes"); + const executableBytes = "#!/bin/sh\nprintf 'bombadil 0.7.2\\n'\n"; + await writeFile(executablePath, executableBytes); + await chmod(executablePath, 0o755); + let snapshotPath: string | null = null; + const error = await rejection(readExactBombadilExecutableVersionForTest( + executablePath, + directory, + { + afterPrivateSnapshotMaterialized: async (path) => { + snapshotPath = path; + await link(path, retainedLink); + }, + }, + )); + expect(error.message).toContain("snapshot file identity changed"); + if (snapshotPath === null) throw new Error("Expected retained snapshot evidence"); + expect(await readFile(snapshotPath, "utf8")).toBe(executableBytes); + expect(await readFile(retainedLink, "utf8")).toBe(executableBytes); + expect((await stat(snapshotPath)).ino).toBe((await stat(retainedLink)).ino); + expect((await stat(snapshotPath)).nlink).toBe(2); + expect((await stat(dirname(snapshotPath))).mode & 0o777).toBe(0o500); + await chmod(dirname(snapshotPath), 0o700); + }); + + test("seals and reports partial snapshot evidence when executable attestation cannot start", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-partial-snapshot-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + await writeFile(executablePath, "non-executable source\n"); + await chmod(executablePath, 0o600); + const error = await rejection(readExactBombadilExecutableVersionForTest( + executablePath, + directory, + )); + expect(error.name).toBe("BombadilPersistenceError"); + expect(error.message).toContain("partial snapshot was retained"); + const retained = (await readdir(directory)).filter((name) => + name.startsWith(".direct-bombadil-version-snapshot-") + ); + expect(retained).toHaveLength(1); + const retainedDirectory = join(directory, retained[0] ?? "missing"); + expect(await readdir(retainedDirectory)).toEqual([]); + expect((await stat(retainedDirectory)).mode & 0o777).toBe(0o500); + await chmod(retainedDirectory, 0o700); + }); + + test("rejects an oversized sparse executable before copying snapshot bytes", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-oversized-snapshot-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + await writeFile(executablePath, "#!/bin/sh\n"); + await truncate(executablePath, 64 * 1024 * 1024 + 1); + await chmod(executablePath, 0o755); + + const error = await rejection(readExactBombadilExecutableVersionForTest( + executablePath, + directory, + )); + expect(error.name).toBe("BombadilPersistenceError"); + expect(error.message).toContain("partial snapshot was retained"); + expect(error.cause).toBeInstanceOf(Error); + expect((error.cause as Error).message) + .toContain("exceeds the 67108864-byte attestation limit"); + + const retained = (await readdir(directory)).filter((name) => + name.startsWith(".direct-bombadil-version-snapshot-") + ); + expect(retained).toHaveLength(1); + const retainedDirectory = join(directory, retained[0] ?? "missing"); + expect(await readdir(retainedDirectory)).toEqual([]); + expect((await stat(retainedDirectory)).mode & 0o777).toBe(0o500); + await chmod(retainedDirectory, 0o700); + }); + + test("rejects wrong, nonzero, oversized, and timed-out executable version probes", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-version-invalid-")); + temporaryDirectories.push(directory); + const cases = [ + { + expected: "must report exactly bombadil 0.7.2", + maximumOutputBytes: undefined, + name: "wrong", + source: "printf 'bombadil 0.7.3\\n'", + timeoutMs: undefined, + }, + { + expected: "exited with status 23", + maximumOutputBytes: undefined, + name: "nonzero", + source: "exit 23", + timeoutMs: undefined, + }, + { + expected: "stdout exceeded 64 bytes", + maximumOutputBytes: 64, + name: "oversized", + source: "i=0; while [ $i -lt 80 ]; do printf x; i=$((i + 1)); done", + timeoutMs: undefined, + }, + { + expected: "exceeded its 50ms wall-clock limit", + maximumOutputBytes: undefined, + name: "timeout", + source: "sleep 5", + timeoutMs: 50, + }, + ] as const; + for (const scenario of cases) { + const executablePath = join(directory, scenario.name); + await writeFile(executablePath, `#!/bin/sh\n${scenario.source}\n`); + await chmod(executablePath, 0o755); + const error = await rejection(readExactBombadilExecutableVersionForTest( + executablePath, + directory, + { + ...(scenario.maximumOutputBytes === undefined + ? {} + : { maximumOutputBytes: scenario.maximumOutputBytes }), + ...(scenario.timeoutMs === undefined ? {} : { timeoutMs: scenario.timeoutMs }), + }, + )); + expect(error.message).toContain(scenario.expected); + } + }); + + test("strips coordination and toolchain environment from the native child", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-environment-")); + temporaryDirectories.push(directory); + const names = [ + "DIRECT_BOMBADIL_RUN_ID", + "DIRECT_BOMBADIL_BUILD_CONTRACT", + "DIRECT_BOMBADIL_EXECUTABLE_PATH", + "DIRECT_BOMBADIL_EXECUTABLE_SHA256", + "DIRECT_BOMBADIL_SOURCE_REVISION", + ] as const; + const previous = new Map(names.map((name) => [name, process.env[name]])); + for (const name of names) process.env[name] = `private-${name}`; + try { + const result = await runBombadilNativeProcess({ + command: [ + process.execPath, + "-e", + `process.stdout.write(JSON.stringify(${JSON.stringify(names)}.map((name) => process.env[name])))`, + ], + cwd: directory, + outputPath: directory, + targetUrl: "http://127.0.0.1:4919/", + wallClockTimeoutMs: 5_000, + }); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout)).toEqual(names.map(() => null)); + } finally { + for (const name of names) { + const value = previous.get(name); + if (value === undefined) Reflect.deleteProperty(process.env, name); + else process.env[name] = value; + } + } + }); + test("permits only an exact Chrome download transient during the live scan", async () => { const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-chrome-transient-")); temporaryDirectories.push(directory); @@ -4108,6 +4554,104 @@ describe("Direct Bombadil process lifecycle", () => { expect(await Bun.file(deferredSentinel).exists()).toBeFalse(); }); + test("does not spawn when cancellation arrives after the final executable attestation", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-final-attestation-abort-")); + temporaryDirectories.push(directory); + const sentinelPath = join(directory, "spawned"); + const controller = new AbortController(); + const result = await runBombadilNativeProcessForTest({ + abortSignal: controller.signal, + command: [ + process.execPath, + "-e", + `require("node:fs").writeFileSync(${JSON.stringify(sentinelPath)}, "spawned");`, + ], + cwd: directory, + outputPath: join(directory, "output"), + targetUrl: "http://127.0.0.1:4919/", + wallClockTimeoutMs: 5_000, + }, { + afterFinalExecutableAttestation: () => { + controller.abort(); + }, + }); + expect(result).toMatchObject({ + exitCode: 137, + stderr: "", + stdout: "", + termination: "aborted", + }); + expect(await Bun.file(sentinelPath).exists()).toBeFalse(); + }); + + test("rejects executable replacement during the artifact baseline before spawn", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-baseline-replacement-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + const replacementPath = join(directory, "replacement"); + const outputPath = join(directory, "output"); + const sentinelPath = join(directory, "spawned"); + await writeFile( + executablePath, + `#!/bin/sh\nprintf spawned > ${JSON.stringify(sentinelPath)}\n`, + ); + await writeFile( + replacementPath, + `#!/bin/sh\nprintf replacement > ${JSON.stringify(sentinelPath)}\n`, + ); + await chmod(executablePath, 0o755); + await chmod(replacementPath, 0o755); + const error = await rejection(runBombadilNativeProcessForTest({ + command: [executablePath], + cwd: directory, + outputPath, + targetUrl: "http://127.0.0.1:4919/", + wallClockTimeoutMs: 5_000, + }, { + beforeArtifactBaselineInspect: async () => { + await rename(replacementPath, executablePath); + }, + })); + expect(error.message).toContain("changed before native process startup"); + expect(await Bun.file(sentinelPath).exists()).toBeFalse(); + }); + + test("runs native work from reviewed snapshot bytes after source replacement", async () => { + const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-native-snapshot-")); + temporaryDirectories.push(directory); + const executablePath = join(directory, "bombadil"); + const replacementPath = join(directory, "replacement"); + const outputPath = join(directory, "output"); + const snapshotDirectory = join(directory, "private-snapshot"); + const sentinelPath = join(directory, "spawned"); + const reviewedBytes = `#!/bin/sh\nprintf reviewed > ${JSON.stringify(sentinelPath)}\n`; + const replacementBytes = `#!/bin/sh\nprintf unreviewed > ${JSON.stringify(sentinelPath)}\n`; + await writeFile(executablePath, reviewedBytes); + await writeFile(replacementPath, replacementBytes); + await chmod(executablePath, 0o755); + await chmod(replacementPath, 0o755); + let snapshotPath: string | null = null; + const result = await runBombadilNativeProcessForTest({ + command: [executablePath], + cwd: directory, + outputPath, + targetUrl: "http://127.0.0.1:4919/", + wallClockTimeoutMs: 5_000, + }, { + afterPrivateSnapshotMaterialized: async (path) => { + snapshotPath = path; + await rename(replacementPath, executablePath); + }, + privateExecutableSnapshotDirectory: snapshotDirectory, + }); + expect(result).toMatchObject({ exitCode: 0, termination: null }); + expect(await readFile(sentinelPath, "utf8")).toBe("reviewed"); + expect(await readFile(executablePath, "utf8")).toBe(replacementBytes); + if (snapshotPath === null) throw new Error("Expected a private native snapshot path"); + expect(await Bun.file(snapshotPath).exists()).toBeFalse(); + expect(await Bun.file(snapshotDirectory).exists()).toBeFalse(); + }); + test("gives an aborted artifact writer no quota-growing TERM grace", async () => { const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-abort-quota-")); temporaryDirectories.push(directory); @@ -4162,6 +4706,324 @@ describe("Direct Bombadil process lifecycle", () => { }); describe("Direct Bombadil run lifecycle", () => { + test("runs a reviewed native toolchain only through a private executable snapshot", async () => { + const { config, repositoryRoot } = await fixture(); + const executablePath = join(repositoryRoot, "artifacts", "toolchain", "bombadil"); + const bytes = Buffer.from([ + "#!/bin/sh", + `[ "$1" = "--version" ] || exit 64`, + "printf 'bombadil 0.7.2\\n'", + "", + ].join("\n"), "utf8"); + await mkdir(dirname(executablePath), { recursive: true }); + await writeFile(executablePath, bytes); + await chmod(executablePath, 0o755); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + const sourceRevision = "2c86560a94788e529da4edb49cfdebb0dfa55bbd"; + const runtime = dependencies({ + expectedBombadilExecutable: executablePath, + expectedBombadilExecutableSha256: sha256, + }); + const overrides = { ...runtime.overrides }; + Reflect.deleteProperty(overrides, "readBombadilVersion"); + const result = await runDirectBombadilFuzz({ + ...config, + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath, + sha256, + sourceRevision, + version: "0.7.2", + }, + }, ["--time-limit=12s"], overrides); + expect(result.kind).toBe("run"); + expect(runtime.calls).toContain("run-bombadil"); + expect(runtime.serverOmittedEnvironment).toEqual([[ + "DIRECT_BOMBADIL_RUN_ID", + "DIRECT_BOMBADIL_BUILD_CONTRACT", + "DIRECT_BOMBADIL_EXECUTABLE_PATH", + "DIRECT_BOMBADIL_EXECUTABLE_SHA256", + "DIRECT_BOMBADIL_SOURCE_REVISION", + ]]); + const manifest = JSON.parse(await readFile( + join(repositoryRoot, "artifacts", "direct-bombadil", "fixture-product", "manifest.json"), + "utf8", + )) as Record; + expect(record(manifest.bombadil, "bombadil").toolchain).toEqual({ + buildContract: "cargo-release-browser-only", + kind: "reviewed-override", + sha256, + sourceRevision, + version: "0.7.2", + }); + if (result.kind !== "run") throw new Error("Expected a Bombadil run result"); + expect(await Bun.file(join(result.artifactDirectory, ".bombadil-toolchain")).exists()) + .toBeFalse(); + const publicPayload = (await Promise.all((await readdir(result.uploadArtifactPath)).map( + async (name) => await readFile(join(result.uploadArtifactPath, name), "utf8"), + ))).join("\n"); + expect(publicPayload).not.toContain(executablePath); + expect(publicPayload).not.toContain(".bombadil-toolchain"); + expect(publicPayload).not.toContain(sourceRevision); + expect(publicPayload).not.toContain(sha256); + }); + + test("retains the exact reviewed snapshot when writer settlement is not proven", async () => { + const { config, repositoryRoot } = await fixture(); + const executablePath = join(repositoryRoot, "artifacts", "toolchain", "bombadil"); + const bytes = Buffer.from("reviewed native executable\n", "utf8"); + await mkdir(dirname(executablePath), { recursive: true }); + await writeFile(executablePath, bytes); + await chmod(executablePath, 0o755); + const plan = artifactRunPlan(repositoryRoot, 44); + const runtime = dependencies({ + expectedBombadilExecutable: executablePath, + expectedBombadilExecutableSha256: createHash("sha256").update(bytes).digest("hex"), + stopFailure: true, + }); + const error = await rejection(runDirectBombadilFuzz({ + ...config, + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath, + sha256: createHash("sha256").update(bytes).digest("hex"), + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + }, + }, { + arguments: [], + artifactRun: plan, + }, { + ...runtime.overrides, + createRunId: () => plan.runId, + })); + expect(error.name).toBe("BombadilWriterSettlementError"); + expect(error.message).toContain("private executable snapshot was retained"); + const snapshotDirectory = join( + repositoryRoot, + "artifacts", + "direct-bombadil", + "fixture-product", + plan.runId, + ".bombadil-toolchain", + ); + const snapshotPath = join(snapshotDirectory, "bombadil"); + expect(await readFile(snapshotPath, "utf8")).toBe(bytes.toString("utf8")); + expect((await stat(snapshotDirectory)).mode & 0o777).toBe(0o500); + expect((await stat(snapshotPath)).mode & 0o777).toBe(0o500); + expect(JSON.parse(await readFile(join( + repositoryRoot, + "artifacts", + "direct-bombadil-upload", + plan.runId, + "receipt.json", + ), "utf8"))).toMatchObject({ + failureCode: "writer-settlement", + status: "failed", + }); + await chmod(snapshotDirectory, 0o700); + }); + + test("classifies refused reviewed-snapshot cleanup as persistence", async () => { + const { config, repositoryRoot } = await fixture(); + const executablePath = join(repositoryRoot, "artifacts", "toolchain", "bombadil"); + const replacementPath = join(repositoryRoot, "artifacts", "toolchain", "replacement"); + const bytes = Buffer.from("reviewed native executable\n", "utf8"); + await mkdir(dirname(executablePath), { recursive: true }); + await writeFile(executablePath, bytes); + await writeFile(replacementPath, bytes); + await chmod(executablePath, 0o755); + await chmod(replacementPath, 0o500); + const plan = artifactRunPlan(repositoryRoot, 45); + const snapshotDirectory = join( + repositoryRoot, + "artifacts", + "direct-bombadil", + "fixture-product", + plan.runId, + ".bombadil-toolchain", + ); + const snapshotPath = join(snapshotDirectory, "bombadil"); + const retainedSnapshotPath = join(snapshotDirectory, "reviewed-bombadil"); + const unrelatedSentinel = join(snapshotDirectory, "unrelated-sentinel"); + const runtime = dependencies({ + afterAcquire: async () => { + await chmod(snapshotDirectory, 0o700); + await rename(snapshotPath, retainedSnapshotPath); + await rename(replacementPath, snapshotPath); + await writeFile(unrelatedSentinel, "preserve me\n"); + await chmod(unrelatedSentinel, 0o400); + await chmod(snapshotDirectory, 0o500); + }, + expectedBombadilExecutable: executablePath, + expectedBombadilExecutableSha256: createHash("sha256").update(bytes).digest("hex"), + }); + const error = await rejection(runDirectBombadilFuzz({ + ...config, + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath, + sha256: createHash("sha256").update(bytes).digest("hex"), + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + }, + }, { + arguments: [], + artifactRun: plan, + }, { + ...runtime.overrides, + createRunId: () => plan.runId, + })); + expect(error.name).toBe("BombadilPersistenceError"); + expect(error.message).toContain("snapshot could not be removed"); + expect(runtime.calls).not.toContain("run-bombadil"); + expect(await readFile(snapshotPath, "utf8")).toBe(bytes.toString("utf8")); + expect(await readFile(retainedSnapshotPath, "utf8")).toBe(bytes.toString("utf8")); + expect((await stat(snapshotPath)).ino).not.toBe((await stat(retainedSnapshotPath)).ino); + expect(await readFile(unrelatedSentinel, "utf8")).toBe("preserve me\n"); + expect((await stat(snapshotDirectory)).mode & 0o777).toBe(0o500); + expect(JSON.parse(await readFile(join( + repositoryRoot, + "artifacts", + "direct-bombadil-upload", + plan.runId, + "receipt.json", + ), "utf8"))).toMatchObject({ + failureCode: "persistence", + status: "failed", + }); + await chmod(snapshotDirectory, 0o700); + }); + + test("rejects a digest-matching override that reports a different version", async () => { + const { config, repositoryRoot } = await fixture(); + const executablePath = join(repositoryRoot, "artifacts", "toolchain", "bombadil"); + const bytes = Buffer.from("#!/bin/sh\nprintf 'bombadil 0.7.3\\n'\n", "utf8"); + await mkdir(dirname(executablePath), { recursive: true }); + await writeFile(executablePath, bytes); + await chmod(executablePath, 0o755); + const runtime = dependencies({ expectedBombadilExecutable: executablePath }); + const overrides = { ...runtime.overrides }; + Reflect.deleteProperty(overrides, "readBombadilVersion"); + const error = await rejection(runDirectBombadilFuzz({ + ...config, + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath, + sha256: createHash("sha256").update(bytes).digest("hex"), + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + }, + }, [], overrides)); + expect(error.message).toContain("must report exactly bombadil 0.7.2"); + expect(runtime.calls).toEqual([]); + }); + + test("keeps the reviewed runtime snapshot after source replacement during server startup", async () => { + const { config, repositoryRoot } = await fixture(); + const executablePath = join(repositoryRoot, "artifacts", "toolchain", "bombadil"); + const replacementPath = join(repositoryRoot, "artifacts", "toolchain", "replacement"); + const bytes = Buffer.from("reviewed native executable\n", "utf8"); + const replacementBytes = Buffer.from("unreviewed replacement executable\n", "utf8"); + await mkdir(dirname(executablePath), { recursive: true }); + await writeFile(executablePath, bytes); + await writeFile(replacementPath, replacementBytes); + await chmod(executablePath, 0o755); + await chmod(replacementPath, 0o755); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + const runtime = dependencies({ + afterAcquire: async () => { + await rename(replacementPath, executablePath); + }, + expectedBombadilExecutable: executablePath, + expectedBombadilExecutableSha256: sha256, + }); + const result = await runDirectBombadilFuzz({ + ...config, + bombadilToolchain: { + buildContract: "nix-default-aarch64-darwin", + executablePath, + sha256, + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + }, + }, [], runtime.overrides); + expect(result.kind).toBe("run"); + if (result.kind !== "run") throw new Error("Expected a Bombadil run result"); + expect(await Bun.file(join(result.artifactDirectory, ".bombadil-toolchain")).exists()) + .toBeFalse(); + expect(await readFile(executablePath)).toEqual(replacementBytes); + expect(runtime.calls).toEqual([ + "acquire-server", + "spawn-server", + "run-bombadil", + "stop-server", + "terminate", + ]); + }); + + test("rejects mismatched, non-executable, and symlinked reviewed binaries before server startup", async () => { + const mismatch = await fixture(); + const mismatchPath = join(mismatch.repositoryRoot, "reviewed-bombadil"); + const mismatchSentinel = join(mismatch.repositoryRoot, "mismatched-probe-ran"); + await writeFile(mismatchPath, [ + "#!/bin/sh", + `printf probed > ${JSON.stringify(mismatchSentinel)}`, + "printf 'bombadil 0.7.2\\n'", + "", + ].join("\n")); + await chmod(mismatchPath, 0o755); + const mismatchRuntime = dependencies({ expectedBombadilExecutable: mismatchPath }); + expect((await rejection(runDirectBombadilFuzz({ + ...mismatch.config, + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath: mismatchPath, + sha256: "0".repeat(64), + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + }, + }, [], mismatchRuntime.overrides))).message).toContain("SHA-256 does not match"); + expect(mismatchRuntime.calls).toEqual([]); + expect(await Bun.file(mismatchSentinel).exists()).toBeFalse(); + + const mode = await fixture(); + const modePath = join(mode.repositoryRoot, "reviewed-bombadil"); + await writeFile(modePath, "binary\n"); + await chmod(modePath, 0o644); + const modeRuntime = dependencies({ expectedBombadilExecutable: modePath }); + expect((await rejection(runDirectBombadilFuzz({ + ...mode.config, + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath: modePath, + sha256: createHash("sha256").update("binary\n").digest("hex"), + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + }, + }, [], modeRuntime.overrides))).message).toContain("executable mode bit"); + expect(modeRuntime.calls).toEqual([]); + + const linked = await fixture(); + const targetPath = join(linked.repositoryRoot, "reviewed-bombadil-target"); + const linkedPath = join(linked.repositoryRoot, "reviewed-bombadil"); + await writeFile(targetPath, "binary\n"); + await chmod(targetPath, 0o755); + await symlink(targetPath, linkedPath); + const linkedRuntime = dependencies({ expectedBombadilExecutable: linkedPath }); + expect((await rejection(runDirectBombadilFuzz({ + ...linked.config, + bombadilToolchain: { + buildContract: "cargo-release-browser-only", + executablePath: linkedPath, + sha256: createHash("sha256").update("binary\n").digest("hex"), + sourceRevision: "2c86560a94788e529da4edb49cfdebb0dfa55bbd", + version: "0.7.2", + }, + }, [], linkedRuntime.overrides))).message).toContain("regular nonsymlink"); + expect(linkedRuntime.calls).toEqual([]); + }); + test("starts and stops the owned server and writes attested passing artifacts", async () => { const { config, repositoryRoot } = await fixture(); const runtime = dependencies(); @@ -4205,6 +5067,15 @@ describe("Direct Bombadil run lifecycle", () => { }); const bombadil = record(manifest.bombadil, "bombadil"); expect(bombadil.version).toBe("0.7.2"); + expect(bombadil.toolchain).toMatchObject({ + buildContract: null, + kind: "npm-package", + sourceRevision: null, + version: "0.7.2", + }); + expect(record(bombadil.toolchain, "bombadil.toolchain").sha256).toMatch( + /^[0-9a-f]{64}$/u, + ); expect(bombadil.termination).toBeNull(); expect(bombadil.logPath).toBeString(); expect(bombadil.rawTracePath).toBeString(); diff --git a/src/tooling/bombadil-runner.ts b/src/tooling/bombadil-runner.ts index c3549cd..9a3e663 100644 --- a/src/tooling/bombadil-runner.ts +++ b/src/tooling/bombadil-runner.ts @@ -8,8 +8,10 @@ import { readFile, realpath, rename, + rmdir, rm, stat, + unlink, } from "node:fs/promises"; import { extname, isAbsolute, join, relative, resolve } from "node:path"; import process from "node:process"; @@ -40,6 +42,23 @@ import { } from "./browser-verification.js"; const EXPECTED_BOMBADIL_VERSION = "0.7.2"; +const BOMBADIL_TOOLCHAIN_BUILD_CONTRACTS = new Set([ + "cargo-release-browser-only", + "nix-default-aarch64-darwin", +]); +const BOMBADIL_TOOLCHAIN_CONFIG_KEYS = [ + "buildContract", + "executablePath", + "sha256", + "sourceRevision", + "version", +] as const; +const BOMBADIL_TOOLCHAIN_ENVIRONMENT_NAMES = [ + "DIRECT_BOMBADIL_BUILD_CONTRACT", + "DIRECT_BOMBADIL_EXECUTABLE_PATH", + "DIRECT_BOMBADIL_EXECUTABLE_SHA256", + "DIRECT_BOMBADIL_SOURCE_REVISION", +] as const; const DEFAULT_TIME_LIMIT_SECONDS = 20; const MIN_TIME_LIMIT_SECONDS = 12; const MAX_TIME_LIMIT_SECONDS = 300; @@ -142,6 +161,7 @@ const MATRIX_SUMMARY_CAMPAIGNS_KEYS = new Set([ "total", ]); const SHA256_PATTERN = /^[0-9a-f]{64}$/u; +const GIT_REVISION_PATTERN = /^[0-9a-f]{40}$/u; const ARTIFACT_EVIDENCE_JSON_LIMITS = Object.freeze({ maxDepth: 8, maxNodes: 2_048, @@ -152,6 +172,10 @@ const ARTIFACT_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u; const MAX_ARTIFACT_IDENTIFIER_LENGTH = 80; const MAX_MATRIX_CAMPAIGNS = 32; const ARTIFACT_COORDINATION_ENVIRONMENT = "DIRECT_BOMBADIL_RUN_ID"; +const BOMBADIL_PRIVATE_ENVIRONMENT_NAMES = new Set([ + ARTIFACT_COORDINATION_ENVIRONMENT, + ...BOMBADIL_TOOLCHAIN_ENVIRONMENT_NAMES, +]); const ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u; const QUERY_PARAMETER_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]*$/u; const PROTOTYPE_PROPERTY_NAMES = new Set(["__proto__", "constructor", "prototype"]); @@ -169,6 +193,9 @@ const RANDOM_RUN_OVERHEAD_MS = 30_000; const REPLAY_WALL_CLOCK_TIMEOUT_MS = MAX_TIME_LIMIT_SECONDS * 1_000 + RANDOM_RUN_OVERHEAD_MS; const PROCESS_TERMINATION_GRACE_MS = 5_000; const MIN_PROCESS_OUTPUT_DRAIN_MS = 500; +const BOMBADIL_VERSION_PROBE_TIMEOUT_MS = 5_000; +const BOMBADIL_VERSION_OUTPUT_LIMIT = 1_024; +const MAX_BOMBADIL_EXECUTABLE_BYTES = 64 * 1024 * 1024; const SERVER_OUTPUT_TIMEOUT_MS = 3_000; const ARTIFACT_MONITOR_INTERVAL_MS = 100; const MAX_LIVE_CHROME_RENAME_RETRIES = 4; @@ -311,6 +338,19 @@ export interface DirectBombadilServerConfig { readonly startupTimeoutMs?: number; } +export type DirectBombadilToolchainBuildContract = + | "cargo-release-browser-only" + | "nix-default-aarch64-darwin"; + +/** Exact repository-confined native Bombadil identity used instead of the npm binary. */ +export interface DirectBombadilToolchainConfig { + readonly buildContract: DirectBombadilToolchainBuildContract; + readonly executablePath: string; + readonly sha256: string; + readonly sourceRevision: string; + readonly version: typeof EXPECTED_BOMBADIL_VERSION; +} + export interface DirectBombadilArtifactPolicy { readonly maxDepth?: number; readonly maxEntries?: number; @@ -529,6 +569,7 @@ export interface DirectBombadilFuzzConfig { readonly artifactPolicy?: DirectBombadilArtifactPolicy; readonly artifactName: string; readonly baseUrl: string; + readonly bombadilToolchain?: DirectBombadilToolchainConfig; readonly entryPath?: `/${string}`; readonly expectedRoute: string; readonly label: string; @@ -604,6 +645,12 @@ export interface DirectBombadilInvocation { readonly wallClockTimeoutMs: number; } +const BOMBADIL_EXECUTABLE_ATTESTATION = Symbol("bombadilExecutableAttestation"); + +type AttestedDirectBombadilInvocation = DirectBombadilInvocation & { + readonly [BOMBADIL_EXECUTABLE_ATTESTATION]?: BombadilExecutableAttestation; +}; + interface BombadilProcessResult { readonly exitCode: number; readonly stderr: string; @@ -651,6 +698,7 @@ export interface DirectBombadilRunnerDependencies { readonly createAbortController?: () => AbortController; readonly createRunId: () => string; readonly now: () => Date; + readonly readBombadilVersion: typeof readExactBombadilVersion; readonly runBombadil: ( invocation: DirectBombadilInvocation, ) => Promise; @@ -686,12 +734,13 @@ interface ProcessSignalController extends ProcessSignalEmitter { type ValidatedConfig = Omit< DirectBombadilFuzzConfig, - "artifactPolicy" | "explorationPolicy" | "server" | "viewport" + "artifactPolicy" | "bombadilToolchain" | "explorationPolicy" | "server" | "viewport" > & { readonly artifactPolicy: ValidatedArtifactPolicy; readonly artifactRoot: string; readonly baseUrl: string; readonly bombadilExecutable: string; + readonly bombadilToolchain: DirectBombadilToolchainConfig | null; readonly entryPath: `/${string}`; readonly explorationPolicy: ValidatedExplorationPolicy | null; readonly port: string; @@ -705,6 +754,37 @@ type ValidatedConfig = Omit< type ValidatedArtifactPolicy = Required; +interface BombadilExecutableAttestation { + readonly device: bigint; + readonly inode: bigint; + readonly linkCount: bigint; + readonly mode: bigint; + readonly sha256: string; + readonly size: number; +} + +interface BombadilExecutableSnapshot { + readonly attestation: BombadilExecutableAttestation; + readonly directoryIdentity: BombadilSnapshotDirectoryIdentity; + readonly directoryPath: string; + readonly executablePath: string; + readonly sourceAttestation: BombadilExecutableAttestation; +} + +interface BombadilSnapshotDirectoryIdentity { + readonly device: bigint; + readonly inode: bigint; + readonly mode: bigint; +} + +interface BombadilToolchainEvidence { + readonly buildContract: DirectBombadilToolchainBuildContract | null; + readonly kind: "npm-package" | "reviewed-override"; + readonly sha256: string; + readonly sourceRevision: string | null; + readonly version: typeof EXPECTED_BOMBADIL_VERSION; +} + interface ArtifactInventoryFile { readonly device: bigint; readonly inode: bigint; @@ -842,6 +922,25 @@ class BombadilPersistenceError extends AggregateError { } } +function retainPrivateBombadilSnapshotAfterWriterSettlementFailure( + failure: unknown, +): BombadilWriterSettlementError { + return new BombadilWriterSettlementError( + "Bombadil writers were not proven absent; the private executable snapshot was retained as protected persistence evidence", + new AggregateError( + [ + failure, + new BombadilPersistenceError( + "Bombadil private executable snapshot removal was suppressed", + [new Error("native process and writer settlement was not proven")], + ), + ], + "Bombadil writer settlement and private snapshot persistence evidence", + { cause: failure }, + ), + ); +} + interface ValidatedViewport { readonly deviceScaleFactor: number; readonly height: number; @@ -912,6 +1011,77 @@ function bombadilNativeBinary(repositoryRoot: string): string { ); } +function validateBombadilToolchainConfig( + value: DirectBombadilToolchainConfig | undefined, + repositoryRoot: string, +): DirectBombadilToolchainConfig | null { + if (value === undefined) return null; + if (!isRecord(value)) { + throw new Error("bombadilToolchain must be an exact object"); + } + const keys = Object.keys(value).sort(compareCodeUnits); + if (JSON.stringify(keys) !== JSON.stringify(BOMBADIL_TOOLCHAIN_CONFIG_KEYS)) { + throw new Error( + `bombadilToolchain must contain exactly ${BOMBADIL_TOOLCHAIN_CONFIG_KEYS.join(", ")}`, + ); + } + const executablePath = Reflect.get(value, "executablePath"); + const sha256 = Reflect.get(value, "sha256"); + const sourceRevision = Reflect.get(value, "sourceRevision"); + const version = Reflect.get(value, "version"); + const buildContract = Reflect.get(value, "buildContract"); + if ( + typeof executablePath !== "string" + || !isAbsolute(executablePath) + || resolve(executablePath) !== executablePath + || !isWithin(repositoryRoot, executablePath) + ) { + throw new Error( + "bombadilToolchain.executablePath must be an absolute normalized path inside repositoryRoot", + ); + } + if (typeof sha256 !== "string" || !SHA256_PATTERN.test(sha256)) { + throw new Error("bombadilToolchain.sha256 must be exactly 64 lowercase hexadecimal characters"); + } + if (typeof sourceRevision !== "string" || !GIT_REVISION_PATTERN.test(sourceRevision)) { + throw new Error( + "bombadilToolchain.sourceRevision must be exactly 40 lowercase hexadecimal characters", + ); + } + if (version !== EXPECTED_BOMBADIL_VERSION) { + throw new Error(`bombadilToolchain.version must be exactly ${EXPECTED_BOMBADIL_VERSION}`); + } + if ( + typeof buildContract !== "string" + || !BOMBADIL_TOOLCHAIN_BUILD_CONTRACTS.has( + buildContract as DirectBombadilToolchainBuildContract, + ) + ) { + throw new Error("bombadilToolchain.buildContract is unsupported"); + } + return Object.freeze({ + buildContract: buildContract as DirectBombadilToolchainBuildContract, + executablePath, + sha256, + sourceRevision, + version: EXPECTED_BOMBADIL_VERSION, + }); +} + +function bombadilToolchainIdentity( + toolchain: DirectBombadilToolchainConfig | null, +): string { + return toolchain === null + ? "npm-package" + : JSON.stringify({ + buildContract: toolchain.buildContract, + executablePath: toolchain.executablePath, + sha256: toolchain.sha256, + sourceRevision: toolchain.sourceRevision, + version: toolchain.version, + }); +} + function requireLocalRootHttpOrigin(value: string): string { const baseUrl = normalizeRootHttpOrigin(value); const url = new URL(baseUrl); @@ -5135,6 +5305,10 @@ export function validateDirectBombadilFuzzConfig( const viewport = validateViewport(config.viewport); const explorationPolicy = validateExplorationPolicy(config.explorationPolicy); const artifactPolicy = validateArtifactPolicy(config.artifactPolicy); + const bombadilToolchain = validateBombadilToolchainConfig( + config.bombadilToolchain, + repositoryRoot, + ); const startupTimeoutMs = config.server.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS; if ( !Number.isSafeInteger(startupTimeoutMs) @@ -5155,7 +5329,9 @@ export function validateDirectBombadilFuzzConfig( specificationPath, baseUrl, artifactRoot: join(repositoryRoot, "artifacts", "direct-bombadil", config.artifactName), - bombadilExecutable: bombadilNativeBinary(repositoryRoot), + bombadilExecutable: bombadilToolchain?.executablePath + ?? bombadilNativeBinary(repositoryRoot), + bombadilToolchain, entryPath, explorationPolicy, port, @@ -5240,6 +5416,36 @@ interface StreamCapture { readonly stop: () => void; } +function bombadilChildEnvironment(): Record { + return Object.fromEntries( + Object.entries({ + ...process.env, + NO_COLOR: "1", + }).filter(([name]) => !BOMBADIL_PRIVATE_ENVIRONMENT_NAMES.has(name)), + ); +} + +async function readBoundedProcessStream( + stream: ReadableStream, + maximumBytes: number, + label: string, +): Promise { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let bytes = 0; + let output = ""; + for (;;) { + const chunk = await reader.read(); + if (chunk.done) return `${output}${decoder.decode()}`; + bytes += chunk.value.byteLength; + if (bytes > maximumBytes) { + void reader.cancel().catch(() => undefined); + throw new Error(`${label} exceeded ${String(maximumBytes)} bytes`); + } + output += decoder.decode(chunk.value, { stream: true }); + } +} + function captureStream( stream: ReadableStream, maximumLength = LOG_LIMIT, @@ -5275,16 +5481,26 @@ function captureStream( return { result, stop: stopCapture }; } -function signalProcessGroup( +async function signalProcessGroup( process_: ReturnType, signal: "SIGKILL", -): void { - try { - process.kill(-process_.pid, signal); - return; - } catch (error) { - if (!isRecord(error) || error.code !== "ESRCH") throw error; - if (process_.exitCode === null) process_.kill(signal); + timeoutMs: number, +): Promise { + const deadline = Date.now() + timeoutMs; + for (;;) { + try { + process.kill(-process_.pid, signal); + return; + } catch (error) { + if (isRecord(error) && error.code === "ESRCH") { + if (process_.exitCode === null) process_.kill(signal); + return; + } + if (!isRecord(error) || error.code !== "EPERM" || Date.now() >= deadline) { + throw error; + } + await Bun.sleep(Math.min(10, Math.max(1, deadline - Date.now()))); + } } } @@ -5333,7 +5549,7 @@ async function settleBombadilProcessGroup(options: { readonly timeoutMs: number; }): Promise { try { - signalProcessGroup(options.process, "SIGKILL"); + await signalProcessGroup(options.process, "SIGKILL", options.timeoutMs); await waitForBombadilLeaderExit(options.process, options.timeoutMs); await waitForProcessGroupExit(options.process.pid, options.timeoutMs); } catch (error) { @@ -5344,6 +5560,109 @@ async function settleBombadilProcessGroup(options: { } } +async function readExactBombadilExecutableVersion( + executablePath: string, + repositoryRoot: string, + expectedAttestation: BombadilExecutableAttestation, + options: Readonly<{ + maximumOutputBytes?: number; + timeoutMs?: number; + }> = {}, +): Promise { + const maximumOutputBytes = options.maximumOutputBytes ?? BOMBADIL_VERSION_OUTPUT_LIMIT; + const timeoutMs = options.timeoutMs ?? BOMBADIL_VERSION_PROBE_TIMEOUT_MS; + const probeAttestation = await attestBombadilExecutable(executablePath); + assertSameBombadilExecutableAttestation(expectedAttestation, probeAttestation); + const process_ = Bun.spawn([executablePath, "--version"], { + cwd: repositoryRoot, + detached: true, + env: bombadilChildEnvironment(), + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }); + const output = Promise.all([ + readBoundedProcessStream(process_.stdout, maximumOutputBytes, "Bombadil --version stdout"), + readBoundedProcessStream(process_.stderr, maximumOutputBytes, "Bombadil --version stderr"), + ]); + let timeout: ReturnType | undefined; + try { + const timeoutPromise = new Promise<{ readonly kind: "timeout" }>((resolveTimeout) => { + timeout = setTimeout(() => resolveTimeout({ kind: "timeout" }), timeoutMs); + }); + const outcome = await Promise.race([ + Promise.all([process_.exited, output]).then(([exitCode, streams]) => ({ + exitCode, + kind: "exited" as const, + stderr: streams[1], + stdout: streams[0], + })), + timeoutPromise, + ]); + if (outcome.kind === "timeout") { + throw new Error(`Bombadil --version exceeded its ${String(timeoutMs)}ms wall-clock limit`); + } + if (outcome.exitCode !== 0) { + throw new Error(`Bombadil --version exited with status ${String(outcome.exitCode)}`); + } + if (outcome.stderr !== "") { + throw new Error("Bombadil --version wrote unexpected stderr"); + } + if (!/^bombadil 0\.7\.2(?:\r?\n)?$/u.test(outcome.stdout)) { + throw new Error(`Bombadil executable must report exactly bombadil ${EXPECTED_BOMBADIL_VERSION}`); + } + return EXPECTED_BOMBADIL_VERSION; + } finally { + if (timeout !== undefined) clearTimeout(timeout); + await settleBombadilProcessGroup({ + process: process_, + timeoutMs: PROCESS_TERMINATION_GRACE_MS, + }); + } +} + +/** @internal Verify one reviewed executable without starting a Direct server. */ +export async function readExactBombadilExecutableVersionForTest( + executablePath: string, + repositoryRoot: string, + options?: Readonly<{ + afterPrivateSnapshotMaterialized?: (snapshotPath: string) => Promise | void; + maximumOutputBytes?: number; + timeoutMs?: number; + }>, +): Promise { + const snapshot = await materializePrivateBombadilExecutableSnapshot({ + directoryPath: join( + repositoryRoot, + `.direct-bombadil-version-snapshot-${randomUUID()}`, + ), + sourcePath: executablePath, + }); + let writersSettled = true; + try { + await options?.afterPrivateSnapshotMaterialized?.(snapshot.executablePath); + return await readExactBombadilExecutableVersion( + snapshot.executablePath, + repositoryRoot, + snapshot.attestation, + { + ...(options?.maximumOutputBytes === undefined + ? {} + : { maximumOutputBytes: options.maximumOutputBytes }), + ...(options?.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), + }, + ); + } catch (error) { + if (error instanceof BombadilWriterSettlementError) { + writersSettled = false; + throw retainPrivateBombadilSnapshotAfterWriterSettlementFailure(error); + } + throw error; + } finally { + if (writersSettled) await removePrivateBombadilExecutableSnapshot(snapshot); + } +} + async function establishCleanBombadilArtifactBaseline(options: { readonly beforeInspect?: () => Promise | void; readonly outputPath: string; @@ -5425,8 +5744,9 @@ function bombadilAbortRequested(signal: AbortSignal | undefined): boolean { } async function runBombadilNativeProcessInternal( - invocation: DirectBombadilInvocation, + invocation: AttestedDirectBombadilInvocation, hooks: Readonly<{ + afterFinalExecutableAttestation?: () => Promise | void; beforeArtifactBaselineInspect?: () => Promise | void; }> = {}, ): Promise { @@ -5434,6 +5754,8 @@ async function runBombadilNativeProcessInternal( if (bombadilAbortRequested(invocation.abortSignal)) { return abortedBombadilProcessResult(); } + const expectedExecutableAttestation = invocation[BOMBADIL_EXECUTABLE_ATTESTATION] + ?? await attestBombadilExecutable(invocation.command[0] ?? ""); await establishCleanBombadilArtifactBaseline({ ...(hooks.beforeArtifactBaselineInspect === undefined ? {} @@ -5444,12 +5766,18 @@ async function runBombadilNativeProcessInternal( if (bombadilAbortRequested(invocation.abortSignal)) { return abortedBombadilProcessResult(); } - const childEnvironment: Record = Object.fromEntries( - Object.entries({ - ...process.env, - NO_COLOR: "1", - }).filter(([name]) => name !== ARTIFACT_COORDINATION_ENVIRONMENT), + const childEnvironment = bombadilChildEnvironment(); + const spawnAttestation = await attestBombadilExecutable(invocation.command[0] ?? ""); + assertSameBombadilExecutableAttestation( + expectedExecutableAttestation, + spawnAttestation, ); + if (hooks.afterFinalExecutableAttestation !== undefined) { + await hooks.afterFinalExecutableAttestation(); + } + if (bombadilAbortRequested(invocation.abortSignal)) { + return abortedBombadilProcessResult(); + } const process_ = Bun.spawn([...invocation.command], { cwd: invocation.cwd, detached: true, @@ -5571,14 +5899,47 @@ export async function runBombadilNativeProcess( return await runBombadilNativeProcessInternal(invocation); } -/** @internal Exercise cancellation while the pre-spawn artifact baseline is pending. */ +/** @internal Exercise cancellation and identity changes at pre-spawn boundaries. */ export async function runBombadilNativeProcessForTest( invocation: DirectBombadilInvocation, hooks: Readonly<{ - beforeArtifactBaselineInspect: () => Promise | void; + afterFinalExecutableAttestation?: () => Promise | void; + afterPrivateSnapshotMaterialized?: (snapshotPath: string) => Promise | void; + beforeArtifactBaselineInspect?: () => Promise | void; + privateExecutableSnapshotDirectory?: string; }>, ): Promise { - return await runBombadilNativeProcessInternal(invocation, hooks); + if (hooks.privateExecutableSnapshotDirectory === undefined) { + return await runBombadilNativeProcessInternal(invocation, hooks); + } + const snapshot = await materializePrivateBombadilExecutableSnapshot({ + directoryPath: hooks.privateExecutableSnapshotDirectory, + sourcePath: invocation.command[0] ?? "", + }); + let writersSettled = true; + try { + await hooks.afterPrivateSnapshotMaterialized?.(snapshot.executablePath); + return await runBombadilNativeProcessInternal({ + ...invocation, + command: [snapshot.executablePath, ...invocation.command.slice(1)], + [BOMBADIL_EXECUTABLE_ATTESTATION]: snapshot.attestation, + }, { + ...(hooks.afterFinalExecutableAttestation === undefined + ? {} + : { afterFinalExecutableAttestation: hooks.afterFinalExecutableAttestation }), + ...(hooks.beforeArtifactBaselineInspect === undefined + ? {} + : { beforeArtifactBaselineInspect: hooks.beforeArtifactBaselineInspect }), + }); + } catch (error) { + if (error instanceof BombadilWriterSettlementError) { + writersSettled = false; + throw retainPrivateBombadilSnapshotAfterWriterSettlementFailure(error); + } + throw error; + } finally { + if (writersSettled) await removePrivateBombadilExecutableSnapshot(snapshot); + } } const processEvents: EventEmitter = process; @@ -5588,6 +5949,7 @@ const defaultDependencies: DirectBombadilRunnerDependencies = { createAbortController: () => new AbortController(), createRunId: randomUUID, now: () => new Date(), + readBombadilVersion: readExactBombadilVersion, runBombadil: runBombadilNativeProcess, signalController: { forward: (signal) => process.kill(process.pid, signal), @@ -5641,6 +6003,513 @@ async function requireRegularFile(path: string, label: string): Promise { if (!metadata.isFile()) throw new Error(`${label} must be a regular file`); } +async function resolveBombadilExecutablePath( + candidate: string, + repositoryRoot: string, +): Promise { + if ( + !isAbsolute(candidate) + || resolve(candidate) !== candidate + || !isWithin(repositoryRoot, candidate) + ) { + throw new Error( + "The root Bombadil executable must be an absolute normalized path inside repositoryRoot", + ); + } + let metadata; + try { + metadata = await lstat(candidate); + } catch { + throw new Error("The root Bombadil executable does not exist at its configured path"); + } + if (metadata.isSymbolicLink() || !metadata.isFile()) { + throw new Error("The root Bombadil executable must be a regular nonsymlink file"); + } + if ((metadata.mode & 0o111) === 0) { + throw new Error("The root Bombadil executable must have an executable mode bit"); + } + const resolved = await resolveExistingRealPath(candidate, "The root Bombadil executable"); + if (resolved !== candidate || !isWithin(repositoryRoot, resolved)) { + throw new Error( + "The root Bombadil executable must not traverse a symlink or escape repositoryRoot", + ); + } + return resolved; +} + +function sameBombadilExecutableIdentity( + first: BigIntStats, + second: BigIntStats, +): boolean { + return first.dev === second.dev + && first.ino === second.ino + && first.nlink === second.nlink + && first.mode === second.mode + && first.size === second.size + && first.mtimeNs === second.mtimeNs + && first.ctimeNs === second.ctimeNs; +} + +async function attestBombadilExecutable( + path: string, + options: Readonly<{ readonly snapshotPath?: string }> = {}, +): Promise { + let lexical: BigIntStats; + try { + lexical = await lstat(path, { bigint: true }); + } catch { + throw new Error("The root Bombadil executable does not exist at its configured path"); + } + if (lexical.isSymbolicLink() || !lexical.isFile()) { + throw new Error("The root Bombadil executable must be a regular nonsymlink file"); + } + if ((lexical.mode & 0o111n) === 0n) { + throw new Error("The root Bombadil executable must have an executable mode bit"); + } + if (lexical.size > BigInt(MAX_BOMBADIL_EXECUTABLE_BYTES)) { + throw new Error( + `The root Bombadil executable exceeds the ${String(MAX_BOMBADIL_EXECUTABLE_BYTES)}-byte attestation limit`, + ); + } + const handle = await open( + path, + fileSystemConstants.O_RDONLY | fileSystemConstants.O_NOFOLLOW, + ); + let snapshotHandle: Awaited> | null = null; + try { + if (options.snapshotPath !== undefined) { + snapshotHandle = await open( + options.snapshotPath, + fileSystemConstants.O_WRONLY + | fileSystemConstants.O_CREAT + | fileSystemConstants.O_EXCL + | fileSystemConstants.O_NOFOLLOW, + 0o600, + ); + } + const before = await handle.stat({ bigint: true }); + if (!before.isFile() || !sameBombadilExecutableIdentity(lexical, before)) { + throw new Error("The root Bombadil executable changed during attestation"); + } + const hash = createHash("sha256"); + const buffer = Buffer.allocUnsafe(1024 * 1024); + let position = 0; + const size = Number(before.size); + while (position < size) { + const length = Math.min(buffer.byteLength, size - position); + const { bytesRead } = await handle.read(buffer, 0, length, position); + if (bytesRead <= 0) { + throw new Error("The root Bombadil executable changed during attestation"); + } + const bytes = buffer.subarray(0, bytesRead); + hash.update(bytes); + if (snapshotHandle !== null) { + let written = 0; + while (written < bytesRead) { + const result = await snapshotHandle.write( + bytes, + written, + bytesRead - written, + position + written, + ); + if (result.bytesWritten <= 0) { + throw new Error("The private Bombadil executable snapshot write made no progress"); + } + written += result.bytesWritten; + } + } + position += bytesRead; + } + if (snapshotHandle !== null) { + await snapshotHandle.chmod(0o500); + await snapshotHandle.sync(); + } + const after = await handle.stat({ bigint: true }); + if (!sameBombadilExecutableIdentity(before, after)) { + throw new Error("The root Bombadil executable changed during attestation"); + } + const finalLexical = await lstat(path, { bigint: true }); + if (!sameBombadilExecutableIdentity(after, finalLexical)) { + throw new Error("The root Bombadil executable changed during attestation"); + } + return Object.freeze({ + device: after.dev, + inode: after.ino, + linkCount: after.nlink, + mode: after.mode, + sha256: hash.digest("hex"), + size, + }); + } finally { + try { + await snapshotHandle?.close(); + } finally { + await handle.close(); + } + } +} + +async function removePrivateBombadilExecutableSnapshot( + snapshot: BombadilExecutableSnapshot, +): Promise { + if (snapshot.attestation.linkCount !== 1n) { + throw new Error("The private Bombadil executable snapshot file identity is not exclusive"); + } + const directory = await openPrivateBombadilSnapshotDirectory(snapshot); + let madeWritable = false; + let directoryRemoved = false; + try { + await requireSamePrivateBombadilSnapshotExecutable(snapshot); + await directory.chmod(0o700); + madeWritable = true; + await requireSameOpenPrivateBombadilSnapshotDirectory( + directory, + snapshot, + 0o700n, + ); + await requireSamePrivateBombadilSnapshotExecutable(snapshot); + await requireSameOpenPrivateBombadilSnapshotDirectory( + directory, + snapshot, + 0o700n, + ); + await requireSameLexicalPrivateBombadilSnapshotExecutable(snapshot); + await unlink(snapshot.executablePath); + await requireMissingPrivateBombadilSnapshotPath( + snapshot.executablePath, + "executable", + ); + await requireSameOpenPrivateBombadilSnapshotDirectory( + directory, + snapshot, + 0o700n, + ); + await rmdir(snapshot.directoryPath); + directoryRemoved = true; + await requireMissingPrivateBombadilSnapshotPath( + snapshot.directoryPath, + "directory", + ); + } catch (error) { + if (madeWritable && !directoryRemoved) { + try { + const descriptor = await directory.stat({ bigint: true }); + if ( + descriptor.isDirectory() + && descriptor.dev === snapshot.directoryIdentity.device + && descriptor.ino === snapshot.directoryIdentity.inode + ) { + await directory.chmod(Number(snapshot.directoryIdentity.mode & 0o777n)); + } + } catch (resealError) { + throw new AggregateError( + [error, resealError], + "Bombadil snapshot cleanup failed and its exact directory could not be resealed", + { cause: error }, + ); + } + } + throw error; + } finally { + await directory.close(); + } +} + +function sameBombadilSnapshotDirectoryIdentity( + metadata: BigIntStats, + identity: BombadilSnapshotDirectoryIdentity, + permissions: bigint = identity.mode & 0o777n, +): boolean { + const expectedMode = (identity.mode & ~0o777n) | permissions; + return metadata.isDirectory() + && !metadata.isSymbolicLink() + && metadata.dev === identity.device + && metadata.ino === identity.inode + && metadata.mode === expectedMode; +} + +async function readPrivateBombadilSnapshotDirectoryIdentity( + path: string, +): Promise { + const metadata = await lstat(path, { bigint: true }); + if (metadata.isSymbolicLink() || !metadata.isDirectory()) { + throw new Error("The private Bombadil executable snapshot path is not a nonsymlink directory"); + } + return Object.freeze({ + device: metadata.dev, + inode: metadata.ino, + mode: metadata.mode, + }); +} + +async function openPrivateBombadilSnapshotDirectory( + snapshot: Pick, +): Promise>> { + const lexical = await lstat(snapshot.directoryPath, { bigint: true }); + if (!sameBombadilSnapshotDirectoryIdentity(lexical, snapshot.directoryIdentity)) { + throw new Error("The private Bombadil executable snapshot directory identity changed"); + } + const directory = await open( + snapshot.directoryPath, + fileSystemConstants.O_RDONLY + | fileSystemConstants.O_DIRECTORY + | fileSystemConstants.O_NOFOLLOW, + ); + try { + const descriptor = await directory.stat({ bigint: true }); + if (!sameBombadilSnapshotDirectoryIdentity(descriptor, snapshot.directoryIdentity)) { + throw new Error("The private Bombadil executable snapshot directory identity changed"); + } + return directory; + } catch (error) { + await directory.close(); + throw error; + } +} + +async function requireSameOpenPrivateBombadilSnapshotDirectory( + directory: Awaited>, + snapshot: Pick, + permissions: bigint, +): Promise { + const descriptor = await directory.stat({ bigint: true }); + const lexical = await lstat(snapshot.directoryPath, { bigint: true }); + if ( + !sameBombadilSnapshotDirectoryIdentity( + descriptor, + snapshot.directoryIdentity, + permissions, + ) + || !sameBombadilSnapshotDirectoryIdentity( + lexical, + snapshot.directoryIdentity, + permissions, + ) + ) { + throw new Error("The private Bombadil executable snapshot directory identity changed"); + } +} + +async function requireSamePrivateBombadilSnapshotExecutable( + snapshot: BombadilExecutableSnapshot, +): Promise { + const attestation = await attestBombadilExecutable(snapshot.executablePath); + if ( + attestation.device !== snapshot.attestation.device + || attestation.inode !== snapshot.attestation.inode + || attestation.linkCount !== snapshot.attestation.linkCount + || attestation.mode !== snapshot.attestation.mode + || attestation.size !== snapshot.attestation.size + || attestation.sha256 !== snapshot.attestation.sha256 + ) { + throw new Error("The private Bombadil executable snapshot file identity changed"); + } +} + +async function requireSameLexicalPrivateBombadilSnapshotExecutable( + snapshot: BombadilExecutableSnapshot, +): Promise { + const metadata = await lstat(snapshot.executablePath, { bigint: true }); + if ( + metadata.isSymbolicLink() + || !metadata.isFile() + || metadata.dev !== snapshot.attestation.device + || metadata.ino !== snapshot.attestation.inode + || metadata.nlink !== snapshot.attestation.linkCount + || metadata.mode !== snapshot.attestation.mode + || metadata.size !== BigInt(snapshot.attestation.size) + ) { + throw new Error("The private Bombadil executable snapshot file identity changed"); + } +} + +async function requireMissingPrivateBombadilSnapshotPath( + path: string, + label: "directory" | "executable", +): Promise { + try { + await lstat(path); + } catch (error) { + if (isRecord(error) && error.code === "ENOENT") return; + throw error; + } + throw new Error(`The private Bombadil executable snapshot ${label} survived cleanup`); +} + +async function sealPartialPrivateBombadilSnapshotForRetention( + snapshot: Pick, +): Promise { + const directory = await openPrivateBombadilSnapshotDirectory(snapshot); + try { + await directory.chmod(0o500); + await requireSameOpenPrivateBombadilSnapshotDirectory( + directory, + snapshot, + 0o500n, + ); + } finally { + await directory.close(); + } +} + +async function materializePrivateBombadilExecutableSnapshot(options: { + readonly directoryPath: string; + readonly sourcePath: string; +}): Promise { + await createExclusiveDirectory( + options.directoryPath, + "Private Bombadil executable snapshot directory", + ); + const executablePath = join(options.directoryPath, "bombadil"); + let initialDirectoryIdentity: BombadilSnapshotDirectoryIdentity | null = null; + let cleanupSnapshot: BombadilExecutableSnapshot | null = null; + try { + initialDirectoryIdentity = await readPrivateBombadilSnapshotDirectoryIdentity( + options.directoryPath, + ); + const sourceAttestation = await attestBombadilExecutable(options.sourcePath, { + snapshotPath: executablePath, + }); + const attestation = await attestBombadilExecutable(executablePath); + cleanupSnapshot = Object.freeze({ + attestation, + directoryIdentity: initialDirectoryIdentity, + directoryPath: options.directoryPath, + executablePath, + sourceAttestation, + }); + if ( + sourceAttestation.sha256 !== attestation.sha256 + || sourceAttestation.size !== attestation.size + ) { + throw new Error("The private Bombadil executable snapshot does not match reviewed bytes"); + } + if ( + sourceAttestation.device === attestation.device + && sourceAttestation.inode === attestation.inode + ) { + throw new Error("The private Bombadil executable snapshot must own a distinct file identity"); + } + if ((attestation.mode & 0o777n) !== 0o500n) { + throw new Error("The private Bombadil executable snapshot must be owner-read-execute only"); + } + if (attestation.linkCount !== 1n) { + throw new Error("The private Bombadil executable snapshot must have exactly one hard link"); + } + const directory = await openPrivateBombadilSnapshotDirectory(cleanupSnapshot); + let directoryIdentity: BombadilSnapshotDirectoryIdentity; + try { + await directory.chmod(0o500); + const descriptor = await directory.stat({ bigint: true }); + const lexical = await lstat(options.directoryPath, { bigint: true }); + if ( + !sameBombadilSnapshotDirectoryIdentity( + descriptor, + initialDirectoryIdentity, + 0o500n, + ) + || !sameBombadilSnapshotDirectoryIdentity( + lexical, + initialDirectoryIdentity, + 0o500n, + ) + ) { + throw new Error("The private Bombadil executable snapshot directory is not sealed"); + } + directoryIdentity = Object.freeze({ + device: descriptor.dev, + inode: descriptor.ino, + mode: descriptor.mode, + }); + } finally { + await directory.close(); + } + return Object.freeze({ + attestation, + directoryIdentity, + directoryPath: options.directoryPath, + executablePath, + sourceAttestation, + }); + } catch (error) { + if (cleanupSnapshot !== null) { + try { + await removePrivateBombadilExecutableSnapshot(cleanupSnapshot); + } catch (cleanupError) { + throw new BombadilPersistenceError( + "Bombadil executable snapshot materialization and cleanup both failed", + [error, cleanupError], + ); + } + } else if (initialDirectoryIdentity !== null) { + try { + await sealPartialPrivateBombadilSnapshotForRetention({ + directoryIdentity: initialDirectoryIdentity, + directoryPath: options.directoryPath, + }); + } catch (retentionError) { + throw new BombadilPersistenceError( + "Bombadil snapshot materialization failed and its exact partial evidence could not be sealed", + [error, retentionError], + ); + } + throw new BombadilPersistenceError( + "Bombadil snapshot materialization failed; its exact partial snapshot was retained as protected persistence evidence", + [error], + ); + } + throw error; + } +} + +function validateBombadilExecutableAttestation( + attestation: BombadilExecutableAttestation, + toolchain: DirectBombadilToolchainConfig | null, + observedVersion: typeof EXPECTED_BOMBADIL_VERSION, +): BombadilToolchainEvidence { + assertBombadilExecutableAttestationMatchesToolchain(attestation, toolchain); + return Object.freeze(toolchain === null + ? { + buildContract: null, + kind: "npm-package" as const, + sha256: attestation.sha256, + sourceRevision: null, + version: observedVersion, + } + : { + buildContract: toolchain.buildContract, + kind: "reviewed-override" as const, + sha256: attestation.sha256, + sourceRevision: toolchain.sourceRevision, + version: observedVersion, + }); +} + +function assertBombadilExecutableAttestationMatchesToolchain( + attestation: BombadilExecutableAttestation, + toolchain: DirectBombadilToolchainConfig | null, +): void { + if (toolchain !== null && attestation.sha256 !== toolchain.sha256) { + throw new Error("The root Bombadil executable SHA-256 does not match bombadilToolchain.sha256"); + } +} + +function assertSameBombadilExecutableAttestation( + before: BombadilExecutableAttestation, + after: BombadilExecutableAttestation, +): void { + if ( + before.device !== after.device + || before.inode !== after.inode + || before.linkCount !== after.linkCount + || before.mode !== after.mode + || before.size !== after.size + || before.sha256 !== after.sha256 + ) { + throw new Error("The root Bombadil executable changed before native process startup"); + } +} + async function requireDirectory(path: string, label: string): Promise { let metadata; try { @@ -5712,6 +6581,12 @@ async function resolveDirectBombadilRealPaths( if (resolvedReplayPath !== null && !resolvedReplayPath.endsWith(".jsonl")) { throw new Error("--replay must resolve to a .jsonl trace inside repositoryRoot"); } + const bombadilExecutable = config.bombadilToolchain === null + ? bombadilNativeBinary(repositoryRoot) + : await resolveBombadilExecutablePath( + config.bombadilToolchain.executablePath, + repositoryRoot, + ); return { config: { ...config, @@ -5723,14 +6598,19 @@ async function resolveDirectBombadilRealPaths( "direct-bombadil", config.artifactName, ), - bombadilExecutable: bombadilNativeBinary(repositoryRoot), + bombadilExecutable, server: { ...config.server, cwd: serverCwd }, }, replayPath: resolvedReplayPath, }; } -async function readExactBombadilVersion(repositoryRoot: string): Promise { +async function readExactBombadilVersion( + repositoryRoot: string, + toolchain: DirectBombadilToolchainConfig | null, + executablePath: string, + expectedExecutableAttestation: BombadilExecutableAttestation, +): Promise { const packagePath = join( repositoryRoot, "node_modules", @@ -5752,6 +6632,13 @@ async function readExactBombadilVersion(repositoryRoot: string): Promise ) { throw new Error(`The root Bombadil package must be exactly ${EXPECTED_BOMBADIL_VERSION}`); } + if (toolchain !== null) { + return await readExactBombadilExecutableVersion( + executablePath, + repositoryRoot, + expectedExecutableAttestation, + ); + } return EXPECTED_BOMBADIL_VERSION; } @@ -5885,6 +6772,15 @@ export async function runDirectBombadilFuzzMatrix( throw new Error("Bombadil matrices support public-summary uploads only"); } campaigns = validateCampaignMatrix(campaignsInput); + const toolchainIdentities = new Set(campaigns.map((campaign) => + bombadilToolchainIdentity(validateBombadilToolchainConfig( + campaign.config.bombadilToolchain, + resolve(campaign.config.repositoryRoot), + )) + )); + if (toolchainIdentities.size !== 1) { + throw new Error("Every Bombadil matrix campaign must use the same toolchain identity"); + } parsed = parseMatrixCampaignArgument(normalizedOptions.arguments); selected = parsed.campaignId === null ? campaigns @@ -6257,7 +7153,12 @@ async function runDirectBombadilFuzzInternal( argument === "{port}" ? validated.port : argument ); - let bombadilVersion: string | null = null; + let bombadilVersion: typeof EXPECTED_BOMBADIL_VERSION | null = null; + let bombadilExecutableAttestation: BombadilExecutableAttestation | null = null; + let bombadilRuntimeExecutableAttestation: BombadilExecutableAttestation | null = null; + let bombadilRuntimeExecutable = validated.bombadilExecutable; + let bombadilExecutableSnapshot: BombadilExecutableSnapshot | null = null; + let bombadilToolchainEvidence: BombadilToolchainEvidence | null = null; let lease: ServerLease | null = null; let processResult: BombadilProcessResult | null = null; let attestation: DirectBombadilTraceAttestation | null = null; @@ -6273,8 +7174,35 @@ async function runDirectBombadilFuzzInternal( let writersSettled = true; { try { - await requireRegularFile(validated.bombadilExecutable, "The root Bombadil executable"); - bombadilVersion = await readExactBombadilVersion(validated.repositoryRoot); + if (validated.bombadilToolchain === null) { + bombadilExecutableAttestation = await attestBombadilExecutable( + validated.bombadilExecutable, + ); + bombadilRuntimeExecutableAttestation = bombadilExecutableAttestation; + } else { + bombadilExecutableSnapshot = await materializePrivateBombadilExecutableSnapshot({ + directoryPath: join(artifactRun.runDirectory, ".bombadil-toolchain"), + sourcePath: validated.bombadilExecutable, + }); + bombadilExecutableAttestation = bombadilExecutableSnapshot.sourceAttestation; + bombadilRuntimeExecutableAttestation = bombadilExecutableSnapshot.attestation; + bombadilRuntimeExecutable = bombadilExecutableSnapshot.executablePath; + } + assertBombadilExecutableAttestationMatchesToolchain( + bombadilExecutableAttestation, + validated.bombadilToolchain, + ); + bombadilVersion = await dependencies.readBombadilVersion( + validated.repositoryRoot, + validated.bombadilToolchain, + bombadilRuntimeExecutable, + bombadilRuntimeExecutableAttestation, + ); + bombadilToolchainEvidence = validateBombadilExecutableAttestation( + bombadilExecutableAttestation, + validated.bombadilToolchain, + bombadilVersion, + ); throwIfBombadilRunAborted(abortController.signal); try { @@ -6292,7 +7220,7 @@ async function runDirectBombadilFuzzInternal( cwd: validated.server.cwd, detachedProcessGroup: true, ...(validated.server.env === undefined ? {} : { env: validated.server.env }), - omitEnvironment: [ARTIFACT_COORDINATION_ENVIRONMENT], + omitEnvironment: [...BOMBADIL_PRIVATE_ENVIRONMENT_NAMES], }); terminateAbortedOwnedServer(abortController.signal, ownedServer); return ownedServer; @@ -6309,7 +7237,30 @@ async function runDirectBombadilFuzzInternal( } let processFailure: unknown = null; try { - processResult = await dependencies.runBombadil(abortableInvocation); + const spawnAttestation = await attestBombadilExecutable( + bombadilRuntimeExecutable, + ); + if ( + bombadilExecutableAttestation === null + || bombadilRuntimeExecutableAttestation === null + ) { + throw new Error("The root Bombadil executable was not attested before server startup"); + } + assertSameBombadilExecutableAttestation( + bombadilRuntimeExecutableAttestation, + spawnAttestation, + ); + bombadilToolchainEvidence = validateBombadilExecutableAttestation( + bombadilExecutableAttestation, + validated.bombadilToolchain, + bombadilVersion, + ); + const attestedInvocation: AttestedDirectBombadilInvocation = { + ...abortableInvocation, + command: [bombadilRuntimeExecutable, ...abortableInvocation.command.slice(1)], + [BOMBADIL_EXECUTABLE_ATTESTATION]: spawnAttestation, + }; + processResult = await dependencies.runBombadil(attestedInvocation); } catch (error) { processFailure = error; } @@ -6361,6 +7312,28 @@ async function runDirectBombadilFuzzInternal( failure ??= error; } } + if (bombadilExecutableSnapshot !== null && writersSettled) { + try { + await removePrivateBombadilExecutableSnapshot(bombadilExecutableSnapshot); + bombadilExecutableSnapshot = null; + } catch (error) { + const persistence = new BombadilPersistenceError( + "Bombadil private executable snapshot could not be removed", + [error], + ); + failure = failure === null + ? persistence + : combinePersistenceFailure( + failure, + persistence, + "Bombadil private executable snapshot could not be removed", + ); + } + } else if (bombadilExecutableSnapshot !== null) { + failure = retainPrivateBombadilSnapshotAfterWriterSettlementFailure( + failure ?? new Error("writer settlement unavailable"), + ); + } if (writersSettled) { try { try { @@ -6511,6 +7484,7 @@ async function runDirectBombadilFuzzInternal( bombadil: { version: bombadilVersion, executable: validated.bombadilExecutable, + toolchain: bombadilToolchainEvidence, exitCode: processResult?.exitCode ?? null, termination: processResult?.termination ?? null, outputPath, diff --git a/src/tooling/bombadil.ts b/src/tooling/bombadil.ts index 5fc4a69..8ef6952 100644 --- a/src/tooling/bombadil.ts +++ b/src/tooling/bombadil.ts @@ -84,6 +84,8 @@ export type { DirectBombadilServerConfig, DirectBombadilTraceAttestation, DirectBombadilTraceBinding, + DirectBombadilToolchainBuildContract, + DirectBombadilToolchainConfig, DirectBombadilUploadMode, DirectBombadilViewportConfig, } from "./bombadil-runner.js"; From bad587c1ed3f82ff8e30c8b49e1dc7e683771f8d Mon Sep 17 00:00:00 2001 From: 0thernet Date: Wed, 2 Sep 2026 14:46:01 -0400 Subject: [PATCH 2/2] test: use bounded Bombadil lifecycle launcher --- src/tooling/bombadil-runner.test.ts | 78 ++++++++++++----------------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/src/tooling/bombadil-runner.test.ts b/src/tooling/bombadil-runner.test.ts index a03127b..2ceb459 100644 --- a/src/tooling/bombadil-runner.test.ts +++ b/src/tooling/bombadil-runner.test.ts @@ -57,6 +57,10 @@ const CHROME_TRANSIENT_OTHER_TEST_ID = "123e4567-e89b-42d3-a456-426614174001"; const CHROME_TRANSIENT_THIRD_TEST_ID = "123e4567-e89b-42d3-a456-426614174002"; const temporaryDirectories: string[] = []; +function testBunEvaluationCommand(source: string): string[] { + return ["/usr/bin/env", process.execPath, "-e", source]; +} + function artifactRunPlan< UploadMode extends "private-vetted" | "public-summary" = "public-summary", >( @@ -2720,11 +2724,9 @@ describe("Direct Bombadil process lifecycle", () => { for (const name of names) process.env[name] = `private-${name}`; try { const result = await runBombadilNativeProcess({ - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( `process.stdout.write(JSON.stringify(${JSON.stringify(names)}.map((name) => process.env[name])))`, - ], + ), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4068,7 +4070,7 @@ describe("Direct Bombadil process lifecycle", () => { maxPathBytes: 256, maxTotalBytes: 2_048, }, - command: [process.execPath, "-e", source], + command: testBunEvaluationCommand(source), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4091,11 +4093,9 @@ describe("Direct Bombadil process lifecycle", () => { await writeFile(join(downloads, fileName), "preexisting\n"); const error = await rejection(runBombadilNativeProcess({ - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( `require("node:fs").writeFileSync(${JSON.stringify(sentinelPath)}, "spawned");`, - ], + ), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4118,7 +4118,7 @@ describe("Direct Bombadil process lifecycle", () => { `fs.writeFileSync(${JSON.stringify(completionPath)}, 'complete');`, ].join(" "); const error = await rejection(runBombadilNativeProcess({ - command: [process.execPath, "-e", source], + command: testBunEvaluationCommand(source), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4200,11 +4200,9 @@ describe("Direct Bombadil process lifecycle", () => { const previous = process.env.DIRECT_BOMBADIL_RUN_ID; process.env.DIRECT_BOMBADIL_RUN_ID = "child-visible-secret"; const running = runBombadilNativeProcess({ - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( "console.log(process.env.DIRECT_BOMBADIL_RUN_ID ?? 'absent')", - ], + ), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4237,7 +4235,7 @@ describe("Direct Bombadil process lifecycle", () => { maxPathBytes: 256, maxTotalBytes: 2_048, }, - command: [process.execPath, "-e", source], + command: testBunEvaluationCommand(source), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4261,11 +4259,9 @@ describe("Direct Bombadil process lifecycle", () => { maxPathBytes: 256, maxTotalBytes: 2_048, }, - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( `require("node:fs").writeFileSync(${JSON.stringify(overflowPath)}, Buffer.alloc(4096));`, - ], + ), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4291,7 +4287,7 @@ describe("Direct Bombadil process lifecycle", () => { ].join(" "); const startedAt = Date.now(); const result = await runBombadilNativeProcess({ - command: [process.execPath, "-e", leaderSource], + command: testBunEvaluationCommand(leaderSource), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4341,7 +4337,7 @@ describe("Direct Bombadil process lifecycle", () => { return kill(processId, signal); }, async () => await runBombadilNativeProcess({ - command: [process.execPath, "-e", leaderSource], + command: testBunEvaluationCommand(leaderSource), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4384,11 +4380,9 @@ describe("Direct Bombadil process lifecycle", () => { async () => { const running = runBombadilNativeProcess({ abortSignal: controller.signal, - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( `require("node:fs").writeFileSync(${JSON.stringify(processIdPath)}, String(process.pid)); setTimeout(() => process.exit(0), 5000); setInterval(() => {}, 1000);`, - ], + ), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4418,11 +4412,9 @@ describe("Direct Bombadil process lifecycle", () => { const directory = await mkdtemp(join(tmpdir(), "direct-bombadil-timeout-")); temporaryDirectories.push(directory); const invocation: DirectBombadilInvocation = { - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( "console.log('timeout output'); process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);", - ], + ), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4446,7 +4438,7 @@ describe("Direct Bombadil process lifecycle", () => { ].join(" "); const startedAt = Date.now(); const result = await runBombadilNativeProcess({ - command: [process.execPath, "-e", childSource], + command: testBunEvaluationCommand(childSource), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4465,11 +4457,9 @@ describe("Direct Bombadil process lifecycle", () => { const startedAt = Date.now(); const result = await runBombadilNativeProcess({ abortSignal: controller.signal, - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( "console.log('abort output'); process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);", - ], + ), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/", @@ -4490,11 +4480,9 @@ describe("Direct Bombadil process lifecycle", () => { alreadyAborted.abort(); const alreadyAbortedResult = await runBombadilNativeProcessForTest({ abortSignal: alreadyAborted.signal, - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( `require("node:fs").writeFileSync(${JSON.stringify(alreadyAbortedSentinel)}, "spawned");`, - ], + ), cwd: alreadyAbortedRoot, outputPath: alreadyAbortedRoot, targetUrl: "http://127.0.0.1:4919/", @@ -4527,11 +4515,9 @@ describe("Direct Bombadil process lifecycle", () => { }); const deferredResult = runBombadilNativeProcessForTest({ abortSignal: deferredAbort.signal, - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( `require("node:fs").writeFileSync(${JSON.stringify(deferredSentinel)}, "spawned");`, - ], + ), cwd: deferredRoot, outputPath: deferredRoot, targetUrl: "http://127.0.0.1:4919/", @@ -4561,11 +4547,9 @@ describe("Direct Bombadil process lifecycle", () => { const controller = new AbortController(); const result = await runBombadilNativeProcessForTest({ abortSignal: controller.signal, - command: [ - process.execPath, - "-e", + command: testBunEvaluationCommand( `require("node:fs").writeFileSync(${JSON.stringify(sentinelPath)}, "spawned");`, - ], + ), cwd: directory, outputPath: join(directory, "output"), targetUrl: "http://127.0.0.1:4919/", @@ -4674,7 +4658,7 @@ describe("Direct Bombadil process lifecycle", () => { maxPathBytes: 256, maxTotalBytes: 8_192, }, - command: [process.execPath, "-e", source], + command: testBunEvaluationCommand(source), cwd: directory, outputPath: directory, targetUrl: "http://127.0.0.1:4919/",