kern-sandbox 0.2.24 → 0.2.26
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/README.md +20 -7
- package/index.js +6 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,10 +12,17 @@ kernel-enforced sandbox out of one static binary, with no daemon, no VM and no c
|
|
|
12
12
|
tool-call, a model's generated snippet, a CI step: code that runs before anyone reads it gets its own
|
|
13
13
|
box, and the box is thrown away after.
|
|
14
14
|
|
|
15
|
-
Network off, memory and PID caps the kernel enforces
|
|
16
|
-
allowlist, and a wall-clock deadline the binding
|
|
17
|
-
cannot outlive it.
|
|
18
|
-
|
|
15
|
+
Network off, memory and PID caps the kernel enforces **where your host delegates them**,
|
|
16
|
+
capabilities dropped, a deny-by-default seccomp allowlist, and a wall-clock deadline the binding
|
|
17
|
+
applies from **outside** the box, so code that hangs cannot outlive it. Whether the caps bind is a property of your
|
|
18
|
+
HOST, not of kern: they need a delegated cgroup, which a desktop session has and a bare root shell
|
|
19
|
+
in a container often does not. `kern doctor` says which you have, and `requireLimits: true` makes an
|
|
20
|
+
unenforceable cap FATAL: the box refuses to start rather than run uncapped. It arrives the way every
|
|
21
|
+
other refusal does, as `fault.type === "startup_failed"` on the result, NOT as an exception from the
|
|
22
|
+
constructor, so a caller that only catches exceptions will walk past it.
|
|
23
|
+
|
|
24
|
+
Dependency-free: it shells out to the `kern` binary and does not re-implement isolation in
|
|
25
|
+
JavaScript.
|
|
19
26
|
|
|
20
27
|
**Your loop reads a field, not a stack trace.** A timeout, an OOM-kill, a blocked syscall or a missing
|
|
21
28
|
interpreter each arrive as a typed `fault` on the result, beside stdout and the exit code, so the
|
|
@@ -222,7 +229,7 @@ is the middle one, and usually the one an agent wants:
|
|
|
222
229
|
|
|
223
230
|
**`network: true` includes the host's LOOPBACK, which is where unauthenticated services live.** It
|
|
224
231
|
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
|
|
232
|
+
`127.0.0.1`: a test's cell connected to `127.0.0.1:22` and read back `SSH-2.0-OpenSSH_9.6p1`,
|
|
226
233
|
and a developer's laptop is where a database, a Redis and a dashboard sit bound to localhost with no
|
|
227
234
|
password. The same connect is refused under the default `network: false`, and `egressAllow` refuses
|
|
228
235
|
it too, because that one goes through kern's proxy rather than through the host's stack.
|
|
@@ -254,11 +261,16 @@ or Redis connection under `egressAllow` cannot resolve its host. For a database,
|
|
|
254
261
|
`prewarm: N` keeps N boxes started in advance, each holding a booted interpreter that has run nothing,
|
|
255
262
|
and refills in the background while your agent thinks. Measured on `python:3.12-slim`:
|
|
256
263
|
|
|
257
|
-
| | first call | p50 within the burst |
|
|
264
|
+
| `runCode` | first call | p50 within the burst |
|
|
258
265
|
|---|---:|---:|
|
|
259
266
|
| default | 30.9 ms | 14.2 ms |
|
|
260
267
|
| `prewarm: 4` | 0.9 ms | **0.8 ms** |
|
|
261
268
|
|
|
269
|
+
**The number is `runCode`, not `run()`.** A prewarmed box holds a BOOTED INTERPRETER, so what the
|
|
270
|
+
pool removes is the interpreter's cost and not the box's. `run(["true"])` starts a fresh box either
|
|
271
|
+
way and reads the same either way: measured, 4.74 ms with the pool against 4.67 ms without it. Time
|
|
272
|
+
the wrong call and prewarming looks like a no-op.
|
|
273
|
+
|
|
262
274
|
**The pool covers a burst, not a rate**, and it refills in the background: past N the cost returns to
|
|
263
275
|
the default, and a call made immediately after construction pays the default until the boxes exist.
|
|
264
276
|
|
|
@@ -323,7 +335,8 @@ hostile or belongs to someone else. It costs what a machine costs: measured here
|
|
|
323
335
|
0.43.0 on the same laptop, half a second per command in a live sandbox and about three seconds to
|
|
324
336
|
create one, against 2 ms and 4 ms for kern, with `uname -r` inside reading its own kernel there and
|
|
325
337
|
the host's here. kern is for the OTHER job, the one an agent loop does a thousand times: a cell per
|
|
326
|
-
call, network off, memory and pids the kernel enforces
|
|
338
|
+
call, network off, memory and pids the kernel enforces where the host delegates them, a
|
|
339
|
+
deadline applied from outside the box.
|
|
327
340
|
Pick by which job you have, not by the ratio.
|
|
328
341
|
|
|
329
342
|
**What the box does NOT hide from the code inside it.** The caps are real and the kernel enforces
|
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.
|
|
39
|
+
const VERSION = "0.2.26";
|
|
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
|
|
@@ -458,12 +458,12 @@ function kernStateDirs() {
|
|
|
458
458
|
const uid = process.getuid();
|
|
459
459
|
const home = os.homedir();
|
|
460
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
|
|
461
|
+
// runtime dir was already spelled both ways; the other three were not, and an independent test measured the
|
|
462
462
|
// consequence in one process - with `XDG_DATA_HOME=/tmp/xdh2`, `~/.local/share/kern` was ACCEPTED
|
|
463
463
|
// and still held `builds` and `volumes`. The variable answers "which kern will this SDK spawn",
|
|
464
464
|
// which is the right input for the guard, but data a previous run left on disk does not move with it.
|
|
465
465
|
//
|
|
466
|
-
// The DATA dir itself joined the list after the same
|
|
466
|
+
// The DATA dir itself joined the list after the same independent test took the refused two as the shape of
|
|
467
467
|
// the rule and looked for the rest: it holds `volumes/`, the CONTENT of every named volume on this
|
|
468
468
|
// host, and `builds/`, the records a later image is assembled from.
|
|
469
469
|
const known = [
|
|
@@ -641,7 +641,7 @@ const VERIFIED_KERN = new Set();
|
|
|
641
641
|
|
|
642
642
|
/** Refuse a binary that does not IDENTIFY ITSELF as kern. Throws `SandboxError` if it does not.
|
|
643
643
|
*
|
|
644
|
-
* MEASURED, and found by an
|
|
644
|
+
* MEASURED, and found by an independent test running the positive control this project wrote for him:
|
|
645
645
|
* with `KERN_BIN=/bin/true` a call returned `success: true, exitCode: 0, fault: null` and an empty
|
|
646
646
|
* stdout. The code never ran and the caller was told it had. Any `kern` earlier in `PATH` that is not
|
|
647
647
|
* kern does this: a leftover wrapper, a shim, a no-op. An agent loop reads `success` and every
|
|
@@ -1130,7 +1130,7 @@ function kernReportedOom(stderr) {
|
|
|
1130
1130
|
*
|
|
1131
1131
|
* WHAT THIS REPLACED: a list of eleven message OPENINGS, each added after a caller measured a
|
|
1132
1132
|
* `fault: null`. Enumerating the error texts of a binary with hundreds of them behind one printer cannot
|
|
1133
|
-
* be finished, and an
|
|
1133
|
+
* be finished, and an independent test ended the argument with `image: ""`, whose
|
|
1134
1134
|
* `error: bad image reference: empty` was in none of the eleven. */
|
|
1135
1135
|
const KERN_SPEAKING = ["error: ", "kern:"];
|
|
1136
1136
|
|
|
@@ -1569,7 +1569,7 @@ class Sandbox {
|
|
|
1569
1569
|
* that will never exist would both litter the workspace and collide with itself. A dry argv is for
|
|
1570
1570
|
* COMPARING, never for running. */
|
|
1571
1571
|
_baseArgv(name, { network, timeoutS, isSetup = false, dry = false }) {
|
|
1572
|
-
// IDENTITY IS RE-ASSERTED PER BOX, not once per Sandbox, and an
|
|
1572
|
+
// IDENTITY IS RE-ASSERTED PER BOX, not once per Sandbox, and an independent test is the reason. He
|
|
1573
1573
|
// overwrote the verified binary IN PLACE with `/bin/true` while a Sandbox was open: the next call
|
|
1574
1574
|
// correctly refused to call an empty run a success, and the message it refused with quoted the
|
|
1575
1575
|
// version from the FIRST verification - stating that a file which now prints `true (GNU coreutils)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kern-sandbox",
|
|
3
|
-
"version": "0.2.
|
|
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
|
|
3
|
+
"version": "0.2.26",
|
|
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, one per call, with no cloud, no account and no VM.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sandbox",
|
|
7
7
|
"kern",
|