@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.
- package/dist/generated/isolate-runtime.d.ts +2 -2
- package/dist/generated/isolate-runtime.js +2 -2
- package/dist/index.d.ts +17 -4
- package/dist/index.js +10 -2
- package/dist/isolate-runtime/require-setup.js +1489 -239
- package/dist/isolate-runtime/setup-dynamic-import.js +31 -0
- package/dist/kernel/device-backend.d.ts +14 -0
- package/dist/kernel/device-backend.js +251 -0
- package/dist/kernel/device-layer.js +9 -0
- package/dist/kernel/file-lock.js +2 -3
- package/dist/kernel/index.d.ts +4 -4
- package/dist/kernel/index.js +3 -3
- package/dist/kernel/kernel.js +141 -122
- package/dist/kernel/mount-table.d.ts +75 -0
- package/dist/kernel/mount-table.js +353 -0
- package/dist/kernel/permissions.d.ts +9 -0
- package/dist/kernel/permissions.js +33 -1
- package/dist/kernel/proc-backend.d.ts +30 -0
- package/dist/kernel/proc-backend.js +428 -0
- package/dist/kernel/proc-layer.js +6 -0
- package/dist/kernel/process-table.d.ts +3 -1
- package/dist/kernel/process-table.js +23 -3
- package/dist/kernel/pty.d.ts +3 -2
- package/dist/kernel/pty.js +13 -2
- package/dist/kernel/socket-table.d.ts +7 -0
- package/dist/kernel/socket-table.js +99 -35
- package/dist/kernel/types.d.ts +45 -4
- package/dist/kernel/types.js +9 -0
- package/dist/kernel/vfs.d.ts +30 -2
- package/dist/kernel/vfs.js +19 -2
- package/dist/shared/api-types.d.ts +6 -0
- package/dist/shared/bridge-contract.d.ts +21 -3
- package/dist/shared/bridge-contract.js +2 -0
- package/dist/shared/console-formatter.js +8 -8
- package/dist/shared/global-exposure.js +95 -0
- package/dist/shared/in-memory-fs.d.ts +14 -59
- package/dist/shared/in-memory-fs.js +97 -597
- package/dist/shared/permissions.js +5 -0
- package/dist/test/block-store-conformance.d.ts +34 -0
- package/dist/test/block-store-conformance.js +251 -0
- package/dist/test/metadata-store-conformance.d.ts +37 -0
- package/dist/test/metadata-store-conformance.js +646 -0
- package/dist/test/vfs-conformance.d.ts +65 -0
- package/dist/test/vfs-conformance.js +842 -0
- package/dist/types.d.ts +1 -0
- package/dist/vfs/chunked-vfs.d.ts +66 -0
- package/dist/vfs/chunked-vfs.js +1290 -0
- package/dist/vfs/host-block-store.d.ts +19 -0
- package/dist/vfs/host-block-store.js +97 -0
- package/dist/vfs/memory-block-store.d.ts +16 -0
- package/dist/vfs/memory-block-store.js +45 -0
- package/dist/vfs/memory-metadata.d.ts +75 -0
- package/dist/vfs/memory-metadata.js +528 -0
- package/dist/vfs/sqlite-metadata.d.ts +91 -0
- package/dist/vfs/sqlite-metadata.js +582 -0
- package/dist/vfs/types.d.ts +210 -0
- package/dist/vfs/types.js +8 -0
- package/package.json +20 -1
- package/dist/kernel/inode-table.d.ts +0 -43
- 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
|
-
//
|
|
200
|
-
|
|
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 &&
|
|
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 = {
|
|
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" &&
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
//
|
|
471
|
-
|
|
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 ||
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
-
})
|
|
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" &&
|
|
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 &&
|
|
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" ||
|
|
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" &&
|
|
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: {
|
|
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" ||
|
|
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
|
}
|
package/dist/kernel/types.d.ts
CHANGED
|
@@ -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
|
package/dist/kernel/types.js
CHANGED
|
@@ -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;
|
package/dist/kernel/vfs.d.ts
CHANGED
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
* Virtual Filesystem interface.
|
|
3
3
|
*
|
|
4
4
|
* POSIX-complete interface that all filesystem backends must implement.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
}
|
package/dist/kernel/vfs.js
CHANGED
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
* Virtual Filesystem interface.
|
|
3
3
|
*
|
|
4
4
|
* POSIX-complete interface that all filesystem backends must implement.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
}
|