Skip to content

Commit e3ab6c3

Browse files
authored
Merge pull request #55 from rivet-dev/ralph/nodejs-conformance-fixes
feat: improve Node.js parity and add dev shell regressions
2 parents ed8c4fc + 931f60f commit e3ab6c3

157 files changed

Lines changed: 20537 additions & 2252 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agent/contracts/compatibility-governance.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,32 @@ Changes affecting bridged or polyfilled Node APIs MUST keep `docs/nodejs-compati
2929
- **WHEN** `docs/nodejs-compatibility.mdx` is updated
3030
- **THEN** the page MUST retain an explicit target Node version statement at the top
3131

32+
### Requirement: Node Conformance Vacuous Self-Skips Must Not Inflate Genuine Pass Counts
33+
Node conformance expectation and reporting flows SHALL reserve `category: "vacuous-skip"` for expected-pass vendored tests that exit `0` only because the test self-skipped without exercising functionality.
34+
35+
#### Scenario: Self-skipping vendored file is treated as vacuous pass
36+
- **WHEN** an expectation is marked `expected: "pass"` only because the vendored test self-skips
37+
- **THEN** it MUST use `category: "vacuous-skip"` and reporting MUST exclude it from the genuine-pass count
38+
39+
#### Scenario: Intentionally skipped file is still a skip
40+
- **WHEN** secure-exec keeps a vendored file under `expected: "skip"` because functionality remains broken or intentionally unsupported
41+
- **THEN** that entry MUST stay under its real failure category rather than `vacuous-skip`
42+
43+
### Requirement: Node Conformance Non-Pass Expectations Must Be Classified By Implementation Intent
44+
Node conformance expectation and reporting flows SHALL classify every non-passing vendored test into exactly one implementation-intent bucket: `implementable`, `will-not-implement`, or `cannot-implement`.
45+
46+
#### Scenario: Remaining non-pass inventory is reported
47+
- **WHEN** expectations or the generated conformance report are updated
48+
- **THEN** the maintained conformance artifacts MUST expose the remaining non-pass counts grouped by implementation intent alongside the existing failure-category breakdown
49+
50+
#### Scenario: Non-pass expectation is categorized
51+
- **WHEN** an expectation remains `expected: "fail"` or `expected: "skip"`
52+
- **THEN** it MUST resolve to exactly one implementation-intent bucket using a specific, verifiable reason that distinguishes policy/out-of-scope exclusions from fundamental architectural blockers
53+
54+
#### Scenario: Conformance target is communicated
55+
- **WHEN** the generated Node conformance report is regenerated
56+
- **THEN** it MUST state that the tracked completion target is 100% of the `implementable` bucket rather than 100% of the upstream vendored suite
57+
3258
### Requirement: Node Compatibility Target Version Tracks Test Type Baseline
3359
The runtime compatibility target MUST align with the `@types/node` package major version used to validate secure-exec tests and type checks. Compatibility documentation and spec references MUST describe the same target major Node line.
3460

@@ -106,6 +132,17 @@ Fixture dependency installation SHALL be cached across repeated test invocations
106132
- **WHEN** fixture files or cache key factors change
107133
- **THEN** the matrix MUST prepare a new cache entry and reinstall dependencies before execution
108134

135+
### Requirement: Kernel-Consolidation Proof Must Use Kernel-Mounted Verification
136+
Stories or docs that claim kernel-consolidation networking behavior is complete SHALL distinguish kernel-mounted proof from compatibility coverage for the retained legacy adapter path.
137+
138+
#### Scenario: Verification targets a retained legacy adapter path
139+
- **WHEN** a test instantiates `createDefaultNetworkAdapter()` or `useDefaultNetwork`
140+
- **THEN** that test MUST be treated as compatibility coverage for the standalone legacy path rather than as proof that kernel-consolidation work is complete
141+
142+
#### Scenario: Verification is used as evidence for kernel-consolidation networking
143+
- **WHEN** a test or document is cited as proof that kernel-backed Node networking works
144+
- **THEN** it MUST execute through `createNodeRuntime()` mounted into a real `Kernel` or an equivalent kernel-mediated path that exercises the shared socket table and host-adapter delegation
145+
109146
### Requirement: Parity Mismatches Remain Failing Until Resolved
110147
Compatibility project-matrix policy SHALL NOT include a "known mismatch" or equivalent pass-through state for parity failures.
111148

.agent/contracts/kernel.md

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ The kernel VFS SHALL provide a POSIX-like filesystem interface with consistent e
6060
- **WHEN** two directory entries refer to the same file through `link(oldPath, newPath)`
6161
- **THEN** `stat(oldPath).ino` and `stat(newPath).ino` MUST be identical until the inode is deleted
6262

63+
#### Scenario: directory nlink reflects self, parent, and child directories
64+
- **WHEN** the InMemoryFileSystem creates or removes directories
65+
- **THEN** each directory MUST report POSIX-style `nlink` metadata: `2` for an empty directory, `2 + childDirectoryCount` for non-root directories, and root `nlink` MUST increase for each immediate child directory
66+
6367
#### Scenario: readDirWithTypes returns entries with type information
6468
- **WHEN** a caller invokes `readDirWithTypes(path)` on a directory containing files and subdirectories
6569
- **THEN** the VFS MUST return `VirtualDirEntry[]` where each entry has `name`, `isDirectory`, and `isSymbolicLink` fields
@@ -111,6 +115,10 @@ The kernel FD table SHALL manage per-process file descriptor allocation with ref
111115
- **WHEN** a process duplicates an FD via `fdDup(pid, fd)`
112116
- **THEN** a new FD MUST be allocated pointing to the same FileDescription, and the FileDescription's `refCount` MUST be incremented
113117

118+
#### Scenario: Duplicated FDs keep deferred-unlink inode data until the last shared close
119+
- **WHEN** a file's pathname is unlinked after `dup`, `dup2`, or fork creates additional FDs that share the same FileDescription
120+
- **THEN** the inode-backed data MUST remain accessible through the remaining shared FD references and MUST be released only when that shared FileDescription's final reference closes
121+
114122
#### Scenario: Dup2 redirects target FD to source FileDescription
115123
- **WHEN** a process invokes `fdDup2(pid, oldFd, newFd)` and `newFd` is already open
116124
- **THEN** `newFd` MUST be closed first, then reassigned to share `oldFd`'s FileDescription with `refCount` incremented
@@ -177,14 +185,29 @@ The kernel process table SHALL manage process lifecycle with atomic PID allocati
177185
- **WHEN** a caller invokes `waitpid(pid)` on a process that has already exited
178186
- **THEN** the Promise MUST resolve immediately with the recorded exit status
179187

180-
#### Scenario: kill sends signal to running process via driver
181-
- **WHEN** a caller invokes `kill(pid, signal)` on a running process
188+
#### Scenario: kill routes default-action signals to the driver
189+
- **WHEN** a caller invokes `kill(pid, signal)` on a running process and the delivered disposition resolves to `SIG_DFL`
182190
- **THEN** the kernel MUST route the signal through `driverProcess.kill(signal)` on the process's DriverProcess handle
183191

184192
#### Scenario: kill on exited process is a no-op or throws
185193
- **WHEN** a caller invokes `kill(pid, signal)` on a process with `status: "exited"`
186194
- **THEN** the kernel MUST NOT attempt to deliver the signal to the driver
187195

196+
### Requirement: Process Signal Handlers And Pending Delivery
197+
The kernel process table SHALL preserve per-process signal dispositions, blocked masks, and pending caught-signal delivery state.
198+
199+
#### Scenario: caught signal handler runs instead of the default driver action
200+
- **WHEN** a running process has a registered caught disposition for a delivered signal
201+
- **THEN** the kernel MUST invoke that handler and MUST NOT route the signal through `driverProcess.kill(signal)` unless a later delivery falls back to `SIG_DFL`
202+
203+
#### Scenario: blocked caught signals remain pending until unmasked
204+
- **WHEN** `sigprocmask()` blocks a delivered signal for a running process
205+
- **THEN** the kernel MUST queue that signal in the process's pending set instead of dispatching it immediately
206+
207+
#### Scenario: unmasking delivers queued pending signals
208+
- **WHEN** `sigprocmask()` later unblocks one or more queued pending signals
209+
- **THEN** the kernel MUST dispatch those pending signals immediately in ascending signal-number order, skipping any that remain blocked
210+
188211
#### Scenario: Zombie processes are cleaned up after TTL
189212
- **WHEN** a process exits and transitions to zombie state
190213
- **THEN** the process entry MUST be cleaned up (removed from the table) after a bounded TTL (60 seconds)
@@ -345,9 +368,20 @@ The kernel pipe manager SHALL provide buffered unidirectional pipes with blockin
345368
- **WHEN** `createPipeFDs(fdTable)` is invoked
346369
- **THEN** the pipe manager MUST create a pipe and install both read and write FileDescriptions as FDs in the specified FD table, returning `{ readFd, writeFd }`
347370

371+
### Requirement: FD Poll Waits Support Indefinite Blocking
372+
The kernel SHALL expose `fdPollWait` readiness waits that can either time out or remain pending until an FD state change occurs.
373+
374+
#### Scenario: poll timeout -1 waits until FD readiness changes
375+
- **WHEN** a runtime calls `fdPollWait(pid, fd, -1)` for a pipe or other waitable FD that is not yet ready
376+
- **THEN** the wait MUST remain pending until that FD becomes readable, writable, or hung up, rather than timing out because of an internal guard interval
377+
348378
### Requirement: Socket Blocking Waits Respect Signal Handlers
349379
The kernel socket table SHALL allow blocking accept/recv waits to observe delivered signals so POSIX-style syscall interruption semantics can be enforced.
350380

381+
#### Scenario: sigaction registration preserves mask and flags
382+
- **WHEN** a runtime registers a caught signal disposition with a signal mask and `SA_*` flags
383+
- **THEN** the kernel MUST retain the handler, blocked-signal mask, and raw flag bits so later delivery and wait-restart behavior observes the same metadata
384+
351385
#### Scenario: SA_RESETHAND resets a caught handler after first delivery
352386
- **WHEN** a process delivers a caught signal whose registered handler includes `SA_RESETHAND`
353387
- **THEN** the kernel MUST invoke that handler once and reset the disposition to `SIG_DFL` before any subsequent delivery of the same signal
@@ -390,6 +424,74 @@ The kernel socket table SHALL reserve listener ports deterministically for loopb
390424
- **WHEN** a loopback `connect()` targets a listening socket whose pending backlog already reached the configured `listen(backlog)` capacity
391425
- **THEN** the connection MUST fail with `ECONNREFUSED` instead of growing the backlog without bound
392426

427+
#### Scenario: listening socket becomes readable while accept backlog is non-empty
428+
- **WHEN** one or more pending connections are queued for a listening socket
429+
- **THEN** `socketTable.poll()` for that listener MUST report `readable: true` until `accept()` drains the backlog
430+
431+
#### Scenario: closing a listener tears down queued unaccepted connections
432+
- **WHEN** a listening socket is closed while its accept backlog still contains pending server-side sockets
433+
- **THEN** the kernel MUST close those queued sockets as part of listener teardown so detached connections do not remain reachable without a listener owner
434+
435+
### Requirement: Socket Options And Per-call Flags Preserve Kernel And Host Semantics
436+
The kernel socket table SHALL track socket options per socket, apply kernel-enforced options during bind/send paths, and preserve per-call read flag semantics across supported socket types.
437+
438+
#### Scenario: getsockopt returns values previously stored by setsockopt
439+
- **WHEN** a caller sets `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_RCVBUF`, `SO_SNDBUF`, or `TCP_NODELAY` on a kernel socket
440+
- **THEN** `getsockopt` MUST return the last value written for that `(level, optname)` pair on that socket
441+
442+
#### Scenario: SO_REUSEADDR changes bind conflict behavior
443+
- **WHEN** a caller binds an internet-domain socket to an already-used local port after setting `SO_REUSEADDR` on the binding socket
444+
- **THEN** the kernel MUST allow that bind instead of rejecting it with `EADDRINUSE`
445+
446+
#### Scenario: host-backed TCP sockets replay stored options after connect
447+
- **WHEN** a socket with previously stored `TCP_NODELAY` or `SO_KEEPALIVE` becomes backed by a host TCP connection
448+
- **THEN** the kernel MUST replay those options onto the host socket
449+
- **AND** later `setsockopt` updates on that connected host-backed socket MUST be forwarded immediately
450+
451+
#### Scenario: MSG_PEEK returns data without consuming it
452+
- **WHEN** `recv()` or `recvFrom()` is called with `MSG_PEEK` and data is queued
453+
- **THEN** the kernel MUST return the readable bytes without removing them from the socket buffer or datagram queue
454+
455+
#### Scenario: MSG_DONTWAIT returns EAGAIN only for empty non-EOF reads
456+
- **WHEN** `recv()` or `recvFrom()` is called with `MSG_DONTWAIT` and no readable data is available yet
457+
- **THEN** the kernel MUST fail immediately with `EAGAIN`
458+
- **AND** if EOF is already known for that read path it MUST still return `null` instead of `EAGAIN`
459+
460+
### Requirement: UDP Datagram Transport Preserves Message Boundaries And Source Addresses
461+
The kernel socket table SHALL model UDP as connectionless datagram delivery, preserving one-send-to-one-recv boundaries and reporting the sender address for each datagram.
462+
463+
#### Scenario: sendTo delivers one datagram to recvFrom with source address metadata
464+
- **WHEN** a bound UDP socket calls `sendTo()` to another kernel-bound UDP socket
465+
- **THEN** the destination socket MUST queue exactly one datagram for `recvFrom()`
466+
- **AND** `recvFrom()` MUST return the sender's address in `srcAddr`
467+
468+
#### Scenario: recvFrom truncates oversized datagrams without leaking the remainder
469+
- **WHEN** a queued UDP datagram exceeds the caller's `maxBytes`
470+
- **THEN** `recvFrom()` MUST return only the leading `maxBytes`
471+
- **AND** the excess bytes MUST be discarded instead of surfacing as a second datagram
472+
473+
#### Scenario: unbound UDP destinations drop silently
474+
- **WHEN** `sendTo()` targets an address with no kernel-bound UDP socket and no host-backed UDP route
475+
- **THEN** the kernel MUST report the datagram length as written
476+
- **AND** the datagram MUST be dropped silently instead of raising `ECONNREFUSED`
477+
478+
#### Scenario: host-backed UDP sockets route through the host adapter
479+
- **WHEN** a bound UDP socket is attached to an external host-backed transport and sends or receives external datagrams
480+
- **THEN** the kernel MUST delegate outbound sends through the configured host adapter
481+
- **AND** inbound host datagrams MUST be surfaced via `recvFrom()` with the host sender address preserved
482+
483+
### Requirement: Kernel Socket Ownership Matches the Process Table
484+
The kernel socket table SHALL only allocate process-owned sockets for PIDs that are currently registered in the kernel process table when the table is kernel-mediated.
485+
486+
#### Scenario: create rejects unknown owner PID in kernel mode
487+
- **WHEN** `createKernel()` provisions the shared `SocketTable` and a caller attempts `socketTable.create(..., pid)` for a PID that is not present in the process table
488+
- **THEN** socket creation MUST fail with `ESRCH`
489+
490+
#### Scenario: process exit cleanup closes only that PID's sockets
491+
- **WHEN** a registered process exits and the kernel runs process-exit cleanup
492+
- **THEN** the socket table MUST close all sockets owned by that PID
493+
- **AND** sockets owned by other still-registered PIDs MUST remain available
494+
393495
### Requirement: Command Registry Resolution and /bin Population
394496
The kernel command registry SHALL map command names to runtime drivers and populate `/bin` stubs for shell PATH-based resolution.
395497

@@ -440,6 +542,17 @@ The kernel permission system SHALL wrap VFS and environment access with deny-by-
440542
- **WHEN** network or child-process permission checks are configured
441543
- **THEN** operations without explicit allowance MUST be denied, consistent with the fs permission model
442544

545+
#### Scenario: Kernel-created socket tables inherit deny-by-default network enforcement
546+
- **WHEN** `createKernel({ permissions })` constructs the shared `SocketTable`
547+
- **THEN** the socket table MUST enforce `permissions.network` for host-visible `listen`, external `connect`, external `send`, host-backed UDP `sendTo`, and host-backed listen/bind operations
548+
- **AND** when `permissions.network` is missing those external socket operations MUST fail with `EACCES`
549+
- **AND** loopback routing to kernel-owned listeners MUST remain allowed without a host-network allow rule
550+
551+
#### Scenario: AF_UNIX sockets bypass host-network permission checks
552+
- **WHEN** a caller binds, listens on, connects to, or sends through an `AF_UNIX` socket path
553+
- **THEN** the kernel MUST keep that traffic entirely in-kernel without consulting `permissions.network`
554+
- **AND** missing Unix listeners MUST fail with `ECONNREFUSED` instead of `EACCES`
555+
443556
#### Scenario: Preset allowAll grants all operations
444557
- **WHEN** `allowAll` permission preset is used
445558
- **THEN** all filesystem, network, child-process, and env operations MUST be allowed

0 commit comments

Comments
 (0)