kern-sandbox 0.2.20 → 0.2.23

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 +23 -10
  2. package/index.js +28 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -78,14 +78,20 @@ spawns a **fresh** box on that shared workspace, so file state persists but in-m
78
78
  (write to disk for continuity). `withSandbox` opens the session and cleans it up, even on throw:
79
79
 
80
80
  ```js
81
- await kern.withSandbox({ setup: "pip install pandas" }, async (sbx) => {
82
- await sbx.writeFile("data.csv", csvBytes);
83
- const r = await sbx.runCode(
84
- "import pandas as pd; print(pd.read_csv('data.csv').describe())",
85
- );
86
- console.log(r.stdout); // network off, capped, isolated
87
- const chart = await sbx.readFile("out.png");
88
- });
81
+ await kern.withSandbox(
82
+ { setup: "pip install pandas matplotlib", memoryMb: 1536 },
83
+ async (sbx) => {
84
+ await sbx.writeFile("data.csv", "a,b\n1,2\n3,4\n");
85
+ const r = await sbx.runCode(
86
+ "import matplotlib; matplotlib.use('Agg')\n" +
87
+ "import pandas as pd, matplotlib.pyplot as plt\n" +
88
+ "df = pd.read_csv('data.csv'); print(df.describe())\n" +
89
+ "df.plot(); plt.savefig('out.png')",
90
+ );
91
+ console.log(r.stdout); // network off, capped, isolated
92
+ const chart = await sbx.readFile("out.png"); // a Uint8Array of the PNG
93
+ },
94
+ );
89
95
  ```
90
96
 
91
97
  `setup` is the **only** moment the network is on (a separate box that installs deps into the workspace
@@ -164,8 +170,8 @@ Every relaxing option says so in its name or docs:
164
170
  - **mounts refused**: the host's own sources (`/`, `/etc`, `/root`, `/boot`, `/proc`, `/sys`, `/dev`,
165
171
  `$HOME`, the docker socket), any path with a **credential directory** in it (`.ssh`, `.aws`, `.gnupg`,
166
172
  `.kube`, `.docker`, `.azure`, `.password-store`, `.netrc`, `.git-credentials`, `.pypirc`, `.npmrc`),
167
- **kern's own state** (`$XDG_RUNTIME_DIR/kern`, the image cache, the config dir: the sandbox's control
168
- plane), and escaping targets.
173
+ **kern's own state** (`$XDG_RUNTIME_DIR/kern`, the image cache, the config dir, the data dir that
174
+ holds every named volume: the sandbox's control plane), and escaping targets.
169
175
  - **workspace I/O contained**: `writeFile`/`readFile` reject `..` escapes, open the final component
170
176
  `O_NOFOLLOW` so a symlink the box plants cannot redirect host I/O, and refuse anything that is not a
171
177
  REGULAR file (see the notes for the FIFO that made a read hang).
@@ -214,6 +220,13 @@ memory, scratch that does not survive a call, and the two writable places a tool
214
220
  `network: false` gives the run phase no network and `network: true` gives it the host's. `egressAllow`
215
221
  is the middle one, and usually the one an agent wants:
216
222
 
223
+ **`network: true` includes the host's LOOPBACK, which is where unauthenticated services live.** It
224
+ puts the box in the host's network namespace, so `127.0.0.1` inside the box is the host's
225
+ `127.0.0.1`: a reviewer's cell connected to `127.0.0.1:22` and read back `SSH-2.0-OpenSSH_9.6p1`,
226
+ and a developer's laptop is where a database, a Redis and a dashboard sit bound to localhost with no
227
+ password. The same connect is refused under the default `network: false`, and `egressAllow` refuses
228
+ it too, because that one goes through kern's proxy rather than through the host's stack.
229
+
217
230
  ```js
218
231
  await withSandbox({ egressAllow: ["pypi.org", "files.pythonhosted.org"] }, async (sbx) => { /* ... */ });
219
232
  ```
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.20";
39
+ const VERSION = "0.2.23";
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
@@ -456,16 +456,35 @@ const REFUSED_MOUNT_SOURCES = new Set([
456
456
  * follow the environment and a service manager moves them. */
457
457
  function kernStateDirs() {
458
458
  const uid = process.getuid();
459
- const runtime = process.env.XDG_RUNTIME_DIR || `/run/user/${uid}`;
460
459
  const home = os.homedir();
461
- const cache = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
462
- const config = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
463
- return [
464
- [path.join(runtime, "kern"), "kern's runtime state (the registry, instance dirs, netns handles and exit files of every box you are running)"],
465
- [`/run/user/${uid}/kern`, "kern's runtime state"],
466
- [path.join(cache, "kern"), "kern's image cache (a box that writes it poisons the rootfs a later box runs)"],
467
- [path.join(config, "kern"), "kern's configuration (the profiles a later box may be given)"],
460
+ // EACH DIRECTORY TWICE: where the environment says it is, AND where XDG says it is by default. The
461
+ // runtime dir was already spelled both ways; the other three were not, and a reviewer measured the
462
+ // consequence in one process - with `XDG_DATA_HOME=/tmp/xdh2`, `~/.local/share/kern` was ACCEPTED
463
+ // and still held `builds` and `volumes`. The variable answers "which kern will this SDK spawn",
464
+ // which is the right input for the guard, but data a previous run left on disk does not move with it.
465
+ //
466
+ // The DATA dir itself joined the list after the same reviewer took the refused two as the shape of
467
+ // the rule and looked for the rest: it holds `volumes/`, the CONTENT of every named volume on this
468
+ // host, and `builds/`, the records a later image is assembled from.
469
+ const known = [
470
+ [process.env.XDG_RUNTIME_DIR, `/run/user/${uid}`,
471
+ "kern's runtime state (the registry, instance dirs, netns handles and exit files of every box you are running)"],
472
+ [process.env.XDG_CACHE_HOME, path.join(home, ".cache"),
473
+ "kern's image cache (a box that writes it poisons the rootfs a later box runs)"],
474
+ [process.env.XDG_CONFIG_HOME, path.join(home, ".config"),
475
+ "kern's configuration (the profiles a later box may be given)"],
476
+ [process.env.XDG_DATA_HOME, path.join(home, ".local", "share"),
477
+ "kern's data (every named volume on this host, and the build records a later image is assembled from)"],
468
478
  ];
479
+ // A MAP, so the usual case where the variable IS the default collapses to one entry instead of
480
+ // listing the same directory twice with two different accounts of what it is.
481
+ const out = new Map();
482
+ for (const [configured, dflt, what] of known)
483
+ for (const base of [configured || dflt, dflt]) {
484
+ const dir = path.join(base, "kern");
485
+ if (!out.has(dir)) out.set(dir, what);
486
+ }
487
+ return [...out];
469
488
  }
470
489
 
471
490
  /** Credential directories, refused as a COMPONENT anywhere in the source. The set above is absolute
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-sandbox",
3
- "version": "0.2.20",
3
+ "version": "0.2.23",
4
4
  "description": "kern is a fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and LLM-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",