@ai-ecoverse/slicc-kernel 1.172.0 → 1.172.2
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 -1
- package/dist/index.js +44 -6
- package/dist/index.js.map +3 -3
- package/dist/kernel-worker.js +96 -35
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.cjs +1 -1
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +8 -2
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +1 -1
- package/dist/types/fs/portable.d.ts +11 -0
- package/dist/types/fs/roots.d.ts +1 -1
- package/dist/types/mount/media.d.ts +3 -1
- package/dist/types/serve.d.ts +6 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ TypeScript declarations ship in `dist/types/`, and each entry (`.`, `./node`, `.
|
|
|
25
25
|
|
|
26
26
|
| option | default | |
|
|
27
27
|
| --- | --- | --- |
|
|
28
|
-
| `root` | `navigator.storage.getDirectory()` | the `FileSystemDirectoryHandle` that becomes
|
|
28
|
+
| `root` | `navigator.storage.getDirectory()` | the `FileSystemDirectoryHandle` that becomes `/`. WebKit cannot clone a handle, so there it must lie in OPFS, and the kernel worker reopens it by path |
|
|
29
29
|
| `modules` | `'/node_modules'` | where installed packages are scanned for commands |
|
|
30
30
|
| `commands` | none | `{ append: ['/opt/user/node_modules'] }`: more `node_modules` directories scanned for commands after all the others, so they never shadow one (see [Commands](#commands)) |
|
|
31
31
|
| `env` | `{}` | environment added to every run |
|
|
@@ -635,6 +635,7 @@ WASIX programs get `AF_INET`/`AF_INET6` `SOCK_DGRAM` sockets, and `AF_UNIX` ones
|
|
|
635
635
|
- **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.
|
|
636
636
|
- **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.
|
|
637
637
|
- **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`. A rejection with `detail: 'transport'` says the transport could not make the request at all. `fetchTransport()` sets it when its `fetch` rejects without a `cause.code`, as a browser's does, and a JS program's `fetch` gets it as `cause.detail` (above). Any other `detail` is dropped.
|
|
638
|
+
- **`User-Agent` in a browser:** `fetchTransport()` leaves the header out when it uses the realm's own `fetch` in a page or a worker, or anywhere else `location` exists. Chromium drops it anyway, while WebKit sends it and preflights the request, which a registry refuses. A `fetch` you pass in gets it, and so does Node.
|
|
638
639
|
- **Streamed request bodies** ([#238](https://github.com/ai-ecoverse/slicc-kernel/issues/238)): a request comes with its body whole, as `body` (a `Uint8Array`), unless the transport sets `traits.streamingBodies`. Then a body the kernel doesn't hold whole comes as `bodyStream`, an async iterable of `Uint8Array`, never together with `body`. That's an upload through the proxy (`Content-Length` or chunked, curl's `-T`, git's push) and a JS program's `fetch` with a stream body. The kernel reads from the program only as fast as the transport pulls, applies no `maxRequestBody` and reserves no memory for it. A transport with a limit of its own rejects with `status: 413`. When the transport answers before it has read the whole body, the proxy closes the program's connection after the response, since the rest of the body is still on it. A transport without the trait keeps getting whole bodies, capped at its `maxRequestBody`, so existing transports need no change. A transport that wraps another (a router, an injector, a logger) and passes on the inner `traits` must forward `bodyStream`, or not claim `streamingBodies`: a wrapper that rebuilds the request from `url`, `method`, `headers`, `body` and `signal` would drop a streamed upload and still answer. `nodeTransport()` streams (undici's `fetch` with `duplex: 'half'`). `fetchTransport({ streamingBodies: true })` turns it on for a page that knows its targets speak HTTP/2 or later, since Chrome streams a `fetch` upload only there, so it's off by default. The trait is also how an embedder's own transport, such as the `slicc` CLI link's exit, takes streamed uploads; across the page and the kernel's worker, and to attached clients, the body is pulled one chunk at a time. `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.
|
|
639
640
|
- **Traits of the embedder's own:** `traits` may carry more fields than the kernel reads. The kernel keeps them, and attached clients get them as they are (`client.transport.traits`, `onTraits`). In TypeScript, `NetworkTraits` types its own fields and leaves any other as `unknown`. One is typed: `exit`, the default exit an embedder routes through, `{ kind, caps }` (`NetworkExit`). `kind` is `'local'`, `'remote'` or `'tailnet'` (or another string), `caps` lists what the exit carries (`['net', 'http', 'ws']`), and further fields are `unknown`. When traits change later, call `kernel.updateTransportTraits()`.
|
|
640
641
|
- **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. After a redirect to another origin the follow-up is expected without `Authorization`, which HTTP clients drop when they follow one; a `Location` matches without its `#fragment`. 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.
|
|
@@ -915,6 +916,7 @@ An `fsa` mount is a drive for a local folder, from the File System Access API, a
|
|
|
915
916
|
- **Insert request:** at the same time the kernel calls the page's `onMountPending({ target, source, insert })`, so the page can show "Insert a folder for /mnt/x" with a button.
|
|
916
917
|
- **Inserting:** calling `insert()` from that button's click picks the folder with `requestDirectory()`, or asks for permission again on a folder the drive had before. The medium goes in without a remount, and the state becomes `ok`. `kernel.insert(target, handle)` inserts a handle the page already has.
|
|
917
918
|
- **Persistence:** the drive's source is `fsa:<id>`, and the kernel keeps the folder's handle under that id in IndexedDB (`<metadata>:media`). Mounting `fsa:<id>` again, after a reboot too, inserts the folder at once while its permission holds, and asks again when it is back to `prompt`. In an Incognito window Chrome crashes a page that reads a folder handle back from IndexedDB ([#95](https://github.com/ai-ecoverse/slicc-kernel/issues/95)). Pass `media: false` there: the kernel then never opens the handle database, and keeps the handles in memory, so a remount finds its folder only within the same kernel, while POSIX metadata still persists. `metadata: false` keeps the handles in memory too.
|
|
919
|
+
- **WebKit:** it has no folder picker and cannot clone a handle or keep one in IndexedDB, so a folder must lie in OPFS. The page sends its path, the kernel worker reopens it, and the media store keeps the path.
|
|
918
920
|
- **Ejecting:** `umount` ejects. So does the permission going away: the next operation fails with `ENOMEDIUM` and the kernel asks again. The folder is untouched either way.
|
|
919
921
|
- **Limits:** the folder has no symlinks or modes, and `df` reports no size for it either.
|
|
920
922
|
|
package/dist/index.js
CHANGED
|
@@ -546,6 +546,32 @@ function integrityUnknown(options) {
|
|
|
546
546
|
return unknown;
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
// src/fs/portable.ts
|
|
550
|
+
var privateTop = () => navigator.storage.getDirectory();
|
|
551
|
+
function clonable(handle) {
|
|
552
|
+
const { port1, port2 } = new MessageChannel();
|
|
553
|
+
try {
|
|
554
|
+
port1.postMessage(handle);
|
|
555
|
+
return true;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
if (err?.name === "DataCloneError") return false;
|
|
558
|
+
throw err;
|
|
559
|
+
} finally {
|
|
560
|
+
port1.close();
|
|
561
|
+
port2.close();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function portable(handle, top = privateTop) {
|
|
565
|
+
if (clonable(handle)) return { handle };
|
|
566
|
+
const path = await (await top()).resolve(handle);
|
|
567
|
+
if (path === null) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
"this browser cannot hand a folder to a worker unless it is in the origin private file system"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return { path };
|
|
573
|
+
}
|
|
574
|
+
|
|
549
575
|
// src/integrity-relay.ts
|
|
550
576
|
function integrityAnswerer(port, lookup) {
|
|
551
577
|
const ask = async ({ id, path }) => {
|
|
@@ -1314,6 +1340,11 @@ async function* chunks(body, idleMs) {
|
|
|
1314
1340
|
await reader.cancel().catch(() => void 0);
|
|
1315
1341
|
}
|
|
1316
1342
|
}
|
|
1343
|
+
function sentHeaders(headers, inPage) {
|
|
1344
|
+
const out = joinHeaders(headers);
|
|
1345
|
+
if (inPage) out.delete("user-agent");
|
|
1346
|
+
return out;
|
|
1347
|
+
}
|
|
1317
1348
|
function streamOf(body) {
|
|
1318
1349
|
const it = body[Symbol.asyncIterator]();
|
|
1319
1350
|
return new ReadableStream(
|
|
@@ -1341,6 +1372,7 @@ function fetchTransport(options = {}) {
|
|
|
1341
1372
|
const Socket = options.webSocket === true ? globalThis.WebSocket : options.webSocket || void 0;
|
|
1342
1373
|
const headers = options.webSocketHeaders === true;
|
|
1343
1374
|
const instance = options.redirect ?? "follow";
|
|
1375
|
+
const inPage = !options.fetch && "location" in globalThis;
|
|
1344
1376
|
return {
|
|
1345
1377
|
traits: {
|
|
1346
1378
|
manualRedirects: instance === "manual",
|
|
@@ -1357,7 +1389,7 @@ function fetchTransport(options = {}) {
|
|
|
1357
1389
|
try {
|
|
1358
1390
|
response = await send(request.url, {
|
|
1359
1391
|
method: request.method,
|
|
1360
|
-
headers:
|
|
1392
|
+
headers: sentHeaders(request.headers, inPage),
|
|
1361
1393
|
...requestBody(request),
|
|
1362
1394
|
signal: request.signal,
|
|
1363
1395
|
redirect: request.redirect ?? instance,
|
|
@@ -2442,11 +2474,17 @@ function uplinkInit(uplink) {
|
|
|
2442
2474
|
const nameserver = typeof uplink.nameserver === "string" ? { nameserver: uplink.nameserver } : {};
|
|
2443
2475
|
return { uplink: { traits, ...uplink.routes ? { routes: uplink.routes } : {}, ...nameserver } };
|
|
2444
2476
|
}
|
|
2445
|
-
function
|
|
2477
|
+
function rootField(at) {
|
|
2478
|
+
return "path" in at ? { rootPath: at.path } : { root: at.handle };
|
|
2479
|
+
}
|
|
2480
|
+
async function insertCall(target, handle, source) {
|
|
2481
|
+
return { op: "insert", target, source, ...await portable(handle) };
|
|
2482
|
+
}
|
|
2483
|
+
async function initCall(options, transport) {
|
|
2446
2484
|
const hooks = sealedHooks(options.sealed);
|
|
2447
2485
|
return {
|
|
2448
2486
|
op: "init",
|
|
2449
|
-
...options.root ?
|
|
2487
|
+
...options.root ? rootField(await portable(options.root)) : {},
|
|
2450
2488
|
...options.modules ? { modules: options.modules } : {},
|
|
2451
2489
|
...options.commands ? { commands: options.commands } : {},
|
|
2452
2490
|
...options.env ? { env: options.env } : {},
|
|
@@ -2742,7 +2780,7 @@ async function createKernel(options = {}) {
|
|
|
2742
2780
|
const pendingMedium = ({ target, source, handle }) => options.onMountPending?.({
|
|
2743
2781
|
target,
|
|
2744
2782
|
source,
|
|
2745
|
-
insert: async () => void await call(
|
|
2783
|
+
insert: async () => void await call(await insertCall(target, await chosen(handle), source))
|
|
2746
2784
|
});
|
|
2747
2785
|
const aside = (data) => {
|
|
2748
2786
|
if (files.event(data)) return true;
|
|
@@ -2815,7 +2853,7 @@ async function createKernel(options = {}) {
|
|
|
2815
2853
|
};
|
|
2816
2854
|
}
|
|
2817
2855
|
const { dial } = dials;
|
|
2818
|
-
await call(initCall(options, transport));
|
|
2856
|
+
await call(await initCall(options, transport));
|
|
2819
2857
|
return {
|
|
2820
2858
|
async run(argv, runOptions = {}) {
|
|
2821
2859
|
const { onStdout, onStderr, stdin, ...rest } = runOptions;
|
|
@@ -2841,7 +2879,7 @@ async function createKernel(options = {}) {
|
|
|
2841
2879
|
mount: async (spec) => await call({ op: "mount", spec }),
|
|
2842
2880
|
umount: async (target) => void await call({ op: "umount", target }),
|
|
2843
2881
|
mounts: async () => await call({ op: "mounts" }),
|
|
2844
|
-
insert: async (target, handle) => void await call(
|
|
2882
|
+
insert: async (target, handle) => void await call(await insertCall(target, handle)),
|
|
2845
2883
|
...pageOnly(call),
|
|
2846
2884
|
watch: files.watch,
|
|
2847
2885
|
terminate() {
|