@rivet-dev/agentos-runtime-core 0.0.0-glibc.4af31c1

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.
Files changed (282) hide show
  1. package/README.md +7 -0
  2. package/commands/[ +0 -0
  3. package/commands/_stubs +0 -0
  4. package/commands/arch +0 -0
  5. package/commands/awk +0 -0
  6. package/commands/b2sum +0 -0
  7. package/commands/base32 +0 -0
  8. package/commands/base64 +0 -0
  9. package/commands/basename +0 -0
  10. package/commands/basenc +0 -0
  11. package/commands/bash +0 -0
  12. package/commands/cat +0 -0
  13. package/commands/chcon +0 -0
  14. package/commands/chgrp +0 -0
  15. package/commands/chmod +0 -0
  16. package/commands/chown +0 -0
  17. package/commands/chroot +0 -0
  18. package/commands/cksum +0 -0
  19. package/commands/codex +0 -0
  20. package/commands/codex-exec +0 -0
  21. package/commands/column +0 -0
  22. package/commands/comm +0 -0
  23. package/commands/cp +0 -0
  24. package/commands/curl +0 -0
  25. package/commands/cut +0 -0
  26. package/commands/date +0 -0
  27. package/commands/dd +0 -0
  28. package/commands/df +0 -0
  29. package/commands/diff +0 -0
  30. package/commands/dir +0 -0
  31. package/commands/dircolors +0 -0
  32. package/commands/dirname +0 -0
  33. package/commands/du +0 -0
  34. package/commands/echo +0 -0
  35. package/commands/egrep +0 -0
  36. package/commands/env +0 -0
  37. package/commands/envsubst +0 -0
  38. package/commands/expand +0 -0
  39. package/commands/expr +0 -0
  40. package/commands/factor +0 -0
  41. package/commands/false +0 -0
  42. package/commands/fd +0 -0
  43. package/commands/fgrep +0 -0
  44. package/commands/file +0 -0
  45. package/commands/find +0 -0
  46. package/commands/fmt +0 -0
  47. package/commands/fold +0 -0
  48. package/commands/git +0 -0
  49. package/commands/git-remote-http +0 -0
  50. package/commands/git-remote-https +0 -0
  51. package/commands/grep +0 -0
  52. package/commands/groups +0 -0
  53. package/commands/gunzip +0 -0
  54. package/commands/gzip +0 -0
  55. package/commands/head +0 -0
  56. package/commands/hostid +0 -0
  57. package/commands/hostname +0 -0
  58. package/commands/http-test +0 -0
  59. package/commands/http_get +0 -0
  60. package/commands/id +0 -0
  61. package/commands/install +0 -0
  62. package/commands/join +0 -0
  63. package/commands/jq +0 -0
  64. package/commands/kill +0 -0
  65. package/commands/link +0 -0
  66. package/commands/ln +0 -0
  67. package/commands/logname +0 -0
  68. package/commands/ls +0 -0
  69. package/commands/md5sum +0 -0
  70. package/commands/mkdir +0 -0
  71. package/commands/mkfifo +0 -0
  72. package/commands/mknod +0 -0
  73. package/commands/mktemp +0 -0
  74. package/commands/more +0 -0
  75. package/commands/mv +0 -0
  76. package/commands/nice +0 -0
  77. package/commands/nl +0 -0
  78. package/commands/nohup +0 -0
  79. package/commands/nproc +0 -0
  80. package/commands/numfmt +0 -0
  81. package/commands/od +0 -0
  82. package/commands/paste +0 -0
  83. package/commands/pathchk +0 -0
  84. package/commands/pinky +0 -0
  85. package/commands/printenv +0 -0
  86. package/commands/printf +0 -0
  87. package/commands/ptx +0 -0
  88. package/commands/pwd +0 -0
  89. package/commands/readlink +0 -0
  90. package/commands/realpath +0 -0
  91. package/commands/rev +0 -0
  92. package/commands/rg +0 -0
  93. package/commands/rm +0 -0
  94. package/commands/rmdir +0 -0
  95. package/commands/runcon +0 -0
  96. package/commands/sed +0 -0
  97. package/commands/seq +0 -0
  98. package/commands/sh +0 -0
  99. package/commands/sha1sum +0 -0
  100. package/commands/sha224sum +0 -0
  101. package/commands/sha256sum +0 -0
  102. package/commands/sha384sum +0 -0
  103. package/commands/sha512sum +0 -0
  104. package/commands/shred +0 -0
  105. package/commands/shuf +0 -0
  106. package/commands/sleep +0 -0
  107. package/commands/sort +0 -0
  108. package/commands/spawn-test-host +0 -0
  109. package/commands/split +0 -0
  110. package/commands/sqlite3 +0 -0
  111. package/commands/stat +0 -0
  112. package/commands/stdbuf +0 -0
  113. package/commands/strings +0 -0
  114. package/commands/stty +0 -0
  115. package/commands/sum +0 -0
  116. package/commands/sync +0 -0
  117. package/commands/tac +0 -0
  118. package/commands/tail +0 -0
  119. package/commands/tar +0 -0
  120. package/commands/tee +0 -0
  121. package/commands/test +0 -0
  122. package/commands/timeout +0 -0
  123. package/commands/touch +0 -0
  124. package/commands/tr +0 -0
  125. package/commands/tree +0 -0
  126. package/commands/true +0 -0
  127. package/commands/truncate +0 -0
  128. package/commands/tsort +0 -0
  129. package/commands/tty +0 -0
  130. package/commands/uname +0 -0
  131. package/commands/unexpand +0 -0
  132. package/commands/uniq +0 -0
  133. package/commands/unlink +0 -0
  134. package/commands/unzip +0 -0
  135. package/commands/uptime +0 -0
  136. package/commands/users +0 -0
  137. package/commands/vdir +0 -0
  138. package/commands/wc +0 -0
  139. package/commands/which +0 -0
  140. package/commands/who +0 -0
  141. package/commands/whoami +0 -0
  142. package/commands/xargs +0 -0
  143. package/commands/xu +0 -0
  144. package/commands/yes +0 -0
  145. package/commands/yq +0 -0
  146. package/commands/zcat +0 -0
  147. package/commands/zip +0 -0
  148. package/dist/binary.d.ts +4 -0
  149. package/dist/binary.js +25 -0
  150. package/dist/bytes.d.ts +2 -0
  151. package/dist/bytes.js +6 -0
  152. package/dist/callbacks.d.ts +41 -0
  153. package/dist/callbacks.js +94 -0
  154. package/dist/cargo.d.ts +2 -0
  155. package/dist/cargo.js +142 -0
  156. package/dist/correlation.d.ts +10 -0
  157. package/dist/correlation.js +49 -0
  158. package/dist/descriptors.d.ts +84 -0
  159. package/dist/descriptors.js +60 -0
  160. package/dist/event-buffer.d.ts +90 -0
  161. package/dist/event-buffer.js +313 -0
  162. package/dist/ext.d.ts +7 -0
  163. package/dist/ext.js +13 -0
  164. package/dist/filesystem.d.ts +41 -0
  165. package/dist/filesystem.js +70 -0
  166. package/dist/frame-payload-codec.d.ts +8 -0
  167. package/dist/frame-payload-codec.js +14 -0
  168. package/dist/frame-rpc.d.ts +40 -0
  169. package/dist/frame-rpc.js +81 -0
  170. package/dist/frame-stream.d.ts +34 -0
  171. package/dist/frame-stream.js +109 -0
  172. package/dist/framing.d.ts +8 -0
  173. package/dist/framing.js +23 -0
  174. package/dist/generated/AcpLimitsConfig.d.ts +4 -0
  175. package/dist/generated/AcpLimitsConfig.js +2 -0
  176. package/dist/generated/CreateVmConfig.d.ts +26 -0
  177. package/dist/generated/CreateVmConfig.js +1 -0
  178. package/dist/generated/FsPermissionRule.d.ts +6 -0
  179. package/dist/generated/FsPermissionRule.js +1 -0
  180. package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
  181. package/dist/generated/FsPermissionRuleSet.js +1 -0
  182. package/dist/generated/FsPermissionScope.d.ts +3 -0
  183. package/dist/generated/FsPermissionScope.js +1 -0
  184. package/dist/generated/HttpLimitsConfig.d.ts +3 -0
  185. package/dist/generated/HttpLimitsConfig.js +2 -0
  186. package/dist/generated/JsModuleResolution.d.ts +1 -0
  187. package/dist/generated/JsModuleResolution.js +2 -0
  188. package/dist/generated/JsRuntimeConfig.d.ts +31 -0
  189. package/dist/generated/JsRuntimeConfig.js +1 -0
  190. package/dist/generated/JsRuntimeLimitsConfig.d.ts +11 -0
  191. package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
  192. package/dist/generated/JsRuntimePlatform.d.ts +1 -0
  193. package/dist/generated/JsRuntimePlatform.js +2 -0
  194. package/dist/generated/MountPluginDescriptor.d.ts +4 -0
  195. package/dist/generated/MountPluginDescriptor.js +2 -0
  196. package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
  197. package/dist/generated/NativeRootFilesystemConfig.js +1 -0
  198. package/dist/generated/PatternPermissionRule.d.ts +6 -0
  199. package/dist/generated/PatternPermissionRule.js +1 -0
  200. package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
  201. package/dist/generated/PatternPermissionRuleSet.js +1 -0
  202. package/dist/generated/PatternPermissionScope.d.ts +3 -0
  203. package/dist/generated/PatternPermissionScope.js +1 -0
  204. package/dist/generated/PermissionMode.d.ts +1 -0
  205. package/dist/generated/PermissionMode.js +2 -0
  206. package/dist/generated/PermissionsPolicy.d.ts +10 -0
  207. package/dist/generated/PermissionsPolicy.js +1 -0
  208. package/dist/generated/PluginLimitsConfig.d.ts +4 -0
  209. package/dist/generated/PluginLimitsConfig.js +2 -0
  210. package/dist/generated/PythonLimitsConfig.d.ts +6 -0
  211. package/dist/generated/PythonLimitsConfig.js +2 -0
  212. package/dist/generated/ResourceLimitsConfig.d.ts +24 -0
  213. package/dist/generated/ResourceLimitsConfig.js +2 -0
  214. package/dist/generated/RootFilesystemConfig.d.ts +9 -0
  215. package/dist/generated/RootFilesystemConfig.js +1 -0
  216. package/dist/generated/RootFilesystemEntry.d.ts +13 -0
  217. package/dist/generated/RootFilesystemEntry.js +1 -0
  218. package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
  219. package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
  220. package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
  221. package/dist/generated/RootFilesystemEntryKind.js +2 -0
  222. package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
  223. package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
  224. package/dist/generated/RootFilesystemMode.d.ts +1 -0
  225. package/dist/generated/RootFilesystemMode.js +2 -0
  226. package/dist/generated/ToolLimitsConfig.d.ts +10 -0
  227. package/dist/generated/ToolLimitsConfig.js +2 -0
  228. package/dist/generated/VmDnsConfig.d.ts +6 -0
  229. package/dist/generated/VmDnsConfig.js +2 -0
  230. package/dist/generated/VmLimitsConfig.d.ts +18 -0
  231. package/dist/generated/VmLimitsConfig.js +1 -0
  232. package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
  233. package/dist/generated/VmListenPolicyConfig.js +2 -0
  234. package/dist/generated/WasmLimitsConfig.d.ts +7 -0
  235. package/dist/generated/WasmLimitsConfig.js +2 -0
  236. package/dist/generated-protocol.d.ts +1196 -0
  237. package/dist/generated-protocol.js +3286 -0
  238. package/dist/index.d.ts +29 -0
  239. package/dist/index.js +28 -0
  240. package/dist/json.d.ts +2 -0
  241. package/dist/json.js +20 -0
  242. package/dist/kernel-proxy.d.ts +166 -0
  243. package/dist/kernel-proxy.js +1777 -0
  244. package/dist/native-client.d.ts +42 -0
  245. package/dist/native-client.js +126 -0
  246. package/dist/node-runtime-options-schema.d.ts +117 -0
  247. package/dist/node-runtime-options-schema.js +121 -0
  248. package/dist/node-runtime.d.ts +593 -0
  249. package/dist/node-runtime.js +914 -0
  250. package/dist/numbers.d.ts +1 -0
  251. package/dist/numbers.js +8 -0
  252. package/dist/ownership.d.ts +18 -0
  253. package/dist/ownership.js +77 -0
  254. package/dist/permissions.d.ts +29 -0
  255. package/dist/permissions.js +68 -0
  256. package/dist/process.d.ts +22 -0
  257. package/dist/process.js +97 -0
  258. package/dist/protocol-client.d.ts +48 -0
  259. package/dist/protocol-client.js +181 -0
  260. package/dist/protocol-frames.d.ts +68 -0
  261. package/dist/protocol-frames.js +139 -0
  262. package/dist/protocol-maps.d.ts +31 -0
  263. package/dist/protocol-maps.js +273 -0
  264. package/dist/protocol-schema.d.ts +10 -0
  265. package/dist/protocol-schema.js +11 -0
  266. package/dist/request-payloads.d.ts +171 -0
  267. package/dist/request-payloads.js +269 -0
  268. package/dist/response-payloads.d.ts +186 -0
  269. package/dist/response-payloads.js +250 -0
  270. package/dist/sidecar-client.d.ts +17 -0
  271. package/dist/sidecar-client.js +1 -0
  272. package/dist/sidecar-errors.d.ts +15 -0
  273. package/dist/sidecar-errors.js +30 -0
  274. package/dist/sidecar-process.d.ts +356 -0
  275. package/dist/sidecar-process.js +1008 -0
  276. package/dist/state.d.ts +40 -0
  277. package/dist/state.js +44 -0
  278. package/dist/test-runtime.d.ts +563 -0
  279. package/dist/test-runtime.js +2171 -0
  280. package/dist/vm-config.d.ts +31 -0
  281. package/dist/vm-config.js +1 -0
  282. package/package.json +204 -0
@@ -0,0 +1,914 @@
1
+ /**
2
+ * NodeRuntime — ergonomic façade for running guest JavaScript end-to-end.
3
+ *
4
+ * Boots a fully virtualized VM (via the native sidecar) and runs guest Node
5
+ * programs with minimal boilerplate. All of the sidecar spawn, session
6
+ * handshake, VM creation, root filesystem bootstrap, runtime-driver mounting,
7
+ * and lifecycle waiting are hidden behind `NodeRuntime.create()`.
8
+ *
9
+ * ```ts
10
+ * const rt = await NodeRuntime.create();
11
+ * const { stdout, exitCode } = await rt.exec("console.log('hi', 1 + 1)");
12
+ * await rt.dispose();
13
+ * ```
14
+ *
15
+ * Guest code is written to an ESM module inside the VM and executed as
16
+ * `node <file>` through the kernel, so all execution stays inside the kernel
17
+ * isolation boundary — no host escapes, no real Node.js builtins for guest
18
+ * work.
19
+ */
20
+ import { existsSync } from "node:fs";
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { createInMemoryFileSystem, createKernel, createNodeRuntime, createWasmVmRuntime, NodeFileSystem, } from "./test-runtime.js";
24
+ import { parseNodeRuntimeCreateOptions } from "./node-runtime-options-schema.js";
25
+ export { resolveNodeRuntimeSidecarBinary } from "./test-runtime.js";
26
+ /** Repository root, used to locate the in-repo WASM command build output. */
27
+ const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
28
+ /**
29
+ * In-repo build output for the WASM coreutils/shell command binaries, produced
30
+ * by the Rust command build (`make -C registry/native wasm`). Only present in a
31
+ * developer checkout; preferred when it exists so local edits are picked up
32
+ * without re-vendoring.
33
+ */
34
+ const REPO_COMMANDS_DIR = path.join(REPO_ROOT, "registry/native/target/wasm32-wasip1/release/commands");
35
+ /**
36
+ * Commands vendored into the published `@rivet-dev/agentos-runtime-core` package by
37
+ * `scripts/copy-wasm-commands.mjs` (listed in `files` as `commands`). This is
38
+ * the directory a real `npm install secure-exec` resolves: from the compiled
39
+ * `dist/node-runtime.js` it sits at `<package>/commands`. This is the analogue
40
+ * of how the sidecar binary ships inside `@rivet-dev/agentos-runtime-sidecar`.
41
+ */
42
+ const BUNDLED_COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
43
+ /**
44
+ * Resolve the directory holding the WASM command binaries (the source of the
45
+ * guest `sh` the kernel needs to spawn any process). Precedence:
46
+ *
47
+ * 1. explicit `commandsDir` option,
48
+ * 2. `AGENTOS_WASM_COMMANDS_DIR` env var,
49
+ * 3. the in-repo build output (developer checkout), when present,
50
+ * 4. the commands vendored into the installed package (published installs).
51
+ *
52
+ * The in-repo path wins over the bundled copy so local development picks up
53
+ * freshly built commands without re-vendoring. A fresh `npm install` has no
54
+ * in-repo path, so it falls through to the bundled copy.
55
+ */
56
+ export function resolveNodeRuntimeCommandsDir(explicit) {
57
+ if (explicit !== undefined) {
58
+ return explicit;
59
+ }
60
+ const fromEnv = process.env.AGENTOS_WASM_COMMANDS_DIR;
61
+ if (fromEnv) {
62
+ return fromEnv;
63
+ }
64
+ if (existsSync(REPO_COMMANDS_DIR)) {
65
+ return REPO_COMMANDS_DIR;
66
+ }
67
+ return BUNDLED_COMMANDS_DIR;
68
+ }
69
+ /**
70
+ * Secure-by-default permission policy applied when the caller passes no
71
+ * `permissions`. Outward-facing capabilities are denied: there is **no network
72
+ * access** (and no host callbacks) by default — guest code cannot reach the
73
+ * network until you opt in. The filesystem, child-process, process, and env
74
+ * scopes are allowed because they are fully virtualized (the guest only ever
75
+ * sees the VM's in-memory filesystem and kernel-managed processes, never the
76
+ * real host) and are required for the runtime to execute a guest program at
77
+ * all. Tighten or widen any scope by passing your own `permissions`.
78
+ */
79
+ const DEFAULT_PERMISSIONS = {
80
+ fs: "allow",
81
+ childProcess: "allow",
82
+ process: "allow",
83
+ env: "allow",
84
+ network: "deny",
85
+ };
86
+ /** Guest path a `nodeModules` mount is projected at by default. */
87
+ const DEFAULT_NODE_MODULES_GUEST_PATH = "/tmp/node_modules";
88
+ let nextProgramId = 0;
89
+ let nextResidentRequestId = 0;
90
+ /**
91
+ * Guest preamble exposing `globalThis.callBinding(name, input?)`: an ergonomic
92
+ * async wrapper over the binding invocation path. It runs the registered binding
93
+ * as the guest would by hand (`<binding> --json <input>` through
94
+ * `node:child_process`), so it inherits every security property of that path:
95
+ * the `binding` permission scope, the binding's input-schema validation, and the
96
+ * host-side handler all still apply. It adds no new trust surface; it only
97
+ * removes the manual `execFile`/JSON boilerplate so guest and agent code can do
98
+ * `const out = await callBinding("add", { a, b })`. The value is a single line
99
+ * so it shifts guest source line numbers by at most one in stack traces.
100
+ *
101
+ * Note: the binding still runs through a guest process. Eliminating that spawn
102
+ * would require a dedicated async guest-to-host binding channel (the synchronous
103
+ * sync-RPC path cannot be used: it runs on the sidecar's main sync-RPC thread and
104
+ * a host round-trip would block it); that is a separate, test-gated change.
105
+ */
106
+ const BINDING_PREAMBLE = `globalThis.callBinding = (name, input = {}) => import("node:child_process").then(({ execFile }) => new Promise((resolve, reject) => { execFile(name, [name, "--json", JSON.stringify(input)], { maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(new Error(String(stderr || "").trim() || error.message)); return; } const text = String(stdout ?? "").trim(); let reply; try { reply = text ? JSON.parse(text) : undefined; } catch { reject(new Error("binding returned invalid JSON: " + text)); return; } if (reply && reply.ok === false) { reject(new Error(reply.error || "binding failed")); return; } resolve(reply && typeof reply === "object" && "result" in reply ? reply.result : reply); }); }));`;
107
+ /** Prepend the binding helper preamble to guest program source. */
108
+ function withBindingPreamble(code) {
109
+ return `${BINDING_PREAMBLE}\n${code}`;
110
+ }
111
+ const RESIDENT_READY_PREFIX = "__AGENTOS_RESIDENT_READY__";
112
+ const RESIDENT_RESULT_PREFIX = "__AGENTOS_RESIDENT_RESULT__";
113
+ /**
114
+ * Ergonomic, batteries-included runtime for executing guest JavaScript.
115
+ *
116
+ * Construct one with {@link NodeRuntime.create}, run programs with
117
+ * {@link NodeRuntime.exec} / {@link NodeRuntime.run}, and release the VM with
118
+ * {@link NodeRuntime.dispose}. A single instance can run many programs; each
119
+ * call executes a fresh guest process.
120
+ */
121
+ export class NodeRuntime {
122
+ kernel;
123
+ constructor(kernel) {
124
+ this.kernel = kernel;
125
+ }
126
+ /**
127
+ * Boot a VM and return a ready-to-use runtime. Spawns the sidecar, opens a
128
+ * session, creates the VM with a bootstrapped root filesystem, mounts the
129
+ * shell and Node runtimes, and waits for the VM to report ready.
130
+ */
131
+ static async create(options = {}) {
132
+ options = parseNodeRuntimeCreateOptions(options);
133
+ const commandsDir = resolveNodeRuntimeCommandsDir(options.commandsDir);
134
+ // Seed caller-provided files into the VM's in-memory filesystem before
135
+ // boot so they are part of the root filesystem snapshot the guest sees
136
+ // (e.g. projected npm packages or fixtures). The host filesystem is
137
+ // never exposed; only these bytes are copied in.
138
+ const filesystem = createInMemoryFileSystem();
139
+ if (options.files) {
140
+ for (const [filePath, content] of Object.entries(options.files)) {
141
+ await filesystem.writeFile(filePath, content);
142
+ }
143
+ }
144
+ // Project host directories into the VM, Docker-style. NodeFileSystem
145
+ // reads lazily through the VFS so large trees never traverse the
146
+ // protocol frame as a single blob.
147
+ const hostMounts = [...(options.mounts ?? [])];
148
+ // The `nodeModules` helper is sugar over a single host directory mount:
149
+ // project the whole host `node_modules` at a guest `node_modules` on the
150
+ // resolution path so any package inside resolves like real Node would.
151
+ if (options.nodeModules !== undefined) {
152
+ const nodeModules = typeof options.nodeModules === "string"
153
+ ? { hostPath: options.nodeModules }
154
+ : options.nodeModules;
155
+ hostMounts.push({
156
+ guestPath: nodeModules.guestPath ?? DEFAULT_NODE_MODULES_GUEST_PATH,
157
+ hostPath: nodeModules.hostPath,
158
+ readOnly: true,
159
+ });
160
+ }
161
+ const mounts = hostMounts.map((mount) => ({
162
+ path: mount.guestPath,
163
+ fs: new NodeFileSystem({ root: mount.hostPath }),
164
+ readOnly: mount.readOnly ?? true,
165
+ }));
166
+ // Grant the `binding` scope when the caller registers bindings but does not
167
+ // set their own binding policy, so the registered bindings are invocable.
168
+ const bindingDefaults = options.bindings &&
169
+ Object.keys(options.bindings).length > 0 &&
170
+ options.permissions?.binding === undefined
171
+ ? { binding: "allow" }
172
+ : {};
173
+ const kernel = createKernel({
174
+ filesystem,
175
+ mounts: mounts.length > 0 ? mounts : undefined,
176
+ // Merge the caller's policy over the secure default so partial
177
+ // opt-ins work: `{ network: "allow" }` enables the network while the
178
+ // execution essentials (fs/childProcess/process/env) stay granted.
179
+ permissions: {
180
+ ...DEFAULT_PERMISSIONS,
181
+ ...bindingDefaults,
182
+ ...options.permissions,
183
+ },
184
+ env: options.env,
185
+ cwd: options.cwd,
186
+ sidecar: options.sidecar,
187
+ onBootTiming: (timing) => options.onBootTiming?.(timing),
188
+ loopbackExemptPorts: options.loopbackExemptPorts,
189
+ jsRuntime: options.jsRuntime,
190
+ });
191
+ try {
192
+ // The shell runtime provides `sh` plus coreutils; the Node runtime
193
+ // provides the real V8-backed `node`. `sh` is REQUIRED to spawn any
194
+ // process: the kernel runs every command through a shell, so without
195
+ // `sh` nothing can be spawned, including the guest `node` program we
196
+ // run here and any child the guest spawns via node:child_process.
197
+ await measureBootTiming("runtime_mount_wasm", options.onBootTiming, () => kernel.mount(createWasmVmRuntime({
198
+ commandDirs: [commandsDir, ...(options.wasmCommandDirs ?? [])],
199
+ })));
200
+ await measureBootTiming("runtime_mount_node", options.onBootTiming, () => kernel.mount(createNodeRuntime()));
201
+ // Register bindings after the runtimes are mounted so they are
202
+ // installed as guest commands the moment the VM is ready.
203
+ const bindings = options.bindings;
204
+ if (bindings && Object.keys(bindings).length > 0) {
205
+ await measureBootTiming("bindings", options.onBootTiming, () => kernel.registerHostTools(bindings));
206
+ }
207
+ }
208
+ catch (error) {
209
+ await kernel.dispose().catch(() => { });
210
+ throw error;
211
+ }
212
+ return new NodeRuntime(kernel);
213
+ }
214
+ async createResidentRunner(_options = {}) {
215
+ return ResidentNodeRunner.create(this);
216
+ }
217
+ /**
218
+ * Run `code` as a guest Node program and capture its output.
219
+ *
220
+ * The source is written to an ES module inside the VM and executed with
221
+ * `node <file>`; it runs as standard ESM (top-level `await`, `import`).
222
+ */
223
+ async exec(code, options = {}) {
224
+ const programPath = `/tmp/secure-exec-program-${nextProgramId++}.mjs`;
225
+ await this.kernel.writeFile(programPath, withBindingPreamble(code));
226
+ return this.runProgram(programPath, options);
227
+ }
228
+ /**
229
+ * Run an already-written guest program file to completion and capture its
230
+ * output, honoring a caller-supplied `signal` for cancellation.
231
+ *
232
+ * Without a `signal`, this runs the program through the shell (`node <file>`)
233
+ * exactly as before. With a `signal`, it starts the program as a guest
234
+ * process so the run can be cancelled: when the signal aborts, the process is
235
+ * killed inside the VM (the kernel delivers `SIGTERM`) and the call rejects
236
+ * with the signal's abort reason.
237
+ */
238
+ async runProgram(programPath, options) {
239
+ const signal = options.signal;
240
+ if (!signal) {
241
+ const result = await this.kernel.exec(`node ${programPath}`, {
242
+ env: options.env,
243
+ cwd: options.cwd,
244
+ stdin: options.stdin,
245
+ timeout: options.timeout,
246
+ onStdout: options.onStdout,
247
+ onStderr: options.onStderr,
248
+ });
249
+ return toExecResult(result);
250
+ }
251
+ if (signal.aborted) {
252
+ throw toAbortError(signal);
253
+ }
254
+ // A signal was supplied, so run the program as a guest process we can
255
+ // kill: aborting the signal maps to a kernel kill of the underlying
256
+ // process. Aggregate the streamed output ourselves to reproduce the
257
+ // run-to-completion result that the shell path returns.
258
+ const stdoutChunks = [];
259
+ const stderrChunks = [];
260
+ const proc = this.kernel.spawn("node", [programPath], {
261
+ env: options.env,
262
+ cwd: options.cwd,
263
+ onStdout: (chunk) => {
264
+ stdoutChunks.push(chunk);
265
+ options.onStdout?.(chunk);
266
+ },
267
+ onStderr: (chunk) => {
268
+ stderrChunks.push(chunk);
269
+ options.onStderr?.(chunk);
270
+ },
271
+ streamStdin: options.stdin !== undefined,
272
+ });
273
+ if (options.stdin !== undefined) {
274
+ proc.writeStdin(options.stdin);
275
+ proc.closeStdin();
276
+ }
277
+ const onAbort = () => {
278
+ // Deliver SIGTERM to cancel the in-flight run inside the VM.
279
+ proc.kill(toSignalNumber("SIGTERM"));
280
+ };
281
+ signal.addEventListener("abort", onAbort, { once: true });
282
+ let timer;
283
+ if (options.timeout !== undefined) {
284
+ timer = setTimeout(() => {
285
+ proc.kill(toSignalNumber("SIGKILL"));
286
+ }, options.timeout);
287
+ }
288
+ try {
289
+ const exitCode = await proc.wait();
290
+ if (signal.aborted) {
291
+ throw toAbortError(signal);
292
+ }
293
+ return {
294
+ stdout: decodeChunks(stdoutChunks),
295
+ stderr: decodeChunks(stderrChunks),
296
+ exitCode,
297
+ };
298
+ }
299
+ finally {
300
+ if (timer !== undefined) {
301
+ clearTimeout(timer);
302
+ }
303
+ signal.removeEventListener("abort", onAbort);
304
+ }
305
+ }
306
+ /**
307
+ * Start `code` as a long-running guest Node program and return a live handle
308
+ * to it, without waiting for it to finish.
309
+ *
310
+ * The source is written to an ES module inside the VM and started with
311
+ * `node <file>`; it runs as standard ESM (top-level `await`, `import`). The
312
+ * returned {@link NodeRuntimeProcess} lets you stream output, write to stdin,
313
+ * signal or kill the process, and await its exit. Pass `onStdout`/`onStderr`
314
+ * to receive output chunks as they are produced.
315
+ *
316
+ * Use this for guests that do not run to completion, such as a dev server you
317
+ * later drive with {@link NodeRuntime.fetch}:
318
+ *
319
+ * ```ts
320
+ * const server = await rt.spawn(`
321
+ * import http from "node:http";
322
+ * http.createServer((_, res) => res.end("ok")).listen(3000);
323
+ * `);
324
+ * const res = await rt.fetch(3000, { path: "/" });
325
+ * server.kill();
326
+ * await server.wait();
327
+ * ```
328
+ */
329
+ async spawn(code, options = {}) {
330
+ const programPath = `/tmp/secure-exec-program-${nextProgramId++}.mjs`;
331
+ await this.kernel.writeFile(programPath, withBindingPreamble(code));
332
+ const proc = this.kernel.spawn("node", [programPath], {
333
+ env: options.env,
334
+ cwd: options.cwd,
335
+ onStdout: options.onStdout,
336
+ onStderr: options.onStderr,
337
+ // Keep stdin open so callers can stream input via writeStdin and signal
338
+ // end-of-input with closeStdin.
339
+ streamStdin: true,
340
+ });
341
+ return {
342
+ pid: proc.pid,
343
+ writeStdin(data) {
344
+ proc.writeStdin(data);
345
+ },
346
+ closeStdin() {
347
+ proc.closeStdin();
348
+ },
349
+ kill(signal) {
350
+ proc.kill(toSignalNumber(signal));
351
+ },
352
+ wait() {
353
+ return proc.wait();
354
+ },
355
+ get exitCode() {
356
+ return proc.exitCode;
357
+ },
358
+ };
359
+ }
360
+ /**
361
+ * Start an arbitrary guest command and return a live handle. This is the
362
+ * command-level companion to {@link spawn}, used by benchmark harnesses that
363
+ * need to measure kernel process spawning directly instead of running a source
364
+ * string through the ergonomic Node wrapper.
365
+ */
366
+ spawnCommand(command, args = [], options = {}) {
367
+ const proc = this.kernel.spawn(command, args, {
368
+ env: options.env,
369
+ cwd: options.cwd,
370
+ onStdout: options.onStdout,
371
+ onStderr: options.onStderr,
372
+ streamStdin: true,
373
+ });
374
+ return {
375
+ pid: proc.pid,
376
+ writeStdin(data) {
377
+ proc.writeStdin(data);
378
+ },
379
+ closeStdin() {
380
+ proc.closeStdin();
381
+ },
382
+ kill(signal) {
383
+ proc.kill(toSignalNumber(signal));
384
+ },
385
+ wait() {
386
+ return proc.wait();
387
+ },
388
+ get exitCode() {
389
+ return proc.exitCode;
390
+ },
391
+ };
392
+ }
393
+ /**
394
+ * Run an arbitrary guest command to completion and capture stdout/stderr.
395
+ * Unlike {@link exec}, this does not write a JavaScript source file first.
396
+ */
397
+ async execCommand(command, args = [], options = {}) {
398
+ const stdoutChunks = [];
399
+ const stderrChunks = [];
400
+ const proc = this.spawnCommand(command, args, {
401
+ env: options.env,
402
+ cwd: options.cwd,
403
+ onStdout: (chunk) => {
404
+ stdoutChunks.push(chunk);
405
+ options.onStdout?.(chunk);
406
+ },
407
+ onStderr: (chunk) => {
408
+ stderrChunks.push(chunk);
409
+ options.onStderr?.(chunk);
410
+ },
411
+ });
412
+ if (options.stdin !== undefined) {
413
+ proc.writeStdin(options.stdin);
414
+ }
415
+ proc.closeStdin();
416
+ let timer;
417
+ if (options.timeout !== undefined) {
418
+ timer = setTimeout(() => proc.kill("SIGKILL"), options.timeout);
419
+ }
420
+ try {
421
+ const exitCode = await proc.wait();
422
+ return {
423
+ stdout: decodeChunks(stdoutChunks),
424
+ stderr: decodeChunks(stderrChunks),
425
+ exitCode,
426
+ };
427
+ }
428
+ finally {
429
+ if (timer !== undefined) {
430
+ clearTimeout(timer);
431
+ }
432
+ }
433
+ }
434
+ /**
435
+ * Run `code` and return the JSON-serializable value it produces.
436
+ *
437
+ * The guest exposes a `__return(value)` function; call it with a
438
+ * JSON-serializable value and that value is decoded on the host as
439
+ * `result.value`. If `__return` is never called the value is `undefined`.
440
+ * stdout/stderr/exitCode are still captured.
441
+ */
442
+ async run(code, options = {}) {
443
+ const id = nextProgramId++;
444
+ const resultPath = `/tmp/secure-exec-result-${id}.json`;
445
+ const programPath = `/tmp/secure-exec-program-${id}.mjs`;
446
+ // Inject the __return helper as a module-level preamble, then the user
447
+ // code at module top level. Import declarations (preamble's and the
448
+ // user's) are hoisted, so __return is defined before the user's
449
+ // executable code runs — and the user keeps full ESM semantics
450
+ // (top-level `import` and top-level `await` both work). Do NOT wrap the
451
+ // user code in an IIFE: that would push their top-level `import`
452
+ // statements inside a function and make them a SyntaxError.
453
+ const wrapped = [
454
+ `import { writeFileSync as __writeFileSync } from "node:fs";`,
455
+ BINDING_PREAMBLE,
456
+ `globalThis.__return = (value) => {`,
457
+ ` __writeFileSync(${JSON.stringify(resultPath)}, JSON.stringify(value === undefined ? null : value));`,
458
+ `};`,
459
+ code,
460
+ ].join("\n");
461
+ await this.kernel.writeFile(programPath, wrapped);
462
+ const exec = await this.runProgram(programPath, options);
463
+ let value;
464
+ if (exec.exitCode === 0) {
465
+ try {
466
+ const bytes = await this.kernel.readFile(resultPath);
467
+ value = JSON.parse(new TextDecoder().decode(bytes));
468
+ }
469
+ catch {
470
+ // No __return() call (or unreadable result): leave value undefined.
471
+ }
472
+ }
473
+ return { ...exec, value };
474
+ }
475
+ /**
476
+ * Drive an HTTP request to a guest HTTP server listening inside the VM and
477
+ * read the response back on the host.
478
+ *
479
+ * Point this at a port a guest program is serving, for example a dev server
480
+ * started with {@link NodeRuntime.exec}. The
481
+ * request and response never leave the VM: the connection is made to the
482
+ * guest's loopback listener through the kernel socket table, so this works
483
+ * even when guest network egress is denied.
484
+ *
485
+ * ```ts
486
+ * const res = await rt.fetch(3000, { path: "/health" });
487
+ * console.log(res.status, res.body);
488
+ * ```
489
+ */
490
+ async fetch(port, input) {
491
+ const body = input.body === undefined
492
+ ? undefined
493
+ : typeof input.body === "string"
494
+ ? input.body
495
+ : new TextDecoder().decode(input.body);
496
+ const responseJson = await this.kernel.vmFetch({
497
+ port,
498
+ method: input.method ?? "GET",
499
+ path: input.path,
500
+ headersJson: JSON.stringify(input.headers ?? {}),
501
+ body,
502
+ });
503
+ return parseFetchResponse(responseJson);
504
+ }
505
+ /**
506
+ * Look up a guest TCP listener once and return it, or `null` when nothing is
507
+ * listening yet.
508
+ *
509
+ * This is the immediate, non-blocking check behind
510
+ * {@link NodeRuntime.waitForListener}: it asks the kernel socket table
511
+ * whether a guest process is accepting connections on the requested `port`
512
+ * (optionally narrowed by `host`/`path`) and returns the match, or `null` if
513
+ * none is up. Use {@link NodeRuntime.waitForListener} when you want to block
514
+ * until one appears.
515
+ *
516
+ * ```ts
517
+ * const listener = rt.findListener({ port: 3000 });
518
+ * if (listener) console.log("up on pid", listener.processId);
519
+ * ```
520
+ */
521
+ findListener(query) {
522
+ const match = this.kernel.socketTable.findListener({
523
+ port: query.port,
524
+ ...(query.host !== undefined ? { host: query.host } : {}),
525
+ ...(query.path !== undefined ? { path: query.path } : {}),
526
+ });
527
+ return match ?? null;
528
+ }
529
+ /**
530
+ * Block until a guest TCP listener is accepting connections on the requested
531
+ * `port` (optionally narrowed by `host`/`path`), then resolve with it.
532
+ *
533
+ * This is the companion to {@link NodeRuntime.spawn} and
534
+ * {@link NodeRuntime.fetch} for dev-server scenarios: start a server, wait
535
+ * until it is actually listening, then drive requests into it. The kernel
536
+ * socket table is polled until a matching listener appears or the wait is
537
+ * cut short. If `timeoutMs` elapses (default 10000) or the supplied `signal`
538
+ * aborts first, the returned promise rejects.
539
+ *
540
+ * ```ts
541
+ * const server = await rt.spawn(`
542
+ * import http from "node:http";
543
+ * http.createServer((_, res) => res.end("ok")).listen(3000);
544
+ * `);
545
+ * const listener = await rt.waitForListener({ port: 3000 });
546
+ * const res = await rt.fetch(listener.port ?? 3000, { path: "/" });
547
+ * server.kill();
548
+ * await server.wait();
549
+ * ```
550
+ */
551
+ async waitForListener(query, options = {}) {
552
+ const timeoutMs = options.timeoutMs ?? 10_000;
553
+ const pollIntervalMs = options.pollIntervalMs ?? 50;
554
+ const signal = options.signal;
555
+ const deadline = Date.now() + timeoutMs;
556
+ for (;;) {
557
+ if (signal?.aborted) {
558
+ throw toAbortError(signal);
559
+ }
560
+ // Await a fresh lookup rather than reading the synchronous cache,
561
+ // which starts null and would otherwise let this loop poll a stale
562
+ // null even after the listener is up (issue #92).
563
+ const match = (await this.kernel.socketTable.findListenerAsync({
564
+ port: query.port,
565
+ ...(query.host !== undefined ? { host: query.host } : {}),
566
+ ...(query.path !== undefined ? { path: query.path } : {}),
567
+ }));
568
+ if (match) {
569
+ return match;
570
+ }
571
+ if (Date.now() >= deadline) {
572
+ throw new Error(`Timed out after ${timeoutMs}ms waiting for a listener on port ${query.port}`);
573
+ }
574
+ await delayUntil(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())), signal);
575
+ }
576
+ }
577
+ /**
578
+ * Register host-side bindings the guest can invoke as shell commands, after
579
+ * the VM is already running. Each entry becomes a named guest command; when
580
+ * the guest runs it, the invocation round-trips back to the host and runs the
581
+ * binding's `handler`, whose return value is delivered back to the guest. This
582
+ * is the same capability as the `bindings` create option, exposed for adding
583
+ * bindings to a live runtime. See `bindings` on {@link NodeRuntime.create} for
584
+ * the invocation shape and permission behavior.
585
+ *
586
+ * When registering bindings this way, make sure the `binding` permission scope
587
+ * is granted (for example `permissions: { binding: "allow" }` on
588
+ * {@link NodeRuntime.create}) so the bindings are invocable.
589
+ */
590
+ async registerBindings(bindings) {
591
+ await this.kernel.registerHostTools(bindings);
592
+ }
593
+ /**
594
+ * Write a file into the VM's virtual filesystem, creating parent
595
+ * directories as needed. Use this to project assets or npm packages into
596
+ * the sandbox after boot; the host filesystem is never touched.
597
+ */
598
+ async writeFile(filePath, content) {
599
+ await this.kernel.writeFile(filePath, content);
600
+ }
601
+ /** Read a file from the VM's virtual filesystem as raw bytes. */
602
+ async readFile(filePath) {
603
+ return this.kernel.readFile(filePath);
604
+ }
605
+ /** Read directory entry names from the VM's virtual filesystem. */
606
+ async readDir(dirPath) {
607
+ return this.kernel.readdir(dirPath);
608
+ }
609
+ /** Read typed directory entries from the VM's virtual filesystem. */
610
+ async readDirWithTypes(dirPath) {
611
+ return this.kernel.vfs.readDirWithTypes(dirPath);
612
+ }
613
+ async getResourceSnapshot() {
614
+ return this.kernel.getResourceSnapshot();
615
+ }
616
+ /** Tear down the VM and release the sidecar. */
617
+ async dispose() {
618
+ await this.kernel.dispose();
619
+ }
620
+ }
621
+ const RESIDENT_RUNNER_SOURCE = `
622
+ import { Buffer } from "node:buffer";
623
+ import { createInterface } from "node:readline";
624
+
625
+ const readyPrefix = ${JSON.stringify(RESIDENT_READY_PREFIX)};
626
+ const resultPrefix = ${JSON.stringify(RESIDENT_RESULT_PREFIX)};
627
+ console.log(readyPrefix);
628
+
629
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
630
+ for await (const line of rl) {
631
+ let request;
632
+ try {
633
+ request = JSON.parse(line);
634
+ const source = Buffer.from(String(request.code), "utf8").toString("base64");
635
+ await import(\`data:text/javascript;base64,\${source}#\${request.id}\`);
636
+ process.stdout.write(resultPrefix + JSON.stringify({
637
+ id: request.id,
638
+ exitCode: 0,
639
+ stderr: "",
640
+ }) + "\\n");
641
+ } catch (error) {
642
+ process.stdout.write(resultPrefix + JSON.stringify({
643
+ id: request?.id,
644
+ exitCode: 1,
645
+ stderr: error instanceof Error ? (error.stack ?? error.message) : String(error),
646
+ }) + "\\n");
647
+ }
648
+ }
649
+ `;
650
+ class ResidentNodeRunner {
651
+ proc = null;
652
+ stdoutBuffer = "";
653
+ active = null;
654
+ readyPromise;
655
+ resolveReady;
656
+ rejectReady;
657
+ constructor() {
658
+ this.readyPromise = new Promise((resolve, reject) => {
659
+ this.resolveReady = resolve;
660
+ this.rejectReady = reject;
661
+ });
662
+ }
663
+ static async create(runtime) {
664
+ const runner = new ResidentNodeRunner();
665
+ runner.proc = await runtime.spawn(RESIDENT_RUNNER_SOURCE, {
666
+ onStdout: (chunk) => runner.handleStdout(chunk),
667
+ onStderr: (chunk) => runner.handleStderr(chunk),
668
+ });
669
+ runner.proc.wait().then((exitCode) => {
670
+ const error = new Error(`resident runner exited before completing request: ${exitCode}`);
671
+ runner.rejectReady(error);
672
+ runner.active?.reject(error);
673
+ runner.active = null;
674
+ }, (error) => {
675
+ const normalized = error instanceof Error ? error : new Error(String(error));
676
+ runner.rejectReady(normalized);
677
+ runner.active?.reject(normalized);
678
+ runner.active = null;
679
+ });
680
+ await runner.readyPromise;
681
+ return runner;
682
+ }
683
+ async exec(code, options = {}) {
684
+ await this.readyPromise;
685
+ if (!this.proc) {
686
+ throw new Error("resident runner is not running");
687
+ }
688
+ if (this.active) {
689
+ throw new Error("resident runner supports one in-flight exec");
690
+ }
691
+ const proc = this.proc;
692
+ const id = nextResidentRequestId++;
693
+ return new Promise((resolve, reject) => {
694
+ const active = {
695
+ id,
696
+ stdout: [],
697
+ stderr: [],
698
+ resolve,
699
+ reject,
700
+ timer: undefined,
701
+ };
702
+ if (options.timeout !== undefined) {
703
+ active.timer = setTimeout(() => {
704
+ proc.kill("SIGKILL");
705
+ this.active = null;
706
+ reject(new Error(`resident runner timed out after ${options.timeout}ms`));
707
+ }, options.timeout);
708
+ }
709
+ this.active = active;
710
+ proc.writeStdin(`${JSON.stringify({ id, code })}\n`);
711
+ });
712
+ }
713
+ async dispose() {
714
+ const proc = this.proc;
715
+ this.proc = null;
716
+ this.active = null;
717
+ if (!proc) {
718
+ return;
719
+ }
720
+ proc.kill("SIGTERM");
721
+ await proc.wait().catch(() => { });
722
+ }
723
+ handleStdout(chunk) {
724
+ this.stdoutBuffer += new TextDecoder().decode(chunk);
725
+ while (true) {
726
+ const newlineIndex = this.stdoutBuffer.indexOf("\n");
727
+ if (newlineIndex < 0) {
728
+ break;
729
+ }
730
+ const rawLine = this.stdoutBuffer.slice(0, newlineIndex);
731
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
732
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
733
+ if (line === RESIDENT_READY_PREFIX) {
734
+ this.resolveReady();
735
+ continue;
736
+ }
737
+ if (line.startsWith(RESIDENT_RESULT_PREFIX)) {
738
+ this.finishRequest(line.slice(RESIDENT_RESULT_PREFIX.length));
739
+ continue;
740
+ }
741
+ this.active?.stdout.push(new TextEncoder().encode(`${line}\n`));
742
+ }
743
+ }
744
+ handleStderr(chunk) {
745
+ this.active?.stderr.push(chunk);
746
+ }
747
+ finishRequest(payload) {
748
+ const active = this.active;
749
+ if (!active) {
750
+ return;
751
+ }
752
+ let parsed;
753
+ try {
754
+ parsed = JSON.parse(payload);
755
+ }
756
+ catch (error) {
757
+ active.reject(error instanceof Error ? error : new Error(String(error)));
758
+ this.active = null;
759
+ return;
760
+ }
761
+ if (parsed.id !== active.id) {
762
+ active.reject(new Error(`resident runner response id mismatch: ${parsed.id}`));
763
+ this.active = null;
764
+ return;
765
+ }
766
+ if (active.timer !== undefined) {
767
+ clearTimeout(active.timer);
768
+ }
769
+ this.active = null;
770
+ const stderr = `${decodeChunks(active.stderr)}${parsed.stderr ?? ""}`;
771
+ active.resolve({
772
+ stdout: decodeChunks(active.stdout),
773
+ stderr,
774
+ exitCode: parsed.exitCode ?? 1,
775
+ });
776
+ }
777
+ }
778
+ async function measureBootTiming(phase, onBootTiming, fn) {
779
+ const start = performance.now();
780
+ try {
781
+ return await fn();
782
+ }
783
+ finally {
784
+ onBootTiming?.({ phase, durationMs: performance.now() - start });
785
+ }
786
+ }
787
+ /**
788
+ * Common POSIX signal numbers, used to translate a signal name passed to
789
+ * {@link NodeRuntimeProcess.kill} into the numeric signal the kernel expects.
790
+ */
791
+ const SIGNAL_NUMBERS = {
792
+ SIGHUP: 1,
793
+ SIGINT: 2,
794
+ SIGQUIT: 3,
795
+ SIGKILL: 9,
796
+ SIGUSR1: 10,
797
+ SIGUSR2: 12,
798
+ SIGTERM: 15,
799
+ SIGSTOP: 19,
800
+ SIGCONT: 18,
801
+ };
802
+ /**
803
+ * Normalize a signal passed to {@link NodeRuntimeProcess.kill} into the numeric
804
+ * signal the kernel expects. Accepts a signal name (e.g. `"SIGKILL"`) or a raw
805
+ * number; defaults to `SIGTERM` when omitted.
806
+ */
807
+ function toSignalNumber(signal) {
808
+ if (signal === undefined) {
809
+ return SIGNAL_NUMBERS.SIGTERM;
810
+ }
811
+ if (typeof signal === "number") {
812
+ return signal;
813
+ }
814
+ const resolved = SIGNAL_NUMBERS[signal];
815
+ if (resolved === undefined) {
816
+ throw new Error(`Unknown signal: ${signal}`);
817
+ }
818
+ return resolved;
819
+ }
820
+ /**
821
+ * Build the error a {@link NodeRuntime.waitForListener} wait rejects with when
822
+ * its abort signal fires, preferring the signal's own `reason` when present.
823
+ */
824
+ function toAbortError(signal) {
825
+ const reason = signal.reason;
826
+ if (reason instanceof Error) {
827
+ return reason;
828
+ }
829
+ const error = new Error("The listener wait was aborted");
830
+ error.name = "AbortError";
831
+ return error;
832
+ }
833
+ /**
834
+ * Resolve after `ms` milliseconds, or reject early if `signal` aborts. Used to
835
+ * pace the polling loop in {@link NodeRuntime.waitForListener} without blocking
836
+ * past an abort.
837
+ */
838
+ function delayUntil(ms, signal) {
839
+ return new Promise((resolve, reject) => {
840
+ if (signal?.aborted) {
841
+ reject(toAbortError(signal));
842
+ return;
843
+ }
844
+ const timer = setTimeout(() => {
845
+ signal?.removeEventListener("abort", onAbort);
846
+ resolve();
847
+ }, ms);
848
+ const onAbort = () => {
849
+ clearTimeout(timer);
850
+ reject(toAbortError(signal));
851
+ };
852
+ signal?.addEventListener("abort", onAbort, { once: true });
853
+ });
854
+ }
855
+ /**
856
+ * Concatenate streamed stdout/stderr chunks and decode them as UTF-8 text,
857
+ * reproducing the aggregated `stdout`/`stderr` strings the shell-backed
858
+ * {@link NodeRuntime.exec} path returns when a run is driven as a process for
859
+ * cancellation support.
860
+ */
861
+ function decodeChunks(chunks) {
862
+ if (chunks.length === 0) {
863
+ return "";
864
+ }
865
+ let total = 0;
866
+ for (const chunk of chunks) {
867
+ total += chunk.length;
868
+ }
869
+ const merged = new Uint8Array(total);
870
+ let offset = 0;
871
+ for (const chunk of chunks) {
872
+ merged.set(chunk, offset);
873
+ offset += chunk.length;
874
+ }
875
+ return new TextDecoder().decode(merged);
876
+ }
877
+ function toExecResult(result) {
878
+ return {
879
+ stdout: result.stdout,
880
+ stderr: result.stderr,
881
+ exitCode: result.exitCode,
882
+ };
883
+ }
884
+ /**
885
+ * Decode the raw JSON the kernel returns for a VM HTTP request into a
886
+ * structured response. The wire shape carries `status`, an optional
887
+ * `statusText`, `headers` (either an array of `[name, value]` pairs or an
888
+ * object), and a `body` that is base64-encoded when `bodyEncoding` is
889
+ * `"base64"`.
890
+ */
891
+ function parseFetchResponse(responseJson) {
892
+ const parsed = JSON.parse(responseJson);
893
+ const headers = {};
894
+ if (Array.isArray(parsed.headers)) {
895
+ for (const [name, value] of parsed.headers) {
896
+ headers[name.toLowerCase()] = value;
897
+ }
898
+ }
899
+ else if (parsed.headers) {
900
+ for (const [name, value] of Object.entries(parsed.headers)) {
901
+ headers[name.toLowerCase()] = value;
902
+ }
903
+ }
904
+ let body = parsed.body ?? "";
905
+ if (parsed.bodyEncoding === "base64" && body.length > 0) {
906
+ body = new TextDecoder().decode(Uint8Array.from(globalThis.atob(body), (char) => char.charCodeAt(0)));
907
+ }
908
+ return {
909
+ status: parsed.status ?? 0,
910
+ statusText: parsed.statusText ?? "",
911
+ headers,
912
+ body,
913
+ };
914
+ }