@ai-ecoverse/slicc-kernel 1.51.0 → 1.52.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 +3 -3
- package/dist/kernel-worker.js +24 -9
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +221 -3
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +24 -9
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +221 -3
- package/dist/process-worker.js.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ That's for programs that walk to the root, such as Go's (`GOOS=wasip1`) and esbu
|
|
|
178
178
|
**Working directory.** wasi-libc's own `getcwd()` stays `/` (Rust's `current_dir()`, `canonicalize(".")`) in either layout, because the C library starts with a working directory of `/` and no runtime can change it. A wasi-libc program that needs its real directory takes it from `PWD` (`set_current_dir($PWD)` at start); relative paths are the cwd either way. WASIX programs ask the kernel for their cwd and get the real one.
|
|
179
179
|
|
|
180
180
|
**Descriptor numbers.** Preopens take consecutive descriptors from 3, since WASI libraries look for them until the first gap:
|
|
181
|
-
- without `preopenRoot`: fds 3 through 3 + *n*, *n* being the number of top-level directories, `/dev`, `/usr` and `/bin` together;
|
|
181
|
+
- without `preopenRoot`: fds 3 through 3 + *n*, *n* being the number of top-level directories, `/dev`, `/proc`, `/usr` and `/bin` together;
|
|
182
182
|
- with it: two more, through 3 + *n* + 2.
|
|
183
183
|
|
|
184
184
|
A descriptor a program inherits at one of those numbers (from bash's `3<file`, or a `posix_spawn` file action) moves to the first free number above them. One above the range keeps its number. A command that takes descriptors by number from its caller should leave `preopenRoot` off, which keeps the shorter range.
|
|
@@ -253,7 +253,7 @@ await kernel.run(['curl', '-sS', 'https://registry.npmjs.org/@ai-ecoverse/wasm-b
|
|
|
253
253
|
- **The proxy** listens on `127.0.0.1:3128` from the first connection on. It takes absolute-form `http:` requests and `CONNECT` tunnels, which it terminates with a certificate for the requested host, issued by the kernel's own CA, then hands each request to the transport. Programs start with `http_proxy`, `https_proxy` (and the upper-case names) pointing at it, `no_proxy` covering loopback and the [node name](#node-name), and `SSL_CERT_FILE`, `CURL_CA_BUNDLE` and `GIT_SSL_CAINFO` pointing at the CA bundle in `/etc/ssl/certs/slicc-kernel-ca.pem`; variables in `env` override them. The CA's key never leaves WebCrypto; it is kept in IndexedDB, in `<metadata>-ca`, or in `slicc-kernel-ca` when `metadata` is the default or `false`, so kernels on one origin share one CA whether or not they keep metadata. `ca` names another database, and `ca: false` keeps a CA in memory for the kernel's lifetime. Each kernel adds its CA to the bundle instead of replacing it, so every kernel on the origin stays trusted; the bundle keeps the newest 16.
|
|
254
254
|
- **TLS** needs [`@ai-ecoverse/wasm-tls-engine`](https://www.npmjs.com/package/@ai-ecoverse/wasm-tls-engine) installed under `modules`, like a command. Without it, `CONNECT` to port 443 is answered `501` with the reason. A tunnel to another port is accepted, and what the program sends first decides: plain HTTP is served as `http://host:port`, without TLS, and a TLS handshake is terminated as above, or the tunnel closes when it cannot be.
|
|
255
255
|
- **The transport** is an object on the page with `traits` (`manualRedirects`, `encodedBodies`, `maxRequestBody`, and `crossOrigin`: `'cors'` or `'any'`) and `fetch(request)`, which answers `{ status, statusText, headers, body, cancel }` with `body` an async iterable of `Uint8Array`, and optionally `url` and `redirected: true` when it followed redirects itself (`fetchTransport()` does), so a JS program's `fetch` reports where it ended. The kernel pulls the body one chunk at a time, so a slow program slows the download. A rejection with a numeric `status` is answered with that status; any other with `502`. `fetchTransport()` is the plain `fetch` of the page, bound by CORS; its `hint` option is appended to the message of a request it could not make, so a program's 502 can name a way around CORS (`registry.npmjs.org` and jsDelivr allow it); an embedder with a way around CORS passes its own. A response body that delivers no data for `bodyIdleMs` (default 5 minutes; `0` or `Infinity` turns it off) fails with `ETIMEDOUT` instead of waiting forever, which matters because Chrome under heavy load can stop handing a page the rest of a body. A slow body that keeps delivering is never cut off.
|
|
256
|
-
- **Redirect hops** ([#302](https://github.com/ai-ecoverse/slicc-kernel/issues/302)): a transport that follows redirects but can see them (a browser extension's `webRequest.onBeforeRedirect` can) sets `traits.redirectHops` and answers `redirects: [{ status, url, headers }]`, one entry per 3xx it followed, in order (`NetworkRedirectHop`). The kernel then treats it as one that does not follow. Programs get each 3xx as it was answered: through the proxy, curl, requests and `hf` see the real redirect, `X-Repo-Commit` and `ETag` included, and `ctx.fetch` with `redirect: 'manual'` answers it. The rest of the chain is held for the process that asked, at most 30 s and 8 chains per process (64 in all), with the final body unread. A follow-up from that process, with the method the redirect leads to and the same `Range`, `If-None-Match`, `If-Modified-Since`, `If-Range` and `Accept-Encoding` (and an `Authorization` if the first had one), is answered from it without a second request; any other request goes to the transport. A chain that expires or is pushed out is cancelled. The page's own `fetch` cannot show a redirect, so `fetchTransport()` in a page stays a transport that follows, and `redirect: 'manual'` fails there as before. `fetchTransport({ redirect: 'manual' })` hands redirects back unfollowed where the platform's `fetch` shows them (Node; a browser answers an opaque status 0), and `nodeTransport()` does so. A kernel socket also records who connected it (`SO_PEERCRED`): the connecting process's pid, uid and gid on the accepting side, and the listener's on the connecting side, kept when the socket passes to another process. The proxy uses this to keep each process's held chains its own.
|
|
256
|
+
- **Redirect hops** ([#302](https://github.com/ai-ecoverse/slicc-kernel/issues/302)): a transport that follows redirects but can see them (a browser extension's `webRequest.onBeforeRedirect` can) sets `traits.redirectHops` and answers `redirects: [{ status, url, headers }]`, one entry per 3xx it followed, in order (`NetworkRedirectHop`). The kernel then treats it as one that does not follow. Programs get each 3xx as it was answered: through the proxy, curl, requests and `hf` see the real redirect, `X-Repo-Commit` and `ETag` included, and `ctx.fetch` with `redirect: 'manual'` answers it. The rest of the chain is held for the process that asked, at most 30 s and 8 chains per process (64 in all), with the final body unread. A follow-up from that process, with the method the redirect leads to and the same `Range`, `If-None-Match`, `If-Modified-Since`, `If-Range` and `Accept-Encoding` (and an `Authorization` if the first had one), is answered from it without a second request; any other request goes to the transport. A chain that expires or is pushed out is cancelled, and so is one whose follow-up arrives and does not match. A redirect to another origin is usually fetched again rather than replayed: HTTP clients drop `Authorization` when they follow one, so the follow-up no longer matches. The page's own `fetch` cannot show a redirect, so `fetchTransport()` in a page stays a transport that follows, and `redirect: 'manual'` fails there as before. `fetchTransport({ redirect: 'manual' })` hands redirects back unfollowed where the platform's `fetch` shows them (Node; a browser answers an opaque status 0), and `nodeTransport()` does so. A kernel socket also records who connected it (`SO_PEERCRED`): the connecting process's pid, uid and gid on the accepting side, and the listener's on the connecting side, kept when the socket passes to another process. The proxy uses this to keep each process's held chains its own.
|
|
257
257
|
- **WebSockets:** a `GET` with `Upgrade: websocket`, in absolute form or inside a tunnel, is bridged to the transport's optional `websocket({ url, protocols, headers, signal })`. It answers `{ protocol, buffered, send(data), messages, close(code, reason), closed }`, with `messages` an async iterable of strings and `Uint8Array`s and `closed` resolving with `{ code, reason }`; a transport that has it says so with `traits.websocket`. The program gets `101 Switching Protocols` with the far end's protocol, then its frames go out as messages and the far end's come back as frames. Pings are answered by the proxy, and close codes pass both ways (a code that may not go on the wire becomes 1000 or 1011). A message over 64 MiB, or more than 16 MiB of payload from the far end waiting for the program, closes with 1009. The proxy stops reading the program while 1 MiB of its messages wait to be sent. A refused connection is `502` with the reason. A transport without WebSockets answers `501` with a message naming the option below, and the kernel logs a warning once, so a program never hangs. `fetchTransport({ webSocket: true })` opens them with the page's `WebSocket` (or pass a constructor); they are off by default because a browser WebSocket carries the user's cookies for the target site, which `fetch` here never sends (`credentials: 'omit'`), so a program could reach a site as the user. A page also cannot send headers on a WebSocket, and an `https:` page cannot open `ws:` to anything but loopback, which is a `502` that says so. `nodeTransport()` has WebSockets on (Node keeps no cookies) and sends the request's headers. The credential-free path for a page is the coming slicc-node relay (`localProxyTransport()`, phase 2 of [#204](https://github.com/ai-ecoverse/slicc-kernel/issues/204)), which has no WebSockets yet.
|
|
258
258
|
- **`localProxyTransport({ url, key })`** sends every request to a local proxy on loopback, such as [slicc-node](https://github.com/ai-ecoverse/slicc-node) or slicc-swift, which fetches it without CORS. Redirects reach the program unfollowed, with every `Set-Cookie`; bodies arrive decoded. `probeLocalProxy({ url, key })` resolves with the proxy's capabilities, or `null` when it is gone or refuses the key, so a page can fall back to `fetchTransport()`. `checkLocalProxy({ url, key })` says why: it resolves with `{ state }`, which is `ready` (with `probe`), `blocked` (the user denied Chrome's Local Network Access permission, `loopback-network`), `unreachable` (nothing answered; its `permission` is `granted`, `prompt` or `unknown`, and with `prompt` the page cannot tell a dismissed prompt from a proxy that is not running, since Chrome asks only once it has a connection), `refused` (with `status` and `error`, such as a stale key) or `incompatible`. The permission is not consulted when the page itself is on loopback. Both take a `fetch` option, and the transport `maxRequestBody` (default 64 MiB) and `bodyIdleMs` like `fetchTransport`.
|
|
259
259
|
- **The host machine** is `host.slicc.internal` (`SLICC_HOST_LOOPBACK`), as `host.docker.internal` is in Docker, while `localhost` and `127.0.0.1` stay the kernel's own loopback. The proxy forwards `http(s)://host.slicc.internal:<port>/…` to the transport as `http(s)://127.0.0.1:<port>/…`, so it reaches the real machine's loopback through the local proxy or Node's `fetch` (through the page's own `fetch`, a request needs CORS from that server and Chrome's Local Network Access permission); other loopback addresses are still refused. This is HTTP and HTTPS through the proxy only: `/etc/hosts` and WASIX name resolution give the name `10.0.2.2`, QEMU's address for its host, so a program that connects a socket to it directly fails with `ENETUNREACH` instead of reaching the kernel's loopback.
|
|
@@ -494,7 +494,7 @@ Programs ask the kernel for their ids:
|
|
|
494
494
|
|
|
495
495
|
### `/proc`
|
|
496
496
|
|
|
497
|
-
Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`) and each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times, load and `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) are placeholders. A process that isn't root sees only its own user's processes (see [Users](#users)).
|
|
497
|
+
Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`) and each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times, load and `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) are placeholders. A process that isn't root sees only its own user's processes (see [Users](#users)). WASI and WASIX programs see the same `/proc`, read-only (opening a file in it for writing fails with `EROFS`), and `/` lists it, so a WASI program gets a preopen for it; `/proc/self` is the program's own pid there too. Neither has `/proc/<pid>/fd` yet.
|
|
498
498
|
|
|
499
499
|
The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#users)). While the root has none of them, or no `/etc/mtab`, reading them gets per-process ones: root is uid 0 and the realm user, uid 1000, is named after `USER`, or `web_user` (Emscripten's default) without one, so it matches `$USER`, with `HOME` as its home, so `id`, `whoami`, `ls -l` and `ps` show names. Nothing is written to OPFS, and a real file there wins.
|
|
500
500
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -6239,6 +6239,7 @@ var DriverConnection = class _DriverConnection {
|
|
|
6239
6239
|
// src/mount/mount-fs.ts
|
|
6240
6240
|
var MAX_LINKS4 = 40;
|
|
6241
6241
|
var STALE_RETRIES = 3;
|
|
6242
|
+
var SETTLE_ROUNDS = 4;
|
|
6242
6243
|
var DEFAULT_IO = 8 * 1024 * 1024;
|
|
6243
6244
|
var UNITS = { "": 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4 };
|
|
6244
6245
|
function parseSize(text) {
|
|
@@ -6556,6 +6557,7 @@ var MountTable = class {
|
|
|
6556
6557
|
maxFile: maxFile ?? caps.maxFile ?? 0,
|
|
6557
6558
|
attrs: /* @__PURE__ */ new Map(),
|
|
6558
6559
|
lists: /* @__PURE__ */ new Map(),
|
|
6560
|
+
epoch: 0,
|
|
6559
6561
|
dispose: () => opened2.dispose(),
|
|
6560
6562
|
...opened2.present ? { present: opened2.present } : {}
|
|
6561
6563
|
};
|
|
@@ -6610,6 +6612,7 @@ var MountTable = class {
|
|
|
6610
6612
|
const session = this.sessions.get(real);
|
|
6611
6613
|
if (!session) return this.committing.get(real)?.done;
|
|
6612
6614
|
this.sessions.delete(real);
|
|
6615
|
+
session.mount.epoch++;
|
|
6613
6616
|
const done4 = (async () => {
|
|
6614
6617
|
try {
|
|
6615
6618
|
await session.mount.conn.call({ op: "release", fh: await session.fh });
|
|
@@ -6662,19 +6665,30 @@ var MountTable = class {
|
|
|
6662
6665
|
this.deps.changed?.(...full);
|
|
6663
6666
|
}
|
|
6664
6667
|
forget(mount, path) {
|
|
6668
|
+
mount.epoch++;
|
|
6665
6669
|
for (const key of [...mount.attrs.keys()]) if (within4(key, path)) mount.attrs.delete(key);
|
|
6666
6670
|
mount.lists.delete(path);
|
|
6667
6671
|
mount.lists.delete(parentOf5(path));
|
|
6668
6672
|
for (const key of [...mount.lists.keys()]) if (within4(key, path)) mount.lists.delete(key);
|
|
6669
6673
|
}
|
|
6670
|
-
async getattr(mount, path, rel2) {
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6674
|
+
async getattr(mount, path, rel2, settled = false) {
|
|
6675
|
+
for (let round = 1; ; round++) {
|
|
6676
|
+
if (mount.conn.failure) throw mount.conn.failure;
|
|
6677
|
+
if (settled) await this.committing.get(path)?.done.catch(() => void 0);
|
|
6678
|
+
const cached = mount.attrs.get(path);
|
|
6679
|
+
if (cached && cached.expires > Date.now()) return cached.value;
|
|
6680
|
+
const seen = mount.epoch;
|
|
6681
|
+
const session = this.sessions.get(path);
|
|
6682
|
+
const attr = await mount.conn.call({ op: "getattr", path: rel2 });
|
|
6683
|
+
if (mount.epoch !== seen) {
|
|
6684
|
+
const moved2 = this.sessions.get(path) !== session || this.committing.has(path);
|
|
6685
|
+
if (settled && moved2 && round < SETTLE_ROUNDS) continue;
|
|
6686
|
+
return attr;
|
|
6687
|
+
}
|
|
6688
|
+
const ttl = mount.caps.attrTtl ?? 1e3;
|
|
6689
|
+
if (ttl > 0) mount.attrs.set(path, { value: attr, expires: Date.now() + ttl });
|
|
6690
|
+
return attr;
|
|
6691
|
+
}
|
|
6678
6692
|
}
|
|
6679
6693
|
async readdir(mount, path, rel2) {
|
|
6680
6694
|
if (mount.conn.failure) throw mount.conn.failure;
|
|
@@ -6955,7 +6969,7 @@ var MountFs = class {
|
|
|
6955
6969
|
const real = normalizePath(path);
|
|
6956
6970
|
if (this.table.writing(real)?.truncated) await this.table.commit(real);
|
|
6957
6971
|
return this.statWriting(
|
|
6958
|
-
await this.table.getattr(found.mount, real, found.rel),
|
|
6972
|
+
await this.table.getattr(found.mount, real, found.rel, true),
|
|
6959
6973
|
real,
|
|
6960
6974
|
found.mount
|
|
6961
6975
|
);
|
|
@@ -10746,6 +10760,7 @@ function replayingHops(inner, heldMs = HELD_MS) {
|
|
|
10746
10760
|
clearTimeout(waiting.timer);
|
|
10747
10761
|
return replay(waiting);
|
|
10748
10762
|
}
|
|
10763
|
+
if (waiting) drop(key);
|
|
10749
10764
|
const response = await inner.fetch(request2);
|
|
10750
10765
|
if (!response.redirects?.length) return response;
|
|
10751
10766
|
return replay({
|