@ai-ecoverse/slicc-kernel 1.181.0 → 1.183.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 +62 -11
- package/dist/driver-worker.js +290 -90
- package/dist/driver-worker.js.map +4 -4
- package/dist/index.js +328 -102
- package/dist/index.js.map +4 -4
- package/dist/kernel-worker.js +1727 -703
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-driver-worker.js +298 -98
- package/dist/node-driver-worker.js.map +4 -4
- package/dist/node-process-worker.cjs +849 -193
- package/dist/node-process-worker.cjs.map +4 -4
- package/dist/node-process-worker.js +849 -193
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +1897 -751
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +805 -149
- package/dist/process-worker.js.map +4 -4
- package/dist/types/cdp/types.d.ts +2 -0
- package/dist/types/client/attach.d.ts +2 -0
- package/dist/types/client/protocol.d.ts +1 -0
- package/dist/types/client/serve-client.d.ts +1 -0
- package/dist/types/devices-relay.d.ts +74 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/kernel/devices.d.ts +155 -0
- package/dist/types/kernel/embedder.d.ts +2 -0
- package/dist/types/kernel/fd-table.d.ts +3 -1
- package/dist/types/kernel/host.d.ts +2 -0
- package/dist/types/kernel/process.d.ts +20 -0
- package/dist/types/kernel/serial-ioctl.d.ts +73 -0
- package/dist/types/kernel/socket.d.ts +2 -0
- package/dist/types/launcher.d.ts +4 -0
- package/dist/types/node.d.ts +6 -2
- package/dist/types/process/js/js-dev.d.ts +24 -0
- package/dist/types/process/kernel-streams.d.ts +11 -1
- package/dist/types/process/process-pty.d.ts +1 -0
- package/dist/types/process/wasi/wasi-fds.d.ts +1 -0
- package/dist/types/process/wasi/wasi-termios.d.ts +1 -0
- package/dist/types/serve.d.ts +14 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -45,8 +45,9 @@ TypeScript declarations ship in `dist/types/`, and each entry (`.`, `./node`, `.
|
|
|
45
45
|
| `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 |
|
|
46
46
|
| `clipboard` | none | `{ read(stamp) => Promise<string>, write(text, stamp) => Promise<void> }`: the clipboard behind [`/dev/clipboard`](#devclipboard). `createNodeKernel` takes it too |
|
|
47
47
|
| `requests` | none | `{ kinds, handle(request) }`: the page's answers to [`/dev/embedder`](#devembedder) for the processes it starts. `createNodeKernel` takes it too |
|
|
48
|
+
| `devices` | none | `{ open(request) }`: the page's answers for the serial device nodes it adds with [`kernel.devices`](#kerneldevices) (see [Embedder devices](#embedder-devices)). `createNodeKernel` takes it too |
|
|
48
49
|
| `approve` | none | `(req, { signal }) => 'once' \| 'always' \| 'deny'` (or a promise of one), or `{ answer: 'once' \| 'always', timestamp: true }` when a person approved: decides a `sudo` request that needs approval (see [Sudo](#sudo) and [Sudo timestamps](#sudo-timestamps)); without it, such a request is denied. `createNodeKernel` takes it too |
|
|
49
|
-
| `cdp` | none | `({ runtime }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
|
|
50
|
+
| `cdp` | none | `({ runtime, stamp }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
|
|
50
51
|
| `facades` | none | `{ link: { port, hook? } }`: kernel-only loopback ports whose connections a hook on the page serves, with the connecting program's identity (see [Page facades](#page-facades)) |
|
|
51
52
|
| `protect` | none | `{ paths, writers? }`: directories no process may change, root included, in every `users` mode, except processes with one of the `writers` tags (see [Protected paths](#protected-paths)). `createNodeKernel` takes it too |
|
|
52
53
|
|
|
@@ -95,6 +96,8 @@ Makes a port for another client of the same kernel. Hand it to a dedicated worke
|
|
|
95
96
|
|
|
96
97
|
**`requests`** binds the port's [`/dev/embedder`](#devembedder) requests when the port is made: a hook (`{ kinds, handle }`, as `createKernel`'s) answers for every process the port's client starts, in the embedder's own realm, so a worker client (an agent) never answers or registers requests itself. `'attach'` lets the client that attaches to the port bring its own hook (`attachKernel(port, { requests })`, protocol 1.13); without it, an attach with `requests` fails with `EPERM`. A port with neither answers nothing: its processes' requests are `ENOTSUP`.
|
|
97
98
|
|
|
99
|
+
**`devices`** binds a [devices hook](#embedder-devices) the same way (protocol 1.18): a hook (`{ open }`) answers the opens of the nodes the page adds for the port (`kernel.devices.add({ client: port, … })`), in the embedder's own realm, and `'attach'` lets the client that attaches to the port bring its own (`attachKernel(port, { devices })`); without it, an attach with `devices` fails with `EPERM`, and a node added for a port with no hook is `ENOTSUP`.
|
|
100
|
+
|
|
98
101
|
**`user`** (a name or uid) binds the client to that user; without it the client is root. The binding is a capability: the client's `spawn`, `run` and `openTerminal` run as that user, its `ps` lists only that user's processes, and its `kill` follows POSIX permission. A bound client can't run anything as another user (`code: 'EPERM'`). A root client may pass `user` to `spawn`, `run` or `openTerminal` to run one command as any user. An unknown user rejects with `code: 'ENOENT'`. **`tag`** is the tag (see `kernel.run`) of every process the client starts; its own `spawn`, `run` and `openTerminal` cannot name another. They may add one with their own `tag` option (protocol 1.12; an older kernel answers `ENOSYS`): the process then has both tags, the connection's first, and so does everything it starts. The option may also be a list of at most 8 tags, added in order (protocol 1.15; against an older kernel the client rejects with `code: 'ENOSYS'` before sending anything), so a caller can mark one call's tree with a tag of its own for [`trace`](#clienttracetag-onevents--limit---promise-close-) beside a tag that means something else. A per-spawn tag only adds; no client, program or child can remove or replace a tag ([#1380](https://github.com/ai-ecoverse/slicc-kernel/issues/1380)).
|
|
99
102
|
|
|
100
103
|
### `kernel.sudoRun(request, { stdout, stderr, tag }?) → Promise<{ pid, pgid, ended }>`
|
|
@@ -132,6 +135,23 @@ await kernel.users.add({ name: 'scoop-site', groups: ['cone'], homeGroup: 'cone'
|
|
|
132
135
|
|
|
133
136
|
The cone's home lets its group in only to pass through (`0710`), and the cone opens what it shares with `chmod g+r`. A scoop's home belongs to the cone's group, so the cone reads it, and new files there take that group. Change `/etc/passwd` and `/etc/group` through `kernel.users` and `kernel.groups`, which keep the two consistent and serialize their writes.
|
|
134
137
|
|
|
138
|
+
### `kernel.devices`
|
|
139
|
+
|
|
140
|
+
Serial device nodes the embedder backs, on the page and the Node entry (not on attached clients); see [Embedder devices](#embedder-devices):
|
|
141
|
+
|
|
142
|
+
| | |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| `add({ path, owner?, group?, mode?, links?, client? })` | adds a character device directly under `/dev` and resolves with `{ remove() }`. `path` is `ttyUSB<n>` or `ttyACM<n>` (with or without `/dev/`, `n` from 0 to 255); `owner` and `group` are names or ids (default `0`), `mode` the permission bits (default `0o600`), `links` names of symlinks the kernel makes to the node in `/dev/serial/by-id/` (a bare name or the full path), and `client` the port from `kernel.connect({ devices })` whose hook answers the node's opens (default: the page's own `devices` hook). A name, a link or a client that doesn't fit, or an unknown owner or group, rejects with `EINVAL`; a name or a link that exists with `EEXIST`; a client without a devices hook with `ENOTSUP` |
|
|
145
|
+
| `remove()` | deletes the node and its links. Descriptors still open on it fail every call with `EIO` from then on, and `poll` reports `POLLHUP` and `POLLERR`, as on Linux when a USB serial adapter is unplugged. A second `remove()` does nothing |
|
|
146
|
+
|
|
147
|
+
When the client whose hook answers a node goes away (its port closes), its nodes are removed the same way.
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
const kernel = await createKernel({ devices: { open: ({ path, flags, stamp, signal }) => port(path) } });
|
|
151
|
+
await kernel.groups.add('dialout', { gid: 20 });
|
|
152
|
+
const node = await kernel.devices.add({ path: 'ttyUSB0', owner: 0, group: 'dialout', mode: 0o640, links: ['usb-0403_6001-if00'] });
|
|
153
|
+
```
|
|
154
|
+
|
|
135
155
|
### `kernel.chmod(path, mode) → Promise<void>`
|
|
136
156
|
|
|
137
157
|
Sets a file's permission bits (`0` to `0o7777`, `EINVAL` otherwise) as root, through the mounts, as the processes see them, for a file the page wrote some other way. For files the page writes straight to OPFS before any kernel runs, see `writeMetadata` under [Metadata](#metadata).
|
|
@@ -177,7 +197,7 @@ Neither is on an attached client (`attachKernel`, `connect()`), so no process or
|
|
|
177
197
|
|
|
178
198
|
### `attachKernel(port, options?) → Promise<KernelClient>`
|
|
179
199
|
|
|
180
|
-
Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker, another tab, or Node. `options.timeoutMs` (10 s by default) bounds the handshake: a port with no kernel behind it rejects with `KernelGoneError`. `options.requests` (`{ kinds, handle }`) answers the [`/dev/embedder`](#devembedder) requests of the client's processes, when the port was made with `connect({ requests: 'attach' })`.
|
|
200
|
+
Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker, another tab, or Node. `options.timeoutMs` (10 s by default) bounds the handshake: a port with no kernel behind it rejects with `KernelGoneError`. `options.requests` (`{ kinds, handle }`) answers the [`/dev/embedder`](#devembedder) requests of the client's processes, when the port was made with `connect({ requests: 'attach' })`. `options.devices` (`{ open }`) answers the opens of the [embedder devices](#embedder-devices) the page added for this client (`kernel.devices.add({ client: port })`), when the port was made with `connect({ devices: 'attach' })` (protocol 1.18; an older kernel ignores it).
|
|
181
201
|
|
|
182
202
|
| Client | |
|
|
183
203
|
|---|---|
|
|
@@ -189,7 +209,7 @@ Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker
|
|
|
189
209
|
| `fs` | `readFile`, `readText`, `writeFile(path, string \| bytes)`, `stat`, `lstat`, `readdir`, `mkdir` (with parents), `rm(path, { force })` (recursive), `rename`, `realpath`, `symlink(target, path)`, `readlink`, `exists`, on the processes' file system, mounts included (a path under a `tmpfs`, `fsa` or `hostfs` mount reads and writes the mounted file system, as `cat` in a process would); failures reject with `KernelCallError` and a POSIX `code` |
|
|
190
210
|
| `fetch({ url, method, headers, body, redirect, signal })` | through the kernel's network transport (the one the page passed to `createKernel`), with a streaming body; `redirect` is the per-request policy (see [Network](#network)); `transport` is the same as a `NetworkTransport` |
|
|
191
211
|
| `fs.watch(paths, { recursive }, onChange)` | resolves with `{ close() }`; `onChange` gets `{ paths }`, the changed paths at, below (or, without `recursive`, directly in) the watched ones, batched per task, or `{ overflow: true }` when there were too many to list, to rescan; it covers every change made through this kernel, by its processes and its clients, and a removal or rename that failed or found nothing to do is none. It also covers what another kernel on the same OPFS root changes (another tab, a worker page) and what a writer outside the kernel reports with [`postFsNotice`](#metadata), as those notices name them: a directory another kernel creates, removes or renames, the file it writes back, and the records of its symlinks, modes, owners and times, and a notice's `trees` as their roots. A peer's notices come only once the kernels share the root (`/proc/sys/kernel/slicc_shared_root` reads `1` from the join handshake on) and only with the shared metadata (not with `metadata: false`), and a writer outside the kernel that posts no notice is never heard. So a client that has to catch a page writing OPFS directly needs that page to call `postFsNotice` after each write. Kernels before this one didn't pass peer and outside changes to a client's watch; they reached only `ctx.fs.watch`. A watch is on names, not files: a watched path need not exist, nor its directories, and one under a regular file is a watch too (every kernel with `watch` does this, protocol 1.1 on), so its creation, by any `mkdir -p` above it, a write or a rename to it, is reported as its path, and so are a removal or rename of it or of a directory above it; the watch goes on hearing the name after that. So a client that waits for a directory to appear watches it, rather than polling `stat` and `readdir`. Under `users`, a watched path the client may not read is `EACCES`, and so is a missing one under a directory the client may not search. Changes are reported by the name they were made under (a file written through a symlink also by its real path), so a client that watches a path under a symlink watches its real path too, for a missing path the real path of its nearest existing directory with the rest of the path after it |
|
|
192
|
-
| `serveCdp(hook, { runtime })` | offers this client's browser to the kernel's programs (protocol 1.5): `hook` is the `cdp` hook of `createKernel`, asked once per program connection; resolves with `{ close() }`, and closing or detaching takes the offer back and closes its connections (see [Browser automation](#browser-automation-cdp)) |
|
|
212
|
+
| `serveCdp(hook, { runtime })` | offers this client's browser to the kernel's programs (protocol 1.5): `hook` is the `cdp` hook of `createKernel`, asked once per program connection with its `runtime` and the connecting process's `stamp` (protocol 1.17); resolves with `{ close() }`, and closing or detaching takes the offer back and closes its connections (see [Browser automation](#browser-automation-cdp)) |
|
|
193
213
|
| `serveFacade(name, hook) → { close() }` | serves a facade that `createKernel({ facades })` declared, the same way as its `hook` (protocol 1.11); the latest registration wins, over the embedder's hook too, and `close()` hands it back. An undeclared name is `EINVAL` |
|
|
194
214
|
| `trace(tag, onEvents, { limit }?)` | reports, as events, every program that a process carrying `tag` runs and every such process's end, with nothing missed (protocol 1.15; against an older kernel the client rejects with `code: 'ENOSYS'`); resolves with `{ close() }` (see [below](#clienttracetag-onevents--limit---promise-close-)) |
|
|
195
215
|
| `sudoRun(request, { stdout, stderr, tag }?)` | runs a `sudo` request again, as `kernel.sudoRun` does, and resolves with `{ pid, pgid, ended }` once it has started (protocol 1.16; against an older kernel the client rejects with `code: 'ENOSYS'` before sending anything). Only a root client may: a client bound to a user rejects with `code: 'EPERM'` (`sudo: only a root client may run a sudo request`). `ended` is tied to the client, as `spawn`'s is, while the process is not (see [Running a request later](#running-a-request-later-sudorun)) |
|
|
@@ -198,7 +218,7 @@ Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker
|
|
|
198
218
|
| `closed` | resolves with the error that ended the client |
|
|
199
219
|
| `users` | the kernel's `users` mode, `'off'`, `'report'` or `'enforce'`; `'off'` from kernels that don't say |
|
|
200
220
|
|
|
201
|
-
When the kernel goes away (the page closed or reloaded, `terminate()`), pending calls and `exited` reject with `KernelGoneError`, and so do later calls. Each side holds a Web Lock and waits on the other's, since a `MessagePort` reports no close in browsers; in Node, the port's `close` event does the same. The first message is a handshake on the protocol version, `1.
|
|
221
|
+
When the kernel goes away (the page closed or reloaded, `terminate()`), pending calls and `exited` reject with `KernelGoneError`, and so do later calls. Each side holds a Web Lock and waits on the other's, since a `MessagePort` reports no close in browsers; in Node, the port's `close` event does the same. The first message is a handshake on the protocol version, `1.17` (`1.0` had no `watch`, `1.1` no mounts, `1.2` no `spawn` into a group, `1.3` no `dial` and `1.4` no `serveCdp`, which reject with `code: 'ENOSYS'` on such a kernel; a `1.5` kernel has no [uplink](#uplink), a `1.6` kernel resolves names through one but connects nothing to it, a `1.7` kernel has no users, a `1.8` kernel has no `sudo` and no tags, a `1.9` kernel never announces new transport traits, a `1.11` kernel has no per-spawn `tag`, a `1.12` kernel takes no `requests` at attach, a `1.13` kernel has no `cwdMissing`, a `1.14` kernel has no `trace` and takes one tag per spawn, not a list, a `1.15` kernel has no `sudoRun`, which the client rejects with `code: 'ENOSYS'` before sending anything, and a `1.16` kernel asks a `serveCdp` hook with no `stamp`): a client or kernel of another major version is refused with an error naming both.
|
|
202
222
|
|
|
203
223
|
#### `client.trace(tag, onEvents, { limit }?) → Promise<{ close() }>`
|
|
204
224
|
|
|
@@ -416,7 +436,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
416
436
|
- **Another tab:** a kernel sharing the OPFS root reports its changes too ([#1391](https://github.com/ai-ecoverse/slicc-kernel/issues/1391)): a structural change (create, remove, rename, mkdir) as `'rename'`, a content or metadata change as `'change'`. Their order across a name isn't guaranteed, and a new directory may also come with a `'change'`, so a watcher should treat either as "look again". Another tab's change never ends or renames a watch, and a directory watch doesn't hear one at the directory itself; a file watch hears another tab's write as `'change'`.
|
|
417
437
|
- **Lifetime:** a watch ends with its process and doesn't keep the program alive.
|
|
418
438
|
- **The older watches:** `kernel.watch` is unchanged, and a client's `fs.watch` with `{ paths }` hears another tab's changes too (see [`attachKernel`](#attachkernelport-options--promisekernelclient)).
|
|
419
|
-
- `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`). `/dev/shm` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Removing a `/dev` entry fails (see [`/dev`](#dev-entries)), and so does a new name directly in `/dev` or `/proc`;
|
|
439
|
+
- `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`), and the embedder's serial nodes and their `/dev/serial/by-id` links (see [Embedder devices](#embedder-devices), whose handles have the terminal controls as methods). `/dev/shm` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Removing a `/dev` entry fails (see [`/dev`](#dev-entries)), and so does a new name directly in `/dev` or `/proc`;
|
|
420
440
|
- `/proc`, in `fs`, `open` and their `sync` forms, the same as a WASI program's (see [`/proc`](#proc)): `stat`, `lstat`, `exists`, `readdir`, `readlink` and `readFile` answer it, and `/proc/self` is the program's own pid. It's read-only, so every change in it is `EROFS`: `writeFile`, `mkdir`, `rm`, `unlink`, `chmod`, a `rename`, `link` or `symlink` into or out of it, and an `open` for writing, creating or truncating. A file opened in it is a snapshot read at the open, with no descriptor (`fd` is `-1`): its `read` and `size` answer from the snapshot until it's closed, and `write` and `truncate` are `EBADF`. Other paths pay only a prefix check;
|
|
421
441
|
- `signals`: `on(signal, handler)` catches a signal (by number or as `'SIGINT'`), `ignore(signal)` ignores it and `reset(signal)` restores its default action. A call blocked in a read or write is restarted after a handler runs; signals without a handler keep their default actions, so an uncaught `SIGINT` ends the program with 130 and `SIGKILL` always does;
|
|
422
442
|
- `sync`: `read`, `write`, `close`, `open` (whose files' methods are synchronous too) and `fs`, made synchronously, for code that cannot `await`, such as a compiled program's file system or a `readFileSync`. A synchronous call holds the worker until the kernel answers, so nothing else of the program runs meanwhile, timers and callbacks included; a signal that arrives during a blocked read or write runs its handler, and the call is then restarted. Descriptors are shared with the rest of the context, so a file opened with `sync.open` can be closed with `close`, and the other way around; `sync.run` and `sync.exit` are described next;
|
|
@@ -790,9 +810,11 @@ interface CdpConnection {
|
|
|
790
810
|
onclose: ((reason?: string) => void) | null;
|
|
791
811
|
close(): void;
|
|
792
812
|
}
|
|
793
|
-
type CdpHook = (request: { runtime?: string }) => Promise<CdpConnection>;
|
|
813
|
+
type CdpHook = (request: { runtime?: string; stamp?: ConnectorStamp }) => Promise<CdpConnection>;
|
|
794
814
|
```
|
|
795
815
|
|
|
816
|
+
- **The stamp** is the connecting process's identity, the same [`ConnectorStamp`](#page-facades) a facade's hook gets: `{ pid, ppid, uid, gid, user?, groups, users, tag?, tags? }`. The kernel takes it at `connect()` from the process's credentials and tags, never from anything the program sends or sets (its URL, headers, environment or arguments), so a program cannot forge it. `tag` is the first tag, and `tags` lists them all when there are more than one, so a host that treats a run's processes differently (slicc-bios drops the `Slicc.action` intent of a process tagged `slicc-taint:<id>`) checks every one of `tags ?? [tag]`. Every hook call for a program's connection to `9222` (`/json/list` and the WebSocket alike) has it, a copy of its own, for the embedder's `cdp` hook and a client's `serveCdp` hook. A kernel from before the field gives no `stamp` (to a client's hook: a kernel before protocol 1.17), so a host should treat a request without one as untrusted. As with a facade, a socket handed to another process keeps the stamp of the process that connected.
|
|
817
|
+
|
|
796
818
|
A connection is one browser-level CDP session; a host with only per-tab debugging emulates the `Target` domain behind it. A connection asking for a `runtime` goes to the newest client that offered that name; any other goes to the `cdp` option, or else to the newest client offer. In the browser the hook runs on the page, with a `MessagePort` per connection to the kernel worker.
|
|
797
819
|
|
|
798
820
|
Process workers can also use the transport directly, without a socket: the `net-request`, `net-read` and `net-close` syscalls (`Module.sliccKernel.http` in a process) open a request and read its body in pieces. `net-open` starts a request and answers its handle at once, and `net-head` waits for its response head, so `net-close` can abort a request whose head has not come yet. Requests are per process and closed when it exits, and a request body has no size limit of its own (the transport's `maxRequestBody` applies to the proxy). `net-open` with `stream: true` instead of `body` (only on a transport with `streamingBodies`, else `ENOTSUP`) takes the body in pieces: `net-write { handle, bytes }` returns once the transport has taken the piece (`EPIPE` once the request is closed or failed), and `net-end { handle }` ends it. JS programs' `fetch` uses it for stream bodies. `net-traits` answers the transport's `traits` with `crossOrigin`: `'cors'` when it is bound by CORS like `fetchTransport()`, `'any'` (the default for a transport that does not say) when it is not; without a transport it fails with `ENETUNREACH`.
|
|
@@ -813,12 +835,12 @@ interface FacadeConnection {
|
|
|
813
835
|
close(reason?: string): void;
|
|
814
836
|
}
|
|
815
837
|
|
|
816
|
-
interface ConnectorStamp { pid: number; ppid: number; uid: number; gid: number; user?: string; groups: number[]; users: 'off' | 'report' | 'enforce'; tag?: string }
|
|
838
|
+
interface ConnectorStamp { pid: number; ppid: number; uid: number; gid: number; user?: string; groups: number[]; users: 'off' | 'report' | 'enforce'; tag?: string; tags?: string[] }
|
|
817
839
|
```
|
|
818
840
|
|
|
819
841
|
- **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.
|
|
820
842
|
- **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.
|
|
821
|
-
- **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)).
|
|
843
|
+
- **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, the [CDP facade](#browser-automation-cdp)'s, and for the realm proxy's when the kernel has a fetch hook (see **The fetch hook** under [`/dev/sealed`](#devsealed)).
|
|
822
844
|
- **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.
|
|
823
845
|
- **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`.
|
|
824
846
|
|
|
@@ -1044,19 +1066,21 @@ The kernel's device nodes answer the mode and owner an Emscripten program sees f
|
|
|
1044
1066
|
- A pty's `/dev/pts/N` is its owner's, with `0620`.
|
|
1045
1067
|
- `/dev/pts` is a directory listing the open ptys, for Emscripten and WASIX programs as for JS ones, and `/dev` lists it ([#1040](https://github.com/ai-ecoverse/slicc-kernel/issues/1040)); an Emscripten program also stats a pty through a symlink to it.
|
|
1046
1068
|
- `/dev/stdin`, `/dev/stdout`, `/dev/stderr` and `/dev/fd/N` answer for the descriptor they name.
|
|
1069
|
+
- The embedder's serial nodes, `/dev/ttyUSB<n>` and `/dev/ttyACM<n>`, are character devices with the owner, group, mode and major and minor they were added with, and `/dev/serial/by-id` lists their links (see [Embedder devices](#embedder-devices)).
|
|
1047
1070
|
- A symlink elsewhere that leads to one of them, such as `/tmp/console -> /dev/tty`, answers the same when followed ([#1042](https://github.com/ai-ecoverse/slicc-kernel/issues/1042)).
|
|
1048
1071
|
|
|
1049
1072
|
An Emscripten program sees those six character devices at `0666` with `users: 'off'` too.
|
|
1050
1073
|
|
|
1051
1074
|
The owner calls follow [File owners](#file-owners): with `users: 'off'`, `fd_chown` and `path_chown` succeed and change nothing, and the four owner calls answer `ENOTSUP`, "owners not tracked", before looking at anything, so a libc can remember that once and take its own `getuid()` and `getgid()` as every file's owner, which is what an Emscripten program built against the credential calls sees. Devices are root's. A pipe's owner is the caller.
|
|
1052
1075
|
|
|
1053
|
-
WASIX's own terminal calls carry only echo and line buffering (`tty_set`) and answer for "the" terminal whatever the descriptor (`tty_get`), so a WASIX program's `cfmakeraw` would leave `^C` a signal and its output post-processed. The kernel adds the import module `slicc_tty`, with the full Linux `struct termios` (musl's 60-byte layout: four `u32` flag words, `c_line`, `c_cc[32]`, then input and output speed; speeds read as `B38400`
|
|
1076
|
+
WASIX's own terminal calls carry only echo and line buffering (`tty_set`) and answer for "the" terminal whatever the descriptor (`tty_get`), so a WASIX program's `cfmakeraw` would leave `^C` a signal and its output post-processed. The kernel adds the import module `slicc_tty`, with the full Linux `struct termios` (musl's 60-byte layout: four `u32` flag words, `c_line`, `c_cc[32]`, then input and output speed; speeds read as the speed bits of `c_cflag`, `B38400` on a terminal, which ignores them when set, while a [serial node](#embedder-devices) uses them) and answers per descriptor, again as a preview1 errno (`ENOTTY` for a descriptor that is no terminal, `EBADF` for one that isn't open, `ENOSYS` from kernels without them):
|
|
1054
1077
|
|
|
1055
1078
|
| Import | Does |
|
|
1056
1079
|
| --- | --- |
|
|
1057
1080
|
| `tcgetattr(fd, termios_out)` | writes the terminal's termios |
|
|
1058
|
-
| `tcsetattr(fd, actions, termios)` | sets every flag and `c_cc` as given (unknown bits are kept and read back), at once for `TCSANOW`, `TCSADRAIN` and `TCSAFLUSH` (0 to 2; anything else is `EINVAL`); `TCSAFLUSH` also drops the input typed but not yet read, the pending line too, as an Emscripten program's `TCSETSF` does |
|
|
1081
|
+
| `tcsetattr(fd, actions, termios)` | sets every flag and `c_cc` as given (unknown bits are kept and read back), at once for `TCSANOW`, `TCSADRAIN` and `TCSAFLUSH` (0 to 2; anything else is `EINVAL`); `TCSAFLUSH` also drops the input typed but not yet read, the pending line too, as an Emscripten program's `TCSETSF` does; on a [serial node](#embedder-devices), `TCSADRAIN` and `TCSAFLUSH` drain the output first |
|
|
1059
1082
|
| `winsize(fd, winsize_out)` | writes `struct winsize` (`u16` rows, columns, then zero pixels) |
|
|
1083
|
+
| `ioctl(fd, request, arg)` | a Linux tty ioctl on a [serial node](#embedder-devices), with `arg` the value or the address Linux takes: `TCGETS`, `TCSETS*` (musl's 60-byte termios), `TCGETS2`, `TCSETS2*` (Linux's 44-byte `struct termios2`), `TIOCMGET`, `TIOCMSET`, `TIOCMBIS`, `TIOCMBIC`, `TIOCSBRK`, `TIOCCBRK`, `TCSBRK`, `TCSBRKP`, `TCFLSH`, `FIONREAD`, `TIOCOUTQ`, `TIOCGWINSZ`, `TIOCSWINSZ`, `TIOCEXCL` and `TIOCNXCL`; `TIOCSCTTY` is `EPERM` and any other request `ENOTTY`. On another descriptor it answers `FIONREAD` (the bytes ready to read) and `ENOTTY` for the rest. A wasix-sysroot routes its `ioctl` there for the requests above |
|
|
1060
1084
|
|
|
1061
1085
|
A wasix-sysroot that builds `tcgetattr`, `tcsetattr`, `isatty` and `ioctl(TCGETS, TCSETS*, TIOCGWINSZ)` on them gives WASIX programs (python's `tty.setraw`, curses, ruby's `IO#raw`) a really raw terminal. Stock `tty_get` answers `ENOTTY` when none of the process's descriptors is a terminal, instead of an 80×24 terminal that isn't there, so stdio on pipes and files isn't taken for a terminal.
|
|
1062
1086
|
|
|
@@ -1302,6 +1326,33 @@ printf '{"kind":"open.url","url":"%s"}\n' "$url" | cat > /dev/embedder
|
|
|
1302
1326
|
- **Limits:** at most 16 open requests per process (`EMFILE`) and 256 MiB of request windows per kernel, 128 requests at 2 MiB each (`EAGAIN` at `open()` beyond); a closed request frees its share.
|
|
1303
1327
|
- `/dev/clipboard` stays as it is, text only. Image clipboards are kinds on this channel (`clipboard.read`, `clipboard.write`), answered by the embedder.
|
|
1304
1328
|
|
|
1329
|
+
### Embedder devices
|
|
1330
|
+
|
|
1331
|
+
Serial ports the page can reach (Web Serial, in seven) appear as Linux character devices, `/dev/ttyUSB<n>` and `/dev/ttyACM<n>`, so `cat`, `stty`, `screen`, `picocom` and pyserial use them unchanged. The page adds them with [`kernel.devices.add`](#kerneldevices); a program can't create or remove them (see [`/dev` entries](#dev-entries)).
|
|
1332
|
+
|
|
1333
|
+
- **The node:** a character device with the owner, group and mode it was added with, major 188 for `ttyUSB` and 166 for `ttyACM` and minor `n`, as on Linux, so `ls -l /dev/ttyUSB0` shows `crw-r----- 1 root dialout 188, 0`. Its links are symlinks `/dev/serial/by-id/<name> -> ../../ttyUSB0`. Every process of the kernel sees them, in every runtime (Emscripten, WASI and WASIX, JS), in `stat`, `readdir` and `readlink`, whatever its session.
|
|
1334
|
+
- **Opening:** the normal permission check applies: reading needs `r` and `O_WRONLY` or `O_RDWR` need `w`, so with `root:dialout 0640` a `dialout` member reads and only root writes (an agent writes with `sudo`); a refusal is `EACCES` and goes to `onDenial`. Then the kernel calls the hook that answers the node, `open({ path, flags, stamp, signal })`, with `flags` the access mode (0, 1 or 2) and `stamp` the opener's `{ pid, uid, gid, groups, users, tag?, tags? }`, as for [`/dev/embedder`](#devembedder). It resolves with `{ readable, writable, control(op, args) }`: a byte `ReadableStream`, a byte `WritableStream` and a function that may return a promise. Its error fails the open, mapped as for `/dev/embedder` (`NotAllowedError` `EACCES`, `NotSupportedError` `ENOTSUP`, `NotFoundError` `ENOENT`, anything else `EIO`); a signal while it waits fails the open with `EINTR` and aborts `signal`. A node has one open file description at a time: a second open is `EBUSY` until the first is closed everywhere (dups, forks and inherited descriptors share the first), as Web Serial ports behave and as `TIOCEXCL`, which pyserial sets, asks. Closing the last descriptor waits up to 30 s for the output to drain (Linux's `closing_wait`), then cancels both streams and releases their locks, and only then calls `control('close')`, so the hook can close a Web Serial port, whose `close()` rejects while its streams are locked.
|
|
1335
|
+
- **Read and write:** the kernel reads `readable` into its own buffer, at most 64 KiB ahead of the program, and reads block, return what is there, honour `O_NONBLOCK` (`EAGAIN`) and wake `poll`, `select`, epoll and `FIONREAD` (`TIOCINQ`), as for a terminal. A write waits while `writable` applies backpressure (`EAGAIN` with `O_NONBLOCK`) and returns once its bytes are queued there. The end of `readable` is a hangup: reads return 0, writes fail with `EIO` and `poll` reports `POLLHUP`; an error of either stream is `EIO`. A signal interrupts a blocked call with `EINTR`, as for pipes.
|
|
1336
|
+
- **Terminal settings:** the node is a terminal (`isatty` is true, `ttyname` names it) with its own termios, which starts as Linux's for a USB serial port made raw (`cfmakeraw`): `B9600`, `CS8`, no parity, one stop bit, no flow control (`CREAD`, `HUPCL` and `CLOCAL` set), `VMIN` 1. The settings stay with the node across opens, so `stty -F /dev/ttyUSB0 115200` holds for the next program, as on Linux, and an open whose saved settings differ from that start configures the port before it returns. `TCGETS`, `TCSETS`, `TCSETSW` (drains output first) and `TCSETSF` (also discards input), and `TCGETS2`/`TCSETS2`/`TCSETSW2`/`TCSETSF2` with `BOTHER` and `c_ispeed`/`c_ospeed` for a custom speed, take every flag and `c_cc` as given and keep them. A change to the speed (`B50` to `B4000000`, from `cfsetispeed`/`cfsetospeed` or `BOTHER`), `CSIZE`, `PARENB`/`PARODD`, `CSTOPB` or `CRTSCTS` calls `control('configure', { baudRate, dataBits, parity, stopBits, flowControl })` once and waits for it (`parity` `'none'`, `'even'` or `'odd'`, `stopBits` 1 or 2, `flowControl` `'none'` or `'hardware'`); its error fails the call and leaves the old settings. `B0` drops DTR and RTS instead, as on Linux, and the line stays hung up: an open whose saved speed is `B0` calls `control('setSignals', { dataTerminalReady: false, requestToSend: false })` rather than `configure`, and the first setting with a real speed configures the port and raises DTR and RTS again. The rest of the line discipline is the kernel's pseudo-terminal one: with `ICANON`, `ECHO`, `ICRNL` or `OPOST`/`ONLCR` set, the node edits lines, echoes to the port and translates as a pty does. A node is never a controlling terminal: `O_NOCTTY` is implied, `TIOCSCTTY` is `EPERM`, `TIOCGPGRP` and `TIOCSPGRP` are `ENOTTY`, and a `VINTR` byte with `ISIG` set signals nobody. `TIOCGWINSZ` answers 0 × 0 until a `TIOCSWINSZ`.
|
|
1337
|
+
- **Owner, mode and times:** an Emscripten program's `chmod`, `chown` and `utimensat` of a node (by its name or through its link), and `fchmod`, `fchown` and `futimens` on an open one, change the node itself under the rules for any file: the mode is the owner's or root's to change, the owner only root's, the group the owner's to change to one of its own groups, and the times the owner's or root's (setting them to now also any user who may write it); anyone else gets `EPERM` (`EACCES` for now without write permission), reported to `onDenial`. The change shows in `stat` in every runtime and lasts until the node is removed. With `users: 'off'` nothing is checked.
|
|
1338
|
+
- **Modem lines:** `TIOCMGET` asks `control('signals')`, which resolves with `{ clearToSend, dataCarrierDetect, dataSetReady, ringIndicator }` (`TIOCM_CTS`, `TIOCM_CD`, `TIOCM_DSR`, `TIOCM_RI`), and adds the DTR and RTS the kernel last set (both on at the first open, as Linux raises them). `TIOCMSET`, `TIOCMBIS` and `TIOCMBIC` call `control('setSignals', { dataTerminalReady, requestToSend })`.
|
|
1339
|
+
- **Break, flush and drain:** `TIOCSBRK` and `TIOCCBRK` call `control('setSignals', { break: true })` and `{ break: false }`; `TCSBRK` with 0 (`tcsendbreak`) drains, then sends a 250 ms break, and `TCSBRKP` one of `arg` × 100 ms. `TCSBRK` with any other value (`tcdrain`) waits until `writable` has taken everything written. `TCFLSH` (`tcflush`) with `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` discards the kernel's own input buffer for the first and third and calls `control('flush', { input, output })`. `TIOCOUTQ` answers the bytes `writable` hasn't taken yet.
|
|
1340
|
+
- **Other ioctls:** `TIOCEXCL` and `TIOCNXCL` are accepted (the node is exclusive anyway); `FIONBIO`, `FIONCLEX`, `FIOCLEX` and `FIOASYNC` go to the runtime's own handling, as for any file. Any other ioctl is `ENOTTY`, as Linux answers for an unknown tty ioctl.
|
|
1341
|
+
- **Runtimes:** an Emscripten program makes these calls with `ioctl`, `tcgetattr`/`tcsetattr`, `tcdrain`, `tcflush` and `tcsendbreak` as on Linux. A WASIX program has them through `slicc_tty` (see [WASI programs](#wasi-programs)): `tcgetattr` and `tcsetattr` (`TCSADRAIN` and `TCSAFLUSH` drain first) and `ioctl(fd, request, arg)` for the rest. A JS program's `ctx.open` of a node (and `ctx.sync.open`) returns a handle with the controls as methods:
|
|
1342
|
+
|
|
1343
|
+
| method | does |
|
|
1344
|
+
| --- | --- |
|
|
1345
|
+
| `termios()` | the node's settings, `{ c_iflag, c_oflag, c_cflag, c_lflag, c_cc, ispeed, ospeed }` (Linux values, speeds in baud) |
|
|
1346
|
+
| `setTermios(termios, when?)` | sets them, speeds from `ispeed` and `ospeed` (any value, as `BOTHER` would), `when` `'now'` (the default), `'drain'` or `'flush'`, as `TCSETS2`, `TCSETSW2` and `TCSETSF2` |
|
|
1347
|
+
| `modem()` | `{ dtr, rts, cts, dsr, cd, ri }`, as `TIOCMGET` |
|
|
1348
|
+
| `setModem({ dtr?, rts? })` | sets the lines given, as `TIOCMBIS` and `TIOCMBIC` |
|
|
1349
|
+
| `setBreak(on)`, `sendBreak()` | `TIOCSBRK`/`TIOCCBRK`, and a 250 ms break after draining |
|
|
1350
|
+
| `drain()` | waits until the port has taken everything written |
|
|
1351
|
+
| `flush(queue?)` | discards `'input'`, `'output'` or `'both'` (the default), as `tcflush` |
|
|
1352
|
+
|
|
1353
|
+
`ctx.sync.open` has the same methods, synchronous. A closed handle's methods fail with `EBADF`.
|
|
1354
|
+
- **Not there yet:** `/sys/class/tty`, through which pyserial's `list_ports` and Node `serialport`'s list find ports, and `TIOCGSERIAL`.
|
|
1355
|
+
|
|
1305
1356
|
### Protected paths
|
|
1306
1357
|
|
|
1307
1358
|
`createKernel({ protect: { paths: ['/node_modules', '/os'] } })` makes those directories read-only for every process, root included, in every `users` mode. They hold code every later process runs, which only the embedder writes, straight to OPFS.
|
|
@@ -1337,7 +1388,7 @@ printf '{"kind":"open.url","url":"%s"}\n' "$url" | cat > /dev/embedder
|
|
|
1337
1388
|
|
|
1338
1389
|
### `/dev` entries
|
|
1339
1390
|
|
|
1340
|
-
`/dev` and `/proc` hold the kernel's own entries, and a program can't add or remove names there. Only `/dev/shm`, a tmpfs, takes new files.
|
|
1391
|
+
`/dev` and `/proc` hold the kernel's own entries, and a program can't add or remove names there. Only `/dev/shm`, a tmpfs, takes new files, and only the embedder adds names: its serial nodes and their `/dev/serial/by-id` links (see [Embedder devices](#embedder-devices)), which a program can't remove either.
|
|
1341
1392
|
- **Removing** an entry fails and leaves it: `EACCES` for a user, `EPERM` for root on a device, `EBUSY` for `/dev` itself.
|
|
1342
1393
|
- **Creating** a name that doesn't exist fails in every runtime, whether it's a file, a directory, a symlink, a device node, or the new name of a link or a rename: `EACCES` for a user, `EPERM` for root in `/dev`, and `EROFS` in `/proc`.
|
|
1343
1394
|
- **Why:** an Emscripten program's `/dev` and `/proc` live in its glue's own memory, so such a file used to look written and then vanish when the process exited. On the root file system no other runtime would see it either.
|