kern-sandbox 0.1.19 → 0.1.20

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **[kern](https://github.com/getkern/kern)** is a fast, rootless sandbox and virtual resource
4
4
  runtime for any workload, including untrusted and AI-generated code: a real, kernel-enforced box
5
- that starts in **3.4 ms** from an OCI image, out of one **~1.8 MB** binary, with no daemon.
5
+ that starts in **3.4 ms** from an OCI image, out of one **1.58 MB** binary, with no daemon.
6
6
  **kern-sandbox**
7
7
  is its Node / TypeScript binding: run untrusted or agent-generated code in a fresh, isolated box, from Node.
8
8
 
@@ -11,7 +11,7 @@ package is on PyPI: [`kern-sandbox`](https://pypi.org/project/kern-sandbox/).
11
11
 
12
12
  It is a thin, dependency-free wrapper around the [`kern`](https://github.com/getkern/kern) binary:
13
13
  a fresh, isolated box per call, network off by default, hard resource caps, and a timeout the binding
14
- itself enforces. Kernel-enforced isolation (namespaces, cgroups v2, seccomp), local, about 1.8 MB, with no cloud, no account, no VM.
14
+ itself enforces. Kernel-enforced isolation (namespaces, cgroups v2, seccomp), local, about 1.58 MB, with no cloud, no account, no VM.
15
15
 
16
16
  ```js
17
17
  const kern = require("kern-sandbox");
@@ -98,7 +98,8 @@ A non-zero exit from *your code* is **not** a fault (`fault` stays `null`): it i
98
98
  |---|---|
99
99
  | `timeout` | the call exceeded `timeoutS`; the binding killed the box |
100
100
  | `escape_blocked` | a syscall was blocked by the seccomp filter (SIGSYS) |
101
- | `killed` | the box was SIGKILLed, most often the cgroup OOM-killer |
101
+ | `oom` | the box was SIGKILLed with a `memoryMb` cap in effect: a breached `memory.max` is the cgroup OOM-killer (`memory.oom.group=1` kills the whole box) |
102
+ | `killed` | the box was SIGKILLed with **no** memory cap set, so the cause is ambiguous (host pressure, an external kill) and is not attributed to OOM |
102
103
 
103
104
  A box that fails to **start** (kern exits 125: a mount refused at runtime, an unmappable `--user`, a
104
105
  seccomp/AppArmor/cgroup setup error, or a pull/image error) is **thrown** as a `SandboxError`, not
@@ -225,11 +226,12 @@ clear error otherwise). The Python binding uses the stdlib `tarfile` and has no
225
226
  ## Honest threat model
226
227
 
227
228
  kern is a **kernel-boundary** sandbox for **your own or semi-trusted** code (CI, dev, edge, your
228
- agents' code). Its seccomp filter is a **denylist**: right for semi-trusted agent code, **not** a hard
229
- boundary against deliberately hostile multi-tenant code. For that, reach for a microVM (Firecracker /
230
- Kata) or gVisor. A deny-by-default seccomp **allowlist** ships as opt-in today: pass
231
- `securityProfile: "untrusted"` (or the `KERN_SECCOMP=allowlist` env); making it the default is future
232
- work. See the project's [SECURITY.md](https://github.com/getkern/kern/blob/main/SECURITY.md).
229
+ agents' code). Its default seccomp filter is a **deny-by-default allowlist** (moby's own default
230
+ filter minus kern's 35 escape syscalls): right for semi-trusted agent code, **not** a hard boundary
231
+ against deliberately hostile multi-tenant code. For that, reach for a microVM (Firecracker / Kata) or
232
+ gVisor. The wider denylist is the opt-out (`KERN_SECCOMP=denylist`), and `securityProfile: "untrusted"`
233
+ bundles the allowlist with `--cap-drop ALL` + `--read-only`. See the project's
234
+ [SECURITY.md](https://github.com/getkern/kern/blob/main/SECURITY.md).
233
235
 
234
236
  ## License
235
237
 
package/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Run LLM/agent-generated code in a fast, local, daemonless kernel sandbox.
3
3
 
4
4
  /** What stopped the code at the SANDBOX level. Reported as data on a result, never thrown. */
5
- export type SandboxFaultType = "timeout" | "escape_blocked" | "killed" | "startup_failed";
5
+ export type SandboxFaultType = "timeout" | "oom" | "escape_blocked" | "killed" | "startup_failed";
6
6
 
7
7
  export interface SandboxFault {
8
8
  type: SandboxFaultType;
package/index.js CHANGED
@@ -1171,8 +1171,15 @@ class Sandbox {
1171
1171
  );
1172
1172
  if (rc === EXIT_SIGSYS || signal === "SIGSYS")
1173
1173
  return sandboxFault("escape_blocked", "a syscall was blocked by the seccomp filter (SIGSYS)");
1174
- if (rc === EXIT_SIGKILL || signal === "SIGKILL")
1175
- return sandboxFault("killed", "the box was killed (SIGKILL) - likely out of memory (exit 137)");
1174
+ if (rc === EXIT_SIGKILL || signal === "SIGKILL") {
1175
+ // A memory-capped box SIGKILLed is the cgroup OOM-killer - what a breached memory.max does (kern
1176
+ // sets memory.oom.group=1, so the whole box dies at once). Claim `oom` when a --memory cap was in
1177
+ // effect: that is a fact WE set on the argv, not the workload's stderr, so it costs no security
1178
+ // discipline. Uncapped, the cause is ambiguous (host pressure, an external kill) - keep `killed`.
1179
+ if (this.memoryMb !== null)
1180
+ return sandboxFault("oom", "the box exceeded its memory cap and was OOM-killed (SIGKILL, exit 137)");
1181
+ return sandboxFault("killed", "the box was killed (SIGKILL); no memory cap was set to attribute it to OOM");
1182
+ }
1176
1183
  if (rc === EXIT_SIGTERM || signal === "SIGTERM")
1177
1184
  return sandboxFault("timeout", "the box exceeded its time limit (reaped by kern's timeout backstop)");
1178
1185
  // Box-not-started: a non-zero exit whose stderr carries kern's OWN setup markers (printed by the
@@ -1754,8 +1761,8 @@ class Kernel {
1754
1761
  return this._teardownResult("killed", `the kernel reply exceeded the ${this._cap}-byte cap`, started);
1755
1762
  if (reply === null) {
1756
1763
  const err = this._stderr.toString("utf8");
1757
- const kind = looksLikeStartupFailure(err) ? "startup_failed" : "killed";
1758
- return this._teardownResult(kind, err.trim() || "the kernel box exited", started);
1764
+ const [kind, dflt] = this._kernelDeathFault(err);
1765
+ return this._teardownResult(kind, err.trim() || dflt, started);
1759
1766
  }
1760
1767
  return this._resultFromReply(reply, started);
1761
1768
  }
@@ -1801,6 +1808,19 @@ class Kernel {
1801
1808
  });
1802
1809
  }
1803
1810
 
1811
+ /** Why the resident kernel box died mid-cell, as `[type, defaultMessage]`. A kern setup marker on
1812
+ * stderr means it never came up (startup_failed). Otherwise, with a memoryMb cap in force, the cgroup
1813
+ * OOM-killer is the dominant cause of a kernel that disappears while running a cell - the SAME
1814
+ * attribution (from the same non-workload signal, the --memory flag WE set) as the one-shot _classify
1815
+ * SIGKILL path. A kernel death has no per-cell exit code to route through _classify, so the OOM
1816
+ * attribution lives here too. Uncapped, the cause is ambiguous -> `killed`. */
1817
+ _kernelDeathFault(err) {
1818
+ if (looksLikeStartupFailure(err)) return ["startup_failed", "the kernel box failed to start"];
1819
+ if (this._sbx.memoryMb !== null)
1820
+ return ["oom", "the kernel box was OOM-killed (it exceeded its memory cap)"];
1821
+ return ["killed", "the kernel box exited"];
1822
+ }
1823
+
1804
1824
  _teardownResult(type, message, started) {
1805
1825
  this._kill();
1806
1826
  // Same rule as the one-shot path: a box that never STARTED (the kernel failed to boot) throws, it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-sandbox",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
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",