@ai-ecoverse/slicc-kernel 1.29.2 → 1.30.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
@@ -140,15 +140,38 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
140
140
  { "slicc": { "abi": "wasi", "commands": { "rg": { "wasm": "bin/rg.wasm" }, "rustc": { "script": "bin/rustc" } } } }
141
141
  ```
142
142
 
143
- Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. 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.
143
+ Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. 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.
144
144
 
145
145
  ### WASI programs
146
146
 
147
- A WASI program runs in a process worker of its own like an Emscripten one, against the same kernel: its descriptors, pipes, terminals and job control are the kernel's, and its files are OPFS. Preopens are `.` (the cwd), `/dev` and every top-level directory, plus the virtual `/usr` and `/bin`. On top of preview1 it gets:
147
+ A WASI program runs in a process worker of its own like an Emscripten one, against the same kernel: its descriptors, pipes, terminals and job control are the kernel's, and its files are OPFS. **Preopens**, in order:
148
+ 1. `.`, the cwd, as fd 3, which Zig's std takes for its cwd;
149
+ 2. `/dev` and every top-level directory, plus the virtual `/usr` and `/bin`, sorted by name.
150
+
151
+ Every WASI program sees `/` through the cwd.
152
+
153
+ **`preopenRoot`.** A command whose manifest entry sets `"preopenRoot": true` gets two more preopens after those:
154
+ 3. `/`, the root;
155
+ 4. `./`, the cwd again.
156
+
157
+ That's for programs that walk to the root, such as Go's (`GOOS=wasip1`) and esbuild's resolver. The order serves each runtime:
158
+ - **Go** takes its working directory from `PWD`, which every process gets, and resolves absolute paths against the longest matching preopen name. So `os.ReadDir("/")`, `filepath.Abs("../..")` and walking up to the root work. Without `preopenRoot`, nothing is named `/` and they fail with `EBADF`.
159
+ - **wasi-libc** (C, and Rust's `wasm32-wasip1`) reads `.`, `/` and `./` as the same empty prefix, and the last one wins. So the trailing `./` keeps relative paths on the cwd.
160
+ - **Zig** keeps fd 3, and a second `.` would stop it from starting.
161
+
162
+ **Working directory.** wasi-libc's own `getcwd()` stays `/` (Rust's `current_dir()`, `canonicalize(".")`) in either layout, because the C library starts with a working directory of `/` and no runtime can change it. A wasi-libc program that needs its real directory takes it from `PWD` (`set_current_dir($PWD)` at start); relative paths are the cwd either way. WASIX programs ask the kernel for their cwd and get the real one.
163
+
164
+ **Descriptor numbers.** Preopens take consecutive descriptors from 3, since WASI libraries look for them until the first gap:
165
+ - without `preopenRoot`: fds 3 through 3 + *n*, *n* being the number of top-level directories, `/dev`, `/usr` and `/bin` together;
166
+ - with it: two more, through 3 + *n* + 2.
167
+
168
+ A descriptor a program inherits at one of those numbers (from bash's `3<file`, or a `posix_spawn` file action) moves to the first free number above them. One above the range keeps its number. A command that takes descriptors by number from its caller should leave `preopenRoot` off, which keeps the shorter range.
169
+
170
+ On top of preview1 a WASI program gets:
148
171
 
149
172
  - **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.
150
173
  - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid`, 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`.
151
- - **Sockets**: the WASIX socket calls (`sock_open`, `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 loopback names and literal addresses.
174
+ - **Sockets**: the WASIX socket calls (`sock_open`, `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.
152
175
  - **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.
153
176
 
154
177
  ### Program imports
@@ -4550,6 +4550,7 @@ function commandOf(pkg, name2, raw, packageAbi, shared) {
4550
4550
  wasm,
4551
4551
  argv0,
4552
4552
  ...raw.argv0Path === true ? { argv0Path: true } : {},
4553
+ ...abi === "wasi" && raw.preopenRoot === true ? { preopenRoot: true } : {},
4553
4554
  ...args ? { args } : {},
4554
4555
  ...withEnv,
4555
4556
  ...imports ? { imports } : {}
@@ -7999,9 +8000,10 @@ var WasmProcess = class {
7999
8000
  json: this.fds.numbers().map((fd) => ({ fd, ...this.fdInfo(fd) }))
8000
8001
  };
8001
8002
  case "fd-meta": {
8002
- const file = this.fds.get(req.fd).file;
8003
- if (!file.held) throw new KernelError("EBADF");
8004
- file.heldMeta = req.meta;
8003
+ if (!this.fds.get(req.fd).file.held) throw new KernelError("EBADF");
8004
+ const cloexec = this.fds.closesOnExec(req.fd);
8005
+ this.fds.installAt(req.fd, heldFile(req.meta));
8006
+ if (cloexec) this.fds.setCloseOnExec(req.fd);
8005
8007
  return { ok: true, kind: "void" };
8006
8008
  }
8007
8009
  case "fd-setfl":
@@ -9893,7 +9895,8 @@ function targetOf(command) {
9893
9895
  ...command.args ? { prefix: command.args } : {},
9894
9896
  ...command.env ? { env: command.env } : {},
9895
9897
  ...command.unset ? { unset: command.unset } : {},
9896
- ...command.imports ? { imports: command.imports } : {}
9898
+ ...command.imports ? { imports: command.imports } : {},
9899
+ ...command.preopenRoot ? { preopenRoot: true } : {}
9897
9900
  };
9898
9901
  }
9899
9902
  function childHandle(handle) {
@@ -10129,7 +10132,8 @@ var Launcher = class {
10129
10132
  ...memory ? { memory } : {},
10130
10133
  foreign,
10131
10134
  ...names ? { names } : {},
10132
- ...imports !== void 0 ? { imports } : {}
10135
+ ...imports !== void 0 ? { imports } : {},
10136
+ ...target.preopenRoot ? { preopenRoot: true } : {}
10133
10137
  };
10134
10138
  }
10135
10139
  async launch(planned, req) {