microsandbox 0.1.1 → 0.3.7
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 +333 -84
- package/index.cjs +609 -0
- package/index.d.cts +751 -0
- package/index.mjs +21 -0
- package/package.json +31 -48
- package/postinstall.js +194 -0
- package/dist/base-sandbox.d.ts +0 -91
- package/dist/base-sandbox.js +0 -264
- package/dist/base-sandbox.js.map +0 -1
- package/dist/command-execution.d.ts +0 -59
- package/dist/command-execution.js +0 -102
- package/dist/command-execution.js.map +0 -1
- package/dist/command.d.ts +0 -24
- package/dist/command.js +0 -84
- package/dist/command.js.map +0 -1
- package/dist/execution.d.ts +0 -52
- package/dist/execution.js +0 -103
- package/dist/execution.js.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -25
- package/dist/index.js.map +0 -1
- package/dist/metrics.d.ts +0 -69
- package/dist/metrics.js +0 -142
- package/dist/metrics.js.map +0 -1
- package/dist/node-sandbox.d.ts +0 -32
- package/dist/node-sandbox.js +0 -92
- package/dist/node-sandbox.js.map +0 -1
- package/dist/python-sandbox.d.ts +0 -32
- package/dist/python-sandbox.js +0 -92
- package/dist/python-sandbox.js.map +0 -1
- package/dist/types.d.ts +0 -110
- package/dist/types.js +0 -88
- package/dist/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,138 +1,387 @@
|
|
|
1
|
-
#
|
|
1
|
+
# microsandbox
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lightweight VM sandboxes for Node.js — run AI agents and untrusted code with hardware-level isolation.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The `microsandbox` npm package provides native bindings to the [MicroSandbox](https://github.com/superradcompany/microsandbox) runtime. It spins up real microVMs (not containers) in under 100ms, runs standard OCI (Docker) images, and gives you full control over execution, filesystem, networking, and secrets — all from a simple async API.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Hardware isolation** — Each sandbox is a real VM with its own Linux kernel
|
|
10
|
+
- **Sub-100ms boot** — No daemon, no server setup, embedded directly in your app
|
|
11
|
+
- **OCI image support** — Pull and run images from Docker Hub, GHCR, ECR, or any OCI registry
|
|
12
|
+
- **Command execution** — Run commands with collected or streaming output, interactive shells
|
|
13
|
+
- **Guest filesystem access** — Read, write, list, copy files inside a running sandbox
|
|
14
|
+
- **Named volumes** — Persistent storage across sandbox restarts, with quotas
|
|
15
|
+
- **Network policies** — Public-only (default), allow-all, or fully airgapped
|
|
16
|
+
- **DNS filtering** — Block specific domains or domain suffixes
|
|
17
|
+
- **TLS interception** — Transparent HTTPS inspection and secret substitution
|
|
18
|
+
- **Secrets** — Credentials that never enter the VM; placeholder substitution at the network layer
|
|
19
|
+
- **Port publishing** — Expose guest TCP/UDP services on host ports
|
|
20
|
+
- **Rootfs patches** — Modify the filesystem before the VM boots
|
|
21
|
+
- **Detached mode** — Sandboxes can outlive the Node.js process
|
|
22
|
+
- **Metrics** — CPU, memory, disk I/O, and network I/O per sandbox
|
|
10
23
|
|
|
11
24
|
## Requirements
|
|
12
25
|
|
|
13
|
-
- Node.js
|
|
14
|
-
-
|
|
26
|
+
- **Node.js** >= 18
|
|
27
|
+
- **Linux** with KVM enabled, or **macOS** with Apple Silicon (M-series)
|
|
28
|
+
|
|
29
|
+
## Supported Platforms
|
|
15
30
|
|
|
16
|
-
|
|
31
|
+
| Platform | Architecture | Package |
|
|
32
|
+
|----------|-------------|---------|
|
|
33
|
+
| macOS | ARM64 (Apple Silicon) | `@superradcompany/microsandbox-darwin-arm64` |
|
|
34
|
+
| Linux | x86_64 | `@superradcompany/microsandbox-linux-x64-gnu` |
|
|
35
|
+
| Linux | ARM64 | `@superradcompany/microsandbox-linux-arm64-gnu` |
|
|
17
36
|
|
|
18
|
-
|
|
37
|
+
Platform-specific binaries are installed automatically via optional dependencies.
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install microsandbox
|
|
22
43
|
```
|
|
23
44
|
|
|
45
|
+
The postinstall script automatically downloads the `msb` binary and `libkrunfw` library to `~/.microsandbox/`.
|
|
46
|
+
|
|
24
47
|
## Quick Start
|
|
25
48
|
|
|
26
|
-
|
|
49
|
+
```typescript
|
|
50
|
+
import { Sandbox } from "microsandbox";
|
|
51
|
+
|
|
52
|
+
// Create a sandbox from an OCI image.
|
|
53
|
+
const sandbox = await Sandbox.create({
|
|
54
|
+
name: "my-sandbox",
|
|
55
|
+
image: "alpine:latest",
|
|
56
|
+
cpus: 1,
|
|
57
|
+
memoryMib: 512,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Run a command.
|
|
61
|
+
const output = await sandbox.shell("echo 'Hello from microsandbox!'");
|
|
62
|
+
console.log(output.stdout());
|
|
63
|
+
|
|
64
|
+
// Stop the sandbox.
|
|
65
|
+
await sandbox.stopAndWait();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Examples
|
|
69
|
+
|
|
70
|
+
### Command Execution
|
|
27
71
|
|
|
28
72
|
```typescript
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
import { Sandbox } from "microsandbox";
|
|
74
|
+
|
|
75
|
+
const sandbox = await Sandbox.create({
|
|
76
|
+
name: "exec-demo",
|
|
77
|
+
image: "python:3.12",
|
|
78
|
+
replace: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Collected output.
|
|
82
|
+
const result = await sandbox.exec("python3", ["-c", "print(1 + 1)"]);
|
|
83
|
+
console.log(result.stdout()); // "2\n"
|
|
84
|
+
console.log(result.code); // 0
|
|
85
|
+
|
|
86
|
+
// Shell command (pipes, redirects, etc.).
|
|
87
|
+
const output = await sandbox.shell("echo hello && pwd");
|
|
88
|
+
console.log(output.stdout());
|
|
89
|
+
|
|
90
|
+
// Full configuration.
|
|
91
|
+
const configured = await sandbox.execWithConfig({
|
|
92
|
+
cmd: "python3",
|
|
93
|
+
args: ["script.py"],
|
|
94
|
+
cwd: "/app",
|
|
95
|
+
env: { PYTHONPATH: "/app/lib" },
|
|
96
|
+
timeoutMs: 30000,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Streaming output.
|
|
100
|
+
const handle = await sandbox.execStream("tail", ["-f", "/var/log/app.log"]);
|
|
101
|
+
let event;
|
|
102
|
+
while ((event = await handle.recv()) !== null) {
|
|
103
|
+
if (event.eventType === "stdout") process.stdout.write(event.data);
|
|
46
104
|
}
|
|
47
105
|
|
|
48
|
-
|
|
106
|
+
await sandbox.stopAndWait();
|
|
49
107
|
```
|
|
50
108
|
|
|
51
|
-
|
|
109
|
+
### Filesystem Operations
|
|
52
110
|
|
|
53
|
-
|
|
111
|
+
```typescript
|
|
112
|
+
const fs = sandbox.fs();
|
|
113
|
+
|
|
114
|
+
// Write and read files.
|
|
115
|
+
await fs.write("/tmp/config.json", Buffer.from('{"debug": true}'));
|
|
116
|
+
const content = await fs.readString("/tmp/config.json");
|
|
54
117
|
|
|
55
|
-
|
|
56
|
-
|
|
118
|
+
// List a directory.
|
|
119
|
+
const entries = await fs.list("/etc");
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
console.log(`${entry.path} (${entry.kind})`);
|
|
122
|
+
}
|
|
57
123
|
|
|
58
|
-
|
|
124
|
+
// Copy between host and guest.
|
|
125
|
+
await fs.copyFromHost("./local-file.txt", "/tmp/file.txt");
|
|
126
|
+
await fs.copyToHost("/tmp/output.txt", "./output.txt");
|
|
59
127
|
|
|
60
|
-
|
|
128
|
+
// Check existence and metadata.
|
|
129
|
+
if (await fs.exists("/tmp/config.json")) {
|
|
130
|
+
const meta = await fs.stat("/tmp/config.json");
|
|
131
|
+
console.log(`size: ${meta.size}, kind: ${meta.kind}`);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
61
134
|
|
|
62
|
-
|
|
135
|
+
### Named Volumes
|
|
63
136
|
|
|
64
137
|
```typescript
|
|
65
|
-
import {
|
|
138
|
+
import { Sandbox, Volume, Mount } from "microsandbox";
|
|
139
|
+
|
|
140
|
+
// Create a 100 MiB named volume.
|
|
141
|
+
const data = await Volume.create({ name: "my-data", quotaMib: 100 });
|
|
142
|
+
|
|
143
|
+
// Mount it in a sandbox.
|
|
144
|
+
const writer = await Sandbox.create({
|
|
145
|
+
name: "writer",
|
|
146
|
+
image: "alpine:latest",
|
|
147
|
+
volumes: { "/data": Mount.named(data.name) },
|
|
148
|
+
replace: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await writer.shell("echo 'hello' > /data/message.txt");
|
|
152
|
+
await writer.stopAndWait();
|
|
153
|
+
|
|
154
|
+
// Mount the same volume in another sandbox (read-only).
|
|
155
|
+
const reader = await Sandbox.create({
|
|
156
|
+
name: "reader",
|
|
157
|
+
image: "alpine:latest",
|
|
158
|
+
volumes: { "/data": Mount.named(data.name, { readonly: true }) },
|
|
159
|
+
replace: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const output = await reader.shell("cat /data/message.txt");
|
|
163
|
+
console.log(output.stdout()); // "hello\n"
|
|
164
|
+
|
|
165
|
+
await reader.stopAndWait();
|
|
166
|
+
|
|
167
|
+
// Cleanup.
|
|
168
|
+
await Sandbox.remove("writer");
|
|
169
|
+
await Sandbox.remove("reader");
|
|
170
|
+
await Volume.remove("my-data");
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Network Policies
|
|
66
174
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
175
|
+
```typescript
|
|
176
|
+
import { Sandbox, NetworkPolicy } from "microsandbox";
|
|
177
|
+
|
|
178
|
+
// Default: public internet only (blocks private ranges).
|
|
179
|
+
const publicOnly = await Sandbox.create({
|
|
180
|
+
name: "public",
|
|
181
|
+
image: "alpine:latest",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Fully airgapped.
|
|
185
|
+
const isolated = await Sandbox.create({
|
|
186
|
+
name: "isolated",
|
|
187
|
+
image: "alpine:latest",
|
|
188
|
+
network: NetworkPolicy.none(),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Unrestricted.
|
|
192
|
+
const open = await Sandbox.create({
|
|
193
|
+
name: "open",
|
|
194
|
+
image: "alpine:latest",
|
|
195
|
+
network: NetworkPolicy.allowAll(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// DNS filtering.
|
|
199
|
+
const filtered = await Sandbox.create({
|
|
200
|
+
name: "filtered",
|
|
201
|
+
image: "alpine:latest",
|
|
202
|
+
network: {
|
|
203
|
+
blockDomains: ["blocked.example.com"],
|
|
204
|
+
blockDomainSuffixes: [".evil.com"],
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
```
|
|
73
208
|
|
|
74
|
-
|
|
75
|
-
const sandbox = await NodeSandbox.create(options);
|
|
209
|
+
### Port Publishing
|
|
76
210
|
|
|
77
|
-
|
|
78
|
-
await
|
|
211
|
+
```typescript
|
|
212
|
+
const sandbox = await Sandbox.create({
|
|
213
|
+
name: "web",
|
|
214
|
+
image: "python:3.12",
|
|
215
|
+
ports: { "8080": 80 }, // host:8080 -> guest:80
|
|
216
|
+
});
|
|
79
217
|
```
|
|
80
218
|
|
|
81
|
-
|
|
219
|
+
### Secrets
|
|
82
220
|
|
|
83
|
-
|
|
221
|
+
Secrets use placeholder substitution — the real value never enters the VM. It is only swapped in at the network layer for HTTPS requests to allowed hosts.
|
|
84
222
|
|
|
85
223
|
```typescript
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
224
|
+
import { Sandbox, Secret } from "microsandbox";
|
|
225
|
+
|
|
226
|
+
const sandbox = await Sandbox.create({
|
|
227
|
+
name: "agent",
|
|
228
|
+
image: "python:3.12",
|
|
229
|
+
secrets: [
|
|
230
|
+
Secret.env("OPENAI_API_KEY", {
|
|
231
|
+
value: process.env.OPENAI_API_KEY!,
|
|
232
|
+
allowHosts: ["api.openai.com"],
|
|
233
|
+
}),
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Guest sees: OPENAI_API_KEY=$MSB_OPENAI_API_KEY (a placeholder)
|
|
238
|
+
// HTTPS to api.openai.com: placeholder is transparently replaced with the real key
|
|
239
|
+
// HTTPS to any other host with the placeholder: request is blocked
|
|
89
240
|
```
|
|
90
241
|
|
|
91
|
-
|
|
242
|
+
### Rootfs Patches
|
|
92
243
|
|
|
93
|
-
|
|
244
|
+
Modify the filesystem before the VM boots:
|
|
94
245
|
|
|
95
246
|
```typescript
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
247
|
+
import { Patch, Sandbox } from "microsandbox";
|
|
248
|
+
|
|
249
|
+
const sandbox = await Sandbox.create({
|
|
250
|
+
name: "patched",
|
|
251
|
+
image: "alpine:latest",
|
|
252
|
+
patches: [
|
|
253
|
+
Patch.text("/etc/greeting.txt", "Hello!\n"),
|
|
254
|
+
Patch.mkdir("/app", { mode: 0o755 }),
|
|
255
|
+
Patch.text("/app/config.json", '{"debug": true}', { mode: 0o644 }),
|
|
256
|
+
Patch.copyDir("./scripts", "/app/scripts"),
|
|
257
|
+
Patch.append("/etc/hosts", "127.0.0.1 myapp.local\n"),
|
|
258
|
+
],
|
|
259
|
+
});
|
|
104
260
|
```
|
|
105
261
|
|
|
106
|
-
|
|
262
|
+
### Detached Mode
|
|
263
|
+
|
|
264
|
+
Sandboxes in detached mode survive the Node.js process:
|
|
107
265
|
|
|
108
266
|
```typescript
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
267
|
+
// Create and detach.
|
|
268
|
+
const sandbox = await Sandbox.createDetached({
|
|
269
|
+
name: "background",
|
|
270
|
+
image: "python:3.12",
|
|
271
|
+
});
|
|
272
|
+
await sandbox.detach();
|
|
273
|
+
|
|
274
|
+
// Later, from another process:
|
|
275
|
+
const handle = await Sandbox.get("background");
|
|
276
|
+
const reconnected = await handle.connect();
|
|
277
|
+
const output = await reconnected.shell("echo reconnected");
|
|
120
278
|
```
|
|
121
279
|
|
|
122
|
-
|
|
280
|
+
### TLS Interception
|
|
123
281
|
|
|
124
|
-
|
|
282
|
+
```typescript
|
|
283
|
+
const sandbox = await Sandbox.create({
|
|
284
|
+
name: "tls-inspect",
|
|
285
|
+
image: "python:3.12",
|
|
286
|
+
network: {
|
|
287
|
+
tls: {
|
|
288
|
+
bypass: ["*.googleapis.com"],
|
|
289
|
+
verifyUpstream: true,
|
|
290
|
+
interceptedPorts: [443],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Metrics
|
|
125
297
|
|
|
126
|
-
```
|
|
127
|
-
|
|
298
|
+
```typescript
|
|
299
|
+
import { Sandbox, allSandboxMetrics } from "microsandbox";
|
|
300
|
+
|
|
301
|
+
const sandbox = await Sandbox.create({
|
|
302
|
+
name: "metrics-demo",
|
|
303
|
+
image: "python:3.12",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Per-sandbox metrics.
|
|
307
|
+
const m = await sandbox.metrics();
|
|
308
|
+
console.log(`CPU: ${m.cpuPercent.toFixed(1)}%`);
|
|
309
|
+
console.log(`Memory: ${(m.memoryBytes / 1024 / 1024).toFixed(1)} MiB`);
|
|
310
|
+
console.log(`Uptime: ${(m.uptimeMs / 1000).toFixed(1)}s`);
|
|
311
|
+
|
|
312
|
+
// All sandboxes at once.
|
|
313
|
+
const all = await allSandboxMetrics();
|
|
314
|
+
for (const [name, metrics] of Object.entries(all)) {
|
|
315
|
+
console.log(`${name}: ${metrics.cpuPercent.toFixed(1)}%`);
|
|
316
|
+
}
|
|
128
317
|
```
|
|
129
318
|
|
|
130
|
-
###
|
|
319
|
+
### Runtime Setup
|
|
131
320
|
|
|
132
|
-
```
|
|
133
|
-
|
|
321
|
+
```typescript
|
|
322
|
+
import { isInstalled, install } from "microsandbox";
|
|
323
|
+
|
|
324
|
+
if (!isInstalled()) {
|
|
325
|
+
await install(); // Downloads msb + libkrunfw to ~/.microsandbox/
|
|
326
|
+
}
|
|
134
327
|
```
|
|
135
328
|
|
|
329
|
+
## API Reference
|
|
330
|
+
|
|
331
|
+
### Classes
|
|
332
|
+
|
|
333
|
+
| Class | Description |
|
|
334
|
+
|-------|-------------|
|
|
335
|
+
| `Sandbox` | Live handle to a running sandbox — lifecycle, execution, filesystem |
|
|
336
|
+
| `SandboxHandle` | Lightweight database handle — use `connect()` or `start()` to get a live `Sandbox` |
|
|
337
|
+
| `ExecOutput` | Captured stdout/stderr with exit status |
|
|
338
|
+
| `ExecHandle` | Streaming execution handle — call `recv()` for events |
|
|
339
|
+
| `ExecSink` | Writable stdin channel for streaming exec |
|
|
340
|
+
| `SandboxFs` | Guest filesystem operations (read, write, list, copy, stat) |
|
|
341
|
+
| `Volume` | Persistent named volume |
|
|
342
|
+
| `VolumeHandle` | Lightweight volume handle from the database |
|
|
343
|
+
|
|
344
|
+
### Factories
|
|
345
|
+
|
|
346
|
+
| Class | Description |
|
|
347
|
+
|-------|-------------|
|
|
348
|
+
| `Mount` | Volume mount configuration — `Mount.bind()`, `Mount.named()`, `Mount.tmpfs()` |
|
|
349
|
+
| `NetworkPolicy` | Network presets — `NetworkPolicy.none()`, `NetworkPolicy.publicOnly()`, `NetworkPolicy.allowAll()` |
|
|
350
|
+
| `Secret` | Secret entry — `Secret.env(name, options)` |
|
|
351
|
+
|
|
352
|
+
### Interfaces
|
|
353
|
+
|
|
354
|
+
| Interface | Description |
|
|
355
|
+
|-----------|-------------|
|
|
356
|
+
| `SandboxConfig` | Sandbox creation configuration |
|
|
357
|
+
| `ExecConfig` | Full command execution options (cmd, args, cwd, env, timeout, tty) |
|
|
358
|
+
| `NetworkConfig` | Network policy with rules, DNS blocking, TLS interception |
|
|
359
|
+
| `MountConfig` | Volume mount (bind, named, or tmpfs) |
|
|
360
|
+
| `PatchConfig` | Pre-boot filesystem modification |
|
|
361
|
+
| `VolumeConfig` | Volume creation options (name, quota, labels) |
|
|
362
|
+
| `SecretEntry` / `SecretEnvOptions` | Secret binding to env var with host allowlist |
|
|
363
|
+
| `ExecEvent` | Stream event: `"started"`, `"stdout"`, `"stderr"`, `"exited"` |
|
|
364
|
+
| `ExitStatus` | Exit code and success flag |
|
|
365
|
+
| `FsEntry` / `FsMetadata` | Filesystem entry info and metadata |
|
|
366
|
+
| `SandboxInfo` | Sandbox listing info (name, status, timestamps) |
|
|
367
|
+
| `SandboxMetrics` | Resource metrics (CPU, memory, disk I/O, network I/O, uptime) |
|
|
368
|
+
| `TlsConfig` | TLS interception options (bypass domains, upstream verification) |
|
|
369
|
+
|
|
370
|
+
### Functions
|
|
371
|
+
|
|
372
|
+
| Function | Description |
|
|
373
|
+
|----------|-------------|
|
|
374
|
+
| `isInstalled()` | Check if `msb` and `libkrunfw` are available |
|
|
375
|
+
| `install()` | Download and install runtime dependencies |
|
|
376
|
+
| `allSandboxMetrics()` | Get metrics for all running sandboxes |
|
|
377
|
+
|
|
378
|
+
### Enums
|
|
379
|
+
|
|
380
|
+
| Enum | Values |
|
|
381
|
+
|------|--------|
|
|
382
|
+
| `LogLevel` | `"trace"`, `"debug"`, `"info"`, `"warn"`, `"error"` |
|
|
383
|
+
| `PullPolicy` | `"always"`, `"if-missing"`, `"never"` |
|
|
384
|
+
|
|
136
385
|
## License
|
|
137
386
|
|
|
138
|
-
Apache
|
|
387
|
+
Apache-2.0
|