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.
Files changed (43) hide show
  1. package/README.md +96 -63
  2. package/assets/logo.png +0 -0
  3. package/bin/sandboxedjs-egress.mjs +25 -10
  4. package/dist/index.cjs +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/service-worker.js +3 -2
  7. package/docs/agent/COMMANDS.md +85 -0
  8. package/docs/agent/DECISION-TREE.md +84 -0
  9. package/docs/agent/INVARIANTS.md +40 -0
  10. package/docs/agent/LAUNCH-PROMPT.md +37 -0
  11. package/docs/agent/LOOP.md +84 -0
  12. package/docs/agent/README.md +77 -0
  13. package/docs/agent/ROADMAP.md +37 -0
  14. package/docs/agent/STATE.md +93 -0
  15. package/docs/agent/tasks/00-verify-inherited-work.md +36 -0
  16. package/docs/agent/tasks/01-authoritative-metadata.md +34 -0
  17. package/docs/agent/tasks/02-native-dependencies.md +35 -0
  18. package/docs/agent/tasks/03-reproducible-inputs.md +27 -0
  19. package/docs/agent/tasks/04-build-frontends.md +27 -0
  20. package/docs/agent/tasks/05-registry-integration.md +27 -0
  21. package/docs/agent/tasks/06-package-cohorts.md +42 -0
  22. package/docs/agent/tasks/07-build-on-miss-boundary.md +31 -0
  23. package/docs/browser-runtime-architecture.md +142 -0
  24. package/docs/compatibility-implementation-plan.md +98 -0
  25. package/docs/developer-tool-packs.md +134 -0
  26. package/docs/frontend-automation.md +49 -0
  27. package/docs/fullstack-deployment.md +163 -0
  28. package/docs/handoff.md +275 -0
  29. package/docs/original-x64.md +39 -0
  30. package/docs/platform-hardening.md +49 -0
  31. package/docs/python/abi.md +97 -0
  32. package/docs/python/architecture.md +94 -0
  33. package/docs/python/baseline-inventory.md +54 -0
  34. package/docs/python/build-on-miss.md +198 -0
  35. package/docs/python/compatibility.md +206 -0
  36. package/docs/python/cross-build.md +354 -0
  37. package/docs/python/extensions.md +282 -0
  38. package/docs/python/release-gates.md +46 -0
  39. package/docs/python/virtual-sockets-plan.md +331 -0
  40. package/docs/runtime-lifecycle-fixes.md +39 -0
  41. package/docs/server-previews.md +268 -0
  42. package/docs/virtual-browser.md +120 -0
  43. 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"); // → hi from linux
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** | 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 |
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({ cwd: "/app", network: { allowOutbound: true } });
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" }); // talk to it from your code
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)"); // → abc in /app
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(); // serialisable; restore() later
100
+ const snapshot = box.snapshot(); // serialisable; restore() later
98
101
 
99
102
  import { Terminal } from "sandboxedjs";
100
- const terminal = new Terminal(box.session(), { write: (d) => xterm.write(d), columns: 80, rows: 24 });
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
- Two different problems hide in that sentence, and SandboxedJS solves them in two different ways.
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 *true there* — it means the
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({ files: project, network: { allowOutbound: true } });
141
+ const box = await createContainer({
142
+ files: project,
143
+ network: { allowOutbound: true },
144
+ });
133
145
 
134
- await box.exec("pip install -r requirements.txt", { cwd: "/app/backend", timeoutMs: 600_000 });
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(3000, { timeoutMs: 60_000 });
140
-
141
- const preview = await createPreview(box); // null where service workers are unavailable
142
- iframe.src = preview!.urlFor(3000);
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 *is* the tab; there is
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 *pages*, not a container limit, and the only way through it is a request made
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, so **no project passes a `proxy` option and no
168
- project is configured twice.**
169
-
170
- | Where you are | What to do |
171
- |---|---|
172
- | Deployed (Pages, Vercel, Netlify) | `npx sandboxedjs-egress init --allow …`, then deploy |
173
- | Developing | `npx sandboxedjs-egress` the probe checks its port |
174
- | You already have a server | `app.use(EGRESS_PATH, egressNodeHandler({ allow: [...] }))`, before any body parser |
175
- | Using `npx sandboxedjs-serve` | Nothing; it mounts the proxy itself |
176
- | Node, not a browser | Nothing; there is no CORS to work around |
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 }) => handleEgressRequest(request, { allow: ["ollama.com"] });
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 *page* can reach, never
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: { headers: {
206
- "Cross-Origin-Opener-Policy": "same-origin",
207
- "Cross-Origin-Embedder-Policy": "require-corp",
208
- } },
209
- optimizeDeps: { exclude: ["@rolldown/binding-wasm32-wasi"] }, // see below
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 | Use | Runs guest code on your origin? |
242
- |---|---|---|
243
- | One response as data | `box.request(port, init)` | No — nothing executes |
244
- | One response rendered safely | `renderInto(box, el, { port })` | No — opaque-origin iframe, no `allow-same-origin` |
245
- | A whole site with real URLs | `createPreview(box)` → `urlFor(port)` | **Yes** — trusted code only |
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 *which client is asking*, so
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({ cwd: "/app", network: { allowOutbound: true } });
300
+ const box = await createContainer({
301
+ cwd: "/app",
302
+ network: { allowOutbound: true },
303
+ });
278
304
  await installSandboxSkills(box);
279
- const agent = createDeepAgent({ model, backend: new SandboxedJsBackend(box, { cwd: "/app" }) });
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("ffmpeg -f lavfi -i testsrc=size=640x480:rate=25:duration=5 -pix_fmt yuv420p clip.mp4");
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 *not* enforced for `node`,
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 *inside* the sandbox**, so tools that call it directly fail. On Node it
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 *No* to prompts like `npm create vite`'s "Install and start now?" and run the steps from
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(defineCommand({
361
- name: "greet",
362
- summary: "say hello",
363
- run: (ctx) => (ctx.line(`hello ${ctx.args[0] ?? "world"}`), 0),
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"); // → HELLO THERE
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
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
- const root = argv.find((a) => !a.startsWith("-")) ?? process.cwd();
94
- const at = argv.indexOf("--target");
95
- const allowAt = argv.indexOf("--allow");
96
- const allowed = allowAt === -1
97
- ? null
98
- : (argv[allowAt + 1] ?? "").split(",").map((h) => h.trim()).filter(Boolean);
99
- const name = at === -1 ? await detectTarget(root) : argv[at + 1];
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.10"};
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.10"};
21089
+ version: "0.2.12"};
21090
21090
 
21091
21091
  // src/python/extension-abi.ts
21092
21092
  var EXTENSION_ABI = {
@@ -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 body = this.options.injectSockets === false ? result.body : withSockets(result.body, headers);
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, "/", event.request, event.resultingClientId || event.clientId));
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
+