@draht/rlm 2026.10.20 → 2026.10.23-1
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 +19 -0
- package/dist/sandbox.d.ts +13 -11
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +48 -30
- package/dist/sandbox.js.map +1 -1
- package/package.json +4 -4
- package/python/linux_sandbox.py +231 -0
package/README.md
CHANGED
|
@@ -130,6 +130,25 @@ To check whether that particular run was worth it cost-wise, take the
|
|
|
130
130
|
`compareCost()` (see Cost envelope, above) rather than assuming either
|
|
131
131
|
direction.
|
|
132
132
|
|
|
133
|
+
## Sandbox requirements
|
|
134
|
+
|
|
135
|
+
Linux RLM sessions require `unshare` or `bwrap`, a 64-bit x86 or ARM Python
|
|
136
|
+
interpreter, seccomp filtering, and an enabled Landlock ABI 3 or newer. The
|
|
137
|
+
bootstrap grants read access to isolated Python module paths, system library
|
|
138
|
+
directories (including the Nix store when present), the driver, and runtime
|
|
139
|
+
support files. Regular-file writes are confined to the session workdir;
|
|
140
|
+
`/dev/null` is also writable. Process execution, networking, and filesystem
|
|
141
|
+
metadata changes are denied. File metadata such as names and sizes is not
|
|
142
|
+
hidden.
|
|
143
|
+
|
|
144
|
+
If these kernel restrictions cannot be installed, the driver does not run.
|
|
145
|
+
macOS continues to use `sandbox-exec`. Both platforms run a sandbox startup
|
|
146
|
+
check before accepting model-generated Python.
|
|
147
|
+
|
|
148
|
+
The preferred `unshare` path keeps Python at the PID monitored for memory
|
|
149
|
+
usage. With the `bwrap` fallback, RSS polling currently observes the wrapper
|
|
150
|
+
process instead; the session's RSS ceiling is not reliable on that path.
|
|
151
|
+
|
|
133
152
|
## Development
|
|
134
153
|
|
|
135
154
|
```bash
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -20,18 +20,19 @@
|
|
|
20
20
|
* are discovered here at spawn time via a real subprocess call, never
|
|
21
21
|
* hardcoded/guessed -- see `resolvePythonInstallation`.
|
|
22
22
|
*
|
|
23
|
-
* **Linux**:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* **Linux**: unshare creates private user/network/mount namespaces. A
|
|
24
|
+
* trusted Python bootstrap then applies Landlock filesystem rules, drops
|
|
25
|
+
* capabilities and installs a seccomp syscall allowlist before loading the
|
|
26
|
+
* driver in the same Python process. This requires Landlock ABI 3+ and
|
|
27
|
+
* seccomp on x86-64/AArch64; unsupported kernels fail closed. bwrap remains
|
|
28
|
+
* the namespace fallback when unshare is unavailable.
|
|
28
29
|
*
|
|
29
30
|
* **Fail closed everywhere**: if the sandboxing mechanism for the current
|
|
30
31
|
* platform can't be established (wrapper binary missing, profile missing,
|
|
31
|
-
* Python installation can't be discovered, unsupported platform),
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Python installation can't be discovered, unsupported platform), command
|
|
33
|
+
* resolution throws `SandboxUnavailableError`. Linux kernel-policy failures
|
|
34
|
+
* terminate the bootstrap before it loads the driver. Neither path falls
|
|
35
|
+
* back to an unsandboxed driver.
|
|
35
36
|
*/
|
|
36
37
|
import { type ChildProcess } from "node:child_process";
|
|
37
38
|
/**
|
|
@@ -87,8 +88,9 @@ export interface SandboxedCommand {
|
|
|
87
88
|
export declare function resolveSandboxedCommand(opts: SandboxSpawnOptions): SandboxedCommand;
|
|
88
89
|
/**
|
|
89
90
|
* Spawns `repl_driver.py` wrapped in the platform's OS-level sandbox.
|
|
90
|
-
* Throws `SandboxUnavailableError`
|
|
91
|
-
*
|
|
91
|
+
* Throws `SandboxUnavailableError` for command-resolution failures. Linux
|
|
92
|
+
* kernel-policy failures terminate the child before the driver runs; the
|
|
93
|
+
* session's startup self-test observes that failure. There is no unwrapped
|
|
92
94
|
* fallback path.
|
|
93
95
|
*/
|
|
94
96
|
export declare function spawnSandboxed(opts: SandboxSpawnOptions): ChildProcess;
|
package/dist/sandbox.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,KAAK,YAAY,EAAuB,MAAM,oBAAoB,CAAC;AAU5E;;;;;;GAMG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,YAAY,OAAO,EAAE,MAAM,EAG1B;CACD;AAED,MAAM,WAAW,mBAAmB;IACnC,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uGAAuG;IACvG,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CACf;AAmOD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,mBAAmB,GAAG,gBAAgB,CAMnF;AA2CD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,mBAAmB,GAAG,YAAY,CAGtE"}
|
package/dist/sandbox.js
CHANGED
|
@@ -20,18 +20,19 @@
|
|
|
20
20
|
* are discovered here at spawn time via a real subprocess call, never
|
|
21
21
|
* hardcoded/guessed -- see `resolvePythonInstallation`.
|
|
22
22
|
*
|
|
23
|
-
* **Linux**:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* **Linux**: unshare creates private user/network/mount namespaces. A
|
|
24
|
+
* trusted Python bootstrap then applies Landlock filesystem rules, drops
|
|
25
|
+
* capabilities and installs a seccomp syscall allowlist before loading the
|
|
26
|
+
* driver in the same Python process. This requires Landlock ABI 3+ and
|
|
27
|
+
* seccomp on x86-64/AArch64; unsupported kernels fail closed. bwrap remains
|
|
28
|
+
* the namespace fallback when unshare is unavailable.
|
|
28
29
|
*
|
|
29
30
|
* **Fail closed everywhere**: if the sandboxing mechanism for the current
|
|
30
31
|
* platform can't be established (wrapper binary missing, profile missing,
|
|
31
|
-
* Python installation can't be discovered, unsupported platform),
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Python installation can't be discovered, unsupported platform), command
|
|
33
|
+
* resolution throws `SandboxUnavailableError`. Linux kernel-policy failures
|
|
34
|
+
* terminate the bootstrap before it loads the driver. Neither path falls
|
|
35
|
+
* back to an unsandboxed driver.
|
|
35
36
|
*/
|
|
36
37
|
import { execFileSync, spawn } from "node:child_process";
|
|
37
38
|
import { existsSync, realpathSync } from "node:fs";
|
|
@@ -39,6 +40,7 @@ import { dirname, join, delimiter as pathDelimiter } from "node:path";
|
|
|
39
40
|
import { fileURLToPath } from "node:url";
|
|
40
41
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
42
|
const MACOS_PROFILE_PATH = join(__dirname, "..", "sandbox", "macos.sb");
|
|
43
|
+
const LINUX_BOOTSTRAP_PATH = join(__dirname, "..", "python", "linux_sandbox.py");
|
|
42
44
|
const DEFAULT_SANDBOX_EXEC_BIN = "/usr/bin/sandbox-exec";
|
|
43
45
|
/**
|
|
44
46
|
* Thrown whenever the OS-level sandbox can't be established for the current
|
|
@@ -63,14 +65,16 @@ const pythonInstallationCache = new Map();
|
|
|
63
65
|
* the answer can't change during this process's lifetime and this runs on
|
|
64
66
|
* every sandboxed spawn.
|
|
65
67
|
*/
|
|
66
|
-
function resolvePythonInstallation(pythonBin) {
|
|
67
|
-
const
|
|
68
|
+
function resolvePythonInstallation(pythonBin, isolated = false) {
|
|
69
|
+
const cacheKey = JSON.stringify([pythonBin, isolated]);
|
|
70
|
+
const cached = pythonInstallationCache.get(cacheKey);
|
|
68
71
|
if (cached)
|
|
69
72
|
return cached;
|
|
70
73
|
let stdout;
|
|
71
74
|
try {
|
|
72
|
-
stdout = execFileSync(pythonBin, ["-c", "import sys; print(sys.executable); print(sys.prefix)"], {
|
|
75
|
+
stdout = execFileSync(pythonBin, [...(isolated ? ["-I", "-S"] : []), "-c", "import sys; print(sys.executable); print(sys.prefix)"], {
|
|
73
76
|
encoding: "utf8",
|
|
77
|
+
...(isolated ? { env: buildScrubbedEnv() } : {}),
|
|
74
78
|
});
|
|
75
79
|
}
|
|
76
80
|
catch (err) {
|
|
@@ -94,7 +98,7 @@ function resolvePythonInstallation(pythonBin) {
|
|
|
94
98
|
const reexecCandidate = join(prefix, "Resources", "Python.app", "Contents", "MacOS", "Python");
|
|
95
99
|
const reexecBin = existsSync(reexecCandidate) ? realpathSync(reexecCandidate) : realBin;
|
|
96
100
|
const installation = { realBin, reexecBin, prefix };
|
|
97
|
-
pythonInstallationCache.set(
|
|
101
|
+
pythonInstallationCache.set(cacheKey, installation);
|
|
98
102
|
return installation;
|
|
99
103
|
}
|
|
100
104
|
/** Resolves a bare command name against `PATH`, returning `null` if not found. Used for the Linux wrappers. */
|
|
@@ -149,6 +153,9 @@ function buildMacosCommand(opts) {
|
|
|
149
153
|
}
|
|
150
154
|
function buildLinuxCommand(opts) {
|
|
151
155
|
const pythonBin = opts.pythonBin ?? "python3";
|
|
156
|
+
if (!existsSync(LINUX_BOOTSTRAP_PATH)) {
|
|
157
|
+
throw new SandboxUnavailableError("sandbox_violation: Linux bootstrap is missing -- refusing to run the REPL driver unsandboxed.");
|
|
158
|
+
}
|
|
152
159
|
let workdir;
|
|
153
160
|
let driverPath;
|
|
154
161
|
try {
|
|
@@ -160,28 +167,34 @@ function buildLinuxCommand(opts) {
|
|
|
160
167
|
}
|
|
161
168
|
const unshareBin = opts.unshareBin ?? resolveOnPath("unshare");
|
|
162
169
|
if (unshareBin && existsSync(unshareBin)) {
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
// workdir bind-mount (rw) plus the rest of the root (ro, implicit --
|
|
167
|
-
// `unshare --mount` alone doesn't remount anything) is a coarser
|
|
168
|
-
// boundary than the macOS profile's explicit allowlist, which is why
|
|
169
|
-
// `--map-root-user` + a private mount namespace is paired with the
|
|
170
|
-
// driver only ever being told about `workdir` for file I/O at the
|
|
171
|
-
// Python-guardrail layer (Phase 28 task 2) -- the kernel-level net
|
|
172
|
-
// denial is what task 1 owns and is unconditional here.
|
|
170
|
+
const { realBin } = resolvePythonInstallation(pythonBin, true);
|
|
171
|
+
// No --fork: Node's RSS monitor and kill target must remain Python's
|
|
172
|
+
// PID. -I -S excludes user paths and site startup code before confinement.
|
|
173
173
|
return {
|
|
174
174
|
command: unshareBin,
|
|
175
|
-
args: [
|
|
175
|
+
args: [
|
|
176
|
+
"--user",
|
|
177
|
+
"--map-root-user",
|
|
178
|
+
"--net",
|
|
179
|
+
"--mount",
|
|
180
|
+
"--",
|
|
181
|
+
realBin,
|
|
182
|
+
"-I",
|
|
183
|
+
"-S",
|
|
184
|
+
LINUX_BOOTSTRAP_PATH,
|
|
185
|
+
driverPath,
|
|
186
|
+
workdir,
|
|
187
|
+
],
|
|
176
188
|
};
|
|
177
189
|
}
|
|
178
190
|
const bwrapBin = opts.bwrapBin ?? resolveOnPath("bwrap");
|
|
179
191
|
if (bwrapBin && existsSync(bwrapBin)) {
|
|
180
|
-
const
|
|
192
|
+
const { realBin, prefix } = resolvePythonInstallation(pythonBin, true);
|
|
193
|
+
const binds = [...new Set(["/usr", "/lib", "/lib64", "/bin", "/sbin", "/nix/store", prefix])]
|
|
181
194
|
.filter((path) => existsSync(path))
|
|
182
195
|
.flatMap((path) => ["--ro-bind", path, path]);
|
|
183
|
-
|
|
184
|
-
binds.push("--ro-bind",
|
|
196
|
+
for (const path of [LINUX_BOOTSTRAP_PATH, driverPath, realBin])
|
|
197
|
+
binds.push("--ro-bind", path, path);
|
|
185
198
|
return {
|
|
186
199
|
command: bwrapBin,
|
|
187
200
|
args: [
|
|
@@ -199,8 +212,12 @@ function buildLinuxCommand(opts) {
|
|
|
199
212
|
"/dev",
|
|
200
213
|
"--chdir",
|
|
201
214
|
workdir,
|
|
202
|
-
|
|
215
|
+
realBin,
|
|
216
|
+
"-I",
|
|
217
|
+
"-S",
|
|
218
|
+
LINUX_BOOTSTRAP_PATH,
|
|
203
219
|
driverPath,
|
|
220
|
+
workdir,
|
|
204
221
|
],
|
|
205
222
|
};
|
|
206
223
|
}
|
|
@@ -262,8 +279,9 @@ function buildScrubbedEnv() {
|
|
|
262
279
|
}
|
|
263
280
|
/**
|
|
264
281
|
* Spawns `repl_driver.py` wrapped in the platform's OS-level sandbox.
|
|
265
|
-
* Throws `SandboxUnavailableError`
|
|
266
|
-
*
|
|
282
|
+
* Throws `SandboxUnavailableError` for command-resolution failures. Linux
|
|
283
|
+
* kernel-policy failures terminate the child before the driver runs; the
|
|
284
|
+
* session's startup self-test observes that failure. There is no unwrapped
|
|
267
285
|
* fallback path.
|
|
268
286
|
*/
|
|
269
287
|
export function spawnSandboxed(opts) {
|
package/dist/sandbox.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OS-level sandbox spawn wrapper for the RLM Python REPL driver.\n *\n * This is the REAL security boundary described in\n * .planning/phases/28-repl-sandbox-safety/28-01-PLAN.md (\"IMPORTANT\" section\n * and Architecture section 1) -- NOT `repl_driver.py`'s Python-level\n * builtins/import allowlist (that's defense-in-depth for a\n * confused/prompt-injected LLM, not something that stops a deliberately\n * adversarial payload; the Python object graph makes in-process\n * confinement of arbitrary Python fundamentally unachievable -- see the\n * plan for the `__subclasses__`/`gi_frame.f_builtins` escapes that defeat\n * any purely in-process approach).\n *\n * **macOS**: wraps the interpreter with `sandbox-exec -f macos.sb ...`. The\n * SBPL profile (`../sandbox/macos.sb`) denies network and process-exec\n * entirely (which alone defeats `os.system`/`subprocess` -- they fork+exec\n * a shell -- independent of any Python-level import block) and scopes file\n * access to exactly the Python installation needed to boot plus the\n * session's workdir. Machine-specific paths (where python3 actually lives)\n * are discovered here at spawn time via a real subprocess call, never\n * hardcoded/guessed -- see `resolvePythonInstallation`.\n *\n * **Linux**: wraps with `unshare --user --map-root-user --net --mount --`\n * (empty net namespace = network unreachable at the kernel; mount\n * namespace for FS confinement), falling back to `bwrap` with equivalent\n * flags if `unshare` isn't available. Availability is detected at spawn\n * time, never assumed.\n *\n * **Fail closed everywhere**: if the sandboxing mechanism for the current\n * platform can't be established (wrapper binary missing, profile missing,\n * Python installation can't be discovered, unsupported platform), every\n * function here throws `SandboxUnavailableError` synchronously rather than\n * returning a command that would run the driver unwrapped. There is no\n * fallback path to an unsandboxed spawn anywhere in this module.\n */\n\nimport { type ChildProcess, execFileSync, spawn } from \"node:child_process\";\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { dirname, join, delimiter as pathDelimiter } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst MACOS_PROFILE_PATH = join(__dirname, \"..\", \"sandbox\", \"macos.sb\");\nconst DEFAULT_SANDBOX_EXEC_BIN = \"/usr/bin/sandbox-exec\";\n\n/**\n * Thrown whenever the OS-level sandbox can't be established for the current\n * platform/environment. Callers (see `session.ts`) must let this propagate\n * and refuse to run rather than catching it and falling back to an\n * unwrapped spawn -- that fallback is exactly the failure mode Architecture\n * section 1's \"fail closed\" requirement exists to prevent.\n */\nexport class SandboxUnavailableError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"SandboxUnavailableError\";\n\t}\n}\n\nexport interface SandboxSpawnOptions {\n\t/** Absolute path to `repl_driver.py`. */\n\tdriverPath: string;\n\t/**\n\t * Absolute path to the session's scratch workdir. The only place the\n\t * sandboxed process may read *and* write freely (besides the read-only\n\t * Python installation itself).\n\t */\n\tworkdir: string;\n\t/**\n\t * Which `python3` to sandbox. Defaults to whatever `python3` resolves to\n\t * on `PATH`. Overridable mainly so tests can point at a specific\n\t * interpreter without mutating `PATH`.\n\t */\n\tpythonBin?: string;\n\t/**\n\t * Override for the macOS `sandbox-exec` binary path. Mainly a test\n\t * injection point for proving fail-closed behavior (point it at a\n\t * nonexistent path and assert setup refuses rather than falling back to\n\t * an unwrapped spawn) -- see Architecture section 1's fail-closed\n\t * requirement and this plan's task 1 test 7.\n\t */\n\tsandboxExecBin?: string;\n\t/** Override for the Linux `unshare` binary path. Mainly for tests. */\n\tunshareBin?: string;\n\t/** Override for the Linux `bwrap` binary path (used if `unshare` is unavailable). Mainly for tests. */\n\tbwrapBin?: string;\n}\n\n/** The concrete command + args a `child_process.spawn` call should use. */\nexport interface SandboxedCommand {\n\tcommand: string;\n\targs: string[];\n}\n\ninterface PythonInstallation {\n\t/** `realpath()`'d absolute path to the interpreter binary that will actually run. */\n\trealBin: string;\n\t/**\n\t * Some macOS \"framework\" Python builds (the python.org installer,\n\t * Homebrew's `--with-framework`) internally `posix_spawn()` themselves\n\t * into a second binary at startup\n\t * (`Resources/Python.app/Contents/MacOS/Python`) before any user code\n\t * runs, to get a normal `.app` bundle identity (Tk/Cocoa access, Dock\n\t * icon, etc.). Confirmed empirically while scoping this profile: without\n\t * allowing this second exec, dyld aborts before `main()` ever runs. Equal\n\t * to `realBin` when no such companion binary exists.\n\t */\n\treexecBin: string;\n\t/** `realpath()`'d `sys.prefix` -- the install root the interpreter needs read access to in order to boot. */\n\tprefix: string;\n}\n\nconst pythonInstallationCache = new Map<string, PythonInstallation>();\n\n/**\n * Asks the given `python3` binary (via a real subprocess call -- there is no\n * static/config-file way to learn a Python install's true executable path\n * and stdlib root; every install lays these out differently) where it and\n * its stdlib actually live, then checks for the macOS framework-build\n * re-exec companion described above. Memoized per `pythonBin` string since\n * the answer can't change during this process's lifetime and this runs on\n * every sandboxed spawn.\n */\nfunction resolvePythonInstallation(pythonBin: string): PythonInstallation {\n\tconst cached = pythonInstallationCache.get(pythonBin);\n\tif (cached) return cached;\n\n\tlet stdout: string;\n\ttry {\n\t\tstdout = execFileSync(pythonBin, [\"-c\", \"import sys; print(sys.executable); print(sys.prefix)\"], {\n\t\t\tencoding: \"utf8\",\n\t\t});\n\t} catch (err) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: could not run ${JSON.stringify(pythonBin)} to discover its installation -- refusing to run the REPL driver unsandboxed. (${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t})`,\n\t\t);\n\t}\n\n\tconst lines = stdout.trim().split(\"\\n\");\n\tconst executableLine = lines[0];\n\tconst prefixLine = lines[1];\n\tif (!executableLine || !prefixLine) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: unexpected output discovering the Python installation for ${JSON.stringify(pythonBin)}: ${JSON.stringify(stdout)}`,\n\t\t);\n\t}\n\n\tlet realBin: string;\n\tlet prefix: string;\n\ttry {\n\t\trealBin = realpathSync(executableLine.trim());\n\t\tprefix = realpathSync(prefixLine.trim());\n\t} catch (err) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: could not resolve real filesystem paths for the Python installation -- refusing to run the REPL driver unsandboxed. (${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t})`,\n\t\t);\n\t}\n\n\tconst reexecCandidate = join(prefix, \"Resources\", \"Python.app\", \"Contents\", \"MacOS\", \"Python\");\n\tconst reexecBin = existsSync(reexecCandidate) ? realpathSync(reexecCandidate) : realBin;\n\n\tconst installation: PythonInstallation = { realBin, reexecBin, prefix };\n\tpythonInstallationCache.set(pythonBin, installation);\n\treturn installation;\n}\n\n/** Resolves a bare command name against `PATH`, returning `null` if not found. Used for the Linux wrappers. */\nfunction resolveOnPath(name: string): string | null {\n\tconst pathEnv = process.env.PATH ?? \"\";\n\tfor (const dir of pathEnv.split(pathDelimiter)) {\n\t\tif (!dir) continue;\n\t\tconst candidate = join(dir, name);\n\t\tif (existsSync(candidate)) return candidate;\n\t}\n\treturn null;\n}\n\nfunction buildMacosCommand(opts: SandboxSpawnOptions): SandboxedCommand {\n\tconst sandboxExecBin = opts.sandboxExecBin ?? DEFAULT_SANDBOX_EXEC_BIN;\n\tif (!existsSync(sandboxExecBin)) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: macOS sandbox-exec binary not found at ${JSON.stringify(sandboxExecBin)} -- refusing to run the REPL driver unsandboxed.`,\n\t\t);\n\t}\n\tif (!existsSync(MACOS_PROFILE_PATH)) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: sandbox profile missing at ${JSON.stringify(MACOS_PROFILE_PATH)} -- refusing to run the REPL driver unsandboxed.`,\n\t\t);\n\t}\n\n\tconst { realBin, reexecBin, prefix } = resolvePythonInstallation(opts.pythonBin ?? \"python3\");\n\n\tlet workdir: string;\n\tlet driverPath: string;\n\ttry {\n\t\tworkdir = realpathSync(opts.workdir);\n\t\tdriverPath = realpathSync(opts.driverPath);\n\t} catch (err) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: could not resolve workdir/driver paths -- refusing to run the REPL driver unsandboxed. (${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t})`,\n\t\t);\n\t}\n\n\treturn {\n\t\tcommand: sandboxExecBin,\n\t\targs: [\n\t\t\t\"-D\",\n\t\t\t`PYTHON_REAL_BIN=${realBin}`,\n\t\t\t\"-D\",\n\t\t\t`PYTHON_REEXEC_BIN=${reexecBin}`,\n\t\t\t\"-D\",\n\t\t\t`PYTHON_PREFIX=${prefix}`,\n\t\t\t\"-D\",\n\t\t\t`DRIVER_PATH=${driverPath}`,\n\t\t\t\"-D\",\n\t\t\t`WORKDIR=${workdir}`,\n\t\t\t\"-f\",\n\t\t\tMACOS_PROFILE_PATH,\n\t\t\trealBin,\n\t\t\tdriverPath,\n\t\t],\n\t};\n}\n\nfunction buildLinuxCommand(opts: SandboxSpawnOptions): SandboxedCommand {\n\tconst pythonBin = opts.pythonBin ?? \"python3\";\n\n\tlet workdir: string;\n\tlet driverPath: string;\n\ttry {\n\t\tworkdir = realpathSync(opts.workdir);\n\t\tdriverPath = realpathSync(opts.driverPath);\n\t} catch (err) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: could not resolve workdir/driver paths -- refusing to run the REPL driver unsandboxed. (${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t})`,\n\t\t);\n\t}\n\n\tconst unshareBin = opts.unshareBin ?? resolveOnPath(\"unshare\");\n\tif (unshareBin && existsSync(unshareBin)) {\n\t\t// Empty user+net namespace: no interfaces besides loopback exist, so\n\t\t// network is unreachable at the kernel regardless of anything Python\n\t\t// itself tries. Mount namespace confines the filesystem view; the\n\t\t// workdir bind-mount (rw) plus the rest of the root (ro, implicit --\n\t\t// `unshare --mount` alone doesn't remount anything) is a coarser\n\t\t// boundary than the macOS profile's explicit allowlist, which is why\n\t\t// `--map-root-user` + a private mount namespace is paired with the\n\t\t// driver only ever being told about `workdir` for file I/O at the\n\t\t// Python-guardrail layer (Phase 28 task 2) -- the kernel-level net\n\t\t// denial is what task 1 owns and is unconditional here.\n\t\treturn {\n\t\t\tcommand: unshareBin,\n\t\t\targs: [\"--user\", \"--map-root-user\", \"--net\", \"--mount\", \"--\", pythonBin, driverPath],\n\t\t};\n\t}\n\n\tconst bwrapBin = opts.bwrapBin ?? resolveOnPath(\"bwrap\");\n\tif (bwrapBin && existsSync(bwrapBin)) {\n\t\tconst binds = [\"/usr\", \"/lib\", \"/bin\", \"/sbin\"]\n\t\t\t.filter((path) => existsSync(path))\n\t\t\t.flatMap((path) => [\"--ro-bind\", path, path]);\n\t\tif (existsSync(\"/lib64\")) binds.push(\"--ro-bind\", \"/lib64\", \"/lib64\");\n\t\treturn {\n\t\t\tcommand: bwrapBin,\n\t\t\targs: [\n\t\t\t\t\"--unshare-user\",\n\t\t\t\t\"--unshare-net\",\n\t\t\t\t\"--unshare-pid\",\n\t\t\t\t\"--die-with-parent\",\n\t\t\t\t...binds,\n\t\t\t\t\"--bind\",\n\t\t\t\tworkdir,\n\t\t\t\tworkdir,\n\t\t\t\t\"--proc\",\n\t\t\t\t\"/proc\",\n\t\t\t\t\"--dev\",\n\t\t\t\t\"/dev\",\n\t\t\t\t\"--chdir\",\n\t\t\t\tworkdir,\n\t\t\t\tpythonBin,\n\t\t\t\tdriverPath,\n\t\t\t],\n\t\t};\n\t}\n\n\tthrow new SandboxUnavailableError(\n\t\t\"sandbox_violation: neither `unshare` nor `bwrap` is available on this Linux host -- refusing to run the REPL driver unsandboxed.\",\n\t);\n}\n\n/**\n * Computes the sandboxed command/args for the current platform without\n * spawning anything. Exposed separately from `spawnSandboxed` so tests can\n * assert on the resolved invocation and so fail-closed behavior (throwing\n * `SandboxUnavailableError`) is observable synchronously, before any\n * process exists.\n */\nexport function resolveSandboxedCommand(opts: SandboxSpawnOptions): SandboxedCommand {\n\tif (process.platform === \"darwin\") return buildMacosCommand(opts);\n\tif (process.platform === \"linux\") return buildLinuxCommand(opts);\n\tthrow new SandboxUnavailableError(\n\t\t`sandbox_violation: no OS-level sandbox implementation for platform ${JSON.stringify(process.platform)} -- refusing to run the REPL driver unsandboxed.`,\n\t);\n}\n\n/**\n * Environment variables the sandboxed interpreter is allowed to see, by\n * allowlist rather than denylist. The OS sandbox itself denies network\n * access outright, so this isn't the primary defense against exfiltrating\n * anything read from here -- but the same object-graph escapes that defeat\n * in-process Python restriction (see this module's doc comment) can still\n * reach `os.environ` and write it into the session workdir even with\n * network denied, so the child process should never see more of the parent\n * Node process's environment (API keys, tokens, credentials, ...) than it\n * actually needs to boot a working interpreter.\n */\nconst SAFE_ENV_ALLOWLIST = [\n\t\"PATH\",\n\t\"HOME\",\n\t\"LANG\",\n\t\"LANGUAGE\",\n\t\"LC_ALL\",\n\t\"LC_CTYPE\",\n\t\"TMPDIR\",\n\t\"TZ\",\n\t\"PYTHONHASHSEED\",\n\t\"PYTHONIOENCODING\",\n\t\"PYTHONUTF8\",\n] as const;\n\n/**\n * Builds a scrubbed environment for the sandboxed child process from\n * `SAFE_ENV_ALLOWLIST` -- deliberately NOT `{ ...process.env }` (see\n * `SAFE_ENV_ALLOWLIST`'s doc comment for why the full parent environment,\n * secrets included, must not be handed to a process running LLM-authored\n * code).\n */\nfunction buildScrubbedEnv(): NodeJS.ProcessEnv {\n\tconst scrubbed: NodeJS.ProcessEnv = {};\n\tfor (const key of SAFE_ENV_ALLOWLIST) {\n\t\tconst value = process.env[key];\n\t\tif (value !== undefined) scrubbed[key] = value;\n\t}\n\treturn scrubbed;\n}\n\n/**\n * Spawns `repl_driver.py` wrapped in the platform's OS-level sandbox.\n * Throws `SandboxUnavailableError` synchronously (before any process is\n * created) if the sandbox can't be established -- there is no unwrapped\n * fallback path.\n */\nexport function spawnSandboxed(opts: SandboxSpawnOptions): ChildProcess {\n\tconst { command, args } = resolveSandboxedCommand(opts);\n\treturn spawn(command, args, { stdio: [\"pipe\", \"pipe\", \"pipe\"], env: buildScrubbedEnv() });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OS-level sandbox spawn wrapper for the RLM Python REPL driver.\n *\n * This is the REAL security boundary described in\n * .planning/phases/28-repl-sandbox-safety/28-01-PLAN.md (\"IMPORTANT\" section\n * and Architecture section 1) -- NOT `repl_driver.py`'s Python-level\n * builtins/import allowlist (that's defense-in-depth for a\n * confused/prompt-injected LLM, not something that stops a deliberately\n * adversarial payload; the Python object graph makes in-process\n * confinement of arbitrary Python fundamentally unachievable -- see the\n * plan for the `__subclasses__`/`gi_frame.f_builtins` escapes that defeat\n * any purely in-process approach).\n *\n * **macOS**: wraps the interpreter with `sandbox-exec -f macos.sb ...`. The\n * SBPL profile (`../sandbox/macos.sb`) denies network and process-exec\n * entirely (which alone defeats `os.system`/`subprocess` -- they fork+exec\n * a shell -- independent of any Python-level import block) and scopes file\n * access to exactly the Python installation needed to boot plus the\n * session's workdir. Machine-specific paths (where python3 actually lives)\n * are discovered here at spawn time via a real subprocess call, never\n * hardcoded/guessed -- see `resolvePythonInstallation`.\n *\n * **Linux**: unshare creates private user/network/mount namespaces. A\n * trusted Python bootstrap then applies Landlock filesystem rules, drops\n * capabilities and installs a seccomp syscall allowlist before loading the\n * driver in the same Python process. This requires Landlock ABI 3+ and\n * seccomp on x86-64/AArch64; unsupported kernels fail closed. bwrap remains\n * the namespace fallback when unshare is unavailable.\n *\n * **Fail closed everywhere**: if the sandboxing mechanism for the current\n * platform can't be established (wrapper binary missing, profile missing,\n * Python installation can't be discovered, unsupported platform), command\n * resolution throws `SandboxUnavailableError`. Linux kernel-policy failures\n * terminate the bootstrap before it loads the driver. Neither path falls\n * back to an unsandboxed driver.\n */\n\nimport { type ChildProcess, execFileSync, spawn } from \"node:child_process\";\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { dirname, join, delimiter as pathDelimiter } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst MACOS_PROFILE_PATH = join(__dirname, \"..\", \"sandbox\", \"macos.sb\");\nconst LINUX_BOOTSTRAP_PATH = join(__dirname, \"..\", \"python\", \"linux_sandbox.py\");\nconst DEFAULT_SANDBOX_EXEC_BIN = \"/usr/bin/sandbox-exec\";\n\n/**\n * Thrown whenever the OS-level sandbox can't be established for the current\n * platform/environment. Callers (see `session.ts`) must let this propagate\n * and refuse to run rather than catching it and falling back to an\n * unwrapped spawn -- that fallback is exactly the failure mode Architecture\n * section 1's \"fail closed\" requirement exists to prevent.\n */\nexport class SandboxUnavailableError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"SandboxUnavailableError\";\n\t}\n}\n\nexport interface SandboxSpawnOptions {\n\t/** Absolute path to `repl_driver.py`. */\n\tdriverPath: string;\n\t/**\n\t * Absolute path to the session's scratch workdir. The only place the\n\t * sandboxed process may read *and* write freely (besides the read-only\n\t * Python installation itself).\n\t */\n\tworkdir: string;\n\t/**\n\t * Which `python3` to sandbox. Defaults to whatever `python3` resolves to\n\t * on `PATH`. Overridable mainly so tests can point at a specific\n\t * interpreter without mutating `PATH`.\n\t */\n\tpythonBin?: string;\n\t/**\n\t * Override for the macOS `sandbox-exec` binary path. Mainly a test\n\t * injection point for proving fail-closed behavior (point it at a\n\t * nonexistent path and assert setup refuses rather than falling back to\n\t * an unwrapped spawn) -- see Architecture section 1's fail-closed\n\t * requirement and this plan's task 1 test 7.\n\t */\n\tsandboxExecBin?: string;\n\t/** Override for the Linux `unshare` binary path. Mainly for tests. */\n\tunshareBin?: string;\n\t/** Override for the Linux `bwrap` binary path (used if `unshare` is unavailable). Mainly for tests. */\n\tbwrapBin?: string;\n}\n\n/** The concrete command + args a `child_process.spawn` call should use. */\nexport interface SandboxedCommand {\n\tcommand: string;\n\targs: string[];\n}\n\ninterface PythonInstallation {\n\t/** `realpath()`'d absolute path to the interpreter binary that will actually run. */\n\trealBin: string;\n\t/**\n\t * Some macOS \"framework\" Python builds (the python.org installer,\n\t * Homebrew's `--with-framework`) internally `posix_spawn()` themselves\n\t * into a second binary at startup\n\t * (`Resources/Python.app/Contents/MacOS/Python`) before any user code\n\t * runs, to get a normal `.app` bundle identity (Tk/Cocoa access, Dock\n\t * icon, etc.). Confirmed empirically while scoping this profile: without\n\t * allowing this second exec, dyld aborts before `main()` ever runs. Equal\n\t * to `realBin` when no such companion binary exists.\n\t */\n\treexecBin: string;\n\t/** `realpath()`'d `sys.prefix` -- the install root the interpreter needs read access to in order to boot. */\n\tprefix: string;\n}\n\nconst pythonInstallationCache = new Map<string, PythonInstallation>();\n\n/**\n * Asks the given `python3` binary (via a real subprocess call -- there is no\n * static/config-file way to learn a Python install's true executable path\n * and stdlib root; every install lays these out differently) where it and\n * its stdlib actually live, then checks for the macOS framework-build\n * re-exec companion described above. Memoized per `pythonBin` string since\n * the answer can't change during this process's lifetime and this runs on\n * every sandboxed spawn.\n */\nfunction resolvePythonInstallation(pythonBin: string, isolated = false): PythonInstallation {\n\tconst cacheKey = JSON.stringify([pythonBin, isolated]);\n\tconst cached = pythonInstallationCache.get(cacheKey);\n\tif (cached) return cached;\n\n\tlet stdout: string;\n\ttry {\n\t\tstdout = execFileSync(\n\t\t\tpythonBin,\n\t\t\t[...(isolated ? [\"-I\", \"-S\"] : []), \"-c\", \"import sys; print(sys.executable); print(sys.prefix)\"],\n\t\t\t{\n\t\t\t\tencoding: \"utf8\",\n\t\t\t\t...(isolated ? { env: buildScrubbedEnv() } : {}),\n\t\t\t},\n\t\t);\n\t} catch (err) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: could not run ${JSON.stringify(pythonBin)} to discover its installation -- refusing to run the REPL driver unsandboxed. (${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t})`,\n\t\t);\n\t}\n\n\tconst lines = stdout.trim().split(\"\\n\");\n\tconst executableLine = lines[0];\n\tconst prefixLine = lines[1];\n\tif (!executableLine || !prefixLine) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: unexpected output discovering the Python installation for ${JSON.stringify(pythonBin)}: ${JSON.stringify(stdout)}`,\n\t\t);\n\t}\n\n\tlet realBin: string;\n\tlet prefix: string;\n\ttry {\n\t\trealBin = realpathSync(executableLine.trim());\n\t\tprefix = realpathSync(prefixLine.trim());\n\t} catch (err) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: could not resolve real filesystem paths for the Python installation -- refusing to run the REPL driver unsandboxed. (${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t})`,\n\t\t);\n\t}\n\n\tconst reexecCandidate = join(prefix, \"Resources\", \"Python.app\", \"Contents\", \"MacOS\", \"Python\");\n\tconst reexecBin = existsSync(reexecCandidate) ? realpathSync(reexecCandidate) : realBin;\n\n\tconst installation: PythonInstallation = { realBin, reexecBin, prefix };\n\tpythonInstallationCache.set(cacheKey, installation);\n\treturn installation;\n}\n\n/** Resolves a bare command name against `PATH`, returning `null` if not found. Used for the Linux wrappers. */\nfunction resolveOnPath(name: string): string | null {\n\tconst pathEnv = process.env.PATH ?? \"\";\n\tfor (const dir of pathEnv.split(pathDelimiter)) {\n\t\tif (!dir) continue;\n\t\tconst candidate = join(dir, name);\n\t\tif (existsSync(candidate)) return candidate;\n\t}\n\treturn null;\n}\n\nfunction buildMacosCommand(opts: SandboxSpawnOptions): SandboxedCommand {\n\tconst sandboxExecBin = opts.sandboxExecBin ?? DEFAULT_SANDBOX_EXEC_BIN;\n\tif (!existsSync(sandboxExecBin)) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: macOS sandbox-exec binary not found at ${JSON.stringify(sandboxExecBin)} -- refusing to run the REPL driver unsandboxed.`,\n\t\t);\n\t}\n\tif (!existsSync(MACOS_PROFILE_PATH)) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: sandbox profile missing at ${JSON.stringify(MACOS_PROFILE_PATH)} -- refusing to run the REPL driver unsandboxed.`,\n\t\t);\n\t}\n\n\tconst { realBin, reexecBin, prefix } = resolvePythonInstallation(opts.pythonBin ?? \"python3\");\n\n\tlet workdir: string;\n\tlet driverPath: string;\n\ttry {\n\t\tworkdir = realpathSync(opts.workdir);\n\t\tdriverPath = realpathSync(opts.driverPath);\n\t} catch (err) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: could not resolve workdir/driver paths -- refusing to run the REPL driver unsandboxed. (${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t})`,\n\t\t);\n\t}\n\n\treturn {\n\t\tcommand: sandboxExecBin,\n\t\targs: [\n\t\t\t\"-D\",\n\t\t\t`PYTHON_REAL_BIN=${realBin}`,\n\t\t\t\"-D\",\n\t\t\t`PYTHON_REEXEC_BIN=${reexecBin}`,\n\t\t\t\"-D\",\n\t\t\t`PYTHON_PREFIX=${prefix}`,\n\t\t\t\"-D\",\n\t\t\t`DRIVER_PATH=${driverPath}`,\n\t\t\t\"-D\",\n\t\t\t`WORKDIR=${workdir}`,\n\t\t\t\"-f\",\n\t\t\tMACOS_PROFILE_PATH,\n\t\t\trealBin,\n\t\t\tdriverPath,\n\t\t],\n\t};\n}\n\nfunction buildLinuxCommand(opts: SandboxSpawnOptions): SandboxedCommand {\n\tconst pythonBin = opts.pythonBin ?? \"python3\";\n\tif (!existsSync(LINUX_BOOTSTRAP_PATH)) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t\"sandbox_violation: Linux bootstrap is missing -- refusing to run the REPL driver unsandboxed.\",\n\t\t);\n\t}\n\n\tlet workdir: string;\n\tlet driverPath: string;\n\ttry {\n\t\tworkdir = realpathSync(opts.workdir);\n\t\tdriverPath = realpathSync(opts.driverPath);\n\t} catch (err) {\n\t\tthrow new SandboxUnavailableError(\n\t\t\t`sandbox_violation: could not resolve workdir/driver paths -- refusing to run the REPL driver unsandboxed. (${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t})`,\n\t\t);\n\t}\n\n\tconst unshareBin = opts.unshareBin ?? resolveOnPath(\"unshare\");\n\tif (unshareBin && existsSync(unshareBin)) {\n\t\tconst { realBin } = resolvePythonInstallation(pythonBin, true);\n\t\t// No --fork: Node's RSS monitor and kill target must remain Python's\n\t\t// PID. -I -S excludes user paths and site startup code before confinement.\n\t\treturn {\n\t\t\tcommand: unshareBin,\n\t\t\targs: [\n\t\t\t\t\"--user\",\n\t\t\t\t\"--map-root-user\",\n\t\t\t\t\"--net\",\n\t\t\t\t\"--mount\",\n\t\t\t\t\"--\",\n\t\t\t\trealBin,\n\t\t\t\t\"-I\",\n\t\t\t\t\"-S\",\n\t\t\t\tLINUX_BOOTSTRAP_PATH,\n\t\t\t\tdriverPath,\n\t\t\t\tworkdir,\n\t\t\t],\n\t\t};\n\t}\n\n\tconst bwrapBin = opts.bwrapBin ?? resolveOnPath(\"bwrap\");\n\tif (bwrapBin && existsSync(bwrapBin)) {\n\t\tconst { realBin, prefix } = resolvePythonInstallation(pythonBin, true);\n\t\tconst binds = [...new Set([\"/usr\", \"/lib\", \"/lib64\", \"/bin\", \"/sbin\", \"/nix/store\", prefix])]\n\t\t\t.filter((path) => existsSync(path))\n\t\t\t.flatMap((path) => [\"--ro-bind\", path, path]);\n\t\tfor (const path of [LINUX_BOOTSTRAP_PATH, driverPath, realBin]) binds.push(\"--ro-bind\", path, path);\n\t\treturn {\n\t\t\tcommand: bwrapBin,\n\t\t\targs: [\n\t\t\t\t\"--unshare-user\",\n\t\t\t\t\"--unshare-net\",\n\t\t\t\t\"--unshare-pid\",\n\t\t\t\t\"--die-with-parent\",\n\t\t\t\t...binds,\n\t\t\t\t\"--bind\",\n\t\t\t\tworkdir,\n\t\t\t\tworkdir,\n\t\t\t\t\"--proc\",\n\t\t\t\t\"/proc\",\n\t\t\t\t\"--dev\",\n\t\t\t\t\"/dev\",\n\t\t\t\t\"--chdir\",\n\t\t\t\tworkdir,\n\t\t\t\trealBin,\n\t\t\t\t\"-I\",\n\t\t\t\t\"-S\",\n\t\t\t\tLINUX_BOOTSTRAP_PATH,\n\t\t\t\tdriverPath,\n\t\t\t\tworkdir,\n\t\t\t],\n\t\t};\n\t}\n\n\tthrow new SandboxUnavailableError(\n\t\t\"sandbox_violation: neither `unshare` nor `bwrap` is available on this Linux host -- refusing to run the REPL driver unsandboxed.\",\n\t);\n}\n\n/**\n * Computes the sandboxed command/args for the current platform without\n * spawning anything. Exposed separately from `spawnSandboxed` so tests can\n * assert on the resolved invocation and so fail-closed behavior (throwing\n * `SandboxUnavailableError`) is observable synchronously, before any\n * process exists.\n */\nexport function resolveSandboxedCommand(opts: SandboxSpawnOptions): SandboxedCommand {\n\tif (process.platform === \"darwin\") return buildMacosCommand(opts);\n\tif (process.platform === \"linux\") return buildLinuxCommand(opts);\n\tthrow new SandboxUnavailableError(\n\t\t`sandbox_violation: no OS-level sandbox implementation for platform ${JSON.stringify(process.platform)} -- refusing to run the REPL driver unsandboxed.`,\n\t);\n}\n\n/**\n * Environment variables the sandboxed interpreter is allowed to see, by\n * allowlist rather than denylist. The OS sandbox itself denies network\n * access outright, so this isn't the primary defense against exfiltrating\n * anything read from here -- but the same object-graph escapes that defeat\n * in-process Python restriction (see this module's doc comment) can still\n * reach `os.environ` and write it into the session workdir even with\n * network denied, so the child process should never see more of the parent\n * Node process's environment (API keys, tokens, credentials, ...) than it\n * actually needs to boot a working interpreter.\n */\nconst SAFE_ENV_ALLOWLIST = [\n\t\"PATH\",\n\t\"HOME\",\n\t\"LANG\",\n\t\"LANGUAGE\",\n\t\"LC_ALL\",\n\t\"LC_CTYPE\",\n\t\"TMPDIR\",\n\t\"TZ\",\n\t\"PYTHONHASHSEED\",\n\t\"PYTHONIOENCODING\",\n\t\"PYTHONUTF8\",\n] as const;\n\n/**\n * Builds a scrubbed environment for the sandboxed child process from\n * `SAFE_ENV_ALLOWLIST` -- deliberately NOT `{ ...process.env }` (see\n * `SAFE_ENV_ALLOWLIST`'s doc comment for why the full parent environment,\n * secrets included, must not be handed to a process running LLM-authored\n * code).\n */\nfunction buildScrubbedEnv(): NodeJS.ProcessEnv {\n\tconst scrubbed: NodeJS.ProcessEnv = {};\n\tfor (const key of SAFE_ENV_ALLOWLIST) {\n\t\tconst value = process.env[key];\n\t\tif (value !== undefined) scrubbed[key] = value;\n\t}\n\treturn scrubbed;\n}\n\n/**\n * Spawns `repl_driver.py` wrapped in the platform's OS-level sandbox.\n * Throws `SandboxUnavailableError` for command-resolution failures. Linux\n * kernel-policy failures terminate the child before the driver runs; the\n * session's startup self-test observes that failure. There is no unwrapped\n * fallback path.\n */\nexport function spawnSandboxed(opts: SandboxSpawnOptions): ChildProcess {\n\tconst { command, args } = resolveSandboxedCommand(opts);\n\treturn spawn(command, args, { stdio: [\"pipe\", \"pipe\", \"pipe\"], env: buildScrubbedEnv() });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@draht/rlm",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.23-1",
|
|
4
4
|
"description": "Recursive Language Models — root-LLM-produces-code / REPL-executes / truncated-stdout scaffold",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"clean": "rm -rf dist"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@draht/ai": "2026.10.
|
|
28
|
-
"@draht/knowledge": "2026.10.
|
|
29
|
-
"@draht/router": "2026.10.
|
|
27
|
+
"@draht/ai": "2026.10.23-1",
|
|
28
|
+
"@draht/knowledge": "2026.10.23-1",
|
|
29
|
+
"@draht/router": "2026.10.23-1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"vitest": "^3.2.4",
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Trusted Linux bootstrap: restrict this Python process, then load the driver.
|
|
2
|
+
|
|
3
|
+
Requires Landlock ABI 3 (Linux 6.2+) and seccomp filters on x86-64 or AArch64.
|
|
4
|
+
Missing kernel support is fatal, before any driver code runs. The unshare
|
|
5
|
+
launcher execs Python without forking, preserving Node's RSS/kill target.
|
|
6
|
+
|
|
7
|
+
Landlock controls file contents and directory changes; seccomp closes its
|
|
8
|
+
metadata-mutation gaps and denies process creation/exec, sockets, namespace
|
|
9
|
+
changes and access to other processes. stat/readlink metadata remains visible.
|
|
10
|
+
No Python monkeypatch is part of this boundary: restrictions survive ctypes.
|
|
11
|
+
|
|
12
|
+
Kernel ABI references:
|
|
13
|
+
https://docs.kernel.org/userspace-api/landlock.html
|
|
14
|
+
https://docs.kernel.org/userspace-api/seccomp_filter.html
|
|
15
|
+
https://github.com/torvalds/linux/tree/v6.17/include/uapi
|
|
16
|
+
https://github.com/torvalds/linux/blob/v6.17/arch/x86/entry/syscalls/syscall_64.tbl
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import ctypes
|
|
20
|
+
import errno
|
|
21
|
+
import os
|
|
22
|
+
import runpy
|
|
23
|
+
import sys
|
|
24
|
+
import sysconfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
LIBC = ctypes.CDLL(None, use_errno=True)
|
|
28
|
+
LIBC.syscall.restype = ctypes.c_long
|
|
29
|
+
LIBC.prctl.argtypes = [ctypes.c_int] + [ctypes.c_ulong] * 4
|
|
30
|
+
|
|
31
|
+
# Columns are Linux x86-64 and asm-generic 64-bit (AArch64) syscall numbers.
|
|
32
|
+
# Only single-process CPython, pipes and Landlock-mediated file operations.
|
|
33
|
+
# In particular, metadata mutation, exec/clone, socket/IPC, signals to other
|
|
34
|
+
# processes, io_uring, ptrace/process_vm/pidfd and mount calls are absent.
|
|
35
|
+
ALLOWED_SYSCALLS = (
|
|
36
|
+
(0, 63), (1, 64), # read, write
|
|
37
|
+
(2, None), (257, 56), (437, 437), # open, openat, openat2
|
|
38
|
+
(3, 57), (436, 436), # close, close_range
|
|
39
|
+
(4, None), (5, 80), (6, None), # stat, fstat, lstat
|
|
40
|
+
(262, 79), (332, 291), # newfstatat, statx
|
|
41
|
+
(8, 62), (17, 67), (18, 68), # lseek, pread64, pwrite64
|
|
42
|
+
(19, 65), (20, 66), # readv, writev
|
|
43
|
+
(21, None), (269, 48), (439, 439), # access, faccessat, faccessat2
|
|
44
|
+
(89, None), (267, 78), # readlink, readlinkat
|
|
45
|
+
(78, None), (217, 61), # getdents, getdents64
|
|
46
|
+
(83, None), (258, 34), (84, None), # mkdir, mkdirat, rmdir
|
|
47
|
+
(87, None), (263, 35), # unlink, unlinkat
|
|
48
|
+
(82, None), (264, 38), (316, 276), # rename, renameat, renameat2
|
|
49
|
+
(86, None), (265, 37), # link, linkat
|
|
50
|
+
(88, None), (266, 36), # symlink, symlinkat
|
|
51
|
+
(76, 45), (77, 46), # truncate, ftruncate (Landlock ABI 3)
|
|
52
|
+
(74, 82), (75, 83), (95, 166), # fsync, fdatasync, umask
|
|
53
|
+
(32, 23), (33, None), (292, 24), # dup, dup2, dup3
|
|
54
|
+
(22, None), (293, 59), # pipe, pipe2
|
|
55
|
+
(80, 49), (81, 50), (79, 17), # chdir, fchdir, getcwd
|
|
56
|
+
(9, 222), (10, 226), (11, 215), # mmap, mprotect, munmap
|
|
57
|
+
(12, 214), (25, 216), (28, 233), # brk, mremap, madvise
|
|
58
|
+
(202, 98), (319, 279), # futex, memfd_create (exec still denied)
|
|
59
|
+
(7, None), (271, 73), (23, None), (270, 72), # poll, ppoll, select, pselect6
|
|
60
|
+
(13, 134), (14, 135), (15, 139), (131, 132), # signal handlers/masks/return/stack
|
|
61
|
+
(219, 128), # restart_syscall
|
|
62
|
+
(228, 113), (229, 114), # clock_gettime, clock_getres
|
|
63
|
+
(35, 101), (230, 115), # nanosleep, clock_nanosleep
|
|
64
|
+
(96, 169), (201, None), (318, 278), # gettimeofday, time, getrandom
|
|
65
|
+
(39, 172), (110, 173), (186, 178), # getpid, getppid, gettid
|
|
66
|
+
(102, 174), (107, 175), (104, 176), (108, 177), (115, 158), # IDs/groups
|
|
67
|
+
(63, 160), (98, 165), # uname, getrusage
|
|
68
|
+
(97, 163), (160, 164), (125, 90), # own rlimits, capget
|
|
69
|
+
(60, 93), (231, 94), # exit, exit_group
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Ruleset(ctypes.Structure):
|
|
74
|
+
_fields_ = [("handled_access_fs", ctypes.c_uint64)]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PathRule(ctypes.Structure):
|
|
78
|
+
_pack_ = 1
|
|
79
|
+
_fields_ = [("allowed_access", ctypes.c_uint64), ("parent_fd", ctypes.c_int32)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CapHeader(ctypes.Structure):
|
|
83
|
+
_fields_ = [("version", ctypes.c_uint32), ("pid", ctypes.c_int)]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CapData(ctypes.Structure):
|
|
87
|
+
_fields_ = [("effective", ctypes.c_uint32), ("permitted", ctypes.c_uint32), ("inheritable", ctypes.c_uint32)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Filter(ctypes.Structure):
|
|
91
|
+
_fields_ = [("code", ctypes.c_ushort), ("jt", ctypes.c_ubyte), ("jf", ctypes.c_ubyte), ("k", ctypes.c_uint32)]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FilterProgram(ctypes.Structure):
|
|
95
|
+
_fields_ = [("len", ctypes.c_ushort), ("filter", ctypes.POINTER(Filter))]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def checked(result, operation):
|
|
99
|
+
if result < 0:
|
|
100
|
+
error = ctypes.get_errno()
|
|
101
|
+
raise OSError(error, f"{operation}: {os.strerror(error)}")
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def syscall(number, *args):
|
|
106
|
+
return LIBC.syscall(ctypes.c_long(number), *(ctypes.c_long(arg) if isinstance(arg, int) else arg for arg in args))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def prctl(option, arg=0):
|
|
110
|
+
return LIBC.prctl(option, arg, 0, 0, 0)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def restrict_filesystem(driver, workdir):
|
|
114
|
+
abi = checked(syscall(444, 0, 0, 1), "query Landlock ABI")
|
|
115
|
+
if abi < 3:
|
|
116
|
+
raise RuntimeError(f"Landlock ABI 3 or newer required; kernel provides ABI {abi}")
|
|
117
|
+
# All filesystem rights through ABI 3, including execute and truncate.
|
|
118
|
+
# Newer device/socket rights need no grants: seccomp denies ioctl/socket.
|
|
119
|
+
handled = (1 << 15) - 1
|
|
120
|
+
read_file, read_dir = 1 << 2, 1 << 3
|
|
121
|
+
scratch = sum(1 << bit for bit in (1, 2, 3, 4, 5, 7, 8, 12, 13, 14))
|
|
122
|
+
readonly = {os.path.realpath(path) for path in sys.path if path and os.path.exists(path)}
|
|
123
|
+
readonly.update(os.path.realpath(path) for path in (
|
|
124
|
+
sysconfig.get_config_var("LIBDIR"), "/usr/lib", "/lib", "/lib64", "/nix/store",
|
|
125
|
+
) if path and os.path.isdir(path))
|
|
126
|
+
# Do not let a broad scratch path turn the readonly runtime or driver writable.
|
|
127
|
+
for path in (*readonly, driver, os.path.realpath(__file__)):
|
|
128
|
+
if os.path.commonpath((path, workdir)) == workdir:
|
|
129
|
+
raise RuntimeError("scratch directory overlaps the readonly runtime or driver")
|
|
130
|
+
attr = Ruleset(handled)
|
|
131
|
+
ruleset = checked(syscall(444, ctypes.byref(attr), ctypes.sizeof(attr), 0), "create Landlock ruleset")
|
|
132
|
+
|
|
133
|
+
def allow(path, rights):
|
|
134
|
+
fd = os.open(path, os.O_PATH | os.O_CLOEXEC)
|
|
135
|
+
try:
|
|
136
|
+
rule = PathRule(rights, fd)
|
|
137
|
+
checked(syscall(445, ruleset, 1, ctypes.byref(rule), 0), f"Landlock rule for {path}")
|
|
138
|
+
finally:
|
|
139
|
+
os.close(fd)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
for path in readonly:
|
|
143
|
+
allow(path, read_file | (read_dir if os.path.isdir(path) else 0))
|
|
144
|
+
for path in (driver, os.path.realpath(__file__), os.path.realpath(sys.executable), "/dev/urandom", "/etc/ld.so.cache"):
|
|
145
|
+
if os.path.isfile(path) or path == "/dev/urandom":
|
|
146
|
+
allow(path, read_file)
|
|
147
|
+
allow("/dev/null", read_file | (1 << 1))
|
|
148
|
+
allow(workdir, scratch)
|
|
149
|
+
checked(syscall(446, ruleset, 0), "enforce Landlock ruleset")
|
|
150
|
+
finally:
|
|
151
|
+
os.close(ruleset)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def drop_capabilities(column):
|
|
155
|
+
for capability in range(64):
|
|
156
|
+
present = prctl(23, capability) # PR_CAPBSET_READ
|
|
157
|
+
if present < 0 and ctypes.get_errno() == errno.EINVAL:
|
|
158
|
+
break
|
|
159
|
+
checked(present, "read capability bounding set")
|
|
160
|
+
if present:
|
|
161
|
+
checked(prctl(24, capability), "drop capability bounding set")
|
|
162
|
+
checked(LIBC.prctl(47, 4, 0, 0, 0), "clear ambient capabilities")
|
|
163
|
+
header, caps = CapHeader(0x20080522, 0), (CapData * 2)()
|
|
164
|
+
checked(syscall((126, 91)[column], ctypes.byref(header), caps), "clear process capabilities")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def restrict_syscalls(column):
|
|
168
|
+
allow, deny = 0x7FFF0000, 0x00050000 | errno.EPERM
|
|
169
|
+
# BPF_LD|W|ABS, JMP|JEQ|K, RET|K. Reject other architectures and x32.
|
|
170
|
+
instructions = [
|
|
171
|
+
(0x20, 0, 0, 4), (0x15, 1, 0, (0xC000003E, 0xC00000B7)[column]),
|
|
172
|
+
(0x06, 0, 0, 0x80000000), (0x20, 0, 0, 0),
|
|
173
|
+
(0x35, 0, 1, 0x40000000), (0x06, 0, 0, deny),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
def allow_argument(number, index, values):
|
|
177
|
+
# seccomp_data.args[] is 64-bit. Require a zero high word as well.
|
|
178
|
+
block = [(0x20, 0, 0, 20 + 8 * index), (0x15, 1, 0, 0), (0x06, 0, 0, deny),
|
|
179
|
+
(0x20, 0, 0, 16 + 8 * index)]
|
|
180
|
+
for value in values:
|
|
181
|
+
block.extend(((0x15, 0, 1, value), (0x06, 0, 0, allow)))
|
|
182
|
+
block.append((0x06, 0, 0, deny))
|
|
183
|
+
instructions.append((0x15, 0, len(block), number))
|
|
184
|
+
instructions.extend(block)
|
|
185
|
+
|
|
186
|
+
allow_argument((302, 261)[column], 0, (0,)) # prlimit64: this process only
|
|
187
|
+
allow_argument((157, 167)[column], 0, (21, 23, 39)) # prctl: inspect seccomp/bounds/no_new_privs
|
|
188
|
+
allow_argument((72, 25)[column], 1, (0, 1, 2, 3, 4, 1030)) # fcntl: FD/status only
|
|
189
|
+
for numbers in ALLOWED_SYSCALLS:
|
|
190
|
+
number = numbers[column]
|
|
191
|
+
if number is not None:
|
|
192
|
+
instructions.extend(((0x15, 0, 1, number), (0x06, 0, 0, allow)))
|
|
193
|
+
instructions.append((0x06, 0, 0, deny))
|
|
194
|
+
filters = (Filter * len(instructions))(*(Filter(*item) for item in instructions))
|
|
195
|
+
program = FilterProgram(len(filters), filters)
|
|
196
|
+
checked(LIBC.prctl(22, 2, ctypes.addressof(program), 0, 0), "install seccomp filter")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main():
|
|
200
|
+
machine = os.uname().machine
|
|
201
|
+
if machine not in ("x86_64", "aarch64") or ctypes.sizeof(ctypes.c_void_p) != 8:
|
|
202
|
+
raise RuntimeError(f"unsupported Linux sandbox architecture: {machine}")
|
|
203
|
+
column = 0 if machine == "x86_64" else 1
|
|
204
|
+
driver, workdir = (os.path.realpath(value) for value in sys.argv[1:])
|
|
205
|
+
if not os.path.isfile(driver) or not os.path.isdir(workdir):
|
|
206
|
+
raise RuntimeError("driver must be a regular file and scratch must be a directory")
|
|
207
|
+
# Landlock does not revoke already-open descriptors. Node passes only the
|
|
208
|
+
# three protocol pipes; close anything else before installing restrictions.
|
|
209
|
+
for entry in os.listdir("/proc/self/fd"):
|
|
210
|
+
fd = int(entry)
|
|
211
|
+
if fd > 2:
|
|
212
|
+
try:
|
|
213
|
+
os.close(fd)
|
|
214
|
+
except OSError as error:
|
|
215
|
+
if error.errno != errno.EBADF:
|
|
216
|
+
raise
|
|
217
|
+
os.chdir(workdir)
|
|
218
|
+
checked(prctl(38, 1), "set no_new_privs")
|
|
219
|
+
restrict_filesystem(driver, workdir)
|
|
220
|
+
drop_capabilities(column)
|
|
221
|
+
restrict_syscalls(column)
|
|
222
|
+
sys.argv = [driver]
|
|
223
|
+
runpy.run_path(driver, run_name="__main__")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
try:
|
|
228
|
+
main()
|
|
229
|
+
except Exception as error:
|
|
230
|
+
print(f"sandbox_violation: Linux sandbox setup/driver failed: {error}", file=sys.stderr)
|
|
231
|
+
sys.exit(1)
|