@ai-ecoverse/slicc-kernel 1.126.1 → 1.127.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.
package/README.md CHANGED
@@ -191,6 +191,8 @@ When the kernel goes away (the page closed or reloaded, `terminate()`), pending
191
191
 
192
192
  `@ai-ecoverse/slicc-kernel/node` runs the same kernel in Node without a browser, so packages built for SLICC can test against it. It is a testing entry, not a supported runtime: there is no OPFS, JS programs get the same guard as in the page but no sandbox (see [JS programs](#js-programs)), and nothing beyond what its tests use is promised.
193
193
 
194
+ **Known issue, Node 24** ([#1129](https://github.com/ai-ecoverse/slicc-kernel/issues/1129)): V8 in Node 24 sometimes crashes the whole Node process with a `jit_page` check failure while a process worker starts or is torn down. slicc-cli measured 15 crashes in 1,600 runs on Node 24 and none in 1,600 on Node 26. Use Node 26 where you can. The `Node versions` workflow (nightly, and on demand) runs the suites that start the Node kernel on both versions and counts the crashes in each job's summary, so the `engines` floor can follow the numbers.
195
+
194
196
  ```js
195
197
  import { createNodeKernel, nodeTransport } from '@ai-ecoverse/slicc-kernel/node';
196
198
 
@@ -430,7 +432,7 @@ The module's default export (or its `main` export) is called with a context and
430
432
  - **`servername`** must be a host name (letters, digits and `-` in labels of at most 63, at most 253 in all, one trailing dot allowed) or an IP literal, else `EINVAL` before anything is dialled. An IP literal sends no SNI and must match an iPAddress subjectAltName.
431
433
  - **The `tls` property** is the handshake's: `{ path: 'kernel', substitutes: false, verified, protocol: 'TLSv1.3' or 'TLSv1.2', alpnProtocol (the agreed protocol, or false), authorized, authorizationError?, servername, peerCertificates, cipher, clientCertificate, sni, hostnameChecked, hostnameMatches? }`.
432
434
  - `peerCertificates` is the server's chain as DER, leaf first.
433
- - `verified` is the kernel's own verdict on the session, whatever the caller's flags: the chain verifies against the kernel's **bundled roots** (never a `ca` the caller passed, which only decides `authorized`) and the leaf names `servername`, which is a host name, not an IP literal. `rejectUnauthorized: false`, `checkHostname: false` and `sni: false` don't change it; a custom `ca` makes it false even when the chain would pass both. It's what sealed substitution in an A2 session will be gated on ([#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)); until then `substitutes` stays false.
435
+ - `verified` is the kernel's own verdict on the session, whatever the caller's flags: the chain verifies against the kernel's **bundled roots** (never a `ca` the caller passed, which only decides `authorized`) and the leaf names `servername`, which is a host name, not an IP literal. `rejectUnauthorized: false`, `checkHostname: false` and `sni: false` don't change it; a custom `ca` makes it false even when the chain would pass both. Sealed substitution in an A2 session is gated on it ([#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)).
434
436
  - `cipher` is the negotiated suite as Node's `getCipher()` gives it: `{ name, standardName, version }`, where `name` is OpenSSL's (`ECDHE-RSA-AES128-GCM-SHA256`) and `standardName` IANA's. As in Node, `version` is the suite's own version, not the session's: `TLSv1.0` for a CBC-SHA suite on a TLS 1.2 session.
435
437
  - `clientCertificate` says whether a client certificate was offered. `sni` says whether the server name was sent: it's false with `sni: false` and for an IP-literal `servername`, which TLS never sends.
436
438
  - `hostnameChecked` says whether the kernel checked the leaf against `servername`. `hostnameMatches` says whether it names it.
@@ -454,7 +456,24 @@ The module's default export (or its `main` export) is called with a context and
454
456
  - **The caller must then check the server's identity itself**, as slicc-jsh does with Node's `checkServerIdentity` and `peerCertificates[0]`; pass it only when the program supplied one.
455
457
  - With `rejectUnauthorized: false` it's `EINVAL`.
456
458
  - **STARTTLS:** `net.connect({ socket: conn, host, tls })` upgrades an open plain connection to an uplink destination in place, as Node's `tls.connect({ socket })` does. It resolves with a connection whose streams now carry the decrypted bytes and whose `tls` is the session. Don't hold a pending read on the plain connection while it upgrades. Pass `host` (or `tls.servername`): the plain connection only knows the address it reached, so without a name the certificate is checked against that IP, and a certificate for the host name fails.
457
- - **Sealed values aren't substituted.** The kernel encrypts end to end, so a placeholder written into an A2 connection reaches the server as written, unsubstituted (it isn't the secret, so nothing leaks). A caller that relies on substitution checks `conn.tls.substitutes` and uses `fetch`, or an A1 connection, for sealed values.
459
+ - **Sealed values in an A2 session** ([#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)) are substituted only at a credential position, only in a session the kernel verified itself, and only as the vault allows. `substitutes` is true when all of these hold:
460
+ - `verified` is true;
461
+ - ALPN didn't agree `h2`;
462
+ - a vault is connected;
463
+ - the uplink said which exit the connection leaves by (`UplinkConn.exit`: `tailscale` or `device:<id>`, the only exits an uplink connection takes). Without one, or with any other value, nothing is substituted.
464
+
465
+ Then:
466
+ - **HTTP/1.1:** if the **first request head** the program writes on the session (it starts with a standard method) has placeholders (`fnox_proxy_<24 hex>`) in exactly one header line, either in the value or in the `user:password` of `Basic` credentials, the kernel asks the vault once (`sessionMaterialize`, below) for that header's position.
467
+ - **What goes out:** the placeholders the vault answers with a value of at least 8 bytes go out as that value (`Basic` decoded, substituted and re-encoded). Everything else goes out as written:
468
+ - the request line and the body;
469
+ - other headers;
470
+ - a head with placeholders in two header lines;
471
+ - later requests on the same connection;
472
+ - anything once the head passes 16 KiB, or once the program pauses for 2 s in the middle of it;
473
+ - SMTP and pg, which come next.
474
+ - **What's read back:** once a process has had a value substituted for a host, everything its whole process tree reads from any A2 session to that host is scrubbed: the process itself, its ancestors and every descendant (forked, spawned or exec'd, before or after the substitution; a forked child's own new session included) with [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub), as S2 does: the value comes back as its placeholder, and a fragment or an encoded form (JSON, percent, HTML, base64, hex) as `[redacted:sealed]`. The scrub stays until the whole tree has ended. A process outside the tree isn't covered. A value shorter than 8 bytes is never substituted, as it couldn't be scrubbed.
475
+ - **The bound:** the server the value goes to is the one the vault rule names, verified against the kernel's own roots. Substitution trusts that server not to reflect the value back in a form the scrub doesn't know (encrypted under a key the program chose, say). That's the same residual as A1's.
476
+ - **Elsewhere:** a placeholder written into any other A2 session reaches the server as written. It isn't the secret, so nothing leaks.
458
477
  - **Limits:** no revocation checking (neither OCSP nor CRLs, as with Node's default), no session resumption, and the clock is the page's `Date.now()`.
459
478
  - `net.resolve(name, { family, signal })` asks the kernel's resolver, which WASIX `resolve` and Emscripten `getaddrinfo` use too: entries in a real `/etc/hosts`, the kernel's own names (`localhost`, the node name, `host.slicc.internal`) and then the uplink's DNS ([#913](https://github.com/ai-ecoverse/slicc-kernel/issues/913)). It answers the addresses, in the resolver's order, for `family` `4`, `6` or `0` (both, IPv4 first; the default). A name with no answer rejects with a `JsCallError` of code `ENOTFOUND`, as Node's `dns.lookup` does, another family with `EAI_FAMILY`, and an aborted `signal` with its reason.
460
479
  - `websocket(url, { protocols, headers, signal })`, a WebSocket over the kernel's transport (its `websocket`, so `fetchTransport({ webSocket: true })` in a browser), shaped like `WebSocketStream`. It resolves once the socket is open, with `url`, the `protocol` the far end chose (always one the program offered: a transport that hands back a socket with another fails the open with `EPROTO`, while browsers and Node's `WebSocket`, which `fetchTransport()` uses, fail such a handshake themselves without saying why, so the open fails with `ECONNREFUSED`), a `readable` of messages (`string` for text, `Uint8Array` for binary), a `writable` for sending them, `closed` (the `{ code, reason }` of the close, from either side, whether or not the program reads) and `close({ code, reason })`. An `http(s):` URL is taken as `ws(s):`. Sending waits while more than 1 MiB is buffered. A signal aborts the open, and after it the socket. A transport without WebSockets rejects with a `TypeError` whose `cause.code` is `ENOTSUP`. One that cannot connect rejects with the `code` its error carries (an errno, such as `EAI_NONAME`, `ECONNREFUSED`, `ETIMEDOUT` or `EPROTO`), else `ECONNREFUSED`. `ws://host.slicc.internal:port` reaches the real machine's loopback, as it does through the proxy;
@@ -940,8 +959,8 @@ The kernel then asks the hook `{ v: 1, op: 'materializeConsume', ticket, stamp,
940
959
 
941
960
  **The child's output is scrubbed (S2).** The kernel compiles a [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub) scrubber (1.0.0, bundled; the published package has no runtime dependency) over exactly the values it swapped in, and gives the child new open file descriptions for fd 1 and fd 2, for every other descriptor it holds on the same descriptions (a `dup` of them), and for every descriptor on a terminal (fd 0 of a terminal session is one), that pass every write through it into the originals (one wrapper per original description, so `2>&1` keeps its order). A value appears as its placeholder, and a fragment or encoded form of it (JSON, percent, HTML, base64, hex, wrapped) as `[redacted:NAME]`. Bytes go through at once unless they may still be the start of a match; such a tail is let out after 50 ms without a write when it's shorter than 16 bytes, and otherwise at the next write or at close. The child's descendants inherit the scrubbed descriptions through fork, spawn, exec and `dup`, and a descendant that opens its controlling terminal (`/dev/tty`, or a terminal by name) gets one scrubbed too, as does a pseudo-terminal slave (`/dev/pts/N`) or [`/dev/clipboard`](#devclipboard) it opens, and a `/dev/clipboard` descriptor it inherited is wrapped at the redeem like fd 1 and 2, so the page's clipboard gets placeholders, never the values. Files, sockets and pipes the child or a descendant opens itself are not scrubbed, so `printf %s "$T" > f` writes the value. A scrubbed fd 1 or 2 is a stream: it doesn't seek. Under `O_NONBLOCK` it gives `EAGAIN` when the pipe or terminal under it would. A write that finds room returns at once, taking at most 64 KiB, and the scrubbed bytes the file couldn't take yet go out before anything written later. Until they have, `poll` reports it unwritable and a nonblocking write gets `EAGAIN`, taking nothing. A blocking write that waits for them, or for room, ends with `EINTR` at a signal, taking nothing; one interrupted after it began writing returns its whole count, and the rest follows. A descendant that redeems a ticket of its own adds its values: its output is scrubbed for both sets. A value under 8 bytes is swapped but can't be scrubbed. The scrubber goes when the last process and description holding it do.
942
961
 
943
- **A2 sessions (`sessionMaterialize`, [#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)).** The request is in place but not sent yet: the next #1090 steps send it from the http1, SMTP and pg writers. For a kernel-terminated TLS session, the kernel will ask the hook `{ v: 1, op: 'sessionMaterialize', caller, stamp, session, host, port, protocol, position, verified, handshakeAt, exit, placeholders, head? }` (slicc-vault 1.10.0, Sessions), once per session:
944
- - `stamp` (also `caller`) is the stamp of the process that owns the socket; `session` the kernel's id for it (16 to 128 of `A-Za-z0-9_-`); `host` the name the leaf was verified against, lower-cased without a trailing dot; `handshakeAt` when the handshake finished (epoch ms); `exit` the exit the session leaves by (`page`, `proxy`, `tailscale` or `device:<name>`);
962
+ **A2 sessions (`sessionMaterialize`, [#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)).** For a kernel-terminated TLS session (see **Sealed values in an A2 session** under JS programs' `ctx.net`), the kernel asks the hook `{ v: 1, op: 'sessionMaterialize', caller, stamp, session, host, port, protocol, position, verified, handshakeAt, exit, placeholders, head? }` (slicc-vault 1.10.0, Sessions), once per session:
963
+ - `stamp` (also `caller`) is the stamp of the process that owns the socket; `session` the kernel's id for it (16 to 128 of `A-Za-z0-9_-`); `host` the name the leaf was verified against, lower-cased without a trailing dot; `handshakeAt` when the handshake finished (epoch ms); `exit` the exit the session leaves by, as the uplink reported it (`tailscale` or `device:<id>`, the id of `[A-Za-z0-9._-]`);
945
964
  - `protocol` is `http1`, `smtp` or `pg`, and `position` one of its own: `header:<lower-cased name>`, `auth-plain` or `auth-login`, or `password-cleartext`; `head` (`{ method, path }`) is there for `http1` only, and required there;
946
965
  - `placeholders` are the 1 to 16 distinct `fnox_proxy_<24 hex>` placeholders found at that position.
947
966
 
@@ -1017,7 +1036,7 @@ In a browser every process worker of the page shares one renderer. V8 reserves a
1017
1036
  - **Below a learned ceiling,** a process that still runs out of memory is started again once another process exits, and the spawn fails with `EAGAIN` after 10 s with none.
1018
1037
  - **`/proc/sys/kernel/slicc_wasm_memories`** reads `<live> <ceiling> <source>`. The source is `default`, `learned`, or `none` (with the ceiling 0).
1019
1038
 
1020
- `/proc/sys/kernel/slicc_shared_root` reads `1` while another kernel shares the root (a tab, or a second kernel in the page) and `0` otherwise. A peer whose kernel ends (its tab closes or reloads) stops counting at once: each kernel queues a request for every peer's alive Web Lock, and the grant tells it the peer has gone ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)). `/proc/sys/kernel/slicc_fs_calls` reads the number of path calls (lookups, stats, opens, creates, removals and the like) the kernel has served its programs since it started. `/proc/sys/kernel/slicc_opfs_calls` counts the OPFS calls the kernel has made to look names up and read stats (`getFile`, `getFileHandle`, `getDirectoryHandle`, `resolve`). With an idle peer, a tar unpack makes at most 1.5 times the calls it makes alone ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)).
1039
+ `/proc/sys/kernel/slicc_shared_root` reads `1` while another kernel shares the root (a tab, or a second kernel in the page) and `0` otherwise. A peer whose kernel ends (its tab closes or reloads) stops counting at once: each kernel queues a request for every peer's alive Web Lock, and the grant tells it the peer has gone ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)). `/proc/sys/kernel/slicc_fs_calls` reads the number of path calls (lookups, stats, opens, creates, removals and the like) the kernel has served its programs since it started. `/proc/sys/kernel/slicc_opfs_calls` counts the OPFS calls the kernel has made to look names up and read stats (`getFile`, `getFileHandle`, `getDirectoryHandle`, `resolve`). `/proc/sys/kernel/slicc_opfs_ops` counts every request the root file system makes of OPFS: those lookups, plus creates, writes (one per write, truncate or write session), reads, listings, renames and removals ([#1017](https://github.com/ai-ecoverse/slicc-kernel/issues/1017)). With an idle peer, a tar unpack makes at most 1.5 times the calls it makes alone ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)).
1021
1040
 
1022
1041
  ### Metadata
1023
1042