sandboxedjs 0.2.10 → 0.2.12
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 +96 -63
- package/assets/logo.png +0 -0
- package/bin/sandboxedjs-egress.mjs +25 -10
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/service-worker.js +3 -2
- package/docs/agent/COMMANDS.md +85 -0
- package/docs/agent/DECISION-TREE.md +84 -0
- package/docs/agent/INVARIANTS.md +40 -0
- package/docs/agent/LAUNCH-PROMPT.md +37 -0
- package/docs/agent/LOOP.md +84 -0
- package/docs/agent/README.md +77 -0
- package/docs/agent/ROADMAP.md +37 -0
- package/docs/agent/STATE.md +93 -0
- package/docs/agent/tasks/00-verify-inherited-work.md +36 -0
- package/docs/agent/tasks/01-authoritative-metadata.md +34 -0
- package/docs/agent/tasks/02-native-dependencies.md +35 -0
- package/docs/agent/tasks/03-reproducible-inputs.md +27 -0
- package/docs/agent/tasks/04-build-frontends.md +27 -0
- package/docs/agent/tasks/05-registry-integration.md +27 -0
- package/docs/agent/tasks/06-package-cohorts.md +42 -0
- package/docs/agent/tasks/07-build-on-miss-boundary.md +31 -0
- package/docs/browser-runtime-architecture.md +142 -0
- package/docs/compatibility-implementation-plan.md +98 -0
- package/docs/developer-tool-packs.md +134 -0
- package/docs/frontend-automation.md +49 -0
- package/docs/fullstack-deployment.md +163 -0
- package/docs/handoff.md +275 -0
- package/docs/original-x64.md +39 -0
- package/docs/platform-hardening.md +49 -0
- package/docs/python/abi.md +97 -0
- package/docs/python/architecture.md +94 -0
- package/docs/python/baseline-inventory.md +54 -0
- package/docs/python/build-on-miss.md +198 -0
- package/docs/python/compatibility.md +206 -0
- package/docs/python/cross-build.md +354 -0
- package/docs/python/extensions.md +282 -0
- package/docs/python/release-gates.md +46 -0
- package/docs/python/virtual-sockets-plan.md +331 -0
- package/docs/runtime-lifecycle-fixes.md +39 -0
- package/docs/server-previews.md +268 -0
- package/docs/virtual-browser.md +120 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="/assets/logo.png" alt="SandboxedJS" width="140" />
|
|
3
|
+
<img src="https://cdn.jsdelivr.net/npm/sandboxedjs@latest/assets/logo.png" alt="SandboxedJS" width="140" />
|
|
4
4
|
|
|
5
5
|
# SandboxedJS
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ const box = await createContainer({
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
await box.exec("ls -la /app");
|
|
32
|
-
await box.exec("node /app/hello.js");
|
|
32
|
+
await box.exec("node /app/hello.js"); // → hi from linux
|
|
33
33
|
await box.exec("python3 -c 'print(2**64)'"); // → 18446744073709551616
|
|
34
34
|
|
|
35
35
|
box.dispose();
|
|
@@ -54,16 +54,16 @@ Node 18.17+. Pure JavaScript and WebAssembly — no build step, no native module
|
|
|
54
54
|
|
|
55
55
|
## What's inside
|
|
56
56
|
|
|
57
|
-
|
|
|
58
|
-
|
|
59
|
-
| **Filesystem**
|
|
60
|
-
| **Shell**
|
|
61
|
-
| **Commands**
|
|
62
|
-
| **Node.js**
|
|
63
|
-
| **Python**
|
|
64
|
-
| **WebAssembly** | A WASI preview1 host: any `wasm32-wasi` binary runs as an ordinary process
|
|
65
|
-
| **FFmpeg**
|
|
66
|
-
| **Network**
|
|
57
|
+
| | |
|
|
58
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
59
|
+
| **Filesystem** | A full FHS tree in memory (`/etc`, `/usr`, `/var`, `/home`), permissions, ownership, symlinks, hard links, `umask` |
|
|
60
|
+
| **Shell** | POSIX `sh` — pipes, redirection, here-docs, globbing, functions, loops, job control, traps, `[[ ]]`, `(( ))` |
|
|
61
|
+
| **Commands** | 160 programs: `ls cat grep sed awk find sort tar gzip curl wget diff sha256sum ps top` … |
|
|
62
|
+
| **Node.js** | Its own runtime reporting v22.12.0 — CommonJS _and_ ESM, `http`, `fs`, streams, `child_process`, npm and npx |
|
|
63
|
+
| **Python** | Source-built CPython 3.13 (WASM) on the same filesystem, with `pip` |
|
|
64
|
+
| **WebAssembly** | A WASI preview1 host: any `wasm32-wasi` binary runs as an ordinary process |
|
|
65
|
+
| **FFmpeg** | `ffmpeg` / `ffprobe` 5.1 over container files — [optional install](#video-and-audio) |
|
|
66
|
+
| **Network** | In-container HTTP servers, loopback, live previews, and a proxy for real outbound calls |
|
|
67
67
|
|
|
68
68
|
One filesystem underneath all of it: a file written by `echo` is read by `require('fs')` and by
|
|
69
69
|
Python's `open()`, in any direction.
|
|
@@ -71,13 +71,16 @@ Python's `open()`, in any direction.
|
|
|
71
71
|
## Quick tour
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
|
-
const box = await createContainer({
|
|
74
|
+
const box = await createContainer({
|
|
75
|
+
cwd: "/app",
|
|
76
|
+
network: { allowOutbound: true },
|
|
77
|
+
});
|
|
75
78
|
|
|
76
79
|
await box.exec("npm install express", { cwd: "/app", timeoutMs: 300_000 });
|
|
77
80
|
box.spawn("node server.js", { cwd: "/app" });
|
|
78
81
|
await box.waitForPort(3000);
|
|
79
82
|
|
|
80
|
-
const res = await box.request(3000, { path: "/api" });
|
|
83
|
+
const res = await box.request(3000, { path: "/api" }); // talk to it from your code
|
|
81
84
|
console.log(res.status, res.json());
|
|
82
85
|
```
|
|
83
86
|
|
|
@@ -87,17 +90,21 @@ A stateful shell, when `exec` alone is too forgetful:
|
|
|
87
90
|
const session = box.session();
|
|
88
91
|
await session.run("cd /app");
|
|
89
92
|
await session.run("export TOKEN=abc");
|
|
90
|
-
await session.run("echo $TOKEN in $(pwd)");
|
|
93
|
+
await session.run("echo $TOKEN in $(pwd)"); // → abc in /app
|
|
91
94
|
```
|
|
92
95
|
|
|
93
96
|
Files in and out, snapshots, and a terminal you can wire to xterm.js:
|
|
94
97
|
|
|
95
98
|
```ts
|
|
96
99
|
await box.fs.writeFile("/app/config.json", JSON.stringify(config));
|
|
97
|
-
const snapshot = box.snapshot();
|
|
100
|
+
const snapshot = box.snapshot(); // serialisable; restore() later
|
|
98
101
|
|
|
99
102
|
import { Terminal } from "sandboxedjs";
|
|
100
|
-
const terminal = new Terminal(box.session(), {
|
|
103
|
+
const terminal = new Terminal(box.session(), {
|
|
104
|
+
write: (d) => xterm.write(d),
|
|
105
|
+
columns: 80,
|
|
106
|
+
rows: 24,
|
|
107
|
+
});
|
|
101
108
|
xterm.onData((d) => terminal.input(d));
|
|
102
109
|
terminal.start();
|
|
103
110
|
```
|
|
@@ -110,7 +117,9 @@ terminal.start();
|
|
|
110
117
|
This is the case most people arrive for: a Vite frontend and a Python backend, in one container, in
|
|
111
118
|
a tab — the frontend calling `http://localhost:8000`, the backend calling a real API.
|
|
112
119
|
|
|
113
|
-
|
|
120
|
+
The frontend-to-backend hop and the backend-to-internet hop have different requirements.
|
|
121
|
+
See the [full-stack deployment guide](docs/fullstack-deployment.md) for production asset staging,
|
|
122
|
+
Cloudflare Pages, Vercel, Node/serverless lifetimes, and troubleshooting.
|
|
114
123
|
|
|
115
124
|
### 1. Frontend → backend: nothing to configure
|
|
116
125
|
|
|
@@ -118,7 +127,7 @@ Two different problems hide in that sentence, and SandboxedJS solves them in two
|
|
|
118
127
|
frontend (:3000) ──▶ http://localhost:8000/agent/run ──▶ FastAPI (:8000)
|
|
119
128
|
```
|
|
120
129
|
|
|
121
|
-
Both servers are inside the container, so `localhost:8000` is
|
|
130
|
+
Both servers are inside the container, so `localhost:8000` is _true there_ — it means the
|
|
122
131
|
container's own backend, never your machine's. Leave the URL exactly as the project writes it. In a
|
|
123
132
|
live preview, an injected script rewrites loopback addresses to the preview's origin and the
|
|
124
133
|
service worker routes them back in by port, so the same code works in the frame without a proxy
|
|
@@ -129,21 +138,31 @@ import { createContainer, createPreview } from "sandboxedjs";
|
|
|
129
138
|
|
|
130
139
|
// `allowOutbound` is still what lets the project install and call out at all;
|
|
131
140
|
// the proxy is how those calls leave a browser, not permission to make them.
|
|
132
|
-
const box = await createContainer({
|
|
141
|
+
const box = await createContainer({
|
|
142
|
+
files: project,
|
|
143
|
+
network: { allowOutbound: true },
|
|
144
|
+
});
|
|
133
145
|
|
|
134
|
-
await box.exec("pip install -r requirements.txt", {
|
|
146
|
+
await box.exec("pip install -r requirements.txt", {
|
|
147
|
+
cwd: "/app/backend",
|
|
148
|
+
timeoutMs: 600_000,
|
|
149
|
+
});
|
|
135
150
|
await box.exec("npm install", { cwd: "/app/frontend", timeoutMs: 600_000 });
|
|
136
151
|
|
|
137
152
|
box.spawn("fastapi run", { cwd: "/app/backend" });
|
|
138
153
|
box.spawn("npm run dev", { cwd: "/app/frontend" });
|
|
139
|
-
await box.waitForPort(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
if (!(await box.waitForPort(8000, { timeoutMs: 60_000 })) ||
|
|
155
|
+
!(await box.waitForPort(3000, { timeoutMs: 60_000 }))) {
|
|
156
|
+
throw new Error("Check backend and frontend startup logs");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const preview = await createPreview(box);
|
|
160
|
+
if (!preview) throw new Error("Preview requires a working service worker");
|
|
161
|
+
iframe.src = preview.urlFor(3000);
|
|
143
162
|
```
|
|
144
163
|
|
|
145
164
|
It works between preview pages and from another tab of the same browser while the owner page is
|
|
146
|
-
open. It does not work from Postman, curl or another machine — the container
|
|
165
|
+
open. It does not work from Postman, curl or another machine — the container _is_ the tab; there is
|
|
147
166
|
no server anywhere to reach.
|
|
148
167
|
|
|
149
168
|
### 2. Backend → the internet: one file, once
|
|
@@ -155,25 +174,26 @@ FastAPI ──▶ egress proxy on your own origin ──▶ https://ollama.com
|
|
|
155
174
|
|
|
156
175
|
A page may only read a response from a host that sends CORS headers, and most APIs send none — an
|
|
157
176
|
`Authorization` header alone forces a preflight plenty of them answer with 405. That is the
|
|
158
|
-
browser's rule about
|
|
177
|
+
browser's rule about _pages_, not a container limit, and the only way through it is a request made
|
|
159
178
|
somewhere a page is not. So add one to the project you already deploy:
|
|
160
179
|
|
|
161
180
|
```bash
|
|
162
|
-
npx sandboxedjs-egress init --allow ollama.com,api.openai.com
|
|
181
|
+
npx sandboxedjs-egress init --target cloudflare --allow ollama.com,api.openai.com
|
|
163
182
|
```
|
|
164
183
|
|
|
165
184
|
That writes a single function file at the path your host serves — Cloudflare Pages, Vercel and
|
|
166
185
|
Netlify are detected, `--target` names one. Nothing else changes: a container in a browser probes
|
|
167
|
-
its own origin for that proxy before giving up
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
186
|
+
its own origin for that proxy before giving up. The guest project needs no proxy setting;
|
|
187
|
+
the host must deploy the generated function. For a separate relay, set `network.proxy`.
|
|
188
|
+
A host serving only static files cannot relay APIs that reject browser requests.
|
|
189
|
+
|
|
190
|
+
| Where you are | What to do |
|
|
191
|
+
| --------------------------------- | ----------------------------------------------------------------------------------- |
|
|
192
|
+
| Deployed (Pages, Vercel, Netlify) | `npx sandboxedjs-egress init --allow …`, then deploy |
|
|
193
|
+
| Developing | `npx sandboxedjs-egress` — the probe checks its port |
|
|
194
|
+
| You already have a server | `app.use(EGRESS_PATH, egressNodeHandler({ allow: [...] }))`, before any body parser |
|
|
195
|
+
| Using `npx sandboxedjs-serve` | Nothing; it mounts the proxy itself |
|
|
196
|
+
| Node, not a browser | Nothing; there is no CORS to work around |
|
|
177
197
|
|
|
178
198
|
```ts
|
|
179
199
|
// Your own Express/Node server:
|
|
@@ -182,12 +202,13 @@ app.use(EGRESS_PATH, egressNodeHandler({ allow: ["ollama.com"] }));
|
|
|
182
202
|
|
|
183
203
|
// A static host — this is what `init` writes for you:
|
|
184
204
|
import { handleEgressRequest } from "sandboxedjs/egress";
|
|
185
|
-
export const onRequest = ({ request }) =>
|
|
205
|
+
export const onRequest = ({ request }) =>
|
|
206
|
+
handleEgressRequest(request, { allow: ["ollama.com"] });
|
|
186
207
|
```
|
|
187
208
|
|
|
188
209
|
Every exit honours it — `curl`, `wget`, a guest's `fetch`, Python's sockets and `httpx` — so it
|
|
189
210
|
means the same thing whatever the project is written in. Loopback still stays inside the container,
|
|
190
|
-
and the container's own policy still applies first: the proxy widens what a
|
|
211
|
+
and the container's own policy still applies first: the proxy widens what a _page_ can reach, never
|
|
191
212
|
what the container is allowed to.
|
|
192
213
|
|
|
193
214
|
> ⚠️ Anything that can reach the proxy can make requests through it carrying whatever credentials
|
|
@@ -202,11 +223,13 @@ no meta tag or polyfill can add them later:
|
|
|
202
223
|
```ts
|
|
203
224
|
// vite.config.ts
|
|
204
225
|
export default {
|
|
205
|
-
server: {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
226
|
+
server: {
|
|
227
|
+
headers: {
|
|
228
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
229
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
optimizeDeps: { exclude: ["@rolldown/binding-wasm32-wasi"] }, // see below
|
|
210
233
|
};
|
|
211
234
|
```
|
|
212
235
|
|
|
@@ -238,13 +261,13 @@ filesystem and a real socket), and the CLI. They fail only if you call them.
|
|
|
238
261
|
|
|
239
262
|
### Showing what runs
|
|
240
263
|
|
|
241
|
-
| Need
|
|
242
|
-
|
|
243
|
-
| One response as data
|
|
244
|
-
| One response rendered safely | `renderInto(box, el, { port })`
|
|
245
|
-
| A whole site with real URLs
|
|
264
|
+
| Need | Use | Runs guest code on your origin? |
|
|
265
|
+
| ---------------------------- | ------------------------------------- | ------------------------------------------------- |
|
|
266
|
+
| One response as data | `box.request(port, init)` | No — nothing executes |
|
|
267
|
+
| One response rendered safely | `renderInto(box, el, { port })` | No — opaque-origin iframe, no `allow-same-origin` |
|
|
268
|
+
| A whole site with real URLs | `createPreview(box)` → `urlFor(port)` | **Yes** — trusted code only |
|
|
246
269
|
|
|
247
|
-
`createPreview` is what makes a dev server work: requests route by
|
|
270
|
+
`createPreview` is what makes a dev server work: requests route by _which client is asking_, so
|
|
248
271
|
`/src/main.js` and `/@vite/client` resolve without rewriting anything, and a tunnelled `WebSocket`
|
|
249
272
|
carries HMR. Serve it from a separate origin for code you did not write. On Node, `box.expose(port)`
|
|
250
273
|
gives a real loopback URL instead. Full lifecycle notes: [Server previews](docs/server-previews.md).
|
|
@@ -274,9 +297,15 @@ not a runtime dependency, so install it alongside your model provider.
|
|
|
274
297
|
```ts
|
|
275
298
|
import { SandboxedJsBackend, installSandboxSkills } from "sandboxedjs/agent";
|
|
276
299
|
|
|
277
|
-
const box = await createContainer({
|
|
300
|
+
const box = await createContainer({
|
|
301
|
+
cwd: "/app",
|
|
302
|
+
network: { allowOutbound: true },
|
|
303
|
+
});
|
|
278
304
|
await installSandboxSkills(box);
|
|
279
|
-
const agent = createDeepAgent({
|
|
305
|
+
const agent = createDeepAgent({
|
|
306
|
+
model,
|
|
307
|
+
backend: new SandboxedJsBackend(box, { cwd: "/app" }),
|
|
308
|
+
});
|
|
280
309
|
```
|
|
281
310
|
|
|
282
311
|
## Video and audio
|
|
@@ -287,7 +316,9 @@ filesystem — inputs and outputs are ordinary container files, so pipelines wor
|
|
|
287
316
|
report themselves missing, as a real system does.
|
|
288
317
|
|
|
289
318
|
```js
|
|
290
|
-
await box.exec(
|
|
319
|
+
await box.exec(
|
|
320
|
+
"ffmpeg -f lavfi -i testsrc=size=640x480:rate=25:duration=5 -pix_fmt yuv420p clip.mp4",
|
|
321
|
+
);
|
|
291
322
|
await box.exec("ffmpeg -i clip.mp4 -vf scale=320:-2 -frames:v 1 thumb.png");
|
|
292
323
|
```
|
|
293
324
|
|
|
@@ -305,7 +336,7 @@ What it is **not**:
|
|
|
305
336
|
- **Not a VM.** Everything runs in your JavaScript engine. A true escape is an engine escape. This
|
|
306
337
|
is isolation from mistakes and ordinary untrusted programs, not from a determined attacker.
|
|
307
338
|
- **The user model does not constrain Node.** `user: "agent"` is enforced for the shell, the
|
|
308
|
-
commands and Python — `cat /root/secret` is denied for real. It is
|
|
339
|
+
commands and Python — `cat /root/secret` is denied for real. It is _not_ enforced for `node`,
|
|
309
340
|
which reaches the volume directly. Model ordinary multi-user behaviour with it; do not treat it
|
|
310
341
|
as a privilege boundary for JavaScript you do not trust.
|
|
311
342
|
- **A preview shares your origin.** `createPreview` serves guest code from the page that registered
|
|
@@ -323,7 +354,7 @@ An honest list:
|
|
|
323
354
|
|
|
324
355
|
- **No compiled native addons.** A `.node` file cannot load; a package needs a JS or WASM fallback.
|
|
325
356
|
`rollup` and `esbuild` get redirected to `@rollup/wasm-node` and `esbuild-wasm` automatically.
|
|
326
|
-
- **esbuild cannot run
|
|
357
|
+
- **esbuild cannot run _inside_ the sandbox**, so tools that call it directly fail. On Node it
|
|
327
358
|
borrows the host's `esbuild-wasm`.
|
|
328
359
|
- **Vite 8 / Rolldown is browser-only.** The Node binding preopens the real filesystem root and
|
|
329
360
|
cannot be handed another, so it looks for your project on the actual disk. Vite 7 works on both.
|
|
@@ -333,7 +364,7 @@ An honest list:
|
|
|
333
364
|
- **Processes are cooperative.** `kill -9` cannot interrupt a tight synchronous loop; `SIGSTOP`
|
|
334
365
|
marks state. `chroot` runs the command in the target rather than isolating it.
|
|
335
366
|
- **`execSync` needs the worker runtime.** Where it is unavailable, it throws naming the command —
|
|
336
|
-
answer
|
|
367
|
+
answer _No_ to prompts like `npm create vite`'s "Install and start now?" and run the steps from
|
|
337
368
|
the shell.
|
|
338
369
|
- **`node:test`** covers what test files use (`test`/`describe`/hooks/`mock.fn`, spec and tap
|
|
339
370
|
reports); `run()`, coverage and mock timers are not implemented.
|
|
@@ -357,13 +388,15 @@ dispatch all find it:
|
|
|
357
388
|
```ts
|
|
358
389
|
import { defineCommand } from "sandboxedjs";
|
|
359
390
|
|
|
360
|
-
box.kernel.installCommand(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}))
|
|
391
|
+
box.kernel.installCommand(
|
|
392
|
+
defineCommand({
|
|
393
|
+
name: "greet",
|
|
394
|
+
summary: "say hello",
|
|
395
|
+
run: (ctx) => (ctx.line(`hello ${ctx.args[0] ?? "world"}`), 0),
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
365
398
|
|
|
366
|
-
await box.exec("greet there | tr a-z A-Z");
|
|
399
|
+
await box.exec("greet there | tr a-z A-Z"); // → HELLO THERE
|
|
367
400
|
```
|
|
368
401
|
|
|
369
402
|
`Kernel`, `Vfs`, `Shell`, `Terminal` and `NetworkStack` are exported too, if you want to embed a
|
package/assets/logo.png
ADDED
|
Binary file
|
|
@@ -45,7 +45,7 @@ const TARGETS = {
|
|
|
45
45
|
${allow}
|
|
46
46
|
/* Cloudflare Pages serves this file at /__sandboxedjs__/egress, which is where
|
|
47
47
|
* a container in this site's pages looks for its way out. */
|
|
48
|
-
export const onRequest = ({ request }) => handleEgressRequest(request, options);
|
|
48
|
+
export const onRequest = ({ request }: { request: Request }) => handleEgressRequest(request, options);
|
|
49
49
|
`,
|
|
50
50
|
},
|
|
51
51
|
vercel: {
|
|
@@ -57,7 +57,7 @@ export const config = { runtime: "edge" };
|
|
|
57
57
|
|
|
58
58
|
/* Vercel serves this file at /api/__sandboxedjs__/egress, which is one of the
|
|
59
59
|
* paths a container in this site's pages probes for its way out. */
|
|
60
|
-
export default (request) => handleEgressRequest(request, options);
|
|
60
|
+
export default (request: Request) => handleEgressRequest(request, options);
|
|
61
61
|
`,
|
|
62
62
|
},
|
|
63
63
|
netlify: {
|
|
@@ -67,7 +67,7 @@ export default (request) => handleEgressRequest(request, options);
|
|
|
67
67
|
${allow}
|
|
68
68
|
export const config = { path: "/.netlify/functions/sandboxedjs-egress" };
|
|
69
69
|
|
|
70
|
-
export default (request) => handleEgressRequest(request, options);
|
|
70
|
+
export default (request: Request) => handleEgressRequest(request, options);
|
|
71
71
|
`,
|
|
72
72
|
},
|
|
73
73
|
};
|
|
@@ -90,13 +90,28 @@ async function detectTarget(root) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async function init(argv) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
let directory;
|
|
94
|
+
let targetName;
|
|
95
|
+
let allowed = null;
|
|
96
|
+
for (let index = 0; index < argv.length; index++) {
|
|
97
|
+
const argument = argv[index];
|
|
98
|
+
if (argument === "--target" || argument === "--allow") {
|
|
99
|
+
const value = argv[++index];
|
|
100
|
+
if (!value || value.startsWith("--")) {
|
|
101
|
+
console.error(`sandboxedjs-egress init: ${argument} requires a value`);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (argument === "--target") targetName = value;
|
|
106
|
+
else allowed = value.split(",").map((host) => host.trim()).filter(Boolean);
|
|
107
|
+
} else if (argument.startsWith("-") || directory !== undefined) {
|
|
108
|
+
console.error(`sandboxedjs-egress init: unexpected argument ${argument}`);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
} else directory = argument;
|
|
112
|
+
}
|
|
113
|
+
const root = directory ?? process.cwd();
|
|
114
|
+
const name = targetName ?? await detectTarget(root);
|
|
100
115
|
if (!name || !TARGETS[name]) {
|
|
101
116
|
console.error(
|
|
102
117
|
name
|
package/dist/index.cjs
CHANGED
|
@@ -21103,7 +21103,7 @@ var wheels_default = {
|
|
|
21103
21103
|
|
|
21104
21104
|
// package.json
|
|
21105
21105
|
var package_default = {
|
|
21106
|
-
version: "0.2.
|
|
21106
|
+
version: "0.2.12"};
|
|
21107
21107
|
|
|
21108
21108
|
// src/python/extension-abi.ts
|
|
21109
21109
|
var EXTENSION_ABI = {
|
package/dist/index.js
CHANGED
|
@@ -21086,7 +21086,7 @@ var wheels_default = {
|
|
|
21086
21086
|
|
|
21087
21087
|
// package.json
|
|
21088
21088
|
var package_default = {
|
|
21089
|
-
version: "0.2.
|
|
21089
|
+
version: "0.2.12"};
|
|
21090
21090
|
|
|
21091
21091
|
// src/python/extension-abi.ts
|
|
21092
21092
|
var EXTENSION_ABI = {
|
package/dist/service-worker.js
CHANGED
|
@@ -530,7 +530,8 @@ var PreviewRouter = class {
|
|
|
530
530
|
this.options.onStale?.(clientId);
|
|
531
531
|
}
|
|
532
532
|
const headers = new Headers(result.headers);
|
|
533
|
-
const
|
|
533
|
+
const bodyless = request.method === "HEAD" || [204, 205, 304].includes(result.status);
|
|
534
|
+
const body = bodyless ? null : this.options.injectSockets === false ? result.body : withSockets(result.body, headers);
|
|
534
535
|
headers.set("Cross-Origin-Resource-Policy", "cross-origin");
|
|
535
536
|
headers.set("Cross-Origin-Embedder-Policy", "require-corp");
|
|
536
537
|
return new Response(body, { status: result.status, statusText: result.statusText, headers });
|
|
@@ -656,7 +657,7 @@ worker.addEventListener("fetch", (event) => {
|
|
|
656
657
|
const claimed = router.claimedPort(url.pathname);
|
|
657
658
|
if (claimed !== null && event.request.mode === "navigate") {
|
|
658
659
|
router.bind(event.resultingClientId || event.clientId, claimed);
|
|
659
|
-
event.respondWith(router.fetch(claimed,
|
|
660
|
+
event.respondWith(router.fetch(claimed, router.containerPath(url.pathname, url.search), event.request, event.resultingClientId || event.clientId));
|
|
660
661
|
return;
|
|
661
662
|
}
|
|
662
663
|
const port = router.portForRequest(url.pathname, event.clientId);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Command catalog
|
|
2
|
+
|
|
3
|
+
Run commands from `/Users/shazi/Projects/SandboxedJs` unless stated otherwise.
|
|
4
|
+
Use focused commands first. Keep long logs out of model context:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
some-command > /tmp/sbx-native-build.log 2>&1
|
|
8
|
+
tail -n 120 /tmp/sbx-native-build.log
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Do not use the redirection form when editing source files; it is for disposable
|
|
12
|
+
logs only.
|
|
13
|
+
|
|
14
|
+
## Cheap inspection
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
git status --short
|
|
18
|
+
git diff --cached --check
|
|
19
|
+
git diff --cached --stat
|
|
20
|
+
python3 -m json.tool python-runtime/abi/extension-abi.json >/dev/null
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Build prerequisites and runtime
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
make -C python-runtime doctor
|
|
27
|
+
make -C python-runtime python PROFILE=dynamic
|
|
28
|
+
make -C python-runtime package PROFILE=dynamic
|
|
29
|
+
npm run build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Do not rebuild CPython when the runtime and ABI inputs have not changed and the
|
|
33
|
+
required output already exists.
|
|
34
|
+
|
|
35
|
+
## Extension fixtures and index
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
python3 python-runtime/scripts/build_extension.py \
|
|
39
|
+
python-runtime/fixtures/sbx_setuptools_probe/recipe.json
|
|
40
|
+
python3 python-runtime/scripts/build_extension.py \
|
|
41
|
+
python-runtime/fixtures/sbx_cython_probe/recipe.json
|
|
42
|
+
python3 python-runtime/scripts/build_extension.py \
|
|
43
|
+
python-runtime/fixtures/sbx_rust_probe/recipe.json
|
|
44
|
+
python3 python-runtime/scripts/build_index.py
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If a wheel changes, rebuild `index.json` before runtime tests or its digest will
|
|
48
|
+
correctly be stale.
|
|
49
|
+
|
|
50
|
+
## Focused tests
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npx vitest run test/python-runtime/extensions.test.ts
|
|
54
|
+
npx vitest run test/python-runtime/resolver.test.ts
|
|
55
|
+
npx vitest run test/python-runtime/extension-abi.test.ts
|
|
56
|
+
npm run typecheck
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Run `npm run check` only at milestone gates. Network-enabled suites are not a
|
|
60
|
+
default native-package check.
|
|
61
|
+
|
|
62
|
+
## Wheel inspection
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
python3 -m zipfile -l path/to/package.whl
|
|
66
|
+
python3 python-runtime/scripts/build_index.py --wheels path/to/wheel-directory
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Prefer adding a small reusable inspection script over repeatedly embedding
|
|
70
|
+
complex Python in shell commands.
|
|
71
|
+
|
|
72
|
+
## Logs
|
|
73
|
+
|
|
74
|
+
Use one log per active failure under `/tmp`, named with the task and package.
|
|
75
|
+
Keep only:
|
|
76
|
+
|
|
77
|
+
- the exact command;
|
|
78
|
+
- toolchain versions when relevant;
|
|
79
|
+
- first causal error;
|
|
80
|
+
- final 80-150 lines;
|
|
81
|
+
- exit code.
|
|
82
|
+
|
|
83
|
+
Warnings are not tasks unless they identify incorrect output or a future
|
|
84
|
+
failure covered by the current milestone.
|
|
85
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Failure classification and decisions
|
|
2
|
+
|
|
3
|
+
Classify an observed failure by its earliest decisive symptom.
|
|
4
|
+
|
|
5
|
+
## Resolver says no usable distribution
|
|
6
|
+
|
|
7
|
+
1. Inspect candidate filenames and tags.
|
|
8
|
+
2. If a pure wheel exists, fix resolver/tag/metadata behavior; do not compile.
|
|
9
|
+
3. If a matching SandboxedJS wheel exists, inspect index `abiId`, digest, and
|
|
10
|
+
dependency metadata.
|
|
11
|
+
4. If only an sdist or host-native wheels exist, route to the external
|
|
12
|
+
cross-builder. Do not let the runtime installer build source yet.
|
|
13
|
+
|
|
14
|
+
## Build frontend cannot start
|
|
15
|
+
|
|
16
|
+
- Missing host Python module: classify as a build requirement.
|
|
17
|
+
- Unpinned requirement: add an exact version and hash acquisition path.
|
|
18
|
+
- Unsupported backend: add a backend adapter, not a package conditional.
|
|
19
|
+
- Network failure: preserve the command and request network approval; do not
|
|
20
|
+
substitute an unverified archive.
|
|
21
|
+
|
|
22
|
+
## Compiler uses host headers or compiler
|
|
23
|
+
|
|
24
|
+
- Inspect generated cross-sysconfig and command line.
|
|
25
|
+
- Fix host/target environment separation.
|
|
26
|
+
- Never solve this by renaming a produced artifact.
|
|
27
|
+
|
|
28
|
+
## Header or library not found
|
|
29
|
+
|
|
30
|
+
1. Determine whether it is a Python dependency or a native target library.
|
|
31
|
+
2. For a target library, require a declared, pinned native dependency.
|
|
32
|
+
3. Build it into the ABI/profile-specific sysroot.
|
|
33
|
+
4. Expose sysroot include/library paths through the cross environment.
|
|
34
|
+
5. Add a reduced fixture proving the dependency mechanism before retrying a
|
|
35
|
+
large real package.
|
|
36
|
+
|
|
37
|
+
## Configure test compiles, then tries to execute
|
|
38
|
+
|
|
39
|
+
This is a cross-compilation probe problem. Prefer, in order:
|
|
40
|
+
|
|
41
|
+
1. upstream-supported cross-file/cache variables;
|
|
42
|
+
2. a generic build-backend cross configuration;
|
|
43
|
+
3. a visible package patch providing the target fact.
|
|
44
|
+
|
|
45
|
+
Never execute target WebAssembly as if it were a host binary. If executing a
|
|
46
|
+
probe under a controlled runner is proposed, stop for architectural review.
|
|
47
|
+
|
|
48
|
+
## Linker failure
|
|
49
|
+
|
|
50
|
+
- Missing `Py*` symbols can remain unresolved in a side module if supplied by
|
|
51
|
+
the main module; confirm rather than blindly adding `-lpython`.
|
|
52
|
+
- Missing native-library symbols require target dependency/link-order work.
|
|
53
|
+
- Shared-memory, PIC, exception, relocation, or atomics errors are ABI issues;
|
|
54
|
+
inspect `INVARIANTS.md` and escalate before changing flags.
|
|
55
|
+
- Duplicate symbols often indicate a static library linked twice; inspect the
|
|
56
|
+
complete link line.
|
|
57
|
+
|
|
58
|
+
## Wheel builds but validation fails
|
|
59
|
+
|
|
60
|
+
- Not `\0asm`: host compiler leakage.
|
|
61
|
+
- No `dylink`: side-module link mode was lost.
|
|
62
|
+
- Missing `PyInit_*`: module naming/export problem.
|
|
63
|
+
- Wrong tag or ABI: wheel construction problem. Rebuild; never retag.
|
|
64
|
+
|
|
65
|
+
## Install succeeds but import fails
|
|
66
|
+
|
|
67
|
+
- Missing Python wrapper files: wheel assembly/layout defect.
|
|
68
|
+
- Missing dependent shared/static symbols: target dependency closure defect.
|
|
69
|
+
- Init symbol mismatch: derive the module name from installed path.
|
|
70
|
+
- Trap, memory corruption, or exception failure: ABI mismatch until disproven.
|
|
71
|
+
|
|
72
|
+
## Import succeeds but behavior fails
|
|
73
|
+
|
|
74
|
+
Classify whether the package needs an unavailable OS capability, contains a
|
|
75
|
+
32-bit assumption, exceeds memory, or has a package defect. Record an honest
|
|
76
|
+
compatibility limitation when the platform capability is genuinely absent.
|
|
77
|
+
|
|
78
|
+
## Test hangs
|
|
79
|
+
|
|
80
|
+
Do not immediately increase timeouts. Determine whether the process is waiting
|
|
81
|
+
on input, network, a child process, a pthread, or a host ABI response. Capture a
|
|
82
|
+
bounded stack/process snapshot where possible. Kill only the exact process you
|
|
83
|
+
started.
|
|
84
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Non-negotiable invariants
|
|
2
|
+
|
|
3
|
+
Read this before changing ABI, wheel, resolver, or compiler behavior.
|
|
4
|
+
|
|
5
|
+
1. `python-runtime/abi/extension-abi.json` is the source of truth for target
|
|
6
|
+
identity and compile/link compatibility. Recipes cannot override ABI flags.
|
|
7
|
+
2. A host-native ELF, Mach-O, `.node`, or other binary is never accepted or
|
|
8
|
+
relabeled as WebAssembly.
|
|
9
|
+
3. A native wheel must match both its Python wheel tag and SandboxedJS
|
|
10
|
+
`abiId`.
|
|
11
|
+
4. Build tools run on the build machine. Target code and target libraries are
|
|
12
|
+
compiled for wasm32. Never put target artifacts on the host interpreter's
|
|
13
|
+
import path.
|
|
14
|
+
5. Build, host, Python-runtime, and native-target dependencies are separate
|
|
15
|
+
concepts and must be represented separately.
|
|
16
|
+
6. Sources and build-tool artifacts are version-pinned and hash-verifiable.
|
|
17
|
+
7. Generic behavior lives in generic machinery. Package-specific behavior is
|
|
18
|
+
visible in a recipe and/or patch, never hidden behind `if package == ...`.
|
|
19
|
+
8. The package's own metadata is authoritative. Recipes may constrain or add
|
|
20
|
+
target facts, but must not silently duplicate drifting `Requires-Dist` data.
|
|
21
|
+
9. Installer operations remain transactional. Failed resolution, download, or
|
|
22
|
+
installation must not leave a partial environment.
|
|
23
|
+
10. A build is supported only after the wheel installs and its compiled module
|
|
24
|
+
executes under the owned SandboxedJS CPython runtime.
|
|
25
|
+
11. Browser compatibility remains mandatory. Runtime TypeScript must not gain
|
|
26
|
+
unconditional Node builtin imports.
|
|
27
|
+
12. Unsupported operating-system capabilities fail honestly. Package success
|
|
28
|
+
must not be manufactured by fake sockets, fake processes, or silent skips.
|
|
29
|
+
|
|
30
|
+
## Forbidden shortcuts
|
|
31
|
+
|
|
32
|
+
- Weakening `verify_side_module` or ABI checks to make a fixture pass.
|
|
33
|
+
- Adding arbitrary recipe compiler/link flags.
|
|
34
|
+
- Copying metadata solely from a handwritten recipe when upstream metadata can
|
|
35
|
+
be generated.
|
|
36
|
+
- Installing unpinned build requirements from the network in a release build.
|
|
37
|
+
- Treating "wheel file exists" as an acceptance result.
|
|
38
|
+
- Starting with NumPy-specific branches in the generic pipeline.
|
|
39
|
+
- Running full tests repeatedly when a focused test identifies the defect.
|
|
40
|
+
|