@ai-ecoverse/slicc-kernel 1.152.2 → 1.154.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.
Files changed (36) hide show
  1. package/README.md +28 -3
  2. package/dist/driver-worker.js +147 -10
  3. package/dist/driver-worker.js.map +4 -4
  4. package/dist/index.js +168 -14
  5. package/dist/index.js.map +4 -4
  6. package/dist/kernel-worker.js +844 -167
  7. package/dist/kernel-worker.js.map +4 -4
  8. package/dist/node-driver-worker.js +147 -10
  9. package/dist/node-driver-worker.js.map +4 -4
  10. package/dist/node-process-worker.js +81 -24
  11. package/dist/node-process-worker.js.map +3 -3
  12. package/dist/node.js +982 -182
  13. package/dist/node.js.map +4 -4
  14. package/dist/process-worker.js +81 -24
  15. package/dist/process-worker.js.map +3 -3
  16. package/dist/types/client/attach.d.ts +2 -0
  17. package/dist/types/client/protocol.d.ts +4 -0
  18. package/dist/types/client/serve-client.d.ts +2 -0
  19. package/dist/types/embedder-relay.d.ts +58 -0
  20. package/dist/types/index.d.ts +4 -0
  21. package/dist/types/kernel/embedder.d.ts +100 -0
  22. package/dist/types/kernel/fd-table.d.ts +3 -1
  23. package/dist/types/kernel/host.d.ts +8 -0
  24. package/dist/types/kernel/net/tls-client.d.ts +4 -1
  25. package/dist/types/kernel/net/tls-engine.d.ts +5 -2
  26. package/dist/types/kernel/net/tls-server.d.ts +2 -0
  27. package/dist/types/kernel/net/tls-uplink.d.ts +2 -0
  28. package/dist/types/kernel/process.d.ts +17 -0
  29. package/dist/types/kernel/socket-syscalls.d.ts +3 -0
  30. package/dist/types/launcher.d.ts +13 -0
  31. package/dist/types/node.d.ts +3 -0
  32. package/dist/types/process/js/js-net.d.ts +3 -0
  33. package/dist/types/process/kernel-streams.d.ts +2 -1
  34. package/dist/types/process/wasi/wasi-fds.d.ts +1 -0
  35. package/dist/types/serve.d.ts +3 -0
  36. package/package.json +4 -4
package/README.md CHANGED
@@ -43,6 +43,7 @@ TypeScript declarations ship in `dist/types/`, and each entry (`.`, `./node`, `.
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
44
  | `sealed` | none | `(req, { signal }) => Promise<object>`: answers requests written to [`/dev/sealed`](#devsealed). `createNodeKernel` takes it too |
45
45
  | `clipboard` | none | `{ read(stamp) => Promise<string>, write(text, stamp) => Promise<void> }`: the clipboard behind [`/dev/clipboard`](#devclipboard). `createNodeKernel` takes it too |
46
+ | `requests` | none | `{ kinds, handle(request) }`: the page's answers to [`/dev/embedder`](#devembedder) for the processes it starts. `createNodeKernel` takes it too |
46
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 |
47
48
  | `cdp` | none | `({ runtime }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
48
49
  | `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)) |
@@ -86,10 +87,12 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
86
87
 
87
88
  Stops the kernel worker and every process. Pending and later calls reject.
88
89
 
89
- ### `kernel.connect({ user, tag }?) → Promise<MessagePort>`
90
+ ### `kernel.connect({ user, tag, requests }?) → Promise<MessagePort>`
90
91
 
91
92
  Makes a port for another client of the same kernel. Hand it to a dedicated worker, or through a SharedWorker to another tab: it can be transferred any number of times. All clients share one process table, so a terminal on the page can `ps` and `kill` what a worker started.
92
93
 
94
+ **`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`.
95
+
93
96
  **`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. 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)).
94
97
 
95
98
  ### `kernel.users`
@@ -166,7 +169,7 @@ Page-only: neither is on an attached client (`attachKernel`, `connect()`), so no
166
169
 
167
170
  ### `attachKernel(port, options?) → Promise<KernelClient>`
168
171
 
169
- 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`.
172
+ 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' })`.
170
173
 
171
174
  | Client | |
172
175
  |---|---|
@@ -451,7 +454,7 @@ The module's default export (or its `main` export) is called with a context and
451
454
  - `sudo(args)`, what the [`sudo`](#sudo) command calls: `args` are its arguments after `sudo`, and it resolves with `{ pid }`, the command the kernel started as the program's child, to `wait` for; `{ run }`, an argv to run as the caller (with `users: 'off'`); `{ list }`, the text of `sudo -l`; or `{ error, errno }`, the message to print;
452
455
  - `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`;
453
456
  - `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), and `close()`. 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`;
454
- - `net.connect({ host, port, tls: { servername, alpn, rejectUnauthorized, ca, path, cert, key, passphrase, sni, checkHostname } })` 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.
457
+ - `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.
455
458
  - **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. That includes sealed placeholders in headers, which reach the transport exactly as from `ctx.fetch`.
456
459
  - **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.
457
460
  - **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`.
@@ -499,6 +502,7 @@ The module's default export (or its `main` export) is called with a context and
499
502
  - `pfx` is `ENOTSUP`: pass `cert` and `key`.
500
503
  - A server that requires a certificate and gets none refuses with an alert. On TLS 1.2 that fails the connect (`ERR_SSL_HANDSHAKE_FAILURE`). On TLS 1.3 the client's side of the handshake is already done, so the next read or write fails with `ERR_SSL_FATAL_ALERT`, as Node's fails after `secureConnect`.
501
504
  - **A fatal alert from the server**, at the handshake or after it, is in the error: its `detail` ends in ` (alert <n>)`, and a JS program's error has `alert: n`, the alert's description number (116 certificate_required, 40 handshake_failure).
505
+ - **`keylog: true`** ([#1504](https://github.com/ai-ecoverse/slicc-kernel/issues/1504), tls-engine 4.2.0-10) gives the session's NSS key log lines as `conn.tls.keylog`, a frozen list of strings: `CLIENT_RANDOM …` on TLS 1.2, the four `…_TRAFFIC_SECRET…` lines on TLS 1.3, the lines Node's `keylog` event and `SSLKEYLOGFILE` carry. **They are the session's secrets: anyone holding them and a capture of the traffic can read it.** So a keylogged session is never sealed (`substitutes: false`; a placeholder goes out as written), and `keylog` on the transport path (A1, where the page makes the TLS) is `ENOTSUP`. Without `keylog` the engine collects nothing. The kernel wipes its copies once the lines are in the reply (the engine zeroes its buffer as it hands them out); the lines reach the program as JS strings, which can't be wiped, so they live as long as the program keeps them, as with its own `key`. A TLS server takes `keylog` too (`listen({ tls })`, `secure(conn, { server: true })`), for every connection it serves.
502
506
  - **`sni: false`** sends no server name. The leaf is still checked against `servername` exactly as with SNI.
503
507
  - **`checkHostname: false`** checks the chain (trust, validity, usage) but **not that the leaf names `servername`**. The connection reports `hostnameChecked: false`, and `hostnameMatches` says what the skipped check found.
504
508
  - **The caller must then check the server's identity itself**, as slicc-jsh does with Node's `checkServerIdentity` and `peerCertificates[0]`; pass it only when the program supplied one.
@@ -1126,6 +1130,27 @@ A character device (`0666`), there in every `users` mode, through which programs
1126
1130
  - **Cost:** other files' reads and closes check one property more, and their writes nothing; an Emscripten close checks one flag.
1127
1131
  - **On the page,** `createKernel` relays the calls from the kernel worker and back, keeping the error's `name` and `message`.
1128
1132
 
1133
+ ### `/dev/embedder`
1134
+
1135
+ A character device (`0666`, every `users` mode) through which a program asks its embedder's page to do what only a page can: open a URL, capture the screen, play or record audio ([#1527](https://github.com/ai-ecoverse/slicc-kernel/issues/1527)). One open file description is one request, and the reply streams back; plain `sh` with coreutils is enough:
1136
+
1137
+ ```sh
1138
+ exec 3<>/dev/embedder
1139
+ { printf '%s\n' '{"kind":"audio.transcribe","language":"en"}'; cat speech.wav; } >&3
1140
+ cat <&3 > text.txt
1141
+ printf '{"kind":"open.url","url":"%s"}\n' "$url" | cat > /dev/embedder
1142
+ ```
1143
+
1144
+ - **The request:** the first line is a JSON object with a `kind` (`a.b-c`: lower-case words with dots, at most 64 bytes) and options (every other key), at most 64 KiB. Everything written after it is the body, streamed to the page as it is written: a write waits while 1 MiB of it is unread by the page. A bad header fails that `write()` with `EINVAL`, and the description stays failed. The request goes to the page when its header line ends.
1145
+ - **The reply:** reads return the page's reply in order, pulled as the program reads, and its end is end-of-file. Opened `O_RDWR`, the first `read()` ends the body. Opened `O_WRONLY` (nobody reads), the last close ends the body and waits for the page to finish, so `close()` returns its error (an Emscripten program's standard output follows `/dev/clipboard`'s exit rule, so `printf … | cat > /dev/embedder` fails with `cat: write error: …`). There is no timeout: a reply may wait for a prompt or a model download; run `timeout` for one.
1146
+ - **Cancel:** a signal during a blocked read, write or `O_WRONLY` close fails it with `EINTR`, and the page's `signal` aborts with reason `interrupted`. Closing the description before the end of the reply aborts it with reason `closed`, and that is no error: it is how `hear` stops recording. A process that ends during a request aborts it too.
1147
+ - **Errors:** the hook's error gives the errno: `NotAllowedError` or `AbortError` (a dismissed prompt, a refused permission) `EACCES`; `NotSupportedError` `ENOTSUP`; `NotFoundError` `ENOENT`; anything else `EIO`. An Emscripten or WASI program first gets `<argv0>: /dev/embedder: <first line of the message>` on standard error, as for `/dev/clipboard`; a JS program has it in the error. `request.progress(line)` from the page writes `<argv0>: <line>` to standard error the same way, at most 4 lines a second.
1148
+ - **Who answers:** the hook of the client the process's session was started through, inherited by its children, forks and `exec`s: `createKernel({ requests })` (and `createNodeKernel`) for the page's own `run` and `openTerminal`, the hook a port was made with (`connect({ requests })`), or the hook its attached client brought (`connect({ requests: 'attach' })`, then `attachKernel(port, { requests })`). Nothing falls back to another client's hook, so a request from another tab's terminal never opens in this one. With no hook, or a kind the hook doesn't list, the request is `ENOTSUP` before the page sees it. A program has no way to register, replace or see a hook.
1149
+ - **The allow-list and policy:** `kinds` maps each kind the hook answers to its policy: `{}`, `{ group: 'video' }`, `{ users: ['alice', 1001] }`. With `users: 'report'` or `'enforce'` the kernel lets a request through only from root, the session's user (the user its port was connected as, or the user `run` or `openTerminal` ran as), a member of the kind's `group`, or a listed user: a process that `sudo -u`'d to someone else gets `EACCES`, and the page never sees it. The handler can still prompt or refuse by the stamp.
1150
+ - **The hook:** `handle({ kind, options, body, stamp, signal, progress })` gets the body as a `ReadableStream<Uint8Array>` and resolves with the reply: a `ReadableStream<Uint8Array>`, bytes, a string, or nothing (an empty reply). `stamp` is `{ pid, uid, gid, groups, users, kind, tag?, tags? }`: the process that sent the header line, with its effective ids and groups.
1151
+ - **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.
1152
+ - `/dev/clipboard` stays as it is, text only. Image clipboards are kinds on this channel (`clipboard.read`, `clipboard.write`), answered by the embedder.
1153
+
1129
1154
  ### Protected paths
1130
1155
 
1131
1156
  `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.
@@ -43,6 +43,111 @@ function serveCdpPort(port, hook, request, onDone) {
43
43
  };
44
44
  }
45
45
 
46
+ // src/embedder-relay.ts
47
+ function fresh(bytes) {
48
+ return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ? bytes : bytes.slice();
49
+ }
50
+ function errorOf(err) {
51
+ const name = err?.name;
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ return { name: typeof name === "string" ? name : "Error", message };
54
+ }
55
+ function bytesOf(reply) {
56
+ if (reply instanceof ReadableStream) return reply;
57
+ const bytes = typeof reply === "string" ? new TextEncoder().encode(reply) : reply instanceof Uint8Array ? reply : new Uint8Array(0);
58
+ return new ReadableStream({
59
+ start(controller) {
60
+ if (bytes.length > 0) controller.enqueue(bytes);
61
+ controller.close();
62
+ }
63
+ });
64
+ }
65
+ function bodyStream(id, state, post) {
66
+ return new ReadableStream(
67
+ {
68
+ pull: async (controller) => {
69
+ if (state.body.length === 0) post({ id, bodyPull: true });
70
+ while (state.body.length === 0 && !state.abort.signal.aborted) {
71
+ await new Promise((resolve) => {
72
+ state.wakeBody = resolve;
73
+ });
74
+ }
75
+ const next = state.body.shift();
76
+ if (next) controller.enqueue(next);
77
+ else controller.close();
78
+ }
79
+ },
80
+ { highWaterMark: 0 }
81
+ );
82
+ }
83
+ function embedderAnswerer(port, hookFor) {
84
+ const running = /* @__PURE__ */ new Map();
85
+ const post = (message, transfer = []) => port.postMessage({ embedderReply: message }, transfer);
86
+ const fail = (id, err) => {
87
+ running.delete(id);
88
+ post({ id, error: errorOf(err) });
89
+ };
90
+ const start = async (ask) => {
91
+ const state = { abort: new AbortController(), body: [], wakeBody: void 0 };
92
+ running.set(ask.id, state);
93
+ try {
94
+ const hook = hookFor(ask.client);
95
+ if (!hook || !Object.hasOwn(hook.kinds, ask.kind))
96
+ throw Object.assign(new Error(`this page does not answer ${ask.kind}`), {
97
+ name: "NotSupportedError"
98
+ });
99
+ const reply = await hook.handle({
100
+ kind: ask.kind,
101
+ options: ask.options,
102
+ body: bodyStream(ask.id, state, post),
103
+ stamp: ask.stamp,
104
+ signal: state.abort.signal,
105
+ progress: (line) => post({ id: ask.id, progress: String(line) })
106
+ });
107
+ state.reader = bytesOf(reply).getReader();
108
+ post({ id: ask.id, ready: true });
109
+ } catch (err) {
110
+ fail(ask.id, err);
111
+ }
112
+ };
113
+ const pull = async (id, state) => {
114
+ try {
115
+ const got = await state.reader.read();
116
+ if (got.done) {
117
+ running.delete(id);
118
+ post({ id, done: true });
119
+ return;
120
+ }
121
+ const bytes = fresh(got.value);
122
+ post({ id, chunk: bytes }, [bytes.buffer]);
123
+ } catch (err) {
124
+ fail(id, err);
125
+ }
126
+ };
127
+ const step = (message) => {
128
+ const state = running.get(message.id);
129
+ if (!state) return;
130
+ if ("body" in message) state.body.push(message.body);
131
+ else if ("bodyEnd" in message) state.body.push(null);
132
+ else if ("pull" in message) void pull(message.id, state);
133
+ else {
134
+ running.delete(message.id);
135
+ state.abort.abort(new DOMException(message.cancel, "AbortError"));
136
+ void state.reader?.cancel(message.cancel).catch(() => void 0);
137
+ }
138
+ state.wakeBody?.();
139
+ };
140
+ return {
141
+ handle(data) {
142
+ const message = data?.embedder;
143
+ if (!message) return false;
144
+ if ("ask" in message) void start(message.ask);
145
+ else step(message);
146
+ return true;
147
+ }
148
+ };
149
+ }
150
+
46
151
  // src/facade/port.ts
47
152
  function serveFacadePort(port, hook, { name, stamp }, onDone) {
48
153
  let conn;
@@ -93,6 +198,27 @@ function serveFacadePort(port, hook, { name, stamp }, onDone) {
93
198
  };
94
199
  }
95
200
 
201
+ // src/kernel/pipe.ts
202
+ var PIPE_CAPACITY = 64 * 1024;
203
+
204
+ // src/kernel/embedder.ts
205
+ var EMBEDDER_HEADER_MAX = 64 * 1024;
206
+ var EMBEDDER_WINDOW = 1024 * 1024;
207
+ var EMBEDDER_BUDGET = 256 * 1024 * 1024;
208
+ function kindsOf(hook) {
209
+ if (typeof hook?.handle !== "function" || typeof hook.kinds !== "object" || !hook.kinds)
210
+ throw new TypeError("requests needs kinds and handle");
211
+ return Object.fromEntries(
212
+ Object.entries(hook.kinds).map(([kind, policy]) => [
213
+ kind,
214
+ {
215
+ ...policy?.group !== void 0 ? { group: String(policy.group) } : {},
216
+ ...policy?.users ? { users: [...policy.users] } : {}
217
+ }
218
+ ])
219
+ );
220
+ }
221
+
96
222
  // src/kernel/net/ws-queue.ts
97
223
  var INBOUND_LIMIT = 16 * 1024 * 1024;
98
224
  var MESSAGE_LIMIT = 64 * 1024 * 1024;
@@ -421,7 +547,7 @@ var KERNEL_SIGINFO = Object.freeze({
421
547
  });
422
548
 
423
549
  // src/bytes.ts
424
- function fresh(bytes, start = 0, end = bytes.length) {
550
+ function fresh2(bytes, start = 0, end = bytes.length) {
425
551
  return new Uint8Array(bytes.subarray(start, end));
426
552
  }
427
553
 
@@ -478,7 +604,7 @@ function dialStream(port, onClose) {
478
604
  const writable = new WritableStream({
479
605
  write(chunk) {
480
606
  if (closed) throw errorWithCode("EPIPE: the connection is closed");
481
- const copy = fresh(chunk);
607
+ const copy = fresh2(chunk);
482
608
  const acked = new Promise((resolve, reject) => acks.push({ resolve, reject }));
483
609
  port.postMessage(copy, [copy.buffer]);
484
610
  return acked;
@@ -665,7 +791,7 @@ async function loopbackFetch(dial, input, options) {
665
791
  }
666
792
 
667
793
  // src/client/protocol.ts
668
- var PROTOCOL = [1, 12];
794
+ var PROTOCOL = [1, 13];
669
795
  function locksOf(scope = globalThis) {
670
796
  const locks = scope.navigator?.locks;
671
797
  return typeof locks?.request === "function" ? locks : void 0;
@@ -693,7 +819,7 @@ var KernelCallError = class extends Error {
693
819
  }
694
820
  };
695
821
  var encoder2 = new TextEncoder();
696
- var bytesOf = (data) => typeof data === "string" ? encoder2.encode(data) : data;
822
+ var bytesOf2 = (data) => typeof data === "string" ? encoder2.encode(data) : data;
697
823
  var UNAVAILABLE = {
698
824
  manualRedirects: false,
699
825
  encodedBodies: false,
@@ -777,7 +903,9 @@ var Channel = class {
777
903
  call(call) {
778
904
  return this.request(call).done;
779
905
  }
906
+ requests;
780
907
  receive(data) {
908
+ if (this.requests?.handle(data)) return;
781
909
  if (data.traits && data.id === void 0) {
782
910
  if (this.remote) this.remote.traits = data.traits;
783
911
  for (const listener of [...this.traitsListeners]) listener(data.traits);
@@ -900,7 +1028,7 @@ async function spawnOn(channel, argv, opts) {
900
1028
  });
901
1029
  const started = Promise.withResolvers();
902
1030
  let endedBy;
903
- const options = { ...rest, ...stdin !== void 0 ? { stdin: bytesOf(stdin) } : {} };
1031
+ const options = { ...rest, ...stdin !== void 0 ? { stdin: bytesOf2(stdin) } : {} };
904
1032
  const { done } = channel.request(
905
1033
  { op: "spawn", argv, options },
906
1034
  {
@@ -983,7 +1111,7 @@ async function terminalOn(channel, argv, opts) {
983
1111
  listener = next;
984
1112
  if (next) for (const bytes of backlog.splice(0)) next(bytes);
985
1113
  },
986
- write: (data) => send({ action: "write", bytes: bytesOf(data) }),
1114
+ write: (data) => send({ action: "write", bytes: bytesOf2(data) }),
987
1115
  resize: (cols, rows) => send({ action: "resize", cols, rows }),
988
1116
  signal: (name) => send({ action: "signal", signal: name }),
989
1117
  close: () => send({ action: "close" })
@@ -1045,8 +1173,10 @@ function since(channel, minor, what, run) {
1045
1173
  }
1046
1174
  return run();
1047
1175
  }
1048
- async function greet(channel, lock, timeoutMs) {
1049
- channel.post({ hello: { protocol: PROTOCOL, ...lock ? { lock } : {} } });
1176
+ async function greet(channel, lock, timeoutMs, requests) {
1177
+ channel.post({
1178
+ hello: { protocol: PROTOCOL, ...lock ? { lock } : {}, ...requests ? { requests } : {} }
1179
+ });
1050
1180
  const timer = setTimeout(
1051
1181
  () => channel.end(
1052
1182
  new KernelGoneError(`no slicc-kernel answered on this port within ${timeoutMs} ms`)
@@ -1071,7 +1201,14 @@ async function attachKernel(port, options = {}) {
1071
1201
  const locks = options.locks === null ? void 0 : options.locks ?? locksOf();
1072
1202
  const own = await holdLock(locks);
1073
1203
  const channel = new Channel(port, own.release);
1074
- const hello = await greet(channel, own.name, options.timeoutMs ?? 1e4);
1204
+ const requests = options.requests;
1205
+ if (requests) channel.requests = embedderAnswerer(port, () => requests);
1206
+ const hello = await greet(
1207
+ channel,
1208
+ own.name,
1209
+ options.timeoutMs ?? 1e4,
1210
+ requests ? kindsOf(requests) : void 0
1211
+ );
1075
1212
  channel.protocol = hello.protocol;
1076
1213
  if (hello.lock && locks) void locks.request(hello.lock, () => channel.end(new KernelGoneError()));
1077
1214
  const transport = new RemoteTransport(
@@ -1234,7 +1371,7 @@ function serveFilesystem(port, handlers, capabilities = {}, options = {}) {
1234
1371
  (result) => {
1235
1372
  if (result instanceof Uint8Array) {
1236
1373
  const whole = result.byteOffset === 0 && result.byteLength === result.buffer.byteLength;
1237
- const bytes = options.owned && whole ? result : fresh(result);
1374
+ const bytes = options.owned && whole ? result : fresh2(result);
1238
1375
  reply({ id: data.id, result: bytes }, [bytes.buffer]);
1239
1376
  } else reply({ id: data.id, result: result ?? null });
1240
1377
  },