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
@@ -0,0 +1,46 @@
1
+ # Release gates
2
+
3
+ A milestone is done when its gate passes, not when its code exists. Numbers here
4
+ are proposed targets, not measured results.
5
+
6
+ | Milestone | Deliverable | Gate | State |
7
+ |---|---|---|---|
8
+ | M0 | behaviour inventory | current failures reproducible and written down, integration failures told apart from interpreter failures | **done** — [baseline-inventory.md](baseline-inventory.md) |
9
+ | M1 | host ABI + C probe | interactive read, exact binary I/O, cancellation, no deadlock | **done** — `test/python-abi/` |
10
+ | M2 | source-built interpreter | scripts, CLI flags, two isolated programs concurrently | **done** — `test/python-runtime/`, and `pythoncheck.html` in the harness |
11
+ | M3 | files and processes | cross-runtime pipelines, correct EOF, cleanup, file consistency | descriptor layer and Emscripten FS bridge done; spawn, pipes into Python, and symlinks not started |
12
+ | M4 | environments | two independent venvs, offline and online wheel installs | **partial** — online wheels install transactionally into the owned interpreter and expose `console_scripts`; the package includes its ABI wheel index, but venvs, offline indexes, source builds, and per-environment site-packages remain |
13
+ | M5 | native packages | Pydantic core and one small extension survive repeated import/exit | **done for the current ABI** — C and PyO3 side-module wheels load through ordinary CPython imports; `pydantic==2.9.0` selects and runs with `pydantic-core==2.23.2` |
14
+ | M6 | local networking | echo server, concurrent connections, timeouts, port reuse | **partial** — the host-owned virtual TCP stack (`src/net/virtual-socket.ts`), its socket ABI family (`1792`), and router integration are done and cover `box.request()` to a non-JS listener plus cross-stack `EADDRINUSE` (`test/virtual-socket.test.ts`, `test/python-socket-abi.test.ts`, `test/universal-server.test.ts`); the CPython `_sbx_socket` adapter and `socket`/`selectors` patches are written but not yet built into the interpreter, so no Python process has bound a port through it |
15
+ | M7 | web serving | FastAPI validation, streaming, WebSocket, shutdown | **partial** — a real FastAPI/Pydantic 2 application validates and answers through ASGI; Uvicorn cannot yet serve through `box.request()` |
16
+ | M8 | threads | sync endpoints, AnyIO operations, thread cleanup | **partial** — pthread CPython, `asyncio.to_thread()`, and a synchronous FastAPI endpoint pass; churn/cleanup soak is outstanding |
17
+ | M9 | persistence | recovery after an interrupted write or install | not started |
18
+ | M10 | ML | correct inference, bounded memory, cancellation | not started |
19
+ | M11 | source builds | compile, install and import a representative C extension in-browser | not started |
20
+ | M12 | release | browser matrix, soak tests, rollback exercise | not started |
21
+
22
+ M6 still gates any broad FastAPI hosting claim. Importing the framework and
23
+ calling its public ASGI interface is useful evidence, but a publishable server
24
+ runtime must also route a real Uvicorn listener through `box.request()`. M5
25
+ still gates current Pydantic 2 releases because their Rust extension is native.
26
+
27
+ ## Standing gates
28
+
29
+ Checked at every release, not once:
30
+
31
+ | Area | Gate |
32
+ |---|---|
33
+ | Process isolation | no module, global or environment leakage across independent processes |
34
+ | Process churn | 10,000 short launches with no growth in live resource counts |
35
+ | I/O | binary correctness, partial reads and writes, EOF, backpressure, Unicode terminal boundaries |
36
+ | Cancellation | kill succeeds during startup, blocked input, network wait, import and inference |
37
+ | Filesystem | rename, unlink and open semantics match the declared contract under concurrent access |
38
+ | Packaging | an interrupted install never reports a complete environment |
39
+ | Threads | repeated creation, blocking, imports and teardown do not deadlock |
40
+ | Storage | reload after an interrupted commit recovers a valid state |
41
+ | Node regression | every supported Node behaviour still passes |
42
+
43
+ Fault injection belongs in the same suite: worker crash mid-syscall, truncated
44
+ runtime assets, missing extension symbols, quota exhaustion, lost connections,
45
+ GPU device loss, cancellation during extraction, stale responses after PID
46
+ reuse, and teardown while children are blocked.
@@ -0,0 +1,331 @@
1
+ # Virtual sockets for Python servers
2
+
3
+ This is the implementation plan for making ordinary Python servers such as Uvicorn, FastAPI CLI, asyncio servers, and raw TCP servers reachable through the SandboxedJs container network.
4
+
5
+ ## Status (2026-09-08)
6
+
7
+ - **Phase 0 — contract**: done. Socket ops occupy family `1792` in
8
+ [host-v1.json](../../python-runtime/abi/host-v1.json) /
9
+ [sbx_host.h](../../python-runtime/abi/sbx_host.h) /
10
+ [host-abi.ts](../../src/runtime/python/host-abi.ts); `AF_INET` + `SOCK_STREAM`
11
+ only; golden-frame coverage in [python-socket-abi.test.ts](../../test/python-socket-abi.test.ts).
12
+ - **Phase 1 — host virtual stream model**: done.
13
+ [src/net/virtual-socket.ts](../../src/net/virtual-socket.ts) holds the listener
14
+ backlog, duplex byte queues, EOF/half-close/reset transitions, and a shared
15
+ `VirtualTcpNetwork` port authority. Tests: [virtual-socket.test.ts](../../test/virtual-socket.test.ts).
16
+ - **Phase 2 — ABI dispatch and descriptors**: done. `SocketDescription`
17
+ implements `OpenFileDescription`, so `close`, `set_flags`, and `poll` reuse the
18
+ generic descriptor handlers; socket ops dispatch in
19
+ [syscall-server.ts](../../src/runtime/python/syscall-server.ts) with blocking
20
+ `accept`/`recv`/`send` that unpark on `whenReady()` and abort on process kill.
21
+ - **Phase 3 — CPython socket adapter**: written, not yet built.
22
+ [native/sbx/sbx_socket.c](../../python-runtime/native/sbx/sbx_socket.c), the
23
+ `socket.py` / `selectors.py` patches
24
+ ([0003](../../python-runtime/patches/cpython/0003-sandboxedjs-host-sockets.patch),
25
+ [0004](../../python-runtime/patches/cpython/0004-sandboxedjs-selector-poll.patch)),
26
+ and the `build_python.py` wiring exist, but the interpreter has not been
27
+ recompiled with `_sbx_socket`, so no Python process has bound a port.
28
+ - **Phase 5 — router/client integration**: done for the non-JS path.
29
+ `LocalRuntimePod.request()` falls through to `requestTcp()` for ports held by
30
+ the socket stack, `proxy.activePorts()` merges both tables, and
31
+ `VirtualHttpRouter.register()` rejects a port already held by a socket
32
+ listener. Tests: [universal-server.test.ts](../../test/universal-server.test.ts).
33
+ - **Phases 4 and 6 — asyncio/Uvicorn, hardening**: not started; blocked on
34
+ Phase 3 build.
35
+
36
+ Next step: rebuild the dynamic CPython profile
37
+ (`python-runtime/scripts/build_python.py`) and add the raw-TCP echo and
38
+ `asyncio.start_server` acceptance tests from the list below.
39
+
40
+ ## Goal
41
+
42
+ This must work without changing FastAPI, Starlette, Uvicorn, or application code:
43
+
44
+ ```sh
45
+ pip install "fastapi[standard]"
46
+ fastapi run main.py
47
+ ```
48
+
49
+ Then the host API must reach it through the same port table used by Node:
50
+
51
+ ```ts
52
+ await box.waitForPort(8000)
53
+ const response = await box.request(8000, { path: "/docs" })
54
+ ```
55
+
56
+ The Python process remains CPython compiled to WebAssembly. FastAPI and Uvicorn remain ordinary Python packages. Only the socket implementation and its host bridge are added.
57
+
58
+ ## Current topology
59
+
60
+ ```mermaid
61
+ flowchart LR
62
+ A[Python Uvicorn] --> B[CPython socket API]
63
+ B --> C[Emscripten socket emulation]
64
+ C -. not connected .-> D[VirtualHttpRouter]
65
+ N[Node http.createServer] --> D
66
+ D --> E[box.request / curl / preview]
67
+ ```
68
+
69
+ Node works because its HTTP implementation calls `VirtualHttpRouter.register()` directly. Python currently calls Emscripten sockets, which can appear to bind but cannot deliver connections to the SandboxedJs router.
70
+
71
+ ## Target topology
72
+
73
+ ```mermaid
74
+ flowchart LR
75
+ A[Python Uvicorn] --> B[CPython socket API]
76
+ B --> C[Emscripten/libc socket adapter]
77
+ C --> D[sbx_host socket operations]
78
+ D --> E[syscall-server.ts]
79
+ E --> F[NetworkStack / VirtualHttpRouter]
80
+ F --> G[box.request / curl / preview]
81
+ F --> H[accepted virtual connection]
82
+ H --> D
83
+ D --> C
84
+ C --> B
85
+ B --> A
86
+ ```
87
+
88
+ The host owns all shared socket state. The WASM process owns only its descriptor numbers and Python-visible socket objects.
89
+
90
+ ## Design decisions
91
+
92
+ ### 1. Implement virtual sockets, not a FastAPI adapter
93
+
94
+ A FastAPI-specific bridge would make one framework work while leaving every other Python server broken. The correct abstraction is POSIX-like sockets behind the existing host ABI. Uvicorn, asyncio, Flask adapters, Django, WebSockets, and raw TCP programs then use the same path.
95
+
96
+ ### 2. Use the existing descriptor table
97
+
98
+ Sockets must be normal process handles in `ProcessDescriptorTable`, with `get_flags`, `set_flags`, `dup`, `dup2`, and `poll` behavior. Do not create a second socket-only handle registry that bypasses descriptor lifecycle.
99
+
100
+ ### 3. Keep HTTP parsing in the Python server
101
+
102
+ The router should transport bytes/connections, not parse HTTP for Python. Uvicorn must receive a real accepted stream and remain responsible for HTTP/1.1, keep-alive, chunking, and WebSocket upgrades. `box.request()` may provide a convenience HTTP client, but it must ultimately feed the same connection abstraction.
103
+
104
+ ### 4. Separate listener registration from connection delivery
105
+
106
+ `bind()` and `listen()` register a listener with the container port authority. `accept()` consumes pending connections. A listener is not considered complete merely because `bind()` returned successfully.
107
+
108
+ ### 5. Preserve the blocking contract
109
+
110
+ A Python `accept()`, `recv()`, or `send()` may wait. The Python worker may block on the synchronous transport while the host event loop continues routing requests and resolving readiness. This follows the existing ABI rule documented in [abi.md](abi.md).
111
+
112
+ ## Existing code to wire
113
+
114
+ ### ABI and generated bindings
115
+
116
+ - [host-v1.json](../../python-runtime/abi/host-v1.json): add socket operation codes in the reserved family `1792`.
117
+ - [generate_abi.py](../../python-runtime/scripts/generate_abi.py): ensure generated C/TypeScript bindings include the new operations and payload definitions if the generator has schema-specific output.
118
+ - [sbx_host.h](../../python-runtime/abi/sbx_host.h): generated C operation constants.
119
+ - [host-abi.ts](../../src/runtime/python/host-abi.ts): generated `Op` constants and capability list.
120
+ - [protocol.ts](../../src/runtime/python/protocol.ts): reuse existing framing; only socket payload readers/writers are needed.
121
+
122
+ Initial operations:
123
+
124
+ | Operation | Purpose |
125
+ |---|---|
126
+ | `socket` | Allocate a stream socket handle with family/type/protocol. |
127
+ | `bind` | Bind a socket to an address and port. |
128
+ | `listen` | Mark a bound socket as a listener with a backlog. |
129
+ | `accept` | Return an accepted connection handle and peer address. |
130
+ | `connect` | Connect to an in-container listener or permitted outbound target. |
131
+ | `send` | Send bytes, allowing short writes. |
132
+ | `recv` | Receive bytes, allowing short reads and EOF. |
133
+ | `shutdown` | Close one or both directions. |
134
+ | `getsockname` | Return local address and port. |
135
+ | `getpeername` | Return remote address and port. |
136
+ | `setsockopt` | At minimum support `SO_REUSEADDR`, `SO_KEEPALIVE`, and TCP options Uvicorn touches. |
137
+ | `getsockopt` | Return values for options that libraries inspect. |
138
+
139
+ Do not add UDP, IPv6, DNS, or arbitrary outbound TCP in the first slice unless a failing acceptance test requires them. Uvicorn HTTP and WebSocket serving need TCP listener/accepted-stream semantics first.
140
+
141
+ ### Host client/server
142
+
143
+ - [syscall-client.ts](../../src/runtime/python/syscall-client.ts): add typed methods for socket operations.
144
+ - [syscall-server.ts](../../src/runtime/python/syscall-server.ts): dispatch operations, validate process generation, and map errors to canonical errno.
145
+ - [descriptors.ts](../../src/kernel/descriptors.ts): confirm duplicated socket descriptions share state and close behavior.
146
+ - [open-file.ts](../../src/kernel/open-file.ts): add a socket description abstraction or a common readiness/close interface if sockets cannot use the current file description directly.
147
+ - [python/worker-entry.ts](../../src/runtime/python/worker-entry.ts): no Python-specific routing should be added here; it already owns the CPython worker and host transport lifecycle.
148
+
149
+ ### Network authority
150
+
151
+ - [stack.ts](../../src/net/stack.ts): make listener registration and connection creation use the same authority as Node server ports.
152
+ - [virtual-http.ts](../../src/runtime/virtual-http.ts): extract or reuse a byte-stream connection primitive for request/response and upgrades. Do not force Python into `VirtualHttpServer`; Python needs accepted sockets, not Node EventEmitter semantics.
153
+ - [contracts.ts](../../src/runtime/contracts.ts): extend `RuntimePod`/network contracts only where the host needs a bidirectional accepted connection. Existing `serveExternal` is a possible compatibility seam but is not sufficient for general sockets.
154
+ - [worker-runtime-pod.ts](../../src/runtime/worker-runtime-pod.ts): preserve the existing Node worker proxy and add Python socket events/connection IDs if the Python worker is hosted in a worker that cannot call the host directly.
155
+ - [local-runtime-pod.ts](../../src/runtime/local-runtime-pod.ts): implement the same socket behavior for realm mode or explicitly report that Python server tests require the worker pod.
156
+
157
+ ### CPython/Emscripten side
158
+
159
+ - [extension-abi.json](../../python-runtime/abi/extension-abi.json): add any required main-module exports and document the socket adapter contract.
160
+ - [library_sbx_posix.js](../../python-runtime/native/js/library_sbx_posix.js): add only libc calls that cannot be redirected through the normal Emscripten syscall path. Do not hide socket behavior in JavaScript globals.
161
+ - [0002-emscripten-pipe-socketpair.patch](../../python-runtime/patches/cpython/0002-emscripten-pipe-socketpair.patch): keep the existing socketpair workaround for local pipes; do not confuse it with network sockets.
162
+ - Add a CPython patch or Emscripten syscall adapter for `socket`, `bind`, `listen`, `accept`, `connect`, `send`, `recv`, `poll`, and socket options. Prefer one libc-level adapter so Python's `socket`, `selectors`, and `asyncio` all see the same semantics.
163
+ - [python-syscalls.ts](../../src/runtime/python-syscalls.ts): the old Pyodide backend's `create_server` refusal remains separate. Do not make the new CPython backend depend on Pyodide's asyncio patches.
164
+
165
+ ## Connection protocol
166
+
167
+ Use a host-owned `SocketDescription` with these states:
168
+
169
+ ```text
170
+ created -> bound -> listening -> closed
171
+ created -> connected -> open -> half-closed/closed
172
+ ```
173
+
174
+ Each connection needs:
175
+
176
+ - owning process generation;
177
+ - local and peer address/port;
178
+ - receive queue and byte count;
179
+ - send backpressure state;
180
+ - readable/writable/closed waiters;
181
+ - listener backlog queue;
182
+ - cancellation/teardown behavior;
183
+ - reference count for duplicated descriptors.
184
+
185
+ Suggested host payloads are fixed-width fields plus length-prefixed byte arrays, matching the existing `Writer`/`Reader` framing. Addresses should initially be normalized to IPv4 strings and `u16` ports. Return the accepted descriptor and peer address from `accept`.
186
+
187
+ ### Incoming request path
188
+
189
+ 1. `box.request(8000, init)` asks the router for port 8000.
190
+ 2. The router finds the Python listener registered by `listen()`.
191
+ 3. The host creates a virtual TCP connection and queues it on the listener.
192
+ 4. A blocked Python `accept()` becomes readable through `poll`.
193
+ 5. Python/Uvicorn accepts the descriptor and reads the serialized HTTP request bytes.
194
+ 6. Uvicorn writes HTTP response bytes to the accepted descriptor.
195
+ 7. The router resolves `box.request()` from the response stream.
196
+ 8. Connection close or keep-alive is handled by the same stream state, not by a Python-specific shortcut.
197
+
198
+ ### Outbound local path
199
+
200
+ For `curl localhost:port` or Python `http.client` to a Node/Python listener:
201
+
202
+ 1. `connect()` resolves the address through the container network authority.
203
+ 2. The host locates the listener in the shared port table.
204
+ 3. It creates a pair of virtual stream endpoints.
205
+ 4. The client descriptor and server listener's accepted descriptor reference opposite endpoints.
206
+ 5. `send`/`recv` and `poll` operate on those queues.
207
+
208
+ ### External outbound path
209
+
210
+ Do not silently map Python TCP to host TCP. Start with the existing HTTP/fetch service for outbound HTTP. Add arbitrary outbound TCP only after capability and security policy are defined. `allowOutbound`, host allowlists, cancellation, and DNS behavior must apply consistently.
211
+
212
+ ## Implementation phases
213
+
214
+ ### Phase 0: freeze the contract
215
+
216
+ - Define socket operation numbers and payload schemas.
217
+ - Define descriptor ownership, backlog, half-close, timeout, and cancellation semantics.
218
+ - Define supported families: initially `AF_INET` + `SOCK_STREAM`.
219
+ - Define unsupported behavior explicitly: UDP, IPv6, raw sockets, arbitrary outbound TCP.
220
+ - Add ABI version/golden-frame tests before implementation.
221
+
222
+ ### Phase 1: host-side virtual stream model
223
+
224
+ - Add `SocketDescription` and listener/backlog state.
225
+ - Connect it to the existing port authority and router.
226
+ - Implement in-memory duplex queues with `whenReady()` and EOF/error transitions.
227
+ - Ensure teardown closes listeners and all accepted connections.
228
+ - Add tests for bind conflicts, backlog, short reads/writes, EOF, half-close, reset, and port reuse.
229
+
230
+ ### Phase 2: ABI dispatch and descriptor integration
231
+
232
+ - Generate operation constants.
233
+ - Implement client methods and server dispatch.
234
+ - Add socket handles to descriptor tables.
235
+ - Implement `poll` readiness for listener-readable, connection-readable, connection-writable, hangup, and error.
236
+ - Test stale process generations and killed workers with blocked socket operations.
237
+
238
+ ### Phase 3: CPython socket adapter
239
+
240
+ - Patch/build the dynamic CPython profile with the host socket adapter.
241
+ - Make `socket.socket(AF_INET, SOCK_STREAM)` allocate host handles.
242
+ - Implement Python-visible `fileno`, blocking/nonblocking mode, timeout, `accept`, `makefile`, `sendall`, `recv`, `shutdown`, `getsockname`, and `getpeername`.
243
+ - Implement enough socket constants/options for Uvicorn and asyncio, including `SO_REUSEADDR`.
244
+ - Verify `selectors.DefaultSelector` observes the host readiness fd model.
245
+
246
+ ### Phase 4: asyncio and Uvicorn
247
+
248
+ - Run an ordinary `asyncio.start_server` echo server.
249
+ - Run `uvicorn main:app` and `fastapi run main.py` without adapters.
250
+ - Verify startup, shutdown, keep-alive, request body, response body, and malformed requests.
251
+ - Confirm the process remains alive while the listener is referenced and exits after close.
252
+
253
+ ### Phase 5: router and client integration
254
+
255
+ - Make `box.waitForPort()` recognize Python listeners.
256
+ - Make `box.request()` send a real HTTP byte stream to the accepted Python socket.
257
+ - Make `curl localhost:port` use the same local virtual connection path.
258
+ - Add `box.expose()` support only after in-container request routing works.
259
+ - Add WebSocket upgrade tests using the same accepted stream abstraction.
260
+
261
+ ### Phase 6: hardening and release
262
+
263
+ - Stress repeated server start/stop and connection churn.
264
+ - Test concurrent requests and backpressure.
265
+ - Test cancellation while blocked in `accept`, `recv`, `send`, and `poll`.
266
+ - Test browser worker and Node worker modes.
267
+ - Test cross-origin isolation requirements for browser hosts.
268
+ - Update M6/M7 release gates only after real `box.request()` and Uvicorn tests pass.
269
+
270
+ ## Acceptance tests
271
+
272
+ The first end-to-end test should be a small Python TCP echo server. The second should be HTTP over Uvicorn:
273
+
274
+ ```ts
275
+ const box = await createContainer({
276
+ workerUrl: WORKER_URL,
277
+ files: {
278
+ "/workspace/main.py": `
279
+ from fastapi import FastAPI
280
+
281
+ app = FastAPI()
282
+
283
+ @app.get("/health")
284
+ def health():
285
+ return {"ok": True}
286
+ `,
287
+ },
288
+ });
289
+
290
+ await box.exec('pip install "fastapi[standard]"', { timeoutMs: 300_000 });
291
+ const process = box.spawn("fastapi run /workspace/main.py --port 8000");
292
+ try {
293
+ expect(await box.waitForPort(8000, { timeoutMs: 60_000 })).toBe(true);
294
+ const response = await box.request(8000, { method: "GET", path: "/health" });
295
+ expect(response.status).toBe(200);
296
+ expect(response.json()).toEqual({ ok: true });
297
+ } finally {
298
+ process.kill();
299
+ box.dispose();
300
+ }
301
+ ```
302
+
303
+ Required test groups:
304
+
305
+ - ABI encoding/decoding and capability handshake.
306
+ - Descriptor lifecycle and duplicated descriptors.
307
+ - Listener registration and port conflicts.
308
+ - Readiness and blocking behavior.
309
+ - Raw TCP echo.
310
+ - Node-to-Python and Python-to-Node local connections.
311
+ - Uvicorn/FastAPI GET and POST validation.
312
+ - Keep-alive and concurrent requests.
313
+ - WebSocket upgrade.
314
+ - Cancellation and teardown.
315
+ - Browser worker and Node worker.
316
+
317
+ ## Definition of done
318
+
319
+ The feature is complete only when all of these are true:
320
+
321
+ - `fastapi run main.py` is found after installing `fastapi[standard]`.
322
+ - The command binds a port through the SandboxedJs socket ABI, not a fake success path.
323
+ - `ss -ltn` reports the Python listener with the right owner/PID metadata.
324
+ - `box.waitForPort()` returns true because a real request can reach the listener.
325
+ - `box.request()` receives a real Uvicorn response.
326
+ - `curl localhost:8000` reaches the same Python server.
327
+ - Node and Python servers share port conflict and lifecycle behavior.
328
+ - Killing the Python process unblocks and closes all accepted connections.
329
+ - No host socket or listener remains after container disposal.
330
+ - Browser and Node worker modes pass the same core tests.
331
+ - The old Pyodide backend remains unchanged and does not accidentally claim this capability.
@@ -0,0 +1,39 @@
1
+ # Runtime startup and preview fixes — 2026-08-31
2
+
3
+ ## Observed failure
4
+
5
+ `npm create vite` could scaffold/install successfully, then `npm run dev` printed its script header and returned to the shell without a ready banner. Other runs left a port alive after the owning process had stopped. These were SandboxedJs runtime lifecycle problems; changing the generated Vite project or replacing its commands did not address them.
6
+
7
+ Earlier startup failures also exposed host prerequisites: synchronous child processes need a worker and shared-memory channel, and Rolldown's browser WASI binding needs cross-origin isolation. The library's worker boot diagnostics and local `sandboxedjs-serve` host make those requirements explicit.
8
+
9
+ ## Runtime changes
10
+
11
+ | Problem | Correction |
12
+ | --- | --- |
13
+ | The guest appeared idle while host/WASM compiler promises were still pending | A per-process host-module tracker accounts for async native export and class-method calls. Startup no longer relies on a guessed grace period inferred from unreferenced timers. |
14
+ | Native CommonJS re-exports could store a process-owned wrapper in the shared binding | Assignment/property definition unwrap tracked functions before writing them back. A later process does not inherit a disposed tracker. |
15
+ | Completed native work could run guest continuations after cancellation and create an orphan server | Disposing the tracker prevents late continuations from resuming the stopped guest. |
16
+ | Listening/closed/unreferenced HTTP servers were not consistently reflected in process lifetime | Server reference state participates in liveness checks, and worker port closure is forwarded to the host. |
17
+ | Killing a wrapper did not reliably stop owned children and release ports | Cancellation follows owned process relationships; workers release their child handles. The terminal targets its foreground pipeline while preserving unrelated background jobs. |
18
+ | Input for a synchronous child went to the parent blocked in `Atomics.wait` | Inherited input and raw-mode changes are routed to the child and restored when it exits. |
19
+ | Input-only `readline.createInterface({ input })` incorrectly acquired a default output and raw mode | Input-only interfaces now remain non-terminal unless explicitly configured otherwise. Terminal interfaces also implement Ctrl+C's `SIGINT`/close behavior. |
20
+ | `timers/promises` bypassed guest resource accounting | Promise timers now use the tracked timer API. |
21
+ | A small service-worker bundle could be inlined into a `data:` URL rejected by registration | The default preview asset URL uses `?no-inline`, preserving a real worker file under Vite builds. |
22
+
23
+ The changes live in the main library, principally `src/runtime/host-module-tracker.ts`, the local/worker runtime pods, `readline-module.ts`, `virtual-http.ts`, terminal/kernel process handling, and `src/preview/register.ts`. No external Vite/npm package was patched.
24
+
25
+ ## CLI integration
26
+
27
+ The companion CLI now uses the same container for terminal execution and site preview. Its browser icon toggles a pane with a port selector, reload and close controls. The pane sits on the left on desktop and below the terminal on mobile; its divider supports dragging and keyboard resizing.
28
+
29
+ The app depends on the local SandboxedJs checkout through a `file:` dependency. Build the library before rebuilding the app. These changes have not been published as a new npm release; the local package still identifies as 0.1.30.
30
+
31
+ ## Verification and remaining scope
32
+
33
+ The final runtime suite passed **339 tests**, with **13 skipped**. Regression coverage includes delayed host work, late completion after kill, repeated native re-exports, server close/unref lifetime, promise timers, synchronous child cleanup/input and readline behavior. Type checking and library/application builds passed.
34
+
35
+ Browser checks exercised create-vite's interactive install-and-start flow, a Vite 8 ready banner, HTML and transformed TypeScript responses, the rendered page and its counter, keyboard shortcuts, and desktop/mobile pane resizing and close/reopen. The user also confirmed the current preview renders; the later screenshot showing connection refusal was from older output. The final shared-binding restart correction has a two-process runtime regression test; do not confuse that with a completed end-to-end browser restart matrix.
36
+
37
+ The CLI's three focused session/cancellation tests passed. Its full existing suite still has four backup-related failures because the adapter only accepts verified 0.1.27/0.1.28 snapshots while the installed runtime is 0.1.30. That compatibility guard was not relaxed as part of this work.
38
+
39
+ HTTP preview does not imply WebSocket HMR, secure execution of arbitrary untrusted browser code, multi-tenant routing, or parity for every native package on every host. See [Render a site running inside SandboxedJs](server-previews.md) for integration examples, prerequisites, security boundaries and current limitations.