From e6f1716bd33e6c4f36637bf0e8db1360a9d80aa7 Mon Sep 17 00:00:00 2001 From: Alain Schlesser Date: Sun, 18 Oct 2026 01:34:11 +0200 Subject: [PATCH 1/2] fix(ci): reuse stable Docker sources in local proofs --- docs/process/feature-captures.md | 5 +++ docs/process/testing.md | 23 +++++++++++ scripts/capture.ts | 32 +++++++++++----- scripts/captures/inside.ts | 4 +- scripts/diagram-export-producer.d.mts | 2 + scripts/diagram-export-producer.mjs | 55 +++++++++++++++++++++++---- scripts/native-proof/stable-run.sh | 45 ++++++++++++++++++++++ scripts/prove-native-host.ts | 29 ++++++++------ scripts/prove-worker-identity.ts | 30 ++++++++------- scripts/workspace-lease-container.ts | 8 ++++ tests/docker-bind-stability.test.ts | 55 +++++++++++++++++++++++++++ tests/workspace-lease.test.ts | 14 +++---- 12 files changed, 251 insertions(+), 51 deletions(-) create mode 100644 scripts/native-proof/stable-run.sh create mode 100644 scripts/workspace-lease-container.ts create mode 100644 tests/docker-bind-stability.test.ts diff --git a/docs/process/feature-captures.md b/docs/process/feature-captures.md index 25363e796..d282da143 100644 --- a/docs/process/feature-captures.md +++ b/docs/process/feature-captures.md @@ -69,6 +69,11 @@ crops retain their source dimensions and fail if the canvas cannot contain them. Component-stage CSS measurements round to their declared image pixels; phone border boxes are exact. Runtime transcripts use the full document height. +Rendering binds the stable checkout directory `tmp/capture-share/` and selects a +fresh staging child by name. Docker Desktop therefore sees the same output bind +source on repeated runs. Verified files are copied into staging on the requested +output filesystem before the existing atomic publication renames. + Docker and the installed Bun binary are required. Builds and Chromium run in the image declared by the shared visual runner, with the frozen installed packages. The manifest records the image ID/digests, browser and Bun binaries, diff --git a/docs/process/testing.md b/docs/process/testing.md index 39bcdf5d1..1d5846643 100644 --- a/docs/process/testing.md +++ b/docs/process/testing.md @@ -314,6 +314,29 @@ in the [CI and merge reference](../../.agents/skills/work-issue/references/ci-an navigation; see [the assertion/domain mapping](proof-domains.md). Dynamic test counts and passing assertions come from actual suite reports, not AST counts. +## Stable Docker proof inputs + +Docker Desktop on WSL2 retains a host mount for each distinct bind source, even +when a container exits. Local proofs bind stable checkout directories and select +fresh run directories inside the container. The first use of a checkout can add +mounts; repeating a proof must not add sources solely because its run ID changed. +Restarting Docker Desktop or `wsl --shutdown` clears older leaked mounts. + +| Caller | Stable host source | Run selection | +| --- | --- | --- | +| Native hosted admission | `tmp/native-host-proof/` | Container-only bind of the selected run to `/proof` | +| Worker identity | `tmp/worker-identity-proof/` | Same `/proof` projection, with read-only helper and variant projections inside the container | +| Editorial capture | `tmp/capture-share/` | A staging directory name selects outputs; verified files are copied to destination-local staging before atomic publication | +| Native diagram export | Private tar-filled named volumes and copied captured Bun bytes | Only source and filtered installation volumes are mounted read-only; output is copied out and owned containers/volumes are removed | +| Pinned workspace lease test | `tmp/workspace-lease-container/` | Container working directory selects the fresh fixture | + +The stable source checkout, installed Bun binary, font cache and lease directory +retain their existing mounts. Browser, raster and audio callers already use stable source paths. Images, isolation flags and proof assertions remain the +same. Container-only projections create no new Docker host bind source. The private +diagram copy transport additionally requires host `tar` for its input archives. A new +checkout still introduces its own stable sources; this does not clear old mounts +or promise zero growth across distinct checkouts. + ## GitHub Actions CI CI and contract checks run from `.github/workflows/ci.yml` and `contract.yml`. diff --git a/scripts/capture.ts b/scripts/capture.ts index eb2818709..37fe3a698 100644 --- a/scripts/capture.ts +++ b/scripts/capture.ts @@ -1,7 +1,7 @@ import { execFileSync } from "node:child_process"; -import { mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from "node:fs/promises"; +import { copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { relative, resolve } from "node:path"; +import { basename, dirname, relative, resolve } from "node:path"; import { readCatalogue, type Viewport } from "./captures/catalogue.ts"; import { readFontLock, verifyFontCache } from "./captures/fonts.ts"; import { prepareFontCache } from "./captures/prepare-fonts.ts"; @@ -50,6 +50,12 @@ async function command(argv: string[]): Promise { if (await process.exited !== 0) throw new Error(`Capture command failed: ${argv.slice(0, 3).join(" ")}`); } +export function captureRenderArgs(root: string, cache: string, staging: string, bun: string, image: string, runnerDirectory: string): string[] { + return ["docker", "run", "--rm", "--network=none", "--read-only", "--ipc=host", "--user", `${process.getuid?.() ?? 0}:${process.getgid?.() ?? 0}`, + "--tmpfs", "/tmp:rw,exec,nosuid,mode=1777", "-v", `${root}:/repo:ro`, "-v", `${cache}:/fonts:ro`, + "-v", `${dirname(staging)}:/captures`, "--env", `BRAIN_CAPTURE_RUN=${basename(staging)}`, "-v", `${bun}:/capture-bun:ro`, "-v", `${bun}:/usr/local/bin/bun:ro`, "-v", `${bun}:/usr/local/bin/bunx:ro`, "-w", "/repo", image, "node", `/repo/${relative(root, runnerDirectory)}/inside.mjs`]; +} + async function run(): Promise { const options = argumentsFor(process.argv.slice(2)); const catalogue = await readCatalogue(root); @@ -73,10 +79,13 @@ async function run(): Promise { const lockDirectory = resolve(root,"tmp/.feature-capture-lock"); try { await mkdir(lockDirectory); } catch { throw new Error("Another capture owns this checkout’s build; finish it before capturing again. If it was interrupted, remove tmp/.feature-capture-lock after verifying no capture is running."); } - let staging: string | undefined, runnerDirectory: string | undefined; + let staging: string | undefined, publication: string | undefined, runnerDirectory: string | undefined; try { await writeFile(resolve(lockDirectory,"owner.json"),JSON.stringify({pid:process.pid})+"\n"); - staging = await mkdtemp(resolve(output, ".capture-")); + const share = resolve(root, "tmp", "capture-share"); + await mkdir(share, { recursive: true }); + staging = await mkdtemp(resolve(share, ".capture-")); + publication = await mkdtemp(resolve(output, ".capture-publication-")); runnerDirectory = await mkdtemp(resolve(root, "tmp/.capture-runner-")); let image: Array<{ Id: string; RepoDigests?: string[] }>; try { image = JSON.parse(execFileSync("docker", ["image", "inspect", pin.image], { encoding: "utf8" })); } @@ -106,9 +115,7 @@ async function run(): Promise { reduced_motion: catalogue.environment.reduced_motion }, }; await writeFile(resolve(staging, "capture-job.json"), JSON.stringify(job) + "\n"); - await command(["docker", "run", "--rm", "--network=none", "--read-only", "--ipc=host", "--user", `${process.getuid?.() ?? 0}:${process.getgid?.() ?? 0}`, - "--tmpfs", "/tmp:rw,exec,nosuid,mode=1777", "-v", `${root}:/repo:ro`, "-v", `${cache}:/fonts:ro`, - "-v", `${staging}:/captures`, "-v", `${bun}:/capture-bun:ro`, "-v", `${bun}:/usr/local/bin/bun:ro`, "-v", `${bun}:/usr/local/bin/bunx:ro`, "-w", "/repo", pin.image, "node", `/repo/${relative(root, runnerDirectory)}/inside.mjs`]); + await command(captureRenderArgs(root, cache, staging, bun, pin.image, runnerDirectory)); if ((await sourceProvenance(root)).inputs_sha256 !== source.inputs_sha256) throw new Error("Capture source changed while rendering; outputs were not accepted"); const result = JSON.parse(await readFile(resolve(staging, "capture-result.json"), "utf8")) as { artifacts: Array<{ id: string; file: string; files: Array<{file:string}>; theme: string; viewport: Viewport }> }; if (result.artifacts.length !== ids.length || new Set(result.artifacts.map((entry)=>entry.id)).size!==ids.length || result.artifacts.some((entry) => !ids.includes(entry.id) || !entry.files.length)) throw new Error("Capture runner did not return every selected recipe"); @@ -120,14 +127,19 @@ async function run(): Promise { const retained = previous.artifacts.filter((entry) => typeof entry === "object" && entry !== null && !replaced.has((entry as { file: string }).file)); const manifest = { purpose: previous.purpose, selected_ids: ids, artifacts: [...retained, ...result.artifacts] }; await writeFile(resolve(staging, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n"); - for (const entry of result.artifacts) for (const file of entry.files) await rename(resolve(staging, file.file), resolve(output, file.file)); - await rename(resolve(staging, "manifest.json"), resolve(output, "manifest.json")); + // Copy to the destination filesystem before the existing atomic publication + // renames. Explicit output directories may live on a different filesystem. + for (const entry of result.artifacts) for (const file of entry.files) await copyFile(resolve(staging, file.file), resolve(publication, file.file)); + await copyFile(resolve(staging, "manifest.json"), resolve(publication, "manifest.json")); + for (const entry of result.artifacts) for (const file of entry.files) await rename(resolve(publication, file.file), resolve(output, file.file)); + await rename(resolve(publication, "manifest.json"), resolve(output, "manifest.json")); console.log(`Captured ${ids.length} selected recipes; each artifact records its own source/environment provenance.`); } finally { if (staging) await rm(staging, { recursive: true, force: true }); + if (publication) await rm(publication, { recursive: true, force: true }); if (runnerDirectory) await rm(runnerDirectory, { recursive: true, force: true }); await rm(lockDirectory, {recursive:true,force:true}); } } -run().catch((error) => { console.error((error as Error).message); process.exitCode = 1; }); +if (import.meta.main) run().catch((error) => { console.error((error as Error).message); process.exitCode = 1; }); diff --git a/scripts/captures/inside.ts b/scripts/captures/inside.ts index 433816cea..c3a98df12 100644 --- a/scripts/captures/inside.ts +++ b/scripts/captures/inside.ts @@ -17,7 +17,9 @@ export interface CaptureJob { } async function run(): Promise { - const root = "/repo", output = "/captures", cache = "/fonts"; + const run = process.env.BRAIN_CAPTURE_RUN; + if (!run || !/^\.capture-[a-zA-Z0-9_-]+$/.test(run)) throw new Error("Capture runner requires its owned staging directory name"); + const root = "/repo", output = resolve("/captures", run), cache = "/fonts"; const job: CaptureJob = JSON.parse(await readFile(resolve(output, "capture-job.json"), "utf8")); const catalogue = await readCatalogue(root); const index = JSON.parse(await readFile(resolve(root, "packages/ui-kit/storybook-static/index.json"), "utf8")); diff --git a/scripts/diagram-export-producer.d.mts b/scripts/diagram-export-producer.d.mts index fff3d6845..c1ac45001 100644 --- a/scripts/diagram-export-producer.d.mts +++ b/scripts/diagram-export-producer.d.mts @@ -2,3 +2,5 @@ export const diagramProducerImage:string; export const diagramProducerImageIdentity:string; export function diagramProducerRuntime():{image:string;bun:string;executable:string;bytes:Uint8Array;sha256:string}; export function prepareDiagramNativeArtifacts(repo:string,projects:string[],expected?:{source:string;installation:string;runtime?:string}):string|undefined; + +export function diagramNativeDockerArgs(repo: string, source: string, runtime: { image: string }, volumes: { source: string; modules: string; output: string; runtime: string }, container: string, program: string): string[]; diff --git a/scripts/diagram-export-producer.mjs b/scripts/diagram-export-producer.mjs index a0b17bd1e..63db2fa53 100644 --- a/scripts/diagram-export-producer.mjs +++ b/scripts/diagram-export-producer.mjs @@ -1,7 +1,7 @@ /** Host-only native producer: its children never inherit the browser's mounts. */ import {spawnSync} from "node:child_process"; import {createHash,randomUUID} from "node:crypto"; -import {accessSync,constants,cpSync,mkdirSync,mkdtempSync,readFileSync,readdirSync,realpathSync,renameSync,rmSync,statSync,writeFileSync} from "node:fs"; +import {accessSync,closeSync,constants,cpSync,mkdirSync,mkdtempSync,openSync,readFileSync,readdirSync,realpathSync,renameSync,rmSync,statSync,writeFileSync} from "node:fs"; import {tmpdir} from "node:os"; import {delimiter,join,resolve} from "node:path"; import {materializeDiagramExportSource} from "./diagram-export-source.mjs"; @@ -17,6 +17,27 @@ export function diagramProducerRuntime(){ const executable=realpathSync(bun),bytes=readFileSync(executable),identity={image,bun:hash(bytes)}; return{...identity,executable,bytes,sha256:hash(Buffer.from(JSON.stringify(identity)))}; } +/** Private named volumes carry only audited inputs; no host path is mounted. */ +export function diagramNativeDockerArgs(repo,source,runtime,volumes,container,program){ + return ["create","--name",container,"--pull=never","--read-only","--network=none","--shm-size=256m","--user",`${process.getuid()}:${process.getgid()}`,"--tmpfs","/tmp:rw,exec,nosuid,nodev,mode=1777", + "--mount",`type=volume,source=${volumes.source},target=${source},readonly`, + "--mount",`type=volume,source=${volumes.modules},target=${join(repo,"node_modules")},readonly`, + "--mount",`type=volume,source=${volumes.output},target=/proof`, + "--mount",`type=volume,source=${volumes.runtime},target=/native-runtime,readonly`, + "-w",source,"-e","HOME=/tmp/home",runtime.image,"/native-runtime/bun","-e",program]; +} +function dockerChecked(args,options={}){ + const result=spawnSync("docker",args,{encoding:"utf8",maxBuffer:4*1024*1024,...options}); + if(result.error||result.status!==0||result.signal)throw Error(`Native diagram Docker operation failed (${args[0]}): ${result.error??result.signal??result.status}\n${result.stderr}\n${result.stdout}`); + return result; +} +function fillPrivateVolume(directory,volume,image,archive){ + const out=openSync(archive,"wx"); + try{const packed=spawnSync("tar",["-C",directory,"-cf","-","."],{stdio:["ignore",out,"pipe"],encoding:"utf8"});if(packed.error||packed.status!==0)throw Error(`Native diagram input archive failed: ${packed.error??packed.stderr}`);}finally{closeSync(out);} + const input=openSync(archive,"r"); + try{dockerChecked(["run","--rm","--interactive","--pull=never","--network=none","--read-only","--mount",`type=volume,source=${volume},target=/inputs`,image,"tar","-xf","-","-C","/inputs"],{stdio:[input,"pipe","pipe"]});}finally{closeSync(input);} +} + /** The complete mounted data is private source + filtered installed packages. * Readonly image files and Bun are identified separately. /tmp and /dev/shm * are fresh container-owned filesystems, not mounts of host state. */ @@ -27,9 +48,12 @@ export function prepareDiagramNativeArtifacts(repo,projects,expected){ const record=JSON.parse(readFileSync(join(repo,"tmp/diagram-export-source.json"),"utf8")),runtime=diagramProducerRuntime(); if(expected?.runtime&&expected.runtime!==runtime.sha256)throw Error("NATIVE_EXPORT_RUNTIME_CHANGED"); const session=randomUUID(),base=join(repo,"tmp/diagram-export-native-prepared",session);mkdirSync(base,{recursive:true}); - const state=mkdtempSync(join(tmpdir(),"odysseus-native-producer-")); + const state=mkdtempSync(join(tmpdir(),"odysseus-native-producer-")),ownedVolumes=[],ownedContainers=[]; + let failure,failed=false;const cleanup=[]; + const volume=label=>{const name=`brain-kit-native-${session}-${label}`;dockerChecked(["volume","create",name]);ownedVolumes.push(name);return name;}; try{ - const executable=join(state,"bun"); + const runtimeInput=join(state,"runtime");mkdirSync(runtimeInput); + const executable=join(runtimeInput,"bun"); // Mount exactly the captured bytes identified by runtime.bun, rather than // rereading a host pathname after validation. The private file has no writer. writeFileSync(executable,runtime.bytes,{mode:0o755,flag:"wx"}); @@ -43,18 +67,35 @@ export function prepareDiagramNativeArtifacts(repo,projects,expected){ for(const name of names){const destination=join(modules,name);mkdirSync(resolve(destination,".."),{recursive:true});cpSync(realpathSync(join(repo,"node_modules",name)),destination,{recursive:true,dereference:false,verbatimSymlinks:true});} } if(JSON.stringify(diagramInstallationIdentity(installed))!==JSON.stringify(record.installation))throw Error("NATIVE_EXPORT_PRIVATE_INSTALL_CHANGED"); + const sourceVolume=volume("source"),modulesVolume=volume("modules"),runtimeVolume=volume("runtime"); + fillPrivateVolume(runtimeInput,runtimeVolume,runtime.image,join(state,"runtime.tar")); + fillPrivateVolume(source,sourceVolume,runtime.image,join(state,"source.tar")); + fillPrivateVolume(modules,modulesVolume,runtime.image,join(state,"modules.tar")); for(const engine of engines){ const destination=join(base,engine); const output=join(state,`output-${engine}`);mkdirSync(output); - const program=`import assert from "node:assert/strict";import {chromium} from "playwright";import {mkdirSync,existsSync} from "node:fs";assert(!existsSync(${JSON.stringify(join(repo,"README.md"))}),"NATIVE_PRODUCER_ORIGINAL_REPO_UNMOUNTED");assert(!existsSync(${JSON.stringify(canary)}),"NATIVE_PRODUCER_HOST_STATE_UNMOUNTED");mkdirSync("/tmp/home",{recursive:true});mkdirSync("/tmp/work");const child=Bun.spawn(["/usr/local/bin/bun","--preload",${JSON.stringify(join(source,"scripts/test-network-child-preload.ts"))},${JSON.stringify(join(source,"packages/module-diagrams/tests/fixtures/export-artifact-probe.ts"))},"/proof"],{cwd:${JSON.stringify(source)},env:{PATH:"/usr/local/bin:/usr/bin:/bin",LANG:"C.UTF-8",TZ:"UTC",HOME:"/tmp/home",TMPDIR:"/tmp/work",PUPPETEER_EXECUTABLE_PATH:chromium.executablePath()},stdout:"inherit",stderr:"inherit"});process.exitCode=await child.exited;`; - const result=spawnSync("docker",["run","--rm","--pull=never","--read-only","--network=none","--shm-size=256m","--user",`${process.getuid()}:${process.getgid()}`,"--tmpfs","/tmp:rw,exec,nosuid,nodev,mode=1777","-v",`${source}:${source}:ro`,"-v",`${modules}:${join(repo,"node_modules")}:ro`,"-v",`${executable}:/usr/local/bin/bun:ro`,"-v",`${output}:/proof`,"-w",source,"-e","HOME=/tmp/home",runtime.image,"/usr/local/bin/bun","-e",program],{encoding:"utf8",maxBuffer:4*1024*1024}); - if(result.error||result.status!==0||result.signal)throw Error(`Native diagram producer failed: ${result.error??result.signal??result.status}\n${result.stderr}\n${result.stdout}`); + const program=`import assert from "node:assert/strict";import {chromium} from "playwright";import {mkdirSync,existsSync} from "node:fs";assert(!existsSync(${JSON.stringify(join(repo,"README.md"))}),"NATIVE_PRODUCER_ORIGINAL_REPO_UNMOUNTED");assert(!existsSync(${JSON.stringify(canary)}),"NATIVE_PRODUCER_HOST_STATE_UNMOUNTED");mkdirSync("/tmp/home",{recursive:true});mkdirSync("/tmp/work");const child=Bun.spawn(["/native-runtime/bun","--preload",${JSON.stringify(join(source,"scripts/test-network-child-preload.ts"))},${JSON.stringify(join(source,"packages/module-diagrams/tests/fixtures/export-artifact-probe.ts"))},"/proof"],{cwd:${JSON.stringify(source)},env:{PATH:"/native-runtime:/usr/local/bin:/usr/bin:/bin",LANG:"C.UTF-8",TZ:"UTC",HOME:"/tmp/home",TMPDIR:"/tmp/work",PUPPETEER_EXECUTABLE_PATH:chromium.executablePath()},stdout:"inherit",stderr:"inherit"});process.exitCode=await child.exited;`; + const outputVolume=volume(`output-${engine}`); + fillPrivateVolume(output,outputVolume,runtime.image,join(state,`output-${engine}.tar`)); + const container=`brain-kit-native-${session}-${engine}`; + dockerChecked(diagramNativeDockerArgs(repo,source,runtime,{source:sourceVolume,modules:modulesVolume,output:outputVolume,runtime:runtimeVolume},container,program)); + ownedContainers.push(container); + const result=dockerChecked(["start","--attach",container]); + const code=Number(dockerChecked(["inspect","--format","{{.State.ExitCode}}",container]).stdout.trim()); + if(code!==0)throw Error(`Native diagram producer failed (${code})\n${result.stderr}\n${result.stdout}`); + dockerChecked(["cp",`${container}:/proof/.`,output]); const artifacts=JSON.parse(result.stdout),files=Object.fromEntries(["svg","png","pdf"].map(format=>{const name=`release.${format}`;return[name,hash(readFileSync(join(output,name)))];})); // Publish only after the actual three-format process and all descendants // have drained successfully. The browser never manufactures this receipt. writeFileSync(join(output,"producer.json"),JSON.stringify({version:1,session,engine,checkout:record.checkout,source:record.sourceIdentity,installation:record.installation.sha256,runtime:{image:runtime.image,bun:runtime.bun,sha256:runtime.sha256},artifacts,files})); const staged=mkdtempSync(join(base,"prepared-"));cpSync(output,staged,{recursive:true});renameSync(staged,destination); } - }finally{rmSync(state,{recursive:true,force:true});} + }catch(error){failure=error;failed=true;}finally{ + for(const container of ownedContainers){const result=spawnSync("docker",["rm","--force",container],{encoding:"utf8"});if(result.status!==0&&!/No such container/.test(result.stderr??""))cleanup.push(Error(`Native diagram container cleanup failed: ${container}`));} + for(const name of ownedVolumes){const result=spawnSync("docker",["volume","rm",name],{encoding:"utf8"});if(result.status!==0)cleanup.push(Error(`Native diagram volume cleanup failed: ${name}`));} + try{rmSync(state,{recursive:true,force:true});}catch(error){cleanup.push(error);} + } + if(cleanup.length)throw new AggregateError([...(failed?[failure]:[]),...cleanup],"Native diagram owned-resource cleanup failed"); + if(failed)throw failure; return session; } diff --git a/scripts/native-proof/stable-run.sh b/scripts/native-proof/stable-run.sh new file mode 100644 index 000000000..00d8844ec --- /dev/null +++ b/scripts/native-proof/stable-run.sh @@ -0,0 +1,45 @@ +# Container-only projection of one run from a stable host directory bind. +set -eu +run=$1 +kind=$2 +shift 2 +case "$run" in run-*) ;; *) echo 'Invalid native proof run name' >&2; exit 2 ;; esac +case "$run" in *[!a-zA-Z0-9_-]*) echo 'Invalid native proof run name' >&2; exit 2 ;; esac +mkdir /proof +mount --bind "/proof-runs/$run" /proof +if [ "$kind" = identity ]; then + mkdir /fixture-variants + mount --bind "/fixture-runs/$run" /fixture-variants + mount -o remount,bind,ro /fixture-variants + readonly_file() { + if [ ! -e "$2" ]; then touch "$2"; fi + mount --bind "/fixture-variants/$1" "$2" + mount -o remount,bind,ro "$2" + } + readonly_file identity-helper /fixture-helper + readonly_file helper.py /helper.py + readonly_file filesystem-filter.py /filter.py + readonly_file setup.sh /setup.sh + boot=false + for variant in "$@"; do + [ "$variant" != -- ] || break + [ "$variant" != boot ] || boot=true + done + while [ "$1" != -- ]; do + case "$1" in + db) readonly_file db-repeat-output.fixture.ts /repo/packages/ui-sdk/src/server/worker-native-output.ts ;; + boot) readonly_file boot-scratch-prune.fixture.ts /repo/packages/ui-server/src/cron/scratch-prune.ts ;; + prune) + if [ "$boot" = false ]; then readonly_file scratch-prune.fixture.ts /repo/packages/ui-server/src/cron/scratch-prune.ts; fi + readonly_file host-boundary.fixture.ts /repo/packages/ui-server/src/brain/host-boundary.ts ;; + inventory) readonly_file maintenance-git.fixture.ts /repo/packages/ui-server/src/brain/maintenance-git.ts ;; + *) echo 'Invalid identity proof variant' >&2; exit 2 ;; + esac + shift + done + shift +elif [ "$kind" != host ]; then + echo 'Invalid native proof kind' >&2 + exit 2 +fi +exec "$@" diff --git a/scripts/prove-native-host.ts b/scripts/prove-native-host.ts index 7b2f63eed..ab15bcda0 100644 --- a/scripts/prove-native-host.ts +++ b/scripts/prove-native-host.ts @@ -1,11 +1,26 @@ /** Required offline Btrfs/Unix-manager hosted proof; missing prerequisites fail. */ import { chmodSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; import { verifyIndexReleaseReproduction } from "./native-proof/verify-index-release-repro.js"; export const NATIVE_PROOF_IMAGE = "brain-kit-native-runtime:1"; export const NATIVE_PROOF_NAME = "native managed hosted admission"; +export function nativeHostDockerArgs(root: string, artifact: string, uid: string, backend: string, apparmor: boolean, retainIndexNamespace: boolean, bun: string): string[] { + const lease = join(root, "tmp", "workspace-operation"); + return ["docker", "run", "--rm", "--pull=never", "--network=none", + ...(retainIndexNamespace ? ["--env=BRAIN_NATIVE_PROOF_RETAIN_INDEX_NAMESPACE=1"] : []), + "--cap-drop=ALL", ...(uid === "0" ? ["--cap-add=SETFCAP"] : []), ...["SYS_ADMIN", "MKNOD", "DAC_OVERRIDE", "FOWNER", "KILL", "CHOWN", "SETUID", "SETGID"].map(value => `--cap-add=${value}`), + "--security-opt=no-new-privileges", `--security-opt=seccomp=${join(root, "scripts/native-proof/seccomp.json")}`, + ...(apparmor ? ["--security-opt=apparmor=brain-kit-native-proof"] : []), + "--security-opt=systempaths=unconfined", "--device=/dev/loop-control", "--device-cgroup-rule=b 7:1790 rwm", + "--mount", `type=bind,source=${dirname(artifact)},target=/proof-runs`, + "--mount", `type=bind,source=${bun},target=/usr/local/bin/bun,readonly`, + "--mount", `type=bind,source=${root},target=/repo,readonly`, + "--mount", `type=bind,source=${lease},target=/repo/tmp/workspace-operation`, + "--workdir=/repo", "--entrypoint=/bin/sh", NATIVE_PROOF_IMAGE, "/repo/scripts/native-proof/stable-run.sh", basename(artifact), "host", "python3", "/repo/scripts/native-proof/fixture.py", backend, uid]; +} + if (import.meta.main) { try { if (process.platform !== "linux" || process.arch !== "x64") throw Error("Native hosted proof requires its qualified Linux x86_64 runtime."); @@ -33,17 +48,7 @@ if (import.meta.main) { writeFileSync(join(artifact, "native-environment.json"), JSON.stringify({ backend, appUid: Number(uid), apparmor, profile: apparmor ? "brain-kit-native-proof" : null })); if (!apparmor) console.log("AppArmor unavailable: this runtime provides no enforced AppArmor qualification."); const lease = join(root, "tmp", "workspace-operation"); mkdirSync(lease, { recursive: true, mode: 0o700 }); - const args = ["docker", "run", "--rm", "--pull=never", "--network=none", - ...(retainIndexNamespace ? ["--env=BRAIN_NATIVE_PROOF_RETAIN_INDEX_NAMESPACE=1"] : []), - "--cap-drop=ALL", ...(uid === "0" ? ["--cap-add=SETFCAP"] : []), ...["SYS_ADMIN", "MKNOD", "DAC_OVERRIDE", "FOWNER", "KILL", "CHOWN", "SETUID", "SETGID"].map(value => `--cap-add=${value}`), - "--security-opt=no-new-privileges", `--security-opt=seccomp=${join(root, "scripts/native-proof/seccomp.json")}`, - ...(apparmor ? ["--security-opt=apparmor=brain-kit-native-proof"] : []), - "--security-opt=systempaths=unconfined", "--device=/dev/loop-control", "--device-cgroup-rule=b 7:1790 rwm", - "--mount", `type=bind,source=${artifact},target=/proof`, - "--mount", `type=bind,source=${realpathSync(process.execPath)},target=/usr/local/bin/bun,readonly`, - "--mount", `type=bind,source=${root},target=/repo,readonly`, - "--mount", `type=bind,source=${lease},target=/repo/tmp/workspace-operation`, - "--workdir=/repo", NATIVE_PROOF_IMAGE, "/repo/scripts/native-proof/fixture.py", backend, uid]; + const args = nativeHostDockerArgs(root, artifact, uid, backend, apparmor, retainIndexNamespace, realpathSync(process.execPath)); console.log(`Native ${backend} uid ${uid} hosted proof artifacts: ${artifact}`); const runnerQuota = readFileSync("/proc/sys/user/max_user_namespaces", "utf8"); const child = Bun.spawn(args, { stdin: "ignore", stdout: "inherit", stderr: "inherit" }); diff --git a/scripts/prove-worker-identity.ts b/scripts/prove-worker-identity.ts index 5d89ddf95..14e05a527 100644 --- a/scripts/prove-worker-identity.ts +++ b/scripts/prove-worker-identity.ts @@ -1,7 +1,7 @@ /** Real cross-UID broker proof; fixture elevation exists only in this container. */ import { chmodSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; import { createHash } from "node:crypto"; -import { join, resolve } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; import { applicationQualificationSelected, applicationQualificationScenario, APPLICATION_SKIPPED_CONTROLS } from "../packages/ui-sdk/tests/fixtures/application-qualification.js"; import { ownedPruneVariant, ownedPruneDiagnosticSelected, bootPruneCaptureVariant, bootPruneCaptureSelected } from "./native-proof/owned-prune-variant.js"; import { dbRepeatStageVariant } from "./native-proof/db-repeat-variant.js"; @@ -63,6 +63,20 @@ process.exitCode = code; return { shim, host, hashes: { originalCollector: collectorHash, originalHost: digest(hostSource), shim: digest(shim), variantHost: digest(host) } }; } +export function workerIdentityDockerArgs(source: string, artifact: string, container: string, image: string, apparmor: boolean, variants: { db: boolean; boot: boolean; prune: boolean; inventory: boolean }, bun: string): string[] { + const capabilities = ["SYS_ADMIN", "SYS_CHROOT", "MKNOD", "DAC_OVERRIDE", "FOWNER", "KILL", "CHOWN", "SETUID", "SETGID"]; + return ["docker", "run", "--name", container, "--rm", "--pull=never", "--network=none", "--cap-drop=ALL", ...capabilities.map(cap => `--cap-add=${cap}`), + `--security-opt=seccomp=${join(source, "scripts/native-proof/seccomp.json")}`, + ...(apparmor ? ["--security-opt=apparmor=brain-kit-native-proof"] : []), "--security-opt=systempaths=unconfined", "--device=/dev/loop-control", "--device-cgroup-rule=b 7:1790 rwm", + "--mount", `type=bind,source=${dirname(artifact)},target=/proof-runs`, + "--mount", `type=bind,source=${dirname(artifact)},target=/fixture-runs,readonly`, + "--mount", `type=bind,source=${source},target=/repo,readonly`, + "--mount", `type=bind,source=${bun},target=/usr/local/bin/bun,readonly`, + "--workdir=/repo", "--entrypoint=/bin/sh", image, "/repo/scripts/native-proof/stable-run.sh", basename(artifact), "identity", + ...(variants.db ? ["db"] : []), ...(variants.boot ? ["boot"] : []), ...(variants.prune ? ["prune"] : []), ...(variants.inventory ? ["inventory"] : []), "--", "/bin/sh", "/setup.sh"]; + +} + if (import.meta.main) { try { if (process.platform !== "linux" || process.arch !== "x64") throw Error("Identity proof requires qualified Linux x86_64"); @@ -297,19 +311,7 @@ exit "$case_code" writeFileSync(join(artifact, "environment.json"), JSON.stringify(metadata, null, 2)); writeFileSync(join(artifact, "native-environment.json"), JSON.stringify({ apparmor })); const container = `brain-kit-identity-proof-${crypto.randomUUID()}`; - const argv = ["docker", "run", "--name", container, "--rm", "--pull=never", "--network=none", "--cap-drop=ALL", ...capabilities.map(cap => `--cap-add=${cap}`), - `--security-opt=seccomp=${join(source, "scripts/native-proof/seccomp.json")}`, - ...(apparmor ? ["--security-opt=apparmor=brain-kit-native-proof"] : []), "--security-opt=systempaths=unconfined", "--device=/dev/loop-control", "--device-cgroup-rule=b 7:1790 rwm", - "--mount", `type=bind,source=${artifact},target=/proof`, - "--mount", `type=bind,source=${source},target=/repo,readonly`, - ...(dbVariant ? ["--mount", `type=bind,source=${join(artifact, "db-repeat-output.fixture.ts")},target=/repo/packages/ui-sdk/src/server/worker-native-output.ts,readonly`] : []), - ...(bootVariant ? ["--mount", `type=bind,source=${join(artifact, "boot-scratch-prune.fixture.ts")},target=/repo/packages/ui-server/src/cron/scratch-prune.ts,readonly`] : []), - ...(pruneVariant ? [...(!bootVariant ? ["--mount", `type=bind,source=${join(artifact, "scratch-prune.fixture.ts")},target=/repo/packages/ui-server/src/cron/scratch-prune.ts,readonly`] : []), - "--mount", `type=bind,source=${join(artifact, "host-boundary.fixture.ts")},target=/repo/packages/ui-server/src/brain/host-boundary.ts,readonly`] : []), - ...(inventoryVariant ? ["--mount", `type=bind,source=${join(artifact, "maintenance-git.fixture.ts")},target=/repo/packages/ui-server/src/brain/maintenance-git.ts,readonly`] : []), - "--mount", `type=bind,source=${realpathSync(process.execPath)},target=/usr/local/bin/bun,readonly`, - "--mount", `type=bind,source=${helper},target=/fixture-helper,readonly`, "--mount", `type=bind,source=${helperScriptPath},target=/helper.py,readonly`, "--mount", `type=bind,source=${filter},target=/filter.py,readonly`, - "--mount", `type=bind,source=${setup},target=/setup.sh,readonly`, "--mount", `type=bind,source=${artifact},target=/fixture-variants,readonly`, "--workdir=/repo", "--entrypoint=/bin/sh", image, "/setup.sh"]; + const argv = workerIdentityDockerArgs(source, artifact, container, image, apparmor, { db: !!dbVariant, boot: !!bootVariant, prune: !!pruneVariant, inventory: !!inventoryVariant }, realpathSync(process.execPath)); console.log(`Worker identity proof artifacts: ${artifact}`); const child = Bun.spawn(argv, { stdin: "ignore", stdout: "pipe", stderr: "pipe" }); const forward = (signal: NodeJS.Signals) => child.kill(signal); diff --git a/scripts/workspace-lease-container.ts b/scripts/workspace-lease-container.ts new file mode 100644 index 000000000..6ed6e7495 --- /dev/null +++ b/scripts/workspace-lease-container.ts @@ -0,0 +1,8 @@ +import { basename, dirname } from "node:path"; + +/** The lease fixture varies inside one checkout-owned Docker share. */ +export function workspaceLeaseDockerArgs(root: string, image: string, user: string): string[] { + return ["docker", "run", "--rm", "--pull=never", "--network=none", "--ipc=host", + "--user", user, "-v", `${dirname(root)}:/repo`, "-w", `/repo/${basename(root)}`, + image, "node", "scripts/visual.mjs", "--inside", "--project=visual"]; +} diff --git a/tests/docker-bind-stability.test.ts b/tests/docker-bind-stability.test.ts new file mode 100644 index 000000000..408674703 --- /dev/null +++ b/tests/docker-bind-stability.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from "bun:test"; +import { diagramNativeDockerArgs } from "../scripts/diagram-export-producer.mjs"; +import { nativeHostDockerArgs } from "../scripts/prove-native-host.ts"; +import { workerIdentityDockerArgs } from "../scripts/prove-worker-identity.ts"; +import { captureRenderArgs } from "../scripts/capture.ts"; +import { workspaceLeaseDockerArgs } from "../scripts/workspace-lease-container.ts"; + +function bindSources(argv: string[]): string[] { + return argv.flatMap((arg, index) => { + if (arg === "-v") return [argv[index + 1]!.split(":")[0]!]; + if (arg === "--mount" && argv[index + 1]!.startsWith("type=bind,")) { + return [argv[index + 1]!.split(",").find(value => value.startsWith("source="))!.slice(7)]; + } + return []; + }); +} +const repo = "/fixture/repo", bun = "/fixture/bin/bun"; + +test("native host Docker bind sources remain stable across backend/run identities", () => { + const argv = ["run-claude-uid0-first", "run-pi-uid1000-second"].map(run => + nativeHostDockerArgs(repo, `${repo}/tmp/native-host-proof/${run}`, "1000", "pi", false, false, bun)); + expect(bindSources(argv[0]!), "NATIVE_HOST_BIND_SOURCES_STABLE").toEqual(bindSources(argv[1]!)); +}); + +test("worker identity Docker bind sources remain stable including every diagnostic variant", () => { + for (const variants of [{ db: false, boot: false, prune: false, inventory: false }, + { db: true, boot: false, prune: false, inventory: false }, + { db: false, boot: true, prune: true, inventory: false }, + { db: false, boot: false, prune: false, inventory: true }]) { + const argv = ["run-first", "run-second"].map(run => + workerIdentityDockerArgs(repo, `${repo}/tmp/worker-identity-proof/${run}`, run, "native-image", false, variants, bun)); + expect(bindSources(argv[0]!), "WORKER_IDENTITY_BIND_SOURCES_STABLE").toEqual(bindSources(argv[1]!)); + } +}); + +test("capture Docker bind sources remain stable across staging and runner identities", () => { + const argv = ["first", "second"].map(run => captureRenderArgs(repo, "/fixture/fonts", `${repo}/tmp/capture-share/.capture-${run}`, bun, "browser-image", `${repo}/tmp/.capture-runner-${run}`)); + expect(bindSources(argv[0]!), "CAPTURE_BIND_SOURCES_STABLE").toEqual(bindSources(argv[1]!)); +}); + +test("workspace lease Docker bind sources remain stable across fresh fixture identities", () => { + const argv = ["first", "second"].map(run => workspaceLeaseDockerArgs(`${repo}/tmp/workspace-lease-container/run-${run}`, "browser-image", "1000:1000")); + expect(bindSources(argv[0]!), "WORKSPACE_LEASE_BIND_SOURCES_STABLE").toEqual(bindSources(argv[1]!)); +}); + +test("native diagram producer copies private inputs without binding fresh host paths", () => { + const argv = ["first", "second"].map(run => diagramNativeDockerArgs(repo, `/tmp/producer-${run}/source`, { image: "browser-image" }, + { source: `source-${run}`, modules: `modules-${run}`, output: `output-${run}`, runtime: `runtime-${run}` }, `container-${run}`, "fixture program")); + expect(bindSources(argv[0]!), "NATIVE_DIAGRAM_BIND_SOURCES_STABLE").toEqual(bindSources(argv[1]!)); + expect(bindSources(argv[0]!)).toEqual([]); + for (const args of argv) { + expect(args).toContain("--read-only"); expect(args).toContain("--network=none"); + expect(args.filter(value => value.includes(",readonly"))).toHaveLength(3); + } +}); diff --git a/tests/workspace-lease.test.ts b/tests/workspace-lease.test.ts index 99db2c5c0..7aa269c34 100644 --- a/tests/workspace-lease.test.ts +++ b/tests/workspace-lease.test.ts @@ -3,6 +3,7 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; +import { workspaceLeaseDockerArgs } from "../scripts/workspace-lease-container.ts"; import { resolveBrowserPin } from "../scripts/captures/provenance.ts"; const project = resolve(import.meta.dir, ".."); @@ -16,8 +17,9 @@ afterEach(async () => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); }); -function fixture(leaseHelper = helper, pauseBeforeRemoval = false) { - const root = mkdtempSync(join(tmpdir(), "brain-workspace-lease-")); dirs.push(root); +function fixture(leaseHelper = helper, pauseBeforeRemoval = false, parent = tmpdir()) { + mkdirSync(parent, { recursive: true }); + const root = mkdtempSync(join(parent, "brain-workspace-lease-")); dirs.push(root); mkdirSync(join(root, "dist")); writeFileSync(join(root, "package.json"), JSON.stringify({ name: "odysseus-build-fixture", type: "module", exports: "./dist/index.js" })); writeFileSync(join(root, "dist/index.js"), 'export const captain = "Odysseus";\n'); @@ -213,15 +215,13 @@ const spawn = (...args) => { }); test("the real pinned container wrapper waits for host ownership of its mounted checkout", async () => { - const root = fixture(); + const root = fixture(helper, false, join(project, "tmp", "workspace-lease-container")); for (const path of ["scripts", "packages/ui-kit", "node_modules/vitest"]) mkdirSync(join(root, path), { recursive: true }); for (const name of ["visual.mjs", "workspace-lease.mjs", "prepare-diagram-export-source.mjs", "diagram-export-source.mjs", "diagram-export-installation.mjs", "diagram-export-producer.mjs"]) copyFileSync(join(project, "scripts", name), join(root, "scripts", name)); - writeFileSync(join(root, "node_modules/vitest/vitest.mjs"), 'import {writeFileSync} from "node:fs";writeFileSync("/repo/vitest-entered","native container acquired ownership\\n");\n'); + writeFileSync(join(root, "node_modules/vitest/vitest.mjs"), 'import {writeFileSync} from "node:fs";writeFileSync(new URL("../../vitest-entered",import.meta.url),"native container acquired ownership\\n");\n'); const owner = start(root, "write", "hold", "host"); await entered(root, "host"); - const container = Bun.spawn(["docker", "run", "--rm", "--pull=never", "--network=none", "--ipc=host", - "--user", `${process.getuid!()}:${process.getgid!()}`, "-v", `${root}:/repo`, "-w", "/repo", - (await resolveBrowserPin(project)).image, "node", "scripts/visual.mjs", "--inside", "--project=visual"], + const container = Bun.spawn(workspaceLeaseDockerArgs(root, (await resolveBrowserPin(project)).image, `${process.getuid!()}:${process.getgid!()}`), { stdin: "ignore", stdout: "pipe", stderr: "pipe" }); children.push(container); const stdout = new Response(container.stdout).text(); From 972e0004d5a0b3d08ebc4b8d61431adc120927e5 Mon Sep 17 00:00:00 2001 From: Alain Schlesser Date: Tue, 20 Oct 2026 04:14:21 +0200 Subject: [PATCH 2/2] test(ci): model native diagram volume transport in runner fixture --- tests/visual-runner.test.ts | 104 ++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 10 deletions(-) diff --git a/tests/visual-runner.test.ts b/tests/visual-runner.test.ts index 513f91fe0..536fb9298 100644 --- a/tests/visual-runner.test.ts +++ b/tests/visual-runner.test.ts @@ -1,5 +1,5 @@ import { afterEach, expect, test } from "bun:test"; -import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, resolve } from "node:path"; import { diagramSourceSetup, diagramSourceDomains } from "../scripts/diagram-export-source.mjs"; @@ -14,7 +14,7 @@ afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); -async function invoke(args: string[], exit = 0, imageState: "warm"|"cold"|"drift" = "warm") { +async function invoke(args: string[], exit = 0, imageState: "warm"|"cold"|"drift" = "warm", nativeExit = 0) { const root = await mkdtemp(resolve(tmpdir(), "brain-visual-runner-")); roots.push(root); for (const path of ["scripts/captures", "packages/ui-kit", "node_modules/vitest", "commands"]) { @@ -44,6 +44,9 @@ async function invoke(args: string[], exit = 0, imageState: "warm"|"cold"|"drift const flock = Bun.which("flock"); if (!flock) throw new Error("visual runner tests require flock"); await symlink(flock, resolve(root, "commands/flock")); + const tar = Bun.which("tar"); + if (!tar) throw new Error("visual runner volume transport requires tar"); + await symlink(tar, resolve(root, "commands/tar")); await writeFile(resolve(root, "scripts/captures/font-lock.json"), "{}\n"); await writeFile(resolve(root, "node_modules/vitest/package.json"), JSON.stringify({name:"vitest",version:"1.0.0"})); const log = resolve(root, "calls.jsonl"); @@ -65,13 +68,63 @@ async function invoke(args: string[], exit = 0, imageState: "warm"|"cold"|"drift {env: process.env, stdio: "inherit"}); process.exit(child.status ?? 92);` : ""; const nativeRouting = command === "docker" ? ` - import {mkdirSync,writeFileSync} from "node:fs"; - import {join} from "node:path"; - const nativeArgs=process.argv.slice(2); - if(nativeArgs[0]==="run"&&nativeArgs.some(value=>value.endsWith(":/proof"))){ - const mount=nativeArgs.find(value=>value.endsWith(":/proof"));const output=mount.slice(0,-7);mkdirSync(output,{recursive:true}); - for(const format of ["svg","png","pdf"])writeFileSync(join(output,"release."+format),"owned wrapper routing output"); - console.log("[]");process.exit(0); + import * as nativeFs from "node:fs"; + import {join as nativeJoin} from "node:path"; + import {spawnSync as nativeSpawn} from "node:child_process"; + import assert from "node:assert/strict"; + const nativeArgs=process.argv.slice(2), nativeRoot=process.env.RUNNER_ROOT; + const store=nativeJoin(nativeRoot,"docker-state"); + const volumePath=name=>{assert(/^brain-kit-native-[a-z0-9-]+$/.test(name));return nativeJoin(store,"volumes",name);}; + const containerPath=name=>{assert(/^brain-kit-native-[a-z0-9-]+$/.test(name));return nativeJoin(store,"containers",name);}; + const nativeRecord=()=>nativeFs.appendFileSync(nativeJoin(nativeRoot,"native-calls.jsonl"),JSON.stringify(nativeArgs)+"\\n"); + const mounts=()=>nativeArgs.flatMap((arg,index)=>arg==="--mount"?[Object.fromEntries(nativeArgs[index+1].split(",").map(part=>part.split("=")))]:[]); + const finish=()=>{nativeRecord();process.exit(0);}; + if(nativeArgs[0]==="volume"){ + const location=volumePath(nativeArgs[2]); + if(nativeArgs[1]==="create")nativeFs.mkdirSync(location,{recursive:true}); + else {assert.equal(nativeArgs[1],"rm");nativeFs.rmSync(location,{recursive:true});} + finish(); + } + if(nativeArgs[0]==="run"&&nativeArgs.includes("/inputs")){ + for(const flag of ["--rm","--interactive","--pull=never","--network=none","--read-only"])assert(nativeArgs.includes(flag)); + const [mount]=mounts();assert.equal(mount.type,"volume");assert.equal(mount.target,"/inputs"); + assert(nativeArgs.includes(imageRef)); + const unpack=nativeSpawn("tar",["-xf","-","-C",volumePath(mount.source)],{stdio:[0,"pipe","pipe"]}); + assert.equal(unpack.status,0,unpack.stderr?.toString());finish(); + } + if(nativeArgs[0]==="create"){ + for(const flag of ["--pull=never","--network=none","--read-only","--user","--tmpfs"])assert(nativeArgs.includes(flag)); + assert(nativeArgs.includes(imageRef));assert(nativeArgs.includes("/native-runtime/bun")); + const mounted=mounts();assert.equal(mounted.length,4); + for(const mount of mounted){assert.equal(mount.type,"volume");assert(nativeFs.statSync(volumePath(mount.source)).isDirectory());} + const source=mounted.find(mount=>mount.target===nativeArgs[nativeArgs.indexOf("-w")+1]); + const runtime=mounted.find(mount=>mount.target==="/native-runtime"); + const modules=mounted.find(mount=>mount.target===nativeJoin(nativeRoot,"node_modules")); + assert(source&&runtime&&modules);for(const mount of [source,runtime,modules])assert("readonly" in mount); + assert(nativeFs.readFileSync(nativeJoin(volumePath(source.source),"scripts/visual.mjs")).equals(nativeFs.readFileSync(nativeJoin(nativeRoot,"scripts/visual.mjs")))); + assert(!nativeFs.existsSync(nativeJoin(volumePath(source.source),"README.md"))); + assert(!nativeFs.existsSync(nativeJoin(volumePath(source.source),"ambient-canary"))); + assert(nativeFs.readFileSync(nativeJoin(volumePath(runtime.source),"bun")).equals(nativeFs.readFileSync(nativeJoin(nativeRoot,"commands/bun")))); + assert.equal(JSON.parse(nativeFs.readFileSync(nativeJoin(volumePath(modules.source),"vitest/package.json"))).name,"vitest"); + const name=nativeArgs[nativeArgs.indexOf("--name")+1]; + nativeFs.mkdirSync(nativeJoin(store,"containers"),{recursive:true}); + nativeFs.writeFileSync(containerPath(name),JSON.stringify({mounted,exit:null}));finish(); + } + if(["start","inspect","cp","rm"].includes(nativeArgs[0])){ + const name=nativeArgs[0]==="cp"?nativeArgs[1].split(":")[0]:nativeArgs.at(-1); + const location=containerPath(name),state=JSON.parse(nativeFs.readFileSync(location)); + const output=volumePath(state.mounted.find(mount=>mount.target==="/proof").source); + if(nativeArgs[0]==="start"){ + assert.deepEqual(nativeArgs.slice(0,2),["start","--attach"]); + for(const format of ["svg","png","pdf"])nativeFs.writeFileSync(nativeJoin(output,"release."+format),"owned volume output "+format); + state.exit=Number(process.env.NATIVE_EXIT);nativeFs.writeFileSync(location,JSON.stringify(state));console.log("[]"); + }else if(nativeArgs[0]==="inspect"){ + assert.equal(nativeArgs[2],"{{.State.ExitCode}}");assert.notEqual(state.exit,null);console.log(state.exit); + }else if(nativeArgs[0]==="cp"){ + assert.equal(nativeArgs[1],name+":/proof/.");assert.notEqual(state.exit,null); + nativeFs.cpSync(output,nativeArgs[2],{recursive:true}); + }else {assert.equal(nativeArgs[1],"--force");nativeFs.rmSync(location);} + finish(); } ` : ""; const bootstrap=command==="docker"?` @@ -93,7 +146,7 @@ async function invoke(args: string[], exit = 0, imageState: "warm"|"cold"|"drift } const child = Bun.spawn([node!, resolve(root, "scripts/visual.mjs"), ...args], { cwd: root, - env: { ...gitEnv, RUNNER_ROOT: root, RUNNER_LOG: log, RUNNER_EXIT: String(exit) }, + env: { ...gitEnv, RUNNER_ROOT: root, RUNNER_LOG: log, RUNNER_EXIT: String(exit), NATIVE_EXIT: String(nativeExit) }, stdout: "pipe", stderr: "pipe", }); const [code, stdout, stderr] = await Promise.all([ @@ -206,3 +259,34 @@ test("a mismatching manifest is refused before native source preparation",async( expect(result.calls).toEqual([]); expect(await Bun.file(resolve(result.root,"tmp/diagram-export-source.json")).exists()).toBe(false); }); + + +test("native volume handoff publishes all formats and drains only owned resources", async () => { + const result = await invoke(["--project=diagrams-chromium", "--project=diagrams-webkit"]); + expect(result.code, result.stderr).toBe(0); + const prepared = resolve(result.root, "tmp/diagram-export-native-prepared"); + const [session] = await readdir(prepared); + for (const engine of ["chromium", "webkit"]) { + const output = resolve(prepared, session!, engine); + const receipt = JSON.parse(await readFile(resolve(output, "producer.json"), "utf8")); + for (const format of ["svg", "png", "pdf"]) { + const bytes = await readFile(resolve(output, `release.${format}`)); + expect(bytes.toString(), "COPIED_VOLUME_OUTPUT_REACHES_BROWSER_RECEIPT").toBe(`owned volume output ${format}`); + expect(receipt.files[`release.${format}`]).toBe(new Bun.CryptoHasher("sha256").update(bytes).digest("hex")); + } + } + expect(result.calls.at(-1)!.command).toBe("vitest"); + expect(await readdir(resolve(result.root, "docker-state/volumes"))).toEqual([]); + expect(await readdir(resolve(result.root, "docker-state/containers"))).toEqual([]); +}); + +test("native volume producer failure blocks browser execution and still drains resources", async () => { + const result = await invoke(["--project=diagrams-chromium"], 0, "warm", 17); + expect(result.code, "NATIVE_FAILURE_MUST_BLOCK_BROWSER").not.toBe(0); + expect(result.stderr).toContain("Native diagram producer failed (17)"); + expect(result.calls.some(call => call.command === "vitest")).toBe(false); + const calls = (await readFile(resolve(result.root, "native-calls.jsonl"), "utf8")).trim().split("\n").map(line => JSON.parse(line)); + expect(calls.some(args => args[0] === "cp")).toBe(false); + expect(await readdir(resolve(result.root, "docker-state/volumes"))).toEqual([]); + expect(await readdir(resolve(result.root, "docker-state/containers"))).toEqual([]); +});