@ai-ecoverse/slicc-kernel 1.141.0 → 1.141.2

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
@@ -1121,7 +1121,7 @@ A character device (`0666`), there in every `users` mode, through which programs
1121
1121
 
1122
1122
  ### `/proc`
1123
1123
 
1124
- Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `exe`, a link to the file the process was started from: the glue or `.wasm` that ran, as resolved (a copy run by its own path names the copy, a symlink its target) (#1264). For WASI and WASIX programs, as for Emscripten ones, `stat`, `open` and reading `/proc/self/exe` act on the program file, while `lstat` and `readlink` show the link (#1273). As on Linux without ptrace access, reading another process's `exe` link is `EACCES` unless the reader is root or its uid and gid match all of the target's real, effective and saved ids; `cmdline` and `status` stay readable, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client. A process that exits while a program reads the `/proc` directory doesn't fail or cut short the listing: its pid is still listed, and opening its files fails with `ENOENT`, which `ps`, `pgrep` and `pidof` skip, as on Linux; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. `/proc/version` names the kernel, Linux-shaped, with the slicc-kernel version that built it: `Linux version 6.0.0-slicc (slicc-kernel <version>) #1 SMP PREEMPT_DYNAMIC`. `/proc/sys/kernel/osrelease` is `6.0.0-slicc` and `/proc/sys/kernel/ostype` is `Linux`. The version is the one npm publishes, set when the package is built (`0.0.0-dev` when run from the sources). A JS program reads it as `ctx.kernel.version`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`; a process with a tag, see `kernel.run`, also has `Tag:\t<tag>` after `Gid:`, shown to root and to the same uid only, with newlines, tabs and carriage returns written `\n`, `\t` and `\r`) and each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times are measured per process, its threads and the images it exec'd included: time a worker spends waiting for the kernel's answer is system time (at most 20 ms of each call; the rest of a longer one, such as a write into a full pipe, counts as blocked), time it spends blocked (a read, a `poll` or `select` that waits, a sleep, `wait`, a futex, while stopped) is neither, from the moment the call actually starts to wait (a call that could wait but answers at once, such as a `select` with a zero timeout or a ready descriptor, a `waitpid` with `WNOHANG`, or a futex wait whose value has already changed, is system time), and the rest is user time; a process's times stop when it ends, a call still in flight then adds nothing, so a loop that never calls the kernel shows as user time while it runs; they show in `/proc/<pid>/stat`, `ps` and `ps()`, and the times of the children a process has waited for (theirs and their own waited-for children's, as POSIX counts them) show as its `cutime` and `cstime` in `/proc/<pid>/stat`; a stopped process is `T` there, in `/proc/<pid>/status` (`T (stopped)`) and in `ps()`; a WASI or WASIX program's `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` (`clock_time_get`) count only the time its thread runs, its own waits and blocked kernel calls left out (a sleep is not CPU time), per thread for now, so a threaded program's process clock is its calling thread's; and `/proc/stat` counts every process of the kernel, the reaped ones included, and `/proc/loadavg` averages the processes not blocked, sampled every 5 seconds. A JS program's time isn't measured yet. `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) is a placeholder. A process that isn't root sees only its own user's processes (see [Users](#users)). WASI and WASIX programs see the same `/proc`, read-only (opening a file in it for writing, or linking one of its files elsewhere, fails with `EROFS`), from every thread, also through a symlink that points into it (where every change, `mkdir`, `rename`, `unlink`, `chmod`, a hard link from it or into it, and so on, fails too: with `EROFS` when the root file system has a physical `/proc`, which stays hidden and untouched, and otherwise with the `ENOENT` of the missing directory, since that check is only paid for when a physical `/proc` exists; with one, a symlink whose target passes `..` after a file or a missing name, such as `/f/../proc`, fails with `ENOTDIR` or `ENOENT`, as on Linux), and `/` lists it, so a WASI program gets a preopen for it; `/proc/self` is the program's own pid there too (the pid an exec'd program took over). WASI and WASIX programs also have `/proc/<pid>/task/<tid>/stat` for each of a process's threads (the main thread is tid 1), whose state is `S` while the thread is blocked in the kernel (a `sem_wait`, a blocking read, a sleep, a futex) and `R` otherwise (`T` and `Z` as for the process). JS programs read the same `/proc` through `ctx.fs` and `ctx.open` (see [JS programs](#js-programs)). Neither has `/proc/<pid>/fd` yet.
1124
+ Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `exe`, a link to the file the process was started from: the glue or `.wasm` that ran, as resolved (a copy run by its own path names the copy, a symlink its target) (#1264). For WASI and WASIX programs, as for Emscripten ones, `stat`, `open` and reading `/proc/self/exe` act on the program file, while `lstat` and `readlink` show the link (#1273). A symlink to it is followed the same way, and once the program's name is moved away or holds another file, `exe` no longer opens it: `readlink` ends in ` (deleted)` and `stat` and `open` are `ENOENT` (#1317). As on Linux without ptrace access, reading another process's `exe` link is `EACCES` unless the reader is root or its uid and gid match all of the target's real, effective and saved ids; `cmdline` and `status` stay readable, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client. A process that exits while a program reads the `/proc` directory doesn't fail or cut short the listing: its pid is still listed, and opening its files fails with `ENOENT`, which `ps`, `pgrep` and `pidof` skip, as on Linux; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. `/proc/version` names the kernel, Linux-shaped, with the slicc-kernel version that built it: `Linux version 6.0.0-slicc (slicc-kernel <version>) #1 SMP PREEMPT_DYNAMIC`. `/proc/sys/kernel/osrelease` is `6.0.0-slicc` and `/proc/sys/kernel/ostype` is `Linux`. The version is the one npm publishes, set when the package is built (`0.0.0-dev` when run from the sources). A JS program reads it as `ctx.kernel.version`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`; a process with a tag, see `kernel.run`, also has `Tag:\t<tag>` after `Gid:`, shown to root and to the same uid only, with newlines, tabs and carriage returns written `\n`, `\t` and `\r`) and each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times are measured per process, its threads and the images it exec'd included: time a worker spends waiting for the kernel's answer is system time (at most 20 ms of each call; the rest of a longer one, such as a write into a full pipe, counts as blocked), time it spends blocked (a read, a `poll` or `select` that waits, a sleep, `wait`, a futex, while stopped) is neither, from the moment the call actually starts to wait (a call that could wait but answers at once, such as a `select` with a zero timeout or a ready descriptor, a `waitpid` with `WNOHANG`, or a futex wait whose value has already changed, is system time), and the rest is user time; a process's times stop when it ends, a call still in flight then adds nothing, so a loop that never calls the kernel shows as user time while it runs; they show in `/proc/<pid>/stat`, `ps` and `ps()`, and the times of the children a process has waited for (theirs and their own waited-for children's, as POSIX counts them) show as its `cutime` and `cstime` in `/proc/<pid>/stat`; a stopped process is `T` there, in `/proc/<pid>/status` (`T (stopped)`) and in `ps()`; a WASI or WASIX program's `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` (`clock_time_get`) count only the time its thread runs, its own waits and blocked kernel calls left out (a sleep is not CPU time), per thread for now, so a threaded program's process clock is its calling thread's; and `/proc/stat` counts every process of the kernel, the reaped ones included, and `/proc/loadavg` averages the processes not blocked, sampled every 5 seconds. A JS program's time isn't measured yet. `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) is a placeholder. A process that isn't root sees only its own user's processes (see [Users](#users)). WASI and WASIX programs see the same `/proc`, read-only (opening a file in it for writing, or linking one of its files elsewhere, fails with `EROFS`), from every thread, also through a symlink that points into it (where every change, `mkdir`, `rename`, `unlink`, `chmod`, a hard link from it or into it, and so on, fails too: with `EROFS` when the root file system has a physical `/proc`, which stays hidden and untouched, and otherwise with the `ENOENT` of the missing directory, since that check is only paid for when a physical `/proc` exists; with one, a symlink whose target passes `..` after a file or a missing name, such as `/f/../proc`, fails with `ENOTDIR` or `ENOENT`, as on Linux), and `/` lists it, so a WASI program gets a preopen for it; `/proc/self` is the program's own pid there too (the pid an exec'd program took over). WASI and WASIX programs also have `/proc/<pid>/task/<tid>/stat` for each of a process's threads (the main thread is tid 1), whose state is `S` while the thread is blocked in the kernel (a `sem_wait`, a blocking read, a sleep, a futex) and `R` otherwise (`T` and `Z` as for the process). JS programs read the same `/proc` through `ctx.fs` and `ctx.open` (see [JS programs](#js-programs)). Neither has `/proc/<pid>/fd` yet.
1125
1125
 
1126
1126
  The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#users)). While the root has none of them, or no `/etc/mtab`, reading them gets per-process ones: root is uid 0 and the realm user, uid 1000, is named after `USER`, or `web_user` (Emscripten's default) without one, so it matches `$USER`, with `HOME` as its home, so `id`, `whoami`, `ls -l` and `ps` show names. Nothing is written to OPFS, and a real file there wins.
1127
1127
 
@@ -1233,11 +1233,7 @@ Releases are cut by semantic-release on every push to `main`, in two jobs, and o
1233
1233
 
1234
1234
  ### Certification (`cert-review`)
1235
1235
 
1236
- 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. A release is then **verified on npm** (`release / verify on npm` in `release.yml`, `tools/cert-review/verify.mjs`), in code, for every tag the run made:
1237
-
1238
- - It waits up to an hour for npm to serve the version (an accepted publish can take a while to appear; 1.125.4 took 55 min). `release-recover` publishes a version it has to recover with a plain `npm publish --provenance` from the folder (so the packument keeps `gitHead`) and logs npm's own exit code and output; it fails only on npm's own non-zero exit, and npm's "cannot publish over the previously published version" on a re-run counts as already done. Whether npm serves the version is this job's to check. A version npm still doesn't serve after the hour, and doesn't list, while a newer one of ours is its `latest`, ends with `verify: SKIPPED <name>@<version>: never served by npm; superseded by <latest>` (the gate takes SKIPPED like PASS); only after the whole wait, since a slow version can appear late and must not go unverified. SKIPPED writes no certified line, so nothing downstream reads it as certified; if npm serves a skipped version later, the coordinator's merge watchdog notes it within 10 min (any version that appears below npm's `latest`), and the coordinator re-runs `release / verify on npm` in that version's Release run for the normal verdict, the certified line or the supply-chain alert. One npm still doesn't serve, with nothing newer or a version list it can't read, is a "not served yet, re-run" failure, never a supply-chain alert. It then downloads the tarball and checks `dist.integrity` (sha512), the SLSA v1 provenance (the tarball's sha512 is its subject; `release.yml` on `refs/heads/main`, a GitHub-hosted builder; `gitCommit` = `gitHead` = the tag, on `main`), and `npm audit signatures`. npm serves a version's attestations a little after the version itself, so a 404 from the attestations, a missing `dist.attestations`, an `npm view` or network error while looking for them, or `npm audit signatures`' E404 on them, is retried within the same hour: it's a supply-chain alert only once the version has been served for 30 min, its packument has stopped changing for 10, and the attestations still answer 404, or the hour has run out. Any other status, or attestations that don't match, fail at once. A version that `release-recover.mjs` skipped because npm's `latest` is newer (#837) has nothing to verify.
1239
- - It then writes `certified @ai-ecoverse/slicc-kernel@<v> sha256=<64 hex> (covers <first>–<v>; ci: <run>; pr-certified: <each merged pull request's cert-review at its merge>; known: <open known-issue issues>)` into the GitHub release's notes, between `<!-- cert-verify -->` markers. homescoop's ci reads exactly that prefix and re-hashes the npm tarball against it. `covers` starts after the last version npm serves, so a skipped version is covered by the next. Notes that already carry the identical line stay unchanged. A pull request's `cert-review` is the newest finished one (success, failure or error) on its head at or before its merge, or the newest pending one if none had finished. A re-review that a later `ready_for_review`, `reopened` or `cert-review-approved` event starts on the same head, still pending at the merge or ending after it, doesn't change what the merge queue saw, and a status posted after the merge can't change a release's line.
1240
- - A mismatch writes no line. The job is red with a `supply-chain alert`, and so is a line already in the notes with another sha256.
1236
+ 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.
1241
1237
 
1242
1238
  The tools in `tools/cert-review/`:
1243
1239
 
@@ -1260,14 +1256,13 @@ The tools in `tools/cert-review/`:
1260
1256
  - 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.
1261
1257
  - 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.
1262
1258
  - 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.
1263
- - **verify** (`verify.mjs`) is the `release / verify on npm` job above.
1264
1259
 
1265
1260
  ### Certification in the pull request (`cert-review`)
1266
1261
 
1267
1262
  `.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.
1268
1263
 
1269
1264
  - **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.
1270
- - **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`, `verify.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.
1265
+ - **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.
1271
1266
  - **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.
1272
1267
  - **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.
1273
1268
  - **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.
@@ -7128,7 +7128,7 @@ function satisfies(version, range2) {
7128
7128
  }
7129
7129
 
7130
7130
  // src/version.ts
7131
- var KERNEL_VERSION = true ? "1.141.0" : "0.0.0-dev";
7131
+ var KERNEL_VERSION = true ? "1.141.2" : "0.0.0-dev";
7132
7132
  var OS_RELEASE = "6.0.0-slicc";
7133
7133
  function procVersion() {
7134
7134
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -8444,6 +8444,8 @@ var CronService = class {
8444
8444
  }
8445
8445
  }
8446
8446
  async pass() {
8447
+ if (this.wake.signal.aborted) this.wake = new AbortController();
8448
+ const wake = this.wake;
8447
8449
  let until;
8448
8450
  try {
8449
8451
  until = await this.tick();
@@ -8452,8 +8454,7 @@ var CronService = class {
8452
8454
  until = this.clock.now() + WAKE_MS;
8453
8455
  }
8454
8456
  if (this.stopping.signal.aborted) return;
8455
- if (this.wake.signal.aborted) this.wake = new AbortController();
8456
- await this.clock.sleep(until, this.wake.signal);
8457
+ await this.clock.sleep(until, wake.signal);
8457
8458
  }
8458
8459
  async provision() {
8459
8460
  const { fs, owners } = this.host;
@@ -13600,6 +13601,10 @@ function mayTrace(viewer, target) {
13600
13601
  const uid = of.ruid === by.euid && of.euid === by.euid && of.suid === by.euid;
13601
13602
  return uid && of.rgid === by.egid && of.egid === by.egid && of.sgid === by.egid;
13602
13603
  }
13604
+ function fileIdentity(ino, mtimeMs) {
13605
+ if (ino === void 0 || mtimeMs === void 0 || !Number.isFinite(mtimeMs)) return void 0;
13606
+ return `${ino}:${Math.trunc(mtimeMs)}`;
13607
+ }
13603
13608
 
13604
13609
  // src/fs/pages.ts
13605
13610
  var PAGE = 64 * 1024;
@@ -14160,12 +14165,7 @@ var ProcTree = class {
14160
14165
  const exe = () => this.info(Number(top))?.exe === void 0 ? [] : ["exe"];
14161
14166
  return { kind: "dir", names: () => [...PID_NAMES, ...exe(), "task"] };
14162
14167
  }
14163
- if (leaf === "exe") {
14164
- const info = this.info(Number(top));
14165
- if (info?.exe === void 0) return void 0;
14166
- const denied2 = !mayTrace(this.ownInfo(), info);
14167
- return { kind: "link", target: info.exe, ...denied2 ? { denied: denied2 } : {} };
14168
- }
14168
+ if (leaf === "exe") return this.exe(Number(top));
14169
14169
  if (!PID_NAMES.includes(leaf)) return void 0;
14170
14170
  return {
14171
14171
  kind: "file",
@@ -14177,6 +14177,13 @@ var ProcTree = class {
14177
14177
  }
14178
14178
  };
14179
14179
  }
14180
+ exe(pid) {
14181
+ const info = this.info(pid);
14182
+ if (info?.exe === void 0) return void 0;
14183
+ const denied2 = !mayTrace(this.ownInfo(), info);
14184
+ const id2 = info.exeId === void 0 ? {} : { id: info.exeId };
14185
+ return { kind: "link", target: info.exe, ...denied2 ? { denied: denied2 } : {}, ...id2 };
14186
+ }
14180
14187
  ownInfo() {
14181
14188
  const processes2 = this.listing().processes;
14182
14189
  return processes2.find((p) => p.tid === this.pid) ?? processes2.find((p) => p.pid === this.pid);
@@ -14242,10 +14249,18 @@ var ProcTree = class {
14242
14249
  return node.names();
14243
14250
  }
14244
14251
  escape(path) {
14252
+ return this.escapeTo(path)?.target;
14253
+ }
14254
+ escapeTo(path) {
14245
14255
  const node = this.node(this.parts(path, true));
14246
14256
  if (node?.kind !== "link" || !node.target.startsWith("/")) return void 0;
14247
14257
  if (node.denied) throw syncError("EACCES", path);
14248
- return node.target;
14258
+ return node.id === void 0 ? { target: node.target } : { target: node.target, id: node.id };
14259
+ }
14260
+ linkTo(path) {
14261
+ const node = this.node(this.parts(path, false));
14262
+ if (node?.kind !== "link" || node.id === void 0 || node.denied) return void 0;
14263
+ return { target: node.target, id: node.id };
14249
14264
  }
14250
14265
  readlink(path) {
14251
14266
  const node = this.find(path, false);
@@ -27927,8 +27942,14 @@ function blobModuleUrl(source) {
27927
27942
  const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
27928
27943
  return { url, revoke: () => URL.revokeObjectURL(url) };
27929
27944
  }
27945
+ function exeIdOf(exeId) {
27946
+ return exeId === void 0 ? {} : { exeId };
27947
+ }
27930
27948
  function describe(req, tty) {
27931
- const exe = req.exe === void 0 ? {} : { exe: req.exe };
27949
+ const exe = {
27950
+ ...req.exe === void 0 ? {} : { exe: req.exe },
27951
+ ...req.exeId === void 0 ? {} : { exeId: req.exeId }
27952
+ };
27932
27953
  return { argv: [req.argv0, ...req.args], tty, started: Date.now(), ...exe };
27933
27954
  }
27934
27955
  function linksOf2(req) {
@@ -29074,6 +29095,15 @@ var Launcher = class {
29074
29095
  const optional = `${root}-names${beside.slice(root.length)}`;
29075
29096
  return await this.base.exists(optional) ? optional : void 0;
29076
29097
  }
29098
+ exeId(target) {
29099
+ if (target.abi !== "wasi" || (target.copy ?? target.glue) !== target.wasm) {
29100
+ return Promise.resolve({});
29101
+ }
29102
+ return this.base.stat(target.wasm).then(
29103
+ (st) => exeIdOf(fileIdentity(st.ino, st.mtime.getTime())),
29104
+ () => ({})
29105
+ );
29106
+ }
29077
29107
  async program(target, env, caller) {
29078
29108
  if (target.abi === "js" && target.wasm === SUDO_PATH) {
29079
29109
  return { abi: "js", glue: SUDO_SOURCE, path: SUDO_PATH };
@@ -29121,11 +29151,12 @@ var Launcher = class {
29121
29151
  const planned = chosen(asked, env);
29122
29152
  const { target, args } = planned;
29123
29153
  let program;
29154
+ let exeId;
29124
29155
  await this.pnpmReady();
29125
29156
  try {
29126
29157
  const caller = { pid: req.ppid ?? 0, uid: req.cred.euid, tag: req.tag };
29127
29158
  if (this.integrity) await this.checkedSources(planned, caller);
29128
- program = await this.program(target, env, caller);
29159
+ [program, exeId] = await Promise.all([this.program(target, env, caller), this.exeId(target)]);
29129
29160
  } catch (err) {
29130
29161
  await req.fds.closeAll();
29131
29162
  throw err;
@@ -29144,6 +29175,7 @@ var Launcher = class {
29144
29175
  program,
29145
29176
  argv0: target.argv0,
29146
29177
  exe: target.copy ?? target.glue,
29178
+ ...exeId,
29147
29179
  args,
29148
29180
  env,
29149
29181
  cwdSlot