@ai-ecoverse/slicc-kernel 1.144.1 → 1.145.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
@@ -191,7 +191,7 @@ When the kernel goes away (the page closed or reloaded, `terminate()`), pending
191
191
 
192
192
  `@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.
193
193
 
194
- **Node 26 or later** ([#1129](https://github.com/ai-ecoverse/slicc-kernel/issues/1129)): `engines` asks for Node >=26. V8 in Node 24 sometimes crashed the whole Node process with a `jit_page` check failure while a process worker started or was torn down; slicc-cli measured 15 crashes in 1,600 runs on Node 24 and none in 1,600 on Node 26. The kernel's CI and release run on 26, and the nightly `Node versions` workflow keeps running the suites that start the Node kernel and counts any such crash.
194
+ **Node 26 or later** ([#1129](https://github.com/ai-ecoverse/slicc-kernel/issues/1129)): `engines` asks for Node >=26. V8 in Node 24 sometimes crashed the whole Node process with a `jit_page` check failure while a process worker started or was torn down; slicc-cli measured 15 crashes in 1,600 runs on Node 24 and none in 1,600 on Node 26. The kernel's CI and release run on 26.
195
195
 
196
196
  ```js
197
197
  import { createNodeKernel, nodeTransport } from '@ai-ecoverse/slicc-kernel/node';
@@ -616,7 +616,7 @@ The kernel answers DNS itself, so DNS clients (BIND's `dig`, `host` and `nslooku
616
616
  ### Node name
617
617
 
618
618
  The kernel has one node name, `slicc` unless `createKernel({ hostname })` (or `createNodeKernel`) names another; it must be a valid host name, and not `localhost`, a `*.localhost` name, `host.slicc.internal`, `wasmer.sh` or an IPv4 address, or `createKernel` rejects. `no_proxy` lists it (and `emscripten`), so HTTP clients reach the kernel's own servers by it directly.
619
- - **Where programs see it:** through `uname`, `gethostname` and `/etc/hostname`, never only from the environment ([#1272](https://github.com/ai-ecoverse/slicc-kernel/issues/1272)). `HOSTNAME` is in the kernel's base and login environments, so `kernel.run`, terminals and login shells have it, and a program keeps whatever `HOSTNAME` its caller passes (`env -i env` shows none). One exception: a WASI or WASIX program always gets `HOSTNAME` set to the node name, because wasix-sysroot's `gethostname` reads it from the environment. So its `os.environ` shows it after `env -i`, until the sysroot asks the kernel instead. Every program can read it from `/etc/hostname`: the node name and a newline, a `0644` root-owned file that a real `/etc/hostname` in the root replaces ([#1297](https://github.com/ai-ecoverse/slicc-kernel/issues/1297)). Emscripten programs also have the synthetic `/etc/hosts` and `/etc/passwd`, which only they have (WASI programs resolve names through the kernel instead), and `Module.sliccKernel.hostname`.
619
+ - **Where programs see it:** through `uname`, `gethostname` and `/etc/hostname`, never only from the environment ([#1272](https://github.com/ai-ecoverse/slicc-kernel/issues/1272)). `HOSTNAME` is in the kernel's base and login environments, so `kernel.run`, terminals and login shells have it, and a program keeps whatever `HOSTNAME` its caller passes (`env -i env` shows none). One exception: a WASI or WASIX program built on wasix-sysroot before 2025.9.30-42 (its `slicc.libc` marker), or with no marker, gets `HOSTNAME` set to the node name, because that `gethostname` reads it from the environment, so its `os.environ` shows it after `env -i`. From -42 on, `gethostname` and `uname` read `/etc/hostname`, and such a program keeps the environment its caller gave it. Every program can read it from `/etc/hostname`: the node name and a newline, a `0644` root-owned file that a real `/etc/hostname` in the root replaces ([#1297](https://github.com/ai-ecoverse/slicc-kernel/issues/1297)). Emscripten programs also have the synthetic `/etc/hosts` and `/etc/passwd`, which only they have (WASI programs resolve names through the kernel instead), and `Module.sliccKernel.hostname`.
620
620
  - **What `uname` says:** for an Emscripten program built with homescoop's `__syscall_uname` import (wasm-coreutils 9.12.0-7 on), the kernel answers `uname(2)`: sysname `Linux`, nodename the node name, release `6.0.0-slicc` (as `/proc/sys/kernel/osrelease`), version `#1 SMP PREEMPT_DYNAMIC` (as `/proc/version`), machine `wasm32` and domainname `(none)`, so `uname -a` and musl's `gethostname()` see them. `uname -o` is coreutils' compiled-in `Emscripten`. Older Emscripten builds still report their C library's `Emscripten 4.0.23` and `emscripten`, and WASIX programs `wasmer.sh`; the kernel has nothing to answer there.
621
621
  - **Resolving it:** `/etc/hosts` maps the node name and both of those names to `127.0.1.1` (Debian's convention), and name resolution answers them, and `localhost.localdomain`, itself, as `127.0.1.1` and `::1`. So a program that looks up its own host name, as git does for every commit, never asks the [uplink](#uplink), and `127.0.1.1` is the kernel's loopback.
622
622
  - **The trade-off:** a raw connection to the real `wasmer.sh` resolves to loopback too. HTTP(S) to it still goes through the realm proxy and the transport, which resolve names themselves; `wasmer.sh` is deliberately not in `no_proxy`.
@@ -1055,7 +1055,7 @@ The kernel runs scheduled jobs itself, with no daemon process: a cron service st
1055
1055
  - **`@reboot`** runs once per kernel boot, when that kernel first fires jobs.
1056
1056
  - **Overlap:** a job's runs don't wait for each other, so a slow job can still be running when it is due again. At most 16 jobs of one user run at once; a run past that is skipped, with a line in the job's log and in `/var/log/cron/cron.err`.
1057
1057
  - **Logs:** each run appends `--- <due time> exit=<status> missed=<n> ---` (or `signal=<name>`), then the job's standard output and error, to `/var/log/cron/<user>/<id>.log`. The directory is root's `0755`, so a user can't put a symlink or another file in place of a log; the file is the user's `0600` and keeps its last 256 KiB. The service writes a log only when its directory is a real directory of root and the log is a regular file of that user or root. Otherwise it leaves the path alone and says why in `cron.err`. There is no mail. Lines that don't parse and tables the service won't run go to `/var/log/cron/cron.err` (root's `0600`), once per version of the table, and so does each skipped run.
1058
- - **Tags:** a job runs with the tags of the process that installed its line through `crontab` (see `kernel.connect`), kept in the root-only `/var/spool/cron/tags` and never in the table's text. A line reinstalled unchanged keeps its tags, with the new installer's added; a new or edited line gets the installer's. With `users` on, a process with any tag can't change `/etc/crontab` or anything under `/var/spool/cron` itself, also through a symlink or a hard link (`EACCES`; renaming or removing a symlink that points there is refused too), so it schedules only through `crontab`; untagged processes keep direct access. With `users: 'off'` this isn't enforced, as every process is root there ([#1380](https://github.com/ai-ecoverse/slicc-kernel/issues/1380)).
1058
+ - **Tags:** a job runs with the tags of the process that installed its line through `crontab` (see `kernel.connect`), kept in the root-only `/var/spool/cron/tags` and never in the table's text. That file is a stable surface for the embedder: JSON `{ "v": 1, "<table path>": { "<job id>": { "tags": [...], "base": <n> } } }`, `base` being how many of `tags` came from the installer's connection; a change of format bumps `v` and is announced to slicc-agent first. A line reinstalled unchanged keeps its tags, with the new installer's added; a new or edited line gets the installer's. With `users` on, a process with any tag can't change `/etc/crontab` or anything under `/var/spool/cron` itself, also through a symlink or a hard link (`EACCES`; renaming or removing a symlink that points there is refused too), so it schedules only through `crontab`; untagged processes keep direct access. With `users: 'off'` this isn't enforced, as every process is root there ([#1380](https://github.com/ai-ecoverse/slicc-kernel/issues/1380)).
1059
1059
  - **Trust:** with owners on (`users` not `'off'`), a user's table runs only if it is a regular file (not a symlink) owned by that user or root and writable by its owner only, and `/etc/crontab` only if it is root's and writable by root only. A table named for no user is skipped. The kernel creates `/var/spool/cron` (`0755`), `/var/spool/cron/crontabs` (`0700`) and `/var/log/cron` (`0755`), all root's.
1060
1060
  - **Changes** to a table (here or in another tab) take effect without a restart. The service also re-reads the tables at least once a minute.
1061
1061
  - **Several tabs:** only one kernel per root fires jobs. It holds the Web Lock `slicc-kernel-cron:<metadata database>` (the database that holds the root's owners and modes, so tabs on one root share it), and when its tab closes, another kernel takes over and catches up. In a background tab the browser throttles timers to once a minute, so a job may run up to a minute late there. A tab that was closed catches up on its next open, by the missed-run rule above.
@@ -1236,7 +1236,7 @@ npm test
1236
1236
  npm run test:unit
1237
1237
  ```
1238
1238
 
1239
- `npm test` builds `dist/` and runs the integration tests in Chromium from `playwright-core`, over raw CDP through the [harness from slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web#integration-test-harness), against a cross-origin isolated test page that installs bash and coreutils into OPFS. Each test writes screenshots and console logs to `artifacts/`. With `SLICC_COVERAGE=1`, V8 coverage from the page and every worker, mapped back to `src/` through the source maps, goes to `coverage/` (each worker pauses at its exit so its coverage is taken first); without it, which is how CI runs, the harness attaches no debugger or profiler to the workers, because nothing reads that coverage (unit-test diff coverage comes from the unit store). With `SLICC_PROFILE=1` the tests also record CPU profiles, and `artifacts/hotspots.md` lists where the time went.
1239
+ `npm test` builds `dist/` and runs the integration tests in Chrome for Testing, installed by `slicc-cdp install-browser`, over raw CDP through the [harness from slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web#integration-test-harness), against a cross-origin isolated test page that installs bash and coreutils into OPFS. Each test writes screenshots and console logs to `artifacts/`. With `SLICC_COVERAGE=1`, V8 coverage from the page and every worker, mapped back to `src/` through the source maps, goes to `coverage/` (each worker pauses at its exit so its coverage is taken first); without it, which is how CI runs, the harness attaches no debugger or profiler to the workers, because nothing reads that coverage (unit-test diff coverage comes from the unit store). With `SLICC_PROFILE=1` the tests also record CPU profiles, and `artifacts/hotspots.md` lists where the time went.
1240
1240
 
1241
1241
  The recorder behind that coverage changes timing and memory in long runs. It attaches to every page and worker with the debugger, profiler and precise coverage on, and pauses every process worker at its exit breakpoint (`exits` in `test/integration/chrome.mjs`) until the coverage of that worker is collected, keeping every collected script until the run ends. A run with thousands of process exits or hundreds of page loads therefore grows the test process by gigabytes, and each exit waits longer as it grows, so a parent can sit in `proc-exec` for tens of seconds while its child has already finished; each further page load also gets slower (in one tab reloaded 300 times, a boot went from 80 to 500 ms with no kernel in any other tab). When a long run looks stuck or leaky, measure it again with a `launch` that passes `coverage: ['/nothing/']` and no `exits` before blaming the kernel (#991).
1242
1242
 
@@ -1244,8 +1244,6 @@ Unit tests live in `test/unit/`, which stays out of git. They run real wasm bash
1244
1244
 
1245
1245
  The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web), which also provides the `slicc-lint-comments` (no comments anywhere), `slicc-no-unit-tests` (no unit tests in git) and `slicc-diff-cover` (100% coverage of changed lines) commands that `npm run lint` and the pre-commit hook use.
1246
1246
 
1247
- **CI runners.** CI's first job, `runner`, picks where `ci / test` runs with `.github/runner.mjs`: the bench pool (`vars.CERT_RUNNER`, else `cloud-run-bench`) for same-repo pull requests and for merge groups whose entries are all same-repo, and GitHub's hosted `ubuntu-latest` for fork pull requests, merge groups with a fork or unknown entry, other events, and whenever the decider is missing or fails. `vars.CI_RUNNER` forces one runner. The `runner` job itself runs hosted for a fork pull request, otherwise on `vars.CERT_LIGHT_RUNNER` or `vars.CERT_RUNNER`. It loads `runner.mjs` from trusted code, never from the code under test: for a merge group from the protected branch itself (`merge_group.base_ref`, `refs/heads/main`), because the group's `base_sha` can be the speculative commit of the group ahead, which carries the PRs ahead in the queue (a fork's `runner.mjs` included); for a pull request from its base commit, which is on `main`; else from the commit itself. So a pull request can't change the answer by editing the decider. That's defence in depth: a fork pull request that edits `ci.yml` itself is stopped by the organisation's approval rule for outside contributors' workflows and the pool's runner-group access. `test/integration/workflows.test.mjs` runs the step's script against the decider, a missing one and a failing one.
1248
-
1249
1247
  `test/integration/fixtures/dlfork/` is a WASIX PIE program (`dlfork.c`) that `dlopen`s a side module (`side.c`, built as `lib/libdlside.so`), forks, and has the child read the module's data and call through function pointers taken before the fork, for the fork-after-`dlopen` test. It is built with wasix-sysroot 2025.9.30-28's legacy-EH PIC variant and homescoop's pinned wasixcc, then Asyncify for `proc_fork`: `INSTALL_WASIXCC=<homescoop>/scripts/install-wasixcc.sh WASIX_SYSROOT_28=<unpacked -28 package> test/integration/fixtures/dlfork/build.sh`. `bin/dlfork.wasm` is about 800 KB, far bigger than the other fixtures: wasixcc links a position-independent main with `--whole-archive --export-all`, so side modules can find any libc symbol, and neither `--gc-sections` nor a narrower export list (`--no-export-dynamic --export=…`) takes that out.
1250
1248
 
1251
1249
  `test/integration/fixtures/preh1stat/` is a tiny Emscripten program (`preh1stat.c`) that prints its `getuid`/`geteuid`/`getgid`/`getegid` and the owner of each path it's given. It's built **without** the credential library (`slicc_cred_js`), with today's emcc from homescoop's pinned `emsdk@0.4.0`, so it stands for the packages installed before H1. Those see every file as `1000:1000` (`stat-owner.test.mjs`), and every published `@ai-ecoverse/wasm-*` package now links the library, so no package can carry that row. Build it with `test/integration/fixtures/preh1stat/build.sh`: it fetches `emsdk@0.4.0` unless `emcc` is on `PATH`, `PREH1STAT_BUILD` sets its work directory, and it refuses a build whose glue carries the credential library.
@@ -1256,59 +1254,13 @@ The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc
1256
1254
 
1257
1255
  `tools/big-file.mjs [bytes]` is a manual check, not part of any suite. It writes a file of that size (64 MiB by default) through a tmpfs mount with the unit-test launcher, reads it back, and prints the peak RSS and its ratio to the file size. A 256 MiB file peaks at about 1.8 times its size. Before ranged I/O (#127), a 1.1 GiB file peaked at 18 GB of RSS. **Do not run it past 1 GiB on a workstation.** It refuses anything over 256 MiB unless `SLICC_BIG_FILE_I_HAVE_THE_RAM=1` is set. Measure a small file and extrapolate instead. The unit suite checks a 32 MiB file the same way, and allows a fixed 192 MiB for garbage V8 has yet to collect, which dominates at that size.
1258
1256
 
1259
- Releases are cut by semantic-release on every push to `main`, in two jobs, and only on `main`. The first job is light work and runs on the light runner (`CERT_LIGHT_RUNNER`), so it starts within seconds of the push, before `main` has moved on. It computes the version, tags the commit and creates the GitHub release, with `npmPublish: false`. A run that starts after `main` has moved skips; the run for the newer commit releases it. The second job runs on a GitHub-hosted runner, because npm's trusted publishing and provenance accept no self-hosted runner. It publishes the tagged commit with `tools/release-recover.mjs`. However long it waits for a runner, it publishes exactly the commit the run started on, so the provenance, the tag and the package's `gitHead` agree. For any version tagged at HEAD, `tools/release-recover.mjs` pushes the tag if origin lacks it, publishes the version if npm lacks it (with retries) and it is newer than npm's `latest` (an older one stays unpublished, so `latest` never moves back to it, and an `npm view` error fails the job instead of guessing; #837), and creates its GitHub release if that is missing too. So a failure after the version is tagged (a rejected push, a failed `npm publish`) doesn't strand it, and re-running the Release workflow completes it the same way.
1260
-
1261
- ### Certification (`cert-review`)
1262
-
1263
- Every change is certified in its pull request, before it merges (`cert-review-pr.yml`, next section), and `cert-review` is a required check of the merge queue, so every commit on `main`, and every release built from it, was certified first. There is no post-publish verification job: being merged on `main` is the certification, and consumers read npm's own provenance (SLSA v1 from `release.yml` on `refs/heads/main`, a GitHub-hosted builder) and integrity, as bios's pin check does. `release-recover` publishes a version semantic-release left unpublished with a plain `npm publish --provenance`, failing only on npm's own non-zero exit.
1264
-
1265
- The tools in `tools/cert-review/`:
1266
-
1267
- - **delta** (`delta.mjs`): lists the pull requests merged between the two tags (their merge commits, plus `pr` for a planted branch). Each is a behaviour change **by the paths it changes**, with the same rule the plan uses (`behaviourFiles`: `src/`, `build.mjs`, `package.json` and its lock, the build and publish workflows, the reviewer…), whatever its title says (#1036 and #1041 were titled "A2 …"), and it gets its conventional-commit kind and the integration test files and titles it adds or changes. Every behaviour change with tests gets its NEGATIVE: its tests run on the previous tree. A `fix` must fail there (`no-negative` otherwise); any other kind is `negative` or `passes`. A behaviour change whose NEGATIVE has no result (`prev-not-run`), or whose changed tests the delta can't name (`no-titles`), makes the record INCOMPLETE. The delta names the test around every changed line: a title on the `test(` line or the line after it (as biome wraps a long one), a template literal, a title in the context lines above the change, and, when the hunk header shows only `test(`, the title read from the file at the merge commit. A pull request that changes only `package.json` and `package-lock.json`, and in `package.json` only the versions in `dependencies`, `devDependencies` and `optionalDependencies` (delta.mjs compares the file before and after its merge), is decided `manifest` with no test gap: it passed the required `cert-review` in the merge queue, as identical dist or with a full review, so the release line doesn't repeat it. A scripts, exports or new-dependency change stays a behaviour change, with `no-test` when it has no tests. The titles are read from the pull request's patch: the added `test(…)` lines and the hunk headers git puts above changed lines. Escapes are decoded as JavaScript does. A title git cut short matches as a prefix, and each `${…}` in a template-literal title matches anything there, so `` `a forked child of a ${kind} WASIX program…` `` matches the rows it ran as. A title made only of placeholders is skipped.
1268
- - **suite** (`suite.mjs`, `reporter.mjs`) runs on the bench pool, without secrets. It takes the tag's tree with its own dependencies and Chromium, and replaces `dist/` with the **published npm tarball** (for a planted branch, its own `npm pack`). The test command runs in a session of its own (`detached`), and `suite.mjs` notes a `SIGINT` (`ignored` in its summary) instead of dying of it. That way a stray signal to the step's process group can't cut the run short, while a `SIGTERM` (a real cancel) still ends it. The log is written as the run goes, and a `SIGTERM` still writes the summary with the results so far (`exit: 'signal SIGTERM'`). A run that left only its `.jsonl` (the runner was lost) is read from that, as `exit: 'no summary'`. The NEGATIVE step runs even when the release suite's step failed. Before it reports, `suite.mjs` re-runs once just the test files that left no result (no rows, a cancelled row, or a file-level failure, such as a lost process), with a second `node --test` over those files, and merges their results (`retried`, `firstExit`, `unfinished` in the summary). That holds for the release suite and for each NEGATIVE run. A fix whose test passes on its baseline stays `no-negative`.
1269
- - The candidate job runs the pull request's whole `test/integration/` suite on its packed build.
1270
- - The base job runs `main`'s own suite on `main`'s packed build, then the NEGATIVE (#861): `main`'s tree, with all of the pull request's `test/**` changes applied (test files, helpers and fixtures), on `main`'s build. So a fix in `tools/` fails there just as a fix in `src/` does. A test file that fails to load on `main` (a module or export that only the pull request adds) leaves no result for its titles, which is `prev-not-run`, never a NEGATIVE.
1271
- - Each run uses the tree's own `npm test` command, with a reporter that records every test and subtest with its status and error.
1272
- - **kernel-compat** (`compat.mjs`) supplies the base-set and catalog evidence: homescoop's kernel-compat run for the release.
1273
- - Its reference kernel is the certified base, not slicc-bios's pin. cert-review passes `--reference <base>`, and `kernel-compat.yml` takes a `reference` input that it hands to homescoop's workflow (homescoop#504), recorded in compat.json. So it uses a dispatched run named `Kernel compat v<version> ref <base>` (kernel-compat has no run of its own after Release). Its `candidate` job checks that npm serves the version, or takes a packed tarball from another run (`run`, `artifact`, `sha256`, for PR builds `x.y.z-pr.N.M`), and prints `@ai-ecoverse/slicc-kernel@<version>`, which compat.mjs matches. It dispatches one when none has finished and waits up to 150 minutes; package reruns carry the same reference.
1274
- - It reads the run's report artifact, and **re-runs every regressing package once** (a dispatch with that `set`). The `set` names each one as `recipe@version`, the exact version that regressed, and compat.json records what the rerun `ran`. A rerun result for another version is a `rerun-mismatch`, counted as reproduced and never as a flake.
1275
- - A regression that doesn't reproduce is a `compat-flake`; one that does is a `compat-regression`. A crash that reproduces and doesn't happen on the reference kernel is a `compat-crash`, which record FAILs in code.
1276
- - A run that failed with no regression in its report is a `compat-incomplete`, missing evidence and not a regression. It names the shards that left no report (for example, a shard that lost its bench runner), and those shards' failed jobs are re-run once first. The same holds for the package rerun's own run, so a rerun that loses its runner is tried again before its missing result counts as reproduced.
1277
- - **review:** `findings.mjs` decides in code, per pull request:
1278
- - `negative` (a fix's test fails on the previous dist and passes on the release) or `passes` (a feature's test passes);
1279
- - otherwise a finding: `no-test`, `no-negative`, `fails-on-release`, `not-run`, `prev-not-run` or `no-titles`.
1280
-
1281
- It also diffs the two suites test by test (`regressed`, `newFailing`, `fixed`, `stillFailing`, `removed`), flags a suite run that didn't finish (`suite-incomplete`), and adds the kernel-compat findings. A release suite that didn't finish is a `missing-evidence` finding that always blocks `certified`, with the remedy `re-run the failed jobs`. That covers a signal, a timeout, a cut-off run, no results, or any test file without a result. Its base tests that didn't run are `unrun`, never `removed`. INCOMPLETE is not FAIL: record writes `cert-ci: INCOMPLETE <name>@<version> (<n> files without results: …; re-run the failed jobs) (ci: …; known: …)`, with the known issues as on a certified line, and the `cert-review` status is `error`, which means re-run, where `failure` means a real defect. A defect found in code (a compat crash) still FAILs. A base suite that is missing or didn't finish is a `missing-evidence` finding. It blocks `certified` in code (record FAILs it) unless the release suite is complete and all-pass and the delta removes or renames no test file and drops no test title (`dropped` per row). Either way the line ends with `base suite incomplete (<n> results)`. Claude, on Bedrock with the key only in that step, then reads those artifacts and `instructions.txt` (which has the roll-forward FAIL list verbatim) and writes `verdict.json` and `comment.md`. Its tools are reading files, `gh pr view`, and `gh issue list`/`view`. `gh issue create` is allowed only for a release on `main`; otherwise, and for an issue in another repository, it lists the issue under `elsewhere` for the cert lane to file.
1282
- - **record** (`record.mjs`), in code, with read-only permissions:
1283
- - It re-downloads the tarball and checks `dist.integrity` (sha512) and the sha256, the SLSA v1 provenance (the tarball is its subject; `release.yml`, `refs/heads/main`, a GitHub-hosted builder; commit = `gitHead` = the tag), `npm audit signatures`, and that the commit is on `main`. A problem there, a `compat-crash`, or a planted build is a FAIL whatever the review says.
1284
- - It adds the **known issues as of the release**: every issue labelled `known-issue` (open or closed) and every one in `known.json`, unless it was fixed before the release. An issue in another repository, whether listed or named by the reviewer, counts only while it is open. That holds when `known.json`'s `fixedIn` names an earlier or equal version, or the merged pull request that closed it is an ancestor of the tag, or it was closed (with no closing pull request) before the release commit. An issue closed as not planned or as a duplicate is dropped.
1285
- - The line is `certified @ai-ecoverse/slicc-kernel@<v> sha256=<…> (ci: <run>; known: …)`. Any test gaps from the findings (`no-test`, `no-negative`, `no-titles`, `not-run`) are appended in code as `test gaps: #<pr> <kind>`, so a fix without a NEGATIVE is never certified silently.
1286
-
1287
- ### Certification in the pull request (`cert-review`)
1288
-
1289
- `.github/workflows/cert-review-pr.yml` certifies a pull request before it merges, so `main`, and with it npm's `latest`, only gets certified changes. It reports as the `cert-review` commit status, a required check of `main`'s merge queue. Every same-repo pull request gets one (`not behaviour`, `draft: not reviewed`, the review's verdict, or the self-guard's hold). **A pull request from a fork gets none**, because fork code never reaches the pools or the key, so it can't enter the queue: only Lars merges one, after reading it.
1290
-
1291
- - **Which pull requests:** those that change shipped behaviour, by a conservative path filter (`behaviourFiles` in `lib.mjs`): `src/`, `build.mjs`, `package.json` and its lock, `tsconfig*.json`, `.releaserc*`, `tools/release-recover.mjs`, the workflows that build or publish (`release.yml`, `ci.yml`), and the reviewer's own code (`tools/cert-review/`, `cert-review*.yml`, `.github/runner.mjs`). **Main's code decides:** every job checks out the pull request's base and fetches only the trees under test, so the path filter, pack, suite, findings, the instructions and record all run from `main`. The merge group's lookup loads its tools from the protected branch itself (`merge_group.base_ref`), not from the group's `base_sha`, which can be the speculative commit of the group ahead and carry the reviewer changes of the pull requests queued before it; `base_sha` is only the lookup's input. A pull request that changes the reviewer is reviewed by `main`'s reviewer. The workflow file of a `pull_request` run still comes from the pull request, so a change to the reviewer is held by the **certification self-guard**, the shared composite action `slicc-shared-web/.github/actions/cert-self-guard` at a pinned sha. Its light `self-guard` job runs on every pull request event and every merge group. When the diff touches the reviewer (`.github/workflows/cert*.yml`, `tools/cert-review/**`, `.github/runner.mjs` and the guard's own paths), `cert-review` is red with `reviewer change: needs coordinator review (cert-review-approved on this head)` and no review runs. The label is the Kernel Coordinator's alone: they add `cert-review-approved` after reading the change, and the review then starts (on a pull request that isn't a draft). The approval is bound to the head's arrival on GitHub, and a push removes the label. A merge group that holds an unapproved reviewer change is red too. Node's glob `**` skips dot segments, so the guard's paths list the dot forms of each reviewer directory too (`REVIEWER_GLOBS` in `lib.mjs`), and the job's `cert` step reads the guard's receipt and fails closed: a reviewer path the globs still miss (any path under `scripts/cert/`, `tools/cert-review/` or the guard's directory, by prefix) without the label on that head is a FAIL, and a PASS without a receipt, or with a member that lacks its `paths` or `touched` list, is INCOMPLETE. If the guard doesn't finish, the status is an error that asks to re-run the failed jobs. **The residual:** the pull request's YAML still decides what runs, so the guard catches an accidental edit, not a deliberate rewrite of this workflow. The pull request that adds this workflow can't pass its own check, because its base has no tools yet. Any other pull request gets a green status that says `not behaviour: <files>`, so the skip is recorded.
1292
- - **A gate's silence never reads as a pass:** every tool a workflow gates on (`pr.mjs plan`, `same`, `lookup` and `notice`, `downstream.mjs`, `same.mjs`, `pack.mjs`, `delta.mjs`, `suite.mjs`, `compat.mjs`, `findings.mjs`, `record.mjs`) ends with one verdict line, `<tool>: PASS <what>` or `<tool>: FAIL <why>`, from the shared `verdict()` in `lib.mjs`. `tools/cert-review/gate.sh [--either|--evidence] <tool> <command>` runs a tool, passes its output through, and exits 70 with an error when the tool's last line starting `<tool>: ` isn't a PASS or FAIL verdict, so a tool replaced by `true`, an early exit 0 or an empty run fails its step. By default the step passes only on PASS with exit 0; a PASS with a non-zero exit, or a FAIL with exit 0, fails it. `--either` keeps the tool's own exit, for tools whose FAIL is evidence that record judges later (suite, compat). `--evidence` needs only the verdict, for the NEGATIVE run on main's tree, which is expected to fail. `suite: PASS` means exit 0, no failures and every expected file has results; `compat: PASS` means a run turned up with no regressions. `prompt.mjs` writes the review prompt to stdout, which goes into a step output, so it prints no verdict; a missing prompt leaves the review without `verdict.json`, and record marks that INCOMPLETE. The gate reads the tool's stdout, and no tool passes the pull request's output there: suite pipes the tests' output into its log file, and compat prints only its own lines, so a test that prints `suite: PASS` can't stand in for a silent suite. A tool that let such output through would need its verdict on a separate descriptor. Because the workflow of a pull request runs main's tools, the tools gain their verdicts first, and the workflows call them through `gate.sh` in the pull request after that.
1293
- - **Evidence comes by id from the job that made it:** the jobs that run the pull request's code (`pack candidate`, which builds it, and both `suite` legs, which run its tests) share the run's artifact store with the jobs that don't, and code in them can reach the runner's artifact token. So no consumer trusts a name or a pattern. Each producer job hands on its artifact's `artifact-id` and `artifact-digest` as job outputs, consumers download by `artifact-ids` with `digest-mismatch: error`, and no upload uses `overwrite`, so a second upload of a name fails instead of replacing it. The base build has its own job, `pack base`, which runs only main's code, so the candidate's job can't speak for it; it restores no Actions cache either, because the candidate job shares the run's cache scope (npm would refuse a tampered tarball against the lockfile's integrity anyway). `tools/cert-review/artifacts.mjs` lists the run's artifacts and fails when one in our namespaces (`kernel-tgz-`, `cert-review-`) wasn't uploaded by its producer, with that producer's id, name and digest, or when a producer's artifact is gone. `same` runs it on the two tarballs before it compares them, so the "not behaviour (identical dist)" skip can't be bought with a planted base tarball. `record` runs it on every artifact before it writes the line, and a failure there makes the record INCOMPLETE. **What this protects:** artifacts of jobs that run no pull-request code (the base build, the delta, kernel-compat, the same-dist evidence, the review's verdict and evidence) can't be forged or replaced. **What it doesn't:** the candidate's own artifacts, its tarball and both suites' results (the base leg runs the pull request's tests on main's tree, for the NEGATIVE), stay the pull request's by design, because code that runs on a runner can always lie about its own results. They're consumed by their exact names, at most one each. That's the same deliberate-malice residual as the workflow-rewrite one above.
1294
- - **Decide by what ships:** when a pull request's behaviour files are only `package.json` and/or `package-lock.json` (a Renovate devDependency bump, say), both sides are packed as usual, and then the `same dist` job (`same.mjs`, in code) compares the two tarballs before anything else runs. It checks the same paths, entry types (a symlink isn't a file) and modes. `package.json` must be equal once `devDependencies`, `version` and `gitHead` are removed, so `scripts` (they ship, a consumer's install runs `postinstall`, and `prepack`/`prepublishOnly` can make the published build differ from the packed one), runtime dependencies, `exports`, `bin`, `files` and `engines` must all match. Every other file must be byte-identical once each side's **exact** version string (build.mjs bakes it into `dist`) is replaced by one placeholder. A version string in one more place, or any looser match, is a difference. Source maps are compared as JSON without their `mappings`, whose columns the longer or shorter version string shifts; the code itself is compared byte for byte. If all of that matches, `cert-review` is green with `not behaviour (identical dist): <candidate sha256> = <base sha256> after normalising <v1>/<v2>`, and the suites, kernel-compat, the review and the record are skipped. `same.json` (uploaded as an artifact) names the rule and both tarballs' full sha256. Any difference, or a `same dist` job that doesn't finish, means the full review. The kernel has no runtime `dependencies`, since esbuild bundles everything into `dist`, so a devDependency that changes the product shows up as a byte difference. A test-only devDependency is checked by `ci / test`, on the pull request and on the merge group.
1295
- - **When:** every same-repo pull request that isn't a draft, on `opened`, `ready_for_review`, `reopened` and every push (`synchronize`). Draft status replaces the old `merge-ok` label: **mark a pull request ready = it gets reviewed, and merges on green**; a pull request held for anything stays a draft. A draft that changes shipped behaviour gets a red status, `draft: not reviewed, mark it ready for review`, and no review. The plan reads whether the pull request is a draft live from the API, not from the event: a push and `ready for review` within a second can leave only the push's run, whose payload still says draft (#1224, #1217). A draft flag it can't read is an `error`, never a review or a pass; `converted_to_draft` cancels a running review (one concurrency group per pull request) and sets that red status, so an older result on the same head can't satisfy the check. Each pull request has its own concurrency group, and a newer push cancels the older review. Labels other than `cert-review-approved` don't start or cancel anything. Fork code never reaches the pools or the key.
1296
- - **What:** the checkout is the event's default `refs/pull/N/merge`, the pull request merged onto `main` at review time, so a review is of what would land, without pushing to the branch. Both sides are packed in the same run, on a GitHub-hosted runner, with no secrets: the candidate (the merge ref, `x.y.z-pr.N.<run>`) and the base (its first parent, `main`). Then:
1297
- - the candidate's whole suite, and the base's;
1298
- - the pull request's new or changed test files on `main`'s tree, with the pull request's `test/**` applied, and `main`'s build (the NEGATIVE: a fix's test must fail on `main` and pass on the candidate);
1299
- - kernel-compat on the candidate's artifact (#812), against npm's `latest`;
1300
- - the review (Claude through `claude-code-action/base-action`, with the Bedrock key in that step only, no issue filing);
1301
- - and record, which re-hashes the candidate's artifact against the pack job's sha256.
1302
- - **The verdict** goes into the commit status on the pull request's head and a comment on the pull request. Certified is green. A FAIL is red with the line, and the pull request needs a fix. **INCOMPLETE** (a job that didn't succeed, no verdict, a suite without results) is red with `re-run the failed jobs`, never a silent pass. Main moving on after a review doesn't trigger another one; a push does.
1303
- - **Merge groups only look up.** On `merge_group`, the job lists the group's pull requests (its merge commits) and requires, for each one, a green status on the head being merged, or no behaviour change. Otherwise it's red, naming the pull request. Then it adds the group's downstream verdict. The group's `ci / test` covers the combined tree.
1304
- - **Downstream, once per merge group:** the `merge group pack` job packs the group's tree with `main`'s tools (`x.y.z-group.<run>`), and `downstream bios canary` calls slicc-bios's `canary.yml` at a pinned full sha (`b59afdf6…`, never a tag) with that tarball, bound by artifact id, digest and sha256, on the `sweep`, `guard-prompt`, `guard-lagging`, `k4-upgrade`, `k4-agent-modes` and `link-serve` shards, each run once, on the runner the `canary runner` job picks: `.github/runner.mjs`'s `decide()` from `main`, as `ci.yml` uses it, gives `vars.CERT_CANARY_RUNNER` only to a merge group whose entries are all same-repo PRs, and `ubuntu-latest` otherwise, without the variable, or if the decider fails (bios honours a runner only for merge groups and pushes to main in ai-ecoverse, so rolling back is unsetting the variable). The verdict line names the shards, or their count past 32 characters (`6 shards`), so a status description (140 characters) keeps the reason. `downstream.mjs` judges its result (`downstreamOf` in `lib.mjs`), and the lookup requires it. The `merge group` job reads that result as `result.json` from the canary's own artifact, downloaded by the call's `artifact-id` with `digest-mismatch: error`, not from the `result` output: that output can be up to 512 KB, and one environment variable holds 128 KB, so passing it that way stopped the step from starting at all (`Argument list too long`, runs 39166647175 and 39168823799). A missing or unreadable artifact leaves no result, so the group is INCOMPLETE:
1305
- - **PASS:** the candidate passes, `downstream: PASS bios … N pass`;
1306
- - **FAIL:** a regression, or a failing test (by the counts, even without its rows) is **not certified**, and the group is red;
1307
- - **INCOMPLETE** blocks too: no result, a result from another bios than the pinned one, a missing shard, a shard that exited without a failing test, a refused candidate, 0 tests run, or no test passed;
1308
- - **not judged:** every failing test fails on `main`'s own pins too (`both`), so the canary is broken on `main`, not by this group. That passes with a note, as do `both` rows next to a pass. Either way, `pr notice` opens one issue, `downstream: bios canary failing on main (<bios sha>)`, or comments on the open one, and the next group that passes closes it. Flaky rows are advisory.
1309
- - **No downstream waiver.** A downstream FAIL or INCOMPLETE in the merge group is not certified; fix the regression or bios's side first (the coordinator-only `downstream-override` label was removed with the 2026-10-19 cert model). A pull request's own certification line ends `downstream: merge group`, since its downstream verdict comes in the group.
1310
-
1311
- Not part of the run yet: the rest of bios's setup (the `rest` shard, the upgrade boot, the sudo rows under `enforce`). More shards plug in through the canary call's `shards`.
1257
+ ### CI and releases
1258
+
1259
+ `.github/workflows/ci.yml` is the whole CI and release: one caller of [slicc-shared-web's pipeline](https://github.com/ai-ecoverse/slicc-shared-web#pipeline), pinned to the release of the `@ai-ecoverse/slicc-shared-web` devDependency, so the workflow and the harness the tests use move together. `test/integration/workflows.test.mjs` holds it to that.
1260
+
1261
+ - **Pull requests and the merge queue:** `npm run lint` (Biome, TypeScript and the shared rules), then the integration tests (`npm test`) split by their timings across 4 shards on the pool, and on a pull request the Claude and Codex reviews, with the kernel's prompt in `review-prompt`. `ci / integration` and `ci / review` are the checks `main`'s merge queue requires. Renovate's pull requests get no review: their integration tests hold them.
1262
+ - **Releases:** every push to `main` runs semantic-release on a GitHub-hosted runner, which tags the commit, creates the GitHub release and publishes to npm through trusted publishing, with provenance from `.github/workflows/ci.yml` on `refs/heads/main`. There is no release workflow, no manual release and no certification after the merge: what reaches `main` went through the merge queue's `ci / integration` and the pull request's `ci / review`, so bios and homescoop can pin any release by its provenance. `fledgling.workflow` in `package.json` names `ci.yml`, and `npm run trust` registers it with npm.
1263
+ - **The packed tarball:** `packed-kernel.test.mjs` packs the package, installs the tarball on its own and runs homescoop's bash and coreutils through its Node entry, so a release that ships without a file its entries load fails CI, and `types-package.test.mjs` type-checks a consumer of its declarations.
1312
1264
 
1313
1265
  ## Test fixtures
1314
1266
 
@@ -7728,7 +7728,7 @@ function satisfies(version, range2) {
7728
7728
  }
7729
7729
 
7730
7730
  // src/version.ts
7731
- var KERNEL_VERSION = true ? "1.144.1" : "0.0.0-dev";
7731
+ var KERNEL_VERSION = true ? "1.145.1" : "0.0.0-dev";
7732
7732
  var OS_RELEASE = "6.0.0-slicc";
7733
7733
  function procVersion() {
7734
7734
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -8497,11 +8497,14 @@ function parseCrontab(text, path, system = false) {
8497
8497
  // src/cron/crontab.ts
8498
8498
  var CRONTAB_MAX = 256 * 1024;
8499
8499
  var TAGS_PATH = "/var/spool/cron/tags";
8500
+ var TAGS_VERSION = 1;
8500
8501
  async function readTags(fs) {
8501
8502
  const text = await fs.readFile(TAGS_PATH).catch(() => "");
8502
8503
  try {
8503
8504
  const parsed2 = JSON.parse(text);
8504
- return parsed2 && typeof parsed2 === "object" ? parsed2 : {};
8505
+ if (!parsed2 || typeof parsed2 !== "object") return {};
8506
+ const { v: _, ...tables } = parsed2;
8507
+ return tables;
8505
8508
  } catch {
8506
8509
  return {};
8507
8510
  }
@@ -8567,8 +8570,8 @@ var Crontabs = class {
8567
8570
  const kept = { ...all };
8568
8571
  if (Object.keys(table).length > 0) kept[path] = table;
8569
8572
  else delete kept[path];
8570
- const json5 = JSON.stringify(kept);
8571
- if (json5 === JSON.stringify(all)) return;
8573
+ const json5 = JSON.stringify({ v: TAGS_VERSION, ...kept });
8574
+ if (json5 === await fs.readFile(TAGS_PATH).catch(() => "")) return;
8572
8575
  const owner = owners ? { uid: 0, gid: 0, mode: 384 } : void 0;
8573
8576
  const fresh2 = `${TAGS_PATH}.${crypto.randomUUID()}.new`;
8574
8577
  await fs.writeFile(fresh2, json5, owner);
@@ -28175,6 +28178,14 @@ function foreignImports(bytes) {
28175
28178
  }
28176
28179
  return {};
28177
28180
  }
28181
+ var LIBC_MARK = /^wasix-sysroot \d{4}\.\d+\.\d+-(\d+)/;
28182
+ function libcVersion(module) {
28183
+ for (const section of WebAssembly.Module.customSections(module, "slicc.libc")) {
28184
+ const found = LIBC_MARK.exec(new TextDecoder().decode(section));
28185
+ if (found) return Number(found[1]);
28186
+ }
28187
+ return void 0;
28188
+ }
28178
28189
 
28179
28190
  // src/refused-command.ts
28180
28191
  var REFUSED_PATH = "/usr/lib/slicc/refused";
@@ -28727,6 +28738,14 @@ function cronPath(path) {
28727
28738
  (at) => path === at || path.startsWith(`${at}/`) || at.startsWith(path === "/" ? "/" : `${path}/`)
28728
28739
  );
28729
28740
  }
28741
+ var ETC_HOSTNAME_LIBC = 42;
28742
+ function readsEtcHostname(program) {
28743
+ return program.abi === "wasi" && (libcVersion(program.module) ?? 0) >= ETC_HOSTNAME_LIBC;
28744
+ }
28745
+ function setOrDrop(env, key, value2) {
28746
+ if (value2 === void 0) delete env[key];
28747
+ else env[key] = value2;
28748
+ }
28730
28749
  function baseName2(path) {
28731
28750
  return path.slice(path.lastIndexOf("/") + 1);
28732
28751
  }
@@ -29889,6 +29908,7 @@ var Launcher = class {
29889
29908
  const env = { ...expandDefaults(asked.target.env, seen), ...req.env };
29890
29909
  for (const key of asked.target.unset ?? []) delete env[key];
29891
29910
  delete env[TICKET_ENV];
29911
+ const own = env.HOSTNAME;
29892
29912
  if (asked.target.abi === "wasi") env.HOSTNAME = this.hostname;
29893
29913
  const planned = chosen(asked, env);
29894
29914
  const { target, args } = planned;
@@ -29899,6 +29919,7 @@ var Launcher = class {
29899
29919
  const caller = { pid: req.ppid ?? 0, uid: req.cred.euid, tag: req.tags?.[0] };
29900
29920
  if (this.integrity) await this.checkedSources(planned, caller);
29901
29921
  [program, exeId] = await Promise.all([this.program(target, env, caller), this.exeId(target)]);
29922
+ if (readsEtcHostname(program)) setOrDrop(env, "HOSTNAME", own);
29902
29923
  } catch (err) {
29903
29924
  await req.fds.closeAll();
29904
29925
  throw err;