@secure-exec/wasmvm 0.2.0-rc.1

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 (70) hide show
  1. package/LICENSE +191 -0
  2. package/dist/browser-driver.d.ts +68 -0
  3. package/dist/browser-driver.d.ts.map +1 -0
  4. package/dist/browser-driver.js +293 -0
  5. package/dist/browser-driver.js.map +1 -0
  6. package/dist/driver.d.ts +43 -0
  7. package/dist/driver.d.ts.map +1 -0
  8. package/dist/driver.js +1420 -0
  9. package/dist/driver.js.map +1 -0
  10. package/dist/fd-table.d.ts +67 -0
  11. package/dist/fd-table.d.ts.map +1 -0
  12. package/dist/fd-table.js +171 -0
  13. package/dist/fd-table.js.map +1 -0
  14. package/dist/index.d.ts +26 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +19 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/kernel-worker.d.ts +14 -0
  19. package/dist/kernel-worker.d.ts.map +1 -0
  20. package/dist/kernel-worker.js +1205 -0
  21. package/dist/kernel-worker.js.map +1 -0
  22. package/dist/module-cache.d.ts +21 -0
  23. package/dist/module-cache.d.ts.map +1 -0
  24. package/dist/module-cache.js +51 -0
  25. package/dist/module-cache.js.map +1 -0
  26. package/dist/permission-check.d.ts +36 -0
  27. package/dist/permission-check.d.ts.map +1 -0
  28. package/dist/permission-check.js +84 -0
  29. package/dist/permission-check.js.map +1 -0
  30. package/dist/ring-buffer.d.ts +64 -0
  31. package/dist/ring-buffer.d.ts.map +1 -0
  32. package/dist/ring-buffer.js +160 -0
  33. package/dist/ring-buffer.js.map +1 -0
  34. package/dist/syscall-rpc.d.ts +95 -0
  35. package/dist/syscall-rpc.d.ts.map +1 -0
  36. package/dist/syscall-rpc.js +48 -0
  37. package/dist/syscall-rpc.js.map +1 -0
  38. package/dist/user.d.ts +58 -0
  39. package/dist/user.d.ts.map +1 -0
  40. package/dist/user.js +143 -0
  41. package/dist/user.js.map +1 -0
  42. package/dist/wasi-constants.d.ts +77 -0
  43. package/dist/wasi-constants.d.ts.map +1 -0
  44. package/dist/wasi-constants.js +122 -0
  45. package/dist/wasi-constants.js.map +1 -0
  46. package/dist/wasi-file-io.d.ts +50 -0
  47. package/dist/wasi-file-io.d.ts.map +1 -0
  48. package/dist/wasi-file-io.js +10 -0
  49. package/dist/wasi-file-io.js.map +1 -0
  50. package/dist/wasi-polyfill.d.ts +368 -0
  51. package/dist/wasi-polyfill.d.ts.map +1 -0
  52. package/dist/wasi-polyfill.js +1438 -0
  53. package/dist/wasi-polyfill.js.map +1 -0
  54. package/dist/wasi-process-io.d.ts +35 -0
  55. package/dist/wasi-process-io.d.ts.map +1 -0
  56. package/dist/wasi-process-io.js +11 -0
  57. package/dist/wasi-process-io.js.map +1 -0
  58. package/dist/wasi-types.d.ts +175 -0
  59. package/dist/wasi-types.d.ts.map +1 -0
  60. package/dist/wasi-types.js +68 -0
  61. package/dist/wasi-types.js.map +1 -0
  62. package/dist/wasm-magic.d.ts +12 -0
  63. package/dist/wasm-magic.d.ts.map +1 -0
  64. package/dist/wasm-magic.js +53 -0
  65. package/dist/wasm-magic.js.map +1 -0
  66. package/dist/worker-adapter.d.ts +32 -0
  67. package/dist/worker-adapter.d.ts.map +1 -0
  68. package/dist/worker-adapter.js +147 -0
  69. package/dist/worker-adapter.js.map +1 -0
  70. package/package.json +35 -0
package/dist/driver.js ADDED
@@ -0,0 +1,1420 @@
1
+ /**
2
+ * WasmVM runtime driver for kernel integration.
3
+ *
4
+ * Discovers WASM command binaries from filesystem directories (commandDirs),
5
+ * validates them by WASM magic bytes, and loads them on demand. Each spawn()
6
+ * creates a Worker thread that loads the per-command binary and communicates
7
+ * with the main thread via SharedArrayBuffer-based RPC for synchronous
8
+ * WASI syscalls.
9
+ *
10
+ * proc_spawn from brush-shell routes through KernelInterface.spawn()
11
+ * so pipeline stages can dispatch to any runtime (WasmVM, Node, Python).
12
+ */
13
+ import { AF_INET, AF_INET6, AF_UNIX, SOCK_STREAM, SOCK_DGRAM, resolveProcSelfPath, } from '@secure-exec/core';
14
+ import { WorkerAdapter } from './worker-adapter.js';
15
+ import { SIGNAL_BUFFER_BYTES, DATA_BUFFER_BYTES, RPC_WAIT_TIMEOUT_MS, SIG_IDX_STATE, SIG_IDX_ERRNO, SIG_IDX_INT_RESULT, SIG_IDX_DATA_LEN, SIG_IDX_PENDING_SIGNAL, SIG_STATE_READY, } from './syscall-rpc.js';
16
+ import { ERRNO_MAP, ERRNO_EIO } from './wasi-constants.js';
17
+ import { isWasmBinary, isWasmBinarySync } from './wasm-magic.js';
18
+ import { resolvePermissionTier } from './permission-check.js';
19
+ import { ModuleCache } from './module-cache.js';
20
+ import { readdir, stat } from 'node:fs/promises';
21
+ import { existsSync, statSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { connect as tlsConnect } from 'node:tls';
24
+ import { lookup } from 'node:dns/promises';
25
+ // wasi-libc bottom-half socket constants differ from the kernel's POSIX-facing
26
+ // constants, so normalize them at the host_net boundary.
27
+ const WASI_AF_INET = 1;
28
+ const WASI_AF_INET6 = 2;
29
+ const WASI_AF_UNIX = 3;
30
+ const WASI_SOCK_DGRAM = 5;
31
+ const WASI_SOCK_STREAM = 6;
32
+ const WASI_SOCK_TYPE_FLAGS = 0x6000;
33
+ function normalizeSocketDomain(domain) {
34
+ switch (domain) {
35
+ case WASI_AF_INET:
36
+ return AF_INET;
37
+ case WASI_AF_INET6:
38
+ return AF_INET6;
39
+ case WASI_AF_UNIX:
40
+ return AF_UNIX;
41
+ default:
42
+ return domain;
43
+ }
44
+ }
45
+ function normalizeSocketType(type) {
46
+ switch (type & ~WASI_SOCK_TYPE_FLAGS) {
47
+ case WASI_SOCK_DGRAM:
48
+ return SOCK_DGRAM;
49
+ case WASI_SOCK_STREAM:
50
+ return SOCK_STREAM;
51
+ default:
52
+ return type & ~WASI_SOCK_TYPE_FLAGS;
53
+ }
54
+ }
55
+ function scopedProcPath(pid, path) {
56
+ return resolveProcSelfPath(path, pid);
57
+ }
58
+ function decodeSocketOptionValue(optval) {
59
+ if (optval.byteLength === 0 || optval.byteLength > 6) {
60
+ throw Object.assign(new Error('EINVAL: invalid socket option length'), { code: 'EINVAL' });
61
+ }
62
+ // Decode little-endian integers exactly as wasi-libc passes them to host_net.
63
+ let value = 0;
64
+ for (let index = 0; index < optval.byteLength; index++) {
65
+ value += optval[index] * (2 ** (index * 8));
66
+ }
67
+ return value;
68
+ }
69
+ function encodeSocketOptionValue(value, byteLength) {
70
+ if (!Number.isInteger(byteLength) || byteLength <= 0 || byteLength > 6) {
71
+ throw Object.assign(new Error('EINVAL: invalid socket option length'), { code: 'EINVAL' });
72
+ }
73
+ const encoded = new Uint8Array(byteLength);
74
+ let remaining = value;
75
+ for (let index = 0; index < byteLength; index++) {
76
+ encoded[index] = remaining % 0x100;
77
+ remaining = Math.floor(remaining / 0x100);
78
+ }
79
+ return encoded;
80
+ }
81
+ function serializeSockAddr(addr) {
82
+ return 'host' in addr ? `${addr.host}:${addr.port}` : addr.path;
83
+ }
84
+ function getKernelWorkerUrl() {
85
+ const siblingWorkerUrl = new URL('./kernel-worker.js', import.meta.url);
86
+ if (existsSync(siblingWorkerUrl)) {
87
+ return siblingWorkerUrl;
88
+ }
89
+ return new URL('../dist/kernel-worker.js', import.meta.url);
90
+ }
91
+ /**
92
+ * All commands available in the WasmVM runtime.
93
+ * Used as fallback when no commandDirs are configured (legacy mode).
94
+ * @deprecated Use commandDirs option instead — commands are discovered from filesystem.
95
+ */
96
+ export const WASMVM_COMMANDS = [
97
+ // Shell
98
+ 'sh', 'bash',
99
+ // Text processing
100
+ 'grep', 'egrep', 'fgrep', 'rg', 'sed', 'awk', 'jq', 'yq',
101
+ // Find
102
+ 'find', 'fd',
103
+ // Built-in implementations
104
+ 'cat', 'chmod', 'column', 'cp', 'dd', 'diff', 'du', 'expr', 'file', 'head',
105
+ 'ln', 'logname', 'ls', 'mkdir', 'mktemp', 'mv', 'pathchk', 'rev', 'rm',
106
+ 'sleep', 'sort', 'split', 'stat', 'strings', 'tac', 'tail', 'test',
107
+ '[', 'touch', 'tree', 'tsort', 'whoami',
108
+ // Compression & Archiving
109
+ 'gzip', 'gunzip', 'zcat', 'tar', 'zip', 'unzip',
110
+ // Data Processing (C programs)
111
+ 'sqlite3',
112
+ // Network (C programs)
113
+ 'curl', 'wget',
114
+ // Build tools (C programs)
115
+ 'make',
116
+ // Version control (C programs)
117
+ 'git', 'git-remote-http', 'git-remote-https',
118
+ // Shim commands
119
+ 'env', 'envsubst', 'nice', 'nohup', 'stdbuf', 'timeout', 'xargs',
120
+ // uutils: text/encoding
121
+ 'base32', 'base64', 'basenc', 'basename', 'comm', 'cut',
122
+ 'dircolors', 'dirname', 'echo', 'expand', 'factor', 'false',
123
+ 'fmt', 'fold', 'join', 'nl', 'numfmt', 'od', 'paste',
124
+ 'printenv', 'printf', 'ptx', 'seq', 'shuf', 'tr', 'true',
125
+ 'unexpand', 'uniq', 'wc', 'yes',
126
+ // uutils: checksums
127
+ 'b2sum', 'cksum', 'md5sum', 'sha1sum', 'sha224sum', 'sha256sum',
128
+ 'sha384sum', 'sha512sum', 'sum',
129
+ // uutils: file operations
130
+ 'link', 'pwd', 'readlink', 'realpath', 'rmdir', 'shred', 'tee',
131
+ 'truncate', 'unlink',
132
+ // uutils: system info
133
+ 'arch', 'date', 'nproc', 'uname',
134
+ // uutils: ls variants
135
+ 'dir', 'vdir',
136
+ // Stubbed commands
137
+ 'hostname', 'hostid', 'more', 'sync', 'tty',
138
+ 'chcon', 'runcon',
139
+ 'chgrp', 'chown',
140
+ 'chroot',
141
+ 'df',
142
+ 'groups', 'id',
143
+ 'install',
144
+ 'kill',
145
+ 'mkfifo', 'mknod',
146
+ 'pinky', 'who', 'users', 'uptime',
147
+ 'stty',
148
+ // Codex CLI (host_process spawn via wasi-spawn)
149
+ 'codex',
150
+ // Codex headless agent (non-TUI entry point)
151
+ 'codex-exec',
152
+ // Internal test: WasiChild host_process spawn validation
153
+ 'spawn-test-host',
154
+ // Internal test: wasi-http HTTP client validation via host_net
155
+ 'http-test',
156
+ ];
157
+ Object.freeze(WASMVM_COMMANDS);
158
+ /**
159
+ * Default permission tiers for known first-party commands.
160
+ * User-provided permissions override these defaults.
161
+ */
162
+ export const DEFAULT_FIRST_PARTY_TIERS = {
163
+ // Shell — needs proc_spawn for pipelines and subcommands
164
+ 'sh': 'full',
165
+ 'bash': 'full',
166
+ // Shims — spawn child processes as their core function
167
+ 'env': 'full',
168
+ 'timeout': 'full',
169
+ 'xargs': 'full',
170
+ 'nice': 'full',
171
+ 'nohup': 'full',
172
+ 'stdbuf': 'full',
173
+ // Build tools — spawns child processes to run recipes
174
+ 'make': 'full',
175
+ // Codex CLI — spawns child processes via wasi-spawn
176
+ 'codex': 'full',
177
+ // Codex headless agent — spawns processes + uses network
178
+ 'codex-exec': 'full',
179
+ // Internal test — exercises WasiChild host_process spawn
180
+ 'spawn-test-host': 'full',
181
+ // Internal test — exercises wasi-http HTTP client via host_net
182
+ 'http-test': 'full',
183
+ // Version control — reads/writes .git objects, remote operations use network
184
+ 'git': 'full',
185
+ 'git-remote-http': 'full',
186
+ 'git-remote-https': 'full',
187
+ // Read-only tools — never need to write files
188
+ 'grep': 'read-only',
189
+ 'egrep': 'read-only',
190
+ 'fgrep': 'read-only',
191
+ 'rg': 'read-only',
192
+ 'cat': 'read-only',
193
+ 'head': 'read-only',
194
+ 'tail': 'read-only',
195
+ 'wc': 'read-only',
196
+ 'sort': 'read-only',
197
+ 'uniq': 'read-only',
198
+ 'diff': 'read-only',
199
+ 'find': 'read-only',
200
+ 'fd': 'read-only',
201
+ 'tree': 'read-only',
202
+ 'file': 'read-only',
203
+ 'du': 'read-only',
204
+ 'ls': 'read-only',
205
+ 'dir': 'read-only',
206
+ 'vdir': 'read-only',
207
+ 'strings': 'read-only',
208
+ 'stat': 'read-only',
209
+ 'rev': 'read-only',
210
+ 'column': 'read-only',
211
+ 'cut': 'read-only',
212
+ 'tr': 'read-only',
213
+ 'paste': 'read-only',
214
+ 'join': 'read-only',
215
+ 'fold': 'read-only',
216
+ 'expand': 'read-only',
217
+ 'nl': 'read-only',
218
+ 'od': 'read-only',
219
+ 'comm': 'read-only',
220
+ 'basename': 'read-only',
221
+ 'dirname': 'read-only',
222
+ 'realpath': 'read-only',
223
+ 'readlink': 'read-only',
224
+ 'pwd': 'read-only',
225
+ 'echo': 'read-only',
226
+ 'envsubst': 'read-only',
227
+ 'printf': 'read-only',
228
+ 'true': 'read-only',
229
+ 'false': 'read-only',
230
+ 'yes': 'read-only',
231
+ 'seq': 'read-only',
232
+ 'test': 'read-only',
233
+ '[': 'read-only',
234
+ 'expr': 'read-only',
235
+ 'factor': 'read-only',
236
+ 'date': 'read-only',
237
+ 'uname': 'read-only',
238
+ 'nproc': 'read-only',
239
+ 'whoami': 'read-only',
240
+ 'id': 'read-only',
241
+ 'groups': 'read-only',
242
+ 'base64': 'read-only',
243
+ 'md5sum': 'read-only',
244
+ 'sha256sum': 'read-only',
245
+ 'tac': 'read-only',
246
+ 'tsort': 'read-only',
247
+ // Network — needs socket access for HTTP, can write with -o/-O
248
+ 'curl': 'full',
249
+ 'wget': 'full',
250
+ // Data processing — need write for file-based databases
251
+ 'sqlite3': 'read-write',
252
+ };
253
+ /**
254
+ * Create a WasmVM RuntimeDriver that can be mounted into the kernel.
255
+ */
256
+ export function createWasmVmRuntime(options) {
257
+ return new WasmVmRuntimeDriver(options);
258
+ }
259
+ class WasmVmRuntimeDriver {
260
+ name = 'wasmvm';
261
+ // Dynamic commands list — populated from filesystem scan or legacy WASMVM_COMMANDS
262
+ _commands = [];
263
+ // Command name → binary path map (commandDirs mode only)
264
+ _commandPaths = new Map();
265
+ _commandDirs;
266
+ // Legacy mode: single binary path
267
+ _wasmBinaryPath;
268
+ _legacyMode;
269
+ // Per-command permission tiers
270
+ _permissions;
271
+ _kernel = null;
272
+ _activeWorkers = new Map();
273
+ _workerAdapter = new WorkerAdapter();
274
+ _moduleCache = new ModuleCache();
275
+ // TLS-upgraded sockets bypass kernel recv — direct host TLS I/O
276
+ _tlsSockets = new Map();
277
+ // Per-PID queue of signals pending cooperative delivery to WASM trampoline
278
+ _wasmPendingSignals = new Map();
279
+ get commands() { return this._commands; }
280
+ constructor(options) {
281
+ this._commandDirs = options?.commandDirs ?? [];
282
+ this._wasmBinaryPath = options?.wasmBinaryPath ?? '';
283
+ this._permissions = options?.permissions ?? {};
284
+ // Legacy mode when wasmBinaryPath is set and commandDirs is not
285
+ this._legacyMode = !options?.commandDirs && !!options?.wasmBinaryPath;
286
+ if (this._legacyMode) {
287
+ // Deprecated path — use static command list
288
+ this._commands = [...WASMVM_COMMANDS];
289
+ }
290
+ // Emit deprecation warning for wasmBinaryPath
291
+ if (options?.wasmBinaryPath && options?.commandDirs) {
292
+ console.warn('WasmVmRuntime: wasmBinaryPath is deprecated and ignored when commandDirs is set. ' +
293
+ 'Use commandDirs only.');
294
+ }
295
+ else if (options?.wasmBinaryPath) {
296
+ console.warn('WasmVmRuntime: wasmBinaryPath is deprecated. Use commandDirs instead.');
297
+ }
298
+ }
299
+ async init(kernel) {
300
+ this._kernel = kernel;
301
+ // Scan commandDirs for WASM binaries (skip in legacy mode)
302
+ if (!this._legacyMode && this._commandDirs.length > 0) {
303
+ await this._scanCommandDirs();
304
+ }
305
+ }
306
+ /**
307
+ * On-demand discovery: synchronously check commandDirs for a binary.
308
+ * Called by the kernel when CommandRegistry.resolve() returns null.
309
+ */
310
+ tryResolve(command) {
311
+ // Not applicable in legacy mode
312
+ if (this._legacyMode)
313
+ return false;
314
+ // Already known
315
+ if (this._commandPaths.has(command))
316
+ return true;
317
+ for (const dir of this._commandDirs) {
318
+ const fullPath = join(dir, command);
319
+ try {
320
+ if (!existsSync(fullPath))
321
+ continue;
322
+ // Skip directories
323
+ const st = statSync(fullPath);
324
+ if (st.isDirectory())
325
+ continue;
326
+ }
327
+ catch {
328
+ continue;
329
+ }
330
+ // Sync 4-byte WASM magic check
331
+ if (!isWasmBinarySync(fullPath))
332
+ continue;
333
+ this._commandPaths.set(command, fullPath);
334
+ if (!this._commands.includes(command))
335
+ this._commands.push(command);
336
+ return true;
337
+ }
338
+ return false;
339
+ }
340
+ spawn(command, args, ctx) {
341
+ const kernel = this._kernel;
342
+ if (!kernel)
343
+ throw new Error('WasmVM driver not initialized');
344
+ // Resolve binary path for this command
345
+ const binaryPath = this._resolveBinaryPath(command);
346
+ // Exit plumbing — resolved once, either on success or error
347
+ let resolveExit;
348
+ let exitResolved = false;
349
+ const exitPromise = new Promise((resolve) => {
350
+ resolveExit = (code) => {
351
+ if (exitResolved)
352
+ return;
353
+ exitResolved = true;
354
+ resolve(code);
355
+ };
356
+ });
357
+ // Set up stdin pipe for writeStdin/closeStdin — skip if FD 0 is already
358
+ // a PTY slave, pipe, or file (shell redirect/pipe wiring must be preserved)
359
+ const stdinIsPty = kernel.isatty(ctx.pid, 0);
360
+ const stdinAlreadyRouted = stdinIsPty || this._isFdKernelRouted(ctx.pid, 0) || this._isFdRegularFile(ctx.pid, 0);
361
+ let stdinWriteFd;
362
+ if (!stdinAlreadyRouted) {
363
+ const stdinPipe = kernel.pipe(ctx.pid);
364
+ kernel.fdDup2(ctx.pid, stdinPipe.readFd, 0);
365
+ kernel.fdClose(ctx.pid, stdinPipe.readFd);
366
+ stdinWriteFd = stdinPipe.writeFd;
367
+ }
368
+ const proc = {
369
+ onStdout: null,
370
+ onStderr: null,
371
+ onExit: null,
372
+ writeStdin: (data) => {
373
+ if (stdinWriteFd !== undefined)
374
+ kernel.fdWrite(ctx.pid, stdinWriteFd, data);
375
+ },
376
+ closeStdin: () => {
377
+ if (stdinWriteFd !== undefined) {
378
+ try {
379
+ kernel.fdClose(ctx.pid, stdinWriteFd);
380
+ }
381
+ catch { /* already closed */ }
382
+ }
383
+ },
384
+ kill: (signal) => {
385
+ const worker = this._activeWorkers.get(ctx.pid);
386
+ if (worker) {
387
+ worker.terminate();
388
+ this._activeWorkers.delete(ctx.pid);
389
+ }
390
+ // Encode signal-killed exit status (POSIX: low 7 bits = signal number)
391
+ const signalStatus = signal & 0x7f;
392
+ resolveExit(signalStatus);
393
+ proc.onExit?.(signalStatus);
394
+ },
395
+ wait: () => exitPromise,
396
+ };
397
+ // Launch worker asynchronously — spawn() returns synchronously per contract
398
+ this._launchWorker(command, args, ctx, proc, resolveExit, binaryPath).catch((err) => {
399
+ const errBytes = new TextEncoder().encode(`${err instanceof Error ? err.message : String(err)}\n`);
400
+ ctx.onStderr?.(errBytes);
401
+ proc.onStderr?.(errBytes);
402
+ resolveExit(1);
403
+ proc.onExit?.(1);
404
+ });
405
+ return proc;
406
+ }
407
+ async dispose() {
408
+ for (const worker of this._activeWorkers.values()) {
409
+ try {
410
+ await worker.terminate();
411
+ }
412
+ catch { /* best effort */ }
413
+ }
414
+ this._activeWorkers.clear();
415
+ // Clean up TLS-upgraded sockets (kernel sockets cleaned up by kernel.dispose)
416
+ for (const sock of this._tlsSockets.values()) {
417
+ try {
418
+ sock.destroy();
419
+ }
420
+ catch { /* best effort */ }
421
+ }
422
+ this._tlsSockets.clear();
423
+ this._moduleCache.clear();
424
+ this._kernel = null;
425
+ }
426
+ // -------------------------------------------------------------------------
427
+ // Command discovery
428
+ // -------------------------------------------------------------------------
429
+ /** Scan all command directories, validating WASM magic bytes. */
430
+ async _scanCommandDirs() {
431
+ this._commandPaths.clear();
432
+ this._commands = [];
433
+ for (const dir of this._commandDirs) {
434
+ let entries;
435
+ try {
436
+ entries = await readdir(dir);
437
+ }
438
+ catch {
439
+ // Directory doesn't exist or isn't readable — skip
440
+ continue;
441
+ }
442
+ for (const entry of entries) {
443
+ // Skip dotfiles
444
+ if (entry.startsWith('.'))
445
+ continue;
446
+ const fullPath = join(dir, entry);
447
+ // Skip directories
448
+ try {
449
+ const st = await stat(fullPath);
450
+ if (st.isDirectory())
451
+ continue;
452
+ }
453
+ catch {
454
+ continue;
455
+ }
456
+ // Validate WASM magic bytes
457
+ if (!(await isWasmBinary(fullPath)))
458
+ continue;
459
+ // First directory containing the command wins (PATH semantics)
460
+ if (!this._commandPaths.has(entry)) {
461
+ this._commandPaths.set(entry, fullPath);
462
+ this._commands.push(entry);
463
+ }
464
+ }
465
+ }
466
+ }
467
+ /** Resolve permission tier for a command with wildcard and default tier support. */
468
+ _resolvePermissionTier(command) {
469
+ // No permissions config → fully unrestricted (backward compatible)
470
+ if (Object.keys(this._permissions).length === 0)
471
+ return 'full';
472
+ // User config checked first (exact, glob, *), defaults as fallback layer
473
+ return resolvePermissionTier(command, this._permissions, DEFAULT_FIRST_PARTY_TIERS);
474
+ }
475
+ /** Resolve binary path for a command. */
476
+ _resolveBinaryPath(command) {
477
+ // commandDirs mode: look up per-command binary path
478
+ const perCommand = this._commandPaths.get(command);
479
+ if (perCommand)
480
+ return perCommand;
481
+ // Legacy mode: all commands use a single binary
482
+ if (this._legacyMode)
483
+ return this._wasmBinaryPath;
484
+ // Fallback to wasmBinaryPath if set (shouldn't reach here normally)
485
+ return this._wasmBinaryPath;
486
+ }
487
+ // -------------------------------------------------------------------------
488
+ // FD helpers
489
+ // -------------------------------------------------------------------------
490
+ /** Check if a process's FD is routed through kernel (pipe or PTY). */
491
+ _isFdKernelRouted(pid, fd) {
492
+ if (!this._kernel)
493
+ return false;
494
+ try {
495
+ const stat = this._kernel.fdStat(pid, fd);
496
+ if (stat.filetype === 6)
497
+ return true; // FILETYPE_PIPE
498
+ return this._kernel.isatty(pid, fd); // PTY slave
499
+ }
500
+ catch {
501
+ return false;
502
+ }
503
+ }
504
+ /** Check if a process's FD points to a regular file (e.g. shell < redirect). */
505
+ _isFdRegularFile(pid, fd) {
506
+ if (!this._kernel)
507
+ return false;
508
+ try {
509
+ const stat = this._kernel.fdStat(pid, fd);
510
+ return stat.filetype === 4; // FILETYPE_REGULAR_FILE
511
+ }
512
+ catch {
513
+ return false;
514
+ }
515
+ }
516
+ // -------------------------------------------------------------------------
517
+ // Worker lifecycle
518
+ // -------------------------------------------------------------------------
519
+ async _launchWorker(command, args, ctx, proc, resolveExit, binaryPath) {
520
+ const kernel = this._kernel;
521
+ // Pre-compile module via cache for fast re-instantiation on subsequent spawns
522
+ let wasmModule;
523
+ try {
524
+ wasmModule = await this._moduleCache.resolve(binaryPath);
525
+ }
526
+ catch (err) {
527
+ // Fail fast with a clear error — don't launch a worker with an undefined module
528
+ const msg = err instanceof Error ? err.message : String(err);
529
+ throw new Error(`wasmvm: failed to compile module for '${command}' at ${binaryPath}: ${msg}`);
530
+ }
531
+ // Create shared buffers for RPC
532
+ const signalBuf = new SharedArrayBuffer(SIGNAL_BUFFER_BYTES);
533
+ const dataBuf = new SharedArrayBuffer(DATA_BUFFER_BYTES);
534
+ // Check if stdio FDs are kernel-routed (pipe, PTY, or regular file redirect)
535
+ const stdinPiped = this._isFdKernelRouted(ctx.pid, 0);
536
+ const stdinIsFile = this._isFdRegularFile(ctx.pid, 0);
537
+ const stdoutPiped = this._isFdKernelRouted(ctx.pid, 1);
538
+ const stdoutIsFile = this._isFdRegularFile(ctx.pid, 1);
539
+ const stderrPiped = this._isFdKernelRouted(ctx.pid, 2);
540
+ const stderrIsFile = this._isFdRegularFile(ctx.pid, 2);
541
+ // Detect which FDs are TTYs (PTY slaves) for brush-shell interactive mode
542
+ const ttyFds = [];
543
+ for (const fd of [0, 1, 2]) {
544
+ if (kernel.isatty(ctx.pid, fd))
545
+ ttyFds.push(fd);
546
+ }
547
+ const permissionTier = this._resolvePermissionTier(command);
548
+ const workerData = {
549
+ wasmBinaryPath: binaryPath,
550
+ command,
551
+ args,
552
+ pid: ctx.pid,
553
+ ppid: ctx.ppid,
554
+ env: ctx.env,
555
+ cwd: ctx.cwd,
556
+ signalBuf,
557
+ dataBuf,
558
+ // Tell worker which stdio channels are kernel-routed (pipe, PTY, or file redirect)
559
+ stdinFd: (stdinPiped || stdinIsFile) ? 99 : undefined,
560
+ stdoutFd: (stdoutPiped || stdoutIsFile) ? 99 : undefined,
561
+ stderrFd: (stderrPiped || stderrIsFile) ? 99 : undefined,
562
+ ttyFds: ttyFds.length > 0 ? ttyFds : undefined,
563
+ wasmModule,
564
+ permissionTier,
565
+ };
566
+ const workerUrl = getKernelWorkerUrl();
567
+ this._workerAdapter.spawn(workerUrl, { workerData }).then((worker) => {
568
+ this._activeWorkers.set(ctx.pid, worker);
569
+ worker.onMessage((raw) => {
570
+ const msg = raw;
571
+ this._handleWorkerMessage(msg, ctx, kernel, signalBuf, dataBuf, proc, resolveExit);
572
+ });
573
+ worker.onError((err) => {
574
+ const errBytes = new TextEncoder().encode(`wasmvm: ${err.message}\n`);
575
+ ctx.onStderr?.(errBytes);
576
+ proc.onStderr?.(errBytes);
577
+ this._activeWorkers.delete(ctx.pid);
578
+ resolveExit(1);
579
+ proc.onExit?.(1);
580
+ });
581
+ worker.onExit((_code) => {
582
+ this._activeWorkers.delete(ctx.pid);
583
+ });
584
+ }, (err) => {
585
+ // Worker creation failed (binary not found, etc.)
586
+ const errMsg = err instanceof Error ? err.message : String(err);
587
+ const errBytes = new TextEncoder().encode(`wasmvm: ${errMsg}\n`);
588
+ ctx.onStderr?.(errBytes);
589
+ proc.onStderr?.(errBytes);
590
+ resolveExit(127);
591
+ proc.onExit?.(127);
592
+ });
593
+ }
594
+ // -------------------------------------------------------------------------
595
+ // Worker message handling
596
+ // -------------------------------------------------------------------------
597
+ _handleWorkerMessage(msg, ctx, kernel, signalBuf, dataBuf, proc, resolveExit) {
598
+ switch (msg.type) {
599
+ case 'stdout':
600
+ ctx.onStdout?.(msg.data);
601
+ proc.onStdout?.(msg.data);
602
+ break;
603
+ case 'stderr':
604
+ ctx.onStderr?.(msg.data);
605
+ proc.onStderr?.(msg.data);
606
+ break;
607
+ case 'exit':
608
+ this._activeWorkers.delete(ctx.pid);
609
+ this._wasmPendingSignals.delete(ctx.pid);
610
+ resolveExit(msg.code);
611
+ proc.onExit?.(msg.code);
612
+ break;
613
+ case 'syscall':
614
+ this._handleSyscall(msg, ctx.pid, kernel, signalBuf, dataBuf);
615
+ break;
616
+ case 'ready':
617
+ // Worker is ready — could be used for stdin/lifecycle signaling
618
+ break;
619
+ }
620
+ }
621
+ // -------------------------------------------------------------------------
622
+ // Syscall RPC handler — dispatches worker requests to KernelInterface
623
+ // -------------------------------------------------------------------------
624
+ async _handleSyscall(msg, pid, kernel, signalBuf, dataBuf) {
625
+ const signal = new Int32Array(signalBuf);
626
+ const data = new Uint8Array(dataBuf);
627
+ let errno = 0;
628
+ let intResult = 0;
629
+ let responseData = null;
630
+ try {
631
+ switch (msg.call) {
632
+ case 'fdRead': {
633
+ const result = await kernel.fdRead(pid, msg.args.fd, msg.args.length);
634
+ if (result.length > DATA_BUFFER_BYTES) {
635
+ errno = 76; // EIO — response exceeds SAB capacity
636
+ break;
637
+ }
638
+ data.set(result, 0);
639
+ responseData = result;
640
+ break;
641
+ }
642
+ case 'fdWrite': {
643
+ intResult = await kernel.fdWrite(pid, msg.args.fd, new Uint8Array(msg.args.data));
644
+ break;
645
+ }
646
+ case 'fdPread': {
647
+ const result = await kernel.fdPread(pid, msg.args.fd, msg.args.length, BigInt(msg.args.offset));
648
+ if (result.length > DATA_BUFFER_BYTES) {
649
+ errno = 76; // EIO — response exceeds SAB capacity
650
+ break;
651
+ }
652
+ data.set(result, 0);
653
+ responseData = result;
654
+ break;
655
+ }
656
+ case 'fdPwrite': {
657
+ intResult = await kernel.fdPwrite(pid, msg.args.fd, new Uint8Array(msg.args.data), BigInt(msg.args.offset));
658
+ break;
659
+ }
660
+ case 'fdOpen': {
661
+ intResult = kernel.fdOpen(pid, msg.args.path, msg.args.flags, msg.args.mode);
662
+ break;
663
+ }
664
+ case 'fdSeek': {
665
+ const offset = await kernel.fdSeek(pid, msg.args.fd, BigInt(msg.args.offset), msg.args.whence);
666
+ intResult = Number(offset);
667
+ break;
668
+ }
669
+ case 'fdClose': {
670
+ kernel.fdClose(pid, msg.args.fd);
671
+ break;
672
+ }
673
+ case 'fdStat': {
674
+ const stat = kernel.fdStat(pid, msg.args.fd);
675
+ // Pack stat into data buffer: filetype(i32) + flags(i32) + rights(f64 for bigint)
676
+ const view = new DataView(dataBuf);
677
+ view.setInt32(0, stat.filetype, true);
678
+ view.setInt32(4, stat.flags, true);
679
+ view.setFloat64(8, Number(stat.rights), true);
680
+ responseData = new Uint8Array(0); // signal data-in-buffer
681
+ Atomics.store(signal, SIG_IDX_DATA_LEN, 16);
682
+ break;
683
+ }
684
+ case 'spawn': {
685
+ // proc_spawn → kernel.spawn() — the critical cross-runtime routing
686
+ // Includes FD overrides for pipe wiring (brush-shell pipeline stages)
687
+ const spawnCtx = {
688
+ env: msg.args.env,
689
+ cwd: msg.args.cwd,
690
+ ppid: pid,
691
+ };
692
+ // Forward FD overrides — only pass non-default values
693
+ const stdinFd = msg.args.stdinFd;
694
+ const stdoutFd = msg.args.stdoutFd;
695
+ const stderrFd = msg.args.stderrFd;
696
+ if (stdinFd !== undefined && stdinFd !== 0)
697
+ spawnCtx.stdinFd = stdinFd;
698
+ if (stdoutFd !== undefined && stdoutFd !== 1)
699
+ spawnCtx.stdoutFd = stdoutFd;
700
+ if (stderrFd !== undefined && stderrFd !== 2)
701
+ spawnCtx.stderrFd = stderrFd;
702
+ const managed = kernel.spawn(msg.args.command, msg.args.spawnArgs, spawnCtx);
703
+ intResult = managed.pid;
704
+ // Exit code is delivered via the waitpid RPC — no async write needed
705
+ break;
706
+ }
707
+ case 'waitpid': {
708
+ const result = await kernel.waitpid(msg.args.pid, msg.args.options);
709
+ // WNOHANG returns null if process is still running (encode as -1 for WASM side)
710
+ intResult = result ? result.status : -1;
711
+ break;
712
+ }
713
+ case 'kill': {
714
+ kernel.kill(msg.args.pid, msg.args.signal);
715
+ break;
716
+ }
717
+ case 'sigaction': {
718
+ // proc_sigaction → register signal disposition in kernel process table
719
+ const sigNum = msg.args.signal;
720
+ const action = msg.args.action;
721
+ let handler;
722
+ if (action === 0) {
723
+ handler = 'default';
724
+ }
725
+ else if (action === 1) {
726
+ handler = 'ignore';
727
+ }
728
+ else {
729
+ // action=2: user handler — queue signal for cooperative delivery
730
+ handler = (sig) => {
731
+ let queue = this._wasmPendingSignals.get(pid);
732
+ if (!queue) {
733
+ queue = [];
734
+ this._wasmPendingSignals.set(pid, queue);
735
+ }
736
+ queue.push(sig);
737
+ };
738
+ }
739
+ kernel.processTable.sigaction(pid, sigNum, { handler, mask: new Set(), flags: 0 });
740
+ break;
741
+ }
742
+ case 'pipe': {
743
+ // fd_pipe → create kernel pipe in this process's FD table
744
+ const pipeFds = kernel.pipe(pid);
745
+ // Pack read + write FDs: low 16 bits = readFd, high 16 bits = writeFd
746
+ intResult = (pipeFds.readFd & 0xFFFF) | ((pipeFds.writeFd & 0xFFFF) << 16);
747
+ break;
748
+ }
749
+ case 'openpty': {
750
+ // pty_open → allocate PTY master/slave pair in this process's FD table
751
+ const ptyFds = kernel.openpty(pid);
752
+ // Pack master + slave FDs: low 16 bits = masterFd, high 16 bits = slaveFd
753
+ intResult = (ptyFds.masterFd & 0xFFFF) | ((ptyFds.slaveFd & 0xFFFF) << 16);
754
+ break;
755
+ }
756
+ case 'fdDup': {
757
+ intResult = kernel.fdDup(pid, msg.args.fd);
758
+ break;
759
+ }
760
+ case 'fdDup2': {
761
+ kernel.fdDup2(pid, msg.args.oldFd, msg.args.newFd);
762
+ break;
763
+ }
764
+ case 'fdDupMin': {
765
+ intResult = kernel.fdDupMin(pid, msg.args.fd, msg.args.minFd);
766
+ break;
767
+ }
768
+ case 'vfsStat':
769
+ case 'vfsLstat': {
770
+ const path = scopedProcPath(pid, msg.args.path);
771
+ const stat = msg.call === 'vfsLstat'
772
+ ? await kernel.vfs.lstat(path)
773
+ : await kernel.vfs.stat(path);
774
+ const enc = new TextEncoder();
775
+ const json = JSON.stringify({
776
+ ino: stat.ino,
777
+ type: stat.isDirectory ? 'dir' : stat.isSymbolicLink ? 'symlink' : 'file',
778
+ mode: stat.mode,
779
+ uid: stat.uid,
780
+ gid: stat.gid,
781
+ nlink: stat.nlink,
782
+ size: stat.size,
783
+ atime: stat.atimeMs,
784
+ mtime: stat.mtimeMs,
785
+ ctime: stat.ctimeMs,
786
+ });
787
+ const bytes = enc.encode(json);
788
+ if (bytes.length > DATA_BUFFER_BYTES) {
789
+ errno = 76; // EIO — response exceeds SAB capacity
790
+ break;
791
+ }
792
+ data.set(bytes, 0);
793
+ responseData = bytes;
794
+ break;
795
+ }
796
+ case 'vfsReaddir': {
797
+ const entries = await kernel.vfs.readDir(scopedProcPath(pid, msg.args.path));
798
+ const bytes = new TextEncoder().encode(JSON.stringify(entries));
799
+ if (bytes.length > DATA_BUFFER_BYTES) {
800
+ errno = 76; // EIO — response exceeds SAB capacity
801
+ break;
802
+ }
803
+ data.set(bytes, 0);
804
+ responseData = bytes;
805
+ break;
806
+ }
807
+ case 'vfsMkdir': {
808
+ await kernel.vfs.mkdir(scopedProcPath(pid, msg.args.path));
809
+ break;
810
+ }
811
+ case 'vfsUnlink': {
812
+ await kernel.vfs.removeFile(scopedProcPath(pid, msg.args.path));
813
+ break;
814
+ }
815
+ case 'vfsRmdir': {
816
+ await kernel.vfs.removeDir(scopedProcPath(pid, msg.args.path));
817
+ break;
818
+ }
819
+ case 'vfsRename': {
820
+ await kernel.vfs.rename(scopedProcPath(pid, msg.args.oldPath), scopedProcPath(pid, msg.args.newPath));
821
+ break;
822
+ }
823
+ case 'vfsSymlink': {
824
+ await kernel.vfs.symlink(msg.args.target, scopedProcPath(pid, msg.args.linkPath));
825
+ break;
826
+ }
827
+ case 'vfsReadlink': {
828
+ const normalizedPath = msg.args.path;
829
+ const target = normalizedPath === '/proc/self'
830
+ ? '/proc/' + pid
831
+ : await kernel.vfs.readlink(scopedProcPath(pid, normalizedPath));
832
+ const bytes = new TextEncoder().encode(target);
833
+ if (bytes.length > DATA_BUFFER_BYTES) {
834
+ errno = 76; // EIO — response exceeds SAB capacity
835
+ break;
836
+ }
837
+ data.set(bytes, 0);
838
+ responseData = bytes;
839
+ break;
840
+ }
841
+ case 'vfsReadFile': {
842
+ const content = await kernel.vfs.readFile(scopedProcPath(pid, msg.args.path));
843
+ if (content.length > DATA_BUFFER_BYTES) {
844
+ errno = 76; // EIO — response exceeds SAB capacity
845
+ break;
846
+ }
847
+ data.set(content, 0);
848
+ responseData = content;
849
+ break;
850
+ }
851
+ case 'vfsWriteFile': {
852
+ await kernel.vfs.writeFile(scopedProcPath(pid, msg.args.path), new Uint8Array(msg.args.data));
853
+ break;
854
+ }
855
+ case 'vfsExists': {
856
+ const exists = await kernel.vfs.exists(scopedProcPath(pid, msg.args.path));
857
+ intResult = exists ? 1 : 0;
858
+ break;
859
+ }
860
+ case 'vfsRealpath': {
861
+ const normalizedPath = msg.args.path;
862
+ const resolved = normalizedPath === '/proc/self'
863
+ ? '/proc/' + pid
864
+ : await kernel.vfs.realpath(scopedProcPath(pid, normalizedPath));
865
+ const bytes = new TextEncoder().encode(resolved);
866
+ if (bytes.length > DATA_BUFFER_BYTES) {
867
+ errno = 76; // EIO — response exceeds SAB capacity
868
+ break;
869
+ }
870
+ data.set(bytes, 0);
871
+ responseData = bytes;
872
+ break;
873
+ }
874
+ // ----- Networking (TCP sockets via kernel socket table) -----
875
+ case 'netSocket': {
876
+ intResult = kernel.socketTable.create(normalizeSocketDomain(msg.args.domain), normalizeSocketType(msg.args.type), msg.args.protocol, pid);
877
+ break;
878
+ }
879
+ case 'netConnect': {
880
+ const socketId = msg.args.fd;
881
+ const socket = kernel.socketTable.get(socketId);
882
+ const addr = msg.args.addr;
883
+ // Parse "host:port" or unix path
884
+ const lastColon = addr.lastIndexOf(':');
885
+ if (lastColon === -1) {
886
+ if (socket && socket.domain !== AF_UNIX) {
887
+ errno = ERRNO_MAP.EINVAL;
888
+ break;
889
+ }
890
+ // Unix domain socket path
891
+ await kernel.socketTable.connect(socketId, { path: addr });
892
+ }
893
+ else {
894
+ const host = addr.slice(0, lastColon);
895
+ const port = parseInt(addr.slice(lastColon + 1), 10);
896
+ if (isNaN(port)) {
897
+ errno = ERRNO_MAP.EINVAL;
898
+ break;
899
+ }
900
+ // Route through kernel socket table (host adapter handles real TCP)
901
+ await kernel.socketTable.connect(socketId, { host, port });
902
+ }
903
+ break;
904
+ }
905
+ case 'netSend': {
906
+ const socketId = msg.args.fd;
907
+ // TLS-upgraded sockets write directly to host TLS socket
908
+ const tlsSock = this._tlsSockets.get(socketId);
909
+ if (tlsSock) {
910
+ const tlsData = Buffer.from(msg.args.data);
911
+ await new Promise((resolve, reject) => {
912
+ tlsSock.write(tlsData, (err) => err ? reject(err) : resolve());
913
+ });
914
+ intResult = tlsData.length;
915
+ break;
916
+ }
917
+ const sendData = new Uint8Array(msg.args.data);
918
+ intResult = kernel.socketTable.send(socketId, sendData, msg.args.flags ?? 0);
919
+ break;
920
+ }
921
+ case 'netRecv': {
922
+ const socketId = msg.args.fd;
923
+ const maxLen = msg.args.length;
924
+ const flags = msg.args.flags ?? 0;
925
+ // TLS-upgraded sockets read directly from host TLS socket
926
+ const tlsRecvSock = this._tlsSockets.get(socketId);
927
+ if (tlsRecvSock) {
928
+ const tlsRecvData = await new Promise((resolve) => {
929
+ const onData = (chunk) => {
930
+ cleanupTls();
931
+ if (chunk.length > maxLen) {
932
+ tlsRecvSock.unshift(chunk.subarray(maxLen));
933
+ resolve(new Uint8Array(chunk.subarray(0, maxLen)));
934
+ }
935
+ else {
936
+ resolve(new Uint8Array(chunk));
937
+ }
938
+ };
939
+ const onEnd = () => { cleanupTls(); resolve(new Uint8Array(0)); };
940
+ const onError = () => { cleanupTls(); resolve(new Uint8Array(0)); };
941
+ const cleanupTls = () => {
942
+ tlsRecvSock.removeListener('data', onData);
943
+ tlsRecvSock.removeListener('end', onEnd);
944
+ tlsRecvSock.removeListener('error', onError);
945
+ };
946
+ tlsRecvSock.once('data', onData);
947
+ tlsRecvSock.once('end', onEnd);
948
+ tlsRecvSock.once('error', onError);
949
+ });
950
+ if (tlsRecvData.length > DATA_BUFFER_BYTES) {
951
+ errno = 76;
952
+ break;
953
+ }
954
+ if (tlsRecvData.length > 0)
955
+ data.set(tlsRecvData, 0);
956
+ responseData = tlsRecvData;
957
+ intResult = tlsRecvData.length;
958
+ break;
959
+ }
960
+ // Kernel socket recv — may need to wait for data from read pump
961
+ let recvResult = kernel.socketTable.recv(socketId, maxLen, flags);
962
+ if (recvResult === null) {
963
+ // Check if more data might arrive (socket still connected, EOF not received)
964
+ const ksock = kernel.socketTable.get(socketId);
965
+ if (ksock && (ksock.state === 'connected' || ksock.state === 'write-closed')) {
966
+ const mightHaveMore = ksock.external
967
+ ? !ksock.peerWriteClosed
968
+ : (ksock.peerId !== undefined && !ksock.peerWriteClosed);
969
+ if (mightHaveMore) {
970
+ await ksock.readWaiters.enqueue(30000).wait();
971
+ recvResult = kernel.socketTable.recv(socketId, maxLen, flags);
972
+ }
973
+ }
974
+ }
975
+ const recvData = recvResult ?? new Uint8Array(0);
976
+ if (recvData.length > DATA_BUFFER_BYTES) {
977
+ errno = 76;
978
+ break;
979
+ }
980
+ if (recvData.length > 0)
981
+ data.set(recvData, 0);
982
+ responseData = recvData;
983
+ intResult = recvData.length;
984
+ break;
985
+ }
986
+ case 'netTlsConnect': {
987
+ const socketId = msg.args.fd;
988
+ // Access the kernel socket's host socket for TLS upgrade
989
+ const ksockTls = kernel.socketTable.get(socketId);
990
+ if (!ksockTls) {
991
+ errno = ERRNO_MAP.EBADF;
992
+ break;
993
+ }
994
+ if (!ksockTls.external || !ksockTls.hostSocket) {
995
+ errno = ERRNO_MAP.EINVAL; // Can't TLS-upgrade loopback sockets
996
+ break;
997
+ }
998
+ // Extract underlying net.Socket from host adapter
999
+ const realSock = ksockTls.hostSocket.socket;
1000
+ if (!realSock) {
1001
+ errno = ERRNO_MAP.EINVAL;
1002
+ break;
1003
+ }
1004
+ // Detach kernel read pump by clearing hostSocket
1005
+ ksockTls.hostSocket = undefined;
1006
+ const hostname = msg.args.hostname;
1007
+ const tlsOpts = {
1008
+ socket: realSock,
1009
+ servername: hostname, // SNI
1010
+ };
1011
+ if (msg.args.verifyPeer === false) {
1012
+ tlsOpts.rejectUnauthorized = false;
1013
+ }
1014
+ try {
1015
+ const tlsSock = await new Promise((resolve, reject) => {
1016
+ const s = tlsConnect(tlsOpts, () => resolve(s));
1017
+ s.on('error', reject);
1018
+ });
1019
+ // TLS socket bypasses kernel — send/recv go directly through _tlsSockets
1020
+ this._tlsSockets.set(socketId, tlsSock);
1021
+ }
1022
+ catch {
1023
+ errno = ERRNO_MAP.ECONNREFUSED;
1024
+ }
1025
+ break;
1026
+ }
1027
+ case 'netGetaddrinfo': {
1028
+ const host = msg.args.host;
1029
+ const port = msg.args.port;
1030
+ try {
1031
+ // Resolve all addresses (IPv4 + IPv6)
1032
+ const result = await lookup(host, { all: true });
1033
+ const addresses = result.map((r) => ({
1034
+ addr: r.address,
1035
+ family: r.family,
1036
+ }));
1037
+ const json = JSON.stringify(addresses);
1038
+ const bytes = new TextEncoder().encode(json);
1039
+ if (bytes.length > DATA_BUFFER_BYTES) {
1040
+ errno = 76; // EIO — response exceeds SAB capacity
1041
+ break;
1042
+ }
1043
+ data.set(bytes, 0);
1044
+ responseData = bytes;
1045
+ intResult = bytes.length;
1046
+ }
1047
+ catch (err) {
1048
+ // dns.lookup returns ENOTFOUND for unknown hosts
1049
+ const code = err.code;
1050
+ if (code === 'ENOTFOUND' || code === 'EAI_NONAME' || code === 'ENODATA') {
1051
+ errno = ERRNO_MAP.ENOENT;
1052
+ }
1053
+ else {
1054
+ errno = ERRNO_MAP.EINVAL;
1055
+ }
1056
+ }
1057
+ break;
1058
+ }
1059
+ case 'netSetsockopt': {
1060
+ const socketId = msg.args.fd;
1061
+ const optvalBytes = new Uint8Array(msg.args.optval);
1062
+ const optval = decodeSocketOptionValue(optvalBytes);
1063
+ kernel.socketTable.setsockopt(socketId, msg.args.level, msg.args.optname, optval);
1064
+ break;
1065
+ }
1066
+ case 'netGetsockopt': {
1067
+ const socketId = msg.args.fd;
1068
+ const optlen = msg.args.optvalLen;
1069
+ const optval = kernel.socketTable.getsockopt(socketId, msg.args.level, msg.args.optname);
1070
+ if (optval === undefined) {
1071
+ errno = ERRNO_MAP.EINVAL;
1072
+ break;
1073
+ }
1074
+ const encoded = encodeSocketOptionValue(optval, optlen);
1075
+ if (encoded.length > DATA_BUFFER_BYTES) {
1076
+ errno = ERRNO_EIO;
1077
+ break;
1078
+ }
1079
+ data.set(encoded, 0);
1080
+ responseData = encoded;
1081
+ intResult = encoded.length;
1082
+ break;
1083
+ }
1084
+ case 'kernelSocketGetLocalAddr': {
1085
+ const socketId = msg.args.fd;
1086
+ const addrBytes = new TextEncoder().encode(serializeSockAddr(kernel.socketTable.getLocalAddr(socketId)));
1087
+ if (addrBytes.length > DATA_BUFFER_BYTES) {
1088
+ errno = ERRNO_EIO;
1089
+ break;
1090
+ }
1091
+ data.set(addrBytes, 0);
1092
+ responseData = addrBytes;
1093
+ intResult = addrBytes.length;
1094
+ break;
1095
+ }
1096
+ case 'kernelSocketGetRemoteAddr': {
1097
+ const socketId = msg.args.fd;
1098
+ const addrBytes = new TextEncoder().encode(serializeSockAddr(kernel.socketTable.getRemoteAddr(socketId)));
1099
+ if (addrBytes.length > DATA_BUFFER_BYTES) {
1100
+ errno = ERRNO_EIO;
1101
+ break;
1102
+ }
1103
+ data.set(addrBytes, 0);
1104
+ responseData = addrBytes;
1105
+ intResult = addrBytes.length;
1106
+ break;
1107
+ }
1108
+ case 'netPoll': {
1109
+ const fds = msg.args.fds;
1110
+ const timeout = msg.args.timeout;
1111
+ const pollKernel = kernel;
1112
+ const revents = [];
1113
+ let ready = 0;
1114
+ // WASI poll constants
1115
+ const POLLIN = 0x1;
1116
+ const POLLOUT = 0x2;
1117
+ const POLLHUP = 0x2000;
1118
+ const POLLNVAL = 0x4000;
1119
+ // Check readiness helper: kernel socket table first, then kernel FD table
1120
+ const checkFd = (fd, events) => {
1121
+ // TLS-upgraded sockets — use host socket readability
1122
+ const tlsSockPoll = this._tlsSockets.get(fd);
1123
+ if (tlsSockPoll) {
1124
+ let rev = 0;
1125
+ if ((events & POLLIN) && tlsSockPoll.readableLength > 0)
1126
+ rev |= POLLIN;
1127
+ if ((events & POLLOUT) && tlsSockPoll.writable)
1128
+ rev |= POLLOUT;
1129
+ if (tlsSockPoll.destroyed)
1130
+ rev |= POLLHUP;
1131
+ return rev;
1132
+ }
1133
+ // Kernel socket table
1134
+ const ksock = kernel.socketTable.get(fd);
1135
+ if (ksock) {
1136
+ const ps = kernel.socketTable.poll(fd);
1137
+ let rev = 0;
1138
+ if ((events & POLLIN) && ps.readable)
1139
+ rev |= POLLIN;
1140
+ if ((events & POLLOUT) && ps.writable)
1141
+ rev |= POLLOUT;
1142
+ if (ps.hangup)
1143
+ rev |= POLLHUP;
1144
+ return rev;
1145
+ }
1146
+ // Kernel FD table (pipes, files)
1147
+ try {
1148
+ const ps = kernel.fdPoll(pid, fd);
1149
+ if (ps.invalid)
1150
+ return POLLNVAL;
1151
+ let rev = 0;
1152
+ if ((events & POLLIN) && ps.readable)
1153
+ rev |= POLLIN;
1154
+ if ((events & POLLOUT) && ps.writable)
1155
+ rev |= POLLOUT;
1156
+ if (ps.hangup)
1157
+ rev |= POLLHUP;
1158
+ return rev;
1159
+ }
1160
+ catch {
1161
+ return POLLNVAL;
1162
+ }
1163
+ };
1164
+ // Recompute readiness after each wait cycle.
1165
+ const refreshReadiness = () => {
1166
+ ready = 0;
1167
+ revents.length = 0;
1168
+ for (const entry of fds) {
1169
+ const rev = checkFd(entry.fd, entry.events);
1170
+ revents.push(rev);
1171
+ if (rev !== 0)
1172
+ ready++;
1173
+ }
1174
+ };
1175
+ // Wait for any polled FD to change state, then re-check them all.
1176
+ const waitForFdActivity = async (waitMs) => {
1177
+ await new Promise((resolve) => {
1178
+ let settled = false;
1179
+ const cleanups = [];
1180
+ const finish = () => {
1181
+ if (settled)
1182
+ return;
1183
+ settled = true;
1184
+ for (const cleanup of cleanups)
1185
+ cleanup();
1186
+ resolve();
1187
+ };
1188
+ const timer = setTimeout(finish, waitMs);
1189
+ cleanups.push(() => clearTimeout(timer));
1190
+ for (const entry of fds) {
1191
+ const tlsSockWait = this._tlsSockets.get(entry.fd);
1192
+ if (tlsSockWait) {
1193
+ if (entry.events & POLLIN) {
1194
+ const onReadable = () => finish();
1195
+ const onEnd = () => finish();
1196
+ tlsSockWait.once('readable', onReadable);
1197
+ tlsSockWait.once('end', onEnd);
1198
+ cleanups.push(() => {
1199
+ tlsSockWait.removeListener('readable', onReadable);
1200
+ tlsSockWait.removeListener('end', onEnd);
1201
+ });
1202
+ }
1203
+ continue;
1204
+ }
1205
+ const ksock = kernel.socketTable.get(entry.fd);
1206
+ if (ksock) {
1207
+ if (entry.events & POLLIN) {
1208
+ const waitQueue = ksock.state === 'listening'
1209
+ ? ksock.acceptWaiters
1210
+ : ksock.readWaiters;
1211
+ const handle = waitQueue.enqueue();
1212
+ void handle.wait().then(finish);
1213
+ cleanups.push(() => waitQueue.remove(handle));
1214
+ }
1215
+ continue;
1216
+ }
1217
+ if (!pollKernel.fdPollWait) {
1218
+ continue;
1219
+ }
1220
+ if ((entry.events & (POLLIN | POLLOUT)) === 0) {
1221
+ continue;
1222
+ }
1223
+ void pollKernel.fdPollWait(pid, entry.fd, waitMs).then(finish).catch(() => { });
1224
+ }
1225
+ });
1226
+ };
1227
+ refreshReadiness();
1228
+ if (ready === 0 && timeout !== 0) {
1229
+ const deadline = timeout > 0 ? Date.now() + timeout : null;
1230
+ while (ready === 0) {
1231
+ const waitMs = timeout < 0
1232
+ ? RPC_WAIT_TIMEOUT_MS
1233
+ : Math.max(0, deadline - Date.now());
1234
+ if (waitMs === 0) {
1235
+ break;
1236
+ }
1237
+ await waitForFdActivity(waitMs);
1238
+ refreshReadiness();
1239
+ if (timeout > 0 && Date.now() >= deadline) {
1240
+ break;
1241
+ }
1242
+ }
1243
+ }
1244
+ // Encode revents as JSON
1245
+ const pollJson = JSON.stringify(revents);
1246
+ const pollBytes = new TextEncoder().encode(pollJson);
1247
+ if (pollBytes.length > DATA_BUFFER_BYTES) {
1248
+ errno = ERRNO_EIO;
1249
+ break;
1250
+ }
1251
+ data.set(pollBytes, 0);
1252
+ responseData = pollBytes;
1253
+ intResult = ready;
1254
+ break;
1255
+ }
1256
+ case 'netBind': {
1257
+ const socketId = msg.args.fd;
1258
+ const socket = kernel.socketTable.get(socketId);
1259
+ const addr = msg.args.addr;
1260
+ // Parse "host:port" or unix path
1261
+ const lastColon = addr.lastIndexOf(':');
1262
+ if (lastColon === -1) {
1263
+ if (socket && socket.domain !== AF_UNIX) {
1264
+ errno = ERRNO_MAP.EINVAL;
1265
+ break;
1266
+ }
1267
+ // Unix domain socket path
1268
+ await kernel.socketTable.bind(socketId, { path: addr });
1269
+ }
1270
+ else {
1271
+ const host = addr.slice(0, lastColon);
1272
+ const port = parseInt(addr.slice(lastColon + 1), 10);
1273
+ if (isNaN(port)) {
1274
+ errno = ERRNO_MAP.EINVAL;
1275
+ break;
1276
+ }
1277
+ await kernel.socketTable.bind(socketId, { host, port });
1278
+ }
1279
+ break;
1280
+ }
1281
+ case 'netListen': {
1282
+ const socketId = msg.args.fd;
1283
+ const backlog = msg.args.backlog;
1284
+ await kernel.socketTable.listen(socketId, backlog);
1285
+ break;
1286
+ }
1287
+ case 'netAccept': {
1288
+ const socketId = msg.args.fd;
1289
+ // accept() returns null if no pending connection — wait for one
1290
+ let newSockId = kernel.socketTable.accept(socketId);
1291
+ if (newSockId === null) {
1292
+ const listenerSock = kernel.socketTable.get(socketId);
1293
+ if (listenerSock) {
1294
+ await listenerSock.acceptWaiters.enqueue(30000).wait();
1295
+ newSockId = kernel.socketTable.accept(socketId);
1296
+ }
1297
+ }
1298
+ if (newSockId === null) {
1299
+ errno = ERRNO_MAP.EAGAIN;
1300
+ break;
1301
+ }
1302
+ intResult = newSockId;
1303
+ // Return the remote address of the accepted socket
1304
+ const acceptedSock = kernel.socketTable.get(newSockId);
1305
+ let addrStr = '';
1306
+ if (acceptedSock?.remoteAddr) {
1307
+ addrStr = serializeSockAddr(acceptedSock.remoteAddr);
1308
+ }
1309
+ const addrBytes = new TextEncoder().encode(addrStr);
1310
+ if (addrBytes.length <= DATA_BUFFER_BYTES) {
1311
+ data.set(addrBytes, 0);
1312
+ responseData = addrBytes;
1313
+ }
1314
+ break;
1315
+ }
1316
+ case 'netSendTo': {
1317
+ const socketId = msg.args.fd;
1318
+ const sendData = new Uint8Array(msg.args.data);
1319
+ const flags = msg.args.flags ?? 0;
1320
+ const addr = msg.args.addr;
1321
+ // Parse "host:port" destination address
1322
+ const lastColon = addr.lastIndexOf(':');
1323
+ if (lastColon === -1) {
1324
+ errno = ERRNO_MAP.EINVAL;
1325
+ break;
1326
+ }
1327
+ const host = addr.slice(0, lastColon);
1328
+ const port = parseInt(addr.slice(lastColon + 1), 10);
1329
+ if (isNaN(port)) {
1330
+ errno = ERRNO_MAP.EINVAL;
1331
+ break;
1332
+ }
1333
+ intResult = kernel.socketTable.sendTo(socketId, sendData, flags, { host, port });
1334
+ break;
1335
+ }
1336
+ case 'netRecvFrom': {
1337
+ const socketId = msg.args.fd;
1338
+ const maxLen = msg.args.length;
1339
+ const flags = msg.args.flags ?? 0;
1340
+ // recvFrom may return null if no datagram queued — wait for one
1341
+ let result = kernel.socketTable.recvFrom(socketId, maxLen, flags);
1342
+ if (result === null) {
1343
+ const sock = kernel.socketTable.get(socketId);
1344
+ if (sock) {
1345
+ await sock.readWaiters.enqueue(30000).wait();
1346
+ result = kernel.socketTable.recvFrom(socketId, maxLen, flags);
1347
+ }
1348
+ }
1349
+ if (result === null) {
1350
+ errno = ERRNO_MAP.EAGAIN;
1351
+ break;
1352
+ }
1353
+ // Pack [data | addr] into combined buffer, intResult = data length
1354
+ const addrStr = serializeSockAddr(result.srcAddr);
1355
+ const addrBytes = new TextEncoder().encode(addrStr);
1356
+ const combined = new Uint8Array(result.data.length + addrBytes.length);
1357
+ combined.set(result.data, 0);
1358
+ combined.set(addrBytes, result.data.length);
1359
+ if (combined.length > DATA_BUFFER_BYTES) {
1360
+ errno = ERRNO_EIO;
1361
+ break;
1362
+ }
1363
+ data.set(combined, 0);
1364
+ responseData = combined;
1365
+ intResult = result.data.length;
1366
+ break;
1367
+ }
1368
+ case 'netClose': {
1369
+ const socketId = msg.args.fd;
1370
+ // Clean up TLS socket if upgraded
1371
+ const tlsCleanup = this._tlsSockets.get(socketId);
1372
+ if (tlsCleanup) {
1373
+ tlsCleanup.destroy();
1374
+ this._tlsSockets.delete(socketId);
1375
+ }
1376
+ kernel.socketTable.close(socketId, pid);
1377
+ break;
1378
+ }
1379
+ default:
1380
+ errno = ERRNO_MAP.ENOSYS; // ENOSYS
1381
+ }
1382
+ }
1383
+ catch (err) {
1384
+ errno = mapErrorToErrno(err);
1385
+ }
1386
+ // Guard against SAB data buffer overflow
1387
+ if (errno === 0 && responseData && responseData.length > DATA_BUFFER_BYTES) {
1388
+ errno = 76; // EIO — response exceeds 1MB SAB capacity
1389
+ responseData = null;
1390
+ }
1391
+ // Piggyback pending signal for cooperative delivery to WASM trampoline
1392
+ const pendingQueue = this._wasmPendingSignals.get(pid);
1393
+ const pendingSig = pendingQueue?.length ? pendingQueue.shift() : 0;
1394
+ // Write response to signal buffer — always set DATA_LEN so workers
1395
+ // never read stale lengths from previous calls (e.g. 0-byte EOF reads)
1396
+ Atomics.store(signal, SIG_IDX_DATA_LEN, responseData ? responseData.length : 0);
1397
+ Atomics.store(signal, SIG_IDX_ERRNO, errno);
1398
+ Atomics.store(signal, SIG_IDX_INT_RESULT, intResult);
1399
+ Atomics.store(signal, SIG_IDX_PENDING_SIGNAL, pendingSig);
1400
+ Atomics.store(signal, SIG_IDX_STATE, SIG_STATE_READY);
1401
+ Atomics.notify(signal, SIG_IDX_STATE);
1402
+ }
1403
+ }
1404
+ /** Map errors to WASI errno codes. Prefers structured .code, falls back to string matching. */
1405
+ export function mapErrorToErrno(err) {
1406
+ if (!(err instanceof Error))
1407
+ return ERRNO_EIO;
1408
+ // Prefer structured code field (KernelError, VfsError)
1409
+ const code = err.code;
1410
+ if (code && code in ERRNO_MAP)
1411
+ return ERRNO_MAP[code];
1412
+ // Fallback: match error code in message string
1413
+ const msg = err.message;
1414
+ for (const [name, errno] of Object.entries(ERRNO_MAP)) {
1415
+ if (msg.includes(name))
1416
+ return errno;
1417
+ }
1418
+ return ERRNO_EIO;
1419
+ }
1420
+ //# sourceMappingURL=driver.js.map