@secure-exec/core 0.2.0-rc.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/generated/isolate-runtime.d.ts +2 -2
  2. package/dist/generated/isolate-runtime.js +2 -2
  3. package/dist/index.d.ts +17 -4
  4. package/dist/index.js +10 -2
  5. package/dist/isolate-runtime/require-setup.js +1489 -239
  6. package/dist/isolate-runtime/setup-dynamic-import.js +31 -0
  7. package/dist/kernel/device-backend.d.ts +14 -0
  8. package/dist/kernel/device-backend.js +251 -0
  9. package/dist/kernel/device-layer.js +9 -0
  10. package/dist/kernel/file-lock.js +2 -3
  11. package/dist/kernel/index.d.ts +4 -4
  12. package/dist/kernel/index.js +3 -3
  13. package/dist/kernel/kernel.js +141 -122
  14. package/dist/kernel/mount-table.d.ts +75 -0
  15. package/dist/kernel/mount-table.js +353 -0
  16. package/dist/kernel/permissions.d.ts +9 -0
  17. package/dist/kernel/permissions.js +33 -1
  18. package/dist/kernel/proc-backend.d.ts +30 -0
  19. package/dist/kernel/proc-backend.js +428 -0
  20. package/dist/kernel/proc-layer.js +6 -0
  21. package/dist/kernel/process-table.d.ts +3 -1
  22. package/dist/kernel/process-table.js +23 -3
  23. package/dist/kernel/pty.d.ts +3 -2
  24. package/dist/kernel/pty.js +13 -2
  25. package/dist/kernel/socket-table.d.ts +7 -0
  26. package/dist/kernel/socket-table.js +99 -35
  27. package/dist/kernel/types.d.ts +45 -4
  28. package/dist/kernel/types.js +9 -0
  29. package/dist/kernel/vfs.d.ts +30 -2
  30. package/dist/kernel/vfs.js +19 -2
  31. package/dist/shared/api-types.d.ts +6 -0
  32. package/dist/shared/bridge-contract.d.ts +21 -3
  33. package/dist/shared/bridge-contract.js +2 -0
  34. package/dist/shared/console-formatter.js +8 -8
  35. package/dist/shared/global-exposure.js +95 -0
  36. package/dist/shared/in-memory-fs.d.ts +14 -59
  37. package/dist/shared/in-memory-fs.js +97 -597
  38. package/dist/shared/permissions.js +5 -0
  39. package/dist/test/block-store-conformance.d.ts +34 -0
  40. package/dist/test/block-store-conformance.js +251 -0
  41. package/dist/test/metadata-store-conformance.d.ts +37 -0
  42. package/dist/test/metadata-store-conformance.js +646 -0
  43. package/dist/test/vfs-conformance.d.ts +65 -0
  44. package/dist/test/vfs-conformance.js +842 -0
  45. package/dist/types.d.ts +1 -0
  46. package/dist/vfs/chunked-vfs.d.ts +66 -0
  47. package/dist/vfs/chunked-vfs.js +1290 -0
  48. package/dist/vfs/host-block-store.d.ts +19 -0
  49. package/dist/vfs/host-block-store.js +97 -0
  50. package/dist/vfs/memory-block-store.d.ts +16 -0
  51. package/dist/vfs/memory-block-store.js +45 -0
  52. package/dist/vfs/memory-metadata.d.ts +75 -0
  53. package/dist/vfs/memory-metadata.js +528 -0
  54. package/dist/vfs/sqlite-metadata.d.ts +91 -0
  55. package/dist/vfs/sqlite-metadata.js +582 -0
  56. package/dist/vfs/types.d.ts +210 -0
  57. package/dist/vfs/types.js +8 -0
  58. package/package.json +20 -1
  59. package/dist/kernel/inode-table.d.ts +0 -43
  60. package/dist/kernel/inode-table.js +0 -85
@@ -93,6 +93,7 @@ export declare class SocketTable {
93
93
  private readonly hostAdapter?;
94
94
  private readonly vfs?;
95
95
  private readonly getSignalState?;
96
+ private readonly processExists?;
96
97
  /** Bound/listening address → socket ID. Used for EADDRINUSE and TCP routing. */
97
98
  private listeners;
98
99
  /** Bound UDP address → socket ID. Separate from TCP listeners. */
@@ -103,7 +104,9 @@ export declare class SocketTable {
103
104
  hostAdapter?: HostNetworkAdapter;
104
105
  vfs?: VirtualFileSystem;
105
106
  getSignalState?: (pid: number) => ProcessSignalState;
107
+ processExists?: (pid: number) => boolean;
106
108
  });
109
+ hasHostNetworkAdapter(): boolean;
107
110
  /**
108
111
  * Create a new socket owned by the given process.
109
112
  * Returns the kernel socket ID.
@@ -279,6 +282,10 @@ export declare class SocketTable {
279
282
  private startAcceptPump;
280
283
  /** Look up a listening socket by exact address key. */
281
284
  private getListeningSocket;
285
+ /** Replay stored socket options onto a host-backed connection. */
286
+ private applySocketOptionsToHostSocket;
287
+ /** Best-effort option forwarding for host-backed sockets. */
288
+ private applySocketOptionToHostSocket;
282
289
  /** Peek up to maxBytes from a socket's readBuffer without consuming. */
283
290
  private peekFromBuffer;
284
291
  /** Consume up to maxBytes from a socket's readBuffer. */
@@ -68,6 +68,7 @@ export class SocketTable {
68
68
  hostAdapter;
69
69
  vfs;
70
70
  getSignalState;
71
+ processExists;
71
72
  /** Bound/listening address → socket ID. Used for EADDRINUSE and TCP routing. */
72
73
  listeners = new Map();
73
74
  /** Bound UDP address → socket ID. Separate from TCP listeners. */
@@ -78,6 +79,10 @@ export class SocketTable {
78
79
  this.hostAdapter = options?.hostAdapter;
79
80
  this.vfs = options?.vfs;
80
81
  this.getSignalState = options?.getSignalState;
82
+ this.processExists = options?.processExists;
83
+ }
84
+ hasHostNetworkAdapter() {
85
+ return this.hostAdapter !== undefined;
81
86
  }
82
87
  /**
83
88
  * Create a new socket owned by the given process.
@@ -87,6 +92,9 @@ export class SocketTable {
87
92
  if (this.sockets.size >= this.maxSockets) {
88
93
  throw new KernelError("EMFILE", "too many open sockets");
89
94
  }
95
+ if (this.processExists && !this.processExists(pid)) {
96
+ throw new KernelError("ESRCH", `cannot create socket for unknown pid ${pid}`);
97
+ }
90
98
  const id = this.nextSocketId++;
91
99
  const socket = {
92
100
  id,
@@ -196,19 +204,26 @@ export class SocketTable {
196
204
  throw new KernelError("EINVAL", "socket must be bound before listen");
197
205
  }
198
206
  socket.backlogLimit = Math.max(0, backlogSize);
199
- // Permission check for listen
200
- if (this.networkCheck) {
207
+ // AF_UNIX listeners stay entirely in-kernel, so host-network policy
208
+ // only applies to inet listeners.
209
+ if (socket.localAddr && isInetAddr(socket.localAddr)) {
201
210
  this.checkNetworkPermission("listen", socket.localAddr);
202
211
  }
203
212
  // External listen — delegate to host adapter
204
- if (options?.external && this.hostAdapter && socket.localAddr && isInetAddr(socket.localAddr)) {
213
+ if (options?.external &&
214
+ this.hostAdapter &&
215
+ socket.localAddr &&
216
+ isInetAddr(socket.localAddr)) {
205
217
  const hostListener = await this.hostAdapter.tcpListen(socket.localAddr.host, socket.requestedEphemeralPort ? 0 : socket.localAddr.port);
206
218
  socket.hostListener = hostListener;
207
219
  socket.external = true;
208
220
  // Update port for ephemeral (port 0) bindings
209
221
  if (socket.requestedEphemeralPort || socket.localAddr.port === 0) {
210
222
  const oldKey = addrKey(socket.localAddr);
211
- socket.localAddr = { host: socket.localAddr.host, port: hostListener.port };
223
+ socket.localAddr = {
224
+ host: socket.localAddr.host,
225
+ port: hostListener.port,
226
+ };
212
227
  // Re-register in listeners map with actual port
213
228
  this.listeners.delete(oldKey);
214
229
  this.listeners.set(addrKey(socket.localAddr), socketId);
@@ -266,7 +281,9 @@ export class SocketTable {
266
281
  */
267
282
  shutdown(socketId, how) {
268
283
  const socket = this.requireSocket(socketId);
269
- if (socket.state !== "connected" && socket.state !== "write-closed" && socket.state !== "read-closed") {
284
+ if (socket.state !== "connected" &&
285
+ socket.state !== "write-closed" &&
286
+ socket.state !== "read-closed") {
270
287
  throw new KernelError("ENOTCONN", "socket is not connected");
271
288
  }
272
289
  // Propagate half-close/full-close semantics to real host sockets so
@@ -341,6 +358,7 @@ export class SocketTable {
341
358
  setsockopt(socketId, level, optname, optval) {
342
359
  const socket = this.requireSocket(socketId);
343
360
  socket.options.set(optKey(level, optname), optval);
361
+ this.applySocketOptionToHostSocket(socket, level, optname, optval);
344
362
  }
345
363
  /** Toggle non-blocking behavior for an existing socket. */
346
364
  setNonBlocking(socketId, nonBlocking) {
@@ -395,16 +413,17 @@ export class SocketTable {
395
413
  }
396
414
  // Unix domain sockets: check VFS for socket file existence
397
415
  if (isUnixAddr(addr) && this.vfs) {
398
- if (!await this.vfs.exists(addr.path)) {
416
+ if (!(await this.vfs.exists(addr.path))) {
399
417
  throw new KernelError("ECONNREFUSED", `connection refused: ${addr.path}`);
400
418
  }
401
419
  }
402
420
  const listener = this.findListener(addr);
403
421
  if (!listener) {
404
- // External connection — check permission (throws EACCES if denied)
405
- if (this.networkCheck) {
406
- this.checkNetworkPermission("connect", addr);
422
+ if (isUnixAddr(addr)) {
423
+ throw new KernelError("ECONNREFUSED", `connection refused: ${addr.path}`);
407
424
  }
425
+ // Check external connections through the deny-by-default network policy.
426
+ this.checkNetworkPermission("connect", addr);
408
427
  // Route through host adapter if available
409
428
  if (this.hostAdapter && isInetAddr(addr)) {
410
429
  if (socket.nonBlocking) {
@@ -418,6 +437,7 @@ export class SocketTable {
418
437
  socket.external = true;
419
438
  socket.remoteAddr = addr;
420
439
  socket.hostSocket = hostSocket;
440
+ this.applySocketOptionsToHostSocket(socket);
421
441
  this.startReadPump(socket);
422
442
  return;
423
443
  }
@@ -467,8 +487,9 @@ export class SocketTable {
467
487
  if (socket.state !== "connected" && socket.state !== "read-closed") {
468
488
  throw new KernelError("ENOTCONN", "socket is not connected");
469
489
  }
470
- // Permission check for external sockets
471
- if (socket.external && this.networkCheck) {
490
+ // Re-check outbound external writes so pre-existing host sockets still
491
+ // honor deny-by-default network policy.
492
+ if (socket.external) {
472
493
  this.checkNetworkPermission("connect", socket.remoteAddr);
473
494
  }
474
495
  // External socket: write to host socket
@@ -480,16 +501,12 @@ export class SocketTable {
480
501
  return data.length;
481
502
  }
482
503
  if (socket.peerId === undefined) {
483
- throw new KernelError("EPIPE", nosignal
484
- ? "broken pipe (MSG_NOSIGNAL)"
485
- : "broken pipe: peer closed");
504
+ throw new KernelError("EPIPE", nosignal ? "broken pipe (MSG_NOSIGNAL)" : "broken pipe: peer closed");
486
505
  }
487
506
  const peer = this.sockets.get(socket.peerId);
488
507
  if (!peer) {
489
508
  socket.peerId = undefined;
490
- throw new KernelError("EPIPE", nosignal
491
- ? "broken pipe (MSG_NOSIGNAL)"
492
- : "broken pipe: peer closed");
509
+ throw new KernelError("EPIPE", nosignal ? "broken pipe (MSG_NOSIGNAL)" : "broken pipe: peer closed");
493
510
  }
494
511
  // Enforce SO_RCVBUF on the peer's receive buffer
495
512
  const rcvBuf = peer.options.get(optKey(SOL_SOCKET, SO_RCVBUF));
@@ -524,7 +541,9 @@ export class SocketTable {
524
541
  return this.consumeFromBuffer(socket, maxBytes);
525
542
  }
526
543
  // Buffer empty — check for EOF (peer gone or peer shut down write)
527
- if (socket.peerId === undefined || !this.sockets.has(socket.peerId) || socket.peerWriteClosed) {
544
+ if (socket.peerId === undefined ||
545
+ !this.sockets.has(socket.peerId) ||
546
+ socket.peerWriteClosed) {
528
547
  return null;
529
548
  }
530
549
  // No data available
@@ -570,10 +589,10 @@ export class SocketTable {
570
589
  }
571
590
  // External routing via host adapter
572
591
  if (socket.hostUdpSocket && this.hostAdapter && isInetAddr(destAddr)) {
573
- if (this.networkCheck) {
574
- this.checkNetworkPermission("connect", destAddr);
575
- }
576
- this.hostAdapter.udpSend(socket.hostUdpSocket, new Uint8Array(data), destAddr.host, destAddr.port).catch(() => { });
592
+ this.checkNetworkPermission("connect", destAddr);
593
+ this.hostAdapter
594
+ .udpSend(socket.hostUdpSocket, new Uint8Array(data), destAddr.host, destAddr.port)
595
+ .catch(() => { });
577
596
  return data.length;
578
597
  }
579
598
  // No loopback target, no host adapter — silently drop (UDP semantics)
@@ -647,12 +666,12 @@ export class SocketTable {
647
666
  if (socket.state !== "bound") {
648
667
  throw new KernelError("EINVAL", "socket must be bound before external UDP bind");
649
668
  }
650
- if (!this.hostAdapter || !socket.localAddr || !isInetAddr(socket.localAddr)) {
669
+ if (!this.hostAdapter ||
670
+ !socket.localAddr ||
671
+ !isInetAddr(socket.localAddr)) {
651
672
  throw new KernelError("EINVAL", "host adapter and inet address required");
652
673
  }
653
- if (this.networkCheck) {
654
- this.checkNetworkPermission("listen", socket.localAddr);
655
- }
674
+ this.checkNetworkPermission("listen", socket.localAddr);
656
675
  const hostUdpSocket = await this.hostAdapter.udpBind(socket.localAddr.host, socket.localAddr.port);
657
676
  socket.hostUdpSocket = hostUdpSocket;
658
677
  socket.external = true;
@@ -680,10 +699,11 @@ export class SocketTable {
680
699
  const closed = socket.state === "closed";
681
700
  const readClosed = socket.state === "read-closed";
682
701
  const writeClosed = socket.state === "write-closed";
702
+ const pendingAccept = socket.state === "listening" && socket.backlog.length > 0;
683
703
  // UDP: readable when datagramQueue has data
684
704
  const readable = socket.type === SOCK_DGRAM
685
705
  ? socket.datagramQueue.length > 0 || closed
686
- : socket.readBuffer.length > 0 || closed || readClosed;
706
+ : socket.readBuffer.length > 0 || pendingAccept || closed || readClosed;
687
707
  const writable = socket.state === "connected" ||
688
708
  socket.state === "created" ||
689
709
  socket.state === "read-closed" ||
@@ -756,6 +776,15 @@ export class SocketTable {
756
776
  }
757
777
  }
758
778
  destroySocket(socket) {
779
+ // Tear down queued-but-unaccepted connections with the listener so they
780
+ // cannot leak detached server-side sockets after the listening endpoint closes.
781
+ for (const pendingId of [...socket.backlog]) {
782
+ const pending = this.sockets.get(pendingId);
783
+ if (pending) {
784
+ this.destroySocket(pending);
785
+ }
786
+ }
787
+ socket.backlog.length = 0;
759
788
  // Propagate EOF to peer: clear peer link and wake readers
760
789
  if (socket.peerId !== undefined) {
761
790
  const peer = this.sockets.get(socket.peerId);
@@ -829,7 +858,9 @@ export class SocketTable {
829
858
  startExternalConnect(socket, addr) {
830
859
  if (!this.hostAdapter)
831
860
  return;
832
- this.hostAdapter.tcpConnect(addr.host, addr.port).then(hostSocket => {
861
+ this.hostAdapter
862
+ .tcpConnect(addr.host, addr.port)
863
+ .then((hostSocket) => {
833
864
  const current = this.sockets.get(socket.id);
834
865
  if (!current || current !== socket || current.state === "closed") {
835
866
  hostSocket.close().catch(() => { });
@@ -839,8 +870,10 @@ export class SocketTable {
839
870
  current.external = true;
840
871
  current.remoteAddr = addr;
841
872
  current.hostSocket = hostSocket;
873
+ this.applySocketOptionsToHostSocket(current);
842
874
  this.startReadPump(current);
843
- }).catch(() => {
875
+ })
876
+ .catch(() => {
844
877
  const current = this.sockets.get(socket.id);
845
878
  if (!current || current !== socket || current.state === "closed") {
846
879
  return;
@@ -859,7 +892,8 @@ export class SocketTable {
859
892
  const hostListener = socket.hostListener;
860
893
  const pump = async () => {
861
894
  try {
862
- while (socket.state === "listening" && socket.hostListener === hostListener) {
895
+ while (socket.state === "listening" &&
896
+ socket.hostListener === hostListener) {
863
897
  const hostSocket = await hostListener.accept();
864
898
  if (socket.backlog.length >= socket.backlogLimit) {
865
899
  hostSocket.close().catch(() => { });
@@ -872,6 +906,7 @@ export class SocketTable {
872
906
  connSock.external = true;
873
907
  connSock.hostSocket = hostSocket;
874
908
  connSock.localAddr = socket.localAddr;
909
+ this.applySocketOptionsToHostSocket(connSock);
875
910
  // Start read pump for the accepted socket
876
911
  this.startReadPump(connSock);
877
912
  // Queue in listener's backlog
@@ -895,6 +930,27 @@ export class SocketTable {
895
930
  return null;
896
931
  return sock;
897
932
  }
933
+ /** Replay stored socket options onto a host-backed connection. */
934
+ applySocketOptionsToHostSocket(socket) {
935
+ for (const [key, value] of socket.options.entries()) {
936
+ const [level, optname] = key.split(":").map(Number);
937
+ if (Number.isNaN(level) || Number.isNaN(optname))
938
+ continue;
939
+ this.applySocketOptionToHostSocket(socket, level, optname, value);
940
+ }
941
+ }
942
+ /** Best-effort option forwarding for host-backed sockets. */
943
+ applySocketOptionToHostSocket(socket, level, optname, optval) {
944
+ if (!socket.external || !socket.hostSocket) {
945
+ return;
946
+ }
947
+ try {
948
+ socket.hostSocket.setOption(level, optname, optval);
949
+ }
950
+ catch {
951
+ // Host adapters may not support every kernel-tracked option.
952
+ }
953
+ }
898
954
  /** Peek up to maxBytes from a socket's readBuffer without consuming. */
899
955
  peekFromBuffer(socket, maxBytes) {
900
956
  const chunks = [];
@@ -972,7 +1028,9 @@ export class SocketTable {
972
1028
  if (socket.external) {
973
1029
  return !socket.peerWriteClosed;
974
1030
  }
975
- return socket.peerId !== undefined && this.sockets.has(socket.peerId) && !socket.peerWriteClosed;
1031
+ return (socket.peerId !== undefined &&
1032
+ this.sockets.has(socket.peerId) &&
1033
+ !socket.peerWriteClosed);
976
1034
  }
977
1035
  /** Wait for socket readiness or an interrupting signal. */
978
1036
  async waitForSocketWake(waiters, pid, op) {
@@ -1048,7 +1106,8 @@ export class SocketTable {
1048
1106
  continue;
1049
1107
  if (existing.localAddr.port !== addr.port)
1050
1108
  continue;
1051
- const existingIsWildcard = existing.localAddr.host === "0.0.0.0" || existing.localAddr.host === "::";
1109
+ const existingIsWildcard = existing.localAddr.host === "0.0.0.0" ||
1110
+ existing.localAddr.host === "::";
1052
1111
  if (isWildcard || existingIsWildcard)
1053
1112
  return true;
1054
1113
  }
@@ -1061,12 +1120,16 @@ export class SocketTable {
1061
1120
  const hostUdpSocket = socket.hostUdpSocket;
1062
1121
  const pump = async () => {
1063
1122
  try {
1064
- while (socket.state !== "closed" && socket.hostUdpSocket === hostUdpSocket) {
1123
+ while (socket.state !== "closed" &&
1124
+ socket.hostUdpSocket === hostUdpSocket) {
1065
1125
  const result = await hostUdpSocket.recv();
1066
1126
  if (socket.datagramQueue.length < MAX_UDP_QUEUE_DEPTH) {
1067
1127
  socket.datagramQueue.push({
1068
1128
  data: result.data,
1069
- srcAddr: { host: result.remoteAddr.host, port: result.remoteAddr.port },
1129
+ srcAddr: {
1130
+ host: result.remoteAddr.host,
1131
+ port: result.remoteAddr.port,
1132
+ },
1070
1133
  });
1071
1134
  socket.readWaiters.wakeOne();
1072
1135
  }
@@ -1097,7 +1160,8 @@ export class SocketTable {
1097
1160
  continue;
1098
1161
  if (existing.localAddr.port !== addr.port)
1099
1162
  continue;
1100
- const existingIsWildcard = existing.localAddr.host === "0.0.0.0" || existing.localAddr.host === "::";
1163
+ const existingIsWildcard = existing.localAddr.host === "0.0.0.0" ||
1164
+ existing.localAddr.host === "::";
1101
1165
  if (isWildcard || existingIsWildcard)
1102
1166
  return true;
1103
1167
  }
@@ -6,6 +6,27 @@
6
6
  */
7
7
  import type { WaitQueue } from "./wait.js";
8
8
  export type { VirtualFileSystem, VirtualDirEntry, VirtualStat, } from "./vfs.js";
9
+ /**
10
+ * Minimal structured logger interface for kernel diagnostics.
11
+ * Compatible with pino and any logger that supports child loggers.
12
+ * The kernel never depends on pino directly — embedders pass their own logger.
13
+ */
14
+ export interface KernelLogger {
15
+ trace(obj: Record<string, unknown>, msg?: string): void;
16
+ debug(obj: Record<string, unknown>, msg?: string): void;
17
+ info(obj: Record<string, unknown>, msg?: string): void;
18
+ warn(obj: Record<string, unknown>, msg?: string): void;
19
+ error(obj: Record<string, unknown>, msg?: string): void;
20
+ child(bindings: Record<string, unknown>): KernelLogger;
21
+ }
22
+ /** No-op logger that discards all records. */
23
+ export declare const noopKernelLogger: KernelLogger;
24
+ /** A filesystem to mount at a specific path inside the kernel VFS. */
25
+ export interface FsMount {
26
+ path: string;
27
+ fs: import("./vfs.js").VirtualFileSystem;
28
+ readOnly?: boolean;
29
+ }
9
30
  export interface KernelOptions {
10
31
  filesystem: import("./vfs.js").VirtualFileSystem;
11
32
  permissions?: Permissions;
@@ -15,6 +36,10 @@ export interface KernelOptions {
15
36
  maxProcesses?: number;
16
37
  /** Host network adapter for external socket routing (TCP, UDP, DNS). */
17
38
  hostNetworkAdapter?: import("./host-adapter.js").HostNetworkAdapter;
39
+ /** Structured debug logger for kernel diagnostics. Defaults to silent no-op. */
40
+ logger?: KernelLogger;
41
+ /** Additional filesystems to mount at boot (after /dev and /proc). */
42
+ mounts?: FsMount[];
18
43
  }
19
44
  export interface Kernel {
20
45
  /** Mount a runtime driver. Calls driver.init() and registers its commands. */
@@ -49,15 +74,23 @@ export interface Kernel {
49
74
  * Returns the shell exit code.
50
75
  */
51
76
  connectTerminal(options?: ConnectTerminalOptions): Promise<number>;
77
+ /** Mount a filesystem at the given path. */
78
+ mountFs(path: string, fs: import("./vfs.js").VirtualFileSystem, options?: {
79
+ readOnly?: boolean;
80
+ }): void;
81
+ /** Unmount the filesystem at the given path. */
82
+ unmountFs(path: string): void;
52
83
  readFile(path: string): Promise<Uint8Array>;
53
84
  writeFile(path: string, content: string | Uint8Array): Promise<void>;
54
85
  mkdir(path: string): Promise<void>;
55
86
  readdir(path: string): Promise<string[]>;
56
87
  stat(path: string): Promise<import("./vfs.js").VirtualStat>;
57
88
  exists(path: string): Promise<boolean>;
89
+ removeFile(path: string): Promise<void>;
90
+ removeDir(path: string): Promise<void>;
91
+ rename(oldPath: string, newPath: string): Promise<void>;
58
92
  readonly socketTable: import("./socket-table.js").SocketTable;
59
93
  readonly timerTable: import("./timer-table.js").TimerTable;
60
- readonly inodeTable: import("./inode-table.js").InodeTable;
61
94
  readonly commands: ReadonlyMap<string, string>;
62
95
  readonly processes: ReadonlyMap<number, ProcessInfo>;
63
96
  /** Number of pending zombie cleanup timers (test observability). */
@@ -84,6 +117,8 @@ export interface SpawnOptions extends ExecOptions {
84
117
  stdoutFd?: number;
85
118
  /** FD in caller's table to wire as child's stderr (pipe write end). */
86
119
  stderrFd?: number;
120
+ /** Enable streaming stdin: writeStdin() delivers data immediately instead of buffering until closeStdin(). */
121
+ streamStdin?: boolean;
87
122
  }
88
123
  export interface ManagedProcess {
89
124
  pid: number;
@@ -172,6 +207,8 @@ export interface ProcessContext {
172
207
  stdinIsTTY?: boolean;
173
208
  stdoutIsTTY?: boolean;
174
209
  stderrIsTTY?: boolean;
210
+ /** Enable streaming stdin delivery (writeStdin data arrives immediately). */
211
+ streamStdin?: boolean;
175
212
  /** Kernel-provided callback for stdout data emitted during spawn. */
176
213
  onStdout?: (data: Uint8Array) => void;
177
214
  /** Kernel-provided callback for stderr data emitted during spawn. */
@@ -292,8 +329,6 @@ export interface FDStat {
292
329
  export interface FileDescription {
293
330
  id: number;
294
331
  path: string;
295
- /** Stable inode identity for FD I/O after the pathname is unlinked. */
296
- inode?: number;
297
332
  cursor: bigint;
298
333
  flags: number;
299
334
  refCount: number;
@@ -348,6 +383,8 @@ export interface ProcessEntry {
348
383
  exitReason: "normal" | "signal" | null;
349
384
  /** Signal that killed the process (0 = normal exit). */
350
385
  termSignal: number;
386
+ /** Epoch ms when the process was registered. */
387
+ startTime: number;
351
388
  exitTime: number | null;
352
389
  env: Record<string, string>;
353
390
  cwd: string;
@@ -368,11 +405,15 @@ export interface ProcessInfo {
368
405
  sid: number;
369
406
  driver: string;
370
407
  command: string;
408
+ args: string[];
409
+ cwd: string;
371
410
  status: "running" | "stopped" | "exited";
372
411
  exitCode: number | null;
412
+ startTime: number;
413
+ exitTime: number | null;
373
414
  }
374
415
  /** POSIX error codes used by the kernel. */
375
- export type KernelErrorCode = "EACCES" | "EADDRINUSE" | "EAGAIN" | "EBADF" | "ECONNREFUSED" | "EINPROGRESS" | "EINTR" | "EEXIST" | "EINVAL" | "EIO" | "EISDIR" | "EMFILE" | "EMSGSIZE" | "ENOENT" | "ENOSPC" | "ENOSYS" | "ENOTCONN" | "ENOTEMPTY" | "ENOTDIR" | "EPERM" | "EPIPE" | "ESPIPE" | "ESRCH" | "ETIMEDOUT";
416
+ export type KernelErrorCode = "EACCES" | "EADDRINUSE" | "EAGAIN" | "EBADF" | "ECONNREFUSED" | "EINPROGRESS" | "EINTR" | "EEXIST" | "EINVAL" | "ELOOP" | "EIO" | "EISDIR" | "EMFILE" | "EMSGSIZE" | "ENOENT" | "ENOSPC" | "ENOSYS" | "ENOTCONN" | "ENOTEMPTY" | "ENOTDIR" | "EPERM" | "EPIPE" | "EROFS" | "ESPIPE" | "ESRCH" | "ETIMEDOUT" | "EXDEV";
376
417
  /**
377
418
  * Structured error for kernel operations.
378
419
  * Carries a machine-readable `code` so callers can map to errno without
@@ -4,6 +4,15 @@
4
4
  * The kernel is the shared OS layer. All runtimes make "syscalls" to the
5
5
  * kernel for filesystem, process, pipe, and FD operations.
6
6
  */
7
+ /** No-op logger that discards all records. */
8
+ export const noopKernelLogger = {
9
+ trace() { },
10
+ debug() { },
11
+ info() { },
12
+ warn() { },
13
+ error() { },
14
+ child() { return noopKernelLogger; },
15
+ };
7
16
  // FD open flags
8
17
  export const O_RDONLY = 0;
9
18
  export const O_WRONLY = 1;
@@ -2,8 +2,25 @@
2
2
  * Virtual Filesystem interface.
3
3
  *
4
4
  * POSIX-complete interface that all filesystem backends must implement.
5
- * Extends the original secure-exec VirtualFileSystem with symlinks,
6
- * links, permissions, and metadata operations needed by WasmVM's WASI polyfill.
5
+ * The primary implementation is ChunkedVFS, which composes an FsMetadataStore
6
+ * (directory tree, inodes, chunk mapping) with an FsBlockStore (key-value blob
7
+ * store) to provide tiered storage with optional write buffering and versioning.
8
+ *
9
+ * Error behavior (KernelError codes):
10
+ * - ENOENT: path does not exist (readFile, stat, pread, pwrite, truncate, readlink, etc.)
11
+ * - EISDIR: operation targets a directory when a file is expected (readFile, pread, pwrite)
12
+ * - ENOTDIR: intermediate path component is not a directory
13
+ * - EEXIST: target already exists (createDir without recursive, link to existing)
14
+ * - ELOOP: symlink resolution exceeds 40 levels
15
+ * - ENOTEMPTY: removeDir on non-empty directory
16
+ * - EPERM: link to directory
17
+ * - EXDEV: cross-mount copy (raised by MountTable, not VFS directly)
18
+ *
19
+ * Optional methods (fsync, copy, readDirStat) may be absent. The kernel and
20
+ * MountTable use optional chaining and provide fallbacks where needed.
21
+ *
22
+ * Usage: create via `createChunkedVfs()` from `./vfs/chunked-vfs.ts`, or use
23
+ * `createInMemoryFileSystem()` from the package root for the default in-memory VFS.
7
24
  */
8
25
  export interface VirtualDirEntry {
9
26
  name: string;
@@ -11,6 +28,9 @@ export interface VirtualDirEntry {
11
28
  isSymbolicLink?: boolean;
12
29
  ino?: number;
13
30
  }
31
+ export interface VirtualDirStatEntry extends VirtualDirEntry {
32
+ stat: VirtualStat;
33
+ }
14
34
  export interface VirtualStat {
15
35
  mode: number;
16
36
  size: number;
@@ -51,4 +71,12 @@ export interface VirtualFileSystem {
51
71
  truncate(path: string, length: number): Promise<void>;
52
72
  /** Read a range from a file without loading the entire file into memory. */
53
73
  pread(path: string, offset: number, length: number): Promise<Uint8Array>;
74
+ /** Write data at a specific offset without replacing the entire file. */
75
+ pwrite(path: string, offset: number, data: Uint8Array): Promise<void>;
76
+ /** Flush buffered writes for the given path to durable storage. */
77
+ fsync?(path: string): Promise<void>;
78
+ /** Copy a file within the same filesystem. */
79
+ copy?(srcPath: string, dstPath: string): Promise<void>;
80
+ /** Combined readdir + stat. Avoids N+1 queries for directory listings. */
81
+ readDirStat?(path: string): Promise<VirtualDirStatEntry[]>;
54
82
  }
@@ -2,7 +2,24 @@
2
2
  * Virtual Filesystem interface.
3
3
  *
4
4
  * POSIX-complete interface that all filesystem backends must implement.
5
- * Extends the original secure-exec VirtualFileSystem with symlinks,
6
- * links, permissions, and metadata operations needed by WasmVM's WASI polyfill.
5
+ * The primary implementation is ChunkedVFS, which composes an FsMetadataStore
6
+ * (directory tree, inodes, chunk mapping) with an FsBlockStore (key-value blob
7
+ * store) to provide tiered storage with optional write buffering and versioning.
8
+ *
9
+ * Error behavior (KernelError codes):
10
+ * - ENOENT: path does not exist (readFile, stat, pread, pwrite, truncate, readlink, etc.)
11
+ * - EISDIR: operation targets a directory when a file is expected (readFile, pread, pwrite)
12
+ * - ENOTDIR: intermediate path component is not a directory
13
+ * - EEXIST: target already exists (createDir without recursive, link to existing)
14
+ * - ELOOP: symlink resolution exceeds 40 levels
15
+ * - ENOTEMPTY: removeDir on non-empty directory
16
+ * - EPERM: link to directory
17
+ * - EXDEV: cross-mount copy (raised by MountTable, not VFS directly)
18
+ *
19
+ * Optional methods (fsync, copy, readDirStat) may be absent. The kernel and
20
+ * MountTable use optional chaining and provide fallbacks where needed.
21
+ *
22
+ * Usage: create via `createChunkedVfs()` from `./vfs/chunked-vfs.ts`, or use
23
+ * `createInMemoryFileSystem()` from the package root for the default in-memory VFS.
7
24
  */
8
25
  export {};
@@ -29,6 +29,10 @@ export interface ProcessConfig {
29
29
  stdoutIsTTY?: boolean;
30
30
  /** Whether stderr is a TTY (PTY slave attached) */
31
31
  stderrIsTTY?: boolean;
32
+ /** Terminal columns (from PTY dimensions). */
33
+ cols?: number;
34
+ /** Terminal rows (from PTY dimensions). */
35
+ rows?: number;
32
36
  }
33
37
  export interface OSConfig {
34
38
  platform?: string;
@@ -72,6 +76,8 @@ export interface ExecOptions {
72
76
  timingMitigation?: TimingMitigation;
73
77
  /** Optional streaming hook for console output events */
74
78
  onStdio?: StdioHook;
79
+ /** Override execution mode. 'run' mode processes async operations (timers, network). */
80
+ mode?: "exec" | "run";
75
81
  }
76
82
  export interface ExecResult extends ExecutionStatus {
77
83
  }