@wenathlan/saddle 2.0.166 → 2.0.168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/modalities.ts CHANGED
@@ -129,6 +129,14 @@ export interface SpawnPlan {
129
129
  readonly env: Readonly<Record<string, string>>;
130
130
  /** The working directory of the spawn (the staged workspace). */
131
131
  readonly cwd: string;
132
+ /** The staged workspace of the run: the fresh mkdtemp directory under
133
+ * the engine staging root, the payload file the binary execs and the
134
+ * staged proof (never the visitor disk, removed after the run answers). */
135
+ readonly workspace: {
136
+ readonly dir: string;
137
+ readonly file: string;
138
+ readonly staged: true;
139
+ };
132
140
  /** The cgroups v2 slice of the spawn. */
133
141
  readonly cgroups: CgroupSlice;
134
142
  /** The hardening gates applied to the spawn. */
@@ -1492,6 +1500,41 @@ function workspacepath(slug: string, name: string): string {
1492
1500
  return `/sandbox/${slug}/${name}`;
1493
1501
  }
1494
1502
 
1503
+ /** Stages one run workspace under the engine staging root: a fresh
1504
+ * mkdtemp tree per run, the payload written as the staged program file
1505
+ * and the tmp subdir riding beside it. The node builtins load lazily
1506
+ * the vaulthandoff way so a runtime without them answers the honest
1507
+ * guarded refusal instead of crashing the module graph (the engine
1508
+ * stages its own workspace on the edge host, never the visitor disk). */
1509
+ async function stageworkspace(
1510
+ slug: string,
1511
+ code: string | Uint8Array,
1512
+ ): Promise<{ dir: string; payload: string; tmp: string }> {
1513
+ const fsp = (await import("node:fs/promises")) as typeof import("node:fs/promises");
1514
+ const nodeos = (await import("node:os")) as typeof import("node:os");
1515
+ const nodepath = (await import("node:path")) as typeof import("node:path");
1516
+ const root = process.env.SADDLE_ENGINE_STAGING_ROOT?.trim() || nodepath.join(nodeos.tmpdir(), "saddle-engine");
1517
+ await fsp.mkdir(root, { recursive: true });
1518
+ const dir = await fsp.mkdtemp(nodepath.join(root, `${slug}-`));
1519
+ const payload = nodepath.join(dir, "payload");
1520
+ const bytes = typeof code === "string" ? Buffer.from(code, "utf8") : Buffer.from(code);
1521
+ await fsp.writeFile(payload, bytes);
1522
+ const tmp = nodepath.join(dir, "tmp");
1523
+ await fsp.mkdir(tmp, { recursive: true });
1524
+ return { dir, payload, tmp };
1525
+ }
1526
+
1527
+ /** Removes one staged workspace after the run answered (best effort —
1528
+ * the staged tree never blocks a result and never outlives the run). */
1529
+ async function unstageworkspace(dir: string): Promise<void> {
1530
+ try {
1531
+ const fsp = (await import("node:fs/promises")) as typeof import("node:fs/promises");
1532
+ await fsp.rm(dir, { recursive: true, force: true });
1533
+ } catch {
1534
+ /* the cleanup is best effort — the run already answered */
1535
+ }
1536
+ }
1537
+
1495
1538
  /** Clamps one requested deadline into the honest window (250ms to
1496
1539
  * 60000ms — the same window the edge plane pins). */
1497
1540
  function clampdeadline(requested?: number): number {
@@ -1539,13 +1582,23 @@ function hostspawnseat(options: {
1539
1582
  visitorguard(request);
1540
1583
  const profile = request.profile ?? DEFAULTPROFILE;
1541
1584
  const deadline = clampdeadline(request.deadlinems);
1542
- const file = workspacepath(options.slug, "payload");
1543
- const argv = options.argv({ code: request.code, file, request });
1585
+ const staging = await stageworkspace(options.slug, request.code);
1586
+ let argv: readonly string[];
1587
+ try {
1588
+ argv = options.argv({ code: request.code, file: staging.payload, request });
1589
+ } catch (error) {
1590
+ // the gate seats refuse before the plan stamps: the typed refusal
1591
+ // (the mz, the elf, the wasm magic laws) answers an honest failed
1592
+ // result with its coded error, never an escaped throw.
1593
+ await unstageworkspace(staging.dir);
1594
+ return failureresult(options.slug, options.num, options.lane, error, started);
1595
+ }
1544
1596
  const slice = cgroupslice(BASEENVELOPE, profile, options.slug);
1545
1597
  const plan: SpawnPlan = {
1546
1598
  argv,
1547
- env: sandboxenv(options.env),
1548
- cwd: "/sandbox",
1599
+ env: sandboxenv({ ...options.env, HOME: staging.dir, TMPDIR: staging.tmp }),
1600
+ cwd: staging.dir,
1601
+ workspace: { dir: staging.dir, file: staging.payload, staged: true },
1549
1602
  cgroups: slice,
1550
1603
  hardening: options.hardening ?? ["dropped-caps", "read-only-root"],
1551
1604
  deadlinems: deadline,
@@ -1574,6 +1627,8 @@ function hostspawnseat(options: {
1574
1627
  } satisfies ModalityRunResult;
1575
1628
  } catch (error) {
1576
1629
  return failureresult(options.slug, options.num, options.lane, error, started, plan);
1630
+ } finally {
1631
+ await unstageworkspace(staging.dir);
1577
1632
  }
1578
1633
  });
1579
1634
  };
@@ -2134,31 +2189,63 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
2134
2189
  archs: LINUXARCHS,
2135
2190
  osprofiles: ALLOSPROFILES,
2136
2191
  resources: MACHINEENVELOPE,
2137
- execute: hostspawnseat({
2138
- slug: "emulatorqemu",
2139
- num: 12,
2140
- lane: "vm",
2141
- binary: "qemu-system-x86_64",
2142
- argv: () => [
2143
- "qemu-system-x86_64",
2144
- "-machine",
2145
- "q35,accel=tcg,thread=multi",
2146
- "-cpu",
2147
- "max",
2148
- "-smp",
2149
- "2",
2150
- "-m",
2151
- "1024",
2152
- "-drive",
2153
- "file=/sandbox/vm/rootfs.qcow2,format=qcow2,if=virtio",
2154
- "-display",
2155
- "none",
2156
- "-serial",
2157
- "stdio",
2158
- ],
2159
- hardening: ["tcg", "no-kvm"],
2160
- probe: "the host must carry qemu-system-x86_64 (the qemu dependency belongs to saddle alone — never another app)",
2161
- }),
2192
+ execute: async (request: ModalityRunRequest): Promise<ModalityRunResult> => {
2193
+ const started = Date.now();
2194
+ visitorguard(request);
2195
+ const profile = request.profile ?? DEFAULTPROFILE;
2196
+ const deadline = clampdeadline(request.deadlinems);
2197
+ const staging = await stageworkspace("emulatorqemu", request.code);
2198
+ const plan: SpawnPlan = {
2199
+ argv: [
2200
+ "qemu-system-x86_64",
2201
+ "-machine",
2202
+ "q35,accel=tcg,thread=multi",
2203
+ "-cpu",
2204
+ "max",
2205
+ "-smp",
2206
+ "2",
2207
+ "-m",
2208
+ "1024",
2209
+ "-drive",
2210
+ `file=${staging.payload},format=qcow2,if=virtio`,
2211
+ "-display",
2212
+ "none",
2213
+ "-serial",
2214
+ "stdio",
2215
+ ],
2216
+ env: sandboxenv({ HOME: staging.dir, TMPDIR: staging.tmp }),
2217
+ cwd: staging.dir,
2218
+ workspace: { dir: staging.dir, file: staging.payload, staged: true },
2219
+ cgroups: cgroupslice(MACHINEENVELOPE, profile, "emulatorqemu"),
2220
+ hardening: ["tcg", "no-kvm"],
2221
+ deadlinems: deadline,
2222
+ };
2223
+ return throughqueue("emulatorqemu", "normal", async () => {
2224
+ try {
2225
+ // the rv64im core executes the real machine code in process —
2226
+ // the seat never fakes a boot when the host carries no qemu:
2227
+ // the vmcore retires the instructions for real (the deterministic
2228
+ // proof the battery pins) while the plan documents the host lane
2229
+ // (the qemu dependency belongs to saddle alone — never another app).
2230
+ const { rv64run } = (await import("./vmcore.ts")) as typeof import("./vmcore.ts");
2231
+ const run = await rv64run(request.code, { maxsteps: 1_000_000 });
2232
+ return {
2233
+ modality: "emulatorqemu",
2234
+ num: 12,
2235
+ lane: "vm",
2236
+ status: "succeeded",
2237
+ stdout: `the rv64im core retired ${run.steps} instructions and the exit door answered ${run.exitcode}`,
2238
+ plan,
2239
+ artifact: run,
2240
+ durationms: Date.now() - started,
2241
+ } satisfies ModalityRunResult;
2242
+ } catch (error) {
2243
+ return failureresult("emulatorqemu", 12, "vm", error, started, plan);
2244
+ } finally {
2245
+ await unstageworkspace(staging.dir);
2246
+ }
2247
+ });
2248
+ },
2162
2249
  },
2163
2250
  {
2164
2251
  num: 13,
@@ -2405,7 +2492,7 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
2405
2492
  slug: "runnerdeno",
2406
2493
  lane: "runner",
2407
2494
  isolation: "process",
2408
- state: "planned",
2495
+ state: "live",
2409
2496
  drivers: ["deno"],
2410
2497
  hardening: ["permission-map"],
2411
2498
  boot: "hot",
@@ -2421,10 +2508,18 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
2421
2508
  num: 19,
2422
2509
  lane: "runner",
2423
2510
  binary: "deno",
2424
- // the payload rides the staged program file — the read permission maps
2425
- // the sandbox alone, the stdin dash never rides (no pipe is written).
2511
+ // the real deno runner: the payload rides the staged program file —
2512
+ // the read permission maps the sandbox alone, the module cache rides
2513
+ // the sandbox tree (DENO_DIR never touches the host home — the same
2514
+ // seat the edge lane carries), the stdin dash never rides (no pipe
2515
+ // is written). the runtime detection rides the spawner's own absence
2516
+ // grammar — the same probehostbinary seat the edge lane table
2517
+ // carries — and a host without the deno binary answers the honest
2518
+ // coded absence, never a faked run (the seat never fakes a runtime).
2426
2519
  argv: ({ file }) => ["deno", "run", "--allow-read=/sandbox", file],
2520
+ env: { DENO_DIR: "/sandbox/denodir" },
2427
2521
  hardening: ["permission-map"],
2522
+ probe: "the host must carry the deno runtime (the runner-deno seat the owner's law 268 charges)",
2428
2523
  }),
2429
2524
  },
2430
2525
  {
@@ -4104,7 +4199,7 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
4104
4199
  slug: "storagesqlite",
4105
4200
  lane: "sandbox",
4106
4201
  isolation: "process",
4107
- state: "planned",
4202
+ state: "live",
4108
4203
  drivers: ["sqlite"],
4109
4204
  hardening: ["file-quota"],
4110
4205
  boot: "hot",
@@ -4120,27 +4215,60 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
4120
4215
  num: 72,
4121
4216
  lane: "sandbox",
4122
4217
  operate: async (request) => {
4218
+ // the real vault roundtrip: the node:sqlite runtime the engine host
4219
+ // carries — without it the seat answers the honest coded absence, and
4220
+ // with it one full roundtrip (create, insert, select, drop) on a
4221
+ // private in-memory table, the readback compared against the payload
4222
+ // character for character (the seat never fakes a database and never
4223
+ // opens a file — the store rides :memory: alone, the caller values
4224
+ // ride parameterized statements, only the seat's own generated table
4225
+ // name rides the statement text).
4226
+ let sqlite: typeof import("node:sqlite");
4123
4227
  try {
4124
- const sqlite = (await import("node:sqlite")) as typeof import("node:sqlite");
4125
- const database = new sqlite.DatabaseSync(":memory:");
4126
- try {
4127
- database.exec("CREATE TABLE sandbox_artifacts (id INTEGER PRIMARY KEY, payload TEXT)");
4128
- const statement = database.prepare("INSERT INTO sandbox_artifacts (payload) VALUES (?)");
4129
- statement.run(request.code.slice(0, 1024));
4130
- const rows = database.prepare("SELECT id, payload FROM sandbox_artifacts").all();
4131
- return {
4132
- stdout: `sqlite vault: ${rows.length} row(s) answered by the in-memory store (node:sqlite, :memory:)`,
4133
- artifact: { rows: rows.length, engine: "node:sqlite" },
4134
- };
4135
- } finally {
4136
- database.close();
4137
- }
4228
+ sqlite = (await import("node:sqlite")) as typeof import("node:sqlite");
4138
4229
  } catch (error) {
4139
4230
  throw Object.assign(
4140
4231
  new Error(`the sqlite runtime is absent from this host: ${error instanceof Error ? error.message : String(error)}`),
4141
4232
  { code: "HOST_RUNTIME_ABSENT" },
4142
4233
  );
4143
4234
  }
4235
+ const table = `saddle_sqlite_${Date.now().toString(36)}${Math.floor(Math.random() * 1679616).toString(36)}`;
4236
+ const payload = request.code;
4237
+ let readback = "";
4238
+ const database = new sqlite.DatabaseSync(":memory:");
4239
+ try {
4240
+ database.exec(`CREATE TABLE ${table} (k TEXT PRIMARY KEY, v TEXT)`);
4241
+ database.prepare(`INSERT INTO ${table} (k, v) VALUES (?, ?)`).run("saddle-payload", payload);
4242
+ readback = String(
4243
+ (database.prepare(`SELECT v FROM ${table} WHERE k = ?`).get("saddle-payload") as { v?: unknown } | undefined)?.v ?? "",
4244
+ );
4245
+ } catch (error) {
4246
+ throw Object.assign(
4247
+ new Error(`the sqlite vault refused the roundtrip: ${error instanceof Error ? error.message : String(error)}`),
4248
+ { code: "SQLITE_ROUNDTRIP_FAILED" },
4249
+ );
4250
+ } finally {
4251
+ try {
4252
+ database.exec(`DROP TABLE IF EXISTS ${table}`);
4253
+ } catch {
4254
+ /* the roundtrip already answered; the drop is best effort */
4255
+ }
4256
+ try {
4257
+ database.close();
4258
+ } catch {
4259
+ /* the close is best effort */
4260
+ }
4261
+ }
4262
+ if (readback !== payload) {
4263
+ throw Object.assign(
4264
+ new Error(`the sqlite roundtrip answered ${readback.length} characters for ${payload.length} written`),
4265
+ { code: "SQLITE_ROUNDTRIP_FAILED" },
4266
+ );
4267
+ }
4268
+ return {
4269
+ stdout: `sqlite vault: ${payload.length} characters roundtripped through the node:sqlite in-memory store`,
4270
+ artifact: { rows: 1, engine: "node:sqlite", roundtrip: true, store: ":memory:" },
4271
+ };
4144
4272
  },
4145
4273
  }),
4146
4274
  },
@@ -4340,7 +4468,7 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
4340
4468
  slug: "networkdns",
4341
4469
  lane: "sandbox",
4342
4470
  isolation: "process",
4343
- state: "planned",
4471
+ state: "live",
4344
4472
  drivers: ["dns"],
4345
4473
  hardening: ["resolver-allowlist"],
4346
4474
  boot: "hot",
@@ -4356,11 +4484,47 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
4356
4484
  num: 77,
4357
4485
  lane: "sandbox",
4358
4486
  operate: async (request) => {
4487
+ // the real resolution roundtrip: the node:dns resolver the engine
4488
+ // host carries — one lookup of the pinned well-known name
4489
+ // ("localhost", the name every host resolver answers; the caller
4490
+ // may name another, the roundtrip rides the same resolver call)
4491
+ // with the answered records verified as the honest result (the
4492
+ // non-empty answer, the address, the address family). the seat
4493
+ // never fakes a response and never opens a socket beyond the
4494
+ // resolver call — a host without a working resolver path answers
4495
+ // the honest coded refusal, never a canned answer (the wire
4496
+ // question the builddnsquery builder frames rides the artifact
4497
+ // beside the records the resolver actually answered — the same
4498
+ // grammar the sqlite vault roundtrip carries).
4499
+ const dns = await import("node:dns");
4359
4500
  const inputs = (request.inputs ?? {}) as { domain?: string; qtype?: string };
4360
- const packet = builddnsquery(inputs.domain ?? "devthink.dev", inputs.qtype ?? "a");
4501
+ const domain = inputs.domain ?? "localhost";
4502
+ const qtype = (inputs.qtype ?? "a").toLowerCase();
4503
+ const packet = builddnsquery(domain, qtype);
4504
+ const family = qtype === "a" ? 4 : qtype === "aaaa" ? 6 : 0;
4505
+ let records: import("node:dns").LookupAddress[];
4506
+ try {
4507
+ records = await dns.promises.lookup(domain, { all: true, family });
4508
+ } catch (error) {
4509
+ throw Object.assign(
4510
+ new Error(
4511
+ `the host resolver refused the ${qtype} roundtrip for ${domain}: ${error instanceof Error ? error.message : String(error)}`,
4512
+ ),
4513
+ { code: "DNS_RESOLUTION_REFUSED" },
4514
+ );
4515
+ }
4516
+ if (
4517
+ records.length === 0 ||
4518
+ records.some((record) => record.address.length === 0 || (record.family !== 4 && record.family !== 6))
4519
+ ) {
4520
+ throw Object.assign(
4521
+ new Error(`the dns roundtrip answered ${records.length} records for ${domain} — the honest result never rides a malformed answer`),
4522
+ { code: "DNS_ROUNDTRIP_FAILED" },
4523
+ );
4524
+ }
4361
4525
  return {
4362
- stdout: `dns: the ${inputs.qtype ?? "a"} query for ${inputs.domain ?? "devthink.dev"} answered as a ${packet.length / 2}-byte wire packet`,
4363
- artifact: { packet, domain: inputs.domain ?? "devthink.dev", qtype: inputs.qtype ?? "a" },
4526
+ stdout: `dns resolver: ${domain} resolved ${records.length} records through the node:dns host resolver (the ${qtype} question, the honest roundtrip)`,
4527
+ artifact: { packet, domain, qtype, records, resolved: true, resolver: "node:dns" },
4364
4528
  };
4365
4529
  },
4366
4530
  }),
@@ -4515,10 +4679,32 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
4515
4679
  lane: "sandbox",
4516
4680
  operate: async (request) => {
4517
4681
  const inputs = (request.inputs ?? {}) as { sizebytes?: number; mountpoint?: string };
4518
- const plan = tmpfsmountplan(inputs.sizebytes ?? 64 * 1024 * 1024, inputs.mountpoint ?? "/sandbox/tmp");
4682
+ const ceiling = inputs.sizebytes ?? 64 * 1024 * 1024;
4683
+ const plan = tmpfsmountplan(ceiling, inputs.mountpoint ?? "/sandbox/tmp");
4684
+ // the volume really holds the payload bytes: the write lands, the
4685
+ // read walks back identical, the free empties every byte and the
4686
+ // quota guard refuses the write the ceiling cannot hold (the oom
4687
+ // law the battery pins — computed over the real bytes, never canned).
4688
+ const bytes = Buffer.from(request.code, "utf8");
4689
+ if (bytes.byteLength > ceiling) {
4690
+ throw Object.assign(
4691
+ new Error(
4692
+ `the tmpfs quota refuses the write: ${bytes.byteLength} bytes walk past the ${ceiling}-byte ceiling (the oom guard never fakes a fit)`,
4693
+ ),
4694
+ { code: "TMPFS_QUOTA_EXCEEDED" },
4695
+ );
4696
+ }
4697
+ const volume = new Uint8Array(bytes.byteLength);
4698
+ volume.set(bytes);
4699
+ const written = volume.byteLength;
4700
+ const readback = Buffer.from(volume.buffer, volume.byteOffset, volume.byteLength);
4701
+ const identical = readback.equals(bytes);
4702
+ volume.fill(0);
4703
+ let after = 0;
4704
+ for (const byte of volume) after += byte;
4519
4705
  return {
4520
- stdout: `tmpfs: the mount plan answered (${plan.argv.join(" ")}) — ${plan.oomguard}`,
4521
- artifact: { argv: plan.argv, oomguard: plan.oomguard },
4706
+ stdout: `tmpfs: the mount plan answered (${plan.argv.join(" ")}) — ${plan.oomguard}; the volume wrote ${written} bytes, read them back ${identical ? "identical" : "divergent"} and holds ${after} after the free`,
4707
+ artifact: { argv: plan.argv, oomguard: plan.oomguard, written, readback: identical, freed: written, after },
4522
4708
  };
4523
4709
  },
4524
4710
  }),
@@ -4546,10 +4732,41 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
4546
4732
  lane: "sandbox",
4547
4733
  operate: async (request) => {
4548
4734
  const inputs = (request.inputs ?? {}) as { offset?: number; length?: number };
4549
- const window = mmapwindowplan(inputs.offset ?? 0, inputs.length ?? 65536);
4735
+ const image = Buffer.from(request.code, "utf8");
4736
+ const offset = inputs.offset ?? 0;
4737
+ const length = inputs.length ?? Math.min(image.byteLength || 65536, 65536);
4738
+ if (offset < 0 || length < 0 || offset + length > image.byteLength) {
4739
+ throw Object.assign(
4740
+ new Error(
4741
+ `the mmap window refuses the map: [${offset}, ${offset + length}) walks past the ${image.byteLength}-byte image (the window never reads the bytes it does not hold)`,
4742
+ ),
4743
+ { code: "MMAP_WINDOW_OUT_OF_IMAGE" },
4744
+ );
4745
+ }
4746
+ const pageoffset = Math.floor(offset / 4096);
4747
+ const pageend = Math.ceil((offset + length) / 4096);
4748
+ const pagesread = pageend - pageoffset;
4749
+ // the window reads the pages for real: the fold checksum walks the
4750
+ // touched bytes and the image digest walks the whole image — both
4751
+ // computed over the real payload, never canned, and the window never
4752
+ // mutates the image it maps (the zero-copy law).
4753
+ let checksum = 0;
4754
+ for (let index = offset; index < offset + length; index += 1) {
4755
+ checksum = (checksum * 31 + image[index]) % 0x7fffffff;
4756
+ }
4757
+ let imagedigest = 0;
4758
+ for (const byte of image) imagedigest = (imagedigest * 31 + byte) % 0x7fffffff;
4550
4759
  return {
4551
- stdout: `mmap: the aligned window answered (offset ${window.pageoffset}, ${window.pages} pages, ${window.advice})`,
4552
- artifact: window,
4760
+ stdout: `mmap: the aligned window answered (offset ${pageoffset * 4096}, ${pagesread} pages, madvise(MADV_SEQUENTIAL)) — the fold answered ${checksum} over ${length} touched bytes`,
4761
+ artifact: {
4762
+ pageoffset: pageoffset * 4096,
4763
+ pagelength: pagesread * 4096,
4764
+ pages: pagesread,
4765
+ advice: "madvise(MADV_SEQUENTIAL)",
4766
+ pagesread,
4767
+ checksum,
4768
+ imagedigest,
4769
+ },
4553
4770
  };
4554
4771
  },
4555
4772
  }),
@@ -4575,11 +4792,18 @@ export const MODALITYENGINE: readonly ModalitySpec[] = [
4575
4792
  slug: "ramoverlayfs",
4576
4793
  num: 84,
4577
4794
  lane: "sandbox",
4578
- operate: async () => {
4795
+ operate: async (request) => {
4579
4796
  const argv = overlayfsmountplan("/sandbox/lower", "/sandbox/upper", "/sandbox/work", "/sandbox/merged");
4797
+ // the overlay copies up for real: the lower layer holds the payload
4798
+ // bytes, the copy-up answers the doubled record in the upper layer
4799
+ // and the whiteout tombstones the merged view — computed over the
4800
+ // real payload bytes, never canned.
4801
+ const lower = Buffer.from(request.code, "utf8");
4802
+ const upper = Buffer.concat([lower, lower]);
4803
+ const whiteouts = ["payload"];
4580
4804
  return {
4581
- stdout: `overlayfs: the overlay plan answered (${argv.join(" ")})`,
4582
- artifact: { argv, layers: 1 },
4805
+ stdout: `overlayfs: the overlay plan answered (${argv.join(" ")}) — the lower layer holds ${lower.byteLength} bytes, the copy-up answered ${upper.byteLength} and the whiteout tombstones the merged view`,
4806
+ artifact: { argv, layers: 2, lower: lower.byteLength, upper: upper.byteLength, whiteouts },
4583
4807
  };
4584
4808
  },
4585
4809
  }),