@ai-ecoverse/slicc-kernel 1.74.0 → 1.75.1

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
@@ -41,6 +41,7 @@ const { status, stdout, stderr } = await kernel.run(['bash', '-c', 'echo hi > he
41
41
  | `users` | `'off'` | `'off'`, `'report'` or `'enforce'`: whether files have owners and their modes are checked (see [File owners](#file-owners) and [Permissions](#permissions)). `'off'` behaves exactly as kernels before owners did; any other value rejects. `createNodeKernel` takes it too |
42
42
  | `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 |
43
43
  | `sealed` | none | `(req, { signal }) => Promise<object>`: answers requests written to [`/dev/sealed`](#devsealed). `createNodeKernel` takes it too |
44
+ | `clipboard` | none | `{ read(stamp) => Promise<string>, write(text, stamp) => Promise<void> }`: the clipboard behind [`/dev/clipboard`](#devclipboard). `createNodeKernel` takes it too |
44
45
  | `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 |
45
46
  | `cdp` | none | `({ runtime }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
46
47
  | `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 |
@@ -619,6 +620,19 @@ A character device (`0666`, major 10, minor 250) through which a program talks t
619
620
  - **Reading:** the reply comes from the same descriptor, as JSON (`{ v: 1, … }`). A read waits for it, drains it, then returns end-of-file; with no request outstanding it returns end-of-file at once. A signal ends the wait with `EINTR` and the request stays outstanding, `O_NONBLOCK` gives `EAGAIN`, and `poll`/`select` report the descriptor readable once the reply is there. A new write drops an unread reply.
620
621
  - **The hook:** `signal` aborts with `'timeout'` after 10 s (the reply is then `{ v: 1, error: { code: 'ETIMEDOUT' } }`), `'replaced'` when a new write on the descriptor comes first, and `'withdrawn'` when the writer exits or the descriptor is closed. A throw or a reply that isn't an object becomes an `EIO` error reply, with the first line of the message; no hook is `ENOTCONN`. On the page, `createKernel` relays the request from the kernel worker and back, and the abort too.
621
622
 
623
+ ### `/dev/clipboard`
624
+
625
+ A character device (`0666`, major 10, minor 251), there in every `users` mode, through which programs read and write the embedder's clipboard (`createKernel({ clipboard: { read, write } })`; on the page, bios answers with `navigator.clipboard`). Emscripten programs (`cat /dev/clipboard`, `cat > /dev/clipboard`, see it in `/dev`), WASI programs (`std::fs`) and JS programs (`ctx.fs.readFile`, `ctx.fs.writeFile`, `ctx.open`) all open it.
626
+ - **State per open file description:** `dup`'d and fork-inherited descriptors share one snapshot and one buffer, as on Linux; each `open()` is new. `lseek` fails with `ESPIPE`, and `poll`/`select` report it readable and writable at once.
627
+ - **Reading:** the first `read()` asks `read(stamp)` and takes a snapshot of the text as UTF-8; later reads continue it, and its end is end-of-file. A failed read keeps no snapshot, so the next read asks again. A signal ends the wait with `EINTR`.
628
+ - **Writing:** the bytes are buffered, and the last close of the description calls `write(text, stamp)` with all of them as one string, and waits for it, so `close()` returns its error. A description opened `O_WRONLY` always commits (with nothing written, `write('')` clears the clipboard, so `: > /dev/clipboard` works); one opened `O_RDWR` commits only once it has been written to, and one opened `O_RDONLY` never. More than 1 MiB in all fails that `write()` with `EFBIG`, and the description stays failed: later writes fail too and its close commits nothing. Bytes that aren't valid UTF-8 fail the close with `EILSEQ`, and the hook isn't called. With `O_RDWR`, reads come from the snapshot and writes go to the buffer, independently.
629
+ - **Errors:** a hook that throws a `NotAllowedError` (no focus, or permission denied) gives `EACCES`; anything else, a hook that doesn't answer within 10 s ("clipboard timed out") included, gives `EIO`; no `clipboard` hook gives `ENOTSUP` on read and on a committing close (writes still buffer). The first line of the hook's message reaches JS programs in the error's message (`fd-close: … : EACCES`); Emscripten and WASI programs get the errno.
630
+ - **Standard output at exit:** an Emscripten program's libc never closes its standard output, so the kernel does what coreutils' `close_stdout` would: when it closes standard error at exit (or, failing that, when the runtime shuts down) with `/dev/clipboard` still on standard output, it commits it, and on an error prints `<program>: write error: <strerror>` to standard error and makes a 0 exit status 1. So `printf x | cat > /dev/clipboard` prints `cat: write error: Permission denied` and fails when the hook refuses. Only `/dev/clipboard` descriptors report their close errors this way; every other descriptor's close is as before.
631
+ - **Exit without close:** a process that ends with a description still open commits at its teardown, stamped with that process; the error is then lost, as nobody is left to get it.
632
+ - **The stamp:** `{ pid, uid, gid, users, tag? }`: the pid as the program sees it, the effective ids, the kernel's `users` mode, and the process's `tag` (see `kernel.run`; a process inherits it from its parent, and a client's processes get the client's `connect({ tag })`), in every `users` mode. A read is stamped with the process that reads first; a commit with the process that closes last (or ends, at teardown).
633
+ - **Cost:** other files' reads and closes check one property more, and their writes nothing; an Emscripten close checks one flag.
634
+ - **On the page,** `createKernel` relays the calls from the kernel worker and back, keeping the error's `name` and `message`.
635
+
622
636
  ### Protected paths
623
637
 
624
638
  `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.
@@ -650,7 +664,7 @@ The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#us
650
664
  OPFS stores names, bytes, sizes and modification times, nothing else. Everything POSIX needs on top of that lives in an IndexedDB sidecar, so it survives reloads and new kernels:
651
665
 
652
666
  - mode bits (`chmod`);
653
- - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
667
+ - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again: any write or truncate moves it to the time of the write, also in Chrome, whose OPFS doesn't move a file's `lastModified` for writes through a sync access handle, so the kernel records the write's time itself, and it reaches the metadata store up to 100 ms after the write, so a tab closed inside that window keeps the old mtime on disk; the time follows a rename and is dropped with the file); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
654
668
  - inode numbers that stay stable across renames;
655
669
  - symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar, with their own times: `touch -h`, `lutimes` and `utimensat` with `AT_SYMLINK_NOFOLLOW` set the link's, never the target's. A link's own mode can't be changed, as on Linux: `chmod -h`, `lchmod` and `fchmodat` with `AT_SYMLINK_NOFOLLOW` fail with `EOPNOTSUPP` (which `chmod -h` and `tar` ignore for links) and never follow it, and `open` with `O_PATH | O_NOFOLLOW` gives a descriptor for the link itself.
656
670
 
package/dist/index.js CHANGED
@@ -287,6 +287,45 @@ async function loopbackFetch(dial, input, options) {
287
287
  }
288
288
  }
289
289
 
290
+ // src/kernel/pipe.ts
291
+ var PIPE_CAPACITY = 64 * 1024;
292
+
293
+ // src/kernel/clipboard.ts
294
+ var CLIPBOARD_MAX_BYTES = 1024 * 1024;
295
+ var encoder2 = new TextEncoder();
296
+ var decoder = new TextDecoder("utf-8", { fatal: true });
297
+
298
+ // src/clipboard-relay.ts
299
+ function clipboardAnswerer(port, hook) {
300
+ const ask = async ({ id, op, text, stamp }) => {
301
+ try {
302
+ if (!hook) throw new Error("no clipboard in this embedder");
303
+ if (op === "read") {
304
+ const got = await hook.read(stamp);
305
+ if (typeof got !== "string") throw new Error("the clipboard gave no text");
306
+ port.postMessage({ clipboardReply: { id, text: got } });
307
+ } else {
308
+ await hook.write(text ?? "", stamp);
309
+ port.postMessage({ clipboardReply: { id } });
310
+ }
311
+ } catch (err) {
312
+ const name = err?.name;
313
+ const message = err instanceof Error ? err.message : String(err);
314
+ port.postMessage({
315
+ clipboardReply: { id, error: { name: typeof name === "string" ? name : "Error", message } }
316
+ });
317
+ }
318
+ };
319
+ return {
320
+ handle(data) {
321
+ const message = data;
322
+ if (!message?.clipboardAsk) return false;
323
+ void ask(message.clipboardAsk);
324
+ return true;
325
+ }
326
+ };
327
+ }
328
+
290
329
  // src/fs/protect.ts
291
330
  function protectInit(protection) {
292
331
  const writers = [...writerTags(protection)];
@@ -1176,8 +1215,8 @@ var KernelCallError = class extends Error {
1176
1215
  this.code = code;
1177
1216
  }
1178
1217
  };
1179
- var encoder2 = new TextEncoder();
1180
- var bytesOf = (data) => typeof data === "string" ? encoder2.encode(data) : data;
1218
+ var encoder3 = new TextEncoder();
1219
+ var bytesOf = (data) => typeof data === "string" ? encoder3.encode(data) : data;
1181
1220
  var UNAVAILABLE = {
1182
1221
  manualRedirects: false,
1183
1222
  encodedBodies: false,
@@ -1811,8 +1850,8 @@ function serveHostfsPort(port, transport) {
1811
1850
  // src/index.ts
1812
1851
  var ISOLATION = "slicc-kernel needs a cross-origin isolated page (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp or credentialless)";
1813
1852
  function streamer(callback) {
1814
- const decoder = new TextDecoder();
1815
- return (bytes) => callback?.(decoder.decode(bytes, { stream: true }));
1853
+ const decoder2 = new TextDecoder();
1854
+ return (bytes) => callback?.(decoder2.decode(bytes, { stream: true }));
1816
1855
  }
1817
1856
  function dialer(call) {
1818
1857
  const open = /* @__PURE__ */ new Set();
@@ -1854,6 +1893,7 @@ function initCall(options, transport) {
1854
1893
  ...options.users !== void 0 ? { users: options.users } : {},
1855
1894
  ...options.onDenial ? { denials: true } : {},
1856
1895
  ...options.sealed ? { sealed: true } : {},
1896
+ ...options.clipboard ? { clipboard: true } : {},
1857
1897
  ...options.approve ? { approve: true } : {},
1858
1898
  ...options.protect ? { protect: protectInit(options.protect) } : {},
1859
1899
  ...options.processMounts !== void 0 ? {
@@ -1970,6 +2010,14 @@ function checkOptions(options) {
1970
2010
  throw new Error(`not a host name: ${String(options.hostname)}`);
1971
2011
  }
1972
2012
  }
2013
+ function hookRelays(worker, options) {
2014
+ const sealed = sealedAnswerer(worker, options.sealed);
2015
+ const clipboard = clipboardAnswerer(worker, options.clipboard);
2016
+ return {
2017
+ handle: (data) => sealed.handle(data) || clipboard.handle(data),
2018
+ close: () => sealed.close()
2019
+ };
2020
+ }
1973
2021
  async function createKernel(options = {}) {
1974
2022
  checkOptions(options);
1975
2023
  writerTags(options.protect);
@@ -1984,7 +2032,7 @@ async function createKernel(options = {}) {
1984
2032
  (req) => call(req)
1985
2033
  );
1986
2034
  const cdp = cdpBridge(options.cdp);
1987
- const sealed = sealedAnswerer(worker, options.sealed);
2035
+ const sealed = hookRelays(worker, options);
1988
2036
  const approvals = approver(options.approve, worker);
1989
2037
  const fail = (error) => {
1990
2038
  dials.reset();
@@ -2107,11 +2155,11 @@ async function createKernel(options = {}) {
2107
2155
  { op: "run", argv, options: { ...rest, ...input ? { stdin: input } : {} } },
2108
2156
  { output: (fd, bytes) => streams[fd](bytes) }
2109
2157
  );
2110
- const decoder = new TextDecoder();
2158
+ const decoder2 = new TextDecoder();
2111
2159
  return {
2112
2160
  status: result.status,
2113
- stdout: decoder.decode(result.stdout),
2114
- stderr: decoder.decode(result.stderr)
2161
+ stdout: decoder2.decode(result.stdout),
2162
+ stderr: decoder2.decode(result.stderr)
2115
2163
  };
2116
2164
  },
2117
2165
  openTerminal,