@torkbot/sandbox 0.1.0-pre.0 → 0.1.0

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 CHANGED
@@ -1,3 +1,240 @@
1
- # @torkbot/sandbox
1
+ # Sandbox
2
2
 
3
- Bootstrap placeholder for npm trusted publishing setup. This is not a usable release.
3
+ Sandbox is a TypeScript-first Node.js library for spawning libkrun-backed microVMs.
4
+
5
+ The target shape is:
6
+
7
+ - boot a guest from a prebuilt read-only rootfs artifact, likely EROFS,
8
+ - mount host-implemented virtual filesystems,
9
+ - intercept guest HTTP request headers through host TypeScript hooks,
10
+ - communicate with guest init over a bidirectional transport,
11
+ - ship as a statically linked host artifact.
12
+
13
+ ```ts
14
+ import {
15
+ acceptPublicInternet,
16
+ acceptTcp,
17
+ binding,
18
+ linuxOverlayFs,
19
+ mount,
20
+ prebuiltRootfs,
21
+ projectInit,
22
+ projectKernel,
23
+ scratchFs,
24
+ createSandbox,
25
+ type SandboxWritableFileSystem,
26
+ } from "@torkbot/sandbox";
27
+
28
+ declare const workspaceFs: SandboxWritableFileSystem;
29
+
30
+ await using sandbox = createSandbox({
31
+ kernel: projectKernel(),
32
+ init: projectInit(),
33
+ rootfs: linuxOverlayFs({
34
+ lower: prebuiltRootfs("dist/rootfs/sandbox.erofs", { format: "erofs" }),
35
+ upper: scratchFs(),
36
+ }),
37
+
38
+ mounts: [
39
+ mount("/sandbox", {
40
+ async stat(path) {
41
+ if (path === "/") {
42
+ return {
43
+ type: "directory",
44
+ sizeBytes: null,
45
+ mediaType: null,
46
+ modifiedAtMs: null,
47
+ };
48
+ }
49
+
50
+ if (path === "/status.json") {
51
+ const body = JSON.stringify({ ready: true });
52
+ return {
53
+ type: "file",
54
+ sizeBytes: Buffer.byteLength(body),
55
+ mediaType: "application/json",
56
+ modifiedAtMs: null,
57
+ };
58
+ }
59
+
60
+ throw new Error(`missing path ${path}`);
61
+ },
62
+
63
+ async list(path) {
64
+ if (path !== "/") throw new Error(`missing directory ${path}`);
65
+ return [{ name: "status.json", type: "file" }];
66
+ },
67
+
68
+ async read(input) {
69
+ if (input.path !== "/status.json") {
70
+ throw new Error(`unknown virtual file: ${input.path}`);
71
+ }
72
+
73
+ return Buffer.from(JSON.stringify({ ready: true }));
74
+ },
75
+ }),
76
+ ],
77
+
78
+ bindings: [
79
+ binding("/workspace", workspaceFs),
80
+ ],
81
+
82
+ network: {
83
+ outbound: {
84
+ policy: "deny",
85
+ rules: [
86
+ acceptTcp({ cidr: "127.0.0.1/32", ports: [8080] }),
87
+ acceptPublicInternet({ ports: [443] }),
88
+ ],
89
+ },
90
+ },
91
+ });
92
+
93
+ sandbox.http.onRequest({ origin: "https://api.github.com" }, (request) => {
94
+ request.headers.set("authorization", `Bearer ${process.env.GITHUB_TOKEN}`);
95
+ });
96
+
97
+ await using vm = await sandbox.run();
98
+ ```
99
+
100
+ Incremental guest operations are explicit:
101
+
102
+ ```ts
103
+ const result = await vm.control.exec({
104
+ id: "tests",
105
+ argv: ["node", "--test", "test/**/*.test.ts"],
106
+ });
107
+
108
+ if (result.exitCode !== 0) {
109
+ throw new Error(result.stderr);
110
+ }
111
+ ```
112
+
113
+ Mounted filesystems expose both the raw callback shape and a host-side tool surface for agent workflows:
114
+
115
+ ```ts
116
+ const sandboxProc = vm.mounts.virtualFs("/sandbox");
117
+ const statusBytes = await sandboxProc.read({
118
+ path: "/status.json",
119
+ signal: AbortSignal.timeout(1_000),
120
+ });
121
+
122
+ console.log(JSON.parse(Buffer.from(statusBytes).toString("utf8")));
123
+
124
+ const workspace = vm.mounts.host("/workspace");
125
+
126
+ const notes = await workspace.read({
127
+ path: "notes.md",
128
+ offset: 1,
129
+ limit: 80,
130
+ });
131
+
132
+ await workspace.write({
133
+ path: "plan.md",
134
+ content: "# Plan\n\nStart here.\n",
135
+ });
136
+
137
+ await workspace.patch({
138
+ path: "plan.md",
139
+ edits: [{ oldText: "Start here.", newText: "Ship the narrow slice." }],
140
+ });
141
+
142
+ const grep = await workspace.bash({
143
+ command: "grep \"Ship\" plan.md",
144
+ timeoutMs: 1_000,
145
+ });
146
+ ```
147
+
148
+ Root filesystems are immutable by default. A writable root is expressed as an explicit Linux overlayfs composition:
149
+
150
+ ```ts
151
+ await using sandbox = createSandbox({
152
+ kernel: projectKernel(),
153
+ init: projectInit(),
154
+ rootfs: linuxOverlayFs({
155
+ lower: prebuiltRootfs("dist/rootfs/base.erofs", { format: "erofs" }),
156
+ upper: scratchFs(),
157
+ }),
158
+ });
159
+
160
+ await using vm = await sandbox.run();
161
+
162
+ await vm.control.exec({
163
+ id: "install-toolchain",
164
+ argv: ["/bin/sh", "-lc", "apk add --no-cache git nodejs"],
165
+ });
166
+ ```
167
+
168
+ `mount(...)` means a guest-visible mount boundary. `binding(...)` means a host-side attachment point into the same filesystem abstraction and does not create a guest mount.
169
+
170
+ The guest contract is intentionally narrow:
171
+
172
+ - `/` is read-only unless the rootfs is a `linuxOverlayFs(...)` composition.
173
+ - `/sandbox` is implemented by the host.
174
+ - HTTP request-header hooks are registered in TypeScript and enforced by the Rust host data plane.
175
+ - Network egress starts from deny; outbound rules opt in the exact protocols, ranges, and ports the guest can reach.
176
+ - The HTTP interception CA is generated and injected by Sandbox. Callers provide request-header hooks, not certificate plumbing.
177
+
178
+ ## Design Targets
179
+
180
+ - no dynamic `libkrun` or `libkrunfw` dependency in the final host artifact,
181
+ - a signed `sandbox-host` process for the Node/Rust host boundary,
182
+ - custom guest init owned by this repo,
183
+ - implicit fd-backed host control sockets owned by Sandbox,
184
+ - avoid host filesystem coordination unless it is intrinsic to the artifact; prefer file descriptors, database handles, bytes, and async iterables over paths,
185
+ - build-time rootfs shaping, with prebuilt rootfs artifacts supplied at VM instantiation,
186
+ - root filesystem composition through small explicit primitives such as `linuxOverlayFs(...)` and `scratchFs()`, with lower and upper expressed as filesystem values,
187
+ - `mount(...)` only for guest-visible mounts; `binding(...)` only for host-side attachment points,
188
+ - programmable virtual filesystems backed by TypeScript callbacks,
189
+ - transparent HTTP interception with TypeScript request-header hooks,
190
+ - default-deny outbound networking with explicit accept rules for protocols, CIDR ranges, public internet reachability, and ports,
191
+ - Rust-native or statically linkable networking components; sidecar network daemons are references, not default runtime dependencies,
192
+ - macOS HVF entitlement signing verified as part of the integration test flow.
193
+
194
+ ## Repository Layout
195
+
196
+ - `src/`: TypeScript API consumed by Node.js callers.
197
+ - `crates/sandbox-host`: signed VM-host helper used for macOS HVF launch.
198
+ - `crates/sandbox`: Rust host implementation that owns the libkrun boundary and host services.
199
+ - `crates/sandbox-init`: custom guest init used to configure the guest before supervising untrusted code.
200
+ - `tests/e2e`: TypeScript e2e scenarios run directly by Node.js 24+ type stripping.
201
+
202
+ See [docs/architecture.md](docs/architecture.md) for the initial design.
203
+
204
+ Kernel artifacts are built separately from runtime VM creation. See [docs/kernel-build.md](docs/kernel-build.md) for the Docker-based `deps/libkrunfw` build entrypoint.
205
+ See [docs/testing-strategy.md](docs/testing-strategy.md) for the integration and e2e verification plan.
206
+
207
+ ## Publishing
208
+
209
+ The npm package is published as `@torkbot/sandbox`. It does not use post-install scripts. The root package contains the TypeScript API and declares platform artifacts as optional dependencies:
210
+
211
+ - `@torkbot/sandbox-darwin-arm64`
212
+ - `@torkbot/sandbox-linux-x64-gnu`
213
+
214
+ Each platform package contains the N-API binding and the `sandbox-host` helper for that target. Runtime artifact resolution only loads the installed optional dependency for the current platform. Local development uses the same layout by materializing the current platform package under `node_modules`.
215
+
216
+ ### macOS signing setup
217
+
218
+ For now, the macOS `sandbox-host` artifact is not Developer ID signed or notarized. This is an explicit, possibly temporary workaround for publishing before this project has an Apple Developer account.
219
+
220
+ macOS users must sign the installed helper locally before launching a VM:
221
+
222
+ ```sh
223
+ npx @torkbot/sandbox setup-macos
224
+ ```
225
+
226
+ This performs an ad-hoc local `codesign` with the `com.apple.security.hypervisor` entitlement required by Hypervisor.framework. It does not contact Apple and does not require an Apple Developer account. If a macOS user tries to launch a VM before running setup, Sandbox throws a runtime error that points back to this command.
227
+
228
+ The release workflow verifies the tag, builds platform packages on their native runners, publishes the platform packages first, and then publishes the root package. That keeps the installable root package from pointing at missing optional artifacts while staying as close as npm allows to a single coordinated release operation.
229
+
230
+ Local release packaging sanity check:
231
+
232
+ ```sh
233
+ npm run release:pack
234
+ ```
235
+
236
+ After rebuilding local native artifacts, refresh the local optional package layout with:
237
+
238
+ ```sh
239
+ npm run artifacts:link-current
240
+ ```
@@ -0,0 +1,12 @@
1
+ type SandboxTarget = {
2
+ readonly packageName: string;
3
+ readonly hostBinaryName: string;
4
+ readonly platform: NodeJS.Platform;
5
+ readonly arch: NodeJS.Architecture;
6
+ readonly libc?: "glibc";
7
+ };
8
+ export declare function currentSandboxTarget(): SandboxTarget;
9
+ export declare function hostBinaryPath(): string;
10
+ export declare function rawHostBinaryPath(): string;
11
+ export {};
12
+ //# sourceMappingURL=artifacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAKA,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,YAAY,CAAC;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAkBF,wBAAgB,oBAAoB,IAAI,aAAa,CAYpD;AAED,wBAAgB,cAAc,IAAI,MAAM,CAIvC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C"}
@@ -0,0 +1,77 @@
1
+ import { createRequire } from "node:module";
2
+ import { spawnSync } from "node:child_process";
3
+ const require = createRequire(import.meta.url);
4
+ const targets = [
5
+ {
6
+ packageName: "@torkbot/sandbox-darwin-arm64",
7
+ hostBinaryName: "sandbox-host",
8
+ platform: "darwin",
9
+ arch: "arm64",
10
+ },
11
+ {
12
+ packageName: "@torkbot/sandbox-linux-x64-gnu",
13
+ hostBinaryName: "sandbox-host",
14
+ platform: "linux",
15
+ arch: "x64",
16
+ libc: "glibc",
17
+ },
18
+ ];
19
+ export function currentSandboxTarget() {
20
+ const target = targets.find((candidate) => {
21
+ return candidate.platform === process.platform && candidate.arch === process.arch;
22
+ });
23
+ if (target === undefined) {
24
+ throw new Error(`unsupported native sandbox target: ${process.platform}-${process.arch}`);
25
+ }
26
+ return target;
27
+ }
28
+ export function hostBinaryPath() {
29
+ const path = rawHostBinaryPath();
30
+ assertMacosHostIsSigned(path);
31
+ return path;
32
+ }
33
+ export function rawHostBinaryPath() {
34
+ const target = currentSandboxTarget();
35
+ return resolveArtifactPath(target, target.hostBinaryName);
36
+ }
37
+ function resolveArtifactPath(target, artifactName) {
38
+ try {
39
+ return require.resolve(`${target.packageName}/${artifactName}`);
40
+ }
41
+ catch (error) {
42
+ const installError = error instanceof Error ? error.message : String(error);
43
+ throw new Error(`missing ${target.packageName} artifact ${artifactName}; reinstall @torkbot/sandbox for ${process.platform}-${process.arch}, or run npm run artifacts:link-current after building local artifacts. ${installError}`);
44
+ }
45
+ }
46
+ function assertMacosHostIsSigned(path) {
47
+ if (process.platform !== "darwin") {
48
+ return;
49
+ }
50
+ let entitlements;
51
+ const result = spawnSync("codesign", ["-d", "--entitlements", ":-", path], {
52
+ encoding: "utf8",
53
+ stdio: ["ignore", "pipe", "pipe"],
54
+ });
55
+ if (result.error !== undefined) {
56
+ throw new Error(macosSigningError(path, result.error.message));
57
+ }
58
+ if (result.status !== 0) {
59
+ throw new Error(macosSigningError(path, `${result.stdout}\n${result.stderr}`.trim()));
60
+ }
61
+ entitlements = `${result.stdout}\n${result.stderr}`;
62
+ if (!entitlements.includes("<key>com.apple.security.hypervisor</key>")) {
63
+ throw new Error(macosSigningError(path, "missing com.apple.security.hypervisor entitlement"));
64
+ }
65
+ }
66
+ function macosSigningError(path, detail) {
67
+ return [
68
+ "sandbox-host is not signed for macOS Hypervisor.framework access.",
69
+ "",
70
+ "Run this once after installing @torkbot/sandbox:",
71
+ " npx @torkbot/sandbox setup-macos",
72
+ "",
73
+ `Artifact: ${path}`,
74
+ `Reason: ${detail}`,
75
+ ].join("\n");
76
+ }
77
+ //# sourceMappingURL=artifacts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { hostBinaryPath, rawHostBinaryPath } from "./artifacts.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const macosHypervisorEntitlements = `<?xml version="1.0" encoding="UTF-8"?>
10
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
11
+ <plist version="1.0">
12
+ <dict>
13
+ <key>com.apple.security.hypervisor</key>
14
+ <true/>
15
+ </dict>
16
+ </plist>
17
+ `;
18
+ const command = process.argv[2];
19
+ if (command === "setup-macos") {
20
+ await setupMacos();
21
+ }
22
+ else {
23
+ console.error("usage: sandbox setup-macos");
24
+ process.exit(2);
25
+ }
26
+ async function setupMacos() {
27
+ if (process.platform !== "darwin") {
28
+ console.error("sandbox setup-macos is only needed on macOS.");
29
+ return;
30
+ }
31
+ const hostPath = rawHostBinaryPath();
32
+ const tempDir = await mkdtemp(join(tmpdir(), "torkbot-sandbox-entitlements-"));
33
+ const entitlementsPath = join(tempDir, "macos-hvf.plist");
34
+ try {
35
+ await writeFile(entitlementsPath, macosHypervisorEntitlements);
36
+ await execFileAsync("codesign", [
37
+ "--force",
38
+ "--sign",
39
+ "-",
40
+ "--entitlements",
41
+ entitlementsPath,
42
+ hostPath,
43
+ ]);
44
+ hostBinaryPath();
45
+ console.log(`Signed sandbox-host for macOS Hypervisor.framework access: ${hostPath}`);
46
+ }
47
+ finally {
48
+ await rm(tempDir, { recursive: true, force: true });
49
+ }
50
+ }
51
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEnE,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,MAAM,2BAA2B,GAAG;;;;;;;;CAQnC,CAAC;AAEF,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAEhC,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;IAC9B,MAAM,UAAU,EAAE,CAAC;AACrB,CAAC;KAAM,CAAC;IACN,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,UAAU;IACvB,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAC9D,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,iBAAiB,EAAE,CAAC;IACrC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,+BAA+B,CAAC,CAAC,CAAC;IAC/E,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;IAE1D,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,gBAAgB,EAAE,2BAA2B,CAAC,CAAC;QAC/D,MAAM,aAAa,CAAC,UAAU,EAAE;YAC9B,SAAS;YACT,QAAQ;YACR,GAAG;YACH,gBAAgB;YAChB,gBAAgB;YAChB,QAAQ;SACT,CAAC,CAAC;QAEH,cAAc,EAAE,CAAC;QACjB,OAAO,CAAC,GAAG,CAAC,8DAA8D,QAAQ,EAAE,CAAC,CAAC;IACxF,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;AACH,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { SandboxControlCommand, SandboxControlEvent } from "./index.ts";
2
+ export declare function encodeControlCommand(command: SandboxControlCommand): Uint8Array;
3
+ export declare function decodeControlEvent(packet: Uint8Array): SandboxControlEvent;
4
+ //# sourceMappingURL=control-codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control-codec.d.ts","sourceRoot":"","sources":["../src/control-codec.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE7E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,UAAU,CAU/E;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG,mBAAmB,CAwB1E"}
@@ -0,0 +1,88 @@
1
+ import { Binary, BSON } from "bson";
2
+ export function encodeControlCommand(command) {
3
+ switch (command.type) {
4
+ case "guest.exec":
5
+ return encodePacket({
6
+ type: "guest.exec",
7
+ id: command.id,
8
+ argv: [...command.argv],
9
+ env: Object.entries(command.env ?? {}).map(([key, value]) => ({ key, value })),
10
+ });
11
+ }
12
+ }
13
+ export function decodeControlEvent(packet) {
14
+ const document = decodePacket(packet);
15
+ const frameType = readString(document, "type");
16
+ switch (frameType) {
17
+ case "init.ready":
18
+ return {
19
+ type: "init.ready",
20
+ guest: {
21
+ root: { readonly: readBoolean(document, "rootReadonly") },
22
+ init: { name: readString(document, "initName") },
23
+ },
24
+ };
25
+ case "guest.exec.complete":
26
+ return {
27
+ type: "guest.exec.complete",
28
+ id: readString(document, "id"),
29
+ exitCode: readNumber(document, "exitCode"),
30
+ stdout: new TextDecoder().decode(readBytes(document, "stdout")),
31
+ stderr: new TextDecoder().decode(readBytes(document, "stderr")),
32
+ };
33
+ default:
34
+ throw new Error(`unknown control frame type: ${frameType}`);
35
+ }
36
+ }
37
+ function encodePacket(document) {
38
+ const frame = BSON.serialize(document);
39
+ const packet = new Uint8Array(4 + frame.byteLength);
40
+ new DataView(packet.buffer, packet.byteOffset, 4).setUint32(0, frame.byteLength, true);
41
+ packet.set(frame, 4);
42
+ return packet;
43
+ }
44
+ function decodePacket(packet) {
45
+ if (packet.byteLength < 4) {
46
+ throw new Error("control packet missing length prefix");
47
+ }
48
+ const frameLength = new DataView(packet.buffer, packet.byteOffset, 4).getUint32(0, true);
49
+ if (packet.byteLength < 4 + frameLength) {
50
+ throw new Error("control packet body is truncated");
51
+ }
52
+ if (packet.byteLength !== 4 + frameLength) {
53
+ throw new Error("control packet has trailing bytes");
54
+ }
55
+ return BSON.deserialize(packet.subarray(4));
56
+ }
57
+ function readString(document, key) {
58
+ const value = document[key];
59
+ if (typeof value !== "string") {
60
+ throw new Error(`control frame field must be a string: ${key}`);
61
+ }
62
+ return value;
63
+ }
64
+ function readBoolean(document, key) {
65
+ const value = document[key];
66
+ if (typeof value !== "boolean") {
67
+ throw new Error(`control frame field must be a boolean: ${key}`);
68
+ }
69
+ return value;
70
+ }
71
+ function readNumber(document, key) {
72
+ const value = document[key];
73
+ if (typeof value !== "number") {
74
+ throw new Error(`control frame field must be a number: ${key}`);
75
+ }
76
+ return value;
77
+ }
78
+ function readBytes(document, key) {
79
+ const value = document[key];
80
+ if (value instanceof Binary) {
81
+ return value.buffer;
82
+ }
83
+ if (value instanceof Uint8Array) {
84
+ return value;
85
+ }
86
+ throw new Error(`control frame field must be binary: ${key}`);
87
+ }
88
+ //# sourceMappingURL=control-codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control-codec.js","sourceRoot":"","sources":["../src/control-codec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { SandboxControl, SandboxControlCommand, SandboxControlEvent } from "./index.ts";
2
+ export interface HostControlChannel {
3
+ writeControlPacket(packet: Uint8Array): void;
4
+ tryReadControlPacket(): Uint8Array | null;
5
+ }
6
+ export declare class HostControlTransport implements SandboxControl {
7
+ #private;
8
+ readonly incoming: AsyncIterable<SandboxControlEvent>;
9
+ constructor(options?: {
10
+ readonly connected?: boolean;
11
+ readonly channel?: HostControlChannel;
12
+ });
13
+ send(message: SandboxControlCommand): Promise<void>;
14
+ exec(input: {
15
+ readonly id?: string;
16
+ readonly argv: readonly string[];
17
+ readonly env?: Record<string, string>;
18
+ }): Promise<Extract<SandboxControlEvent, {
19
+ type: "guest.exec.complete";
20
+ }>>;
21
+ close(): Promise<void>;
22
+ emit(event: SandboxControlEvent): void;
23
+ }
24
+ //# sourceMappingURL=control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,kBAAkB;IACjC,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IAC7C,oBAAoB,IAAI,UAAU,GAAG,IAAI,CAAC;CAC3C;AAED,qBAAa,oBAAqB,YAAW,cAAc;;IACzD,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC;IAWtD,YAAY,OAAO,GAAE;QACnB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC;KAClC,EAeL;IAEK,IAAI,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMxD;IAEK,IAAI,CAAC,KAAK,EAAE;QAChB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;QACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACvC,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,EAAE;QAAE,IAAI,EAAE,qBAAqB,CAAA;KAAE,CAAC,CAAC,CAqBzE;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ3B;IAED,IAAI,CAAC,KAAK,EAAE,mBAAmB,GAAG,IAAI,CAGrC;CAuEF"}