kern-sandbox 0.2.4 → 0.2.6
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 +31 -52
- package/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,11 +28,11 @@ const r = await kern.runCode("print(sum(range(100)))");
|
|
|
28
28
|
console.log(r.stdout, r.success); // "4950\n" true
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
TypeScript types ship in the
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
31
|
+
TypeScript types ship in the package. `Buffer` is in the public surface, so a TypeScript consumer needs
|
|
32
|
+
`@types/node` **and** a `tsconfig.json` that includes it. MEASURED with `tsc` 7.0.2: without the types,
|
|
33
|
+
8 errors saying `Cannot find name 'Buffer'`; with the types installed but no `tsconfig.json`, the same 8;
|
|
34
|
+
with `{ "compilerOptions": { "types": ["node"] } }`, zero. Installing the package `tsc` names is half the
|
|
35
|
+
remedy, which is worth stating because the error message only hints at the other half.
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
38
|
import { runCode, withSandbox, Sandbox } from "kern-sandbox";
|
|
@@ -120,35 +120,16 @@ const r = await kern.runCode("console.log([1,2,3].map(x => x * x))", {
|
|
|
120
120
|
| `truncated` | output hit the cap and overflow was discarded |
|
|
121
121
|
|
|
122
122
|
A non-zero exit from *your code* is **not** a fault (`fault` stays `null`): it is a normal result.
|
|
123
|
-
|
|
124
|
-
`stderr` is one stream shared by kern and your code, so a note about overlayfs or an undelegated
|
|
125
|
-
cgroup arrives interleaved with the program's own output. That is right for a human reading a
|
|
126
|
-
terminal and wrong for anything that puts `stderr` into a prompt, where it spends context on the
|
|
127
|
-
runtime's housekeeping and reads like an error the code produced. `codeStderr` is the same string
|
|
128
|
-
without those lines, and nothing is hidden: `runtimeNotes` holds exactly what was taken out. The
|
|
129
|
-
LangChain tool and the MCP server already use it.
|
|
130
123
|
`fault` is only set when the **sandbox** acted:
|
|
131
124
|
|
|
132
125
|
| `fault.type` | when |
|
|
133
126
|
|---|---|
|
|
134
127
|
| `timeout` | the call exceeded `timeoutS`; the binding killed the box |
|
|
135
128
|
| `escape_blocked` | a syscall was blocked by the seccomp filter (SIGSYS) |
|
|
136
|
-
| `oom` |
|
|
137
|
-
| `killed` |
|
|
138
|
-
| `exec_failed` | the box started
|
|
139
|
-
| `startup_failed` |
|
|
140
|
-
|
|
141
|
-
**An enforced `pids` cap produces no fault, and that is deliberate.** When `pids` binds, the refused
|
|
142
|
-
`fork` returns `EAGAIN`. Code that catches it exits 0, so the call reports `fault: null, success:
|
|
143
|
-
true` and a contained fork bomb reads as a successful run. `EAGAIN` is an ordinary errno a program is
|
|
144
|
-
allowed to handle, unlike a SIGKILL it cannot; labelling it a sandbox fault would misreport a process
|
|
145
|
-
that exited cleanly. Code that does **not** catch it dies naming "Resource temporarily unavailable".
|
|
146
|
-
The cap itself is enforced: on WSL2, `pids: 32` blocked at 29 forks while `pids: 256` let 120 through,
|
|
147
|
-
same code and same image.
|
|
148
|
-
|
|
149
|
-
A box that fails to **start** (kern exits 125: a mount refused at runtime, an unmappable `--user`, a
|
|
150
|
-
seccomp/AppArmor/cgroup setup error, or a pull/image error) is **thrown** as a `SandboxError`, not
|
|
151
|
-
returned as a fault, because the code never ran.
|
|
129
|
+
| `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 |
|
|
130
|
+
| `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 |
|
|
131
|
+
| `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 |
|
|
132
|
+
| `startup_failed` | your `timeoutS` fired while kern was still BUILDING the box, so the code never ran. A longer timeout does not help: a bind source on a dead NFS export does this |
|
|
152
133
|
|
|
153
134
|
```js
|
|
154
135
|
const r = await kern.runCode("while True: pass", { timeoutS: 5 });
|
|
@@ -156,6 +137,14 @@ r.success; // false
|
|
|
156
137
|
r.fault.type; // "timeout"
|
|
157
138
|
```
|
|
158
139
|
|
|
140
|
+
A box that fails to **start** is **thrown** as a `SandboxError`, not returned as a fault, because the
|
|
141
|
+
code never ran.
|
|
142
|
+
|
|
143
|
+
`stderr` is one stream shared by kern and your code, so a note about an undelegated cgroup arrives
|
|
144
|
+
interleaved with the program's own output. Right for a human at a terminal, wrong for anything that puts
|
|
145
|
+
`stderr` into a prompt. `codeStderr` is the same string without kern's own lines, and nothing is hidden:
|
|
146
|
+
`runtimeNotes` holds exactly what was taken out.
|
|
147
|
+
|
|
159
148
|
## Safe by default
|
|
160
149
|
|
|
161
150
|
Every relaxing option says so in its name or docs:
|
|
@@ -169,13 +158,9 @@ Every relaxing option says so in its name or docs:
|
|
|
169
158
|
line, so a credential in `env` does not leak into `ps`.
|
|
170
159
|
- **mounts refused**: sensitive host sources (`/`, `/etc`, `/root`, `/proc`, `/sys`, `/dev`, the docker
|
|
171
160
|
socket, `$HOME`) and escaping targets are refused even when asked.
|
|
172
|
-
- **workspace I/O contained**: `writeFile`/`readFile` reject `..` escapes
|
|
173
|
-
`O_NOFOLLOW
|
|
174
|
-
|
|
175
|
-
a box can leave at a name: `mkfifo out.png` used to make `readFile("out.png")` wait for a writer that
|
|
176
|
-
never comes, with no timeout, so the box chose how long the host's call took. The flag alone would be
|
|
177
|
-
worse than the hang, because a non-blocking read of a writer-less FIFO returns zero bytes and the
|
|
178
|
-
call would report an EMPTY FILE. Both halves ship: it returns promptly, and it refuses.
|
|
161
|
+
- **workspace I/O contained**: `writeFile`/`readFile` reject `..` escapes, open the final component
|
|
162
|
+
`O_NOFOLLOW` so a symlink the box plants cannot redirect host I/O, and refuse anything that is not a
|
|
163
|
+
REGULAR file (see the notes for the FIFO that made a read hang).
|
|
179
164
|
|
|
180
165
|
### Options
|
|
181
166
|
|
|
@@ -238,32 +223,26 @@ you did not read.
|
|
|
238
223
|
## Prewarming: a box ready before the call arrives
|
|
239
224
|
|
|
240
225
|
`prewarm: N` keeps N boxes started in advance, each holding a booted interpreter that has run nothing,
|
|
241
|
-
|
|
242
|
-
`python:3.12-slim`, six calls each: **14.2 ms p50 by default against 0.8 ms with `prewarm: 4`**, and
|
|
243
|
-
30.9 ms against 0.9 for the first call.
|
|
226
|
+
and refills in the background while your agent thinks. Measured on `python:3.12-slim`:
|
|
244
227
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
228
|
+
| | first call | p50 within the burst |
|
|
229
|
+
|---|---:|---:|
|
|
230
|
+
| default | 30.9 ms | 14.2 ms |
|
|
231
|
+
| `prewarm: 4` | 0.9 ms | **0.8 ms** |
|
|
249
232
|
|
|
250
|
-
The
|
|
251
|
-
|
|
252
|
-
default cost. N is the burst you want covered, not a throughput knob.
|
|
233
|
+
**The pool covers a burst, not a rate**, and it refills in the background: past N the cost returns to
|
|
234
|
+
the default, and a call made immediately after construction pays the default until the boxes exist.
|
|
253
235
|
|
|
254
|
-
Each prewarmed box serves ONE call and is
|
|
255
|
-
|
|
256
|
-
|
|
236
|
+
Each prewarmed box still serves ONE call and is thrown away, so the isolation is unchanged: only the
|
|
237
|
+
moment of creation moves. The pool key includes the image, the caps and the profiles, so a session never
|
|
238
|
+
receives a box built for another one.
|
|
257
239
|
|
|
258
240
|
```js
|
|
259
|
-
await withSandbox({ image: "python:3.12-slim", prewarm: 4 }, async (sbx) => {
|
|
241
|
+
await kern.withSandbox({ image: "python:3.12-slim", prewarm: 4 }, async (sbx) => {
|
|
260
242
|
const r = await sbx.runCode("print(1)"); // served from the pool
|
|
261
243
|
});
|
|
262
244
|
```
|
|
263
245
|
|
|
264
|
-
The pool key includes the image, the caps and the profiles, so a session with different settings never
|
|
265
|
-
receives a box built for another one.
|
|
266
|
-
|
|
267
246
|
## Run pi's coding tools in a box
|
|
268
247
|
|
|
269
248
|
[`integrations/pi`](https://github.com/getkern/kern/tree/main/integrations/pi) is an extension for
|
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.6";
|
|
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.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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",
|