@ai-ecoverse/slicc-kernel 1.110.5 → 1.110.7

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
@@ -227,6 +227,8 @@ A command's name is any single path component without whitespace or control char
227
227
 
228
228
  **More roots:** `createKernel({ commands: { append: ['/opt/user/node_modules'] } })` (and `createNodeKernel`) scans more directories, each absolute and normalised (anything else throws a `TypeError`). A name is looked up in a user's own global packages first, then in `modules`, then in root's global packages, then in the appended roots in the order given, and the first match wins, so an appended root never shadows another command (or `sudo`). Their commands are in `/usr/bin` for every user, like root's global packages, and their `binfmt` and file system entries count too. A package in an appended root also gets a command for each `bin` entry of its `package.json` (a string `bin` is named after the package, without its scope), which runs as a `#!` script; a name its `slicc.commands` gives wins. A `bin` entry is skipped when its name isn't a command name (see above), or its file, with symlinks resolved, isn't a regular file inside the package. `/node_modules` and the global packages keep reading only `slicc.commands`. A missing root counts as empty until it appears, and an install or removal in a root is picked up as it happens. With `users: 'enforce'` a root counts only when it and every directory above it are root's and not writable by group or others; another root is skipped without a word. Under `'report'` such a root is still scanned, and reported once to `onDenial` and the report log as `{ pid: 0, uid: 0, op: 'commands', path, errno: 'EACCES' }`. Running a `bin` command needs `r` and `x` on its file under `users` (see [Permissions](#permissions)), checked when it runs, so a `0644` file is `EACCES` under `'enforce'`.
229
229
 
230
+ **Running a glue by path:** executing a package's Emscripten glue by its path runs the command that names it (the one named like the file, when several share it), even with no `.wasm` beside it. A byte-identical copy of the glue in the same directory, named for one of the package's commands, runs as that command too ([#899](https://github.com/ai-ecoverse/slicc-kernel/issues/899)). wasm-clang ships `bin/clang`, `bin/wasm-ld` and the rest as copies of `bin/llvm`, and emcc executes `$LLVM_ROOT/clang`. The caller must be able to read the copy. A copy elsewhere, or a same-named file whose bytes differ, is not the command. The comparison is cached by path, size, mtime and inode.
231
+
230
232
  **Variants by environment** ([homescoop#357](https://github.com/ai-ecoverse/homescoop/issues/357)): one package can carry more than one build of a wasm command and choose between them by the environment. `"variants": [{ "env": { "PYTHON_FORK": "true" }, "wasm": "bin/python-fork.wasm" }]` on a command entry (Emscripten or WASI) is matched in order against the environment the new process gets, at spawn and at exec. Every key must equal its value exactly, and the first match runs its `wasm`; no match runs the command's own `wasm`. This holds however the kernel reaches the entry: by name, by its path (`/usr/bin/python3`, as `sys.executable` gives it to `subprocess` and `multiprocessing`), or through a symlink to that path (a venv's `bin/python`). So `PYTHON_FORK=true python3`, `PYTHON_FORK=true exec /usr/bin/python3` and a child that inherits `PYTHON_FORK=true` all get the variant, and `PYTHON_FORK=1` doesn't. Commands of one package that share a build (`python`, `python3`, `pip`, `pip3`) each carry the same `variants`. A variant replaces only the module: the `abi`, the glue, `env`, `argv0`, `argv0Path` and the `/usr/bin` entry stay the command's. For an Emscripten command that means a variant swaps only the `.wasm` and keeps the command's JS glue, so it must be built against that same glue; a WASI or WASIX command has no glue, so there's no such constraint. Only the chosen module is read, checked and compiled, and compiled modules are cached by its path. A read or `stat` of `/usr/bin/<name>` still shows the default `wasm`. A `variants` that isn't an array, or has an entry without an `env` object of strings and a `wasm` inside the package, is ignored as a whole, with one warning per command, and the command runs its `wasm`. An older kernel ignores the field.
231
233
 
232
234
  A package can also name a command that runs files by their suffix, as `binfmt_misc` does: `"slicc": { "binfmt": { ".jsh": "jsh" } }`. Executing a file that has no `#!` line and is not a wasm module, whose name ends in a mapped suffix, runs that command with the file's path as invoked and the arguments after it, as a `#!` line naming the command would; a `#!` line still wins. The longest matching suffix wins, the whole name is never a suffix (`.jsh` alone is not one), and of two packages that map one suffix, the one whose name sorts first wins, as for commands; the kernel warns once (`console.warn`) when another package maps that suffix to a different command. A suffix starts with a dot and the command must be a name the catalog resolves when the file runs.
@@ -422,7 +424,7 @@ The kernel has one node name, `slicc` unless `createKernel({ hostname })` (or `c
422
424
  - **What `uname` says:** for an Emscripten program built with homescoop's `__syscall_uname` import (wasm-coreutils 9.12.0-7 on), the kernel answers `uname(2)`: sysname `Linux`, nodename the node name, release `6.0.0-slicc` (as `/proc/sys/kernel/osrelease`), version `#1 SMP PREEMPT_DYNAMIC` (as `/proc/version`), machine `wasm32` and domainname `(none)`, so `uname -a` and musl's `gethostname()` see them. `uname -o` is coreutils' compiled-in `Emscripten`. Older Emscripten builds still report their C library's `Emscripten 4.0.23` and `emscripten`, and WASIX programs `wasmer.sh`; the kernel has nothing to answer there.
423
425
  - **Resolving it:** `/etc/hosts` maps the node name and both of those names to `127.0.1.1` (Debian's convention), and name resolution answers them, and `localhost.localdomain`, itself, as `127.0.1.1` and `::1`. So a program that looks up its own host name, as git does for every commit, never asks the [uplink](#uplink), and `127.0.1.1` is the kernel's loopback.
424
426
  - **The trade-off:** a raw connection to the real `wasmer.sh` resolves to loopback too. HTTP(S) to it still goes through the realm proxy and the transport, which resolve names themselves; `wasmer.sh` is deliberately not in `no_proxy`.
425
- - **A real file wins:** a root that has its own `/etc/hostname` (or `/etc/hosts`, `/etc/passwd`) keeps it, and programs read that file rather than the synthetic one. The kernel's name still drives `HOSTNAME` and name resolution.
427
+ - **A real file wins:** a root that has its own `/etc/hostname` (or `/etc/hosts`, `/etc/passwd`) keeps it, and programs read that file rather than the synthetic one. The kernel's name still drives `HOSTNAME` and name resolution. A program that opens one of these synthetic files for writing (as root, since `/etc` is root's) gets a real one first: a root-owned `0644` copy of the defaults. So `echo '10.0.0.9 db' >> /etc/hosts` keeps `localhost` and the other default entries, `>` replaces them as on Linux, and `rm` returns to the synthetic file. The copy is `0644` whatever the umask. Programs that read `/etc/hosts` themselves (WASIX/musl ones, such as python's `socket.gethostbyname`) resolve an added name. The kernel's own resolver, which Emscripten programs, `ctx.net` and HTTP through the transport use, doesn't read the file yet ([#898](https://github.com/ai-ecoverse/slicc-kernel/issues/898)).
426
428
 
427
429
  ### Uplink
428
430
 
@@ -6176,7 +6176,7 @@ function satisfies(version, range2) {
6176
6176
  }
6177
6177
 
6178
6178
  // src/version.ts
6179
- var KERNEL_VERSION = true ? "1.110.5" : "0.0.0-dev";
6179
+ var KERNEL_VERSION = true ? "1.110.7" : "0.0.0-dev";
6180
6180
  var OS_RELEASE = "6.0.0-slicc";
6181
6181
  function procVersion() {
6182
6182
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -22576,13 +22576,34 @@ var Launcher = class {
22576
22576
  return { abi: "emscripten", glue, wasm, argv0: baseName2(argv0 || file), ...asked };
22577
22577
  }
22578
22578
  const owner = await this.glueOwner(glue, baseName2(file), scope);
22579
- return owner && { ...targetOf(owner), ...asked };
22579
+ if (!owner) return void 0;
22580
+ return { ...targetOf(owner), ...asked, ...owner.glue === glue ? {} : { copy: glue } };
22580
22581
  }
22581
22582
  async glueOwner(glue, name2, scope) {
22582
- const owners = [...(await this.commands(scope)).values()].filter(
22583
- (c) => c.abi === "emscripten" && !c.script && c.glue === glue
22584
- );
22585
- return owners.find((c) => c.name === name2) ?? owners[0];
22583
+ const commands = [...(await this.commands(scope)).values()];
22584
+ const owners = commands.filter((c) => c.abi === "emscripten" && !c.script && c.glue === glue);
22585
+ if (owners.length > 0) return owners.find((c) => c.name === name2) ?? owners[0];
22586
+ const leaf = baseName2(glue);
22587
+ const dir = glue.slice(0, glue.lastIndexOf("/"));
22588
+ for (const c of commands) {
22589
+ if (c.abi !== "emscripten" || c.script || c.name !== leaf) continue;
22590
+ const own = await physicalPathAsync(this.base, c.glue).catch(() => c.glue);
22591
+ if (own.slice(0, own.lastIndexOf("/")) !== dir) continue;
22592
+ if (await this.sameFile(glue, own)) return c;
22593
+ }
22594
+ return void 0;
22595
+ }
22596
+ sameFiles = /* @__PURE__ */ new Map();
22597
+ async sameFile(a, b) {
22598
+ const [x, y] = await Promise.all([this.base.stat(a), this.base.stat(b)]).catch(() => []);
22599
+ if (!x?.isFile || !y?.isFile || x.size !== y.size) return false;
22600
+ const stamp = [x, y].map((st) => `${st.size}:${st.mtime.getTime()}:${st.ino}`).join("|");
22601
+ const known = this.sameFiles.get(`${a}|${b}`);
22602
+ if (known?.stamp === stamp) return known.same;
22603
+ const [p, q] = await Promise.all([this.base.readFileBuffer(a), this.base.readFileBuffer(b)]);
22604
+ const same = p.length === q.length && p.every((byte, i) => byte === q[i]);
22605
+ this.sameFiles.set(`${a}|${b}`, { stamp, same });
22606
+ return same;
22586
22607
  }
22587
22608
  async wasmOwner(wasm, name2, scope) {
22588
22609
  const leaf = baseName2(wasm);
@@ -22680,6 +22701,7 @@ var Launcher = class {
22680
22701
  await need(g, "exec", this.fs.resolvePath(req.cwd, file), want);
22681
22702
  }
22682
22703
  for (const bin of planned?.bins ?? []) await need(g, "exec", bin, R_OK | X_OK);
22704
+ if (planned?.target.copy) await need(g, "exec", planned.target.copy, R_OK | X_OK);
22683
22705
  }
22684
22706
  async checkedPlan(req, { ppid, cred, scope }, fds) {
22685
22707
  const refused2 = async (err) => {