@ai-ecoverse/slicc-kernel 1.106.3 → 1.107.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
@@ -481,7 +481,7 @@ In the browser, the page serves the uplink to the kernel worker over the same po
481
481
  - `reset(code)` (default `ECONNRESET`) breaks the connection both ways: writes the peer has not read fail, the program's next read fails with `ECONNRESET`, and its writes with `EPIPE`.
482
482
  - `closed` resolves when the kernel closes the connection.
483
483
 
484
- `fakeCronClock(start?, utc?)` is a clock for the kernel's cron to run on in tests, so a test moves time instead of waiting for it. It starts at `start` (default 2026-01-01T00:00Z) and reads schedules in UTC unless `utc` is `false`. `now()` is the fake time. `sleep(until, signal)` resolves when `advance(ms)` or `set(ms)` moves time to `until` or past it, or when `signal` aborts. `sleepers` counts the sleeps still waiting. `flush()` resolves once every sleeper that a time move woke has gone back to sleep for a later time, which is the point where a scheduler has done what was due. It never resolves while nothing sleeps.
484
+ `fakeCronClock(start?, utc?)` is a clock for the kernel's cron to run on in tests, so a test moves time instead of waiting for it. It starts at `start` (default 2026-01-01T00:00Z) and reads schedules in UTC unless `utc` is `false`. `now()` is the fake time. `sleep(until, signal)` resolves when `advance(ms)` moves time to `until` or past it, or when `signal` aborts. `set(ms)` sets the wall clock, forward or back, and wakes every sleeper, as a scheduler's next periodic wake would after a real clock change. `sleepers` counts the sleeps still waiting. `flush()` resolves once every sleeper that a time move woke has gone back to sleep for a later time, which is the point where a scheduler has done what was due. It never resolves while nothing sleeps.
485
485
 
486
486
  ### Browser automation (CDP)
487
487
 
@@ -757,6 +757,22 @@ With `users: 'enforce'` the kernel checks every file call that reaches it agains
757
757
  - **Symlinks:** a command `sudo` started, and every process it starts in turn, follows a symlink only when root or the target user owns it: Linux's `protected_symlinks`, applied in every directory rather than only in sticky world-writable ones. Any other link is `EACCES`, and the denial names the link. That holds in every runtime: in the kernel's own path walk (JS programs' `ctx.fs` and `ctx.open`, WASI paths, `exec` and `cd` by path), in an Emscripten program's lookups (where the denial's op is `follow`), and where a WASI program resolves `..` itself. So a link the caller put where a rule's file or directory is expected (`/tmp/k4l` → `/etc/fnox`, or `config.toml` → `/etc/fnox/config.toml` in the caller's own directory) leads nowhere. `readlink`, `ls -l`, `lstat`, and removing or renaming a link still work, since they don't follow it.
758
758
  - **Cost:** with `users: 'off'`, and with owners on for programs that don't call `sudo`, nothing is added to any call. The symlink check costs those processes one owner compare per link in the kernel's walk (the owner is already loaded) and one kernel call per link an Emscripten program follows; other processes pay nothing for it.
759
759
 
760
+ ### Cron
761
+
762
+ The kernel runs scheduled jobs itself, with no daemon process: a cron service starts with the kernel and stops with `terminate()`. It reads `/etc/crontab` and the per-user tables in `/var/spool/cron/crontabs/<user>`, and runs each due job as `$SHELL -c '<command>'` (`/bin/bash` unless the table sets `SHELL`) as the table's user, in that user's home (`/` if the home is missing), with no terminal and `/dev/null` as standard input.
763
+
764
+ - **Format:** Vixie's. Five fields (minute, hour, day of month, month, weekday) with lists, ranges, steps and the names `jan`–`dec` and `sun`–`sat` (7 is Sunday too), or one of `@yearly`/`@annually`, `@monthly`, `@weekly`, `@daily`/`@midnight`, `@hourly` and `@reboot`. As in cronie, when the day of month or the weekday starts with `*`, a day must match both fields; when both are restricted, it may match either. `/etc/crontab` has a user field after the schedule; a user's table doesn't. `NAME=value` lines set the environment for the jobs after them, `#` lines are comments, and `# name: <id>` on the line above a job gives it a stable id (letters, digits, `_`, `.`, `-`), which names its log. Without one, the id is a hash of the table, the user, the schedule and the command. `%` is not special. Times are the kernel's local time.
765
+ - **Environment:** only what a login of that user gets, and nothing from the page or the embedder's `env`. That is `HOME`, `USER`, `LOGNAME`, `PATH`, `PNPM_HOME`, `NPM_CONFIG_PREFIX`, the kernel's network settings (`http_proxy`, `SSL_CERT_FILE`, …), `SHELL`, `PWD`, the table's `NAME=value` lines, and `SLICC_CRON=1`, `SLICC_CRON_JOB=<id>`, `SLICC_CRON_DUE=<ISO time of the due run>` and `SLICC_CRON_MISSED=<runs folded into this one>`. An `agent send` from a job therefore knows it was scheduled.
766
+ - **Missed runs:** when the kernel wasn't running (a closed tab) or time jumped forward, every due run since the last one is folded into **one** run, with `SLICC_CRON_MISSED` saying how many more were due (counted up to 100 000). The next run is then computed from now. A clock set back more than two minutes runs nothing until the next due time after the new time.
767
+ - **At most once:** the service writes each due run to `/var/spool/cron/stamp` before it starts the job. A kernel that ends mid-job (a reload, a crash) never runs that due again; a stamp that can't be read counts every job as run just now.
768
+ - **`@reboot`** runs once per kernel boot, when that kernel first fires jobs.
769
+ - **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`.
770
+ - **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.
771
+ - **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.
772
+ - **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.
773
+ - **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.
774
+ - **On Node,** `createNodeKernel({ cron: { clock } })` runs the service on a clock of your own, such as `fakeCronClock()` from `@ai-ecoverse/slicc-kernel/testing`. Kernels on the same `root` in one process share one lock. That uses Node's `navigator.locks`; on a Node without it, every kernel fires on its own, so run one kernel per root there.
775
+
760
776
  ### `/dev/sealed`
761
777
 
762
778
  A character device (`0666`) through which a program talks to the embedder's `sealed` hook (`createKernel({ sealed })`), the secrets vault on the page. Emscripten programs, WASI programs (`std::fs`) and JS programs (`ctx.open`, `ctx.sync.open`, listed by `ctx.fs.readdir('/dev')`) all open it, through the same kernel device, with the same framing, errnos and stamp: a JS program's stamp is built in the kernel from its own process, as for any other. The kernel holds the mode a descriptor was opened with, in every runtime, for its `dup`s and for the copies children inherit through fork, spawn and exec: one opened read-only refuses writes with `EBADF`, and one opened write-only refuses reads. Emscripten programs see it in `/dev` and open it by any path that leads to it (`./sealed` from `/dev`, `..`, a symlink to it or to `/dev`), and `fstat` and `/proc/self/fd` show it as `/dev/sealed`, a character device, after fork and exec too; WASI and JS programs open it by a path with no symlink on the way, and WASI programs by one without `..`.