The state format
numos.state is the single artifact numinit
reads at boot. It is line-oriented, not JSON, and that choice is load-bearing.
Why not JSON
The design originally specified JSON. Implementation killed it: the boot floor
is POSIX shell, and shell cannot parse JSON without a parser. Shipping one
contradicts the whole premise of a smallest bootstrap — so the runtime
format became line-oriented and read-native, and JSON survives only
as a web view for tooling that wants it.
The result is a format a shell can consume with grep and
cut and nothing else.
The six record types
V <version> C <sha256-of-body> A <canonical> <aliases_csv> <zig_triple|-> <busybox_variant> <artifact_sha256|-> P <ordinal> <name> <on_failure> <required_units_csv|-> U <name> <kind> <restart> <backoff_ms> <backoff_max_ms> <arch_mask|-> <health_probe|-> <requires|-> <after|-> <exec...> X <name> <interval_s> <threshold> <local_action> <on_fire|-> <probe...>
V C A P U X; records are sorted
within each block.Three rules make it safe to read from shell:
- Field positions are a wire contract. The shell reads them
with
cut -d' ' -fN. An off-by-one silently reads the wrong attribute, so the field order is fixed and tested from both sides. - Anything that can contain spaces goes last.
execandprobeare the final field on their line, which is what lets them hold arbitrary commands. -is the null sentinel. Empty lists and absent values both render as a single dash.
Because a space would shift every subsequent field, export-time validation rejects any non-final field containing one. A unit whose name has a space is not a formatting inconvenience — it is a state file that parses as something entirely different, and it is refused before it can be written.
The content hash
The C record carries the SHA-256 of every other line. Both
verifiers — the Python one at build time and the shell one at boot —
recompute it and refuse a mismatch.
Getting those two to agree turned out to be the subtle part. They must have
identical opinions about what the body is: whether blank lines count,
whether a second C record is legal, and whether trailing content
after the hash is ignored or fatal. Two verifiers that disagree produce a file
one accepts and the other rejects, which is worse than either being wrong
alone. They agree now, and tests assert it from both directions.
Determinism
The same graph state must produce a byte-identical file. Two things enforce
it: generated_at is excluded from the hashed body, and every block
is sorted so registry ordering cannot leak into the output. A test asserts that
reversing the input operation list yields identical bytes.
This is what makes drift detectable. A changed hash means the graph changed; it never means the exporter felt different today.
Never hand-edit it
Editing numos.state breaks its content hash, and
numinit will correctly refuse to boot from it. To change what a
machine does, change the graph and re-export. That is not a limitation working
around — it is the entire point of the design.
python3 -m numos.export_state --out dist