@ai-ecoverse/slicc-kernel 1.170.2 → 1.170.4
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 +12 -4
- package/dist/kernel-worker.js +163 -43
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.cjs +3 -3
- package/dist/node-process-worker.cjs.map +2 -2
- package/dist/node-process-worker.js +3 -3
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +163 -43
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +3 -3
- package/dist/process-worker.js.map +2 -2
- package/dist/types/commands.d.ts +2 -1
- package/dist/types/kernel/net/http-syscalls.d.ts +1 -0
- package/dist/types/kernel/net/placeholder-floor.d.ts +8 -0
- package/dist/types/launcher.d.ts +2 -0
- package/dist/types/realm/sync-fs-wire.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -234,7 +234,7 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
|
|
|
234
234
|
|
|
235
235
|
A command's name is any single path component without whitespace or control characters, so coreutils' `[` is in `/usr/bin` too; a name that is empty, `.` or `..`, or has a `/`, a space, a tab, a newline, NUL or another control character is skipped. Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The catalog reads its roots in order, and the first root that provides a name wins it: a user's own global packages (for that user's processes), `/node_modules`, root's global packages, then each appended root. Within a root, packages are read by name and the first wins (compared as strings by UTF-16 code unit, so a scoped `@scope/name` comes before names that start with a letter, but after those that start with a digit), whatever order the file system lists them in; a `PNPM_HOME`'s install groups count as one root, read group by group (by the group directory's name, effectively arbitrary for pnpm's hashed names), each by package name. A variant can take over from the package it extends with `"slicc": { "replaces": ["@ai-ecoverse/wasm-imagemagick"] }`: when it and a package it names are in the same root, it wins every command name they share, whatever the order; names only the other provides stay the other's, a third package that provides the name and sorts first still wins it, and packages that replace each other fall back to the name order. A replace never reaches into an earlier root, so root's global packages can't take a name from `/node_modules` or a user's own packages. The field must be an array of package names; anything else is ignored. `sh` runs bash as `sh` unless a package provides its own. A package's `engines.slicc-kernel` is a floor the kernel holds it to: when the running kernel's version doesn't satisfy that npm range (comparators, `^`, `~`, x-ranges, hyphen ranges, `||`), or the range can't be read, its commands are refused. They stay in `/usr/bin`, and running one by name, by path or as a `#!` interpreter exits 126 with `<cmd>: <pkg>@<version> needs slicc-kernel <range>; this kernel is <version>` on stderr. Each refused package is logged once to the console. A non-string `engines.slicc-kernel`, or none, is no constraint. A development build reports the newest release tag as `<tag>+dev` and checks against that; a build of unknown version (`0.0.0`) checks nothing. The refusal follows the program, not the name: its glue or `.wasm` run by path (directly or through a symlink), and a byte-identical copy of its glue beside it named for one of its commands (#390, #899), exit 126 with the same message ([#1235](https://github.com/ai-ecoverse/slicc-kernel/issues/1235)). Only commands are refused: a package without commands (a `py-*` library, a side module) isn't, and a refused package that replaces base commands makes those commands fail with 126 rather than fall back to the package it replaces. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. An entry reads as its program, as a PATH search that opens what it finds expects (`ruby -S rake`, `perl -S`, Pod::Usage looking itself up, bundler checking that `/usr/bin/ruby` isn't empty). A script command reads as its script, a JS command as its module, and a WASI or Emscripten command as its `.wasm`, so `cat /usr/bin/hello` prints the script and `head -c4 /usr/bin/cat` gives `\0asm`. The entry is a plain file and no link: it stays a root-owned `0755` regular file with its own inode, `readlink` of it is `EINVAL`, and `which` and `command -v` print `/usr/bin/<name>`. No file stands behind it, so unlinking it is `ENOENT`, open or not, for a user's own entry in `~/.local/share/pnpm/bin` too: `rm -f` does nothing, `pnpm remove -g` counts it as gone, and the entry leaves with its package. Its size and times are the program's when the caller may read the program (anyone, for a kernel-internal look or with users off, only when everyone may), and 0 and the epoch otherwise. Running it still runs the catalog command, with its `env`, `args` and `argv0`. The read is the caller's: with users on, reading an entry also needs `r` on its program, and that includes a user's own commands in `~/.local/share/pnpm/bin`, so an entry whose program the caller can't read is `EACCES`. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program; when it is the `wasm` of a WASI command in the caller's catalog, the user's own packages included, it runs as that command, with its `env` defaults, `args` and `argv0`, the command whose name matches the file's, without `.wasm`, if several share it, while a copy elsewhere runs bare), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. A spawn by a bare name (no `/`) that the catalog doesn't have, and that didn't ask for no search (a WASIX `posix_spawn` or `execve`, as opposed to its `posix_spawnp`/`execvp`, keeps its bare name), searches the `PATH` of the spawn's own environment, in order, with an empty entry meaning the working directory; each candidate is run exactly as a spawn of that path would be. The first that runs wins; if one was found but refused and nothing else matched, the spawn fails with `EACCES` (126), otherwise `ENOENT` (127). A catalog command still wins over the same name on `PATH`, the reverse of a shell's order. A searched bare name is never looked up in the working directory itself: a file of that name there runs only through an empty `PATH` entry, as with `execvp` ([#1234](https://github.com/ai-ecoverse/slicc-kernel/issues/1234)). The `PATH` searched is the new process's, from the spawn's environment; POSIX `posix_spawnp` uses the caller's, which is the same unless the caller hands the child another one. This is `posix_spawnp`/`execvp` semantics for every caller that asks for a search: JS programs' `ctx.spawn`, WASI programs' spawn import, WASIX `posix_spawnp`, and Emscripten programs built on homescoop's shim before its own `PATH` search (gmake, tar, git and others hand the kernel the bare name). wasm-coreutils from 9.12.0-11 searches `PATH` itself and hands the kernel a path ([#1165](https://github.com/ai-ecoverse/slicc-kernel/issues/1165), [#1168](https://github.com/ai-ecoverse/slicc-kernel/issues/1168)). A `..` in a path that is run is resolved physically, as the file system resolves it: `pkg/../tools/x` after a symlinked `pkg` is the `tools/x` beside the link's target, and its exec permission is checked there. The walk is the caller's: with users on, each directory it passes needs search (`x`) permission (`EACCES`, 126 in bash, as on Linux), and a sudo process follows only the links it trusts, as for any other path. Commands also come from pnpm's global directory. Every process gets a `PNPM_HOME` and has `$PNPM_HOME/bin` on `PATH`: root's is `/usr/local/share/pnpm` (unless the embedder sets its own), a user's `~/.local/share/pnpm` (see [Users](#users)). The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
|
|
236
236
|
|
|
237
|
-
**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'`.
|
|
237
|
+
**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`). Commands of a package that a global install only brought along as a dependency come after all of these: see [A global package's dependencies](#commands). 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'`.
|
|
238
238
|
|
|
239
239
|
**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.
|
|
240
240
|
|
|
@@ -242,6 +242,8 @@ A command's name is any single path component without whitespace or control char
|
|
|
242
242
|
|
|
243
243
|
**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.
|
|
244
244
|
|
|
245
|
+
**A global package's dependencies** ([slicc-cdp#676](https://github.com/ai-ecoverse/slicc-cdp/issues/676)): pnpm hoists a global install's dependencies into the same `node_modules` as the package itself, so the kernel tells them apart by the `dependencies` of the global project's `package.json` (`global/v11/<project>/package.json`), which names only what was installed directly. A package there that the list doesn't name keeps its commands, but they are looked up last, after every other root, for root's global packages as for a user's. So installing a package never changes which `sed` or `playwright-cli` you run through its dependencies, and a dependency's `perl` still runs when nothing else provides `perl`. A package installed directly in any of a user's global projects counts as direct, even if another one also brings it along as a dependency. A project without a readable `package.json` counts every package as direct. This covers commands only; `binfmt` and file system entries of a dependency count as before.
|
|
246
|
+
|
|
245
247
|
**A pnpm bin of a kernel command** ([#1453](https://github.com/ai-ecoverse/slicc-kernel/issues/1453)): a `#!/usr/bin/env node` (or `#!/usr/bin/node`) script run by its path, whose package lists the same name in `slicc.commands`, runs that kernel command, even when a `node` command exists. This is the bin shim `pnpm add -g` writes into `$PNPM_HOME/bin`, which names its target in a `// pnpm-wasm-shim={"target":…}` line; a script without that line is run as a script, so a same-named file in a package that differs from its glue still doesn't run as the command (#899). The shim's target, with its links resolved, must lie in a package installed directly under one of the command roots, and the name must be the catalog's command from that same package, so a shim only ever runs a command the caller could run by name, with the same checks; with `integrity` on, the shim itself is verified as a source. So `pnpm add -g <pkg> && <cmd>` works in one line.
|
|
246
248
|
|
|
247
249
|
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.
|
|
@@ -477,7 +479,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
477
479
|
- `crontab({ action, user?, text? })`, what the [`crontab`](#cron) command calls: `action` is `'list'`, `'remove'` or `'install'` (with the table as `text`), on the caller's own table or, for an effective uid of 0, on `user`'s. It resolves with `{ text }` (the table for `list`, `''` otherwise) or `{ error }`, the message to print. A malformed call is `EINVAL`;
|
|
478
480
|
- `net`: `listen({ port, host, backlog })` and `connect({ host, port, signal })` (`host` defaults to `127.0.0.1`) on the kernel's loopback, the same sockets WASIX programs use, so a JS server is reached by other processes, by the page as `<port>.kernel.localhost` (`loopbackFetch`, `dial`) and by attached clients. A listener has its `port` (as bound, so port `0` picks a free one), `accept({ signal })`, `close()` and `for await` over its connections. A connection has `local` and `remote` addresses, `readable` and `writable` web streams (closing the writable shuts down the sending side, so the peer reads the end), `close()`, and `reset()`, which closes it with an RST as Node's `resetAndDestroy()` does ([#1530](https://github.com/ai-ecoverse/slicc-kernel/issues/1530)): on the kernel's loopback, the peer first reads the bytes it has already received, as on Linux and BSD ([#1577](https://github.com/ai-ecoverse/slicc-kernel/issues/1577)), and then its reads fail with `ECONNRESET` instead of seeing the end; its writes fail with `ECONNRESET` at once. A TLS connection (`secure()`, a `tls` connect on the loopback) resets the same way. On an uplink connection `reset()` closes at once with a FIN, as the transport can't send an RST: bytes still buffered for the peer are discarded instead of sent. The loopback is IPv4 (`127.0.0.1` by default, or `0.0.0.0`), so an IPv6 host such as `::1` fails with `EADDRNOTAVAIL` for both `listen` and `connect`, and binding and connecting fail as they would for a WASIX program (`EADDRINUSE`, `ECONNREFUSED`, `EADDRNOTAVAIL`). `sync.net.listen` takes the same options and gives the same listener, bound and listening when it returns, so its `port` is known at once, as Node's `listen()` needs ([#1398](https://github.com/ai-ecoverse/slicc-kernel/issues/1398)); it needs the sync lane. A blocked accept, read or write waits without holding anything in the kernel, so aborting it or closing the socket leaves nothing behind. A write to a connection the peer closed fails with `EPIPE` and no `SIGPIPE`;
|
|
479
481
|
- `net.connect({ host, port, tls: { servername, alpn, rejectUnauthorized, ca, path, cert, key, passphrase, sni, checkHostname, keylog } })` takes one of two paths. By default it's **A1, TLS through the transport** ([#914](https://github.com/ai-ecoverse/slicc-kernel/issues/914)), described first below. For a destination the uplink routes it can also be **A2, a TLS handshake in the kernel** ([#922](https://github.com/ai-ecoverse/slicc-kernel/issues/922)), described after it. `conn.tls.path` (`'transport'` or `'kernel'`) and `conn.tls.substitutes` say which one was taken.
|
|
480
|
-
- **What it does:** there's no handshake in the kernel. The connection carries plain HTTP/1.1 (and WebSocket upgrades) to the realm proxy's HTTP path for the origin `https://<servername or host>[:port]`, exactly as a `CONNECT` through `127.0.0.1:3128` does after the kernel terminates its TLS. The transport (the page's `fetch` or WebSocket, or the uplink's) makes the real TLS connection and verifies it. So `https.request` over a socket, keep-alive agents, `agent-base` and `wss://` clients work with the transport's semantics.
|
|
482
|
+
- **What it does:** there's no handshake in the kernel. The connection carries plain HTTP/1.1 (and WebSocket upgrades) to the realm proxy's HTTP path for the origin `https://<servername or host>[:port]`, exactly as a `CONNECT` through `127.0.0.1:3128` does after the kernel terminates its TLS. The transport (the page's `fetch` or WebSocket, or the uplink's) makes the real TLS connection and verifies it. So `https.request` over a socket, keep-alive agents, `agent-base` and `wss://` clients work with the transport's semantics. A request with a sealed placeholder is refused there as from `ctx.fetch` (see **The transport** under [`/dev/sealed`](#devsealed)).
|
|
481
483
|
- **The `tls` property:** the connection's `tls` is `{ path: 'transport', substitutes: true, protocol: 'transport', alpnProtocol: 'http/1.1', authorized: true, servername }`. `authorized` means the transport verified the server. There's no peer certificate chain and no TLS version of the kernel's own, so don't read it as a real handshake's result.
|
|
482
484
|
- **Options:** the transport always verifies, so `rejectUnauthorized: false` is accepted and changes nothing. Outside the uplink, a custom `ca` is refused (`ENOTSUP`), and so is an `alpn` list without `http/1.1` (`EPROTONOSUPPORT`). The kernel's own loopback never goes to the transport. A `host` of `localhost` or `127.x` makes the handshake in the kernel instead (Loopback, below), so with nothing listening it's `ECONNREFUSED`. The rest of it (`*.localhost`, `0.0.0.0`, `::1`, and any loopback `servername` for another host) is `ENOTSUP` at connect, as `ctx.fetch` refuses `https:` and `wss:` there. `host.slicc.internal` goes to the transport under its policy, as for `ctx.fetch`.
|
|
483
485
|
- **Non-HTTP protocols aren't carried:**
|
|
@@ -485,7 +487,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
485
487
|
- non-HTTP first bytes on any other port get a `400` and a close;
|
|
486
488
|
- a server-speaks-first protocol on another port (the client sends nothing) waits for the proxy's 120 s idle limit.
|
|
487
489
|
- For a destination the uplink routes, these cases take A2 instead (below).
|
|
488
|
-
- **Which path** (A2 needs a destination the uplink routes, `net.destination(host) === 'uplink'`; an uplink-routed host on 443 with default options stays on A1,
|
|
490
|
+
- **Which path** (A2 needs a destination the uplink routes, `net.destination(host) === 'uplink'`; an uplink-routed host on 443 with default options stays on A1, where a request with a sealed placeholder is refused):
|
|
489
491
|
|
|
490
492
|
| The call | Uplink destination | Any other destination |
|
|
491
493
|
|---|---|---|
|
|
@@ -1026,7 +1028,7 @@ Every process has credentials in the kernel, as on Linux: real, effective and sa
|
|
|
1026
1028
|
|
|
1027
1029
|
- **The database:** `/etc/passwd` and `/etc/group` are real files. On first start the kernel writes them when they're missing, with `root` (uid 0, home `/root`, created `0700`) and `nobody` (65534), and the groups `root`, `sudo` (27), `users` (100) and `nogroup` (65534); files already there are kept as they are. The kernel reads them for every lookup, so an edit counts at once. [`kernel.users`](#kernelusers) adds and removes users.
|
|
1028
1030
|
- **Environment:** a process started as a user gets `HOME`, `USER`, `LOGNAME`, `PNPM_HOME`, `NPM_CONFIG_PREFIX` and `PATH`. A user's `PNPM_HOME` is `~/.local/share/pnpm` and its npm prefix `~/.local`; root's are `/usr/local/share/pnpm` (or the embedder's `env.PNPM_HOME`) and `/usr/local`. `PATH` is `$PNPM_HOME/bin:/usr/local/bin:/usr/bin:/bin`. pnpm makes only `$PNPM_HOME/bin`, so the kernel makes root's `PNPM_HOME` when it is missing, just before a process starts: once at boot, and again after any mount or unmount, a removable medium inserted, a `hostfs` folder coming back, or a change at that directory or above it (removing it, say), and after a failed try (no medium yet). With owners on, the directories it makes there get root's owner and `0755`, as the boot layout does. A process already running when a medium arrives gets it at its next child launch. Writes inside it cost nothing extra.
|
|
1029
|
-
- **Commands per user:** root's global packages (`pnpm add -g` as root) are commands every user finds in `/usr/bin`. A user's own global packages are commands for that user only: they show up in the user's `~/.local/share/pnpm/bin`, first on its `PATH`, and a program the user starts finds them by name before the system's; another user, root included, doesn't. A user's packages can map file suffixes (`binfmt`) for that user too. The kernel picks up an install, a removal and a new user as they happen.
|
|
1031
|
+
- **Commands per user:** root's global packages (`pnpm add -g` as root) are commands every user finds in `/usr/bin`. A user's own global packages are commands for that user only: they show up in the user's `~/.local/share/pnpm/bin`, first on its `PATH`, and a program the user starts finds them by name before the system's; another user, root included, doesn't. A command that only a dependency of those packages provides shows up there only when no other root has the name (see [Commands](#commands)). A user's packages can map file suffixes (`binfmt`) for that user too. The kernel picks up an install, a removal and a new user as they happen.
|
|
1030
1032
|
- **Changing credentials:** POSIX `setresuid`/`setresgid` rules. Root (effective uid 0) may set any id and the group list; anyone else may only swap among their own real, effective and saved ids, and gets `EPERM` otherwise.
|
|
1031
1033
|
- **Signals:** a process may signal another if its effective uid is 0, or if its real or effective uid is the target's real or saved uid; otherwise `kill` fails with `EPERM`. A group kill signals the members it may, and fails with `EPERM` only when it may signal none.
|
|
1032
1034
|
- **Visibility:** a process that isn't root sees only processes whose real or effective uid is its effective uid, in `/proc` and in `ps`, like Linux's `hidepid=2`; so does a client bound to a user.
|
|
@@ -1131,6 +1133,12 @@ The kernel then asks the hook `{ v: 1, op: 'materializeConsume', ticket, stamp,
|
|
|
1131
1133
|
|
|
1132
1134
|
The kernel never asks for a session that isn't `verified`, whose handshake is more than 60 s old (or in the future), or whose fields aren't of that shape. The reply is `{ values: { [placeholder]: value } }` within 10 s; the kernel takes only the placeholders it asked for, with a non-empty value free of CR, LF and NUL, and anything else (an error such as `{ v: 1, error: { code } }`, a timeout, a malformed reply, no hook) means nothing is substituted: the placeholders go out as written.
|
|
1133
1135
|
|
|
1136
|
+
**The transport.** Nothing fills a placeholder in a request that leaves through the transport, so the kernel refuses that request itself and never hands it to the transport. This covers the realm proxy (`curl`, `CONNECT`, TLS on A1), `ctx.fetch`, `ctx.websocket` and a client's `fetch` and `transport.websocket`. The check needs no vault and no `sealed` hook: anything of the form `fnox_proxy_<24 hex>` is refused, whether or not the vault knows it.
|
|
1137
|
+
|
|
1138
|
+
- **Where it looks:** the URL (host, user info, path, query and fragment, percent-encoded or not), header names and values, WebSocket subprotocols, and `Basic` credentials, decoded for the check (the user, the password, or a credential that is malformed or not UTF-8). A `Basic` credential without a placeholder goes out byte for byte. Bodies aren't read: a placeholder in a body goes out as written.
|
|
1139
|
+
- **What a refusal looks like:** the realm proxy answers `403` with the reason in the body, for a WebSocket upgrade too. `ctx.fetch` and `ctx.websocket` reject with a `TypeError` whose `cause.code` is `EACCES` and, for `ctx.fetch`, whose `cause.detail` is the reason. A client's `fetch` and `transport.websocket` reject with `code` `EACCES` and `status` `403`. Nothing is sent, retried or sent another way, and a held redirect isn't replayed for it.
|
|
1140
|
+
- **What stays as it was:** the kernel's own loopback, A2 sessions (above) and raw sockets never reach this check.
|
|
1141
|
+
|
|
1134
1142
|
### `/dev/clipboard`
|
|
1135
1143
|
|
|
1136
1144
|
A character device (`0666`), there in every `users` mode, through which programs read and write the embedder's clipboard (`createKernel({ clipboard: { read, write } })`; on the page, bios answers with `navigator.clipboard`). Emscripten programs (`cat /dev/clipboard`, `cat > /dev/clipboard`, see it in `/dev`, reach it by any path that leads to it, and see it as a character device after fork and exec), WASI programs (`std::fs`) and JS programs (`ctx.fs.readFile`, `ctx.fs.writeFile`, `ctx.open`) all open it.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -8498,7 +8498,7 @@ function satisfies(version, range2) {
|
|
|
8498
8498
|
}
|
|
8499
8499
|
|
|
8500
8500
|
// src/version.ts
|
|
8501
|
-
var KERNEL_VERSION = true ? "1.170.
|
|
8501
|
+
var KERNEL_VERSION = true ? "1.170.4" : "0.0.0-dev";
|
|
8502
8502
|
var OS_RELEASE = "6.0.0-slicc";
|
|
8503
8503
|
function procVersion() {
|
|
8504
8504
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -8767,9 +8767,19 @@ function replacesOf(manifest) {
|
|
|
8767
8767
|
if (!Array.isArray(replaces)) return [];
|
|
8768
8768
|
return replaces.every((name2) => typeof name2 === "string" && PACKAGE_NAME.test(name2)) ? replaces : [];
|
|
8769
8769
|
}
|
|
8770
|
-
async function
|
|
8770
|
+
async function directInstalls(fs, root) {
|
|
8771
|
+
try {
|
|
8772
|
+
const project = root.slice(0, root.lastIndexOf("/"));
|
|
8773
|
+
const { dependencies } = JSON.parse(await fs.readFile(`${project}/package.json`));
|
|
8774
|
+
return new Set(Object.keys(dependencies ?? {}));
|
|
8775
|
+
} catch {
|
|
8776
|
+
return void 0;
|
|
8777
|
+
}
|
|
8778
|
+
}
|
|
8779
|
+
async function tierPackages(fs, physical, roots, bins, kernel, global) {
|
|
8771
8780
|
const out = [];
|
|
8772
8781
|
for (const root of roots) {
|
|
8782
|
+
const direct = global.has(root) ? await directInstalls(fs, root) : void 0;
|
|
8773
8783
|
for (const pkg of await packages(fs, root)) {
|
|
8774
8784
|
let manifest;
|
|
8775
8785
|
try {
|
|
@@ -8781,12 +8791,15 @@ async function tierPackages(fs, physical, roots, bins, kernel) {
|
|
|
8781
8791
|
const named = new Set(own.map((command) => command.name));
|
|
8782
8792
|
const extra = bins ? await binCommandsOf(physical, pkg, manifest, named) : [];
|
|
8783
8793
|
const refused3 = engineRefusal(manifest, kernel);
|
|
8784
|
-
const
|
|
8794
|
+
const name2 = pkg.slice(root.length + 1);
|
|
8795
|
+
const dependency = direct?.has(name2) === false;
|
|
8796
|
+
const marks = { ...refused3 ? { refused: refused3 } : {}, ...dependency ? { dependency } : {} };
|
|
8785
8797
|
out.push({
|
|
8786
|
-
name:
|
|
8798
|
+
name: name2,
|
|
8787
8799
|
pkg,
|
|
8788
8800
|
replaces: replacesOf(manifest),
|
|
8789
|
-
commands:
|
|
8801
|
+
commands: [...own, ...extra].map((command) => ({ ...command, ...marks })),
|
|
8802
|
+
dependency
|
|
8790
8803
|
});
|
|
8791
8804
|
}
|
|
8792
8805
|
}
|
|
@@ -8812,19 +8825,25 @@ function winners(tier) {
|
|
|
8812
8825
|
}
|
|
8813
8826
|
return out;
|
|
8814
8827
|
}
|
|
8815
|
-
async function scanCommands(fs, modules, binsFrom = Number.POSITIVE_INFINITY, kernel = KERNEL_VERSION, refused3) {
|
|
8828
|
+
async function scanCommands(fs, modules, binsFrom = Number.POSITIVE_INFINITY, kernel = KERNEL_VERSION, refused3, global = /* @__PURE__ */ new Set()) {
|
|
8816
8829
|
const found = /* @__PURE__ */ new Map();
|
|
8817
8830
|
const physical = physicalResolver(fs);
|
|
8818
8831
|
const tiers = (typeof modules === "string" ? [modules] : modules).map(
|
|
8819
8832
|
(tier) => typeof tier === "string" ? [tier] : tier
|
|
8820
8833
|
);
|
|
8821
|
-
|
|
8822
|
-
const tier = await tierPackages(fs, physical, roots, index >= binsFrom, kernel);
|
|
8823
|
-
if (refused3) refused3.push(...tier.flatMap((entry) => entry.commands.filter((c) => c.refused)));
|
|
8834
|
+
const take = async (tier) => {
|
|
8824
8835
|
for (const [name2, { pkg, command }] of winners(tier)) {
|
|
8825
8836
|
if (!found.has(name2)) found.set(name2, await withPackagePaths(fs, pkg, command));
|
|
8826
8837
|
}
|
|
8838
|
+
};
|
|
8839
|
+
const dependencies = [];
|
|
8840
|
+
for (const [index, roots] of tiers.entries()) {
|
|
8841
|
+
const tier = await tierPackages(fs, physical, roots, index >= binsFrom, kernel, global);
|
|
8842
|
+
if (refused3) refused3.push(...tier.flatMap((entry) => entry.commands.filter((c) => c.refused)));
|
|
8843
|
+
await take(tier.filter((entry) => !entry.dependency));
|
|
8844
|
+
dependencies.push(tier.filter((entry) => entry.dependency));
|
|
8827
8845
|
}
|
|
8846
|
+
for (const tier of dependencies) await take(tier);
|
|
8828
8847
|
if (!found.has("sh") && found.has("bash")) {
|
|
8829
8848
|
found.set("sh", { ...found.get("bash"), name: "sh", argv0: "sh" });
|
|
8830
8849
|
}
|
|
@@ -10373,6 +10392,78 @@ var CronService = class {
|
|
|
10373
10392
|
}
|
|
10374
10393
|
};
|
|
10375
10394
|
|
|
10395
|
+
// src/kernel/net/placeholder-floor.ts
|
|
10396
|
+
var PLACEHOLDER = /fnox_proxy_[0-9a-f]{24}/;
|
|
10397
|
+
var ESCAPE = /%([0-9a-f]{2})/gi;
|
|
10398
|
+
var BASIC = /\bbasic\b(.*)/is;
|
|
10399
|
+
var PERCENT_LAYERS = 8;
|
|
10400
|
+
var BASE64_GROUP = 4;
|
|
10401
|
+
var PLACEHOLDER_REFUSED = "refused: this request has a sealed placeholder in its URL, a header or a Basic credential, and nothing fills it, so it was not sent";
|
|
10402
|
+
var PlaceholderRefused = class extends Error {
|
|
10403
|
+
status = 403;
|
|
10404
|
+
code = "EACCES";
|
|
10405
|
+
constructor() {
|
|
10406
|
+
super(PLACEHOLDER_REFUSED);
|
|
10407
|
+
}
|
|
10408
|
+
};
|
|
10409
|
+
function carries(text) {
|
|
10410
|
+
let layer = text;
|
|
10411
|
+
for (let i = 0; i < PERCENT_LAYERS; i++) {
|
|
10412
|
+
if (PLACEHOLDER.test(layer)) return true;
|
|
10413
|
+
const next3 = layer.replace(
|
|
10414
|
+
ESCAPE,
|
|
10415
|
+
(_, hex4) => String.fromCharCode(Number.parseInt(hex4, 16))
|
|
10416
|
+
);
|
|
10417
|
+
if (next3 === layer) return false;
|
|
10418
|
+
layer = next3;
|
|
10419
|
+
}
|
|
10420
|
+
return PLACEHOLDER.test(layer);
|
|
10421
|
+
}
|
|
10422
|
+
function basicCarries(value2) {
|
|
10423
|
+
const credential = BASIC.exec(value2)?.[1];
|
|
10424
|
+
if (credential === void 0) return false;
|
|
10425
|
+
const letters = credential.replace(/[^A-Za-z0-9+/_-]/g, "").replaceAll("-", "+").replaceAll("_", "/");
|
|
10426
|
+
for (let shift = 0; shift < BASE64_GROUP; shift++) {
|
|
10427
|
+
const part = letters.slice(shift);
|
|
10428
|
+
const whole = part.length % BASE64_GROUP === 1 ? part.slice(0, -1) : part;
|
|
10429
|
+
if (carries(atob(whole))) return true;
|
|
10430
|
+
}
|
|
10431
|
+
return false;
|
|
10432
|
+
}
|
|
10433
|
+
function headersCarry(headers) {
|
|
10434
|
+
return headers.some(
|
|
10435
|
+
([name2, value2]) => carries(name2.toLowerCase()) || carries(value2) || basicCarries(value2)
|
|
10436
|
+
);
|
|
10437
|
+
}
|
|
10438
|
+
function urlCarries(url) {
|
|
10439
|
+
return carries(url) || URL.canParse(url) && carries(new URL(url).href);
|
|
10440
|
+
}
|
|
10441
|
+
function refuse(request3) {
|
|
10442
|
+
if (urlCarries(request3.url) || headersCarry(request3.headers)) throw new PlaceholderRefused();
|
|
10443
|
+
}
|
|
10444
|
+
function refuseSocket(request3) {
|
|
10445
|
+
refuse(request3);
|
|
10446
|
+
if (request3.protocols.some(carries)) throw new PlaceholderRefused();
|
|
10447
|
+
}
|
|
10448
|
+
function placeholderFloor(inner) {
|
|
10449
|
+
const open2 = inner.websocket?.bind(inner);
|
|
10450
|
+
return {
|
|
10451
|
+
get traits() {
|
|
10452
|
+
return inner.traits;
|
|
10453
|
+
},
|
|
10454
|
+
...open2 ? {
|
|
10455
|
+
websocket: async (request3) => {
|
|
10456
|
+
refuseSocket(request3);
|
|
10457
|
+
return open2(request3);
|
|
10458
|
+
}
|
|
10459
|
+
} : {},
|
|
10460
|
+
async fetch(request3, owner) {
|
|
10461
|
+
refuse(request3);
|
|
10462
|
+
return inner.fetch(request3, owner);
|
|
10463
|
+
}
|
|
10464
|
+
};
|
|
10465
|
+
}
|
|
10466
|
+
|
|
10376
10467
|
// src/kernel/net/proxy-service.ts
|
|
10377
10468
|
var SEND_BUFFER2 = 1024 * 1024;
|
|
10378
10469
|
var TLS_HANDSHAKE = 22;
|
|
@@ -10879,7 +10970,8 @@ var RealmProxy = class {
|
|
|
10879
10970
|
});
|
|
10880
10971
|
} catch (e) {
|
|
10881
10972
|
waiting.abort();
|
|
10882
|
-
|
|
10973
|
+
const status = e instanceof PlaceholderRefused ? e.status : 502;
|
|
10974
|
+
throw new HttpError(status, e instanceof Error ? e.message : String(e));
|
|
10883
10975
|
}
|
|
10884
10976
|
ctx.idle.ms = Number.POSITIVE_INFINITY;
|
|
10885
10977
|
try {
|
|
@@ -11093,7 +11185,7 @@ async function relay(socket, conn) {
|
|
|
11093
11185
|
// src/fs/attest.ts
|
|
11094
11186
|
var ATTEST_MAX = 1024 * 1024;
|
|
11095
11187
|
var UNOWNED = 1e3;
|
|
11096
|
-
function
|
|
11188
|
+
function refuse2(code, path, why) {
|
|
11097
11189
|
throw Object.assign(new Error(`attest ${path}: ${why}`), { code });
|
|
11098
11190
|
}
|
|
11099
11191
|
function chainOf(path) {
|
|
@@ -11135,11 +11227,11 @@ async function trustedParents(fs, path, o) {
|
|
|
11135
11227
|
const parents = [];
|
|
11136
11228
|
for (const dir of chainOf(path).slice(0, -1)) {
|
|
11137
11229
|
const st = await fs.lstat(dir);
|
|
11138
|
-
if (st.isSymbolicLink)
|
|
11139
|
-
if (!st.isDirectory)
|
|
11230
|
+
if (st.isSymbolicLink) refuse2("EACCES", path, `${dir} is a symlink`);
|
|
11231
|
+
if (!st.isDirectory) refuse2("ENOTDIR", path, `${dir} is not a directory`);
|
|
11140
11232
|
const perm = owned(dir, st);
|
|
11141
11233
|
if (o.users !== "off" && (perm.uid !== 0 || (perm.mode & 18) !== 0)) {
|
|
11142
|
-
|
|
11234
|
+
refuse2("EACCES", path, `${dir} is not root-owned or is writable by group or others`);
|
|
11143
11235
|
}
|
|
11144
11236
|
parents.push(perm);
|
|
11145
11237
|
}
|
|
@@ -11147,27 +11239,27 @@ async function trustedParents(fs, path, o) {
|
|
|
11147
11239
|
}
|
|
11148
11240
|
async function attest(fs, asked, o) {
|
|
11149
11241
|
if (typeof asked !== "string" || !asked.startsWith("/")) {
|
|
11150
|
-
|
|
11242
|
+
refuse2("EINVAL", String(asked), "not an absolute path");
|
|
11151
11243
|
}
|
|
11152
11244
|
const path = normalizePath(asked);
|
|
11153
11245
|
const mount = o.mounts.find((target) => {
|
|
11154
11246
|
const at = normalizePath(target);
|
|
11155
11247
|
return at === "/" || under2(path, at);
|
|
11156
11248
|
});
|
|
11157
|
-
if (mount !== void 0)
|
|
11249
|
+
if (mount !== void 0) refuse2("EACCES", path, `under the mount ${mount}`);
|
|
11158
11250
|
const parents = await trustedParents(fs, path, o);
|
|
11159
11251
|
for (let tries = 0; ; tries++) {
|
|
11160
11252
|
const before = await fs.lstat(path);
|
|
11161
|
-
if (before.isSymbolicLink)
|
|
11253
|
+
if (before.isSymbolicLink) refuse2("EACCES", path, "is a symlink");
|
|
11162
11254
|
if (!before.isFile)
|
|
11163
|
-
|
|
11164
|
-
if (before.size > ATTEST_MAX)
|
|
11255
|
+
refuse2(before.isDirectory ? "EISDIR" : "EINVAL", path, "is not a regular file");
|
|
11256
|
+
if (before.size > ATTEST_MAX) refuse2("EFBIG", path, `is over ${ATTEST_MAX} bytes`);
|
|
11165
11257
|
const bytes = (await fs.readFileBuffer(path)).slice();
|
|
11166
11258
|
const after = await fs.lstat(path);
|
|
11167
11259
|
if (unchanged(before, after, bytes)) {
|
|
11168
11260
|
return attested(path, after, bytes, await sha2562(bytes), o, parents);
|
|
11169
11261
|
}
|
|
11170
|
-
if (tries >= 1)
|
|
11262
|
+
if (tries >= 1) refuse2("EAGAIN", path, "changed while it was read");
|
|
11171
11263
|
}
|
|
11172
11264
|
}
|
|
11173
11265
|
var WATCH_DEBOUNCE_MS = 50;
|
|
@@ -15735,7 +15827,7 @@ function encodeSabResult(result) {
|
|
|
15735
15827
|
message: result.message,
|
|
15736
15828
|
...result.direct ? { direct: true } : {},
|
|
15737
15829
|
...result.dev !== void 0 ? { dev: result.dev } : {},
|
|
15738
|
-
...result.detail
|
|
15830
|
+
...result.detail ? { detail: result.detail } : {}
|
|
15739
15831
|
})
|
|
15740
15832
|
)
|
|
15741
15833
|
};
|
|
@@ -19197,23 +19289,29 @@ var HttpHandles = class {
|
|
|
19197
19289
|
return response.redirected ? { ...head, json: { ...head.json, redirected: true } } : head;
|
|
19198
19290
|
} catch (e) {
|
|
19199
19291
|
entry.upload?.cancel();
|
|
19200
|
-
|
|
19201
|
-
|
|
19202
|
-
const status = e.status;
|
|
19203
|
-
if (text === NO_TRANSPORT || typeof status !== "number" || status === 502) {
|
|
19204
|
-
const code = codeOf2(e);
|
|
19205
|
-
const errno3 = text === NO_TRANSPORT ? "ENETUNREACH" : code !== "EIO" ? code : "ECONNREFUSED";
|
|
19206
|
-
const failed3 = e.detail === "transport";
|
|
19207
|
-
return { ok: false, errno: errno3, message: text, ...failed3 ? { detail: "transport" } : {} };
|
|
19208
|
-
}
|
|
19209
|
-
entry.body = once(new TextEncoder().encode(text))[Symbol.asyncIterator]();
|
|
19210
|
-
return this.headOf(handle, req.url, {
|
|
19211
|
-
status,
|
|
19212
|
-
statusText: "",
|
|
19213
|
-
headers: [["content-type", "text/plain; charset=utf-8"]]
|
|
19214
|
-
});
|
|
19292
|
+
if (port !== void 0) return { ok: false, errno: codeOf2(e), message: message2(e) };
|
|
19293
|
+
return this.failure(handle, req, e, entry);
|
|
19215
19294
|
}
|
|
19216
19295
|
}
|
|
19296
|
+
failure(handle, req, e, entry) {
|
|
19297
|
+
const text = message2(e);
|
|
19298
|
+
if (e instanceof PlaceholderRefused) {
|
|
19299
|
+
return { ok: false, errno: e.code, message: text, detail: "placeholder" };
|
|
19300
|
+
}
|
|
19301
|
+
const status = e.status;
|
|
19302
|
+
if (text === NO_TRANSPORT || typeof status !== "number" || status === 502) {
|
|
19303
|
+
const code = codeOf2(e);
|
|
19304
|
+
const errno3 = text === NO_TRANSPORT ? "ENETUNREACH" : code !== "EIO" ? code : "ECONNREFUSED";
|
|
19305
|
+
const failed3 = e.detail === "transport";
|
|
19306
|
+
return { ok: false, errno: errno3, message: text, ...failed3 ? { detail: "transport" } : {} };
|
|
19307
|
+
}
|
|
19308
|
+
entry.body = once(new TextEncoder().encode(text))[Symbol.asyncIterator]();
|
|
19309
|
+
return this.headOf(handle, req.url, {
|
|
19310
|
+
status,
|
|
19311
|
+
statusText: "",
|
|
19312
|
+
headers: [["content-type", "text/plain; charset=utf-8"]]
|
|
19313
|
+
});
|
|
19314
|
+
}
|
|
19217
19315
|
headOf(handle, url, response) {
|
|
19218
19316
|
const { status, statusText, headers } = response;
|
|
19219
19317
|
return { ok: true, kind: "json", json: { handle, status, statusText, url, headers } };
|
|
@@ -22423,7 +22521,7 @@ var HEAD_MAX = 16 * 1024;
|
|
|
22423
22521
|
var ANY_PLACEHOLDER = /fnox_proxy_[0-9a-f]{24}/g;
|
|
22424
22522
|
var REQUEST_LINE2 = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+) ([\x21-\x7e]+) HTTP\/1\.[01]$/;
|
|
22425
22523
|
var HEADER_LINE = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+):[ \t]*(.*?)[ \t]*$/;
|
|
22426
|
-
var
|
|
22524
|
+
var BASIC2 = /^(Basic[ \t]+)([A-Za-z0-9+/]+={0,2})$/i;
|
|
22427
22525
|
var encoder8 = new TextEncoder();
|
|
22428
22526
|
function binary(text) {
|
|
22429
22527
|
let out = "";
|
|
@@ -22439,7 +22537,7 @@ function sniffHttp1(start2) {
|
|
|
22439
22537
|
return "no";
|
|
22440
22538
|
}
|
|
22441
22539
|
function basicOf(value2) {
|
|
22442
|
-
const m =
|
|
22540
|
+
const m = BASIC2.exec(value2);
|
|
22443
22541
|
if (!m) return void 0;
|
|
22444
22542
|
try {
|
|
22445
22543
|
return { scheme: m[1], decoded: atob(m[2]) };
|
|
@@ -29012,7 +29110,8 @@ function replayingHops(inner, heldMs = HELD_MS) {
|
|
|
29012
29110
|
return { ...inner.traits, manualRedirects: true };
|
|
29013
29111
|
},
|
|
29014
29112
|
...inner.websocket ? { websocket: inner.websocket.bind(inner) } : {},
|
|
29015
|
-
async fetch(request3,
|
|
29113
|
+
async fetch(request3, caller) {
|
|
29114
|
+
const owner = caller ?? KERNEL;
|
|
29016
29115
|
const key = keyOf3(owner, request3.method, request3.url);
|
|
29017
29116
|
const waiting = held.get(key);
|
|
29018
29117
|
if (waiting && waiting.fingerprint === fingerprint(request3.headers)) {
|
|
@@ -29021,7 +29120,7 @@ function replayingHops(inner, heldMs = HELD_MS) {
|
|
|
29021
29120
|
return replay(waiting);
|
|
29022
29121
|
}
|
|
29023
29122
|
if (waiting) drop(key);
|
|
29024
|
-
const response = await inner.fetch(request3);
|
|
29123
|
+
const response = await inner.fetch(request3, caller);
|
|
29025
29124
|
if (!response.redirects?.length) return response;
|
|
29026
29125
|
return replay({
|
|
29027
29126
|
owner,
|
|
@@ -31864,7 +31963,7 @@ var Launcher = class {
|
|
|
31864
31963
|
...options.env
|
|
31865
31964
|
};
|
|
31866
31965
|
this.ca = kernelCa(options.caStore ?? memoryCaStore());
|
|
31867
|
-
this.transport = replayingHops(options.transport ?? missingTransport());
|
|
31966
|
+
this.transport = placeholderFloor(replayingHops(options.transport ?? missingTransport()));
|
|
31868
31967
|
this.announcedTraits = JSON.stringify(this.transport.traits);
|
|
31869
31968
|
this.uplink = options.uplink;
|
|
31870
31969
|
this.routes = new Routes(options.uplink?.traits.ipv6 === true);
|
|
@@ -32025,7 +32124,8 @@ var Launcher = class {
|
|
|
32025
32124
|
tiers,
|
|
32026
32125
|
tiers.length - appended2.length,
|
|
32027
32126
|
void 0,
|
|
32028
|
-
refused3
|
|
32127
|
+
refused3,
|
|
32128
|
+
await this.globalRoots(scope)
|
|
32029
32129
|
);
|
|
32030
32130
|
this.refusedOf.set(commands, refused3.map(refusing));
|
|
32031
32131
|
return commands;
|
|
@@ -32050,11 +32150,31 @@ var Launcher = class {
|
|
|
32050
32150
|
ownCommands(scope) {
|
|
32051
32151
|
let found = this.ownCatalogs.get(scope);
|
|
32052
32152
|
if (!found) {
|
|
32053
|
-
found =
|
|
32153
|
+
found = this.ownCatalog(scope);
|
|
32054
32154
|
this.ownCatalogs.set(scope, found);
|
|
32055
32155
|
}
|
|
32056
32156
|
return found;
|
|
32057
32157
|
}
|
|
32158
|
+
async ownCatalog(scope) {
|
|
32159
|
+
const roots = await pnpmGlobalRoots(this.base, scope);
|
|
32160
|
+
const own = await scanCommands(
|
|
32161
|
+
this.scanned,
|
|
32162
|
+
roots,
|
|
32163
|
+
void 0,
|
|
32164
|
+
void 0,
|
|
32165
|
+
void 0,
|
|
32166
|
+
new Set(roots)
|
|
32167
|
+
);
|
|
32168
|
+
const system = await this.commands();
|
|
32169
|
+
return new Set(
|
|
32170
|
+
[...own.values()].filter((command) => !command.dependency || !system.has(command.name)).map((command) => command.name)
|
|
32171
|
+
);
|
|
32172
|
+
}
|
|
32173
|
+
async globalRoots(scope) {
|
|
32174
|
+
const homes = scope ? [scope, this.pnpmHome] : [this.pnpmHome];
|
|
32175
|
+
const roots = await Promise.all(homes.map((home) => pnpmGlobalRoots(this.base, home)));
|
|
32176
|
+
return new Set(roots.flat());
|
|
32177
|
+
}
|
|
32058
32178
|
interpreters(scope = "") {
|
|
32059
32179
|
let found = this.binfmts.get(scope);
|
|
32060
32180
|
if (!found) {
|