The state format
numos.state is the sole runtime configuration for
numinit: line-oriented, content-hashed, shell-readable with
grep/cut.
Why not JSON at boot
The shell floor has no JSON parser. Shipping one would enlarge the bootstrap for no gain. Runtime format is field-positioned text; JSON remains an optional export view for tooling.
The seven record types
V <version> C <sha256-of-body> A <canonical> <aliases_csv> <zig_triple|-> <floor_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...> R <name> <cpu_pct|-> <ram_mb|-> <timeout_s|-> X <name> <interval_s> <threshold> <local_action> <on_fire|-> <probe...>
V C A P U R X; records are sorted
within each block. A unit with no resource envelope emits no R
record at all — envelopes are optional and must not shift U
field indices.On the shell floor, resource envelopes are enforced where POSIX allows and refused where it does not:
| field | floor behavior |
|---|---|
ram_mb | applied via ulimit -v on the unit process |
timeout_s | applied via timeout(1); missing binary is a named halt |
cpu_pct | refused — needs cgroups; a percentage is not ulimit -t |
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