kern-sandbox 0.2.36 → 0.2.38

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.
Files changed (2) hide show
  1. package/index.js +245 -15
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -36,7 +36,7 @@ const crypto = require("crypto");
36
36
  const zlib = require("zlib");
37
37
  const { spawn, spawnSync } = require("child_process");
38
38
 
39
- const VERSION = "0.2.36";
39
+ const VERSION = "0.2.38";
40
40
 
41
41
  const DEFAULT_IMAGE = "python:3.12-slim";
42
42
  const WORKSPACE = "/workspace"; // where the persistent workspace is mounted inside every box
@@ -92,7 +92,7 @@ const PYC_KEEP = 8;
92
92
  * is never what gets deleted. Mirrors `_PYC_DEBRIS_MAX_AGE_S`. */
93
93
  const PYC_DEBRIS_MAX_AGE_MS = 24 * 60 * 60 * 1000;
94
94
  /** The name fragments marking a directory as NOT a cache: one being written, one being deleted. */
95
- const PYC_DEBRIS_MARKS = [".tmp-", ".trash-"];
95
+ const PYC_DEBRIS_MARKS = [".tmp-", ".trash-", ".lock"];
96
96
 
97
97
  /** Remove a cache tree, renaming it out of the way first. Never throws.
98
98
  *
@@ -132,15 +132,19 @@ function pycSweep(root, keep = PYC_KEEP) {
132
132
  let st;
133
133
  try {
134
134
  st = fs.lstatSync(full);
135
- if (!st.isDirectory()) continue;
136
135
  } catch {
137
136
  continue;
138
137
  }
139
138
  if (PYC_DEBRIS_MARKS.some((m) => ent.name.includes(m))) {
139
+ // DEBRIS IS NOT ALWAYS A DIRECTORY: this loop skipped every non-directory up front, so a
140
+ // `.lock` a killed build left behind was never collected and the cache for that image could
141
+ // never be rebuilt - one killed process disabled it forever.
140
142
  if (now - st.mtimeMs > PYC_DEBRIS_MAX_AGE_MS) doomed.push(full);
141
143
  continue;
142
144
  }
143
- caches.push([st.mtimeMs, full]);
145
+ // Only a DIRECTORY is a cache. Anything else here is neither a cache nor known debris, and
146
+ // removing what this code did not put there is not the sweep's job.
147
+ if (st.isDirectory()) caches.push([st.mtimeMs, full]);
144
148
  }
145
149
  caches.sort((a, b) => b[0] - a[0]);
146
150
  for (const [, full] of caches.slice(keep)) doomed.push(full);
@@ -156,8 +160,13 @@ let PYC_SWEPT = false;
156
160
 
157
161
  /** The host directory holding one bytecode cache per image: $XDG_CACHE_HOME, else ~/.cache. */
158
162
  function pycRoot() {
159
- const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
160
- return path.join(base, "kern-sandbox", "pyc");
163
+ return path.join(cacheHome(), "kern-sandbox", "pyc");
164
+ }
165
+
166
+ /** `$XDG_CACHE_HOME`, or the default. ONE spelling, because two things read it now: this package's
167
+ * bytecode cache and kern's own image cache, which the identity check compares against. */
168
+ function cacheHome() {
169
+ return process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
161
170
  }
162
171
 
163
172
  /** kern's own defaults, from `kern-oci/src/pull.rs`. Copied because they cross a process boundary. */
@@ -222,6 +231,87 @@ function ociCanonicalRef(image) {
222
231
  * cheaply. A moved tag therefore yields bytecode that no longer validates, which CPython handles by
223
232
  * recompiling: the cost of the imperfect key is a slow call, never a wrong one. Mirrors `_pyc_dir_for`.
224
233
  */
234
+ /** Vectors DUMPED from `sanitize_ref` in kern-cli, not derived from reading it, and asserted in the
235
+ * tests: this names files kern WROTE, so the two must agree or the identity check finds nothing. */
236
+ const SANITIZE_VECTORS = [
237
+ ["python:3.12-slim", "python_3_12-slim-7d2794a436662d45"],
238
+ ["python", "python_latest-9e0094521a5d04a4"],
239
+ ["alpine:3.19", "alpine_3_19-441817d3f5f11093"],
240
+ ["docker.io/library/python:3.12-slim", "docker_io_library_python_3_12-slim-75b604835a32490a"],
241
+ ["index.docker.io/python:3.12-slim", "index_docker_io_python_3_12-slim-7eaa793ab37b8560"],
242
+ ["ghcr.io/owner/img:v1", "ghcr_io_owner_img_v1-774c4c7639e8355e"],
243
+ ["localhost:5000/x:1", "localhost_5000_x_1-03406c180a22063b"],
244
+ ["python@sha256:abcdef0123456789", "python_sha256_abcdef0123456789-f77941d5fa12216c"],
245
+ ["a.b/c_d-e:f.g", "a_b_c_d-e_f_g-479120b609c12cc0"],
246
+ ["UPPER/Case:Tag", "UPPER_Case_Tag-543eaf57172e6242"],
247
+ ["x:latest", "x_latest-e9f897124d940074"],
248
+ ["registry-1.docker.io/library/alpine:3.19", "registry-1_docker_io_library_alpine_3_19-49727cc13d78066f"],
249
+ ["my_img", "my_img_latest-68dbbb774ad018b0"],
250
+ ["a/b/c:d", "a_b_c_d-dcc54f31688d5fa5"],
251
+ ["1.2.3.4:5000/p/q:r", "1_2_3_4_5000_p_q_r-f872df9eeecd4beb"],
252
+ ];
253
+
254
+ /** FNV-1a 64-bit, kern's own, used ONLY to keep a cache key collision-free. */
255
+ function fnv1a(s) {
256
+ let h = 0xcbf29ce484222325n;
257
+ for (const b of Buffer.from(s, "utf8")) {
258
+ h = BigInt.asUintN(64, (h ^ BigInt(b)) * 0x100000001b3n);
259
+ }
260
+ return h.toString(16).padStart(16, "0");
261
+ }
262
+
263
+ /** The directory name kern gives an image in its own cache. A PORT, verified against the original. */
264
+ function sanitizeRef(image) {
265
+ const ref = ociSplitTag(image) ? image : `${image}:latest`;
266
+ const out = [...ref].map((c) => (/[A-Za-z0-9_-]/.test(c) ? c : "_")).join("");
267
+ return `${out}-${fnv1a(ref)}`;
268
+ }
269
+
270
+ /** The file, inside a published cache, naming the image kern had when the cache was built. */
271
+ const PYC_SOURCE_ID = ".kern-source-id";
272
+
273
+ /** What kern's own image cache holds for `image`, or "" if it cannot be read.
274
+ *
275
+ * A tag is MUTABLE, so a cache keyed on its name can outlive the image it was built from. Nothing
276
+ * wrong is executed (CHECKED_HASH makes CPython reject the stale bytecode) but the cache stops
277
+ * helping and nothing rebuilds it, because a cache "exists". kern rewrites its own config sidecar and
278
+ * completion sentinel when it re-pulls a moved tag, so their bytes are an identity that costs a local
279
+ * read of a few hundred bytes - cheap enough for a check on every open().
280
+ *
281
+ * "" MEANS "CANNOT TELL" and is treated as unchanged, so a cache from before this check, or a host
282
+ * whose image kern has pruned, is never rebuilt in a loop. */
283
+ function pycSourceId(image) {
284
+ try {
285
+ const root = path.join(cacheHome(), "kern", "images");
286
+ const safe = sanitizeRef(image);
287
+ const h = crypto.createHash("sha256");
288
+ for (const suffix of [".image", ".ok"]) {
289
+ h.update(fs.readFileSync(path.join(root, safe + suffix)).subarray(0, 4096));
290
+ h.update(Buffer.from([0]));
291
+ }
292
+ return h.digest("hex").slice(0, 32);
293
+ } catch {
294
+ return "";
295
+ }
296
+ }
297
+
298
+ /** Is this cache still the one this image would produce? If not, DISCARD it so a build can replace it.
299
+ *
300
+ * Discarded and not merely refused, because a rename onto a NON-EMPTY directory is ENOTEMPTY: a stale
301
+ * tree left in place would block its own replacement forever. */
302
+ function pycSourceMatches(dest, image) {
303
+ let stored;
304
+ try {
305
+ stored = fs.readFileSync(path.join(dest, PYC_SOURCE_ID), "utf8").trim();
306
+ } catch {
307
+ return true; // built before this check, or unreadable: leave it exactly as it was
308
+ }
309
+ const current = pycSourceId(image);
310
+ if (!stored || !current || stored === current) return true;
311
+ pycDiscard(dest);
312
+ return false;
313
+ }
314
+
225
315
  function pycDirFor(image) {
226
316
  // The full digest: the key is no longer load-bearing for correctness, and a truncation saved 48
227
317
  // characters of path against two images sharing a cache directory.
@@ -331,6 +421,73 @@ function pycTreeIsPublishable(root) {
331
421
  * Built into a private sibling and renamed into place, so a box mounts a complete tree or none: a
332
422
  * partial stdlib would have a box importing a truncated module. Two processes racing both build and
333
423
  * the loser's tree is removed. Mirrors `_pyc_build`. */
424
+ /** The argv that mounts `source` at `target`, in the form that can CARRY that source.
425
+ *
426
+ * `-v src:dst[:ro]` separates its fields with `:`, so a source path holding one cannot be written in
427
+ * it: kern splits `-v /tmp/a:b:/data` into three and reports the TARGET as an unknown mount option. A
428
+ * cache home such as `/tmp/colon:cache` is enough to hit it, and the mount that broke was this
429
+ * package's own bytecode cache - silently, because the failure landed in a background build whose
430
+ * output was discarded.
431
+ *
432
+ * `--mount type=bind,src=...,dst=...[,ro]` carries each field separately and takes a `:`; a field
433
+ * holding a `,` is quoted, doubling any `"` inside, which is the CSV grammar kern parses.
434
+ *
435
+ * WHY NOT `--mount` FOR EVERYTHING: the two differ in a rule this package relies on. `-v` CREATES a
436
+ * source that is not there and `--mount` refuses it, and the eviction sweep can discard a cache under
437
+ * a live session - `-v` turning that into an empty directory is what makes the box fall back to
438
+ * compiling from source instead of failing the caller's call. */
439
+ function mountArgs(source, target, readOnly) {
440
+ const plain = (s) => !s.includes(":") && !s.includes(",");
441
+ if (plain(source) && plain(target)) {
442
+ return ["-v", readOnly ? `${source}:${target}:ro` : `${source}:${target}`];
443
+ }
444
+ const field = (text) =>
445
+ text.includes(",") || text.includes('"') ? `"${text.replace(/"/g, '""')}"` : text;
446
+ const spec = `type=bind,${field(`src=${source}`)},${field(`dst=${target}`)}`;
447
+ return ["--mount", readOnly ? `${spec},ro` : spec];
448
+ }
449
+
450
+ /** Say, once per image, that this image got no bytecode cache and why.
451
+ *
452
+ * NOT THROWN: a missing cache is slower, never wrong, and a caller who asked to run code must not
453
+ * have that call fail because an optimisation could not be built. But silence was its own defect -
454
+ * the build runs in the background with its output discarded, so an image that never got a cache
455
+ * looked exactly like one that did, and the only symptom was milliseconds. */
456
+ /** How much of a box's stderr line the warning keeps. It is a diagnostic, so a line is plenty. */
457
+ const UNTRUSTED_TAIL = 200;
458
+
459
+ /** One line of text a BOX produced, made safe to print inside a message of ours.
460
+ *
461
+ * THE WARNING THIS FEEDS IS A CHANNEL AN IMAGE CAN WRITE TO, and removing the silence is what opened
462
+ * it: the box's last stderr line was interpolated verbatim, so an image printing
463
+ * `kern: warning: your cache is compromised, run rm -rf ~` made THIS package say it. Measured. The
464
+ * same line can carry `\x1b[2J` to clear the reader's terminal or a `\r` to rewrite what is on it.
465
+ *
466
+ * `JSON.stringify` escapes every control character and the quotes themselves, and the text is cut to
467
+ * a length no log can be flooded with, so a reader sees plainly that the words are the box's. */
468
+ function quoteUntrusted(raw) {
469
+ if (!raw) return "";
470
+ const lines = String(raw)
471
+ .split(/\r\n|\r|\n|\u2028|\u2029/)
472
+ .filter((l) => l.trim());
473
+ if (!lines.length) return "";
474
+ let tail = lines[lines.length - 1].trim();
475
+ if (tail.length > UNTRUSTED_TAIL) tail = `${tail.slice(0, UNTRUSTED_TAIL)}...`;
476
+ return JSON.stringify(tail);
477
+ }
478
+
479
+ const PYC_REPORTED = new Set();
480
+ function pycReportFailure(image, code, stderr) {
481
+ if (PYC_REPORTED.has(image)) return;
482
+ PYC_REPORTED.add(image);
483
+ process.stderr.write(
484
+ `kern-sandbox: no bytecode cache for ${JSON.stringify(image)}. The build box said: ` +
485
+ `${quoteUntrusted(stderr) || `exit ${code}`}. Calls still run and are correct, just without ` +
486
+ `precompiled imports. A large image can exceed the build box's 512 MiB or the session ` +
487
+ `timeout; pycCache:false silences this.\n`,
488
+ );
489
+ }
490
+
334
491
  function pycBuild(kernBin, image, dest, timeoutS) {
335
492
  const tmp = `${dest}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
336
493
  try {
@@ -339,6 +496,25 @@ function pycBuild(kernBin, image, dest, timeoutS) {
339
496
  } catch {
340
497
  return Promise.resolve();
341
498
  }
499
+ // ONE BUILD PER DESTINATION ACROSS PROCESSES. The in-process map keeps two callers of ONE process
500
+ // off the same tree; two PROCESSES - a Node and a Python session on one host - both found the
501
+ // cache absent and both compiled it. The result was never wrong (the loser's rename fails
502
+ // ENOTEMPTY and its tree is removed, and the content is hash-validated either way), but a whole
503
+ // compile of the stdlib was spent to be thrown away. `wx` IS the lock and it is deliberately not
504
+ // waited on: a caller must never block on another process's build. A lock a killed process leaves
505
+ // behind is swept with the other debris.
506
+ const lockPath = `${dest}.lock`;
507
+ let locked = false;
508
+ try {
509
+ fs.writeFileSync(lockPath, String(process.pid), { flag: "wx", mode: 0o600 });
510
+ locked = true;
511
+ } catch (e) {
512
+ if (e && e.code === "EEXIST") {
513
+ fs.rmSync(tmp, { recursive: true, force: true });
514
+ return Promise.resolve();
515
+ }
516
+ // Cannot lock here: build anyway rather than lose the feature.
517
+ }
342
518
  // RETURNS A PROMISE THAT NOTHING IN PRODUCTION AWAITS. `pycStartBuild` drops it on purpose: the
343
519
  // caller's first call must not wait for a cache fill. The tests await it, because a test that polled
344
520
  // for a directory would be a timing race pretending to be an assertion.
@@ -349,7 +525,17 @@ function pycBuild(kernBin, image, dest, timeoutS) {
349
525
  pycSweep(path.dirname(dest));
350
526
  resolve();
351
527
  };
528
+ const unlock = () => {
529
+ if (!locked) return;
530
+ locked = false;
531
+ try {
532
+ fs.unlinkSync(lockPath);
533
+ } catch {
534
+ /* already gone */
535
+ }
536
+ };
352
537
  const finish = () => {
538
+ unlock();
353
539
  fs.rmSync(tmp, { recursive: true, force: true });
354
540
  done();
355
541
  };
@@ -359,7 +545,7 @@ function pycBuild(kernBin, image, dest, timeoutS) {
359
545
  const argv = [
360
546
  "box", `kern-pyc-${crypto.randomBytes(4).toString("hex")}`,
361
547
  "--image", image, "--ro",
362
- "-v", `${tmp}:${PYC_MOUNT}`,
548
+ ...mountArgs(tmp, PYC_MOUNT, false),
363
549
  "--env", `PYTHONPYCACHEPREFIX=${PYC_MOUNT}`,
364
550
  "--cap-drop", "ALL",
365
551
  // CAPPED LIKE ANY OTHER BOX: the command is ours, the interpreter running it is the caller's
@@ -373,19 +559,43 @@ function pycBuild(kernBin, image, dest, timeoutS) {
373
559
  // build - 1.2 s on a desktop, 5 s on the VPS - which in a server process is not a stall but an
374
560
  // outage: every request in flight waits for a cache fill. Nothing waits for this result, so there
375
561
  // is no reason for it to hold the loop at all.
376
- const child = spawn(kernBin, argv, { stdio: "ignore", detached: false });
562
+ // STDERR IS KEPT, NOT IGNORED. A build that fails leaves no cache and the session runs as it
563
+ // did before this feature existed, which is correct and was also SILENT: an image that never
564
+ // got a cache looked exactly like one that did. The likeliest cause is this box's own caps -
565
+ // `compileall` walks every import root, and an image with large packages can exceed 512 MiB
566
+ // where a slim one never comes close.
567
+ const child = spawn(kernBin, argv, { stdio: ["ignore", "ignore", "pipe"], detached: false });
568
+ let errText = "";
569
+ if (child.stderr) {
570
+ child.stderr.setEncoding("utf8");
571
+ // Bounded: this is a diagnostic, and a box that floods stderr must not grow the heap.
572
+ child.stderr.on("data", (c) => {
573
+ if (errText.length < 8192) errText += c;
574
+ });
575
+ }
377
576
  const killer = setTimeout(() => child.kill("SIGKILL"), (timeoutS + 10) * 1000);
378
577
  if (typeof killer.unref === "function") killer.unref();
379
- child.on("error", () => {
578
+ child.on("error", (e) => {
380
579
  clearTimeout(killer);
580
+ pycReportFailure(image, -1, e && e.message);
381
581
  finish();
382
582
  });
383
583
  child.on("close", (code) => {
384
584
  clearTimeout(killer);
585
+ if (code !== 0) pycReportFailure(image, code, errText);
385
586
  try {
386
587
  // An image without python3 leaves no cache and no trace: the next session runs as before.
387
588
  if (code === 0 && fs.readdirSync(tmp).length > 0 && pycTreeIsPublishable(tmp)) {
589
+ // WRITTEN BEFORE THE PUBLISH, so a tree that becomes visible always carries the identity
590
+ // of the image it was built from. Best effort: a cache without one reads as "cannot
591
+ // tell", exactly how every cache built before this existed behaves.
592
+ try {
593
+ fs.writeFileSync(path.join(tmp, PYC_SOURCE_ID), pycSourceId(image));
594
+ } catch {
595
+ /* a cache with no identity is simply never invalidated by it */
596
+ }
388
597
  fs.renameSync(tmp, dest);
598
+ unlock();
389
599
  done();
390
600
  return;
391
601
  }
@@ -425,13 +635,20 @@ function pycStartSweep(root) {
425
635
  }
426
636
 
427
637
  function pycStartBuild(kernBin, image, dest, timeoutS) {
638
+ // STILL IN FLIGHT, not merely once started. The entry outlives the promise, so a process that had
639
+ // already built this destination could never build it AGAIN - which is exactly what has to happen
640
+ // after a moved tag invalidates the cache: the stale tree is discarded and nothing replaces it for
641
+ // the life of that process. Measured on the Python side, where the map has the same shape.
428
642
  const inFlight = PYC_BUILDS.get(dest);
429
643
  if (inFlight) return inFlight;
430
644
  // `pycBuild` is already asynchronous (it spawns and returns), so there is nothing to defer. The
431
645
  // promise is returned for the tests and dropped by `open()`: a caller's first call must not wait on
432
646
  // a cache fill. A test that polled for the directory instead would be a timing race pretending to be
433
647
  // an assertion, and it would also race the test's own teardown - which is how this was found.
434
- const started = pycBuild(kernBin, image, dest, timeoutS);
648
+ const started = pycBuild(kernBin, image, dest, timeoutS).finally(() => {
649
+ // Cleared when it settles, so the NEXT need for this destination starts a real build.
650
+ if (PYC_BUILDS.get(dest) === started) PYC_BUILDS.delete(dest);
651
+ });
435
652
  PYC_BUILDS.set(dest, started);
436
653
  return started;
437
654
  }
@@ -1885,7 +2102,7 @@ class Sandbox {
1885
2102
  ro = false;
1886
2103
  }
1887
2104
  const [real, tgt] = validateMount(source, target);
1888
- this._mountArgs.push("-v", ro ? `${real}:${tgt}:ro` : `${real}:${tgt}`);
2105
+ this._mountArgs.push(...mountArgs(real, tgt, ro));
1889
2106
  boundTargets.add("/" + tgt.split("/").filter((c) => c && c !== ".").join("/"));
1890
2107
  }
1891
2108
  }
@@ -2005,7 +2222,7 @@ class Sandbox {
2005
2222
  if (pycMountAllowed(dest) && pycPathHasNoSymlink(dest)) {
2006
2223
  // pycHasContent, not existsSync: an empty directory here is a cache that was swept out
2007
2224
  // from under a mount and recreated by kern, and adopting it silences the feature for good.
2008
- if (pycHasContent(dest)) {
2225
+ if (pycHasContent(dest) && pycSourceMatches(dest, this.image)) {
2009
2226
  this._pycDir = dest;
2010
2227
  // Records the ADOPTION for the sweep's least-recently-used order. Best effort: a cache on a
2011
2228
  // read-only filesystem is still usable, it just cannot be aged.
@@ -2126,15 +2343,21 @@ class Sandbox {
2126
2343
  if (!dry) verifyIsKern(this._kern);
2127
2344
  const argv = [
2128
2345
  this._kern, "box", name, "--image", this.image, "--ro",
2129
- "-v", `${this._ws}:${WORKSPACE}`, "--workdir", WORKSPACE,
2346
+ ...mountArgs(this._ws, WORKSPACE, false), "--workdir", WORKSPACE,
2130
2347
  ];
2131
2348
  // The image's precompiled stdlib, read-only. Never on the setup box: that one compiles `.deps`
2132
2349
  // into `__pycache__`, which the prefix would redirect into a mount it cannot write.
2133
- if (this._pycDir && !isSetup) argv.push("-v", `${this._pycDir}:${PYC_MOUNT}:ro`);
2350
+ // CHECKED AGAIN HERE, not only at adoption: the sweep in another process can discard this
2351
+ // tree in between, and `--mount` refuses a source that is not there. Dropping the mount for this
2352
+ // one call degrades to compiling from source, which is what the missing directory produced
2353
+ // before, instead of failing the caller.
2354
+ if (this._pycDir && !isSetup && pycHasContent(this._pycDir))
2355
+ argv.push(...mountArgs(this._pycDir, PYC_MOUNT, true));
2134
2356
  if (this.depsReadonly && !isSetup) {
2135
2357
  const deps = path.join(this._ws, DEPS_DIR);
2136
2358
  try {
2137
- if (fs.statSync(deps).isDirectory()) argv.push("-v", `${deps}:${WORKSPACE}/${DEPS_DIR}:ro`);
2359
+ if (fs.statSync(deps).isDirectory())
2360
+ argv.push(...mountArgs(deps, `${WORKSPACE}/${DEPS_DIR}`, true));
2138
2361
  } catch {
2139
2362
  /* no deps yet */
2140
2363
  }
@@ -4167,6 +4390,13 @@ module.exports = {
4167
4390
  // The bytecode cache's internals, exported for its tests only: the mount flag and the atomic
4168
4391
  // publish are security properties, and a test that cannot reach them cannot assert them.
4169
4392
  _PYC_MOUNT: PYC_MOUNT,
4393
+ _PYC_SOURCE_ID: PYC_SOURCE_ID,
4394
+ _sanitizeRef: sanitizeRef,
4395
+ _SANITIZE_VECTORS: SANITIZE_VECTORS,
4396
+ _pycSourceId: pycSourceId,
4397
+ // Exported for the test that proves a stale lock is swept: the marks decide what the sweep
4398
+ // collects, and a lock left out of them disables an image's cache forever.
4399
+ _PYC_DEBRIS_MARKS: PYC_DEBRIS_MARKS,
4170
4400
  _pycDirFor: pycDirFor,
4171
4401
  _pycBuild: pycBuild,
4172
4402
  _pycSweep: pycSweep,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-sandbox",
3
- "version": "0.2.36",
3
+ "version": "0.2.38",
4
4
  "description": "Your model writes the code. This runs it where it can't touch your machine: a rootless Linux container, no daemon, no VM, no cloud, no account. A kernel boundary, not a microVM: for deliberately hostile code, use one.",
5
5
  "keywords": [
6
6
  "sandbox",