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,134 @@
|
|
|
1
|
+
# Optional developer tools and binary backends
|
|
2
|
+
|
|
3
|
+
This is an extension mechanism and two limited developer-tool adapters, not
|
|
4
|
+
universal Linux compatibility. The core has no new third-party dependencies,
|
|
5
|
+
paid endpoint, account requirement, or automatic downloads.
|
|
6
|
+
|
|
7
|
+
## Working developer tools
|
|
8
|
+
|
|
9
|
+
Install engines in the **host application**, then pass them to the adapters:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install sandboxedjs isomorphic-git @electric-sql/pglite
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import git from 'isomorphic-git';
|
|
17
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
18
|
+
import { createContainer, createGitCommand, createSqlCommand } from 'sandboxedjs';
|
|
19
|
+
|
|
20
|
+
const box = await createContainer({ cwd: '/app' });
|
|
21
|
+
const db = new PGlite();
|
|
22
|
+
box.kernel.installCommand(createGitCommand(git));
|
|
23
|
+
box.kernel.installCommand(createSqlCommand(db));
|
|
24
|
+
|
|
25
|
+
await box.exec('git init');
|
|
26
|
+
await box.fs.writeFile('/app/hello.txt', 'hello');
|
|
27
|
+
await box.exec('git add hello.txt');
|
|
28
|
+
await box.exec('git commit -m first', {
|
|
29
|
+
env: { GIT_AUTHOR_NAME: 'Your Name', GIT_AUTHOR_EMAIL: 'you@example.com' },
|
|
30
|
+
});
|
|
31
|
+
console.log(await box.exec('git log'));
|
|
32
|
+
console.log(await box.exec('sql -c "SELECT 42 AS answer"'));
|
|
33
|
+
|
|
34
|
+
// The application owns the database lifetime and persistence configuration.
|
|
35
|
+
await db.close();
|
|
36
|
+
box.dispose();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Git supports `init`, `add PATH...`, `commit -m MESSAGE`, `status`, and `log`.
|
|
40
|
+
These are local operations backed by isomorphic-git and the credential-aware
|
|
41
|
+
container filesystem. Clone, fetch, push, branches, checkout and full Git CLI
|
|
42
|
+
compatibility are not implemented. No CORS proxy is configured implicitly.
|
|
43
|
+
|
|
44
|
+
`sql -c SQL` and `sql -f FILE` print result rows as JSON. This is an embedded
|
|
45
|
+
SQL adapter, not `psql`, a PostgreSQL server, or a TCP endpoint for `pg` clients.
|
|
46
|
+
Database storage belongs to PGlite, separately from the container filesystem.
|
|
47
|
+
Use its persistence options in the host; container snapshots do not capture it.
|
|
48
|
+
The supplied database API does not provide hard query cancellation; a timeout
|
|
49
|
+
is not a transaction rollback. Do not share the same database across tenants.
|
|
50
|
+
|
|
51
|
+
Both adapters are trusted host integrations. This pass validated real Git
|
|
52
|
+
commits with isomorphic-git 1.42.2 and real SQL with PGlite 0.5.8 on Node,
|
|
53
|
+
not a browser UI. Their upstream engines
|
|
54
|
+
support browsers; host asset loading must still be configured and verified.
|
|
55
|
+
|
|
56
|
+
## Precompiled command packs
|
|
57
|
+
|
|
58
|
+
Packs live outside the core. A host can load their manifests and artifact bytes
|
|
59
|
+
from its own storage and call:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { installWasmCommands } from 'sandboxedjs';
|
|
63
|
+
installWasmCommands(box.kernel, [
|
|
64
|
+
{ name: 'my-tool', bytes: wasmBytes, sha256: trustedManifest.sha256 },
|
|
65
|
+
]);
|
|
66
|
+
await box.exec('my-tool --help');
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Supply a wasm32-wasi command compatible with this runtime's WASI preview1 host.
|
|
70
|
+
Hashes detect changed artifact bytes; trust in the manifest comes from the host.
|
|
71
|
+
Names, collisions, hashes and Wasm validity are checked before commands are
|
|
72
|
+
installed. No network fetch, registry, package archive or license acceptance is
|
|
73
|
+
hidden in this API. WASI imports and behaviors still need compatibility checks;
|
|
74
|
+
valid Wasm alone does not prove that a command can execute here.
|
|
75
|
+
|
|
76
|
+
## ELF execution and fallback
|
|
77
|
+
|
|
78
|
+
The kernel now recognizes executable ELF files instead of treating them as
|
|
79
|
+
shell scripts. It consults `box.kernel.binaries` in this order:
|
|
80
|
+
|
|
81
|
+
1. `compatibility`: exact binary hashes mapped to tested ports.
|
|
82
|
+
2. `translation`: a supplied compiler producing compatible WASI commands.
|
|
83
|
+
3. `emulation`: a supplied emulator backend.
|
|
84
|
+
|
|
85
|
+
`createWasmCompatibilityBackend(id, entries)` registers mappings of
|
|
86
|
+
`{ elfSha256, wasm: { name, bytes, sha256 } }`. Matching content rather than a
|
|
87
|
+
command name prevents silently substituting a port for a different version.
|
|
88
|
+
|
|
89
|
+
`createTranslationBackend(translator, maxCacheBytes?)` wraps a compiler with a
|
|
90
|
+
32 MiB default in-memory LRU output cache. The compiler supplies `id`,
|
|
91
|
+
`supports(info)` and `translate(request, signal)`. Give it a versioned identity.
|
|
92
|
+
Return null only when the input is unsupported. Thrown errors are surfaced.
|
|
93
|
+
This wrapper **does not contain an ELF compiler**. Its checks use a fixture
|
|
94
|
+
translator, not a claim that native instructions were translated in the test.
|
|
95
|
+
Ordinary elfconv/Emscripten output cannot be assumed to match our WASI ABI.
|
|
96
|
+
|
|
97
|
+
An emulator implements `BinaryBackend`, with `tier: 'emulation'` and
|
|
98
|
+
`prepare(request, signal)`. Register it with `box.kernel.binaries.register()`;
|
|
99
|
+
the returned function unregisters it. `list()` reports installed providers.
|
|
100
|
+
An original experimental x86-64 translator and interpreter are now available
|
|
101
|
+
through `createOriginalX64Backends()`. They support a small freestanding subset;
|
|
102
|
+
see [Original engines](original-x64.md). No Linux image is shipped.
|
|
103
|
+
|
|
104
|
+
Preparation returns either `{ supported: false, reason }` or
|
|
105
|
+
`{ supported: true, program: { run(ctx) } }`. Preparation must not execute guest
|
|
106
|
+
code or modify guest files. Backends receive an AbortSignal and must honor it.
|
|
107
|
+
After `run()` starts, its exit code is final: retrying on another backend could
|
|
108
|
+
duplicate writes or other effects. A native program with no supported backend
|
|
109
|
+
exits 126 with a diagnostic. Hard interruption requires a backend worker; the
|
|
110
|
+
existing in-realm WASI runner cannot interrupt a tight compute loop.
|
|
111
|
+
|
|
112
|
+
## Package priorities
|
|
113
|
+
|
|
114
|
+
| Priority | Tools | Approach and current status |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| 1 | Git | Optional isomorphic-git adapter; local subset implemented |
|
|
117
|
+
| 1 | PostgreSQL SQL | Optional PGlite adapter implemented; embedded SQL only |
|
|
118
|
+
| 2 | jq, ripgrep, SQLite CLI, diff/patch | Candidate separate WASI packs; not bundled or validated here |
|
|
119
|
+
| 3 | C/C++ compiler and build tools | Separate large toolchain pack; compilation and subprocess support need work |
|
|
120
|
+
| Later | Redis-compatible services | Evaluate a separate engine and exact protocol/command coverage |
|
|
121
|
+
| Separate project | Docker Engine | Requires Linux kernel facilities; not a small WASI shim |
|
|
122
|
+
|
|
123
|
+
Broad binary translation and emulation coverage remain substantive follow-up work.
|
|
124
|
+
Do not advertise arbitrary ELF, Docker, complete PostgreSQL service compatibility,
|
|
125
|
+
or WebContainer performance parity based on this extension layer.
|
|
126
|
+
|
|
127
|
+
## Cost and licensing
|
|
128
|
+
|
|
129
|
+
SandboxedJs remains MIT. Upstream PGlite and isomorphic-git publish permissive
|
|
130
|
+
licenses, but retain the notices and check the specific versions you distribute.
|
|
131
|
+
A compatibility layer does not remove a tool's license obligations or make
|
|
132
|
+
enterprise software free. Host-supplied packs let users choose their own tools
|
|
133
|
+
and licenses. Hosting, compilation and network services have real resource costs;
|
|
134
|
+
this API does not require a paid provider or promise free third-party hosting.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Frontend browser automation
|
|
2
|
+
|
|
3
|
+
`createFrontendPlaywright(options?)` provides a Playwright-shaped API that runs
|
|
4
|
+
entirely in the page hosting SandboxedJs. It drives a same-origin `<iframe>`
|
|
5
|
+
using the browser the user already has. It does **not** bundle, download or
|
|
6
|
+
launch Chromium, and it is not Playwright.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { createFrontendPlaywright } from 'sandboxedjs';
|
|
10
|
+
|
|
11
|
+
const pw = createFrontendPlaywright({ mount: document.querySelector('#tests') });
|
|
12
|
+
const browser = await pw.chromium.launch();
|
|
13
|
+
const page = await browser.newPage();
|
|
14
|
+
|
|
15
|
+
await page.goto('/preview/index.html');
|
|
16
|
+
await page.getByRole('textbox', { name: 'Email' }).fill('a@example.com');
|
|
17
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
18
|
+
await pw.expect(page.getByText('Thanks')).toBeVisible();
|
|
19
|
+
|
|
20
|
+
await pw.dispose();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Supported
|
|
24
|
+
|
|
25
|
+
`page`: `goto`, `setContent`, `title`, `content`, `evaluate`,
|
|
26
|
+
`setDefaultTimeout`, `close`; locators via `locator`, `getByTestId`,
|
|
27
|
+
`getByText`, and `getByRole` for `button`, `textbox`, `link`, `checkbox`
|
|
28
|
+
(plus any explicit `role=` attribute).
|
|
29
|
+
|
|
30
|
+
`locator`: `first`, `nth`, `locator`, `count`, `isVisible`, `textContent`,
|
|
31
|
+
`innerText`, `inputValue`, `getAttribute`, `click`, `fill`, `waitFor`.
|
|
32
|
+
Locators are strict: more than one match throws unless you narrow with
|
|
33
|
+
`first()`/`nth()`. Actions wait for the element to be visible and enabled.
|
|
34
|
+
|
|
35
|
+
`expect(locator)`: `toBeVisible`, `toHaveText`.
|
|
36
|
+
|
|
37
|
+
## Not supported
|
|
38
|
+
|
|
39
|
+
`screenshot`, `route`/network interception, cross-origin navigation, browser
|
|
40
|
+
launch options, and `firefox`/`webkit` all throw
|
|
41
|
+
`FrontendAutomationUnsupported`. Clicks and typing are dispatched DOM events,
|
|
42
|
+
not OS-level trusted input; `capabilities.trustedEvents` is `false`. `goto`
|
|
43
|
+
returns `null` rather than a Response. `evaluate` uses `eval` inside the frame
|
|
44
|
+
and fails where the page's CSP forbids it.
|
|
45
|
+
|
|
46
|
+
Pass `resolveUrl` to map a container's virtual server URL to the same-origin
|
|
47
|
+
preview URL served by the SandboxedJs service worker. Inspect
|
|
48
|
+
`pw.capabilities` before relying on any behavior; treat this as a labeled
|
|
49
|
+
subset for smoke-testing your own preview output, not a Chromium replacement.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Full-stack projects across browser and Node hosts
|
|
2
|
+
|
|
3
|
+
The guest project can keep its frontend, FastAPI backend, and `http://127.0.0.1:8000`
|
|
4
|
+
API URLs. Hosting prerequisites belong to the outer application that embeds SandboxedJS.
|
|
5
|
+
They cannot all be removed by a JavaScript library.
|
|
6
|
+
|
|
7
|
+
| Where SandboxedJS executes | Frontend → guest backend | Backend → external API | Lifetime |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| Browser, served by a static CDN | `createPreview(box)` rewrites loopback fetch/XHR/EventSource requests into virtual ports | Direct when browser policies permit; otherwise a deployed egress function or an explicitly configured relay | Owner tab stays open |
|
|
10
|
+
| Long-running Node process | `box.request()` or an application HTTP adapter; `box.expose()` for local access | Node performs outbound requests | Process stays alive |
|
|
11
|
+
| Node serverless function | Use `box.request()` within the invocation; return its result through the platform's HTTP response | Node performs outbound requests | Invocation only; do not rely on a background server surviving the response |
|
|
12
|
+
|
|
13
|
+
A Vercel-hosted browser app is still a **browser** runtime. Serving its assets from Vercel
|
|
14
|
+
does not give code in the visitor's browser Node networking permissions. A purely static
|
|
15
|
+
host cannot itself relay an API that rejects cross-origin browser calls. Use a function on
|
|
16
|
+
that host, or provide `network.proxy` pointing to a relay you operate.
|
|
17
|
+
|
|
18
|
+
## Browser host setup
|
|
19
|
+
|
|
20
|
+
1. Serve over HTTPS (localhost is suitable for development).
|
|
21
|
+
2. Send these HTTP response headers on the outer document:
|
|
22
|
+
|
|
23
|
+
```http
|
|
24
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
25
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
3. Serve the matching worker and Python runtime assets from the installed package. When
|
|
29
|
+
copying them into `public`, copy them on **every production build**, not just on `dev`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"scripts": {
|
|
34
|
+
"predev": "npm run stage:runtime",
|
|
35
|
+
"prebuild": "npm run stage:runtime",
|
|
36
|
+
"build": "tsc && vite build"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The staging script must copy `dist/service-worker.js` from the same installed
|
|
42
|
+
`sandboxedjs` as the host bundle. Copy the guest worker, Python worker, and Python
|
|
43
|
+
distribution if your host explicitly serves those from `public` too. Do not keep an
|
|
44
|
+
independently maintained old service worker. Alternatively, let your bundler emit the
|
|
45
|
+
default worker asset used by `createPreview()`.
|
|
46
|
+
|
|
47
|
+
4. Register the emitted worker and use its URL for the iframe:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const preview = await createPreview(box, { scriptUrl: '/service-worker.js' });
|
|
51
|
+
if (!preview) throw new Error('Preview worker could not be registered');
|
|
52
|
+
iframe.src = preview.urlFor(3000);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Without a copied/custom worker, omit `scriptUrl`. Preserve the same-origin worker asset
|
|
56
|
+
as a file; do not inline it as a data URL. A strict guest CSP that rejects the injected
|
|
57
|
+
scripts, compressed HTML, or `{ websocket: false }` prevents the current loopback shim
|
|
58
|
+
from being installed. Use normal uncompressed preview HTML with injection enabled.
|
|
59
|
+
|
|
60
|
+
5. Enable outbound access for package installation and API calls:
|
|
61
|
+
`network: { allowOutbound: true }`. Start both guest servers, check their exit/output
|
|
62
|
+
and `waitForPort()` results, and keep the owning container alive.
|
|
63
|
+
|
|
64
|
+
## External APIs: create the relay in the right directory
|
|
65
|
+
|
|
66
|
+
Run from the **host application root**, not the guest's `/workspace` shell:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
# Cloudflare Pages
|
|
70
|
+
npx sandboxedjs-egress init --target cloudflare --allow ollama.com,api.openai.com
|
|
71
|
+
# Vercel
|
|
72
|
+
npx sandboxedjs-egress init --target vercel --allow ollama.com,api.openai.com
|
|
73
|
+
# Netlify
|
|
74
|
+
npx sandboxedjs-egress init --target netlify --allow ollama.com,api.openai.com
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use one command for the platform you deploy to. An explicit project directory may appear
|
|
78
|
+
before or after the flags. Older CLI builds incorrectly treated a flag value as the
|
|
79
|
+
project directory; if you see a folder called `ollama.com,api.openai.com` or `cloudflare`,
|
|
80
|
+
upgrade the CLI and generate the function at the root instead.
|
|
81
|
+
|
|
82
|
+
Expected paths:
|
|
83
|
+
|
|
84
|
+
| Platform | File relative to host project root | Probe URL |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| Pages | `functions/__sandboxedjs__/egress.ts` | `/__sandboxedjs__/egress` |
|
|
87
|
+
| Vercel | `api/__sandboxedjs__/egress.ts` | `/api/__sandboxedjs__/egress` |
|
|
88
|
+
| Netlify | `netlify/functions/sandboxedjs-egress.ts` | `/.netlify/functions/sandboxedjs-egress` |
|
|
89
|
+
|
|
90
|
+
A GET to the deployed probe must return JSON containing
|
|
91
|
+
`{"sandboxedjs":"egress","protocol":1}`, not the site's HTML. Containers discover these
|
|
92
|
+
paths automatically. Development uses `npx sandboxedjs-egress --allow ollama.com` or
|
|
93
|
+
`npx sandboxedjs-serve dist`; Vite alone does not execute Pages Functions.
|
|
94
|
+
|
|
95
|
+
Deploy the function along with the static build. Cloudflare dashboard drag-and-drop does
|
|
96
|
+
not compile a `functions` directory: use Git integration or Wrangler from the project root.
|
|
97
|
+
See [Cloudflare's deployment documentation](https://developers.cloudflare.com/pages/get-started/direct-upload/).
|
|
98
|
+
|
|
99
|
+
Keep the relay's allowlist narrow and apply your application's authentication and rate limits
|
|
100
|
+
at the host route. Guest `.env` files and browser-delivered keys are visible to the visitor;
|
|
101
|
+
operator-owned credentials belong in server-side secrets. Do not commit actual keys.
|
|
102
|
+
|
|
103
|
+
## FastAPI and a static frontend
|
|
104
|
+
|
|
105
|
+
Inside one container, use the project's normal commands:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
cd /workspace/backend
|
|
109
|
+
pip install -r requirements.txt
|
|
110
|
+
fastapi run main.py --host 0.0.0.0 --port 8000 &
|
|
111
|
+
cd /workspace/frontend
|
|
112
|
+
npx serve . -l 3000
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Preview port 3000 with `preview.urlFor(3000)`. Its fetch to
|
|
116
|
+
`http://127.0.0.1:8000/agent/run/stream` must appear in browser developer tools as a
|
|
117
|
+
same-origin `/…/__sbx__/8000/agent/run/stream` request. No CORS relay is needed for this
|
|
118
|
+
internal hop. An external Ollama request made by FastAPI uses the separate egress path.
|
|
119
|
+
|
|
120
|
+
The bridge currently buffers response bodies. A finite SSE response arrives after the
|
|
121
|
+
backend finishes, preserving its event text, but tokens do not appear progressively.
|
|
122
|
+
An indefinitely open stream will time out. `createPreview(box, { timeoutMs: 600_000 })`
|
|
123
|
+
changes the preview timeout; it cannot change a hosting platform's function limits.
|
|
124
|
+
|
|
125
|
+
## Node and serverless usage
|
|
126
|
+
|
|
127
|
+
The stable transport API is `box.request(port, { method, path, headers, body })`. Start the
|
|
128
|
+
backend and wait for its virtual port before invoking it. Return `response.bytes`, status,
|
|
129
|
+
and headers using your framework's response adapter. Clean up the container when its
|
|
130
|
+
owning request/session ends. `box.expose()` opens a local socket and is useful for local
|
|
131
|
+
Node development; it does not create a public route in Vercel.
|
|
132
|
+
|
|
133
|
+
Serverless workers may be frozen or discarded between invocations. Store files/session
|
|
134
|
+
state externally if needed and reconstruct the container, or use a persistent host for
|
|
135
|
+
interactive sessions. Package support, available workers/WASM, memory, payload sizes and
|
|
136
|
+
[function duration limits](https://vercel.com/docs/functions/limitations) still apply.
|
|
137
|
+
This project does not promise that an arbitrary browser workload runs unchanged inside an
|
|
138
|
+
edge runtime.
|
|
139
|
+
|
|
140
|
+
## Diagnose the actual failed hop
|
|
141
|
+
|
|
142
|
+
- **Frontend still requests `127.0.0.1:8000` in developer tools:** check the deployed worker
|
|
143
|
+
for `installPreviewFetch`, rebuild staged assets, then reload the host and reopen the
|
|
144
|
+
preview. Also check CSP/injection restrictions. Changing FastAPI CORS does not fix an
|
|
145
|
+
address that points at the visitor's machine.
|
|
146
|
+
- **Same-origin `__sbx__/8000` returns 502:** check backend process output and
|
|
147
|
+
`await box.request(8000, { path: '/docs' })`.
|
|
148
|
+
- **Backend receives the request but its AI call fails:** verify the egress probe, allowed
|
|
149
|
+
hostname, upstream credentials and upstream response. Test authenticated and ordinary
|
|
150
|
+
user sessions independently; application permissions may differ.
|
|
151
|
+
- **504 after a long wait:** inspect backend completion and buffering/host time limits.
|
|
152
|
+
- **Works locally but not after build:** inspect actual deployed headers, worker bytes,
|
|
153
|
+
copied runtime versions and function deployment. A successful asset build alone does
|
|
154
|
+
not deploy a function.
|
|
155
|
+
|
|
156
|
+
## Regression evidence
|
|
157
|
+
|
|
158
|
+
`test/preview-worker-build.test.ts` evaluates the distributed service worker, executes its
|
|
159
|
+
injected script, and verifies the loopback POST route and navigation paths.
|
|
160
|
+
`test/egress-cli.test.ts` checks generated paths and flag order for all three platforms.
|
|
161
|
+
`test/fixtures/fullstack-browser` builds a real static browser fixture with a Python
|
|
162
|
+
FastAPI backend, a separate frontend, and a finite SSE POST. It needs no AI credentials;
|
|
163
|
+
see its README for running it.
|
package/docs/handoff.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Handoff — SandboxedJS
|
|
2
|
+
|
|
3
|
+
## Repo state
|
|
4
|
+
|
|
5
|
+
`npx tsc --noEmit` passes, `npm run build` passes, and **316 tests pass**
|
|
6
|
+
including the network acceptance suite:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
SANDBOXEDJS_CLEAN_NETWORK_TESTS=1 npx vitest run
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
(The network tests hit the real npm registry and take ~2 min: Express,
|
|
13
|
+
`npx serve`, `npm install` + `npx`, `npm create vite`, a Vite 7 dev server, and
|
|
14
|
+
an interactive clack prompt driven through `Terminal`.)
|
|
15
|
+
|
|
16
|
+
## Context
|
|
17
|
+
|
|
18
|
+
`@scelar/nodepod` was removed entirely (licence: Commons Clause). The Node.js
|
|
19
|
+
runtime is this package's own, under `src/runtime/`. It must stay
|
|
20
|
+
browser-capable: **nothing in `src/runtime/` may import a `node:` builtin**
|
|
21
|
+
except through `nodeOnlyModule`/`nodeBuiltin` (`src/util/binary.ts`) on a path
|
|
22
|
+
that returns `null` off Node. Several npm shims had to be replaced because they
|
|
23
|
+
are not browser-safe — see `util-module.ts`, `assert-module.ts`,
|
|
24
|
+
`zlib-module.ts`, `url-module.ts`, `readline-module.ts`, `readable-from.ts`.
|
|
25
|
+
|
|
26
|
+
## Verified in a real browser
|
|
27
|
+
|
|
28
|
+
Chrome, against the Vite host app at
|
|
29
|
+
`/Users/shazi/Practice/sandboxedjs tes/browser-server` (`npm run dev`). That
|
|
30
|
+
project has a `check.html` / `src/browser-check.ts` page that boots a container
|
|
31
|
+
and drives `Terminal` through the whole set; open `/check.html` and it prints a
|
|
32
|
+
pass/fail line per check:
|
|
33
|
+
|
|
34
|
+
- `readline.question` answered, with the typed text echoed as it is typed
|
|
35
|
+
- `https.get` from inside the sandbox reaching the npm registry
|
|
36
|
+
- clipboard round-trip through `pbcopy`/`pbpaste`
|
|
37
|
+
- `npm create vite@latest` accepting a project name and advancing
|
|
38
|
+
- `ctrl+c` cancelling a prompt and returning to the shell
|
|
39
|
+
- `npx serve --debug` starting with no errors and serving a file
|
|
40
|
+
|
|
41
|
+
## How isolation is put together
|
|
42
|
+
|
|
43
|
+
`WorkerRuntimePod` (`src/runtime/worker-runtime-pod.ts`) extends
|
|
44
|
+
`LocalRuntimePod` and overrides `spawn` alone; everything else in the contract
|
|
45
|
+
is identical. Both are held to `test/pod-contract.ts`, which is the thing to
|
|
46
|
+
extend when either changes.
|
|
47
|
+
|
|
48
|
+
The topology is forced, not chosen. A synchronous call must block its caller
|
|
49
|
+
while the work it waits on still progresses, so the blocking side cannot own
|
|
50
|
+
the shared state — otherwise a child needing the filesystem would call into a
|
|
51
|
+
frozen thread. Hence: guest in the Worker, volume and kernel on the host.
|
|
52
|
+
|
|
53
|
+
- `sync-channel.ts` — `SharedArrayBuffer` + `Atomics.wait`, chunked both ways.
|
|
54
|
+
Only the client blocks; the server answers from its ordinary event loop.
|
|
55
|
+
- `remote-volume.ts` — `RuntimeVolume` over that channel. Because the interface
|
|
56
|
+
was already the seam, `core-modules`, `Vfs` and the kernel are untouched.
|
|
57
|
+
- `sync-syscalls.ts` — the filesystem and `spawnSync` share one channel.
|
|
58
|
+
- `worker-entry.ts` — the guest half, built as its own bundle.
|
|
59
|
+
|
|
60
|
+
Two packaging traps, both already paid for:
|
|
61
|
+
|
|
62
|
+
1. The guest bundle must be **fully self-contained** (`noExternal` in
|
|
63
|
+
`tsup.config.ts`). The main bundle leaves `buffer` and friends external and
|
|
64
|
+
lets the host's bundler map them onto polyfills; the Worker is fetched as
|
|
65
|
+
its own module graph and gets no such help, so an external `buffer` resolves
|
|
66
|
+
to raw CommonJS with no named exports and the Worker dies before it runs.
|
|
67
|
+
2. `new URL("./worker-entry.js", import.meta.url)` **does** survive Vite's
|
|
68
|
+
dependency pre-bundling — verified in Chrome, it fetches from
|
|
69
|
+
`node_modules/sandboxedjs/dist/`. Do not "fix" this with a blob URL: a blob
|
|
70
|
+
worker has no module-resolution context and could no longer `import()` the
|
|
71
|
+
host's esbuild or Rolldown binding.
|
|
72
|
+
|
|
73
|
+
## Verifying the preview service worker
|
|
74
|
+
|
|
75
|
+
The in-app browser pane refuses service worker registration outright — even a
|
|
76
|
+
one-line worker fails — so the preview cannot be checked there and will always
|
|
77
|
+
report itself unavailable. Real Chrome works. The quickest check is
|
|
78
|
+
`/swcheck.html` in the harness, which stands up a tiny server and asserts that
|
|
79
|
+
an absolute-path subresource is routed; `/preview.html` runs the full
|
|
80
|
+
`create-vite` flow and takes about half a minute. Headless Chrome over CDP works
|
|
81
|
+
too, which is how this was verified.
|
|
82
|
+
|
|
83
|
+
Three things bit during that verification and are easy to re-introduce:
|
|
84
|
+
`navigator.serviceWorker.ready` never resolves for a worker whose scope excludes
|
|
85
|
+
the registering page; the claim path arrives under the worker's scope directory,
|
|
86
|
+
so its pattern must not be anchored to the start; and a framed response needs
|
|
87
|
+
`Cross-Origin-Embedder-Policy` as well as `Cross-Origin-Resource-Policy` or an
|
|
88
|
+
isolated parent refuses it.
|
|
89
|
+
|
|
90
|
+
## A trap worth remembering: inherited stdin is a terminal
|
|
91
|
+
|
|
92
|
+
`stdio: "inherit"` hands a child the parent's standard input, and a terminal
|
|
93
|
+
never ends. Give it a pipe and anything that reads to end-of-file first will
|
|
94
|
+
wait forever — and `node` does that in `captureStdin` (`src/runtime/node.ts`)
|
|
95
|
+
before it runs a script. The symptom is remote from the cause: a shell script
|
|
96
|
+
that launches node prints nothing at all and hangs, with no error anywhere.
|
|
97
|
+
|
|
98
|
+
`KernelChildProcess` (`node-child-process-bridge.ts`) therefore marks an
|
|
99
|
+
inheriting child's stdin `isTTY`/`interactive`. `test/sync-child-process.test.ts`
|
|
100
|
+
pins the reduced case — `spawnSync('sh', ['-c', 'node -e …'], {stdio:"inherit"})`
|
|
101
|
+
— which is much cheaper to run than the create-vite flow it was found in.
|
|
102
|
+
|
|
103
|
+
## The interactive-input chain, for future work
|
|
104
|
+
|
|
105
|
+
1. `Terminal.key()` — `src/container/terminal.ts`. While a command runs it
|
|
106
|
+
writes each keystroke to `this.currentStdin` (a `Pipe`). **Raw mode changes
|
|
107
|
+
what it does**: no local echo, no CR→LF translation, and `ctrl+c`/`ctrl+d`
|
|
108
|
+
are passed through as keystrokes rather than becoming a signal and EOF.
|
|
109
|
+
2. `node` command — `src/runtime/node.ts`, `execute()`. Forwards `ctx.stdin`
|
|
110
|
+
into `proc.write(...)`, propagates EOF via `proc.endInput()`, and mirrors the
|
|
111
|
+
program's `rawmode` event back onto `ctx.stdin.rawMode`.
|
|
112
|
+
3. `LocalProcess.write` — `src/runtime/local-runtime-pod.ts`. Buffers input
|
|
113
|
+
until the task starts, then hands it to `core.writeStdin`.
|
|
114
|
+
4. `createCoreModules` — `src/runtime/core-modules.ts`. With
|
|
115
|
+
`interactiveStdin: true`, `process.stdin` is a `PassThrough` that stays open;
|
|
116
|
+
`tty: true` sets `isTTY` on all three streams; `setRawMode` calls back into
|
|
117
|
+
`options.onRawMode`.
|
|
118
|
+
5. `readline-module.ts`. `emitKeypressEvents(stream)` turns incoming data into
|
|
119
|
+
`keypress` events. `createInterface` defaults `terminal` from `output.isTTY`,
|
|
120
|
+
as Node does; a terminal `Interface` owns raw mode, keeps `line`/`cursor`
|
|
121
|
+
current, and echoes. **Prompt libraries read `rl.line` for the answer**, so
|
|
122
|
+
anything that stops it tracking shows up as an empty answer and a re-prompt,
|
|
123
|
+
not as an error.
|
|
124
|
+
|
|
125
|
+
`settle()` in `local-runtime-pod.ts` decides when a process is finished. It
|
|
126
|
+
counts pending timers, `core.readingStdin()` and `core.pendingRequests()` — an
|
|
127
|
+
in-flight HTTP request is event-loop work and schedules no timer of its own.
|
|
128
|
+
Check this first if a process exits before something asynchronous completes.
|
|
129
|
+
|
|
130
|
+
## Known gaps
|
|
131
|
+
|
|
132
|
+
- **Vite 8 / Rolldown works in a browser, not under a Node host.** (The old note
|
|
133
|
+
saying it cannot run at all was wrong.) In a browser it needs two things from
|
|
134
|
+
the host app, both now in the README: COOP/COEP headers, and
|
|
135
|
+
`optimizeDeps.exclude: ["@rolldown/binding-wasm32-wasi"]` so the bundler does
|
|
136
|
+
not pre-bundle away the `import.meta.url` its WASI worker is created from.
|
|
137
|
+
With those, `npm create vite` installs and the dev server serves transformed
|
|
138
|
+
modules — verified in Chrome. Under Node the binding's other build makes a
|
|
139
|
+
`node:wasi` instance preopening the real filesystem root, which cannot be
|
|
140
|
+
redirected at the sandbox volume, so Rolldown never finds the project and Vite
|
|
141
|
+
answers with its fallback page. Fixing that means running the *browser* build
|
|
142
|
+
on Node, which needs a `fetch` that handles `file:` URLs and a global `Worker`
|
|
143
|
+
over `worker_threads`. Vite 7 works on both and is what the Node test pins.
|
|
144
|
+
|
|
145
|
+
- **`spawnSync`/`execSync`/`execFileSync` work under the Worker pod**, which is
|
|
146
|
+
the default. See *Isolation* in the README for the fallback rules. Under the
|
|
147
|
+
in-realm pod they still throw, naming the command — guest, child and event
|
|
148
|
+
loop share a thread there, so blocking the caller stops the child.
|
|
149
|
+
- **esbuild in a browser.** `host-esbuild.ts` borrows the host's `esbuild-wasm`
|
|
150
|
+
on Node and returns `null` in a browser, so browser Vite transforms fail.
|
|
151
|
+
- **No service worker**, so preview iframes have no URL. `box.request()` works
|
|
152
|
+
everywhere.
|
|
153
|
+
- **`curl` to a non-CORS host cannot work in a browser.** Platform limit, not a
|
|
154
|
+
bug; the error message says so. The npm registry does send CORS headers, which
|
|
155
|
+
is why installing packages works.
|
|
156
|
+
- **`child_process.execSync` and friends** cannot exist: they would have to
|
|
157
|
+
block the JS thread. They throw `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM`.
|
|
158
|
+
- Vite prints two `util` externalization warnings from `readable-stream`, which
|
|
159
|
+
declares `"util": false` for browsers and falls back on its own. Harmless.
|
|
160
|
+
|
|
161
|
+
## Python: blocking syscalls, and what they are built on
|
|
162
|
+
|
|
163
|
+
`src/runtime/python-syscalls.ts` is the layer that makes Pyodide behave like a
|
|
164
|
+
Linux Python rather than a sandboxed evaluator. The constraint it removes is
|
|
165
|
+
that **WebAssembly cannot wait for a JavaScript promise**: Pyodide's `setStdin`
|
|
166
|
+
callback is synchronous, so anything only the host can answer asynchronously
|
|
167
|
+
had to be answered immediately or not at all. Answering "not at all" is what
|
|
168
|
+
made `input()` raise `EOFError`, and it is the same wall behind `socket`,
|
|
169
|
+
`subprocess` and `time.sleep`.
|
|
170
|
+
|
|
171
|
+
The primitive is WebAssembly stack switching (JSPI), reached through Pyodide's
|
|
172
|
+
`run_sync`. It suspends the whole interpreter stack until a host promise
|
|
173
|
+
settles, so an ordinary `def` — nested arbitrarily deep — can block while the
|
|
174
|
+
event loop keeps running. Verified available unflagged in Node 25 and Chrome
|
|
175
|
+
137+. **Adding a blocking syscall is now a method on the host facade**, not
|
|
176
|
+
another special case; that is the point of the file.
|
|
177
|
+
|
|
178
|
+
Three are wired:
|
|
179
|
+
|
|
180
|
+
- **stdin.** `sys.stdin` is rebuilt as a real `TextIOWrapper`, not a patched
|
|
181
|
+
`input()` — otherwise `csv.reader(sys.stdin)` stays broken. EOF still raises
|
|
182
|
+
`EOFError` as CPython does.
|
|
183
|
+
- **processes.** Only `subprocess.Popen` is replaced; `run`, `call`,
|
|
184
|
+
`check_call` and `check_output` are written in terms of it upstream, so the
|
|
185
|
+
family comes with it. The child is a *live* process on the host's event loop
|
|
186
|
+
(`kernel.spawn` + pipes), not a finished result: the parent suspends only
|
|
187
|
+
when it reads, waits or communicates, so `for line in p.stdout` follows a
|
|
188
|
+
child that is still running and `poll()` can say "running". `os.system` and
|
|
189
|
+
`os.popen` go to the same place — `os.system` previously returned 0 having
|
|
190
|
+
run *nothing*, which reads as success and is worse than an error.
|
|
191
|
+
- **HTTP.** `urllib` handlers call `performRequest` (`src/net/commands.ts`),
|
|
192
|
+
the same path as `curl`. Deliberately not `fetch`: that is what makes Python
|
|
193
|
+
obey the container's `allowOutbound` policy instead of routing around it.
|
|
194
|
+
|
|
195
|
+
### Third-party HTTP stacks, and a network-policy escape
|
|
196
|
+
|
|
197
|
+
A library that brings its own transport does not go through `urllib`, and in
|
|
198
|
+
Pyodide several reach JavaScript's `fetch` directly. **`requests` used to reach
|
|
199
|
+
the internet from a container with `allowOutbound` off, while `curl` in the
|
|
200
|
+
same container was correctly refused** — a real hole in the sandbox, found by
|
|
201
|
+
testing the policy rather than the feature. `test/python-syscalls.test.ts`
|
|
202
|
+
guards it.
|
|
203
|
+
|
|
204
|
+
Such libraries cannot all be patched at boot, because pip installs them later.
|
|
205
|
+
So adapters are registered by module name and applied when that module is first
|
|
206
|
+
imported, through a hook on `builtins.__import__`. **Supporting another stack
|
|
207
|
+
is a small function registered there, not a change to the machinery.** The
|
|
208
|
+
`requests` adapter replaces `HTTPAdapter.send` — the one seam every call
|
|
209
|
+
crosses, below sessions/redirects/cookies/retries and above the urllib3
|
|
210
|
+
transport that would otherwise reach the network itself.
|
|
211
|
+
|
|
212
|
+
### Hosts without stack switching
|
|
213
|
+
|
|
214
|
+
Everything blocking rests on JSPI, and Pyodide 0.28 ships JSPI-only — there is
|
|
215
|
+
no Asyncify build to fall back to. Where it is missing, the degradation is
|
|
216
|
+
deliberate rather than incidental:
|
|
217
|
+
|
|
218
|
+
- piped and redirected stdin still work, from the pre-drained buffer;
|
|
219
|
+
- anything that must genuinely wait (interactive input, a child process) says
|
|
220
|
+
so plainly instead of faking end-of-file;
|
|
221
|
+
- `requests` falls back to the library's own transport rather than breaking —
|
|
222
|
+
**but the network policy is not the part that degrades.** A host the
|
|
223
|
+
container forbids is still refused. Capability degrades; the sandbox does not.
|
|
224
|
+
|
|
225
|
+
The only way to get blocking without JSPI is a Worker plus
|
|
226
|
+
`SharedArrayBuffer`/`Atomics.wait` — the machinery `sync-channel.ts` already
|
|
227
|
+
has for the Node runtime. It is a real project: Pyodide would move off the main
|
|
228
|
+
thread and its Emscripten filesystem would have to reach the volume over the
|
|
229
|
+
channel, and in a browser it additionally requires cross-origin isolation
|
|
230
|
+
(COOP/COEP), which is a deployment requirement and not only code.
|
|
231
|
+
|
|
232
|
+
Five traps, each already paid for and each cheap to reintroduce:
|
|
233
|
+
|
|
234
|
+
1. **Pyodide maps JS `null` to a truthy `JsNull` proxy; only `undefined`
|
|
235
|
+
becomes `None`.** `buffered()` returns `undefined` to mean "you must
|
|
236
|
+
suspend", and a `?? EMPTY` on that path silently turned it back into
|
|
237
|
+
end-of-file — the exact bug the bridge exists to remove.
|
|
238
|
+
2. **One interpreter is shared per container**, so its streams belong to
|
|
239
|
+
whichever program is running. A Python child binds over its parent while the
|
|
240
|
+
parent is suspended, so every host call that can suspend goes through
|
|
241
|
+
`resuming()`, which puts the parent's binding back.
|
|
242
|
+
3. **`kernel.spawn` does not close a child's output pipes when it exits** —
|
|
243
|
+
`Container.spawn` does that itself — and a reader waiting on a pipe that
|
|
244
|
+
never ends waits forever. This deadlocked the whole suite once.
|
|
245
|
+
4. **`Popen` must not run at construction.** `subprocess.run` hands `input=` to
|
|
246
|
+
`communicate()`, never to `Popen`.
|
|
247
|
+
5. **Pyodide builds urllib without `ssl`, so `urllib.request.HTTPSHandler` does
|
|
248
|
+
not exist** to subclass. One handler subclassing `HTTPHandler` serves both
|
|
249
|
+
schemes, which also keeps `build_opener` treating it as a replacement.
|
|
250
|
+
|
|
251
|
+
`asyncio.run` and `run_until_complete` are patched onto `run_sync` for the same
|
|
252
|
+
reason: programs execute under `eval_code_async`, so Pyodide's loop is always
|
|
253
|
+
already running and `asyncio.run` refuses to start a second one.
|
|
254
|
+
|
|
255
|
+
Known remaining gaps:
|
|
256
|
+
|
|
257
|
+
- **Python cannot accept connections.** `loop.create_server` is unimplemented
|
|
258
|
+
on Pyodide's WebLoop, so uvicorn and aiohttp install, import and start but
|
|
259
|
+
never bind — outbound requests work, inbound do not. Closing this means
|
|
260
|
+
widening the `RuntimePod` contract with a serve hook so Python can register
|
|
261
|
+
with `VirtualHttpRouter` (`src/runtime/virtual-http.ts`), the same router
|
|
262
|
+
that already routes `curl localhost:3000` to a Node server in the container,
|
|
263
|
+
plus an asyncio transport that frames HTTP between the router's structured
|
|
264
|
+
requests and the protocol's raw bytes. `router` is currently `protected` on
|
|
265
|
+
`LocalRuntimePod`; `WorkerRuntimePod` inherits it, so one implementation
|
|
266
|
+
covers both. For now the failure at least explains itself.
|
|
267
|
+
|
|
268
|
+
- A library with its own transport and no registered adapter can still reach
|
|
269
|
+
the network directly and escape the policy. `requests` and `urllib` are
|
|
270
|
+
covered; the hook makes the next one cheap, but it is opt-in by design.
|
|
271
|
+
- Package resolution: `loadPackagesFromImports` only sees Pyodide's lockfile
|
|
272
|
+
and `micropip` only installs pure-Python wheels, so `opencv-python` and
|
|
273
|
+
everything else needing a C extension built for wasm32-emscripten remains out
|
|
274
|
+
of reach. The fix is a resolution chain (lockfile → PyPI pure-Python → a wasm
|
|
275
|
+
binary-wheel index → clear failure), not a special case per library.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Original experimental x86-64 engines
|
|
2
|
+
|
|
3
|
+
`createOriginalX64Backends()` returns two optional backends written in this
|
|
4
|
+
repository: an instruction-to-WebAssembly translator and a JavaScript instruction
|
|
5
|
+
interpreter. Neither embeds a third-party compiler or CPU emulator. Register the
|
|
6
|
+
returned backends with `box.kernel.binaries.register(backend)`.
|
|
7
|
+
|
|
8
|
+
The translator emits real Wasm integer operations, using mutable register globals
|
|
9
|
+
and an instruction cache. It translates lazily during execution, rather than
|
|
10
|
+
producing a distributable standalone Wasm file. The interpreter executes the same
|
|
11
|
+
decoded instructions directly. They currently support the same instruction subset;
|
|
12
|
+
register only the `emulation` backend to exercise the interpreter directly.
|
|
13
|
+
|
|
14
|
+
Supported inputs are freestanding, static, little-endian x86-64 ET_EXEC ELF files.
|
|
15
|
+
Loadable segments must fit a 16 MiB memory window and use addresses below 4 GiB.
|
|
16
|
+
Dynamic linking is rejected. Supported instructions: 32/64-bit immediate and
|
|
17
|
+
register MOV, register ADD/SUB/XOR/CMP, selected immediate ADD/SUB/CMP, RIP-relative
|
|
18
|
+
LEA, NOP, short JMP/JZ/JNZ, relative JMP, and SYSCALL. Only the zero flag is modeled.
|
|
19
|
+
There is no stack/argv ABI, arbitrary memory operand, SSE, libc, file-open syscall,
|
|
20
|
+
networking, threads, or Docker support. Unsupported instructions fail explicitly.
|
|
21
|
+
|
|
22
|
+
Linux calls: read stdin, write stdout/stderr, exit/exit_group, getpid. Unsupported
|
|
23
|
+
calls return ENOSYS. Segment read/write permissions are checked for I/O buffers;
|
|
24
|
+
self-modifying code is rejected. Each run owns its registers and memory. The
|
|
25
|
+
one-million-instruction default budget and periodic event-loop yields bound loops
|
|
26
|
+
and permit cancellation between batches. This is not a hardened security boundary
|
|
27
|
+
or a claim of native performance. Translation currently has substantial per-
|
|
28
|
+
instruction overhead; performance optimization is future work.
|
|
29
|
+
|
|
30
|
+
Tests use `test/fixtures/native/hello.s`, assembled by Clang for Linux. The checked
|
|
31
|
+
in ELF is built with the adjacent `package-elf.py` script. Both engines run the
|
|
32
|
+
same binary, including a conditional loop and Linux console output; an infinite
|
|
33
|
+
loop fixture verifies the instruction limit. Clang is only used to produce test
|
|
34
|
+
inputs; it is not part of either engine at runtime.
|
|
35
|
+
|
|
36
|
+
The local browser project registers both backends and exposes `native-hello`.
|
|
37
|
+
Opening `/?native=emulation` selects the interpreter. Node and Python commands
|
|
38
|
+
continue through their existing runtimes. This is an initial working CPU subset,
|
|
39
|
+
not arbitrary Linux package compatibility.
|