sandboxedjs 0.1.72 → 0.1.73
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 +21 -0
- package/bin/sandboxedjs-build-wheels.mjs +143 -0
- package/dist/agent.d.cts +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/{container-WPf_A76-.d.ts → container-B5X6stfx.d.ts} +8 -0
- package/dist/{container-BwXE2J26.d.cts → container-BxEoVOmX.d.cts} +8 -0
- package/dist/index.cjs +532 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +532 -302
- package/dist/index.js.map +1 -1
- package/dist/python-abi.cjs +6 -6
- package/dist/python-abi.cjs.map +1 -1
- 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 +1 -1
- package/dist/rolldown-wasi-worker.js.map +1 -1
- package/dist/worker-entry.js +22 -22
- package/dist/worker-entry.js.map +1 -1
- package/package.json +3 -2
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
|
|
@@ -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
package/dist/agent.d.ts
CHANGED
|
@@ -1238,6 +1238,14 @@ interface PythonOptions {
|
|
|
1238
1238
|
* no published wheel targets wasm32-emscripten.
|
|
1239
1239
|
*/
|
|
1240
1240
|
wheelIndex?: WheelIndex | string | null;
|
|
1241
|
+
/**
|
|
1242
|
+
* Build a missing wheel instead of reporting it.
|
|
1243
|
+
*
|
|
1244
|
+
* `true` builds locally, which needs Node and the build pipeline. A URL asks
|
|
1245
|
+
* a build service -- which is how a browser gets a wheel built, having no
|
|
1246
|
+
* compiler of its own. See docs/python/build-on-miss.md.
|
|
1247
|
+
*/
|
|
1248
|
+
buildFromSource?: boolean | string;
|
|
1241
1249
|
}
|
|
1242
1250
|
declare function configurePython(options?: PythonOptions): void;
|
|
1243
1251
|
declare const isPythonAvailable: typeof isCPythonAvailable;
|
|
@@ -1238,6 +1238,14 @@ interface PythonOptions {
|
|
|
1238
1238
|
* no published wheel targets wasm32-emscripten.
|
|
1239
1239
|
*/
|
|
1240
1240
|
wheelIndex?: WheelIndex | string | null;
|
|
1241
|
+
/**
|
|
1242
|
+
* Build a missing wheel instead of reporting it.
|
|
1243
|
+
*
|
|
1244
|
+
* `true` builds locally, which needs Node and the build pipeline. A URL asks
|
|
1245
|
+
* a build service -- which is how a browser gets a wheel built, having no
|
|
1246
|
+
* compiler of its own. See docs/python/build-on-miss.md.
|
|
1247
|
+
*/
|
|
1248
|
+
buildFromSource?: boolean | string;
|
|
1241
1249
|
}
|
|
1242
1250
|
declare function configurePython(options?: PythonOptions): void;
|
|
1243
1251
|
declare const isPythonAvailable: typeof isCPythonAvailable;
|