@ai-ecoverse/slicc-kernel 1.162.0 → 1.163.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/README.md CHANGED
@@ -207,7 +207,7 @@ kernel.terminate();
207
207
 
208
208
  `createNodeKernel({ root, modules, commands, env, network, worker, facades, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch, users, onDenial, approve, compileCache, warmWorker, warmWorkers })` takes the options of `createKernel` except `metadata`, and `fstabRetries`, the delays in ms between tries of a failing `/etc/fstab` line (default `[1000, 4000, 16000]`). `root` is an in-memory directory by default (`memoryRoot()` makes another), which keeps each file in 1 MiB chunks, and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal`, `users`, `setRoutes`, `updateTransportTraits` and `terminate` as above, plus `integrity.revoke(path?)`, `root`, `writeFile(path, data, { mode }?)` (creating the parent directories; `mode` sets the permission bits, `0` to `0o7777`, before the data is written), `chmod(path, mode)` and `readFile(path)` to put files in place and read results; like the `fs` of an attached client, they go through the mounts, as the processes see them. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect({ user })` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
209
209
 
210
- `compileCache` (#1285, slicc-jsh#566) is where each process worker keeps the compiled code of the JS programs it imports (`module.enableCompileCache`), so the next spawn of the same program skips V8's compile. **By default a Node kernel writes a private directory of its own** (#1597): `slicc-kernel-cc-<pid>-<random>` under the host's temp directory (`os.tmpdir()`), mode 0700, removed by `terminate()`. The first kernel in a host process also removes any such directory whose process is gone, so a crashed host leaves none behind for good. Measured on Node 26 with a 2 MB module, the default takes a spawn from about 21 to 8 ms and adds no measurable RSS per process. `compileCache: false` turns it off, and an absolute host directory uses that one instead, shared by every kernel given it. It stores V8's code cache, keyed and checksummed by the module's source, one file per program (about 7 MB for a 2.4 MB bundle), shared by every kernel given the same directory; programs can't reach or write it (the worker hands it to Node before the lockdown, and `node:worker_threads` stays refused). Measured on a 2.4 MB bundle, a spawn went from about 54 to 39 ms; a trivial program costs about 1 ms more. **The directory must be private to the embedder:** V8's code cache is trusted bytecode that Node only checksums, so whoever can write it can plant code that runs inside a process worker past the lockdown. Never make it reachable from inside the kernel (no `hostfs` mount of it or of a parent), and don't share one between kernels of different trust. A host's own `NODE_COMPILE_CACHE` is never handed to the process workers, with or without the option, so programs never land in a host-wide cache. If a host preload (`--import` or `--require`) has already enabled another compile cache in the workers, a kernel given a `compileCache` directory refuses to run programs there instead of caching them in the host's directory; a kernel on its default private directory leaves it to the host's, as it did before the default.
210
+ `compileCache` (#1285, slicc-jsh#566) is where each process worker keeps the compiled code of the JS programs it imports (`module.enableCompileCache`), so the next spawn of the same program skips V8's compile. **By default a Node kernel writes a private directory of its own** (#1597): `<pid>-<random>` in a per-user parent `slicc-kernel-cc-u<uid>` under the host's temp directory (`os.tmpdir()`), both mode 0700, removed by `terminate()`. A parent that isn't a real directory of this user closed to others (a symlink, another user's, or group- or world-accessible) isn't used, and the kernel runs without a cache. The first kernel in a host process also removes the parent's directories whose process is gone, so a crashed host leaves none behind for good; it lists only that parent, never the temp directory itself, which can hold tens of thousands of entries (1.160.3 listed it, costing a host about 15 MB of RSS and 30 ms at start, slicc-jsh#566). Measured on Node 26 with a 2 MB module, the default takes a spawn from about 21 to 8 ms and adds no measurable RSS per process. `compileCache: false` turns it off, and an absolute host directory uses that one instead, shared by every kernel given it. It stores V8's code cache, keyed and checksummed by the module's source, one file per program (about 7 MB for a 2.4 MB bundle), shared by every kernel given the same directory; programs can't reach or write it (the worker hands it to Node before the lockdown, and `node:worker_threads` stays refused). Measured on a 2.4 MB bundle, a spawn went from about 54 to 39 ms; a trivial program costs about 1 ms more. **The directory must be private to the embedder:** V8's code cache is trusted bytecode that Node only checksums, so whoever can write it can plant code that runs inside a process worker past the lockdown. Never make it reachable from inside the kernel (no `hostfs` mount of it or of a parent), and don't share one between kernels of different trust. A host's own `NODE_COMPILE_CACHE` is never handed to the process workers, with or without the option, so programs never land in a host-wide cache. If a host preload (`--import` or `--require`) has already enabled another compile cache in the workers, a kernel given a `compileCache` directory refuses to run programs there instead of caching them in the host's directory; a kernel on its default private directory leaves it to the host's, as it did before the default.
211
211
 
212
212
  `warmWorker` (on by default; `false` turns it off; #1341, slicc-jsh#566) keeps one process worker booted ahead of the next spawn, fork or thread on the Node entry, so the spawn skips the worker's boot: it has loaded the process worker module and nothing else, no pid, credentials, environment, cwd, descriptors, lockdown or program, all of which arrive with its init message, and every worker still serves exactly one process or thread and is then terminated. Its own `process.env` is a snapshot taken when the spare is created rather than at the spawn, but a program never sees it (`process` is withheld, and a program's environment is the one its spawn gives it). The spare never keeps the host alive, is replaced after each spawn off the critical path, is dropped if it errors or exits while idle, and is released after 30 s without a spawn; `terminate()` ends it. It costs about 11 MB of host RSS while it waits. `warmWorkers: n` (opt-in; #1361, slicc-jsh#566) keeps up to `n` spares instead of one (an integer from 0 to 64; `warmWorker: false` still means none), so a burst of starts, as worker pools make, finds a spare for each: all are refilled after each take off the critical path, and all are released together after 30 s without a spawn and ended by `terminate()`. Each costs about 7–11 MB of host RSS while it waits. It pays off for bursts of `Worker` threads (jsh measured about 13 ms less per start in a burst of 8); for bursts of child processes that boot a large runtime of their own, the refills compete for CPU with the children and the gain is small or none.
213
213
 
@@ -422,6 +422,11 @@ The module's default export (or its `main` export) is called with a context and
422
422
  - `asyncContext` (the Node entry only; absent in a browser, so a runtime feature-detects it; #1350, slicc-jsh#375) is `{ run(store, fn, ...args), getStore(), enterWith(store) }`, backed by one host `AsyncLocalStorage` the process worker makes for each program, since a program can't import `node:async_hooks` and the realm has no promise hooks of its own. A store follows the program's native `await` continuations and promise jobs, as an `AsyncLocalStorage` does in Node, so Next.js, OpenTelemetry and request-context loggers keep their context. Each process and each thread has its own. Only these three frozen functions on a frozen null-prototype object reach the program: they hold the host storage in a closure, `run` calls the program's function with its own arguments, and nothing they return or expose is a host object.
423
423
  - `sync.runScript(source, { filename })` (#1541, slicc-jsh#977) runs `source` as a classic script in the program's own global scope, synchronously, as Node's `vm.runInThisContext` does: `let`, `const` and `class` land in the global lexical scope, `var` (in strict code too) on the global object, and stack frames are named `filename` (default `[eval]`). On the Node entry it returns `{ value, completion: true }` with the completion value, and an error the script throws is thrown as it is. In a browser, where process workers are module workers with no synchronous classic-script path, it returns `undefined`, so a runtime feature-detects it with `ctx.sync.runScript('1') !== undefined`. A non-string `source` or `filename` is a `TypeError` everywhere. The script sees exactly the globals the program sees, `import()` inside it is refused, and a `var`, function or `let` declaration can't replace a global the program can't assign: a non-configurable one stays as it is, and the only configurable, non-assignable ones (`crypto`, `navigator`) are ones the program can already redefine with `Object.defineProperty`.
424
424
  - `sync.promiseState(promise)` (#1531, slicc-jsh#974) tells a native promise's state synchronously, as Node's `util.inspect` does: `{ state: 'pending' | 'fulfilled' | 'rejected', value }`. `value` is the very fulfillment value or rejection reason (`undefined` while pending), and probing doesn't handle a rejection. On the Node entry it reads the promise through an inspector session the process worker opens on first use, one per process and per thread; the probe runs no program code. Anything that isn't a native promise (a thenable, a proxy) is a `TypeError`. It answers `undefined` in a browser, where a worker has no inspector, on a Node built without one, once a program has changed `Object.prototype` or put a `toJSON` on a built-in prototype, and when the global object can't take a new property (`Object.preventExtensions(globalThis)`, or a frozen global).
425
+ - `sync.setStackFormatter(fn | null)` (#1612, slicc-jsh#1091) is Node only, so check it with `typeof ctx.sync.setStackFormatter === 'function'`. It returns the previous formatter, and `null` removes it.
426
+ - **When `fn` runs:** it formats an error's `.stack` the way V8's prepare-stack-trace callback does, as `fn(error, callSites)` returning the string. It runs only when no own `Error.prepareStackTrace` function is set, as in Node, so `Error.prepareStackTrace` stays an ordinary property: after `delete` it's `undefined`, `Object.getPrototypeOf(Error)` stays `Function.prototype`, and an assignment to a frozen subclass throws in strict mode.
427
+ - **What `fn` gets:** a fresh array of V8 CallSites holding only the program's own frames (its modules, `data:` imports, `sync.runScript` scripts and code they `eval`), `node:` frames and native frames. Kernel frames are dropped by script identity, so a script named like a kernel file doesn't let them through, and so are host files. A program's strict frames give `getFunction()` and `getThis()` as `undefined`.
428
+ - **Fallback:** if `fn` throws or returns a non-string, or an error is formatted while `fn` runs, the default stack is used.
429
+ - **How it works:** process workers start with `--expose-internals`, so the worker can install the formatter before the lockdown. Host flags a worker refuses are dropped. If the worker still can't start with the flag, the method is absent. Every `internal/*` and `node:internal/*` module stays refused to programs, as do `node:module` and `node:worker_threads`.
425
430
  - **Kernel frames in a program's stacks** (#1583): on the Node entry, the process worker's bundle names itself `slicc-kernel:/node-process-worker.js` (a `sourceURL`), so the kernel's own frames in an error a program sees (its own errors, a failed `ctx` call, `runScript`) show that name and not the host directory the kernel is installed in. The same holds in `CallSite.toString()` and `getScriptNameOrSourceURL()`, which a program's own `Error.prepareStackTrace` gets; `getFileName()` still returns the bundle's file URL. Line and column numbers are unchanged, so they still point into `dist/node-process-worker.js`. Don't run an embedder with `--enable-source-maps` if program-visible paths matter: Node then maps the kernel's frames back to the absolute paths of its sources. In a browser the frames keep the bundle's URL, because Chrome ignores `sourceURL` in a module worker loaded by URL.
426
431
  - `module` (#1598, slicc-jsh#566) is a frozen `{ path }` with the command module's own path in the kernel's file system (e.g. `/usr/lib/node_modules/jsh/dist/jsh.mjs`), so a program can read its package's other files through `ctx.fs` on first use. It's the path the kernel found the package under, with symlinks left as they are (a pnpm-style `node_modules/<name>` link stays in it), and reads the same files through `ctx.fs`. It's a kernel path, never a host one, and a thread gets the same. It's absent for the kernel's built-in programs (`sudo`, `crontab`) and for a program run from inline source with no file.
427
432
  - `memory()`, synchronous and without a kernel call, measures the calling isolate: each `thread` answers for its own (#1240, slicc-jsh#397). It returns a fresh plain object of numbers in bytes, each field only where it's measured: `heapUsed`, `heapTotal`, `heapLimit` (V8's heap size limit), `external`, `arrayBuffers` and `rss`. On the Node entry they come from V8's heap statistics and Node's memory usage, through functions the worker captured before its lockdown, so nothing reachable from the program leads back to `process` or `v8`. `rss` is the host process's resident set size, as a Node worker thread reports it in `process.memoryUsage().rss`: it isn't per process, but shared by every process and thread of the kernel, and by any other kernel in that host process (#1563, slicc-jsh#539). In a browser `memory()` answers `{}` for now, with no `rss`.
@@ -529,7 +534,7 @@ The module's default export (or its `main` export) is called with a context and
529
534
  - A connection's `tls` is `{ path: 'kernel', protocol, cipher ({ name, standardName, version }, as a client's; [#1500](https://github.com/ai-ecoverse/slicc-kernel/issues/1500)), alpnProtocol, servername (the SNI the client sent, or ''), authorized (whether a client certificate verified), authorizationError?, peerCertificates (the client's chain) }`.
530
535
  - On a `listen({ tls })` listener, the kernel handshakes accepted connections concurrently, from the first `accept` on, and `accept` (and `for await`) yields each as its handshake finishes, so a client that connects and stays silent doesn't hold up the next. It holds at most `backlog` (up to 64) connections in a handshake or handshaken but not yet taken; beyond that it stops accepting, leaving the rest in the kernel's backlog, until one finishes or is taken. A connection whose handshake fails is closed and never yielded. `secure()` rejects instead.
531
536
  - `handshakeTimeout` (milliseconds, 120000 by default, as Node's) ends a handshake that hasn't finished with `ERR_TLS_HANDSHAKE_TIMEOUT`. A listener closes that connection. After `secure()` rejects, the connection stays open and is the program's to answer or close, as with Node's `tlsClientError` ([#1504](https://github.com/ai-ecoverse/slicc-kernel/issues/1504)).
532
- - A client that sends a plain HTTP request instead of a ClientHello fails the handshake with `ERR_SSL_HTTP_REQUEST`, as in Node. A client that resets during the handshake fails it with `ECONNRESET` and `read ECONNRESET`, as Node's server reports it, and one that closes with `ECONNRESET` and `the client closed the connection during the TLS handshake` ([#1578](https://github.com/ai-ecoverse/slicc-kernel/issues/1578)). A handshake, server or loopback client, waits without holding the program's other calls, and an aborted `accept({ signal })` or `connect({ signal })` ends it. A signal delivered to the program doesn't interrupt a handshake, as with `SA_RESTART`.
537
+ - A client that sends a plain HTTP request instead of a ClientHello fails the handshake with `ERR_SSL_HTTP_REQUEST`, as in Node. A client that resets during the handshake fails it with `ECONNRESET` and `read ECONNRESET`, as Node's server reports it, and one that closes with `ECONNRESET` and `the client closed the connection during the TLS handshake` ([#1578](https://github.com/ai-ecoverse/slicc-kernel/issues/1578)). A loopback TLS client whose server resets before or during the handshake fails the same way, with `read ECONNRESET`, not `EPIPE` ([#1611](https://github.com/ai-ecoverse/slicc-kernel/issues/1611)). A handshake, server or loopback client, waits without holding the program's other calls, and an aborted `accept({ signal })` or `connect({ signal })` ends it. A signal delivered to the program doesn't interrupt a handshake, as with `SA_RESTART`.
533
538
  - **Choosing per connection** ([#1501](https://github.com/ai-ecoverse/slicc-kernel/issues/1501), [#1502](https://github.com/ai-ecoverse/slicc-kernel/issues/1502), tls-engine 4.2.0-9's `TLS_SERVER_SELECT`), for Node's `SNICallback`, `addContext` and `ALPNCallback`: `tls.select({ servername, protocols })` on `listen` or `secure` is called once the client's ClientHello is in, with the SNI (or `''`) and the protocols the client offers. It returns (or resolves with) `{ context, alpn }` or `{ refuse }`:
534
539
  - `context` is a certificate from `net.tlsContext({ cert, key, passphrase })` (or its `fd`); without it the listener's own `cert` serves, and with `select` that one is optional. `alpn` is one of `protocols`, or none; another is `EINVAL` and refuses the handshake. `select` and a fixed `alpn` list together are `EINVAL` (`ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS`), as in Node.
535
540
  - `refuse` sends a fatal alert and ends the handshake: `'unrecognized_name'` (112), `'no_application_protocol'` (120), `'handshake_failure'` (40), `'access_denied'` (49) or `'internal_error'` (80), or the number. A `select` that throws refuses with 40, and one that doesn't answer within `handshakeTimeout` times the handshake out. On a listener the connection is dropped and the listener keeps accepting; `secure()` rejects with the error.
@@ -6650,7 +6650,9 @@ var KernelSocket = class _KernelSocket {
6650
6650
  throw new KernelError("ECONNRESET", "read ECONNRESET");
6651
6651
  return bytes.length > 0 ? bytes : null;
6652
6652
  },
6653
- write: (bytes) => raw2.tx.write(bytes),
6653
+ write: (bytes) => raw2.tx.write(bytes).catch((err) => {
6654
+ throw this.refused ? new KernelError("ECONNRESET", "read ECONNRESET") : err;
6655
+ }),
6654
6656
  closeWrite: () => raw2.tx.closeWrite(),
6655
6657
  close: () => {
6656
6658
  raw2.rx.closeRead();
@@ -8435,7 +8437,7 @@ function satisfies(version, range2) {
8435
8437
  }
8436
8438
 
8437
8439
  // src/version.ts
8438
- var KERNEL_VERSION = true ? "1.162.0" : "0.0.0-dev";
8440
+ var KERNEL_VERSION = true ? "1.163.0" : "0.0.0-dev";
8439
8441
  var OS_RELEASE = "6.0.0-slicc";
8440
8442
  function procVersion() {
8441
8443
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC