From 14a80d97570645d870c76f6fadf811ba9345e387 Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Fri, 14 Aug 2026 13:18:20 +0700 Subject: [PATCH] feat(sandbox): mount extra virtiofs tags from mounts.json in the guest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit codev-init now reads /codev-config/mounts.json (written by the vz provider every start) and mounts each listed tag, non-fatally per entry so one bad share cannot brick the boot. This is the one-time image change: all future mount-list changes are host-side manifest writes. Compatibility is safe in both directions — an old image ignores the extra shares (they sit unmounted and inert), and a new image without a manifest skips the block. Existing macOS sandboxes pick the new init up via Reset, which re-provisions from the current image. e2e-vz.ts gains a mounts probe: the guest reads a marker from /mnt/extra and writes back into it, exercising both directions. Co-Authored-By: Claude Fable 5 --- packages/codev-sandbox/image/guest/codev-init | 11 +++++ packages/codev-sandbox/script/e2e-vz.ts | 46 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/codev-sandbox/image/guest/codev-init b/packages/codev-sandbox/image/guest/codev-init index 3f9d76338a..68c3fa4bf6 100644 --- a/packages/codev-sandbox/image/guest/codev-init +++ b/packages/codev-sandbox/image/guest/codev-init @@ -16,6 +16,17 @@ mount -t tmpfs tmpfs /tmp mount -t virtiofs workspace /workspace mount -t virtiofs bundle /opt/codev mount -t virtiofs config /codev-config +# Extra host folders arrive as additional virtiofs tags listed in mounts.json, +# which the host rewrites on every start. Tags and guest paths are sanitized +# host-side (no spaces or quotes); a failed mount is logged and skipped so one +# bad share cannot brick the boot. +if [ -f /codev-config/mounts.json ]; then + node -e ' + for (const m of require("/codev-config/mounts.json").mounts ?? []) console.log(m.tag, m.path) + ' | while read -r tag path; do + mkdir -p "$path" && mount -t virtiofs "$tag" "$path" || echo "codev-init: mount $tag at $path failed" >&2 + done +fi ip link set lo up [ -f /etc/codev-guest.env ] && . /etc/codev-guest.env diff --git a/packages/codev-sandbox/script/e2e-vz.ts b/packages/codev-sandbox/script/e2e-vz.ts index 34287b52d1..e099d54db8 100644 --- a/packages/codev-sandbox/script/e2e-vz.ts +++ b/packages/codev-sandbox/script/e2e-vz.ts @@ -24,6 +24,11 @@ const work = await mkdtemp(join(tmpdir(), "codev-vz-e2e-")) const projectDir = argValue("--project") ?? join(work, "project") await mkdir(projectDir, { recursive: true }) await writeFile(join(projectDir, "hello.txt"), "hello from the host project\n") +// A second host folder mounted on top of the project dir, to prove the +// mounts.json path: the guest must see it at /mnt/extra. +const extraDir = join(work, "extra") +await mkdir(extraDir, { recursive: true }) +await writeFile(join(extraDir, "marker.txt"), "extra-mount-ok\n") const payload = Bun.file(image) const hash = createHash("sha256") @@ -55,7 +60,13 @@ const provider = createVzProvider({ }) console.log("availability:", await provider.availability()) -const config: SandboxConfig = { id: "sandbox:e2e", name: "e2e", backend: "vz", projectDir } +const config: SandboxConfig = { + id: "sandbox:e2e", + name: "e2e", + backend: "vz", + projectDir, + mounts: [{ path: extraDir }], +} console.time("create") await provider.create(config) @@ -106,6 +117,37 @@ const brokerOk = await (async () => { })() console.log(`broker: ${brokerOk ? "OK" : "FAILED"}`) +// Extra mount: the guest reads the marker from /mnt/extra (mounted by +// codev-init from mounts.json) and writes proof both into the workspace and +// back into the extra mount itself, exercising read and write. +console.log(`guest mounts: ${JSON.stringify(sandbox.mounts)}`) +await fetch(`${sandbox.url}/api/pty`, { + method: "POST", + headers: { authorization: auth, "content-type": "application/json" }, + body: JSON.stringify({ + command: "/bin/sh", + args: [ + "-c", + "cat /mnt/extra/marker.txt > /workspace/mount-probe.txt 2>&1; echo written-from-guest > /mnt/extra/guest-write.txt 2>/dev/null", + ], + cwd: "/workspace", + }), +}).catch(() => null) +const mountOk = await (async () => { + for (let attempt = 0; attempt < 30; attempt++) { + const read = await Bun.file(join(projectDir, "mount-probe.txt")) + .text() + .catch(() => "") + const written = await Bun.file(join(extraDir, "guest-write.txt")) + .text() + .catch(() => "") + if (read.trim() === "extra-mount-ok" && written.trim() === "written-from-guest") return true + await new Promise((resolve) => setTimeout(resolve, 500)) + } + return false +})() +console.log(`extra mount: ${mountOk ? "OK" : "FAILED"}`) + if (hold) { console.log("holding the VM up; Ctrl-C to stop") await new Promise(() => {}) @@ -117,7 +159,7 @@ await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 10_000) await provider.destroy(config) server.stop(true) await rm(work, { recursive: true, force: true }) -if (!healthy || !pathsOk || !brokerOk) process.exit(1) +if (!healthy || !pathsOk || !brokerOk || !mountOk) process.exit(1) console.log("e2e OK") process.exit(0)