sandboxedjs 0.1.72 → 0.1.74
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 +46 -2
- package/bin/sandboxedjs-build-wheels.mjs +143 -0
- package/dist/agent.d.cts +2 -2
- package/dist/agent.d.ts +2 -2
- package/dist/{container-BwXE2J26.d.cts → container-CsLNpeyi.d.ts} +26 -3
- package/dist/{container-WPf_A76-.d.ts → container-DAWPIkGG.d.cts} +26 -3
- package/dist/{contracts-CVgctitO.d.cts → contracts-C2_dTgk3.d.cts} +30 -1
- package/dist/{contracts-CVgctitO.d.ts → contracts-C2_dTgk3.d.ts} +30 -1
- package/dist/index.cjs +3005 -1585
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -5
- package/dist/index.d.ts +57 -5
- package/dist/index.js +3004 -1584
- package/dist/index.js.map +1 -1
- package/dist/{memory-volume-CWuWScIs.d.ts → memory-volume-CG56p660.d.ts} +1 -1
- package/dist/{memory-volume-DlMSAh-K.d.cts → memory-volume-DqEWFo7t.d.cts} +1 -1
- package/dist/python-abi.cjs +6 -6
- package/dist/python-abi.cjs.map +1 -1
- package/dist/python-abi.d.cts +3 -3
- package/dist/python-abi.d.ts +3 -3
- package/dist/python-abi.js +6 -6
- package/dist/python-abi.js.map +1 -1
- package/dist/python-worker.js +7 -7
- package/dist/python-worker.js.map +1 -1
- package/dist/rolldown-wasi-worker.js +6 -2
- package/dist/rolldown-wasi-worker.js.map +1 -1
- package/dist/worker-entry.js +883 -77
- package/dist/worker-entry.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -22,6 +22,27 @@ await box.exec("python3 -c 'print(2**64)'"); // → 18446744073709551616
|
|
|
22
22
|
box.dispose();
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Python packages with compiled extensions
|
|
26
|
+
|
|
27
|
+
`pip install` works for packages with C, Cython, Rust or Meson extensions. A
|
|
28
|
+
wheel built for this runtime is used when one exists; otherwise one is built
|
|
29
|
+
from source, from a recipe generated out of what PyPI and the package already
|
|
30
|
+
state.
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
configurePython({ buildFromSource: true }); // build here (Node)
|
|
34
|
+
configurePython({ buildFromSource: "http://localhost:4180/build" }); // ask a builder
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Browsers have no compiler, so they ask a machine that does:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx sandboxedjs-build-wheels 4180
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
See `docs/python/build-on-miss.md`, and `docs/python/compatibility.md` for what
|
|
44
|
+
has been built and tested.
|
|
45
|
+
|
|
25
46
|
## Why
|
|
26
47
|
|
|
27
48
|
Sometimes you need to run untrusted or generated code, give an AI agent a shell, build a
|
|
@@ -144,6 +165,17 @@ await session.run("echo $TOKEN in $(pwd)"); // → abc in /app
|
|
|
144
165
|
|
|
145
166
|
### Long-running processes
|
|
146
167
|
|
|
168
|
+
A job put in the background with `&` keeps running after the command that started it returns,
|
|
169
|
+
even from a stateless `exec`, and stops with `kill %N` in a session or when the container is
|
|
170
|
+
disposed:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
await box.exec("node server.js > /tmp/server.log 2>&1 &", { cwd: "/app" });
|
|
174
|
+
await box.waitForPort(3000);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
To hold the process yourself, spawn it:
|
|
178
|
+
|
|
147
179
|
```ts
|
|
148
180
|
const proc = box.spawn("node server.js", { cwd: "/app" });
|
|
149
181
|
|
|
@@ -524,7 +556,10 @@ The container has no access to your filesystem, environment, or network unless y
|
|
|
524
556
|
- The filesystem is entirely in memory. Code inside cannot read or write a host path — there is
|
|
525
557
|
no `/Users`, no `/home/you`, no way to reach one.
|
|
526
558
|
- Outbound network access is **off by default**; `curl https://…` fails until you pass
|
|
527
|
-
`network: { allowOutbound: true }`, optionally narrowed with `allowedHosts`.
|
|
559
|
+
`network: { allowOutbound: true }`, optionally narrowed with `allowedHosts`. The same policy
|
|
560
|
+
binds a program's own `fetch`, `http`, `https` and `WebSocket`: a refused request fails with
|
|
561
|
+
`ENETUNREACH`. `localhost` and `127.0.0.1` always mean the container's own servers — never the
|
|
562
|
+
host's.
|
|
528
563
|
- Host files enter only through `files`, `mount()` or `copyIn()`, and leave only through
|
|
529
564
|
`copyOut()` or `fs.readFile()`.
|
|
530
565
|
- `timeoutMs` bounds runaway commands, and `exec` settles even when a process ignores its kill
|
|
@@ -979,7 +1014,16 @@ Honest list of what does not work:
|
|
|
979
1014
|
through is to answer **No** to a prompt like `npm create vite`'s "Install with npm and start
|
|
980
1015
|
now?" and run `npm install && npm run dev` from the shell instead.
|
|
981
1016
|
- **No `net`, `tls`, `worker_threads` or `vm`.** `http` and `https` are served by a virtual stack
|
|
982
|
-
that `request()` talks to directly, so servers work; raw sockets do not.
|
|
1017
|
+
that `request()` talks to directly, so servers work; raw sockets do not. A program can reach
|
|
1018
|
+
servers anywhere in the container over HTTP (`http.get`, `fetch`), but not open a `WebSocket` to
|
|
1019
|
+
one.
|
|
1020
|
+
- **`node:test` covers what test files use** — `test`/`it`, `describe`, hooks, subtests, `skip`,
|
|
1021
|
+
`todo`, `only`, `mock.fn` and `mock.method`, with `spec` and `tap` reports — and `node --test`
|
|
1022
|
+
finds and runs test files as Node 22 does. `run()`, coverage and mock timers are not
|
|
1023
|
+
implemented. A test file exits when its tests finish, as under `--test-force-exit`.
|
|
1024
|
+
- **On the in-realm runtime, programs share one global object.** A global one program sets is
|
|
1025
|
+
visible to the next. The worker runtime (the default where shared memory is available) gives
|
|
1026
|
+
each program its own.
|
|
983
1027
|
- **Python is source-built CPython/WASM.** Each program gets its own interpreter
|
|
984
1028
|
process worker. Pure-Python wheels install normally; native extensions must
|
|
985
1029
|
be linked or published for Emscripten. The bundled wheel index includes the
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A wheel index that builds what it does not have.
|
|
4
|
+
*
|
|
5
|
+
* A browser cannot compile a C extension and never will, so "build it from
|
|
6
|
+
* source" there can only mean "ask a machine that can". This is that machine,
|
|
7
|
+
* reduced to the smallest thing that works: it serves the wheel index over
|
|
8
|
+
* HTTP, and it builds a package on request using the same pipeline and the
|
|
9
|
+
* same ABI contract as everything else.
|
|
10
|
+
*
|
|
11
|
+
* sandboxedjs-build-wheels [port=4180]
|
|
12
|
+
*
|
|
13
|
+
* Point a host at it:
|
|
14
|
+
*
|
|
15
|
+
* configurePython({
|
|
16
|
+
* wheelIndex: "http://localhost:4180",
|
|
17
|
+
* buildFromSource: "http://localhost:4180/build",
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* It binds loopback only. Building runs a package's own build system, which is
|
|
21
|
+
* arbitrary code execution by design, so exposing this to a network would be
|
|
22
|
+
* handing that to whoever can reach it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createServer } from "node:http";
|
|
26
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join, resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { spawn } from "node:child_process";
|
|
30
|
+
|
|
31
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../python-runtime");
|
|
32
|
+
const wheels = join(root, "out/wheels");
|
|
33
|
+
|
|
34
|
+
if (!existsSync(join(root, "scripts/build_extension.py"))) {
|
|
35
|
+
console.error("sandboxedjs-build-wheels: the build pipeline is only present in a checkout");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const port = Number(process.argv[2] ?? 4180);
|
|
40
|
+
const building = new Map();
|
|
41
|
+
const finished = new Map();
|
|
42
|
+
|
|
43
|
+
const run = (args) => new Promise((done) => {
|
|
44
|
+
const child = spawn("python3", args, { cwd: root });
|
|
45
|
+
let stdout = "", stderr = "";
|
|
46
|
+
child.stdout.on("data", (c) => { stdout += String(c); });
|
|
47
|
+
child.stderr.on("data", (c) => { stderr += String(c); });
|
|
48
|
+
child.on("error", (e) => done({ code: -1, stdout, stderr: String(e) }));
|
|
49
|
+
child.on("close", (code) => done({ code: code ?? -1, stdout, stderr }));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
async function build(requirement) {
|
|
53
|
+
const recipe = await run(["scripts/auto_recipe.py", requirement]);
|
|
54
|
+
if (recipe.code !== 0) {
|
|
55
|
+
let reported = {};
|
|
56
|
+
try { reported = JSON.parse(recipe.stdout.trim().split("\n").at(-1) ?? "{}"); } catch {}
|
|
57
|
+
return {
|
|
58
|
+
built: false,
|
|
59
|
+
classification: reported.classification ?? "blocked-toolchain",
|
|
60
|
+
reason: reported.reason ?? recipe.stderr.trim().slice(-400),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const path = recipe.stdout.trim().split("\n").at(-1);
|
|
64
|
+
const made = await run(["scripts/build_extension.py", path]);
|
|
65
|
+
if (made.code !== 0) {
|
|
66
|
+
return {
|
|
67
|
+
built: false, classification: "blocked-toolchain",
|
|
68
|
+
reason: `${made.stdout}\n${made.stderr}`.trim().split("\n").slice(-12).join("\n"),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const indexed = await run(["scripts/build_index.py"]);
|
|
72
|
+
if (indexed.code !== 0) {
|
|
73
|
+
return { built: false, classification: "blocked-toolchain", reason: indexed.stderr.slice(-300) };
|
|
74
|
+
}
|
|
75
|
+
return { built: true, classification: "supported-generic" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const server = createServer(async (request, response) => {
|
|
79
|
+
const cors = {
|
|
80
|
+
"access-control-allow-origin": request.headers.origin ?? "*",
|
|
81
|
+
"access-control-allow-headers": "content-type",
|
|
82
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
83
|
+
};
|
|
84
|
+
if (request.method === "OPTIONS") { response.writeHead(204, cors).end(); return; }
|
|
85
|
+
|
|
86
|
+
const path = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
|
|
87
|
+
|
|
88
|
+
if (request.method === "POST" && path === "/build") {
|
|
89
|
+
const body = await new Promise((done) => {
|
|
90
|
+
let text = ""; request.on("data", (c) => { text += c; }); request.on("end", () => done(text));
|
|
91
|
+
});
|
|
92
|
+
let requirement = "";
|
|
93
|
+
try { requirement = String(JSON.parse(body || "{}").requirement ?? ""); } catch {}
|
|
94
|
+
/* Only a package name and optional pin. Anything else would be handed to a
|
|
95
|
+
* shell-adjacent build as an argument. */
|
|
96
|
+
if (!/^[A-Za-z0-9._-]+(==[A-Za-z0-9._+-]+)?$/.test(requirement)) {
|
|
97
|
+
response.writeHead(400, { ...cors, "content-type": "application/json" })
|
|
98
|
+
.end(JSON.stringify({ built: false, reason: "invalid requirement" }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
/* Submit and poll, never one long request. A build takes minutes and an
|
|
102
|
+
* HTTP client -- especially one inside a sandboxed worker -- will drop a
|
|
103
|
+
* connection held open that long, which looks to the caller like the
|
|
104
|
+
* service being unreachable while it is in fact working. This is why the
|
|
105
|
+
* protocol in docs/python/build-on-miss.md has states rather than a single
|
|
106
|
+
* blocking call.
|
|
107
|
+
*
|
|
108
|
+
* One build per package: two identical requests are the same build. */
|
|
109
|
+
if (!building.has(requirement) && !finished.has(requirement)) {
|
|
110
|
+
console.log(`building ${requirement}`);
|
|
111
|
+
building.set(requirement, build(requirement).then((verdict) => {
|
|
112
|
+
console.log(` ${requirement}: ${verdict.built ? "built" : verdict.classification}`);
|
|
113
|
+
finished.set(requirement, verdict);
|
|
114
|
+
building.delete(requirement);
|
|
115
|
+
return verdict;
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
const done = finished.get(requirement);
|
|
119
|
+
const verdict = done
|
|
120
|
+
? { ...done, state: done.built ? "published" : "failed" }
|
|
121
|
+
: { state: "building-wheel" };
|
|
122
|
+
response.writeHead(200, { ...cors, "content-type": "application/json" })
|
|
123
|
+
.end(JSON.stringify(verdict));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const name = path === "/" ? "/index.json" : path;
|
|
128
|
+
const file = join(wheels, name);
|
|
129
|
+
if (!file.startsWith(wheels) || !existsSync(file)) {
|
|
130
|
+
response.writeHead(404, cors).end("not found");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
response.writeHead(200, {
|
|
134
|
+
...cors,
|
|
135
|
+
"content-type": name.endsWith(".json") ? "application/json" : "application/octet-stream",
|
|
136
|
+
}).end(readFileSync(file));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
server.listen(port, "127.0.0.1", () => {
|
|
140
|
+
console.log(`SandboxedJs wheel builder: http://127.0.0.1:${port}`);
|
|
141
|
+
console.log(` index http://127.0.0.1:${port}/index.json`);
|
|
142
|
+
console.log(` build POST http://127.0.0.1:${port}/build {"requirement":"numpy"}`);
|
|
143
|
+
});
|
package/dist/agent.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as Container } from './container-
|
|
2
|
-
import './contracts-
|
|
1
|
+
import { C as Container } from './container-DAWPIkGG.cjs';
|
|
2
|
+
import './contracts-C2_dTgk3.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Structural copies of the LangChain Deep Agents backend contract.
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as Vfs, C as Cred, f as RuntimePod, D as DirEntry, p as Stats } from './contracts-
|
|
1
|
+
import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-C2_dTgk3.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Byte streams for stdin/stdout/stderr, pipelines and redirections.
|
|
@@ -487,6 +487,8 @@ declare class NetworkStack {
|
|
|
487
487
|
timeoutMs?: number;
|
|
488
488
|
intervalMs?: number;
|
|
489
489
|
}): Promise<boolean>;
|
|
490
|
+
/** The policy every way out of the container applies, not only the shell's. */
|
|
491
|
+
get policy(): OutboundPolicy;
|
|
490
492
|
outboundAllowed(url: string): boolean;
|
|
491
493
|
procNetDev(): string;
|
|
492
494
|
procNetRoute(): string;
|
|
@@ -979,8 +981,13 @@ declare class Shell {
|
|
|
979
981
|
captureSubshell(command: string): Promise<string>;
|
|
980
982
|
/** `<(cmd)` — run the command now and hand back a path holding its output. */
|
|
981
983
|
private makeProcessSubstitution;
|
|
982
|
-
/**
|
|
983
|
-
|
|
984
|
+
/**
|
|
985
|
+
* A copy that shares nothing mutable with this shell.
|
|
986
|
+
*
|
|
987
|
+
* Subshells and pipeline stages run as part of this shell's process. A
|
|
988
|
+
* background job passes a process of its own, the way fork(2) gives one.
|
|
989
|
+
*/
|
|
990
|
+
fork(proc?: Process): Shell;
|
|
984
991
|
private currentIO;
|
|
985
992
|
run(node: Node, io: ShellIO): Promise<number>;
|
|
986
993
|
private runList;
|
|
@@ -1238,6 +1245,14 @@ interface PythonOptions {
|
|
|
1238
1245
|
* no published wheel targets wasm32-emscripten.
|
|
1239
1246
|
*/
|
|
1240
1247
|
wheelIndex?: WheelIndex | string | null;
|
|
1248
|
+
/**
|
|
1249
|
+
* Build a missing wheel instead of reporting it.
|
|
1250
|
+
*
|
|
1251
|
+
* `true` builds locally, which needs Node and the build pipeline. A URL asks
|
|
1252
|
+
* a build service -- which is how a browser gets a wheel built, having no
|
|
1253
|
+
* compiler of its own. See docs/python/build-on-miss.md.
|
|
1254
|
+
*/
|
|
1255
|
+
buildFromSource?: boolean | string;
|
|
1241
1256
|
}
|
|
1242
1257
|
declare function configurePython(options?: PythonOptions): void;
|
|
1243
1258
|
declare const isPythonAvailable: typeof isCPythonAvailable;
|
|
@@ -1535,6 +1550,14 @@ declare class Container {
|
|
|
1535
1550
|
}): Session;
|
|
1536
1551
|
/** The container-wide session used by `shell()` shorthand helpers. */
|
|
1537
1552
|
get shell(): Session;
|
|
1553
|
+
/**
|
|
1554
|
+
* The streams one call collects into.
|
|
1555
|
+
*
|
|
1556
|
+
* They are detachable because a background job started by the command keeps
|
|
1557
|
+
* writing after the call has returned. Nothing reads the result any more by
|
|
1558
|
+
* then, so without `detach` a chatty server would grow the buffer for as long
|
|
1559
|
+
* as it ran; afterwards its output still reaches the container-wide taps.
|
|
1560
|
+
*/
|
|
1538
1561
|
private makeStdio;
|
|
1539
1562
|
/** Send an HTTP request to a server running inside the container. */
|
|
1540
1563
|
request(port: number, init?: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as Vfs, C as Cred, f as RuntimePod, D as DirEntry, p as Stats } from './contracts-
|
|
1
|
+
import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-C2_dTgk3.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Byte streams for stdin/stdout/stderr, pipelines and redirections.
|
|
@@ -487,6 +487,8 @@ declare class NetworkStack {
|
|
|
487
487
|
timeoutMs?: number;
|
|
488
488
|
intervalMs?: number;
|
|
489
489
|
}): Promise<boolean>;
|
|
490
|
+
/** The policy every way out of the container applies, not only the shell's. */
|
|
491
|
+
get policy(): OutboundPolicy;
|
|
490
492
|
outboundAllowed(url: string): boolean;
|
|
491
493
|
procNetDev(): string;
|
|
492
494
|
procNetRoute(): string;
|
|
@@ -979,8 +981,13 @@ declare class Shell {
|
|
|
979
981
|
captureSubshell(command: string): Promise<string>;
|
|
980
982
|
/** `<(cmd)` — run the command now and hand back a path holding its output. */
|
|
981
983
|
private makeProcessSubstitution;
|
|
982
|
-
/**
|
|
983
|
-
|
|
984
|
+
/**
|
|
985
|
+
* A copy that shares nothing mutable with this shell.
|
|
986
|
+
*
|
|
987
|
+
* Subshells and pipeline stages run as part of this shell's process. A
|
|
988
|
+
* background job passes a process of its own, the way fork(2) gives one.
|
|
989
|
+
*/
|
|
990
|
+
fork(proc?: Process): Shell;
|
|
984
991
|
private currentIO;
|
|
985
992
|
run(node: Node, io: ShellIO): Promise<number>;
|
|
986
993
|
private runList;
|
|
@@ -1238,6 +1245,14 @@ interface PythonOptions {
|
|
|
1238
1245
|
* no published wheel targets wasm32-emscripten.
|
|
1239
1246
|
*/
|
|
1240
1247
|
wheelIndex?: WheelIndex | string | null;
|
|
1248
|
+
/**
|
|
1249
|
+
* Build a missing wheel instead of reporting it.
|
|
1250
|
+
*
|
|
1251
|
+
* `true` builds locally, which needs Node and the build pipeline. A URL asks
|
|
1252
|
+
* a build service -- which is how a browser gets a wheel built, having no
|
|
1253
|
+
* compiler of its own. See docs/python/build-on-miss.md.
|
|
1254
|
+
*/
|
|
1255
|
+
buildFromSource?: boolean | string;
|
|
1241
1256
|
}
|
|
1242
1257
|
declare function configurePython(options?: PythonOptions): void;
|
|
1243
1258
|
declare const isPythonAvailable: typeof isCPythonAvailable;
|
|
@@ -1535,6 +1550,14 @@ declare class Container {
|
|
|
1535
1550
|
}): Session;
|
|
1536
1551
|
/** The container-wide session used by `shell()` shorthand helpers. */
|
|
1537
1552
|
get shell(): Session;
|
|
1553
|
+
/**
|
|
1554
|
+
* The streams one call collects into.
|
|
1555
|
+
*
|
|
1556
|
+
* They are detachable because a background job started by the command keeps
|
|
1557
|
+
* writing after the call has returned. Nothing reads the result any more by
|
|
1558
|
+
* then, so without `detach` a chatty server would grow the buffer for as long
|
|
1559
|
+
* as it ran; afterwards its output still reaches the container-wide taps.
|
|
1560
|
+
*/
|
|
1538
1561
|
private makeStdio;
|
|
1539
1562
|
/** Send an HTTP request to a server running inside the container. */
|
|
1540
1563
|
request(port: number, init?: {
|
|
@@ -347,6 +347,27 @@ declare class VirtualTcpNetwork {
|
|
|
347
347
|
closeAll(): void;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
/**
|
|
351
|
+
* The outbound network policy, as plain functions every client consults.
|
|
352
|
+
*
|
|
353
|
+
* The container has several ways out — `curl` and `wget` in the shell, `http`,
|
|
354
|
+
* `https`, `fetch` and `WebSocket` in a Node program, sockets in Python — and
|
|
355
|
+
* they used to decide separately. Only the shell asked: a Node program's
|
|
356
|
+
* `fetch("https://…")` reached the internet from a container booted with
|
|
357
|
+
* outbound access off. One policy, applied at each exit, is what makes
|
|
358
|
+
* `network: { allowOutbound: false }` mean what it says.
|
|
359
|
+
*
|
|
360
|
+
* Loopback is not "outbound" at all. `127.0.0.1` inside the container is the
|
|
361
|
+
* container, so those requests are routed to its own servers and never handed
|
|
362
|
+
* to the host's network stack, whatever the policy allows.
|
|
363
|
+
*/
|
|
364
|
+
interface OutboundPolicy {
|
|
365
|
+
/** Whether requests may leave the container at all. */
|
|
366
|
+
allowOutbound: boolean;
|
|
367
|
+
/** When outbound is on, the hosts it may reach (subdomains included). `null` means any. */
|
|
368
|
+
allowedHosts: string[] | null;
|
|
369
|
+
}
|
|
370
|
+
|
|
350
371
|
/**
|
|
351
372
|
* Clean-room contracts between SandboxedJS and its JavaScript runtime.
|
|
352
373
|
*
|
|
@@ -479,9 +500,17 @@ interface RuntimePod {
|
|
|
479
500
|
headers: Record<string, string>;
|
|
480
501
|
body: Uint8Array;
|
|
481
502
|
}) => Promise<RuntimeHttpResponse>): () => void;
|
|
503
|
+
/**
|
|
504
|
+
* Apply the container's outbound policy to programs this pod runs.
|
|
505
|
+
*
|
|
506
|
+
* Optional so a pod written elsewhere still satisfies the contract, but a pod
|
|
507
|
+
* without it cannot keep a program's own `fetch` inside the policy — only
|
|
508
|
+
* the shell's `curl` would honour it.
|
|
509
|
+
*/
|
|
510
|
+
setNetworkPolicy?(policy: OutboundPolicy): void;
|
|
482
511
|
snapshot(options?: Record<string, unknown>): unknown;
|
|
483
512
|
restore(snapshot: unknown, options?: Record<string, unknown>): Promise<void>;
|
|
484
513
|
teardown(): void;
|
|
485
514
|
}
|
|
486
515
|
|
|
487
|
-
export { type Cred as C, type DirEntry as D, type RuntimeVolume as R, type SpawnChild as S, Vfs as V, type WriteOptions as W, VirtualTcpNetwork as a, type RuntimeHttpResponse as b, type SyncSpawn as c, type VolumeStat as d, type VolumeStats as e, type RuntimePod as f, type RuntimePackageInstaller as g, type ChildSpawnConfig as h, type ChildHandle as i, type RuntimeProcess as j, type RuntimeSocketPeer as k, type RuntimeConnection as l, ROOT_CRED as m, type RuntimeProcessManager as n, type RuntimeProcessResult as o, Stats as p, type VirtualNode as q, type VirtualProvider as r, applyChmod as s, createChildProcessModule as t, formatMode as u, makeCred as v, octalMode as w, parseUmask as x };
|
|
516
|
+
export { type Cred as C, type DirEntry as D, type OutboundPolicy as O, type RuntimeVolume as R, type SpawnChild as S, Vfs as V, type WriteOptions as W, VirtualTcpNetwork as a, type RuntimeHttpResponse as b, type SyncSpawn as c, type VolumeStat as d, type VolumeStats as e, type RuntimePod as f, type RuntimePackageInstaller as g, type ChildSpawnConfig as h, type ChildHandle as i, type RuntimeProcess as j, type RuntimeSocketPeer as k, type RuntimeConnection as l, ROOT_CRED as m, type RuntimeProcessManager as n, type RuntimeProcessResult as o, Stats as p, type VirtualNode as q, type VirtualProvider as r, applyChmod as s, createChildProcessModule as t, formatMode as u, makeCred as v, octalMode as w, parseUmask as x };
|
|
@@ -347,6 +347,27 @@ declare class VirtualTcpNetwork {
|
|
|
347
347
|
closeAll(): void;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
/**
|
|
351
|
+
* The outbound network policy, as plain functions every client consults.
|
|
352
|
+
*
|
|
353
|
+
* The container has several ways out — `curl` and `wget` in the shell, `http`,
|
|
354
|
+
* `https`, `fetch` and `WebSocket` in a Node program, sockets in Python — and
|
|
355
|
+
* they used to decide separately. Only the shell asked: a Node program's
|
|
356
|
+
* `fetch("https://…")` reached the internet from a container booted with
|
|
357
|
+
* outbound access off. One policy, applied at each exit, is what makes
|
|
358
|
+
* `network: { allowOutbound: false }` mean what it says.
|
|
359
|
+
*
|
|
360
|
+
* Loopback is not "outbound" at all. `127.0.0.1` inside the container is the
|
|
361
|
+
* container, so those requests are routed to its own servers and never handed
|
|
362
|
+
* to the host's network stack, whatever the policy allows.
|
|
363
|
+
*/
|
|
364
|
+
interface OutboundPolicy {
|
|
365
|
+
/** Whether requests may leave the container at all. */
|
|
366
|
+
allowOutbound: boolean;
|
|
367
|
+
/** When outbound is on, the hosts it may reach (subdomains included). `null` means any. */
|
|
368
|
+
allowedHosts: string[] | null;
|
|
369
|
+
}
|
|
370
|
+
|
|
350
371
|
/**
|
|
351
372
|
* Clean-room contracts between SandboxedJS and its JavaScript runtime.
|
|
352
373
|
*
|
|
@@ -479,9 +500,17 @@ interface RuntimePod {
|
|
|
479
500
|
headers: Record<string, string>;
|
|
480
501
|
body: Uint8Array;
|
|
481
502
|
}) => Promise<RuntimeHttpResponse>): () => void;
|
|
503
|
+
/**
|
|
504
|
+
* Apply the container's outbound policy to programs this pod runs.
|
|
505
|
+
*
|
|
506
|
+
* Optional so a pod written elsewhere still satisfies the contract, but a pod
|
|
507
|
+
* without it cannot keep a program's own `fetch` inside the policy — only
|
|
508
|
+
* the shell's `curl` would honour it.
|
|
509
|
+
*/
|
|
510
|
+
setNetworkPolicy?(policy: OutboundPolicy): void;
|
|
482
511
|
snapshot(options?: Record<string, unknown>): unknown;
|
|
483
512
|
restore(snapshot: unknown, options?: Record<string, unknown>): Promise<void>;
|
|
484
513
|
teardown(): void;
|
|
485
514
|
}
|
|
486
515
|
|
|
487
|
-
export { type Cred as C, type DirEntry as D, type RuntimeVolume as R, type SpawnChild as S, Vfs as V, type WriteOptions as W, VirtualTcpNetwork as a, type RuntimeHttpResponse as b, type SyncSpawn as c, type VolumeStat as d, type VolumeStats as e, type RuntimePod as f, type RuntimePackageInstaller as g, type ChildSpawnConfig as h, type ChildHandle as i, type RuntimeProcess as j, type RuntimeSocketPeer as k, type RuntimeConnection as l, ROOT_CRED as m, type RuntimeProcessManager as n, type RuntimeProcessResult as o, Stats as p, type VirtualNode as q, type VirtualProvider as r, applyChmod as s, createChildProcessModule as t, formatMode as u, makeCred as v, octalMode as w, parseUmask as x };
|
|
516
|
+
export { type Cred as C, type DirEntry as D, type OutboundPolicy as O, type RuntimeVolume as R, type SpawnChild as S, Vfs as V, type WriteOptions as W, VirtualTcpNetwork as a, type RuntimeHttpResponse as b, type SyncSpawn as c, type VolumeStat as d, type VolumeStats as e, type RuntimePod as f, type RuntimePackageInstaller as g, type ChildSpawnConfig as h, type ChildHandle as i, type RuntimeProcess as j, type RuntimeSocketPeer as k, type RuntimeConnection as l, ROOT_CRED as m, type RuntimeProcessManager as n, type RuntimeProcessResult as o, Stats as p, type VirtualNode as q, type VirtualProvider as r, applyChmod as s, createChildProcessModule as t, formatMode as u, makeCred as v, octalMode as w, parseUmask as x };
|