The boot chain
From kernel handoff to steady state, and what happens at each point where something can go wrong.
Two entry vectors
They differ deliberately in how destructive they are.
| bare metal / VM | existing Linux host | |
|---|---|---|
| entry | initramfs whose /init is bootstrap.sh | curl numericalos.com/boot | sh |
| PID 1 | numinit becomes it | untouched — runs under existing init |
| to take PID 1 | automatic | requires explicit --take-pid1 |
Silently replacing PID 1 on someone's running server is destructive, so the convenient vector does not do it. That asymmetry is intentional.
Stage 1 — bootstrap.sh
1. mount /proc /sys /dev
2. ARCH=$(uname -m) -> canonical, via the generated ArchTarget table
3. resolve init artifact:
prefer numinit-$ARCH (static)
else busybox-static-$ARCH + numinit.sh
4. sha256 verify -- FAIL-CLOSED
5. load numos.state -> verify content hash
6. exec numinit as PID 1Step 2 uses a table generated from the graph, so an unrecognized
uname -m halts rather than guessing. Step 4 is the security
boundary: a hash mismatch halts with both the expected and the actual value,
because that message is what a human debugs from.
bootstrap.sh currently resolves the architecture and verifies a
state file it is handed; it does not fetch an artifact and does not exec
numinit. Artifact publication is later work. Stating this here
rather than letting the diagram imply otherwise.
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
| kind | behavior |
|---|---|
oneshot | runs synchronously; failure propagates to the phase |
target | synchronous grouping point |
longrun | backgrounded; 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_failure | result |
|---|---|
halt | boot stops, nonzero exit, stderr names the phase; later phases do not run |
degrade | boot continues; the node advertises degraded=1 so the coordinator stops scheduling onto it |
continue | boot 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 is meant to supervise
longrun units with exponential backoff and run
HealthPredicate probes on an interval, escalating fires to a swarm
once the network is up.
numos_main does not call them. Health predicates round-trip through
the state format but nothing executes them — interval supervision needs a
real event loop, which the shell path does not do well. That is the clearest
argument for the static-binary fast path.