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.
- package/README.md +16 -9
- package/assets/logo.png +0 -0
- package/bin/sandboxedjs-egress.mjs +25 -10
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/service-worker.js +3 -2
- package/docs/agent/COMMANDS.md +85 -0
- package/docs/agent/DECISION-TREE.md +84 -0
- package/docs/agent/INVARIANTS.md +40 -0
- package/docs/agent/LAUNCH-PROMPT.md +37 -0
- package/docs/agent/LOOP.md +84 -0
- package/docs/agent/README.md +77 -0
- package/docs/agent/ROADMAP.md +37 -0
- package/docs/agent/STATE.md +93 -0
- package/docs/agent/tasks/00-verify-inherited-work.md +36 -0
- package/docs/agent/tasks/01-authoritative-metadata.md +34 -0
- package/docs/agent/tasks/02-native-dependencies.md +35 -0
- package/docs/agent/tasks/03-reproducible-inputs.md +27 -0
- package/docs/agent/tasks/04-build-frontends.md +27 -0
- package/docs/agent/tasks/05-registry-integration.md +27 -0
- package/docs/agent/tasks/06-package-cohorts.md +42 -0
- package/docs/agent/tasks/07-build-on-miss-boundary.md +31 -0
- package/docs/browser-runtime-architecture.md +142 -0
- package/docs/compatibility-implementation-plan.md +98 -0
- package/docs/developer-tool-packs.md +134 -0
- package/docs/frontend-automation.md +49 -0
- package/docs/fullstack-deployment.md +163 -0
- package/docs/handoff.md +275 -0
- package/docs/original-x64.md +39 -0
- package/docs/platform-hardening.md +49 -0
- package/docs/python/abi.md +97 -0
- package/docs/python/architecture.md +94 -0
- package/docs/python/baseline-inventory.md +54 -0
- package/docs/python/build-on-miss.md +198 -0
- package/docs/python/compatibility.md +206 -0
- package/docs/python/cross-build.md +354 -0
- package/docs/python/extensions.md +282 -0
- package/docs/python/release-gates.md +46 -0
- package/docs/python/virtual-sockets-plan.md +331 -0
- package/docs/runtime-lifecycle-fixes.md +39 -0
- package/docs/server-previews.md +268 -0
- package/docs/virtual-browser.md +120 -0
- package/package.json +5 -3
|
@@ -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.
|