kern-sandbox 0.1.15 → 0.1.17

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 +4 -1
  2. package/index.js +23 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -99,7 +99,10 @@ A non-zero exit from *your code* is **not** a fault (`fault` stays `null`): it i
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
101
  | `killed` | the box was SIGKILLed, most often the cgroup OOM-killer |
102
- | `startup_failed` | kern could not start the box (bad image, pull error, ...) |
102
+
103
+ A box that fails to **start** (kern exits 125: a mount refused at runtime, an unmappable `--user`, a
104
+ seccomp/AppArmor/cgroup setup error, or a pull/image error) is **thrown** as a `SandboxError`, not
105
+ returned as a fault, because the code never ran.
103
106
 
104
107
  ```js
105
108
  const r = await kern.runCode("while True: pass", { timeoutS: 5 });
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.1.15";
39
+ const VERSION = "0.1.17";
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
@@ -387,8 +387,11 @@ const REFUSED_MOUNT_SOURCES = new Set([
387
387
  "/run/docker.sock",
388
388
  ]);
389
389
 
390
- /** A PROGRAMMER/config error, THROWN: bad argument, illegal mount, or `kern` not installed. Runtime
391
- * sandbox events (timeout, blocked escape, OOM-kill) are NOT thrown - they are data in result.fault. */
390
+ /** A PROGRAMMER/config error, THROWN: bad argument, illegal mount, `kern` not installed, or the box
391
+ * FAILED TO START (kern exits 125 - a mount refused at runtime, an unmappable `--user`, a seccomp or
392
+ * AppArmor setup error). A box that never started ran no user code, so it rejects rather than resolve a
393
+ * hollow result. Runtime sandbox events where the code DID run (timeout, blocked escape, OOM-kill) are
394
+ * NOT thrown - they are data on `result.fault`. */
392
395
  class SandboxError extends Error {
393
396
  constructor(message) {
394
397
  super(message);
@@ -1057,6 +1060,15 @@ class Sandbox {
1057
1060
  const stderr = err.buffer().toString("utf8");
1058
1061
  const rc = toRc(code, signal);
1059
1062
  const fault = this._classify(rc, signal, stderr, timedOut, timeoutS);
1063
+ // A box that FAILED TO START ran no user code, so REJECT rather than resolve a hollow
1064
+ // ExecutionResult (empty stdout). Gated on `rc === 125` (kern's box-not-started code) AND the
1065
+ // startup_failed classification (which requires kern's own stderr marker): the confident pair
1066
+ // that tells a genuine box-not-started apart from a workload that itself exited 125 (no marker ->
1067
+ // fault null -> a normal result). An older kern (127) is returned as a data fault, not thrown.
1068
+ // Runtime events where the code DID run (timeout, OOM, escape) stay as data on `.fault`.
1069
+ if (rc === 125 && fault && fault.type === "startup_failed") {
1070
+ return reject(new SandboxError(fault.message || "the box failed to start"));
1071
+ }
1060
1072
  const files = before ? this._diff(before) : [];
1061
1073
  resolve(
1062
1074
  new ExecutionResult({
@@ -1129,6 +1141,11 @@ class Sandbox {
1129
1141
  return sandboxFault("killed", "the box was killed (SIGKILL) - likely out of memory (exit 137)");
1130
1142
  if (rc === EXIT_SIGTERM || signal === "SIGTERM")
1131
1143
  return sandboxFault("timeout", "the box exceeded its time limit (reaped by kern's timeout backstop)");
1144
+ // Box-not-started: a non-zero exit whose stderr carries kern's OWN setup markers (printed by the
1145
+ // PARENT before the box runs). kern's box-not-started paths BOTH exit 125 AND print a `kern:` marker,
1146
+ // so `rc === 125 && marker` is the reliable signal - the marker is REQUIRED so a workload that merely
1147
+ // exits 125 ITSELF (the code ran and chose 125) is NOT mislabeled. `finish` REJECTS only on rc===125;
1148
+ // a non-125 startup_failed (an older kern's 127, or a forged marker) is returned as DATA, not thrown.
1132
1149
  if (rc !== 0 && looksLikeStartupFailure(stderr))
1133
1150
  return sandboxFault("startup_failed", stderr.trim().slice(0, 500));
1134
1151
  // Any other non-zero exit (incl. 139 SIGSEGV) is the USER's code failing - a normal Result.
@@ -1752,6 +1769,9 @@ class Kernel {
1752
1769
 
1753
1770
  _teardownResult(type, message, started) {
1754
1771
  this._kill();
1772
+ // Same rule as the one-shot path: a box that never STARTED (the kernel failed to boot) throws, it
1773
+ // does not return a hollow result. timeout/killed stay as data on the returned result.
1774
+ if (type === "startup_failed") throw new SandboxError(message || "the box failed to start");
1755
1775
  return new ExecutionResult({
1756
1776
  stdout: "",
1757
1777
  stderr: "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-sandbox",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",