@ai-ecoverse/slicc-kernel 1.171.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 +25 -8
- package/dist/driver-worker.js +4 -2
- package/dist/driver-worker.js.map +2 -2
- package/dist/index.js +260 -42
- package/dist/index.js.map +4 -4
- package/dist/kernel-worker.js +602 -230
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-driver-worker.js +4 -2
- package/dist/node-driver-worker.js.map +2 -2
- package/dist/node-process-worker.cjs +1 -1
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +462 -233
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +1 -1
- package/dist/testing.js +498 -3
- package/dist/testing.js.map +4 -4
- package/dist/types/fs/attest.d.ts +12 -2
- package/dist/types/fs/portable.d.ts +11 -0
- package/dist/types/fs/roots.d.ts +1 -1
- package/dist/types/index.d.ts +12 -4
- package/dist/types/kernel/net/http-syscalls.d.ts +3 -1
- package/dist/types/kernel/net/loopback-transport.d.ts +1 -1
- package/dist/types/kernel/net/placeholder-floor.d.ts +9 -3
- package/dist/types/kernel/net/remote-transport.d.ts +4 -1
- package/dist/types/kernel/net/sealed-fetch.d.ts +27 -0
- package/dist/types/kernel/net/transport.d.ts +2 -1
- package/dist/types/kernel/socket.d.ts +4 -1
- package/dist/types/launcher.d.ts +12 -2
- package/dist/types/mount/media.d.ts +3 -1
- package/dist/types/node.d.ts +12 -4
- package/dist/types/sealed-relay.d.ts +12 -2
- package/dist/types/serve.d.ts +14 -5
- package/dist/types/testing.d.ts +3 -0
- package/dist/types/transport.d.ts +2 -1
- 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 |
|
|
@@ -41,7 +41,7 @@ TypeScript declarations ship in `dist/types/`, and each entry (`.`, `./node`, `.
|
|
|
41
41
|
| `hostname` | `'slicc'` | the kernel's node name (see [Node name](#node-name)) |
|
|
42
42
|
| `users` | `'enforce'` | `'off'`, `'report'` or `'enforce'`: whether files have owners and their modes are checked (see [File owners](#file-owners) and [Permissions](#permissions)), and whether `sudo` elevates (see [Sudo](#sudo)). `'off'` behaves exactly as kernels before owners did; any other value rejects. `createNodeKernel` takes it too, with the same default. Kernels up to 1.73 defaulted to `'off'` |
|
|
43
43
|
| `onDenial` | none | `({ pid, uid, op, path, errno, tag? }) => void`: called for every permission check that fails, under `'report'` and `'enforce'` (see [Permissions](#permissions)). `createNodeKernel` takes it too |
|
|
44
|
-
| `sealed` | none | `(req, { signal }) => Promise<object
|
|
44
|
+
| `sealed` | none | `(req, { signal }) => Promise<object>`, or `{ request, fetch? }` with that function as `request`: `request` answers requests written to [`/dev/sealed`](#devsealed), and `fetch` fills requests that carry a sealed placeholder (see **The fetch hook** under [`/dev/sealed`](#devsealed)). `createNodeKernel` takes it too. An exported `SEALED_FETCH_VERSION`, the number `1`, says that this kernel takes the object |
|
|
45
45
|
| `clipboard` | none | `{ read(stamp) => Promise<string>, write(text, stamp) => Promise<void> }`: the clipboard behind [`/dev/clipboard`](#devclipboard). `createNodeKernel` takes it too |
|
|
46
46
|
| `requests` | none | `{ kinds, handle(request) }`: the page's answers to [`/dev/embedder`](#devembedder) for the processes it starts. `createNodeKernel` takes it too |
|
|
47
47
|
| `approve` | none | `(req, { signal }) => 'once' \| 'always' \| 'deny'` (or a promise of one): decides a `sudo` request that needs approval (see [Sudo](#sudo)); without it, such a request is denied. `createNodeKernel` takes it too |
|
|
@@ -153,13 +153,14 @@ Reach a server a program runs on the kernel's loopback (a dev server, `python -m
|
|
|
153
153
|
|
|
154
154
|
### `kernel.attest(path)` and `kernel.watch(paths, onChange)`
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
Neither is on an attached client (`attachKernel`, `connect()`), so no process or client can ask for them. `watch` is page-only; `attest` is on `createNodeKernel`'s kernel too. They are for a page that has to trust a root-owned file, such as a policy file that only `sudo` may change.
|
|
157
157
|
|
|
158
158
|
- **`attest(path)`** resolves with exactly `{ path, bytes, uid, gid, mode, ino, mtimeMs, sha256, users, onRootFs, parents }`.
|
|
159
159
|
- `path` is the canonical absolute path. `bytes` is a `Uint8Array` of its own, holding the file's contents. `sha256` is the lowercase hex digest of exactly those bytes. `mode` includes the type bits. `users` is the kernel's `users` mode. `onRootFs` is always `true`. `parents` lists `{ path, uid, gid, mode }` for every directory from `/` down to the file's own.
|
|
160
160
|
- The kernel answers it itself, over the root file system without permission checks, and lstats every component. It reads the file between two lstats and accepts the read only when the inode, the modification time and the size are unchanged and the bytes match the size. Otherwise it reads once more, then fails with `EAGAIN`.
|
|
161
161
|
- It refuses with an `Error` whose `code` is: `EACCES` for a symlink at the path or in any parent, for a path at or under a mount (any mount, one at `/` included; a mount that failed and isn't installed doesn't count), and for a parent that isn't owned by uid 0 or that group or others may write; `EISDIR` for a directory; `EINVAL` for another kind of file or a path that isn't absolute; `ENOTDIR` for a parent that isn't a directory; `ENOENT` for a missing one; and `EFBIG` for a file over 1 MiB.
|
|
162
162
|
- Under `users: 'off'` there are no owners, so the parent check is skipped and the answer shows what the file system shows (uid and gid 1000, as processes see them). A consumer should accept only `users: 'enforce'`.
|
|
163
|
+
- **`attest(path, { absent: 'allow' })`** answers the same for a file that exists. When the path itself is missing, and every directory from `/` to its parent exists and passes the checks above (no symlink, owned by uid 0 and not writable by group or others unless `users` is `'off'`, not at or under a mount), it resolves with exactly `{ path, absent: true, users, onRootFs: true, parents }`: a trusted statement that the file isn't there, with no ids, hash or bytes. A missing parent, a symlink, a directory, another kind of file, an untrusted parent, a mount or a file that's too big rejects as without the option, and any other value of `absent` is `EINVAL`.
|
|
163
164
|
- **`watch(paths, onChange)`** resolves with an unsubscribe function. `onChange(path)` is called once per burst of changes, 50 ms after the last one, for each watched file that changed.
|
|
164
165
|
- A change is a write, a truncate, a rename, a removal, or a change of mode, owner or times of the file or of any directory above it. A change to a parent directory's own metadata counts too, and creating or removing a sibling changes it when the change comes from another kernel.
|
|
165
166
|
- It sees changes made through any slicc kernel on the origin, in this tab or another one: the kernels tell each other which files are watched and report writes to those. `watch` resolves once every other live kernel on the origin has the watch (a kernel that doesn't answer within 500 ms is not waited for), so a write made after it resolves is reported. A kernel that ends with watches still on is forgotten (by its Web Lock) at the next reported write or unanswered announcement. This needs the shared metadata (the default): with `metadata: false` a kernel sees only its own changes. It doesn't see writes made with the OPFS API outside a kernel, such as a page script or an installer writing the root directly. After such a write the page should attest again itself (and may also attest on tab focus, as a safety net).
|
|
@@ -479,7 +480,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
479
480
|
- `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`;
|
|
480
481
|
- `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`;
|
|
481
482
|
- `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.
|
|
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
|
|
483
|
+
- **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 goes the same way as from `ctx.fetch` (see **The transport** under [`/dev/sealed`](#devsealed)).
|
|
483
484
|
- **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.
|
|
484
485
|
- **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`.
|
|
485
486
|
- **Non-HTTP protocols aren't carried:**
|
|
@@ -487,7 +488,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
487
488
|
- non-HTTP first bytes on any other port get a `400` and a close;
|
|
488
489
|
- a server-speaks-first protocol on another port (the client sends nothing) waits for the proxy's 120 s idle limit.
|
|
489
490
|
- For a destination the uplink routes, these cases take A2 instead (below).
|
|
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):
|
|
491
|
+
- **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 goes to the fetch hook or is refused):
|
|
491
492
|
|
|
492
493
|
| The call | Uplink destination | Any other destination |
|
|
493
494
|
|---|---|---|
|
|
@@ -634,6 +635,7 @@ WASIX programs get `AF_INET`/`AF_INET6` `SOCK_DGRAM` sockets, and `AF_UNIX` ones
|
|
|
634
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.
|
|
635
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.
|
|
636
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.
|
|
637
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.
|
|
638
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()`.
|
|
639
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.
|
|
@@ -780,7 +782,7 @@ interface ConnectorStamp { pid: number; ppid: number; uid: number; gid: number;
|
|
|
780
782
|
|
|
781
783
|
- **Declared at boot:** `facades` names each facade (`[a-z][a-z0-9-]*`) and its port, from 1 to 65535. A port may not be 9222 (the CDP facade), 3128 (the realm proxy) or another facade's; anything else throws a `TypeError`. `createNodeKernel` takes `facades` too. The hook may be left out and supplied later by an attached client's `client.serveFacade(name, hook)`, which works only for a declared name.
|
|
782
784
|
- **Programs** find each facade in their environment as `SLICC_<NAME>_URL`, upper-cased with `-` as `_`, e.g. `SLICC_LINK_URL=tcp://127.0.0.1:7400`, and connect to it with an ordinary TCP `connect()`: a JS program's `ctx.net.connect`, an Emscripten program's socket, or a WASIX program's `sock_connect`. Every ABI ends in the same kernel call.
|
|
783
|
-
- **The stamp** is the connecting process's identity, taken by the kernel at `connect()` from its credentials, as Linux's `SO_PEERCRED` is: its pid and parent pid as `ps` shows them, effective uid and gid, user name, groups, the `users` mode and the process's tag (see `kernel.run`), the same fields as a `/dev/sealed` request's `caller`. A program cannot forge it. The hook gets it and `{ name }`, so one function can serve several facades, and decides: it resolves with a connection, whose bytes then pass both ways until either side closes, or it rejects, and the program's connection is closed. A connection while no hook is attached is closed at once, the same as a refusal. Bytes from the hook wait in the kernel until the program reads them, up to 16 MiB per connection: past that, the connection is closed and the hook's `close(reason)` gets `slicc-kernel: more than 16777216 bytes are waiting for the program to read`. A program that connects to anything else pays nothing for this: the stamp is built only for a facade's port.
|
|
785
|
+
- **The stamp** is the connecting process's identity, taken by the kernel at `connect()` from its credentials, as Linux's `SO_PEERCRED` is: its pid and parent pid as `ps` shows them, effective uid and gid, user name, groups, the `users` mode and the process's tag (see `kernel.run`), the same fields as a `/dev/sealed` request's `caller`. A program cannot forge it. The hook gets it and `{ name }`, so one function can serve several facades, and decides: it resolves with a connection, whose bytes then pass both ways until either side closes, or it rejects, and the program's connection is closed. A connection while no hook is attached is closed at once, the same as a refusal. Bytes from the hook wait in the kernel until the program reads them, up to 16 MiB per connection: past that, the connection is closed and the hook's `close(reason)` gets `slicc-kernel: more than 16777216 bytes are waiting for the program to read`. A program that connects to anything else pays nothing for this: the stamp is built only for a facade's port, and for the realm proxy's when the kernel has a fetch hook (see **The fetch hook** under [`/dev/sealed`](#devsealed)).
|
|
784
786
|
- **Handing a connection on hands over the identity:** a facade connection that a process passes to another (a fork, an exec, an inherited descriptor) still speaks as the process that connected, as on Linux. So a privileged process should not pass one to a less trusted one.
|
|
785
787
|
- **Reserved:** a facade's port is reserved from the start. A program's `bind()` to it fails with `EADDRINUSE`, so no program can squat it and impersonate the page, and ephemeral ports skip it. Port 9222 is reserved the same way. Like 9222, a facade's port is kernel-only: the page's `kernel.dial`, a client's `dial` and `loopbackFetch` (and so `<port>.kernel.localhost`) are refused with `ECONNREFUSED`.
|
|
786
788
|
|
|
@@ -914,6 +916,7 @@ An `fsa` mount is a drive for a local folder, from the File System Access API, a
|
|
|
914
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.
|
|
915
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.
|
|
916
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.
|
|
917
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.
|
|
918
921
|
- **Limits:** the folder has no symlinks or modes, and `df` reports no size for it either.
|
|
919
922
|
|
|
@@ -1134,12 +1137,26 @@ The kernel then asks the hook `{ v: 1, op: 'materializeConsume', ticket, stamp,
|
|
|
1134
1137
|
|
|
1135
1138
|
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.
|
|
1136
1139
|
|
|
1137
|
-
**The transport.**
|
|
1140
|
+
**The transport.** A request with a sealed placeholder never reaches the transport. The kernel looks for one before anything else, so a held redirect isn't replayed for it either. 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>` counts, whether or not the vault knows it, and so does anything in the list of `kernel.sealed.setPlaceholders`. The kernel then asks the fetch hook (below) or refuses.
|
|
1138
1141
|
|
|
1139
1142
|
- **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.
|
|
1140
|
-
- **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
|
|
1143
|
+
- **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.
|
|
1141
1144
|
- **What stays as it was:** the kernel's own loopback, A2 sessions (above) and raw sockets never reach this check.
|
|
1142
1145
|
|
|
1146
|
+
**The fetch hook.** `createKernel({ sealed: { request, fetch } })`, or `createNodeKernel` with the same option, gives the kernel `fetch(req, { signal })`, which is asked instead of a refusal. A bare function as `sealed` is `{ request }`, so `/dev/sealed`, tickets and A2 sessions work with either form. `SealedFetchHook` and `SealedFetchRequest` are exported from both entries.
|
|
1147
|
+
|
|
1148
|
+
- **When it's asked:** only with `users: 'enforce'`, only for a request of a process, and never for a WebSocket. In every other case, and with no hook, the request is refused as above: there's nothing else to fall back on.
|
|
1149
|
+
- **`req`** is `{ v: 1, caller, method, url, headers, redirect: 'manual', body?, bodyStream? }`. `headers` is the flat list of pairs of a transport request. `bodyStream` is an async iterable that yields one chunk each time it's asked, so an upload moves at the speed the hook reads it.
|
|
1150
|
+
- **`caller`** is the same stamp as a `/dev/sealed` request's, and no program can supply it. For the realm proxy it's the stamp of the process that connected, taken at `connect()`, as `SO_PEERCRED` is. For `ctx.fetch` it's built for each request from the process's current credentials, so it follows a `setresuid`, and a process that has exited has none.
|
|
1151
|
+
- **The reply** is a transport response that has been scrubbed: `{ status, statusText, headers, body, cancel, url? }`. The kernel gives it to the program as it is, one hop, and keeps none of it. A redirect is a 3xx for the program to follow, with its original placeholders and a new call to the hook.
|
|
1152
|
+
- **A failed call:** a hook that throws, a `signal` that aborts, and a reply that isn't a response end in a refusal with `refused: the request with a sealed placeholder was not completed` and the reason, which the program sees, so a hook's error message must be scrubbed too. So does a reply with `redirects`, even an empty list, or `redirected: true`, which means something followed a redirect with the credential: the kernel cancels it. The request never reaches the transport.
|
|
1153
|
+
- **`signal`** aborts when the program drops the request, on `terminate()`, and after 5 minutes with neither a head, an upload chunk nor a response chunk. After the head, a dropped response also calls `cancel()`.
|
|
1154
|
+
- **On the page**, the call runs from the kernel worker through a `MessagePort` of its own. Each upload or response chunk is sent when the far side asks for it, so one chunk at most is on the way in each direction. When the port closes or the kernel ends, both sides stop.
|
|
1155
|
+
|
|
1156
|
+
The stamp also goes to the transport, as the third argument of `transport.fetch(request, owner, stamp)`, while the kernel has a fetch hook. It's for the transport's own routing: the kernel never takes a stamp or an owner from a client or a process.
|
|
1157
|
+
|
|
1158
|
+
**`kernel.sealed.setPlaceholders(list)`** replaces the list of placeholders the check looks for beside the form above, for a vault whose placeholders don't all have that form. `list` is an array of non-empty strings (`EINVAL` otherwise), placeholders and never values. It's on the page's `Kernel` and on `createNodeKernel`'s, and not on an attached client or a process. The form above is always checked, so an empty or outdated list changes nothing about it.
|
|
1159
|
+
|
|
1143
1160
|
### `/dev/clipboard`
|
|
1144
1161
|
|
|
1145
1162
|
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/driver-worker.js
CHANGED
|
@@ -397,7 +397,7 @@ var RemoteTransport = class {
|
|
|
397
397
|
waiter?.reject(new TransportError("the request was cancelled"));
|
|
398
398
|
this.port.postMessage({ net: "cancel", nid });
|
|
399
399
|
}
|
|
400
|
-
async fetch(request) {
|
|
400
|
+
async fetch(request, owner, stamp) {
|
|
401
401
|
const nid = ++this.nextId;
|
|
402
402
|
const { signal, redirect } = request;
|
|
403
403
|
signal.throwIfAborted();
|
|
@@ -427,7 +427,9 @@ var RemoteTransport = class {
|
|
|
427
427
|
method: request.method,
|
|
428
428
|
headers: request.headers,
|
|
429
429
|
...request.bodyStream ? { streamed: true } : request.body ? { body: request.body } : {},
|
|
430
|
-
...redirect !== void 0 ? { redirect } : {}
|
|
430
|
+
...redirect !== void 0 ? { redirect } : {},
|
|
431
|
+
...owner !== void 0 ? { owner } : {},
|
|
432
|
+
...stamp ? { stamp } : {}
|
|
431
433
|
});
|
|
432
434
|
} catch (e) {
|
|
433
435
|
finish();
|