NumericalOS

The boot chain

Acquire, verify, phase walk, steady state. Fail-closed at each boundary with a named reason on stderr.

Entry vectors

initramfs / VM / metalexisting Linux host
entry/init = bootstrap.shcurl …/boot | sh
PID 1numinitunchanged
PID 1 takeoverby design of the image--take-pid1 prints procedure only

Host installer never silently replaces init.

Stage 1 — bootstrap.sh

1. ARCH=$(uname -m) → canonical (generated ArchTarget table)
2. resolve init from manifest:
     F numinit-$ARCH  if published
     else numinit.sh
3. acquire artifact (local verified prefer; fetch if missing)
4. sha256 verify — fail-closed
5. verify numos.state C hash
6. exec numinit
(mount /proc /sys /dev: initramfs or host — not bootstrap)

Unknown arch halts. Hash mismatch reports expected and actual digests. Init selection is manifest-authoritative: the arch table answers “could a static build exist”; the manifest answers “is one published.” Conflating those announced static and fetched shell.

Offline / self-contained initramfs: images produced by the build skills set NUMOS_OFFLINE=1, NUMOS_PREFIX=/opt/numericalos, and pre-populate that prefix with numinit.sh, manifest.txt, and a verified numos.state (often also at /etc/numos.state). /init is a thin wrapper that exports those variables then execs packaged bootstrap.sh. Without offline mode, bootstrap tries to refresh the manifest over the network and will halt if no HTTP client is available.

NUMOS_OFFLINE=1 refuses fetch. Tampered local copy: halt, leave file (evidence). Failed fetch: delete partial. Online hosts still prefer a verified on-disk artifact when present.

No static numinit binaries are published yet. The manifest currently carries only the shell floor, so every architecture resolves to numinit.sh. Building the per-arch static binaries needs a Zig toolchain and is separate work; the resolution logic that will use them is in place and tested.

Stage 2 — numinit as PID 1

It walks BootPhase records by ordinal. Within each phase it resolves the unit DAG from requires and after edges, then runs units in dependency order.

The resolver is iterative — Kahn's algorithm — and that is not a style preference. POSIX shell has no local, so every variable in a function is global; a recursive resolver's recursive call clobbers its caller's loop variable and returns the wrong unit. The iterative form has no such hazard.

Unit kinds

kindbehavior
oneshotruns synchronously; failure propagates to the phase
targetsynchronous grouping point
longrunbackgrounded; the phase continues once it starts

A backgrounded longrun unit's failure is not visible to its phase's failure policy — nothing observes its exit code. That matches how real supervisors behave, and it is worth knowing before you write a state that depends on the opposite.

Failure policy

on_failureresult
haltboot stops, nonzero exit, stderr names the phase; later phases do not run
degradeboot continues; the node advertises degraded=1 so the coordinator stops scheduling onto it
continueboot continues, logged, not degraded

degrade is the interesting one. A partially broken node stays in the topology and says so, rather than dropping out entirely or pretending to be healthy.

A note on halting from shell

Fail-closed in POSIX shell has a trap that this project fell into and had to dig out of. exit inside a command substitution terminates only the subshell. A helper that calls numos_die from inside $(...) prints its alarming message to stderr, and the caller then continues with exit status 0.

Two conditions — a dependency cycle and a dangling unit reference — "booted successfully" this way while printing numos: HALT:. Every such substitution is now captured to a variable and status-checked, and the tests for those paths assert on exit status, not on the presence of the message, because the broken version emitted the message too.

Steady state

After the phase walk, numinit enters a tick loop. This is what makes it an init system rather than a boot script — and what makes it a valid PID 1 at all, since a PID 1 that returns panics the kernel. In a real boot the loop never exits.

Each tick does three things:

Escalation never blocks the loop. An on_fire swarm reference is appended to a queue in the runtime directory and drained later. An offline machine has to keep supervising itself, so reachability is never on the critical path of a tick.

No control socket. numctl status named-halts. Floor implements identity-init, capability, join (local state + optional coordinator POST), and run-op as a dispatch edge. Not a query API for a running numinit.

Observed under QEMU

Self-init profile (graph-resolved offline distro) on x86_64 under QEMU 10.2.2 — both kernel+initrd and hybrid ISO/GRUB serial (-cdrom):

numos: arch x86_64
numos: init numinit.sh (shell floor)
numos: state verified
numos: boot complete degraded=0
numos: steady state entered

ISO chain observed: SeaBIOS → El Torito → GRUB 2.06 → kernel → initramfs → /init. Serial entry must use only console=ttyS0 (last console= becomes /dev/console). See what is proven and docs/observations/2026-08-09-self-init-iso-cdrom.md.

Fleet profile on the same emulator still ends degraded=1 when net/clock/join units lack eth0/ntpd/coordinator — expected, not a packer failure.