kern-sandbox 0.2.23 → 0.2.25
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 +30 -4
- package/index.d.ts +6 -0
- package/index.js +17 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ A non-zero exit from *your code* is **not** a fault (`fault` stays `null`): it i
|
|
|
140
140
|
| `oom` | the kernel's OOM killer took the box against its own memory cap. Read from a descriptor the code in the box cannot write, so it is an observation and not a guess from the exit code |
|
|
141
141
|
| `killed` | SIGKILL with **no** OOM reported: an external kill (`kern stop`, a signal, the host out of memory), or a cap that did not bind here, which the message names |
|
|
142
142
|
| `exec_failed` | the box started, the command did not exist inside it. `{language:"node"}` on an image with no `node` is the ordinary way there; the message names the binary AND the image |
|
|
143
|
-
| `startup_failed` | the box never ran, and kern said why in `stderr`. Two shapes: your `timeoutS` fired while kern was still BUILDING the box (run it again:
|
|
143
|
+
| `startup_failed` | the box never ran, and kern said why in `stderr`. Two shapes: your `timeoutS` fired while kern was still BUILDING the box (run it again: if the second call is fast it was a cold image read, and if it is not, look for a bind source on a dead NFS export), or kern refused to build it at all (an image that cannot be pulled, a mount it will not make). The cold read is the FIRST call on a new machine and it is not small: 38 s for an arm64 image on a Raspberry Pi 5 here, against 0.1 s warm |
|
|
144
144
|
|
|
145
145
|
```js
|
|
146
146
|
const r = await kern.runCode("while True: pass", { timeoutS: 5 });
|
|
@@ -222,13 +222,13 @@ is the middle one, and usually the one an agent wants:
|
|
|
222
222
|
|
|
223
223
|
**`network: true` includes the host's LOOPBACK, which is where unauthenticated services live.** It
|
|
224
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
|
|
225
|
+
`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
226
|
and a developer's laptop is where a database, a Redis and a dashboard sit bound to localhost with no
|
|
227
227
|
password. The same connect is refused under the default `network: false`, and `egressAllow` refuses
|
|
228
228
|
it too, because that one goes through kern's proxy rather than through the host's stack.
|
|
229
229
|
|
|
230
230
|
```js
|
|
231
|
-
await withSandbox({ egressAllow: ["pypi.org", "files.pythonhosted.org"] }, async (sbx) => { /* ... */ });
|
|
231
|
+
await kern.withSandbox({ egressAllow: ["pypi.org", "files.pythonhosted.org"] }, async (sbx) => { /* ... */ });
|
|
232
232
|
```
|
|
233
233
|
|
|
234
234
|
The box stays in its own network namespace and reaches the internet only through kern's filtering
|
|
@@ -293,7 +293,7 @@ bare expression**, every **`display(obj)`**, and **every open matplotlib figure
|
|
|
293
293
|
no `savefig`. Accessors: `.png`, `.jpeg`, `.html`, `.svg`, `.markdown`, `.json`, `.text`.
|
|
294
294
|
|
|
295
295
|
```js
|
|
296
|
-
await withSandbox({ setup: "pip install pandas matplotlib" }, async (sbx) => {
|
|
296
|
+
await kern.withSandbox({ setup: "pip install pandas matplotlib" }, async (sbx) => {
|
|
297
297
|
await sbx.writeFile("data.csv", "a,b\n1,2\n3,4\n");
|
|
298
298
|
const r = await sbx.runCode("import pandas as pd; pd.read_csv('data.csv').describe()");
|
|
299
299
|
r.results[0].html; // the DataFrame as an HTML table
|
|
@@ -317,6 +317,32 @@ gVisor. The wider denylist is the opt-out (`KERN_SECCOMP=denylist`), and `securi
|
|
|
317
317
|
bundles the allowlist with `--cap-drop ALL` + `--read-only`. See the project's
|
|
318
318
|
[SECURITY.md](https://github.com/getkern/kern/blob/main/SECURITY.md).
|
|
319
319
|
|
|
320
|
+
**Two jobs, and the handoff is the point.** A microVM product (Docker Sandboxes, Firecracker, Kata,
|
|
321
|
+
gVisor) gives the code a kernel of its own, and that is the right answer when the code is actively
|
|
322
|
+
hostile or belongs to someone else. It costs what a machine costs: measured here against `sbx`
|
|
323
|
+
0.43.0 on the same laptop, half a second per command in a live sandbox and about three seconds to
|
|
324
|
+
create one, against 2 ms and 4 ms for kern, with `uname -r` inside reading its own kernel there and
|
|
325
|
+
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, a deadline applied from outside the box.
|
|
327
|
+
Pick by which job you have, not by the ratio.
|
|
328
|
+
|
|
329
|
+
**What the box does NOT hide from the code inside it.** The caps are real and the kernel enforces
|
|
330
|
+
them where it can, but the box still reads the HOST's numbers for things nothing charges it for:
|
|
331
|
+
`df` on the workspace reports the host's filesystem, because that is what it is, a bind mount with
|
|
332
|
+
no quota, and `nproc` reports the host's core count even under a `cpus` cap, which caps TIME and not
|
|
333
|
+
the count (measured here: 28 inside a box capped at 0.5 cores, 28 outside). Anything that sizes
|
|
334
|
+
itself from a cgroup-unaware API is in the same family: Go's `GOMAXPROCS`, some JVMs, `ray`-style
|
|
335
|
+
CPU detection. `memoryMb` and `pids` ARE enforced and visible as limits, but ONLY where the host
|
|
336
|
+
gives kern a delegated cgroup: on one that does not (a root shell with no user manager, some CI
|
|
337
|
+
runners) kern warns and the box runs UNCAPPED. `kern doctor` says which path a host takes, and
|
|
338
|
+
`requireLimits: true` refuses to start rather than run a box whose caps are decoration.
|
|
339
|
+
|
|
340
|
+
**`npm install kern-sandbox` does not install the sandbox.** The binding drives a `kern` binary it
|
|
341
|
+
finds on `PATH` or in `$KERN_BIN`, and that is a SECOND thing to install and to keep current: a
|
|
342
|
+
binary that is not kern is refused by name, but an OLDER kern runs fine and answers fewer questions,
|
|
343
|
+
because the fault taxonomy reads bytes only newer builds write. If a verdict looks wrong, print
|
|
344
|
+
`kern --version` before anything else.
|
|
345
|
+
|
|
320
346
|
## License
|
|
321
347
|
|
|
322
348
|
[Apache-2.0](https://github.com/getkern/kern/blob/main/LICENSE).
|
package/index.d.ts
CHANGED
|
@@ -71,6 +71,12 @@ export class ExecutionResult {
|
|
|
71
71
|
results: Result[];
|
|
72
72
|
/** True iff the code exited 0 AND no sandbox fault fired. */
|
|
73
73
|
readonly success: boolean;
|
|
74
|
+
/** `stderr` with kern's own `note:`/`warning:` lines removed: what the CODE wrote. Feed this to a
|
|
75
|
+
* model, not `stderr`, or kern's diagnostics read as the workload's. */
|
|
76
|
+
readonly codeStderr: string;
|
|
77
|
+
/** The complement of `codeStderr`: the lines kern wrote about itself, one per entry. `stderr`
|
|
78
|
+
* still holds both, in order. */
|
|
79
|
+
readonly runtimeNotes: string[];
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
/** A PROGRAMMER/config error, THROWN: bad argument, illegal mount, or `kern` not installed. */
|
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.25";
|
|
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)
|
|
@@ -1972,6 +1972,17 @@ class Sandbox {
|
|
|
1972
1972
|
// -- workspace file I/O (host-direct; single-uid -> box files are host-owned) ---------------------
|
|
1973
1973
|
|
|
1974
1974
|
_wsPath(rel) {
|
|
1975
|
+
// A NUL BYTE IS REFUSED HERE, in the same words Python uses. Without it, node's own check fires
|
|
1976
|
+
// deeper and the caller reads "The argument 'path' must be a string, Uint8Array...", which names
|
|
1977
|
+
// the argument's TYPE for a path that is a perfectly good string. Found through the Python MCP
|
|
1978
|
+
// server, where the same input surfaced as `internal error: ValueError`; both bindings answer the
|
|
1979
|
+
// question that was asked now.
|
|
1980
|
+
if (typeof rel === "string" && rel.includes("\u0000")) {
|
|
1981
|
+
throw new SandboxError(
|
|
1982
|
+
`path contains a NUL byte: ${JSON.stringify(rel)}. A NUL terminates a path for every API ` +
|
|
1983
|
+
`below this one, so the name cannot mean what it appears to say`,
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1975
1986
|
// Lexical containment: normalize `..`/`.`, require it stays under the workspace base. Symlinks in
|
|
1976
1987
|
// the final component are neutralized by O_NOFOLLOW on the actual open below.
|
|
1977
1988
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kern-sandbox",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.25",
|
|
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",
|