sandboxedjs 0.2.10 → 0.2.11
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 +81 -55
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="assets/logo.png" alt="SandboxedJS" width="140" />
|
|
4
4
|
|
|
5
5
|
# SandboxedJS
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ const box = await createContainer({
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
await box.exec("ls -la /app");
|
|
32
|
-
await box.exec("node /app/hello.js");
|
|
32
|
+
await box.exec("node /app/hello.js"); // → hi from linux
|
|
33
33
|
await box.exec("python3 -c 'print(2**64)'"); // → 18446744073709551616
|
|
34
34
|
|
|
35
35
|
box.dispose();
|
|
@@ -54,16 +54,16 @@ Node 18.17+. Pure JavaScript and WebAssembly — no build step, no native module
|
|
|
54
54
|
|
|
55
55
|
## What's inside
|
|
56
56
|
|
|
57
|
-
|
|
|
58
|
-
|
|
59
|
-
| **Filesystem**
|
|
60
|
-
| **Shell**
|
|
61
|
-
| **Commands**
|
|
62
|
-
| **Node.js**
|
|
63
|
-
| **Python**
|
|
64
|
-
| **WebAssembly** | A WASI preview1 host: any `wasm32-wasi` binary runs as an ordinary process
|
|
65
|
-
| **FFmpeg**
|
|
66
|
-
| **Network**
|
|
57
|
+
| | |
|
|
58
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
59
|
+
| **Filesystem** | A full FHS tree in memory (`/etc`, `/usr`, `/var`, `/home`), permissions, ownership, symlinks, hard links, `umask` |
|
|
60
|
+
| **Shell** | POSIX `sh` — pipes, redirection, here-docs, globbing, functions, loops, job control, traps, `[[ ]]`, `(( ))` |
|
|
61
|
+
| **Commands** | 160 programs: `ls cat grep sed awk find sort tar gzip curl wget diff sha256sum ps top` … |
|
|
62
|
+
| **Node.js** | Its own runtime reporting v22.12.0 — CommonJS _and_ ESM, `http`, `fs`, streams, `child_process`, npm and npx |
|
|
63
|
+
| **Python** | Source-built CPython 3.13 (WASM) on the same filesystem, with `pip` |
|
|
64
|
+
| **WebAssembly** | A WASI preview1 host: any `wasm32-wasi` binary runs as an ordinary process |
|
|
65
|
+
| **FFmpeg** | `ffmpeg` / `ffprobe` 5.1 over container files — [optional install](#video-and-audio) |
|
|
66
|
+
| **Network** | In-container HTTP servers, loopback, live previews, and a proxy for real outbound calls |
|
|
67
67
|
|
|
68
68
|
One filesystem underneath all of it: a file written by `echo` is read by `require('fs')` and by
|
|
69
69
|
Python's `open()`, in any direction.
|
|
@@ -71,13 +71,16 @@ Python's `open()`, in any direction.
|
|
|
71
71
|
## Quick tour
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
|
-
const box = await createContainer({
|
|
74
|
+
const box = await createContainer({
|
|
75
|
+
cwd: "/app",
|
|
76
|
+
network: { allowOutbound: true },
|
|
77
|
+
});
|
|
75
78
|
|
|
76
79
|
await box.exec("npm install express", { cwd: "/app", timeoutMs: 300_000 });
|
|
77
80
|
box.spawn("node server.js", { cwd: "/app" });
|
|
78
81
|
await box.waitForPort(3000);
|
|
79
82
|
|
|
80
|
-
const res = await box.request(3000, { path: "/api" });
|
|
83
|
+
const res = await box.request(3000, { path: "/api" }); // talk to it from your code
|
|
81
84
|
console.log(res.status, res.json());
|
|
82
85
|
```
|
|
83
86
|
|
|
@@ -87,17 +90,21 @@ A stateful shell, when `exec` alone is too forgetful:
|
|
|
87
90
|
const session = box.session();
|
|
88
91
|
await session.run("cd /app");
|
|
89
92
|
await session.run("export TOKEN=abc");
|
|
90
|
-
await session.run("echo $TOKEN in $(pwd)");
|
|
93
|
+
await session.run("echo $TOKEN in $(pwd)"); // → abc in /app
|
|
91
94
|
```
|
|
92
95
|
|
|
93
96
|
Files in and out, snapshots, and a terminal you can wire to xterm.js:
|
|
94
97
|
|
|
95
98
|
```ts
|
|
96
99
|
await box.fs.writeFile("/app/config.json", JSON.stringify(config));
|
|
97
|
-
const snapshot = box.snapshot();
|
|
100
|
+
const snapshot = box.snapshot(); // serialisable; restore() later
|
|
98
101
|
|
|
99
102
|
import { Terminal } from "sandboxedjs";
|
|
100
|
-
const terminal = new Terminal(box.session(), {
|
|
103
|
+
const terminal = new Terminal(box.session(), {
|
|
104
|
+
write: (d) => xterm.write(d),
|
|
105
|
+
columns: 80,
|
|
106
|
+
rows: 24,
|
|
107
|
+
});
|
|
101
108
|
xterm.onData((d) => terminal.input(d));
|
|
102
109
|
terminal.start();
|
|
103
110
|
```
|
|
@@ -118,7 +125,7 @@ Two different problems hide in that sentence, and SandboxedJS solves them in two
|
|
|
118
125
|
frontend (:3000) ──▶ http://localhost:8000/agent/run ──▶ FastAPI (:8000)
|
|
119
126
|
```
|
|
120
127
|
|
|
121
|
-
Both servers are inside the container, so `localhost:8000` is
|
|
128
|
+
Both servers are inside the container, so `localhost:8000` is _true there_ — it means the
|
|
122
129
|
container's own backend, never your machine's. Leave the URL exactly as the project writes it. In a
|
|
123
130
|
live preview, an injected script rewrites loopback addresses to the preview's origin and the
|
|
124
131
|
service worker routes them back in by port, so the same code works in the frame without a proxy
|
|
@@ -129,21 +136,27 @@ import { createContainer, createPreview } from "sandboxedjs";
|
|
|
129
136
|
|
|
130
137
|
// `allowOutbound` is still what lets the project install and call out at all;
|
|
131
138
|
// the proxy is how those calls leave a browser, not permission to make them.
|
|
132
|
-
const box = await createContainer({
|
|
139
|
+
const box = await createContainer({
|
|
140
|
+
files: project,
|
|
141
|
+
network: { allowOutbound: true },
|
|
142
|
+
});
|
|
133
143
|
|
|
134
|
-
await box.exec("pip install -r requirements.txt", {
|
|
144
|
+
await box.exec("pip install -r requirements.txt", {
|
|
145
|
+
cwd: "/app/backend",
|
|
146
|
+
timeoutMs: 600_000,
|
|
147
|
+
});
|
|
135
148
|
await box.exec("npm install", { cwd: "/app/frontend", timeoutMs: 600_000 });
|
|
136
149
|
|
|
137
150
|
box.spawn("fastapi run", { cwd: "/app/backend" });
|
|
138
151
|
box.spawn("npm run dev", { cwd: "/app/frontend" });
|
|
139
152
|
await box.waitForPort(3000, { timeoutMs: 60_000 });
|
|
140
153
|
|
|
141
|
-
const preview = await createPreview(box);
|
|
154
|
+
const preview = await createPreview(box); // null where service workers are unavailable
|
|
142
155
|
iframe.src = preview!.urlFor(3000);
|
|
143
156
|
```
|
|
144
157
|
|
|
145
158
|
It works between preview pages and from another tab of the same browser while the owner page is
|
|
146
|
-
open. It does not work from Postman, curl or another machine — the container
|
|
159
|
+
open. It does not work from Postman, curl or another machine — the container _is_ the tab; there is
|
|
147
160
|
no server anywhere to reach.
|
|
148
161
|
|
|
149
162
|
### 2. Backend → the internet: one file, once
|
|
@@ -155,7 +168,7 @@ FastAPI ──▶ egress proxy on your own origin ──▶ https://ollama.com
|
|
|
155
168
|
|
|
156
169
|
A page may only read a response from a host that sends CORS headers, and most APIs send none — an
|
|
157
170
|
`Authorization` header alone forces a preflight plenty of them answer with 405. That is the
|
|
158
|
-
browser's rule about
|
|
171
|
+
browser's rule about _pages_, not a container limit, and the only way through it is a request made
|
|
159
172
|
somewhere a page is not. So add one to the project you already deploy:
|
|
160
173
|
|
|
161
174
|
```bash
|
|
@@ -167,13 +180,13 @@ Netlify are detected, `--target` names one. Nothing else changes: a container in
|
|
|
167
180
|
its own origin for that proxy before giving up, so **no project passes a `proxy` option and no
|
|
168
181
|
project is configured twice.**
|
|
169
182
|
|
|
170
|
-
| Where you are
|
|
171
|
-
|
|
172
|
-
| Deployed (Pages, Vercel, Netlify) | `npx sandboxedjs-egress init --allow …`, then deploy
|
|
173
|
-
| Developing
|
|
174
|
-
| You already have a server
|
|
175
|
-
| Using `npx sandboxedjs-serve`
|
|
176
|
-
| Node, not a browser
|
|
183
|
+
| Where you are | What to do |
|
|
184
|
+
| --------------------------------- | ----------------------------------------------------------------------------------- |
|
|
185
|
+
| Deployed (Pages, Vercel, Netlify) | `npx sandboxedjs-egress init --allow …`, then deploy |
|
|
186
|
+
| Developing | `npx sandboxedjs-egress` — the probe checks its port |
|
|
187
|
+
| You already have a server | `app.use(EGRESS_PATH, egressNodeHandler({ allow: [...] }))`, before any body parser |
|
|
188
|
+
| Using `npx sandboxedjs-serve` | Nothing; it mounts the proxy itself |
|
|
189
|
+
| Node, not a browser | Nothing; there is no CORS to work around |
|
|
177
190
|
|
|
178
191
|
```ts
|
|
179
192
|
// Your own Express/Node server:
|
|
@@ -182,12 +195,13 @@ app.use(EGRESS_PATH, egressNodeHandler({ allow: ["ollama.com"] }));
|
|
|
182
195
|
|
|
183
196
|
// A static host — this is what `init` writes for you:
|
|
184
197
|
import { handleEgressRequest } from "sandboxedjs/egress";
|
|
185
|
-
export const onRequest = ({ request }) =>
|
|
198
|
+
export const onRequest = ({ request }) =>
|
|
199
|
+
handleEgressRequest(request, { allow: ["ollama.com"] });
|
|
186
200
|
```
|
|
187
201
|
|
|
188
202
|
Every exit honours it — `curl`, `wget`, a guest's `fetch`, Python's sockets and `httpx` — so it
|
|
189
203
|
means the same thing whatever the project is written in. Loopback still stays inside the container,
|
|
190
|
-
and the container's own policy still applies first: the proxy widens what a
|
|
204
|
+
and the container's own policy still applies first: the proxy widens what a _page_ can reach, never
|
|
191
205
|
what the container is allowed to.
|
|
192
206
|
|
|
193
207
|
> ⚠️ Anything that can reach the proxy can make requests through it carrying whatever credentials
|
|
@@ -202,11 +216,13 @@ no meta tag or polyfill can add them later:
|
|
|
202
216
|
```ts
|
|
203
217
|
// vite.config.ts
|
|
204
218
|
export default {
|
|
205
|
-
server: {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
server: {
|
|
220
|
+
headers: {
|
|
221
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
222
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
optimizeDeps: { exclude: ["@rolldown/binding-wasm32-wasi"] }, // see below
|
|
210
226
|
};
|
|
211
227
|
```
|
|
212
228
|
|
|
@@ -238,13 +254,13 @@ filesystem and a real socket), and the CLI. They fail only if you call them.
|
|
|
238
254
|
|
|
239
255
|
### Showing what runs
|
|
240
256
|
|
|
241
|
-
| Need
|
|
242
|
-
|
|
243
|
-
| One response as data
|
|
244
|
-
| One response rendered safely | `renderInto(box, el, { port })`
|
|
245
|
-
| A whole site with real URLs
|
|
257
|
+
| Need | Use | Runs guest code on your origin? |
|
|
258
|
+
| ---------------------------- | ------------------------------------- | ------------------------------------------------- |
|
|
259
|
+
| One response as data | `box.request(port, init)` | No — nothing executes |
|
|
260
|
+
| One response rendered safely | `renderInto(box, el, { port })` | No — opaque-origin iframe, no `allow-same-origin` |
|
|
261
|
+
| A whole site with real URLs | `createPreview(box)` → `urlFor(port)` | **Yes** — trusted code only |
|
|
246
262
|
|
|
247
|
-
`createPreview` is what makes a dev server work: requests route by
|
|
263
|
+
`createPreview` is what makes a dev server work: requests route by _which client is asking_, so
|
|
248
264
|
`/src/main.js` and `/@vite/client` resolve without rewriting anything, and a tunnelled `WebSocket`
|
|
249
265
|
carries HMR. Serve it from a separate origin for code you did not write. On Node, `box.expose(port)`
|
|
250
266
|
gives a real loopback URL instead. Full lifecycle notes: [Server previews](docs/server-previews.md).
|
|
@@ -274,9 +290,15 @@ not a runtime dependency, so install it alongside your model provider.
|
|
|
274
290
|
```ts
|
|
275
291
|
import { SandboxedJsBackend, installSandboxSkills } from "sandboxedjs/agent";
|
|
276
292
|
|
|
277
|
-
const box = await createContainer({
|
|
293
|
+
const box = await createContainer({
|
|
294
|
+
cwd: "/app",
|
|
295
|
+
network: { allowOutbound: true },
|
|
296
|
+
});
|
|
278
297
|
await installSandboxSkills(box);
|
|
279
|
-
const agent = createDeepAgent({
|
|
298
|
+
const agent = createDeepAgent({
|
|
299
|
+
model,
|
|
300
|
+
backend: new SandboxedJsBackend(box, { cwd: "/app" }),
|
|
301
|
+
});
|
|
280
302
|
```
|
|
281
303
|
|
|
282
304
|
## Video and audio
|
|
@@ -287,7 +309,9 @@ filesystem — inputs and outputs are ordinary container files, so pipelines wor
|
|
|
287
309
|
report themselves missing, as a real system does.
|
|
288
310
|
|
|
289
311
|
```js
|
|
290
|
-
await box.exec(
|
|
312
|
+
await box.exec(
|
|
313
|
+
"ffmpeg -f lavfi -i testsrc=size=640x480:rate=25:duration=5 -pix_fmt yuv420p clip.mp4",
|
|
314
|
+
);
|
|
291
315
|
await box.exec("ffmpeg -i clip.mp4 -vf scale=320:-2 -frames:v 1 thumb.png");
|
|
292
316
|
```
|
|
293
317
|
|
|
@@ -305,7 +329,7 @@ What it is **not**:
|
|
|
305
329
|
- **Not a VM.** Everything runs in your JavaScript engine. A true escape is an engine escape. This
|
|
306
330
|
is isolation from mistakes and ordinary untrusted programs, not from a determined attacker.
|
|
307
331
|
- **The user model does not constrain Node.** `user: "agent"` is enforced for the shell, the
|
|
308
|
-
commands and Python — `cat /root/secret` is denied for real. It is
|
|
332
|
+
commands and Python — `cat /root/secret` is denied for real. It is _not_ enforced for `node`,
|
|
309
333
|
which reaches the volume directly. Model ordinary multi-user behaviour with it; do not treat it
|
|
310
334
|
as a privilege boundary for JavaScript you do not trust.
|
|
311
335
|
- **A preview shares your origin.** `createPreview` serves guest code from the page that registered
|
|
@@ -323,7 +347,7 @@ An honest list:
|
|
|
323
347
|
|
|
324
348
|
- **No compiled native addons.** A `.node` file cannot load; a package needs a JS or WASM fallback.
|
|
325
349
|
`rollup` and `esbuild` get redirected to `@rollup/wasm-node` and `esbuild-wasm` automatically.
|
|
326
|
-
- **esbuild cannot run
|
|
350
|
+
- **esbuild cannot run _inside_ the sandbox**, so tools that call it directly fail. On Node it
|
|
327
351
|
borrows the host's `esbuild-wasm`.
|
|
328
352
|
- **Vite 8 / Rolldown is browser-only.** The Node binding preopens the real filesystem root and
|
|
329
353
|
cannot be handed another, so it looks for your project on the actual disk. Vite 7 works on both.
|
|
@@ -333,7 +357,7 @@ An honest list:
|
|
|
333
357
|
- **Processes are cooperative.** `kill -9` cannot interrupt a tight synchronous loop; `SIGSTOP`
|
|
334
358
|
marks state. `chroot` runs the command in the target rather than isolating it.
|
|
335
359
|
- **`execSync` needs the worker runtime.** Where it is unavailable, it throws naming the command —
|
|
336
|
-
answer
|
|
360
|
+
answer _No_ to prompts like `npm create vite`'s "Install and start now?" and run the steps from
|
|
337
361
|
the shell.
|
|
338
362
|
- **`node:test`** covers what test files use (`test`/`describe`/hooks/`mock.fn`, spec and tap
|
|
339
363
|
reports); `run()`, coverage and mock timers are not implemented.
|
|
@@ -357,13 +381,15 @@ dispatch all find it:
|
|
|
357
381
|
```ts
|
|
358
382
|
import { defineCommand } from "sandboxedjs";
|
|
359
383
|
|
|
360
|
-
box.kernel.installCommand(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}))
|
|
384
|
+
box.kernel.installCommand(
|
|
385
|
+
defineCommand({
|
|
386
|
+
name: "greet",
|
|
387
|
+
summary: "say hello",
|
|
388
|
+
run: (ctx) => (ctx.line(`hello ${ctx.args[0] ?? "world"}`), 0),
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
365
391
|
|
|
366
|
-
await box.exec("greet there | tr a-z A-Z");
|
|
392
|
+
await box.exec("greet there | tr a-z A-Z"); // → HELLO THERE
|
|
367
393
|
```
|
|
368
394
|
|
|
369
395
|
`Kernel`, `Vfs`, `Shell`, `Terminal` and `NetworkStack` are exported too, if you want to embed a
|
package/dist/index.cjs
CHANGED
|
@@ -21103,7 +21103,7 @@ var wheels_default = {
|
|
|
21103
21103
|
|
|
21104
21104
|
// package.json
|
|
21105
21105
|
var package_default = {
|
|
21106
|
-
version: "0.2.
|
|
21106
|
+
version: "0.2.11"};
|
|
21107
21107
|
|
|
21108
21108
|
// src/python/extension-abi.ts
|
|
21109
21109
|
var EXTENSION_ABI = {
|
package/dist/index.js
CHANGED
|
@@ -21086,7 +21086,7 @@ var wheels_default = {
|
|
|
21086
21086
|
|
|
21087
21087
|
// package.json
|
|
21088
21088
|
var package_default = {
|
|
21089
|
-
version: "0.2.
|
|
21089
|
+
version: "0.2.11"};
|
|
21090
21090
|
|
|
21091
21091
|
// src/python/extension-abi.ts
|
|
21092
21092
|
var EXTENSION_ABI = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sandboxedjs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "A Linux-like container that runs entirely inside Node.js — POSIX shell, ~140 coreutils, Node.js and Python runtimes, virtual filesystem and networking. No Docker, no VM, no native modules.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|