@ai-ecoverse/slicc-kernel 1.142.1 → 1.142.3

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,7 +191,7 @@ 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.
194
+ **Node 26 or later** ([#1129](https://github.com/ai-ecoverse/slicc-kernel/issues/1129)): `engines` asks for Node >=26. V8 in Node 24 sometimes crashed the whole Node process with a `jit_page` check failure while a process worker started or was torn down; slicc-cli measured 15 crashes in 1,600 runs on Node 24 and none in 1,600 on Node 26. The kernel's CI and release run on 26, and the nightly `Node versions` workflow keeps running the suites that start the Node kernel and counts any such crash.
195
195
 
196
196
  ```js
197
197
  import { createNodeKernel, nodeTransport } from '@ai-ecoverse/slicc-kernel/node';
@@ -202,10 +202,12 @@ const { status, stdout } = await kernel.run(['bash', '-c', 'echo hi'], { cwd: '/
202
202
  kernel.terminate();
203
203
  ```
204
204
 
205
- `createNodeKernel({ root, modules, commands, env, network, worker, facades, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch, users, onDenial, approve, compileCache })` 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.
205
+ `createNodeKernel({ root, modules, commands, env, network, worker, facades, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch, users, onDenial, approve, compileCache, warmWorker })` 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.
206
206
 
207
207
  `compileCache` (opt-in, off by default; #1285, slicc-jsh#566) is an absolute host directory 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. 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 `compileCache` refuses to run programs there instead of caching them in the host's directory.
208
208
 
209
+ `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.
210
+
209
211
  By default `nodeTransport()` refuses destinations on the host itself, because Node can reach them and a page can't: local DevTools on 9222–9229, dev servers, local admin APIs, and the cloud metadata address.
210
212
  - **What it refuses:** loopback (`127.0.0.0/8`, `::1`, and their IPv4-mapped forms), unspecified addresses (`0.0.0.0/8`, `::`), `localhost` and `*.localhost`, and link-local addresses (`169.254.0.0/16`, `fe80::/10`), plus AWS's IPv6 metadata address `fd00:ec2::254`.
211
213
  - **How it checks:** it matches on the addresses the name resolves to, so a public name that resolves to `127.0.0.1` is refused too. It checks every request and WebSocket open, `host.slicc.internal` and each redirect hop included.
@@ -1275,6 +1275,8 @@ var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
1275
1275
  var RENAMES = "/.slicc-renames";
1276
1276
  var RENAME_LOCK = "slicc-rename:";
1277
1277
  var RENAME_ADMIT = "slicc-rename-admit:";
1278
+ var escapeRoot = (name2) => name2.replace(/[%:]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
1279
+ var LEGACY_RENAME_LOCK = /^slicc-rename:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
1278
1280
  var PAIRED = /* @__PURE__ */ new Set(["rename", "link", "copyFile"]);
1279
1281
  var LISTINGS = /* @__PURE__ */ new Set(["readdir", "readdirStat"]);
1280
1282
  var RETRY = /* @__PURE__ */ Symbol("retry");
@@ -1439,6 +1441,7 @@ var OpfsFs = class {
1439
1441
  locks;
1440
1442
  lockPrefix;
1441
1443
  rootName;
1444
+ renamePrefix;
1442
1445
  constructor(root, meta = new MemoryMeta(), channel, options = {}) {
1443
1446
  this.root = root;
1444
1447
  this.meta = sidecar(meta);
@@ -1446,6 +1449,7 @@ var OpfsFs = class {
1446
1449
  this.locks = options.locks === void 0 ? lockManager() : options.locks ?? void 0;
1447
1450
  this.lockPrefix = `slicc-excl:${channel ?? ""}:`;
1448
1451
  this.rootName = channel;
1452
+ this.renamePrefix = `${RENAME_LOCK}${escapeRoot(channel ?? "")}:`;
1449
1453
  this.channel = channel === void 0 ? void 0 : new BroadcastChannel(channel);
1450
1454
  this.remote = channel === void 0 ? void 0 : new RemoteWatch(`${channel}:watch`, { locks: this.locks ?? null });
1451
1455
  if (this.remote) {
@@ -2610,7 +2614,7 @@ var OpfsFs = class {
2610
2614
  try {
2611
2615
  const locks = this.locks;
2612
2616
  if (locks) {
2613
- lock = `${RENAME_LOCK}${crypto.randomUUID()}`;
2617
+ lock = `${this.renameLocks()}${crypto.randomUUID()}`;
2614
2618
  this.ownLocks.add(lock);
2615
2619
  call?.release();
2616
2620
  await this.admitted(locks, lock, source.path, destination, place, hooks);
@@ -2702,6 +2706,13 @@ var OpfsFs = class {
2702
2706
  if (!known.trees.some(([a, b]) => a === tree2[0] && b === tree2[1])) known.trees.push(tree2);
2703
2707
  return known;
2704
2708
  }
2709
+ renameLocks() {
2710
+ return this.renamePrefix;
2711
+ }
2712
+ renameHere(name2) {
2713
+ if (name2 === void 0) return false;
2714
+ return name2.startsWith(this.renameLocks()) || LEGACY_RENAME_LOCK.test(name2);
2715
+ }
2705
2716
  async learnRenames() {
2706
2717
  for (const { rename } of await this.meta.under(RENAMES)) {
2707
2718
  if (rename?.lock === void 0 || this.ownLocks.has(rename.lock)) continue;
@@ -2710,7 +2721,7 @@ var OpfsFs = class {
2710
2721
  }
2711
2722
  async renamingElsewhere() {
2712
2723
  const { held = [] } = await this.locks.query();
2713
- return held.some(({ name: name2 }) => name2?.startsWith(RENAME_LOCK) && !this.ownLocks.has(name2));
2724
+ return held.some(({ name: name2 }) => this.renameHere(name2) && !this.ownLocks.has(name2));
2714
2725
  }
2715
2726
  lookForRenames() {
2716
2727
  const locks = this.locks;
@@ -2722,7 +2733,7 @@ var OpfsFs = class {
2722
2733
  this.queries++;
2723
2734
  const { held = [] } = await locks.query();
2724
2735
  const unknown = () => held.some(
2725
- ({ name: name2 }) => name2?.startsWith(RENAME_LOCK) && !this.ownLocks.has(name2) && !this.peerMoves.has(name2)
2736
+ ({ name: name2 }) => this.renameHere(name2) && !this.ownLocks.has(name2) && !this.peerMoves.has(name2)
2726
2737
  );
2727
2738
  if (unknown()) await this.learnRenames();
2728
2739
  if (!unknown()) {
@@ -7584,7 +7595,7 @@ function satisfies(version, range2) {
7584
7595
  }
7585
7596
 
7586
7597
  // src/version.ts
7587
- var KERNEL_VERSION = true ? "1.142.1" : "0.0.0-dev";
7598
+ var KERNEL_VERSION = true ? "1.142.3" : "0.0.0-dev";
7588
7599
  var OS_RELEASE = "6.0.0-slicc";
7589
7600
  function procVersion() {
7590
7601
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC