|
| 1 | +import assert from "node:assert/strict" |
| 2 | +import { createHash } from "node:crypto" |
| 3 | +import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" |
| 4 | +import { tmpdir } from "node:os" |
| 5 | +import { join } from "node:path" |
| 6 | + |
| 7 | +import { startPlaygroundCliServer, type PlaygroundCliModule } from "../packages/runtime-playground/src/playground-cli-runner.js" |
| 8 | +import type { RuntimeCreateSpec } from "../packages/runtime-core/src/index.js" |
| 9 | + |
| 10 | +const root = await mkdtemp(join(tmpdir(), "wp-codebox-readonly-mounts-")) |
| 11 | +const readonlySource = join(root, "readonly.bin") |
| 12 | +const readwriteSource = join(root, "readwrite.bin") |
| 13 | +const readonlyBytes = Buffer.from([0, 255, 1, 2, 3, 127, 128]) |
| 14 | +const readwriteBytes = Buffer.from([10, 20, 30]) |
| 15 | +await writeFile(readonlySource, readonlyBytes) |
| 16 | +await writeFile(readwriteSource, readwriteBytes) |
| 17 | + |
| 18 | +const spec: RuntimeCreateSpec = { |
| 19 | + backend: "wordpress-playground", |
| 20 | + environment: { version: "6.8", phpVersion: "8.4", blueprint: {} }, |
| 21 | + policy: { network: "deny", filesystem: "readwrite-mounts", commands: ["wordpress.run-php"], secrets: "none", approvals: "never" }, |
| 22 | +} |
| 23 | + |
| 24 | +let mountedReadonlyPath = "" |
| 25 | +const cliModule: PlaygroundCliModule = { |
| 26 | + async runCLI(options) { |
| 27 | + const readonlyMount = options.mount.find((mount) => mount.vfsPath === "/readonly") |
| 28 | + const readwriteMount = options.mount.find((mount) => mount.vfsPath === "/readwrite") |
| 29 | + assert.ok(readonlyMount) |
| 30 | + assert.ok(readwriteMount) |
| 31 | + mountedReadonlyPath = readonlyMount.hostPath |
| 32 | + // This is the host path Playground's writable Node mount handler receives. |
| 33 | + await writeFile(readonlyMount.hostPath, Buffer.from("sandbox overwrite")) |
| 34 | + await writeFile(readwriteMount.hostPath, Buffer.from("sandbox overwrite")) |
| 35 | + return { |
| 36 | + serverUrl: "http://127.0.0.1:65535", |
| 37 | + playground: { async run() { return { text: "" } } }, |
| 38 | + async [Symbol.asyncDispose]() {}, |
| 39 | + } |
| 40 | + }, |
| 41 | +} |
| 42 | + |
| 43 | +try { |
| 44 | + const beforeReadonlyHash = sha256(await readFile(readonlySource)) |
| 45 | + const server = await startPlaygroundCliServer(spec, [ |
| 46 | + { type: "file", source: readonlySource, target: "/readonly", mode: "readonly" }, |
| 47 | + { type: "file", source: readwriteSource, target: "/readwrite", mode: "readwrite" }, |
| 48 | + ], { cliModule }) |
| 49 | + |
| 50 | + assert.equal(sha256(await readFile(readonlySource)), beforeReadonlyHash, "readonly source bytes must survive a sandbox overwrite") |
| 51 | + assert.deepEqual(await readFile(readwriteSource), Buffer.from("sandbox overwrite"), "readwrite mounts must retain host-write behavior") |
| 52 | + assert.notEqual(mountedReadonlyPath, readonlySource, "readonly mounts must use a private staged path") |
| 53 | + |
| 54 | + await server[Symbol.asyncDispose]() |
| 55 | + await assert.rejects(access(mountedReadonlyPath), /ENOENT/, "readonly mount staging must be removed with the runtime") |
| 56 | +} finally { |
| 57 | + await rm(root, { recursive: true, force: true }) |
| 58 | +} |
| 59 | + |
| 60 | +function sha256(bytes: Buffer): string { |
| 61 | + return createHash("sha256").update(bytes).digest("hex") |
| 62 | +} |
| 63 | + |
| 64 | +console.log("playground readonly mount isolation ok") |
0 commit comments