@ai-ecoverse/slicc-kernel 1.48.1 → 1.49.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
@@ -66,7 +66,7 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
66
66
  | --- | --- |
67
67
  | `pid` | the session leader's pid |
68
68
  | `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
69
- | `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group. A key typed while a child of that group is still setting up is held until the child has finished, then sent to the group in the foreground at that point. Setting up means the child's calls before its first other one: `setpgid`, `tcsetpgrp`, signal masks, and starting the program it execs (which then sets itself up too). The hold ends 2 s after the child's first call at the latest. A key typed while the foreground group is busy, not reading the terminal (a shell between reading a command line and starting it), goes to that group and also to the job the group starts next, once that job has taken the terminal; the second copy is dropped when the group reads the terminal again. So `^C` or `^Z` right after a command line reaches the new job, as it does on Linux, where starting a job takes microseconds rather than a worker start. For the same reason, a child that is still setting up cannot give the terminal to its own group once its parent has already given the terminal to that group and taken it back: on Linux the child's `tcsetpgrp` would have landed first, so the parent's later choice stands. Otherwise `tcsetpgrp` behaves as before. |
69
+ | `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group. A key typed while a child of that group is still setting up is held until the child has finished, then sent to the group in the foreground at that point. Setting up means the child's calls before its first other one: `setpgid`, `tcsetpgrp`, signal masks, and starting the program it execs (which then sets itself up too). The hold ends 2 s after the child's first call at the latest. A key typed while the foreground group is busy, not reading the terminal (a shell between reading a command line and starting it), goes to that group and also to the job the group starts next, once that job has taken the terminal; the second copy is dropped when the group reads the terminal again. So `^C` or `^Z` right after a command line reaches the new job, as it does on Linux, where starting a job takes microseconds rather than a worker start. For the same reason, a child that is still setting up cannot give the terminal to its own group once its parent has already given the terminal to that group and taken it back: on Linux the child's `tcsetpgrp` would have landed first, so the parent's later choice stands. Otherwise `tcsetpgrp` behaves as before. A `^C`, `^\` or `^Z` that a program with a handler for it takes between two system calls, rather than while waiting, ends the program's next wait in the kernel that would block (reading nothing yet, `select` or `poll` with a timeout, a blocking write, `waitpid`) with `EINTR`, once, as if the key had come during that wait. A zero-timeout `select` or `poll` does not use it up. Without this, an interactive shell whose `^C` landed while it was taking in the key before it would wait for the next key before showing a new prompt: readline acts on `^C` only when a read ends with `EINTR`. On Linux that window is the microseconds between two reads; here it is a few kernel calls per key. A signal sent with `kill` does not do this. |
70
70
  | `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
71
71
  | `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
72
72
  | `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
@@ -127,7 +127,7 @@ When the kernel goes away (the page closed or reloaded, `terminate()`), pending
127
127
 
128
128
  ### Headless in Node, for tests
129
129
 
130
- `@ai-ecoverse/slicc-kernel/node` runs the same kernel in Node without a browser, so packages built for SLICC can test against it. It is a testing entry, not a supported runtime: there is no OPFS and no isolation, and nothing beyond what its tests use is promised.
130
+ `@ai-ecoverse/slicc-kernel/node` runs the same kernel in Node without a browser, so packages built for SLICC can test against it. It is a testing entry, not a supported runtime: there is no OPFS, JS programs get the same guard as in the page but no sandbox (see [JS programs](#js-programs)), and nothing beyond what its tests use is promised.
131
131
 
132
132
  ```js
133
133
  import { createNodeKernel, nodeTransport } from '@ai-ecoverse/slicc-kernel/node';
@@ -187,8 +187,9 @@ On top of preview1 a WASI program gets:
187
187
 
188
188
  - **Threads**: `wasi.thread-spawn` (wasm32-wasip1-threads) and WASIX `thread_spawn_v2` start a worker per thread on the process's shared memory, sized from the module's own import (a 2 GiB maximum when the engine cannot reserve more). The threads share one descriptor table; `exit` or a trap in any thread ends the process. At most 64 threads per process; `SLICC_WASM_THREADS` sets another cap, up to 256.
189
189
  - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`.
190
+ - **Foreign calls** (libffi's WASIX backend, so Python's `ctypes`): `call_dynamic` calls a function through the program's indirect function table (a static module's exported `__indirect_function_table`, or the dynamic linker's), its arguments and results packed in memory one after another (`i32`/`f32` 4 bytes, `i64`/`f64` 8). With `strict` set, the buffers must fit the function exactly (else `EINVAL`); without it, missing arguments are zero and results that do not fit are left out. A trap or exception of the callee goes through to the caller. JS cannot ask a function for its type, so the kernel reads the types from the module files (the program's and each side module's, once, when a program first calls through `call_dynamic`): a table slot that is empty, past the end, holds a function of no module the kernel knows of, or takes or returns a `v128`, gives `EINVAL`. For closures (callbacks such as `CFUNCTYPE`), a dynamically linked program gets table slots from `closure_allocate`, handed out by the kernel so that the program's threads, each with its own copy of the table, never pick the same one, and `closure_free` gives them back for reuse; a static program gets `ENOTSUP`. `closure_prepare`, which puts a function into such a slot, is not there yet and answers `ENOSYS`. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
190
191
  - **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names.
191
- - **Pipes** show to WASI and WASIX programs as stream sockets (`fstat` gives `S_IFSOCK`), because WASI has no FIFO file type and an unknown type made programs refuse them (Python's asyncio subprocess pipes). They are still read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. In a WASIX shell, `[ -p ]` is therefore false for a pipe and `[ -S ]` true. Emscripten programs, bash included, still see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output: WASI and WASIX programs see it as a stream socket too.
192
+ - **Pipes** have no file type for WASI and WASIX programs' `fstat` (`st_mode` type 0), because WASI has no FIFO file type. A socket type, as kernels 1.47.3 to 1.48.1 gave them, made Ruby treat a `popen` pipe as a socket (`close_write` then never ends the child's input). The kernel's `slicc_fs.fd_mode` answers `S_IFIFO | 0600` (Linux numbering) for a pipe, and wasix-sysroot 2025.9.30-21 and later take the type from it, so programs built on it see a FIFO (`S_ISFIFO`); older libcs keep only the permission bits. Until then Python's asyncio refuses a pipe for a subprocess (`Pipe transport is only for pipes, sockets and character devices`). They are read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. Emscripten programs, bash included, see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output.
192
193
  - **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream pair, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names.
193
194
  - **Diagnostics**: a trap ends the program with 134 and its message on stderr; with `SLICC_WASM_BACKTRACE=1` the wasm frames follow, named from the module's name section, or from a sidecar for a module shipped without one (`<module>.names` beside it, or the same path in a `<package>-names` package). `SLICC_WASI_STATS=1` makes a program print its calls, counted and timed, as it ends.
194
195
 
@@ -228,11 +229,14 @@ The module's default export (or its `main` export) is called with a context and
228
229
  - `websocket(url, { protocols, headers, signal })`, a WebSocket over the kernel's transport (its `websocket`, so `fetchTransport({ webSocket: true })` in a browser), shaped like `WebSocketStream`. It resolves once the socket is open, with `url`, the `protocol` the far end chose (always one the program offered: a transport that hands back a socket with another fails the open with `EPROTO`, while browsers and Node's `WebSocket`, which `fetchTransport()` uses, fail such a handshake themselves without saying why, so the open fails with `ECONNREFUSED`), a `readable` of messages (`string` for text, `Uint8Array` for binary), a `writable` for sending them, `closed` (the `{ code, reason }` of the close, from either side, whether or not the program reads) and `close({ code, reason })`. An `http(s):` URL is taken as `ws(s):`. Sending waits while more than 1 MiB is buffered. A signal aborts the open, and after it the socket. A transport without WebSockets rejects with a `TypeError` whose `cause.code` is `ENOTSUP`, and one that cannot connect with `ECONNREFUSED`;
229
230
  - `fetch(input, init)`, the web's `fetch` over the kernel's transport, the one the proxy uses, so the embedder's transport, its WebSocket and proxy settings and the kernel's routes apply. The response body streams as the program reads it. The request body is read whole first, since a transport takes it whole (streaming it is [#238](https://github.com/ai-ecoverse/slicc-kernel/issues/238)), and one larger than the transport's `maxRequestBody` is answered `413`, as through the proxy. A signal aborts the request at any stage. Redirects follow the fetch rules: at most 20, a 301 or 302 after a `POST` and a 303 turn into a `GET` without a body, `Authorization` is dropped across origins, and `redirect: 'manual'` returns the redirect itself. On a transport that follows redirects on its own (`fetchTransport()` in a browser) only `'follow'` works: `'manual'` and `'error'` reject with a `TypeError` whose `cause.code` is `ENOTSUP`, since such a transport never shows the program a redirect. A status the transport chose comes back as a response whose body is the reason. An unreachable host, or no transport at all, rejects with a `TypeError` whose `cause.code` is `ECONNREFUSED` or `ENETUNREACH`, as Node's `fetch` does. A `data:` URL is answered by `fetch` itself, as the fetch standard decodes it, without a transport: for any method, a `HEAD` without a body, and its type parsed and serialized as a MIME type (lowercase, no spaces, invalid parameters dropped), as Node's `fetch` answers; any other scheme than `http:` and `https:` rejects with a `TypeError` whose `cause.code` is `EPROTONOSUPPORT`, at the start as for a redirect target. The global `fetch` stays withheld;
230
231
  - `tty`, for programs on a terminal: `getAttr(fd)` and `setAttr(fd, termios)` read and set its termios (`{ c_iflag, c_oflag, c_cflag, c_lflag, c_cc }`, Linux values), `setRaw(fd, raw)` switches raw mode on as libuv (and Node's `setRawMode`) does (no echo, no line editing, no `^C` signal, each key read as it comes) and off again to the termios it had before (per terminal: after raw mode on fds 0 and 1 of one terminal, turning it off on either restores the termios from before the first), `size(fd)` answers `{ columns, rows }`, and `foreground(fd)` and `setForeground(fd, pgrp)` get and set the foreground process group, under the same job-control rules as other programs. A window resize arrives as `SIGWINCH` through `signals`. A terminal the program left raw is set back when the program ends, whether it returns, exits or throws (a program killed by a signal cannot do so). On a descriptor that is not a terminal, each call fails with `ENOTTY`;
231
- - `exit(status)`, which ends the program once its pending writes are done.
232
+ - `exit(status)`, which ends the program once its pending writes are done;
233
+ - `onUncaught(listener)` and `onUnhandledRejection(listener)`, which take the errors the program throws outside any call and the promises it leaves rejected (`listener(error, promise)`), as Node's `uncaughtException` and `unhandledRejection` events do. While a listener is registered, the program decides what such an error means; each call answers a function that removes its listener. A listener that throws ends the program as an unhandled error does.
232
234
 
233
- A program that throws, or leaves an error or a rejected promise unhandled, ends with status 1 and the message on stderr; one whose module cannot be loaded, or exports no function, ends with 126. A write to a pipe whose reader is gone ends the program with 141, unless it ignores or catches `SIGPIPE` (an ignore inherited from its parent counts), in which case the write fails with `EPIPE`. Errors carry a POSIX `code`. Reads and writes of pipes, terminals and sockets go through the kernel's asynchronous operations, so a program blocked reading stdin can still write and handle signals; other calls of one process are made one at a time. A JS program cannot `fork`, and threads it starts itself are not the kernel's.
235
+ A program that throws, or leaves an error or a rejected promise unhandled with no listener for it, ends with status 1 and the message on stderr; one whose module cannot be loaded, or exports no function, ends with 126. A write to a pipe whose reader is gone ends the program with 141, unless it ignores or catches `SIGPIPE` (an ignore inherited from its parent counts), in which case the write fails with `EPIPE`. Errors carry a POSIX `code`. Reads and writes of pipes, terminals and sockets go through the kernel's asynchronous operations, so a program blocked reading stdin can still write and handle signals; other calls of one process are made one at a time. A JS program cannot `fork`, and threads it starts itself are not the kernel's.
234
236
 
235
- The module has the trust of an Emscripten program's glue or a WASI program's imports module: it comes from a package that was installed, and it runs in the worker of its own process. The kernel withholds the worker's own ways out before it loads the module: `fetch`, `XMLHttpRequest`, `WebSocket`, `WebTransport`, `EventSource`, `importScripts`, `indexedDB`, `caches`, `cookieStore`, `Worker`, `SharedWorker`, `BroadcastChannel`, `postMessage`, `close`, and `navigator.storage`, `navigator.locks` and `navigator.serviceWorker` are `undefined`, and a worker where one of them cannot be removed refuses to run the program (126). A program's files are the kernel's, through the context; it reaches the kernel only through its own process's system calls, as the context makes them, and no other process's. This is a guard against programs going around the kernel, not a sandbox: `import()` of a URL is a request the kernel cannot withhold. Under the Node entry, a JS program also has Node's own modules.
237
+ The module has the trust of an Emscripten program's glue or a WASI program's imports module: it comes from a package that was installed, and it runs in the worker of its own process. The kernel withholds the worker's own ways out before it loads the module: `fetch`, `XMLHttpRequest`, `WebSocket`, `WebTransport`, `EventSource`, `importScripts`, `indexedDB`, `caches`, `cookieStore`, `Worker`, `SharedWorker`, `BroadcastChannel`, `postMessage`, `close`, and `navigator.storage`, `navigator.locks` and `navigator.serviceWorker` are `undefined`, and a worker where one of them cannot be removed refuses to run the program (126). A program's files are the kernel's, through the context; it reaches the kernel only through its own process's system calls, as the context makes them, and no other process's. This is a guard against programs going around the kernel, not a sandbox: `import()` of a URL is a request the kernel cannot withhold.
238
+
239
+ Under the Node entry, the kernel also withholds the host's Node from the program before its module loads ([#297](https://github.com/ai-ecoverse/slicc-kernel/issues/297)). `require`, `module`, `exports`, `__filename`, `__dirname`, `localStorage` and `sessionStorage` are `undefined` for good. `process`, `Buffer`, `global`, `setImmediate` and `clearImmediate` are `undefined` too, but stay writable, so a program can install its own (as slicc-jsh does). Imports of Node's builtin modules (`node:fs`, `fs`, `node:worker_threads`, …) and of anything but a `data:` URL, `file:` included, fail with `ERR_ACCESS_DENIED`, whether static, dynamic or through `import.meta.resolve`. A process worker that exits or crashes on its own ends its process with 70. This is the same guard, not a sandbox: a host that runs code it does not trust, such as an agent's, needs its own isolation around the Node process as well.
236
240
 
237
241
  ## Network
238
242
 
@@ -9410,6 +9410,8 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
9410
9410
  "proc-cred",
9411
9411
  "proc-setcred",
9412
9412
  "dl-log",
9413
+ "dl-closure",
9414
+ "dl-closure-free",
9413
9415
  "fd-renumber",
9414
9416
  "fd-promote",
9415
9417
  "fd-open-tty",
@@ -9448,6 +9450,7 @@ function isWasmSyscall(req) {
9448
9450
  return typeof op === "string" && SYSCALL_OPS.has(op);
9449
9451
  }
9450
9452
  var MAX_READ = 1024 * 1024;
9453
+ var KEYBOARD = /* @__PURE__ */ new Set([SIG.INT, SIG.QUIT, SIG.TSTP]);
9451
9454
  var DEFAULT_UMASK = 18;
9452
9455
  var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
9453
9456
  var HOSTS_ONLY = new Resolver({ routes: new Routes() });
@@ -9466,6 +9469,8 @@ var WasmProcess = class {
9466
9469
  execTermsig;
9467
9470
  stopped = 0;
9468
9471
  stops = 0;
9472
+ owed = false;
9473
+ waits = 0;
9469
9474
  resumed = Promise.resolve();
9470
9475
  wake;
9471
9476
  stateListeners = [];
@@ -9521,6 +9526,7 @@ var WasmProcess = class {
9521
9526
  return "terminate";
9522
9527
  }
9523
9528
  this.options.onPending?.(sig);
9529
+ if (this.options.jobs?.keyboard && KEYBOARD.has(sig)) this.owed = true;
9524
9530
  const blocked = this.interrupt;
9525
9531
  this.interrupt = new AbortController();
9526
9532
  blocked.abort();
@@ -9565,8 +9571,10 @@ var WasmProcess = class {
9565
9571
  await this.resumed;
9566
9572
  if (this.dying) return DYING;
9567
9573
  const stops = this.stops;
9574
+ const waits = this.waits;
9568
9575
  const result = await this.dispatch(req, cancelled);
9569
9576
  if (this.dying) return DYING;
9577
+ if (!result.ok && result.errno === "EINTR" && this.waits !== waits) this.owed = false;
9570
9578
  const restart = !result.ok && result.errno === "EINTR" && this.stops !== stops;
9571
9579
  if (restart && !this.options.hasPending?.()) continue;
9572
9580
  await this.resumed;
@@ -9619,8 +9627,11 @@ var WasmProcess = class {
9619
9627
  refuseIfPending(restart) {
9620
9628
  if (restart && this.options.hasPending?.()) throw new KernelError("EINTR");
9621
9629
  }
9622
- blockingSignal(cancelled) {
9623
- if (this.options.hasPending?.() || cancelled?.aborted) throw new KernelError("EINTR");
9630
+ blockingSignal(cancelled, owing = true) {
9631
+ if (owing) this.waits++;
9632
+ if (this.options.hasPending?.() || cancelled?.aborted || owing && this.owed) {
9633
+ throw new KernelError("EINTR");
9634
+ }
9624
9635
  return cancelled ? AbortSignal.any([this.interrupt.signal, cancelled]) : this.interrupt.signal;
9625
9636
  }
9626
9637
  async read(req, cancelled) {
@@ -9766,7 +9777,7 @@ var WasmProcess = class {
9766
9777
  return { ok: true, kind: "json", json: this.options.jobs?.terminalNames() ?? [] };
9767
9778
  case "fd-select": {
9768
9779
  const { read, write, timeoutMs } = req;
9769
- const signal = this.blockingSignal(cancelled);
9780
+ const signal = this.blockingSignal(cancelled, timeoutMs !== 0);
9770
9781
  const selected = await selectFds(this.fds, read, write, timeoutMs, signal);
9771
9782
  return { ok: true, kind: "json", json: selected };
9772
9783
  }
@@ -10017,6 +10028,11 @@ var WasmProcess = class {
10017
10028
  case "dl-log":
10018
10029
  if (req.append) this.dlLog.push(req.append);
10019
10030
  return { ok: true, kind: "json", json: this.dlLog.slice(req.from) };
10031
+ case "dl-closure":
10032
+ return { ok: true, kind: "json", json: this.allocateClosure(req.floor) };
10033
+ case "dl-closure-free":
10034
+ this.freeClosure(req.index);
10035
+ return { ok: true, kind: "void" };
10020
10036
  case "sig-mask":
10021
10037
  this.caught = req.caught;
10022
10038
  this.ignored = req.ignored;
@@ -10053,6 +10069,18 @@ var WasmProcess = class {
10053
10069
  }
10054
10070
  alarm;
10055
10071
  dlLog = [];
10072
+ closures = { next: 0, free: [], live: /* @__PURE__ */ new Set() };
10073
+ allocateClosure(floor) {
10074
+ const index = this.closures.free.pop() ?? Math.max(floor, this.closures.next);
10075
+ this.closures.next = Math.max(this.closures.next, index + 1);
10076
+ this.closures.live.add(index);
10077
+ this.dlLog.push({ kind: "closure", index });
10078
+ return index;
10079
+ }
10080
+ freeClosure(index) {
10081
+ if (!this.closures.live.delete(index)) throw new KernelError("EINVAL");
10082
+ this.closures.free.push(index);
10083
+ }
10056
10084
  asyncOps = new AsyncOps(
10057
10085
  (req, cancelled) => this.syscall(req, cancelled),
10058
10086
  () => this.options.onAsync?.()
@@ -10442,6 +10470,7 @@ var JobTable = class {
10442
10470
  handoffs = /* @__PURE__ */ new Map();
10443
10471
  foreground = /* @__PURE__ */ new Map();
10444
10472
  terminals = /* @__PURE__ */ new Map();
10473
+ keyboard = false;
10445
10474
  add(pid, parentPid, signal, terminal) {
10446
10475
  const parent = parentPid === void 0 ? void 0 : this.members.get(parentPid);
10447
10476
  if (!parent && terminal) this.terminals.set(pid, terminal);
@@ -10611,6 +10640,14 @@ var JobTable = class {
10611
10640
  signalForeground(tty, fallback, sig) {
10612
10641
  this.killGroup(this.tcgetpgrp(tty, fallback), sig);
10613
10642
  }
10643
+ fromKeyboard(send) {
10644
+ this.keyboard = true;
10645
+ try {
10646
+ send();
10647
+ } finally {
10648
+ this.keyboard = false;
10649
+ }
10650
+ }
10614
10651
  };
10615
10652
 
10616
10653
  // src/kernel/settling.ts
@@ -12210,6 +12247,7 @@ var Launcher = class {
12210
12247
  module,
12211
12248
  ...memory ? { memory } : {},
12212
12249
  foreign,
12250
+ path: target.wasm,
12213
12251
  ...names ? { names } : {},
12214
12252
  ...imports !== void 0 ? { imports } : {},
12215
12253
  ...target.preopenRoot ? { preopenRoot: true } : {}
@@ -12437,7 +12475,8 @@ var Launcher = class {
12437
12475
  keySignal(tty, fallback, sig, send) {
12438
12476
  const pgid = this.jobs.tcgetpgrp(tty, fallback);
12439
12477
  const foreground = () => this.jobs.tcgetpgrp(tty, fallback);
12440
- this.settling.deliver(sig, pgid, send, tty.reading ? void 0 : foreground);
12478
+ const keyed = () => this.jobs.fromKeyboard(send);
12479
+ this.settling.deliver(sig, pgid, keyed, tty.reading ? void 0 : foreground);
12441
12480
  }
12442
12481
  umaskOf(pid) {
12443
12482
  return this.processes.get(pid)?.umask?.() ?? DEFAULT_UMASK;