Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ONIX — Architecture & Build Plan

An atomic, moss-managed machine with a persistent Nix toolbox — built from scratch on musl. ONIX is the hard machine layer everything else sits on. The moss-managed machine plane is the foundation; the Nix toolbox is the living soil on top. It borrows AerynOS’s tooling (moss, boulder, the .stone format) and its naming world — you’re extending the geology, not breaking it.

Core rule: moss controls the machine. Nix controls the toolbox.

Direction change (2026-07). ONIX does not consume AerynOS’s ISO or its glibc package repos. Their live image is desktop-first (GNOME) and their whole volatile repo is glibc; neither fits. ONIX keeps only the toolingmoss (atomic package/state manager) and boulder (the .stone builder) — and bootstraps its own smallest-possible base on musl, from scratch. Alpine is the throwaway forge: a tiny musl host where moss+boulder are built and the first stones are cut. The endgame is our own musl distro, managed by moss, with Nix supplying the (glibc) software long tail on top.

Naming rule: public branding is always ONIX; machine IDs and package names use onix.


0. Identity & naming conventions

Locked in up front so every later artifact is consistent.

ThingNameNotes
Distro nameONIXPretty name
Machine-readable IDonixLowercase everywhere machine-readable
C librarymuslNon-negotiable; the whole base is musl. Nix apps bring their own glibc (see §3)
Magic number6649“ONIX” on a phone keypad — nixbld GID, VM MAC suffix, port offsets
Git orgonix-osRepos: image, recipes (musl .stone recipes), nix-integration, docs
Package namespaceonix-*All custom .stone packages: onix-base, onix-nix-integration, onix-cli, onix-branding, onix-desktop
Moss repoonixmoss repo add onix <url> — the only repo; there is no upstream repo beneath it
Upstream toolingos-toolsgithub.com/AerynOS/os-tools — moss + boulder, pinned. The one external dependency
CLI wrapperonixThin wrapper over moss + nix (see §6). onix update, onix rollback, onix status
Boot entry tokenonixBLS Type #1 entries: onix-<txid>.conf
Volume labelsONIX-ESP, ONIX-BOOT, onix-root, ONIX-PERSIST
ForgeAlpine musl VM, hostname quarryThrowaway bootstrap host — builds moss+boulder and the first stones.

/usr/lib/os-release (shipped by onix-branding):

NAME="ONIX"
ID=onix
PRETTY_NAME="ONIX (atomic musl base + Nix toolbox)"
VERSION_ID=rolling
BUILD_ID=<txid injected at image build>
HOME_URL="https://onix-os.com"
ANSI_COLOR="38;2;140;120;100"

No ID_LIKE: ONIX is not a downstream of any distro’s package set — it shares AerynOS’s tooling, not its packages, and its libc (musl) diverges from AerynOS (glibc). It stands on its own foundation.


1. Architecture overview

Two planes with a hard ownership contract:

┌─────────────────────────────────────────────────────────┐
│  USER TOOLBOX PLANE (persistent, user-driven)           │
│  /nix  ·  nix-daemon  ·  per-user profiles              │
│  nix shell / develop / profile · home-manager (opt)     │
│  GUI leaf apps, dev environments, the long tail         │
│  (nixpkgs is glibc — apps are self-contained, run on    │
│   the musl base because they carry their own libc)      │
├─────────────────────────────────────────────────────────┤
│  INTEGRATION SEAM (owned by onix-* stone packages)      │
│  /etc/nix/nix.conf · nixbld sysusers · nix-daemon unit  │
│  /run/opengl-driver · XDG/fontconfig/locale glue        │
├─────────────────────────────────────────────────────────┤
│  MACHINE PLANE (moss-owned, transactional, atomic)      │
│  MUSL base · /usr (stateless, renameat2-swapped)        │
│  /.moss store · kernel + initrd + BLS entries (blsforme)│
│  init, udev, Mesa/DRM, PipeWire, portals, compositor    │
│  — all built from our own musl .stone recipes           │
└─────────────────────────────────────────────────────────┘

Ownership contract (the constitution)

SurfaceOwnerNever touched by
/usr, kernel, initrd, boot entriesmossNix
/.moss content store & fstx historymossNix
Mesa/libdrm/firmware, compositor, D-Bus, PipeWire, portalsmoss (base packages, musl-built)Nix (system-wide)
/etc baseline defaultsmoss via /usr/share/defaultsNix
/etc/nix/*onix-nix-integration stone seeds it; nix daemon reads itmanual drift
/nix, /nix/store, /nix/varNixmoss
Per-user profiles, dev shells, user servicesNixmoss
setuid/setcap binaries, kernel modulesmoss onlyNix — hard rule

The single most important consequence: a moss rollback and a Nix profile rollback are independent operations that must never corrupt each other. Phase 5 validation exists specifically to prove this.


2. Bootstrapping the machine plane

This is the section the pivot rewrote most. There is no upstream to consume — the base is built from zero on musl, with boulder.

2.1 The forge (Alpine musl bootstrap host)

You cannot build .stone packages without moss + boulder, and you can’t run them meaningfully without a musl host to build in. Alpine is that host:

  1. Build a minimal Alpine/musl VM from scratch — from the 3.7 MB minirootfs tarball, not their ISO — into a bootable disk (vm/ in this repo already does this). Hostname quarry.
  2. Build moss + boulder from github.com/AerynOS/os-tools (just get-started). They’re ordinary Rust binaries; they build and run on musl. boulder explicitly supports non-AerynOS hosts (--data-dir, --config-dir, --moss-root).
  3. Alpine is scaffolding, thrown away in the final design. During the current Phase 2 boot proof, ONIX temporarily borrows Alpine’s virt kernel/initramfs/module payload so image assembly and systemd-on-musl can be proved before kernel ownership becomes its own project. Phase 3 exists to remove that shortcut later. The forge still provides the musl toolchain and userns environment used to run boulder.

2.2 Bootstrap the musl base as .stone packages

No AerynOS recipe is musl — a musl base is a genuine bootstrap, and that is the point of the project.

  1. Author .stone recipes (stone.yaml) for the core musl userland: musl, a compiler toolchain, the base command set, the handful of packages onix-base needs. Use Alpine’s APKBUILDs as a reference for the musl-specific patches; keep the base set short — the long tail is Nix’s job (§3).
    • Coreutils strategy (Rust-first, matches the moss/boulder ethos): ship busybox as the working base first — it is small, musl-native, and already proven in the forge. Then bring in uutils/coreutils (Rust) alongside it. Once uutils is proven, the plain tool names (ls, cp, …) resolve to uutils, and busybox stays reachable but not default — invoked as busybox <tool>, not removed. Sequenced for Phase 1, after the forge builds stones.
  2. boulder supports bootstrapping explicitly: a shared control file can disable tests during initial rounds, then be removed for final builds with tests re-enabled.
  3. Stand up the onix moss repo — a static, locally-served index of your boulder-built stones. Start as file:///var/lib/onix/repo/ on the forge; graduate to static HTTPS (any dumb file server; no vessel needed at this scale). It is the only repo — nothing sits beneath it.
  4. Pin os-tools. The one external dependency is the tooling. The forge records this as OS_TOOLS_REF in vm/phase0/config.sh; an onix mirror habit later hedges against upstream churn.

2.3 Image building

Assemble a bootable image from the musl base stones (an onix-os/image repo):

  • moss-install onix-base + onix-branding (os-release above) into a fresh root: kernel, initrd tooling, init, udev, dbus, a network stack, busybox/coreutils, filesystem tools (xfsprogs, dosfstools), moss itself.
  • Keep a QEMU/OVMF smoke-test — that’s CI from day one (the vm/ harness generalizes to boot the built image).
  • Every image build records: os-tools commit, onix repo commit, resulting fstx ID. That triple is the reproducible build pin for a rolling personal distro.

2.4 Boot artifacts

Target BLS Type #1 entries exactly as moss/blsforme do: entries named onix-<txid>.conf, kernel+initrd promoted to $BOOT (XBOOTLDR), transaction ID on the kernel cmdline, old entries pruned by moss. blsforme is a bootloader-spec tool and does not itself require systemd as PID 1 — but the surrounding glue (sysusers/tmpfiles) is systemd-flavored, which forces the init decision below. UKI + Secure Boot is later future work.

Init decision. ONIX uses systemd as PID 1 plus systemd-boot/BLS rather than GRUB for the real image. Phase 209 showed that the pinned Nix universe contains a musl-targeted pkgsMusl.systemd, so the current project path is systemd-on-musl. Phase 0’s Alpine OpenRC/GRUB setup remains throwaway forge scaffolding only.


3. The Nix plane and the integration seam

3.1 Do not run the Nix installer

Neither the official curl installer nor the Determinate installer fits a stateless-/usr system: they mutate /etc, drop init units imperatively, and useradd. That’s exactly the drift ONIX exists to avoid. (They also assume glibc/systemd — doubly wrong here.)

Instead, the linchpin deliverable is one package:

3.2 onix-nix-integration (.stone)

Ships everything Nix needs, declaratively, through the machine plane:

ComponentPathContent
Nix binariesvia /nixOne-time seed /nix from a Nix release; Nix then upgrades itself. The stone ships integration, not Nix — keeps moss out of tracking Nix releases. Default profile bin on PATH.
Build userssysusers.d (or boot-script equiv. under init-(b))g nixbld 6649 + u nixbld1..32 -:nixbld (GID 6649 — “ONIX”). Materialized every boot; survives an ephemeral-/etc future.
Daemon unitnix-daemon.service + .socket (or native service)Ordering hardened: after /nix mount, ConditionPathIsDirectory=/nix/store.
Mount unitnix.mountOr generated from fstab — see §4.
Runtime dirstmpfiles.d (or equiv.)/nix/var/nix/daemon-socket, gcroots dirs, profiles/per-user.
System config/usr/share/defaults/nix/nix.conf/etc/nix/nix.conf overridesbuild-users-group = nixbld, sandbox = true, auto-optimise-store = true, allowed-users = @users, trusted-users = root, experimental-features = nix-command flakes.
Shell hooks/usr/share/defaults/etc/profile.d/onix-nix.shPATH, NIX_REMOTE=daemon default, XDG_DATA_DIRS, LOCALE_ARCHIVE guard, fontconfig include.

3.3 Graphics: /run/opengl-driver as a first-class feature

The classic failure mode of Nix GUI apps on foreign distros: they load nixpkgs’ Mesa, which mismatches the host kernel/DRM stack → llvmpipe or crashes. ONIX fixes this at the machine layer — the distro’s genuinely distinguishing feature:

  • onix-opengl-driver glue in onix-nix-integration populates /run/opengl-driver/lib at boot with symlinks into the moss-managed Mesa/libdrm/Vulkan ICDs under /usr.
  • Nixpkgs apps honor the NixOS /run/opengl-driver convention, so they pick up host-matched userspace drivers with zero per-app wrapping.
  • Because the symlink farm is rebuilt each boot from active /usr state, a moss rollback of the graphics stack re-coheres the Nix GUI world on next boot. No nixGL, no per-app hacks.
  • Same glue handles /run/opengl-driver/share/vulkan/icd.d and EGL vendor files.

musl-specific caveat (bigger than the original glibc-skew note). Our base Mesa is musl-built; nixpkgs GL apps are glibc. Nix apps carry their own glibc and most libraries from the store, so they run on a musl base — but the /run/opengl-driver seam is exactly where a musl-built Mesa meets a glibc GL app, and that boundary can break (symbol/ABI mismatch). Mitigation to carry forward: the opengl-driver bridge may need to expose a glibc-built Mesa variant for the Nix world (built as its own stone, or pulled from nixpkgs), rather than symlinking the musl base Mesa directly. Resolve in Phase 7; keep it in mind from Phase 5.

3.4 Optional: home-manager standalone

Fine from Phase 5 onward, standalone mode only — manages ~/.config and user services, never system state. Composes cleanly with the contract. Personal preference, not architecture.


4. Filesystem layout & persistence

4.1 Partitions (VM prototype; same shape on hardware)

PartitionFSSize (VM)LabelMount
ESPFAT32512 MiBONIX-ESP/efi
XBOOTLDRFAT322 GiBONIX-BOOT/boot
RootXFS40 GiB+onix-root/ (holds /.moss; bigger = more rollback states)
PersistXFS or ext4restONIX-PERSIST/persist
  • /nix = bind mount from /persist/nix (mount unit ordered before local-fs, nix-daemon after it as in §3.2). One persistence surface to snapshot/back up.
  • /home = bind from /persist/home.

(The forge disk is simpler — a plain GPT ESP + ext4 root; the table above is the ONIX image target.)

4.2 Persistence policy — Phase-appropriate

Phases 2–8: fully persistent /etc and /var. The /usr/share/defaults + /etc-overrides model bounds drift; ephemeral overlays add early-boot ordering complexity for marginal gain now. Add drift visibility instead: onix status diffs /etc against shipped defaults and reports every override.

Later optional, post-daily-driver: ephemeral /etc+/var overlays with an explicit allowlist. Must-persist set, documented now:

/etc/machine-id · /etc/ssh/ssh_host_* · /etc/nix/ · init/service state (/var/lib/…) · NetworkManager/bluetooth state · /var/log (your choice) · /home · /nix · /.moss


5. Phased roadmap

Each phase has an exit gate. Don’t advance until the gate passes — the gates are the real deliverable.

Phase 0 — The forge (musl tooling host)

Build a minimal Alpine/musl VM from the minirootfs (this repo’s vm/). Build moss + boulder from os-tools. Cut a trivial first .stone with boulder; install/remove it with moss into a test root; inspect moss state list. Gate: moss + boulder run on musl; you can boulder-build a hello-world .stone, moss-install it, roll the moss state back, and remove it — cleanly.

Current smoke-test command:

make doctor     # common health check
make phase 004  # once per fresh forge image: build moss + boulder
make phase 005  # build/check/extract/index/install/run onix-hello
make phase 006  # real moss state install/remove/rollback smoke test

Recipe gotcha learned in the forge: Boulder build directories inherit g+s. If a recipe creates /usr//usr/bin with the setgid bit still present, Boulder treats those directories as special and emits a /usr/ layout entry. Moss currently rejects that path during extract/install. For simple recipes that install directly into /usr/bin, clear setgid in install:

install     : |
    install -Dm00755 my-tool %(installroot)%(bindir)/my-tool
    chmod g-s %(installroot)/usr %(installroot)%(bindir)

Phase 1 — Bootstrap the musl base stones

Author stone.yaml recipes for the core musl userland (musl, toolchain, busybox/coreutils, essentials). Stand up the onix moss repo (file:// then static HTTPS). Keep the base set short. Gate: the onix repo carries a self-consistent base stone set; moss installs it into a fresh root that chroots and runs a shell + coreutils.

Phase 2 — First bootable ONIX image

Assemble and boot the first ONIX disk image in QEMU/OVMF. This phase proves the image layout, systemd-boot/BLS path, systemd-on-musl userspace, root filesystem, persistent partitions, and first module-loading payload. It temporarily borrows the Alpine forge’s virt kernel/initramfs/modules so the boot proof can move without starting kernel work too early. Gate: the image boots through systemd to multi-user mode; /boot, /efi, /persist, /home, and /nix mount; the boot log has no missing modprobe or module-compression errors.

Phase 3 — ONIX-owned kernel/initramfs/modules (reserved)

Replace the borrowed Alpine kernel payload with ONIX-owned kernel artifacts. This phase owns the kernel config, kernel build, module package, initramfs generation, and boot-entry/update policy. It is intentionally deferred while Phase 4 makes the already-booting image usable. Gate: kernel, initramfs, and modules are built and packaged by ONIX; their versions match; boot entries select coherent generations; rollback behavior is understood.

Phase 4 — Booted ONIX base userspace

Turn the Phase 2 boot proof into a usable base system while still using the borrowed kernel payload. Materialize live /etc from /usr/share/defaults; define users/groups/login shell policy; prove serial login; add minimal networking and remote inspection. Gate: a booted ONIX image can be logged into and inspected intentionally; base identity, fstab, users, shells, hostname, and early services are owned by ONIX policy rather than ad-hoc image glue.

Phase 5 — Nix plane (the critical phase)

Ship onix-nix-integration; seed /persist/nix from a Nix release tarball; verify daemon-mode multi-user builds, nix shell/develop/profile install. Confirm glibc nixpkgs apps run on the musl base. Gate — the composition matrix, all green:

TestExpected
nix profile install nixpkgs#ripgrep → reboottool still on PATH, runs on musl base
Install Nix tool → moss rollback → rebootmachine state rolled back, Nix tool untouched
moss update → nix profile rollbackboth planes at chosen generations, no interference
moss state prune + nix store gc back-to-backno cross-corruption, both stores intact
Kill nix-daemon mid-build → restartstore consistent

Phase 6 — Recipe set & overlay matures

boulder workflow for the growing musl recipe set (onix-cli, base additions — keep short; the long tail is Nix). Static HTTPS hosting; onix mirror snapshot habit (mirror os-tools + your stones). Gate: clean-room rebuild from the pinned snapshot triple (§2.3) boots identically.

Phase 7 — Desktop

Pick one desktop (start minimal — a Wayland compositor). All of: compositor, Mesa, PipeWire + WirePlumber, xdg-desktop-portal backend, fonts — from the machine plane (musl-built). Enable the /run/opengl-driver glue. Gate: a Nix-installed GL app renders hardware-accelerated with no wrapper (this is where the musl-Mesa vs glibc-app seam of §3.3 gets solved); portals work from a Nix app; ~/.nix-profile desktop entries appear; then roll back a Mesa update and confirm the Nix app still renders on the previous stack after reboot.

Phase 8 — Hardware

AMD/Intel graphics strongly preferred (NVIDIA complicates both musl and the /run/opengl-driver story). Dedicated drive — no same-disk dual boot at this maturity. Migrate /persist by restore-from-backup, not disk surgery. Gate: two weeks of daily driving with at least one rollback drill and one restore-from-backup drill of /persist.

Later / optional

Init decision revisited & musl recipes contributed upstream · UKI + Secure Boot · disk encryption (initrd unlock) · ephemeral /etc allowlist mode · onix from shell script to small Rust binary.


6. The onix CLI

Thin, honest wrapper — it must never hide which plane a command touches:

onix update      # moss sync + report pending reboot
onix rollback    # moss state activation to previous, prints boot instructions
onix status      # active fstx, boot entry, /etc drift vs defaults, nix-daemon health,
                 # /run/opengl-driver coherence check
onix gc          # moss state prune + nix store gc, sequenced, with guardrails
onix mirror      # snapshot os-tools + depended-on stones to /persist/mirror
onix doctor      # runs the Phase 5 composition matrix as an automated self-test

Ships as onix-cli from the onix repo. Start as ~200 lines of shell; rewrite in Rust when it stops being embarrassing to. (onix solid as an alias for a fully-green onix doctor run is optional but encouraged.)


7. Risks & maintenance posture

RiskExposureMitigation
From-scratch musl bootstrap is a large effortHigh — this is the whole project nowPhase gates; Alpine forge does the heavy lifting for the toolchain; keep the base tiny — Nix covers the long tail so the base recipe set stays small
No musl .stone recipes exist upstreamHighAuthor/port them, using Alpine APKBUILDs as the musl-patch reference; ruthless minimalism on the base set
systemd-on-musl maintenanceHigh / structuralONIX keeps the systemd-on-musl path while it remains feasible; package it intentionally as onix-systemd and test PID 1, udev, tmpfiles, sysusers, and service startup in QEMU. §2.4
moss/boulder are alpha & glibc-testedMediumThey’re open Rust you can read/patch; pin os-tools; fix any glibc assumptions and upstream them
glibc skew: musl base Mesa vs glibc Nix appsMedium, intermittentNix apps are self-contained so most run; concentrate the fix at the /run/opengl-driver seam — possibly a glibc Mesa variant for the Nix world (§3.3)
Branding driftMedium during early developmentKeep public names as ONIX or onix only; grep tracked docs/scripts before commits
NVIDIAHigh if chosenDon’t. AMD/Intel for this project
Solo-maintainer burdenReal, and higher than a derivative would beBudget honestly; keep base scope minimal; if it trends high, cut base scope and lean harder on Nix
Data lossTransactional /usr protects nothing you care about/persist (= /home + /nix + configs) gets real backups — borg/restic to external target — from Phase 5, not Phase 8. moss rollback is not a backup

8. What ONIX is, in one paragraph

A small, auditable, atomic base you build yourself on musl — using AerynOS’s excellent tooling (moss + boulder) but none of its packages — that you can always roll back; a huge optional software universe on top through persistent multi-user Nix (glibc apps riding on a musl base, carrying their own libc); and a machine-layer /run/opengl-driver bridge that makes Nix GUI apps first-class in a way almost no foreign distro manages. Alpine is the forge where the tooling is built and the first stones are cut, then discarded. The machine plane is the foundation; everything you actually live in grows on top of it — and the ground never shifts under you without your say-so. The machine manager controls the machine. Nix controls the toolbox.