kern-sandbox 0.1.40 → 0.1.41

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 (3) hide show
  1. package/README.md +16 -94
  2. package/index.js +1 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -211,52 +211,10 @@ new Sandbox({
211
211
  });
212
212
  ```
213
213
 
214
- **Writable paths: `/workspace`, `/tmp` and `/dev/shm`.** The box root is read-only, so `/tmp` is a
215
- 64 MiB tmpfs this binding mounts for you. Without it a write naming `/tmp` fails with `EROFS` and
216
- temp-file helpers fall back to the current directory, quietly putting scratch into your persistent
217
- workspace where `listFiles` then reports it. The bytes are charged to the box's own memory cgroup, so
218
- filling `/tmp` OOM-kills the box and never fills the host disk. Resize with `tmpfs: { "/tmp": "512m" }`,
219
- remove with `tmpfs: {}`, or bind your own directory at `/tmp` through `mounts` and the default steps
220
- aside (a `:ro` bind included, which leaves `/tmp` read-only: your call, not an accident). **The unit is
221
- required and the target may not contain a `:`.** kern's CLI takes both spellings and means the
222
- opposite of what you do: a bare `"64"` is 64 BYTES, `"0"` is UNLIMITED, and `["/scratch:9g"]` mounts
223
- `/scratch` at 9 GiB rather than a directory by that name. All three measured, all three refused here. A size larger than `memoryMb` is refused for the same family
224
- of reason: `df` would report it to a program that preflights. The binding's own default is clamped to
225
- half the cap instead.
226
-
227
- **`memoryMb` bounds the cgroup, not the workload's usable memory.** The cap is shared with
228
- memory-backed filesystems in the same box, and `/dev/shm` is one of them with **no size at all** (the
229
- kernel's tmpfs default, half of host RAM, so it scales with the machine and not with your config).
230
- Measured: 200 MiB written there under `memoryMb: 128` OOM-kills the box whatever `/tmp` is set to.
231
- `tmpfs: { "/dev/shm": ... }` is refused by kern; `mounts` at the same target IS accepted and stacks over
232
- kern's own mount; measured through it, `multiprocessing.shared_memory` and POSIX semaphores still
233
- work. Two costs: a plain directory is unbounded on DISK instead of in RAM, so bounding it means
234
- binding a host directory that is itself a sized tmpfs, and it has no tmpfs lifetime, so what the box
235
- writes to `/dev/shm` is still on the host after the box dies.
236
-
237
- **Scratch does not survive a call.** Each `runCode` is a fresh box, so `/tmp` is fresh too while the
238
- workspace persists. Put anything a later call must find in the workspace. The `setup` box is the exception: an install needs unbounded scratch, so the default is not
239
- applied there (an explicit `tmpfs` still is).
240
-
241
- **Toolchains in the box** need two writable places, and the error names neither. Go reports `failed to
242
- initialize build cache at /root/.cache`, which says nothing about `HOME`; npm renders a failed
243
- `mkdir /root/.npm` as `Invalid response body while trying to fetch https://registry.npmjs.org/...`,
244
- which reads as a network fault and is not one. Measured on `node:22`: neither -> exit 2, `HOME` alone
245
- with a read-only `/tmp` -> still exit 2, both -> exit 0. Pass both:
246
-
247
- ```js
248
- new Sandbox({
249
- image: "golang:1.23-alpine",
250
- env: { HOME: "/workspace" }, // npm's ~/.npm, Go's ~/.cache, Rust's CARGO_HOME, .NET's NuGet
251
- tmpfs: { "/tmp": "512m" }, // scratch; 64 MiB fits a small install, a real one needs more
252
- });
253
- ```
254
-
255
- `runCode`/`run` also take `timeoutS`/`onStdout`/`onStderr` as **per-call** options that override the
256
- session defaults for that one call. A `vcpu:` profile can carry `cpus`+`memory`; `memoryMb`/`cpus` are
257
- explicit flags that **override** a profile's values (and the `memoryMb` default `512` shadows a profile's
258
- `memory`, so pass `memoryMb: null` to let the profile apply). The **MCP server** (`kern-mcp`, for Claude
259
- Desktop / Cursor) ships in the Python package `kern-sandbox` (`pip install kern-sandbox`).
214
+ **The sharp edges are in [SANDBOX-NOTES.md](https://github.com/getkern/kern/blob/main/bindings/node/SANDBOX-NOTES.md):**
215
+ the writable paths and why `/tmp` is a tmpfs, `memoryMb` bounding the cgroup rather than usable
216
+ memory, scratch that does not survive a call, and the two writable places a toolchain needs before
217
+ `npm install` stops reporting a network error that is not one. Each is a measured surprise.
260
218
 
261
219
  ## Egress: the setting between no network and the host's
262
220
 
@@ -317,60 +275,24 @@ package manager imports `globSync` from `node:fs`, which landed in 22.
317
275
 
318
276
  ## Charts, rich results, live output, and checkpoints
319
277
 
320
- **Rich results (the "code interpreter" pattern).** `runCode` runs Python by default, and like a
321
- Jupyter cell it captures rich, mime-typed values into `result.results` (a list of `Result`) with
322
- **no Jupyter kernel**: the value of the code's **last bare expression**, every **`display(obj)`** call,
323
- and **every open matplotlib figure automatically** (no `savefig`). Accessors: `.png`/`.jpeg` (Buffer),
324
- `.html`, `.svg`, `.markdown`, `.json`, `.text`.
278
+ `runCode` captures mime-typed values into `result.results` the way a notebook cell does: the **last
279
+ bare expression**, every **`display(obj)`**, and **every open matplotlib figure automatically**, with
280
+ no `savefig`. Accessors: `.png`, `.jpeg`, `.html`, `.svg`, `.markdown`, `.json`, `.text`.
325
281
 
326
282
  ```js
327
- await kern.withSandbox({ setup: "pip install matplotlib pandas" }, async (sbx) => {
328
- let r = await sbx.runCode("import matplotlib; matplotlib.use('Agg')\n" +
329
- "import matplotlib.pyplot as plt; plt.plot([1,4,9])");
330
- const png = r.results.map((x) => x.png).find(Boolean) ?? null; // figure Buffer, auto-captured
331
-
332
- r = await sbx.runCode("import pandas as pd; pd.DataFrame({'a':[1,2]})");
333
- r.results[0].html; // the DataFrame as an HTML table (also .text)
283
+ await withSandbox({ setup: "pip install pandas matplotlib" }, async (sbx) => {
284
+ await sbx.writeFile("data.csv", "a,b\n1,2\n3,4\n");
285
+ const r = await sbx.runCode("import pandas as pd; pd.read_csv('data.csv').describe()");
286
+ r.results[0].html; // the DataFrame as an HTML table
334
287
  });
335
288
  ```
336
289
 
337
- Capture never touches `stdout`/`stderr`/`exitCode`; a statement returning `None` yields no result. You
338
- can still WRITE an artifact to the workspace and `readFile` it if you prefer.
339
-
340
- **Warm kernel (kill the interpreter boot).** Each `runCode` starts a **fresh** interpreter, paying the
341
- CPython boot (~12 ms) every call. When you run many cells that share state (a REPL, a notebook, an
342
- agent's tool loop), open a `kernel()`: ONE warm interpreter in a long-lived box, fed cells over a pipe.
343
- In-memory state persists across cells and the per-cell cost drops from ~14 ms to **sub-millisecond**
344
- (~300x). Same rich `results` capture as `runCode`.
345
-
346
- ```js
347
- await kern.withSandbox(async (sbx) => {
348
- const k = await sbx.kernel();
349
- try {
350
- await k.runCode("import numpy as np; a = np.arange(1_000_000)"); // imports paid once
351
- const r = await k.runCode("a.sum()"); // 'a' is still here; ~sub-ms
352
- console.log(r.results[0].text); // 499999500000
353
- } finally {
354
- await k.close(); // tears the box down
355
- }
356
- });
357
- ```
290
+ Capture never touches `stdout`, `stderr` or `exitCode`. Pass `onStdout` / `onStderr` to stream output
291
+ as it arrives (best-effort: a slow callback drops chunks rather than stalling the box).
358
292
 
359
- The trade vs `runCode`: cells in a kernel share one process and one box, so it is call-fast but not
360
- call-isolated (still network-off and resource-capped; a fresh session or kernel is clean). An uncaught
361
- error is confined (`exitCode` 1, traceback on `stderr`, the kernel keeps serving); a per-cell `timeoutS`
362
- tears the kernel down (a running cell cannot be interrupted), after which it refuses further cells.
363
-
364
- **Live output.** Pass `onStdout` / `onStderr` to stream each chunk as it arrives. The callback is
365
- best-effort, not lossless: a SLOW callback drops chunks rather than applying backpressure to the box
366
- (the full capped output is always in `result.stdout`).
367
-
368
- **Checkpoints.** `sbx.snapshot(dest)` writes a portable `.tar.gz` of the workspace (a FILESYSTEM
369
- checkpoint, not memory); `sbx.restore(src)` extracts it back, refusing absolute / `..` / symlink
370
- members. Interoperable with `tar` and the Python binding (both write plain USTAR, so a workspace path
371
- must be under 100 bytes). The Node path uses a hand-rolled tar reader,
372
- so while it is new it is **opt-in**: set `KERN_SANDBOX_SNAPSHOT=1` to enable it (it fails closed with a
373
- clear error otherwise). The Python binding uses the stdlib `tarfile` and has no such gate.
293
+ `snapshot(dest)` and `restore(src)` write a portable `.tar.gz` checkpoint of the **workspace**;
294
+ `restore` refuses absolute, `..` and symlink members. Nothing in `/tmp` is on it, because a tmpfs is
295
+ on no layer.
374
296
 
375
297
  ## Honest threat model
376
298
 
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.1.40";
39
+ const VERSION = "0.1.41";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-sandbox",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "kern is a fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and AI-generated code; kern-sandbox is its Node/TypeScript binding. Run untrusted or agent-generated code (Python/JS/Bash) in a real, kernel-enforced box in single-digit milliseconds, with no cloud, no account and no VM.",
5
5
  "keywords": [
6
6
  "sandbox",