@ai-ecoverse/slicc-kernel 1.12.0 → 1.14.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 CHANGED
@@ -119,7 +119,7 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
119
119
  { "slicc": { "abi": "wasi", "commands": { "rg": { "wasm": "bin/rg.wasm" }, "rustc": { "script": "bin/rustc" } } } }
120
120
  ```
121
121
 
122
- Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. The catalog is re-read at the start of every `run`, so packages installed in between are picked up.
122
+ Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. Commands also come from pnpm's global directory. Every process gets `PNPM_HOME=/home/.local/share/pnpm`, unless the embedder sets its own, and has `$PNPM_HOME/bin` on `PATH`. The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
123
123
 
124
124
  ### WASI programs
125
125
 
@@ -179,6 +179,27 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
179
179
 
180
180
  Directories are renamed with `FileSystemHandle.move()` where available, else by copy and delete.
181
181
 
182
+ ### Mounts
183
+
184
+ `kernel.mount({ type, source, target, options })` mounts a file system on an existing directory. Every process sees it, Emscripten and WASI alike, and so does every attached client. `kernel.umount(target)` unmounts it, and fails with `EBUSY` while a process has a file open under it. `kernel.mounts()` lists the table, as does `/proc/mounts`. The Node entry and attached clients have the same three calls (client protocol 1.2).
185
+
186
+ - **Built in:** `tmpfs`, which lives in memory until unmounted.
187
+ - **Package drivers:** a package declares a type in `"slicc": { "filesystems": { "<type>": { "module": "<file>" } } }`, and the kernel starts that module in a worker of its own for each mount. The module is a single self-contained file: it can't import bare specifiers.
188
+ - Its default export receives `{ fetch }`, the kernel's network transport. It returns `{ handlers, capabilities }`.
189
+ - The handlers are path-based: `getattr`, `readdir`, `open`/`read`/`write`/`release` on handles, `mkdir`, `rmdir`, `unlink`, `rename`, and optionally `symlink`, `readlink`, `setattr` and `statfs`.
190
+ - An optional `mount({ source, options })` handler validates the source.
191
+ - Errors carry a POSIX `code`.
192
+ - `@ai-ecoverse/slicc-kernel/driver` exports the types and `fsError`.
193
+ - **Capabilities:** `readonly`, `symlinks`, `chmod`, `maxIo` (the largest read or write per message), `maxFile`, `listingStats`, and `attrTtl`/`entryTtl` (in ms, default 1000).
194
+ - **Caching:** the kernel caches attributes and listings for those TTLs, and a driver can push `invalidate` for changes made outside. File contents are read whole when a program opens a file and written back when it closes it, which gives close-to-open consistency as NFS has.
195
+ - **Rules on a mount:**
196
+ - a rename across mounts is `EXDEV`, so `mv` copies;
197
+ - `df` reports each mount from its driver's `statfs`;
198
+ - each mount has its own device number;
199
+ - without `chmod` support, `chmod` succeeds and changes nothing;
200
+ - a file over `maxFile` is `EFBIG`, and `options.maxfile` (`"2G"`, or `"0"` for none) changes that limit for one mount;
201
+ - a driver that crashes or doesn't answer within 30 s makes its mount's calls fail with `EIO`, and the mount is listed as `failed`.
202
+
182
203
  ### `/proc`
183
204
 
184
205
  Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self/` has the same files for the process that reads them. They come from the kernel's process table when they are opened, so a terminal sees the processes of every client; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so is each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times, load and `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) are placeholders. Only Emscripten processes see this `/proc`.
@@ -0,0 +1,574 @@
1
+ // src/kernel/net/remote-transport.ts
2
+ var TransportError = class extends Error {
3
+ status;
4
+ code;
5
+ constructor(message, status, code) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ }
10
+ };
11
+ var RemoteTransport = class {
12
+ traits;
13
+ port;
14
+ waiting = /* @__PURE__ */ new Map();
15
+ nextId = 0;
16
+ failure;
17
+ constructor(port, traits) {
18
+ this.port = port;
19
+ this.traits = traits;
20
+ }
21
+ receive(reply) {
22
+ const waiter = this.waiting.get(reply.nid);
23
+ if (!waiter) return;
24
+ this.waiting.delete(reply.nid);
25
+ if (reply.net === "error") {
26
+ waiter.reject(new TransportError(reply.message, reply.status, reply.code));
27
+ } else waiter.resolve(reply);
28
+ }
29
+ fail(error) {
30
+ this.failure = error;
31
+ for (const waiter of this.waiting.values()) waiter.reject(error);
32
+ this.waiting.clear();
33
+ }
34
+ ask(call) {
35
+ if (this.failure) return Promise.reject(this.failure);
36
+ return new Promise((resolve, reject) => {
37
+ this.waiting.set(call.nid, { resolve, reject });
38
+ this.port.postMessage(call);
39
+ });
40
+ }
41
+ cancel(nid) {
42
+ const waiter = this.waiting.get(nid);
43
+ this.waiting.delete(nid);
44
+ waiter?.reject(new TransportError("the request was cancelled"));
45
+ this.port.postMessage({ net: "cancel", nid });
46
+ }
47
+ async fetch(request) {
48
+ const nid = ++this.nextId;
49
+ const { signal } = request;
50
+ signal.throwIfAborted();
51
+ let finished = false;
52
+ const abort = () => {
53
+ finished = true;
54
+ this.cancel(nid);
55
+ };
56
+ signal.addEventListener("abort", abort, { once: true });
57
+ const finish = () => {
58
+ if (finished) return;
59
+ finished = true;
60
+ signal.removeEventListener("abort", abort);
61
+ };
62
+ let head;
63
+ try {
64
+ head = await this.ask({
65
+ net: "fetch",
66
+ nid,
67
+ url: request.url,
68
+ method: request.method,
69
+ headers: request.headers,
70
+ ...request.body ? { body: request.body } : {}
71
+ });
72
+ } catch (e) {
73
+ finish();
74
+ throw e;
75
+ }
76
+ const { status, statusText, headers } = head;
77
+ const read = async () => {
78
+ if (finished) return void 0;
79
+ const reply = await this.ask({ net: "read", nid }).catch((e) => {
80
+ finish();
81
+ throw e;
82
+ });
83
+ if (reply.net === "chunk") return reply.bytes;
84
+ finish();
85
+ return void 0;
86
+ };
87
+ const cancel = async () => {
88
+ if (finished) return;
89
+ finish();
90
+ this.cancel(nid);
91
+ };
92
+ return {
93
+ status,
94
+ statusText,
95
+ headers,
96
+ cancel,
97
+ body: {
98
+ [Symbol.asyncIterator]: () => ({
99
+ next: async () => {
100
+ const bytes = await read();
101
+ return bytes ? { value: bytes, done: false } : { value: void 0, done: true };
102
+ },
103
+ return: async () => {
104
+ await cancel();
105
+ return { value: void 0, done: true };
106
+ }
107
+ })
108
+ }
109
+ };
110
+ }
111
+ };
112
+
113
+ // src/client/protocol.ts
114
+ var PROTOCOL = [1, 2];
115
+ function locksOf(scope = globalThis) {
116
+ const locks = scope.navigator?.locks;
117
+ return typeof locks?.request === "function" ? locks : void 0;
118
+ }
119
+ function versionError(theirs) {
120
+ const major = Array.isArray(theirs) ? theirs[0] : void 0;
121
+ if (major === PROTOCOL[0]) return void 0;
122
+ const named = typeof major === "number" ? `${major}.x` : "without a version";
123
+ return `slicc-kernel client protocol ${named} is not supported: this side speaks ${PROTOCOL.join(".")}`;
124
+ }
125
+
126
+ // src/client/attach.ts
127
+ var KernelGoneError = class extends Error {
128
+ constructor(message = "the slicc-kernel this client was attached to is gone") {
129
+ super(message);
130
+ this.name = "KernelGoneError";
131
+ }
132
+ };
133
+ var KernelCallError = class extends Error {
134
+ code;
135
+ constructor(message, code) {
136
+ super(message);
137
+ this.name = "KernelCallError";
138
+ this.code = code;
139
+ }
140
+ };
141
+ var encoder = new TextEncoder();
142
+ var bytesOf = (data) => typeof data === "string" ? encoder.encode(data) : data;
143
+ var UNAVAILABLE = {
144
+ manualRedirects: false,
145
+ encodedBodies: false,
146
+ maxRequestBody: 0,
147
+ unavailable: true
148
+ };
149
+ async function holdLock(locks) {
150
+ if (!locks) return { release() {
151
+ } };
152
+ const name = `slicc-kernel-client:${crypto.randomUUID()}`;
153
+ const granted = Promise.withResolvers();
154
+ const held = Promise.withResolvers();
155
+ void locks.request(name, () => {
156
+ granted.resolve();
157
+ return held.promise;
158
+ });
159
+ await granted.promise;
160
+ return { name, release: () => held.resolve() };
161
+ }
162
+ function concat(chunks) {
163
+ const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
164
+ let at = 0;
165
+ for (const chunk of chunks) {
166
+ out.set(chunk, at);
167
+ at += chunk.length;
168
+ }
169
+ return out;
170
+ }
171
+ var Channel = class {
172
+ greeted = Promise.withResolvers();
173
+ ended = Promise.withResolvers();
174
+ remote;
175
+ failure;
176
+ pending = /* @__PURE__ */ new Map();
177
+ watching = /* @__PURE__ */ new Map();
178
+ protocol = PROTOCOL;
179
+ nextId = 0;
180
+ port;
181
+ release;
182
+ constructor(port, release) {
183
+ this.port = port;
184
+ this.release = release;
185
+ this.greeted.promise.catch(() => void 0);
186
+ port.addEventListener("message", (event) => this.receive(event.data));
187
+ port.addEventListener("close", () => this.end(new KernelGoneError()));
188
+ port.start?.();
189
+ }
190
+ post(message) {
191
+ if (!this.failure) this.port.postMessage(message);
192
+ }
193
+ end(error) {
194
+ if (this.failure) return;
195
+ this.failure = error;
196
+ this.greeted.reject(error);
197
+ this.remote?.fail(error);
198
+ for (const call of this.pending.values()) call.reject(error);
199
+ this.pending.clear();
200
+ this.release();
201
+ this.port.close?.();
202
+ this.ended.resolve(error);
203
+ }
204
+ request(call, handlers = {}) {
205
+ const id = ++this.nextId;
206
+ const { failure } = this;
207
+ if (failure) return { id, done: Promise.reject(failure) };
208
+ const done = new Promise((resolve, reject) => {
209
+ this.pending.set(id, { resolve, reject, ...handlers });
210
+ this.port.postMessage({ ...call, id });
211
+ });
212
+ return { id, done };
213
+ }
214
+ call(call) {
215
+ return this.request(call).done;
216
+ }
217
+ receive(data) {
218
+ if (data.watch !== void 0) {
219
+ this.watching.get(data.watch)?.(data.change);
220
+ return;
221
+ }
222
+ if (data.bye !== void 0) {
223
+ this.end(new KernelGoneError(data.bye));
224
+ return;
225
+ }
226
+ if (data.hello) {
227
+ this.greeted.resolve(data.hello);
228
+ return;
229
+ }
230
+ if (data.net !== void 0) {
231
+ this.remote?.receive(data);
232
+ return;
233
+ }
234
+ const reply = data;
235
+ const call = this.pending.get(reply.id);
236
+ if (!call) return;
237
+ if (reply.fd !== void 0) {
238
+ call.output?.(reply.fd, reply.bytes);
239
+ return;
240
+ }
241
+ if (reply.started !== void 0) {
242
+ call.started?.(reply.started);
243
+ return;
244
+ }
245
+ this.pending.delete(reply.id);
246
+ if (reply.error !== void 0) call.reject(new KernelCallError(reply.error, reply.code));
247
+ else call.resolve(reply.result);
248
+ }
249
+ };
250
+ async function spawnOn(channel, argv, opts) {
251
+ const { onStdout, onStderr, stdin, group: _, ...rest } = opts;
252
+ const started = Promise.withResolvers();
253
+ const options = { ...rest, ...stdin !== void 0 ? { stdin: bytesOf(stdin) } : {} };
254
+ const { done } = channel.request(
255
+ { op: "spawn", argv, options },
256
+ {
257
+ output: (fd, bytes) => (fd === 1 ? onStdout : onStderr)?.(bytes),
258
+ started: started.resolve
259
+ }
260
+ );
261
+ done.catch(started.reject);
262
+ const pid = await started.promise;
263
+ return {
264
+ pid,
265
+ pgid: pid,
266
+ exited: done,
267
+ signal: async (name = "SIGTERM") => void await channel.call({ op: "kill", pid: -pid, signal: name })
268
+ };
269
+ }
270
+ async function runOn(channel, argv, opts) {
271
+ const { onStdout, onStderr, ...rest } = opts;
272
+ const chunks = { 1: [], 2: [] };
273
+ const live = { 1: new TextDecoder(), 2: new TextDecoder() };
274
+ const tee = (fd, on) => (bytes) => {
275
+ chunks[fd].push(bytes);
276
+ on?.(live[fd].decode(bytes, { stream: true }));
277
+ };
278
+ const child = await spawnOn(channel, argv, {
279
+ ...rest,
280
+ onStdout: tee(1, onStdout),
281
+ onStderr: tee(2, onStderr)
282
+ });
283
+ const status = await child.exited;
284
+ const text = (fd) => new TextDecoder().decode(concat(chunks[fd]));
285
+ return { pid: child.pid, status, stdout: text(1), stderr: text(2) };
286
+ }
287
+ async function terminalOn(channel, argv, opts) {
288
+ const { onData, ...options } = opts;
289
+ let listener = onData ?? null;
290
+ const backlog = [];
291
+ const started = Promise.withResolvers();
292
+ const { id, done } = channel.request(
293
+ { op: "open-terminal", argv, options },
294
+ {
295
+ output: (_fd, bytes) => listener ? listener(bytes) : backlog.push(bytes),
296
+ started: started.resolve
297
+ }
298
+ );
299
+ done.catch(started.reject);
300
+ const pid = await started.promise;
301
+ const send = (action) => void channel.call({ op: "terminal", terminal: id, ...action }).catch(() => void 0);
302
+ return {
303
+ pid,
304
+ exited: done,
305
+ get onData() {
306
+ return listener;
307
+ },
308
+ set onData(next) {
309
+ listener = next;
310
+ if (next) for (const bytes of backlog.splice(0)) next(bytes);
311
+ },
312
+ write: (data) => send({ action: "write", bytes: bytesOf(data) }),
313
+ resize: (cols, rows) => send({ action: "resize", cols, rows }),
314
+ signal: (name) => send({ action: "signal", signal: name }),
315
+ close: () => send({ action: "close" })
316
+ };
317
+ }
318
+ function fsOn(channel) {
319
+ const fs = (method, ...args) => channel.call({ op: "fs", method, args });
320
+ const done = async (method, ...args) => void await fs(method, ...args);
321
+ return {
322
+ readFile: (path) => fs("readFile", path),
323
+ readText: async (path) => new TextDecoder().decode(await fs("readFile", path)),
324
+ writeFile: (path, data) => done("writeFile", path, data),
325
+ stat: (path) => fs("stat", path),
326
+ lstat: (path) => fs("lstat", path),
327
+ readdir: (path) => fs("readdir", path),
328
+ mkdir: (path) => done("mkdir", path),
329
+ rm: (path, o = {}) => done("rm", path, o.force === true),
330
+ rename: (from, to) => done("rename", from, to),
331
+ realpath: (path) => fs("realpath", path),
332
+ symlink: (target, path) => done("symlink", target, path),
333
+ readlink: (path) => fs("readlink", path),
334
+ exists: (path) => fs("exists", path),
335
+ async watch(paths, options, onChange) {
336
+ if (channel.protocol[1] < 1) {
337
+ throw new KernelCallError(
338
+ `this kernel speaks protocol ${channel.protocol.join(".")}, which has no watch`,
339
+ "ENOSYS"
340
+ );
341
+ }
342
+ const { id, done: done2 } = channel.request({
343
+ op: "watch",
344
+ paths,
345
+ recursive: options.recursive === true
346
+ });
347
+ channel.watching.set(id, onChange);
348
+ try {
349
+ await done2;
350
+ } catch (error) {
351
+ channel.watching.delete(id);
352
+ throw error;
353
+ }
354
+ return {
355
+ close: () => {
356
+ if (!channel.watching.delete(id)) return;
357
+ void channel.call({ op: "unwatch", watch: id }).catch(() => void 0);
358
+ }
359
+ };
360
+ }
361
+ };
362
+ }
363
+ function since(channel, minor, what, run) {
364
+ if (channel.protocol[1] < minor) {
365
+ return Promise.reject(
366
+ new KernelCallError(
367
+ `this kernel speaks protocol ${channel.protocol.join(".")}, which has no ${what}`,
368
+ "ENOSYS"
369
+ )
370
+ );
371
+ }
372
+ return run();
373
+ }
374
+ async function greet(channel, lock, timeoutMs) {
375
+ channel.post({ hello: { protocol: PROTOCOL, ...lock ? { lock } : {} } });
376
+ const timer = setTimeout(
377
+ () => channel.end(
378
+ new KernelGoneError(`no slicc-kernel answered on this port within ${timeoutMs} ms`)
379
+ ),
380
+ timeoutMs
381
+ );
382
+ let hello;
383
+ try {
384
+ hello = await channel.greeted.promise;
385
+ } finally {
386
+ clearTimeout(timer);
387
+ }
388
+ const refused = hello.error ?? versionError(hello.protocol);
389
+ if (refused) {
390
+ const error = new Error(refused);
391
+ channel.end(error);
392
+ throw error;
393
+ }
394
+ return hello;
395
+ }
396
+ async function attachKernel(port, options = {}) {
397
+ const locks = options.locks === null ? void 0 : options.locks ?? locksOf();
398
+ const own = await holdLock(locks);
399
+ const channel = new Channel(port, own.release);
400
+ const hello = await greet(channel, own.name, options.timeoutMs ?? 1e4);
401
+ channel.protocol = hello.protocol;
402
+ if (hello.lock && locks) void locks.request(hello.lock, () => channel.end(new KernelGoneError()));
403
+ const transport = new RemoteTransport(
404
+ { postMessage: (call) => channel.post(call) },
405
+ hello.traits ?? UNAVAILABLE
406
+ );
407
+ channel.remote = transport;
408
+ return {
409
+ protocol: hello.protocol,
410
+ transport,
411
+ closed: channel.ended.promise,
412
+ spawn: (argv, opts = {}) => spawnOn(channel, argv, opts),
413
+ run: (argv, opts = {}) => runOn(channel, argv, opts),
414
+ openTerminal: (argv, opts = {}) => terminalOn(channel, argv, opts),
415
+ ps: () => channel.call({ op: "ps" }),
416
+ mount: (spec) => since(channel, 2, "mount", () => channel.call({ op: "mount", spec })),
417
+ umount: async (target) => void await since(channel, 2, "umount", () => channel.call({ op: "umount", target })),
418
+ mounts: () => since(channel, 2, "mounts", () => channel.call({ op: "mounts" })),
419
+ kill: async (pid, signal = "SIGTERM") => void await channel.call({ op: "kill", pid, signal }),
420
+ fetch: (req) => {
421
+ const { signal, ...rest } = req;
422
+ return transport.fetch({ ...rest, signal: signal ?? new AbortController().signal });
423
+ },
424
+ fs: fsOn(channel),
425
+ async close(o = {}) {
426
+ if (channel.failure) return;
427
+ await channel.call({ op: "detach", ...o.kill ? { kill: true } : {} }).catch(() => void 0);
428
+ channel.end(new KernelGoneError("this client is closed"));
429
+ }
430
+ };
431
+ }
432
+
433
+ // src/mount/protocol.ts
434
+ var DRIVER_PROTOCOL = [1, 0];
435
+ function driverVersionError(theirs) {
436
+ const major = Array.isArray(theirs) ? theirs[0] : void 0;
437
+ if (major === DRIVER_PROTOCOL[0]) return void 0;
438
+ const named = typeof major === "number" ? `${major}.x` : "without a version";
439
+ return `slicc-kernel driver protocol ${named} is not supported: this side speaks ${DRIVER_PROTOCOL.join(".")}`;
440
+ }
441
+
442
+ // src/mount/driver.ts
443
+ function fsError(code, message = code) {
444
+ return Object.assign(new Error(message), { code });
445
+ }
446
+ async function dispatch(h, req) {
447
+ switch (req.op) {
448
+ case "getattr":
449
+ return h.getattr(req.path);
450
+ case "readdir":
451
+ return h.readdir(req.path);
452
+ case "open":
453
+ return h.open(req.path, {
454
+ write: req.write,
455
+ create: req.create,
456
+ truncate: req.truncate,
457
+ exclusive: req.exclusive
458
+ });
459
+ case "read":
460
+ return h.read(req.fh, req.offset, req.size);
461
+ case "write":
462
+ return h.write(req.fh, req.offset, req.bytes);
463
+ case "release":
464
+ return h.release(req.fh);
465
+ case "mkdir":
466
+ return h.mkdir(req.path);
467
+ case "rmdir":
468
+ return h.rmdir(req.path);
469
+ case "unlink":
470
+ return h.unlink(req.path);
471
+ case "rename":
472
+ return h.rename(req.from, req.to);
473
+ case "symlink":
474
+ if (!h.symlink) throw fsError("EPERM", "this file system has no symlinks");
475
+ return h.symlink(req.target, req.path);
476
+ case "readlink":
477
+ if (!h.readlink) throw fsError("EINVAL", `${req.path} is not a symlink`);
478
+ return h.readlink(req.path);
479
+ case "setattr":
480
+ return h.setattr?.(req.path, {
481
+ ...req.mode !== void 0 ? { mode: req.mode } : {},
482
+ ...req.mtime !== void 0 ? { mtime: req.mtime } : {}
483
+ });
484
+ case "statfs":
485
+ return h.statfs?.() ?? null;
486
+ default:
487
+ throw fsError("ENOSYS", `unknown driver op ${req.op}`);
488
+ }
489
+ }
490
+ function serveFilesystem(port, handlers, capabilities = {}) {
491
+ let open = false;
492
+ const reply = (message, transfer = []) => port.postMessage(message, transfer);
493
+ async function hello(message) {
494
+ const refused = driverVersionError(message.protocol);
495
+ if (refused) {
496
+ reply({ hello: { protocol: DRIVER_PROTOCOL, error: refused, errno: "EPROTO" } });
497
+ return;
498
+ }
499
+ try {
500
+ const refined = await handlers.mount?.(message.mount);
501
+ open = true;
502
+ reply({
503
+ hello: { protocol: DRIVER_PROTOCOL, capabilities: { ...capabilities, ...refined } }
504
+ });
505
+ } catch (err) {
506
+ const code = err?.code;
507
+ reply({
508
+ hello: {
509
+ protocol: DRIVER_PROTOCOL,
510
+ error: err instanceof Error ? err.message : String(err),
511
+ errno: typeof code === "string" ? code : "EIO"
512
+ }
513
+ });
514
+ }
515
+ }
516
+ port.addEventListener("message", (event) => {
517
+ const data = event.data;
518
+ if (data.hello) {
519
+ void hello(data.hello);
520
+ return;
521
+ }
522
+ if (!open) return;
523
+ dispatch(handlers, data).then(
524
+ (result) => {
525
+ if (result instanceof Uint8Array) {
526
+ const bytes = result.slice();
527
+ reply({ id: data.id, result: bytes }, [bytes.buffer]);
528
+ } else reply({ id: data.id, result: result ?? null });
529
+ },
530
+ (err) => {
531
+ const code = err?.code;
532
+ reply({
533
+ id: data.id,
534
+ errno: typeof code === "string" ? code : "EIO",
535
+ message: err instanceof Error ? err.message : String(err)
536
+ });
537
+ }
538
+ );
539
+ });
540
+ port.start?.();
541
+ return { invalidate: (paths) => reply({ invalidate: paths }) };
542
+ }
543
+
544
+ // src/mount/host.ts
545
+ async function hostDriver(start, load) {
546
+ try {
547
+ const factory = (await load(start.code)).default;
548
+ if (typeof factory !== "function") {
549
+ throw new Error("a slicc file system module must export a default function");
550
+ }
551
+ const client = await attachKernel(start.client, { locks: null });
552
+ const made = await factory({ fetch: (request) => client.fetch(request) });
553
+ serveFilesystem(start.driver, made.handlers, made.capabilities ?? {});
554
+ } catch (err) {
555
+ start.driver.postMessage({
556
+ hello: {
557
+ protocol: DRIVER_PROTOCOL,
558
+ error: `the driver did not start: ${err instanceof Error ? err.message : String(err)}`,
559
+ errno: "EIO"
560
+ }
561
+ });
562
+ }
563
+ }
564
+
565
+ // src/driver-worker.ts
566
+ addEventListener(
567
+ "message",
568
+ (event) => void hostDriver(
569
+ event.data,
570
+ (code) => import(URL.createObjectURL(new Blob([code], { type: "text/javascript" })))
571
+ ),
572
+ { once: true }
573
+ );
574
+ //# sourceMappingURL=driver-worker.js.map