sandboxedjs 0.2.11 → 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 +16 -9
  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,49 @@
1
+ # Platform hardening
2
+
3
+ SandboxedJs is a virtual OS runtime for browser clients and Node hosts. It is
4
+ not a recording application. Continue developing its own JavaScript module
5
+ engine, virtual kernel, filesystem, network and CPython WebAssembly integration;
6
+ do not replace them with Nodepod or Pyodide.
7
+
8
+ ## Release gates
9
+
10
+ Completion requires evidence in both a real browser and a supported Node host.
11
+ A successful bundle alone does not establish browser runtime compatibility.
12
+
13
+ 1. Runtime lifecycle: repeated boot, cancellation, child-process termination,
14
+ disposal and startup failure leave no workers, timers, sockets or mounts.
15
+ 2. Isolation: concurrent guests cannot observe each other's environment or
16
+ filesystem. Worker execution and realm fallback must be tested separately.
17
+ 3. Package workloads: install, run, edit and rebuild representative frontend
18
+ and API projects. Test actual package imports rather than lookalike APIs.
19
+ 4. Python: test dotenv loading, imports, subprocesses, HTTP servers and async
20
+ I/O in the shipped interpreter. Resolve extension setup timeouts before
21
+ claiming the complete Python suite passes.
22
+ 5. Performance: measure cold and warm boot, installation, module loading,
23
+ rebuild latency, memory after disposal and large console output. Record
24
+ runtime version, host, workload and repetitions with each result.
25
+ 6. Distribution: verify published ESM, CommonJS, worker and static browser
26
+ assets, including missing assets and unsupported host features.
27
+
28
+ ## Current findings
29
+
30
+ - Corrected environment support to expose `util.parseEnv`; the previous
31
+ `process.parseEnv` addition did not match native Node. Added
32
+ `process.loadEnvFile` and native-Node differential parser fixtures.
33
+ - Typed-array inspection now reads at most 100 entries instead of copying the
34
+ entire array. DataView logging no longer attempts to iterate a non-iterable.
35
+ - The full test run was interrupted after Python extension setup timed out.
36
+ Follow-up isolated EPERM on the test's loopback listener and fixed missing
37
+ setup error handling. All 12 extension tests passed when loopback listening
38
+ was permitted. This is still not a full release pass.
39
+ - Several Node modules still have stubs: cluster, dgram, diagnostics_channel,
40
+ domain, http2, inspector, tls, v8, vm and worker_threads. Implement and test
41
+ usable APIs before marking those modules supported.
42
+ - The Python thread/syscall limitation documented in
43
+ browser-runtime-architecture.md needs interpreter-image work.
44
+ - Browser CORS, static asset hosting, cross-origin isolation requirements,
45
+ native addons and platform-specific binaries remain constraints. Arbitrary
46
+ packages cannot be promised to run solely from this JavaScript runtime.
47
+
48
+ The platform objective remains open. These gates are an implementation and
49
+ verification backlog, not a claim of complete Node, Python or Linux parity.
@@ -0,0 +1,97 @@
1
+ # `sbx_host_v1` — the host ABI
2
+
3
+ The contract between compiled guest code and the SandboxedJs kernel.
4
+
5
+ Everything a guest cannot do for itself — files, descriptors, pipes, readiness,
6
+ time, identity, entropy, and later sockets, processes and signals — crosses this
7
+ boundary and nothing else. Emscripten and WASI adapters translate *into* it;
8
+ they are not the source of truth. That distinction is the point: compiling a
9
+ program to WebAssembly does not give it an operating system, and pretending an
10
+ Emscripten build's own filesystem is the container's is how two divergent copies
11
+ of the same state come to exist.
12
+
13
+ ## Source of truth
14
+
15
+ `python-runtime/abi/host-v1.json` defines the version, the operation codes, the
16
+ errno values and the capability names. `make -C python-runtime abi` regenerates:
17
+
18
+ - `python-runtime/abi/sbx_host.h` — the C view.
19
+ - `src/python/host-abi.ts` — the TypeScript view.
20
+
21
+ Both are committed. Neither is edited by hand: a C file and a TypeScript file
22
+ that disagree about an opcode produce a wrong answer, not a link error.
23
+
24
+ ## Framing
25
+
26
+ ```
27
+ request: u16 version | u16 op | u32 request_id | u32 generation | u32 length | payload
28
+ response: u16 version | u16 op | u32 request_id | u32 generation | i32 status | u32 length | payload
29
+ ```
30
+
31
+ Little-endian, fixed width, UTF-8 for text, raw bytes for content, 64-bit file
32
+ offsets. `status` is the operation's result: `>= 0` on success, `-errno` on
33
+ failure, with the canonical (Linux/musl) numbers.
34
+
35
+ Three rules the transport must keep, each of which was a real defect before it
36
+ was written down:
37
+
38
+ 1. **A transport failure is never a successful empty result.** `read` returning
39
+ zero bytes means end of input. If losing the host can produce that same
40
+ answer, input silently vanishes instead of raising.
41
+ 2. **A stale answer is not an answer.** `request_id` and `generation` are
42
+ checked on the way back. A completion belonging to a previous incarnation of
43
+ a PID must not land on the current process's descriptor table.
44
+ 3. **A closed transport stays closed.** Once the host has gone, every later call
45
+ fails immediately. Without that, a killed process republishes a request over
46
+ the closed marker and hangs on its way out.
47
+
48
+ The guest writes a zero into `generation`; the transport stamps the real value,
49
+ because the transport belongs to one process and that is where the authority
50
+ lives. Nothing in guest-controlled data may be trusted as identity.
51
+
52
+ ## Blocking
53
+
54
+ Only the guest blocks, and the guest never owns shared state.
55
+
56
+ ```
57
+ Python calls read(fd)
58
+ → libc adapter invokes sbx_host_v1
59
+ → the process worker parks in Atomics.wait
60
+ → the kernel thread waits for input on its own event loop
61
+ → the kernel writes the response and notifies
62
+ → the worker resumes, read() returns
63
+ ```
64
+
65
+ Keyboard input, network responses and storage completions must reach the
66
+ *kernel*, not arrive as messages the blocked worker would have to process. A
67
+ blocked worker processes nothing.
68
+
69
+ This requires `SharedArrayBuffer` and `Atomics.wait`, so a browser host must be
70
+ cross-origin isolated, and the guest must never run on the main thread.
71
+
72
+ ## Status
73
+
74
+ Implemented and covered by `test/python-abi/`: files, descriptors, pipes,
75
+ readiness, time, identity, entropy. Reserved but not implemented: sockets,
76
+ processes, signals, storage, services, threads — `handshake` reports which is
77
+ which, so a guest can tell "not present" from "not implemented" rather than
78
+ discovering it at the first call.
79
+
80
+ ## Known limit
81
+
82
+ `unlink` and `rename` preserve open-file semantics only for names changed
83
+ *through this ABI*. A name removed by the shell or the Node side cannot be
84
+ intercepted, and an open description will then fail with `ENOENT` rather than
85
+ reading stale bytes. Making that hold everywhere means moving the operations
86
+ into the volume itself, which is M3 work.
87
+
88
+ ## Errno translation is not a formality
89
+
90
+ The canonical values here are Linux's. Emscripten's musl uses different
91
+ numbers — `ENOENT` is 44 there and 2 here, and 2 is `EACCES`. Passing a kernel
92
+ errno through an Emscripten adapter unchanged turns "no such file" into
93
+ "permission denied", and the symptom is a `PermissionError` carrying the errno
94
+ of an entirely different failure, several layers from the cause. Every adapter
95
+ translates at its own edge; `src/runtime/python/sbxfs.ts` does it by name,
96
+ against the build's own `ERRNO_CODES`, so it cannot drift from the interpreter
97
+ it is loaded into.
@@ -0,0 +1,94 @@
1
+ # Python on SandboxedJs — architecture
2
+
3
+ The goal is an operating-system compatibility layer with an owned Python
4
+ distribution on top of it, not an interpreter swap. Replacing Pyodide with a
5
+ differently-built CPython in the same lifecycle would inherit the same problems,
6
+ because the problems are not in the interpreter.
7
+
8
+ ## What "our own Python" means here
9
+
10
+ A **runtime distribution**, not a new language implementation. SandboxedJs owns
11
+ the OS-facing ABI, the process model, the filesystem integration, the build
12
+ pipeline and the tests. Upstream CPython supplies Python semantics, adapted by a
13
+ maintained patch series. Building from source gives ownership and
14
+ reproducibility; stability comes from process isolation, consistent semantics
15
+ and systematic testing.
16
+
17
+ ## Governing rules
18
+
19
+ 1. One active Python process owns one interpreter instance.
20
+ 2. One kernel authority owns shared mutable container resources.
21
+ 3. Blocking callers never own the services they are waiting for.
22
+ 4. All guest I/O crosses an explicit, versioned boundary.
23
+ 5. Processes share files and channels — not Python globals.
24
+ 6. Every resource has an owner and a cleanup path.
25
+
26
+ ## Shape
27
+
28
+ ```
29
+ browser app / shell kernel owner (volume, process table)
30
+ │ │
31
+ ├── preview service worker ───────────┤
32
+ │ ├── VFS + descriptors + storage
33
+ │ ├── process supervisor + signals
34
+ │ └── virtual sockets + net policy
35
+ │ ▲
36
+ python worker A ──── sbx_host_v1 ────────────────── ┤
37
+ python worker B ──── sbx_host_v1 ────────────────── ┤
38
+ node workers ────────────────────────────────────── ┘
39
+ ```
40
+
41
+ Kernel services stay on the host that owns them today; Python runs in dedicated
42
+ workers and calls back. Moving the kernel into its own worker is a later
43
+ optimisation with its own migration plan, driven by measured UI responsiveness,
44
+ and is not a prerequisite for any of this.
45
+
46
+ ## Three compatibility targets
47
+
48
+ | Target | Behaviour | Mechanism |
49
+ |---|---|---|
50
+ | Python development | scripts, REPL, venv, pip, subprocesses, local servers | CPython to Wasm plus this kernel |
51
+ | Scientific / ML | supported packages, model inference | recompiled extensions, dedicated compute services |
52
+ | Linux binaries | existing Linux executables and wheels | optional CPU emulation running a real Linux guest |
53
+
54
+ The first is the product. The third is a separate execution backend and must
55
+ never silently substitute for the first when an install fails: it changes
56
+ architecture, path semantics, performance and persistence.
57
+
58
+ ## What exists today
59
+
60
+ - `python-runtime/abi/` — the versioned ABI and its generated bindings.
61
+ - `src/kernel/open-file.ts`, `src/kernel/descriptors.ts` — pathnames, inodes,
62
+ open-file descriptions and per-process descriptor tables.
63
+ - `src/runtime/python/` — framing, the blocking transport, the kernel-side
64
+ dispatcher, the typed guest client, the Emscripten filesystem bridge, the
65
+ release manifest, and the per-process supervisor and worker.
66
+ - `python-runtime/native/probe/` — a C program that exercises the whole path.
67
+ - `python-runtime/scripts/` — the pinned build: fetch and verify, host Python,
68
+ cross-built CPython, and the packaged release with its manifest.
69
+ - `test/python-abi/`, `test/python-runtime/` — the baseline inventory and the
70
+ M1 and M2 gates.
71
+
72
+ ### The owned interpreter
73
+
74
+ `make -C python-runtime fetch python package` produces
75
+ `out/sbx-cpython-<version>-<profile>/`: `python.js`, `python.wasm`,
76
+ `python.data`, and a `runtime.json` naming the ABI it was built against and the
77
+ capabilities the profile actually has. A host selects it with
78
+ `configurePython({ backend: "sbx-cpython-wasm", manifest })`; Pyodide remains
79
+ the default until the parity gates pass.
80
+
81
+ Two things about the build are worth knowing before reading it:
82
+
83
+ - Upstream's browser target links a *page* — a classic script that assigns a
84
+ global `Module` and runs `main` on load. A process worker needs a factory it
85
+ instantiates once per process, so the final link is repeated with
86
+ `-sMODULARIZE -sEXPORT_ES6 -sINVOKE_RUN=0`. Only the link changes.
87
+ - Some of what a program asks an operating system for is compiled into libc
88
+ rather than routed through anything a host can serve. musl's `getpid` under
89
+ Emscripten returns a constant, so every process would report the same
90
+ identity however many interpreters were running. `-Wl,--wrap=` is what makes
91
+ those reachable, and `python-runtime/native/js/library_sbx_posix.js` is where
92
+ the first of them live.
93
+
94
+ See [release-gates.md](release-gates.md) for what each milestone has to prove.
@@ -0,0 +1,54 @@
1
+ # M0 — baseline inventory
2
+
3
+ What the *current* Python integration does, measured rather than remembered, so
4
+ the owned runtime has something to be compared against. Every entry is pinned by
5
+ a test in `test/python-abi/baseline.test.ts`, which asserts today's behaviour —
6
+ including the wrong answers, on purpose. Turning one of those assertions around
7
+ is what a milestone landing looks like.
8
+
9
+ Setup: `src/runtime/cpython.ts` caches **one Pyodide interpreter per container**
10
+ (keyed on the VFS) and runs each program with a fresh globals dict.
11
+
12
+ ## Findings
13
+
14
+ | # | Behaviour | Verified | Cause | Fixed by |
15
+ |---|---|---|---|---|
16
+ | 1 | A module left in `sys.modules` by one program is importable by the next, unrelated one | yes | fresh globals do not reset `sys.modules` | **fixed in M2** — one interpreter per process |
17
+ | 2 | `os.environ` does **not** leak between programs | yes | the environment is explicitly rebound per run | — (the exception, not the rule) |
18
+ | 3 | `asyncio.start_server` fails | yes | `python-syscalls.ts` replaces parts of `asyncio` and refuses server creation | M6 — virtual sockets |
19
+ | 4 | Every program reports the same `os.getpid()` | yes | one interpreter is one process | **fixed in M2** — a process per program, with `getpid` wrapped to ask the kernel |
20
+ | 5 | `subprocess.run` succeeds, via the container's command table | yes | a bridge, not a process: no fresh interpreter, no descriptor inheritance, no process groups | M3 |
21
+
22
+ Fresh globals reset none of: `sys.modules`, logging handlers, registered
23
+ callbacks, native extension state, or running tasks. Finding 2 is worth naming
24
+ precisely because it is the one piece of state the integration *does* rebind —
25
+ which is why "each program gets a clean slate" feels true until it isn't.
26
+
27
+ ## What this means for the plan
28
+
29
+ - Findings 1 and 4 are integration failures, not interpreter failures. A
30
+ differently-built CPython dropped into the same lifecycle would reproduce both.
31
+ That is the argument for building the kernel first.
32
+ - Finding 3 is a missing subsystem. No amount of interpreter work reaches it.
33
+ - Finding 5 is the one that reads as working and is not, which makes it the most
34
+ expensive to leave in place.
35
+
36
+ ## Not yet measured
37
+
38
+ Concurrency and server behaviour under load, native extension state across
39
+ process exits, and interpreter memory growth over repeated runs. These need the
40
+ process model of M3 before the measurement means anything.
41
+
42
+ ## Known gaps in the owned runtime (M2)
43
+
44
+ Recorded so they are not rediscovered as surprises. None of them is hidden at
45
+ runtime: each either refuses with a reason or is declared absent in
46
+ `runtime.json`.
47
+
48
+ | Gap | Symptom | Milestone |
49
+ |---|---|---|
50
+ | No packaging | `pip` is micropip and targets the Pyodide interpreter; under this backend it refuses rather than installing into the wrong interpreter | M4 |
51
+ | No `rlcompleter` / `_pyrepl` in the stdlib image | the REPL prints `warning: can't use pyrepl` and falls back to the basic prompt, which works | M2 follow-up — a `wasm_assets` inclusion |
52
+ | No sockets | anything binding a port, Uvicorn included, cannot start | M6 |
53
+ | No spawn from Python | `subprocess` has no backend on this runtime | M3 |
54
+ | Symlinks | `symlink()` returns ENOSYS through the kernel filesystem | M3 |
@@ -0,0 +1,198 @@
1
+ # The build-on-miss boundary
2
+
3
+ What happens when the installer finds no wheel it can use, and how a host may
4
+ turn that into a build without the runtime ever deciding to.
5
+
6
+ Nothing described here is deployed. `src/runtime/python/build-service.ts`
7
+ defines the shape and ships a local fake; there is no client, no endpoint, and
8
+ no configuration that would produce one.
9
+
10
+ ## The rule
11
+
12
+ The browser runtime is an installer. It does not compile, and it does not ask
13
+ anyone else to compile on its behalf.
14
+
15
+ A resolver that reached out to have a wheel built would be performing a remote
16
+ mutation the caller never asked for — spending someone else's compute,
17
+ publishing an artifact — at exactly the moment a user is least able to notice
18
+ it, during a routine `pip install`. So the runtime does one thing: it reports
19
+ the miss precisely enough to act on. `test/python-runtime/build-service.test.ts`
20
+ checks structurally that `resolver.ts`, `install.ts` and `pip-command.ts` do not
21
+ import the build service at all.
22
+
23
+ ## The seam
24
+
25
+ 1. Resolution fails and raises a `ResolutionError` carrying a
26
+ `ResolutionFailure` — machine-readable, distinguishing
27
+ `no-compatible-distribution` (building might help) from
28
+ `conflicting-requirements` (it cannot).
29
+ 2. The **host** — application code, not the runtime — may call
30
+ `buildRequestFor(failure, { recipeRevision, … })`. It is a pure function:
31
+ it contacts nothing and queues nothing, and returns `null` when building
32
+ could not help or when no source exists to build from.
33
+ 3. The host decides whether to submit that request anywhere.
34
+
35
+ ## Requests converge
36
+
37
+ A build request promises that its artifact is interchangeable with one built
38
+ anywhere else. That holds only if everything which can change the artifact is
39
+ named in the request, so `idempotencyKey` is *derived* from the requirement,
40
+ the target ABI, the wheel tag and the recipe revision — never invented by a
41
+ client, because a random key makes every retry a new build.
42
+
43
+ What is deliberately excluded is evidence about *where* the requester saw the
44
+ source: two clients looking at different mirrors of the same release should
45
+ converge on one build. What is deliberately included is the ABI id, so an ABI
46
+ rollover does not serve a wheel the new runtime cannot load, and the recipe
47
+ revision, since the same source built under different rules is a different
48
+ artifact.
49
+
50
+ ## States
51
+
52
+ `queued`, `resolving`, `building-host-tools`,
53
+ `building-target-dependencies`, `building-wheel`, `testing`, then one of
54
+ `published`, `unsupported`, `failed`, `cancelled`.
55
+
56
+ The intermediate states are the ones someone waiting would ask about, and they
57
+ fail differently: `building-target-dependencies` failing means a native library
58
+ did not cross compile, which is a different report from the package's own build
59
+ failing.
60
+
61
+ `unsupported` is terminal and separate from `failed` on purpose. "This package
62
+ cannot work on this platform" is worth caching forever and showing to the user
63
+ as a fact; "this build did not succeed" may be worth retrying. Collapsing them
64
+ makes a permanent answer look transient, and every client retries it forever.
65
+
66
+ Cancelling something already terminal does not rewrite its outcome: a published
67
+ wheel does not become uncancelled work because someone asked late.
68
+
69
+ ## Security requirements
70
+
71
+ These are requirements on any implementation, stated here because the request
72
+ carries the policy and an audit should be answerable from the request alone. A
73
+ client-supplied policy is a statement of intent, never a grant of permission —
74
+ a service must enforce its own.
75
+
76
+ - **Source allowlist.** Fetch only from named hosts; the default is PyPI.
77
+ - **No network during the build.** A build that can reach the network can fetch
78
+ an unpinned dependency, and then the artifact depends on the day it was
79
+ built. Everything needed is pinned before the build starts.
80
+ - **Resource limits.** Wall-clock and memory ceilings, so a pathological build
81
+ cannot occupy a worker indefinitely.
82
+ - **Nothing published before it runs.** A wheel that compiled is not a wheel
83
+ that works; the `testing` state exists so that publication follows an actual
84
+ import and exercise in the owned runtime, as `package-cohorts.test.ts` does
85
+ locally.
86
+ - **Digest verification.** Immutable artifact locations and recorded hashes. A
87
+ wheel that could be replaced under a URL makes every recorded digest a lie.
88
+ - **Auditability.** The provenance record the wheel already carries — source
89
+ digest, build tools, native dependencies, ABI, recipe revision, patches — is
90
+ echoed in the result for clients that will not open the archive.
91
+
92
+ ## Building here: `buildFromSource`
93
+
94
+ The seam above describes talking to a service. The same boundary also has a
95
+ local implementation, so a host with the toolchain does not need one.
96
+
97
+ ```js
98
+ configurePython({ buildFromSource: true }); // build here
99
+ configurePython({ buildFromSource: "http://localhost:4180/build" }); // ask a machine
100
+ ```
101
+
102
+ `true` is the default **only** where building can possibly work: a Node
103
+ process, running from a checkout that contains the build pipeline. A browser
104
+ has no compiler and will not get one; a published package has no pipeline. In
105
+ both, attempting a build would replace a clear "no wheel for this package" with
106
+ a hang or a confusing failure, so both keep reporting instead.
107
+
108
+ When `pip` hits a `no-compatible-distribution` failure for a package that has a
109
+ source distribution, it asks the builder, then retries the install once against
110
+ the index that now contains the wheel. The loop is bounded, because each pass
111
+ can only make progress by adding one package — without a bound, a graph whose
112
+ every member is unbuildable would rebuild forever rather than report.
113
+
114
+ Two failure classes are deliberately *not* built: conflicting requirements,
115
+ which compiling cannot fix, and packages with no source distribution, which
116
+ have nothing to build from.
117
+
118
+ ### Where the recipe comes from
119
+
120
+ `python-runtime/scripts/auto_recipe.py` writes one. A recipe states which
121
+ package, where its verified source is, and which backend drives it — and PyPI
122
+ publishes the first two while the source declares the third, so requiring a
123
+ person to transcribe them is what made every new package a small project.
124
+
125
+ Nothing that affects the artifact is inferred. Compiler flags still come from
126
+ `abi/extension-abi.json`, and a package needing a patch, a native library or a
127
+ build-environment switch still needs those declared by hand. A build backend
128
+ with no adapter is refused **by name** rather than attempted — attempting it
129
+ fails deep inside someone else's build system, where the message is about a
130
+ missing CMake rather than about this pipeline not supporting CMake.
131
+
132
+ Build requirements a package declares and the lock has never seen are pinned
133
+ at that moment rather than dropped. Dropping them is what made `ujson` fail
134
+ with `ModuleNotFoundError: setuptools_scm` raised from inside its own
135
+ `setup.py` — a message about the package, caused by the generator quietly
136
+ omitting a requirement the package had stated plainly.
137
+
138
+ ## Serving builds to a browser
139
+
140
+ ```bash
141
+ npx sandboxedjs-build-wheels 4180
142
+ ```
143
+
144
+ It serves the wheel index over HTTP and builds on request, binding loopback
145
+ only — a build runs a package's own build system, which is arbitrary code
146
+ execution by design, so exposing it to a network hands that to whoever can
147
+ reach it.
148
+
149
+ The exchange is a package name in, a verdict out. The wheel itself comes back
150
+ the ordinary way, fetched from the index and checked against its digest like
151
+ every other wheel; a service that returned bytes directly would bypass that.
152
+
153
+ It is submit-and-poll, not one long request. A build takes minutes, and a
154
+ connection held open that long is dropped somewhere in between — which reads
155
+ to the caller as the service being unreachable while it is in fact working.
156
+
157
+ ## What is not decided here
158
+
159
+ Whether to run a *hosted* service, where, and who pays for it. That is the
160
+ user's decision, and this project does not own cloud accounts, deployment,
161
+ billing or signing infrastructure.
162
+
163
+ ## What "no wheel" actually means, and what it does not
164
+
165
+ A package with no compiled extension needs no wheel of ours: `requests`, `rich`
166
+ and everything else pure-Python installs from PyPI unchanged. A package with a
167
+ compiled extension needs one built for `cp313-cp313-emscripten_5_0_6_wasm32`,
168
+ because no such wheel is published anywhere.
169
+
170
+ There is no compiler inside the container and there is not going to be one. A
171
+ C/C++ toolchain targeting WebAssembly is clang, lld and a sysroot -- hundreds of
172
+ megabytes before a single package is built -- and it would still not cover the
173
+ Fortran in SciPy or the Rust in pydantic-core and cryptography. Shipping that to
174
+ a browser tab to install one package is not a trade worth making, and calling it
175
+ "free" ignores what serving it costs. So the boundary stays where it is: a host
176
+ with the toolchain builds locally, a host without one points at a build service,
177
+ and a host with neither is told plainly that no wheel exists rather than being
178
+ left to wait on a compiler that cannot run.
179
+
180
+ A local build that fails because the *build machine* lacks a package now says
181
+ so and names what to install, rather than printing the backend's traceback:
182
+ `pip install scipy` reported a `ModuleNotFoundError` for `mesonpy` where it
183
+ meant "install meson-python into the interpreter running this pipeline".
184
+
185
+ ## Standard library coverage
186
+
187
+ 276 of the 290 modules in `sys.stdlib_module_names` import. Of the rest:
188
+
189
+ - `msvcrt`, `nt`, `winreg`, `winsound` are Windows-only. CPython on Linux or
190
+ macOS does not have them either; a program importing `msvcrt` unguarded is
191
+ already broken everywhere but Windows.
192
+ - `tkinter`, `turtle`, `turtledemo`, `idlelib`, `curses`, `readline` need a GUI
193
+ or a terminal device that a container in a page does not have.
194
+ - `antigravity`, `pydoc_data`, `webbrowser` are absent for no good reason and
195
+ are cheap to add; they live in the interpreter's data image, so adding them
196
+ means rebuilding it.
197
+ - `ctypes` is the one real gap. It needs libffi cross-compiled and linked into
198
+ the image, and packages that import it unconditionally will fail until then.