kern-sandbox 0.1.38 → 0.1.40
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 +12 -3
- package/index.js +58 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ allowlist, and a wall-clock deadline the binding applies from **outside** the bo
|
|
|
12
12
|
cannot outlive it. Dependency-free: it shells out to the `kern` binary and does not re-implement
|
|
13
13
|
isolation in JavaScript.
|
|
14
14
|
|
|
15
|
-
**
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
**Your loop reads a field, not a stack trace.** A timeout, an OOM-kill, a blocked syscall or a missing
|
|
16
|
+
interpreter each arrive as a typed `fault` on the result, beside stdout and the exit code, so the
|
|
17
|
+
agent branches on a value instead of parsing text to work out who ended the run.
|
|
18
18
|
|
|
19
19
|
On npm: [`npm install kern-sandbox`](https://www.npmjs.com/package/kern-sandbox). Python gets the same
|
|
20
20
|
package on PyPI: [`kern-sandbox`](https://pypi.org/project/kern-sandbox/), which also ships an **MCP
|
|
@@ -109,6 +109,8 @@ const r = await kern.runCode("console.log([1,2,3].map(x => x * x))", {
|
|
|
109
109
|
| field | meaning |
|
|
110
110
|
|---|---|
|
|
111
111
|
| `stdout`, `stderr` | captured output (each capped at `maxOutputBytes`) |
|
|
112
|
+
| `codeStderr` | `stderr` with kern's own `note:`/`warning:` lines removed: what the code wrote. Feed THIS to a model |
|
|
113
|
+
| `runtimeNotes` | the complement: the lines kern wrote about itself. `stderr` still holds both, in order |
|
|
112
114
|
| `exitCode` | the process exit code |
|
|
113
115
|
| `durationMs` | wall-clock duration of the call, in ms |
|
|
114
116
|
| `success` | `true` iff `exitCode === 0` **and** no sandbox fault |
|
|
@@ -118,6 +120,13 @@ const r = await kern.runCode("console.log([1,2,3].map(x => x * x))", {
|
|
|
118
120
|
| `truncated` | output hit the cap and overflow was discarded |
|
|
119
121
|
|
|
120
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.
|
|
121
130
|
`fault` is only set when the **sandbox** acted:
|
|
122
131
|
|
|
123
132
|
| `fault.type` | when |
|
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.
|
|
39
|
+
const VERSION = "0.1.40";
|
|
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
|
|
@@ -510,6 +510,47 @@ class ExecutionResult {
|
|
|
510
510
|
get success() {
|
|
511
511
|
return this.exitCode === 0 && this.fault === null;
|
|
512
512
|
}
|
|
513
|
+
/** `stderr` with kern's own `note:`/`warning:`/posture lines removed: what the code actually wrote.
|
|
514
|
+
*
|
|
515
|
+
* This is what belongs in a model's context. A workload CAN forge one of kern's prefixes, and the
|
|
516
|
+
* consequence is its own line moving to {@link runtimeNotes}: the trick removes its text from this
|
|
517
|
+
* field, it cannot inject text into it.
|
|
518
|
+
*
|
|
519
|
+
* The guarantee is LINE-ALIGNED, not absolute: a workload that leaves a line unterminated and is
|
|
520
|
+
* then interleaved with a `kern: warning:` on the shared stderr produces one line starting with the
|
|
521
|
+
* workload's text, which no prefix matches, so kern's warning lands here framed by bytes the
|
|
522
|
+
* workload chose. Racy rather than reliable, and in the less harmful direction, but real. Mirrors
|
|
523
|
+
* `ExecutionResult.code_stderr` in Python. */
|
|
524
|
+
get codeStderr() {
|
|
525
|
+
return this._splitStderr()[0];
|
|
526
|
+
}
|
|
527
|
+
/** Partition `stderr` ONCE into [what the code wrote, the lines kern wrote].
|
|
528
|
+
*
|
|
529
|
+
* One pass and one cache, mirroring `_split_stderr` in Python. The two public halves are a single
|
|
530
|
+
* partition, so computing them separately left two filters that had to agree by inspection rather
|
|
531
|
+
* than by construction; and each was O(n) on every read, measured at 13.9 ms on a 200k-line stderr
|
|
532
|
+
* in the Python binding before this. Keyed on the string it partitioned, so reassigning `stderr`
|
|
533
|
+
* recomputes rather than serving a stale answer. Non-enumerable, so it stays out of JSON and out of
|
|
534
|
+
* anything that walks the result's own keys. */
|
|
535
|
+
_splitStderr() {
|
|
536
|
+
const raw = String(this.stderr || "");
|
|
537
|
+
if (this._stderrSplit && this._stderrSplit[0] === raw) {
|
|
538
|
+
return [this._stderrSplit[1], this._stderrSplit[2]];
|
|
539
|
+
}
|
|
540
|
+
const kept = [];
|
|
541
|
+
const notes = [];
|
|
542
|
+
for (const line of raw.split("\n")) (isKernDiagnostic(line) ? notes : kept).push(line);
|
|
543
|
+
const joined = kept.join("\n");
|
|
544
|
+
Object.defineProperty(this, "_stderrSplit", {
|
|
545
|
+
value: [raw, joined, notes], writable: true, enumerable: false, configurable: true,
|
|
546
|
+
});
|
|
547
|
+
return [joined, notes];
|
|
548
|
+
}
|
|
549
|
+
/** The lines on `stderr` that KERN wrote, the complement of {@link codeStderr}. Reported rather
|
|
550
|
+
* than removed: `stderr` still holds every byte in its original order. */
|
|
551
|
+
get runtimeNotes() {
|
|
552
|
+
return this._splitStderr()[1].slice();
|
|
553
|
+
}
|
|
513
554
|
}
|
|
514
555
|
|
|
515
556
|
/** A sandbox event `{type, message}` for `result.fault`. NB: `startup_failed` is decided from an
|
|
@@ -821,6 +862,21 @@ function execFailureBinary(stderr) {
|
|
|
821
862
|
return m ? { what: m[1], reason: (m[2] || "").trim() } : null;
|
|
822
863
|
}
|
|
823
864
|
|
|
865
|
+
/** The prefixes of stderr lines KERN writes about itself, as opposed to lines the workload wrote: the
|
|
866
|
+
* `--security-profile` posture banner and any `warning:`/`note:` diagnostic.
|
|
867
|
+
*
|
|
868
|
+
* ONE definition, used for two purposes that must agree by construction: `codeStderr` subtracts these
|
|
869
|
+
* to build what a model should read, and `looksLikeStartupFailure` skips them so a benign note is not
|
|
870
|
+
* read as a box that failed to start. Mirrors `_KERN_DIAGNOSTICS` in the Python binding. */
|
|
871
|
+
const KERN_DIAGNOSTICS = ["kern: security-profile=", "kern: warning:", "kern: note:"];
|
|
872
|
+
|
|
873
|
+
function isKernDiagnostic(line) {
|
|
874
|
+
const s = line.replace(/^\s+/, "");
|
|
875
|
+
return KERN_DIAGNOSTICS.some((p) => s.startsWith(p));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
|
|
824
880
|
function looksLikeStartupFailure(stderr) {
|
|
825
881
|
const markers = [
|
|
826
882
|
"kern:",
|
|
@@ -833,14 +889,9 @@ function looksLikeStartupFailure(stderr) {
|
|
|
833
889
|
"error: oci:",
|
|
834
890
|
"error: image:",
|
|
835
891
|
];
|
|
836
|
-
// kern also writes BENIGN `kern:` diagnostics that are NOT a box-start failure: the
|
|
837
|
-
// `--security-profile` posture banner, and `warning:`/`note:` lines. They start with `kern:` too, so
|
|
838
|
-
// without this skip a workload that merely exits non-zero WHILE one is on stderr (e.g. code run under
|
|
839
|
-
// securityProfile: "untrusted" that hits a network error) would be mislabeled `startup_failed`.
|
|
840
|
-
const benign = ["kern: security-profile=", "kern: warning:", "kern: note:"];
|
|
841
892
|
for (const line of stderr.split("\n")) {
|
|
842
893
|
const s = line.replace(/^\s+/, "");
|
|
843
|
-
if (
|
|
894
|
+
if (isKernDiagnostic(s)) continue;
|
|
844
895
|
if (s.includes("sandbox setup failed") || markers.some((m) => s.startsWith(m))) return true;
|
|
845
896
|
}
|
|
846
897
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kern-sandbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
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",
|