@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 +4 -2
- package/dist/kernel-worker.js +15 -4
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +83 -8
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +1 -1
- package/dist/types/fs/opfs.d.ts +3 -0
- package/dist/types/node.d.ts +8 -1
- package/package.json +7 -7
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
|
-
**
|
|
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.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -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 = `${
|
|
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
|
|
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
|
|
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.
|
|
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
|