kern-sandbox 0.2.14 → 0.2.16

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 (2) hide show
  1. package/index.js +43 -30
  2. package/package.json +1 -1
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.14";
39
+ const VERSION = "0.2.16";
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
@@ -1104,34 +1104,31 @@ function kernReportedOom(stderr) {
1104
1104
  return false;
1105
1105
  }
1106
1106
 
1107
+ /** The two prefixes kern's CLI writes AT COLUMN 0, which is the whole vocabulary of "kern is speaking".
1108
+ * `kern-cli/src/main.rs` reports every error through one `eprintln!("error: {}", ...)` and
1109
+ * `ui::scrub_message` indents every continuation line so a hostile value inside a message cannot forge a
1110
+ * line at column 0. Mirrors `_KERN_SPEAKING`.
1111
+ *
1112
+ * WHAT THIS REPLACED: a list of eleven message OPENINGS, each added after a caller measured a
1113
+ * `fault: null`. Enumerating the error texts of a binary with hundreds of them behind one printer cannot
1114
+ * be finished, and an external reviewer ended the argument with `image: ""`, whose
1115
+ * `error: bad image reference: empty` was in none of the eleven. */
1116
+ const KERN_SPEAKING = ["error: ", "kern:"];
1117
+
1118
+ /** True iff KERN ITSELF reported an error on this box, rather than the workload failing.
1119
+ *
1120
+ * It does NOT decide forgery from the text: a workload can print `error: anything` at column 0, and no
1121
+ * list of openings ever stopped that. The `KERN_STARTED_FD` byte does, and the callers pair it with this
1122
+ * predicate (`_runOne` drops a `startup_failed` when kern signalled a start; the kernel paths ask the
1123
+ * byte first). Mirrors `_looks_like_startup_failure`. */
1107
1124
  function looksLikeStartupFailure(stderr) {
1108
- const markers = [
1109
- "kern:",
1110
- "error: pull:",
1111
- "error: curl failed:",
1112
- "error: registry:",
1113
- "error: manifest:",
1114
- "error: sandbox:",
1115
- "error: box:",
1116
- "error: oci:",
1117
- "error: image:",
1118
- // REACHABLE EXACTLY WHEN A CALLER PASSES `profiles`, and missing until it was measured: a profile
1119
- // name that is well formed but absent from `kern.toml` makes kern refuse before any box exists, and
1120
- // without this marker the call came back `exitCode 1, fault null`, which a caller reading `fault`
1121
- // cannot tell from their own code exiting 1.
1122
- "error: config:",
1123
- // The binding builds its own argv, so these mean IT got something wrong; either way the box never ran.
1124
- "error: usage:",
1125
- "error: invalid box name:",
1126
- ];
1127
- // The OOM sentence is skipped for a sharper reason than the benign notes: it is a report about a box
1128
- // that RAN, and it is `kern:`-prefixed, so it used to satisfy this predicate. MEASURED, that is how a
1129
- // real OOM on a resident kernel came back as `startup_failed` and was THROWN instead of returning an
1130
- // `oom` fault.
1125
+ // kern's BENIGN lines are subtracted first, which is why this cannot be a bare prefix test: the
1126
+ // posture banner and `warning:`/`note:` carry `kern:` too. The OOM sentence is skipped for a sharper
1127
+ // reason: it is a report about a box that RAN, and MEASURED that is how a real OOM on a resident
1128
+ // kernel came back as `startup_failed` and was THROWN instead of returning an `oom` fault.
1131
1129
  for (const line of stderr.split("\n")) {
1132
- const s = line.replace(/^\s+/, "");
1133
- if (isKernDiagnostic(s) || kernReportedOom(s)) continue;
1134
- if (s.includes("sandbox setup failed") || markers.some((m) => s.startsWith(m))) return true;
1130
+ if (isKernDiagnostic(line) || kernReportedOom(line)) continue;
1131
+ if (KERN_SPEAKING.some((m) => line.startsWith(m)) || line.includes("sandbox setup failed")) return true;
1135
1132
  }
1136
1133
  return false;
1137
1134
  }
@@ -1792,10 +1789,17 @@ class Sandbox {
1792
1789
  detail = reason || "the box could not execute it";
1793
1790
  }
1794
1791
  fault = sandboxFault("exec_failed", `'${what}' could not be started in the box: ${detail}`);
1795
- } else if (boxStarted && fault && fault.type === "startup_failed") {
1792
+ } else if (fault && fault.type === "startup_failed" && (boxStarted || stdout.trim())) {
1796
1793
  // kern signalled the box STARTED, so a `startup_failed` here is only the stderr heuristic
1797
1794
  // matching a marker the WORKLOAD wrote (code-based faults are decided first). The box
1798
1795
  // demonstrably ran: this is the workload's own non-zero exit - reclassify to a normal result.
1796
+ //
1797
+ // STDOUT IS THE SECOND WITNESS, and it is here because the first one can be absent. MEASURED
1798
+ // with a KERN_BIN wrapper that closes `KERN_STARTED_FD` before exec'ing the real kern: the box
1799
+ // ran, printed, exited 1 with `error: forged` on stderr, and came back `startup_failed`. The
1800
+ // same hole is open on any kern too old to write the byte. A box that never started cannot
1801
+ // print, and every genuine startup failure measured returns stdout EMPTY, so this is only ever
1802
+ // read as evidence FOR a box having run, never against: a silent workload loses nothing.
1799
1803
  fault = null;
1800
1804
  }
1801
1805
  // A box that FAILED TO START ran no user code, so REJECT rather than resolve a hollow
@@ -2813,7 +2817,13 @@ class Kernel {
2813
2817
  `the code crashed: the cell died on signal ${workloadSignal} (${SIGNAL_NAMES[workloadSignal]}), which took the kernel box with it because the interpreter is its PID 1. The sandbox did not act; the next call reopens a kernel`,
2814
2818
  rc,
2815
2819
  ];
2816
- if (looksLikeStartupFailure(err)) return ["startup_failed", "the kernel box failed to start", rc];
2820
+ // AND THE BYTE DECIDES WHETHER THE TEXT IS BELIEVED. kern writes the teardown payload only for a box
2821
+ // that existed, so `kernWrotePayload` is positive proof that this kernel STARTED and a
2822
+ // `startup_failed` here could only be a cell printing kern's prefix at column 0 and dying of
2823
+ // something else. The one-shot path gets this from `_runOne`, which drops the verdict when the start
2824
+ // byte is set; this path had no such guard, so the widened predicate gets it here.
2825
+ if (!kernWrotePayload && looksLikeStartupFailure(err))
2826
+ return ["startup_failed", "the kernel box failed to start", rc];
2817
2827
  if (capSignal === 2)
2818
2828
  return [
2819
2829
  "killed",
@@ -3249,7 +3259,10 @@ class WarmBox {
3249
3259
  if (oomVerdict(oomSignal, err, boxStarted)) {
3250
3260
  type = "oom";
3251
3261
  dflt = "the box exceeded its memory cap and was OOM-killed";
3252
- } else if (looksLikeStartupFailure(err)) {
3262
+ } else if (!boxStarted && looksLikeStartupFailure(err)) {
3263
+ // `!boxStarted` for the reason `_kernelDeathFault` states: kern writes the teardown payload only
3264
+ // for a box that existed, so with the byte set this THROW would be a cell's own column-0 line
3265
+ // deciding that the box never came up.
3253
3266
  throw new SandboxError(err.trim() || "the box failed to start");
3254
3267
  } else if (capSignal === 2) {
3255
3268
  dflt =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-sandbox",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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",