The radiantwave 1.8 tag was force-moved from 34c72b5 to a5a6b5b while flake.lock still pinned the old commit, so the ISO baked a baseline that was not the closure RadiantWave CI published to Attic. With version 1.8 overridden this could still compile RadiantWave from source instead of substituting it. - Update flake.lock so radiantwave?ref=refs/tags/1.8 resolves to a5a6b5b. - Expose channelVersion/channelAtticUrl/channelManifestDir/baselineOutPaths from flake.nix for CI verification. - Prod workflows: fail fast unless channel.appVersion equals the channel manifest version, baselineOutPaths.<system> equals the manifest storePath, and that store path exists in the channel's Attic cache.
9.8 KiB
AGENTS.md
What this repo is
NixOS flake that defines kiosk systems running the RadiantWave app. Not application code — this is the OS image and deployment configuration. The actual app lives in the radiantwave flake inputs (from gitea.radiantwavetech.com).
Hosted on Gitea: https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig
Prod/dev split
Two branches — main (prod) and dev. There are no -dev configs: each hardware target has exactly one config, and the branch selects its channel via the single channel block at the top of flake.nix:
main:channel.appInput = radiantwave(pinned to tag1.8),atticRepo = "radiantwave",gitRef = ""(default branch),appVersion = "1.8".dev:channel.appInput = radiantwave-dev,atticRepo = "radiantwave-dev",gitRef = "?ref=dev",appVersion = <dev fingerprint>.
Everything downstream (the configs, ISOs, updater module, autoUpgrade refs) reads from channel, so a dev→main merge carries your changes automatically — no copying between sibling configs. To flip a channel, change only those four channel values. When merging, reconcile just this one block.
channel.appVersion is applied to the baked app via appPackage = system: channel.appInput.packages.${system}.default.override { version = channel.appVersion; }. RadiantWave's own flake only injects its version from the ambient APP_VERSION env var on impure evals, so pure evals (local nix build, build-rpi.sh) fall back to 0.1.0. Declaring the version here keeps local and CI builds correct without --impure. Bump appVersion whenever the pinned input changes (release tag on main, short commit hash on dev).
The baked baseline must be byte-identical to the closure RadiantWave CI publishes to Attic — that is what lets the ISO/sdImage substitute RadiantWave instead of compiling it from source, and makes the first live update a no-op. The prod workflows assert this in "Verify baked baseline matches the published channel": it compares channelVersion to the manifest's version, baselineOutPaths.<system> to the manifest's storePath, and confirms that store path exists in the channel's Attic cache. If it fails, the usual cause is a moved release tag: radiantwave is pinned by tag ref in flake.lock, so re-tagging 1.8 (as happened when it moved from 34c72b5 to a5a6b5b) leaves the lock on the old commit until you run nix flake update radiantwave. The RadiantWave flake also includes .gitea/ in the app source, so its CI workflow changes alter the derivation hash.
Both gitea repos are public: all git access is plain HTTPS (git+https://...), no SSH keys or access tokens on the kiosks. Don't reintroduce git+ssh URLs in flake/upgrade refs.
Key commands
# Build a system closure (any config name from flake.nix)
nix build .#nixosConfigurations.<name>.config.system.build.toplevel
# Build a bootable ISO
nix build .#nixosConfigurations.<name>.config.system.build.isoImage
# Deploy to a remote kiosk over SSH
nixos-rebuild switch --flake .#<name> --target-host root@<host>
# Push build result to Attic cache (not currently wired into any config)
attic push kiosk ./result
# Evaluate available config names
nix eval .#nixosConfigurations --apply builtins.attrNames
Config variants
One config per hardware target. Dev/prod is the branch, not the config — see channel in flake.nix:
| Name | Target hardware | Disk device | App input |
|---|---|---|---|
radiantwavekiosk |
Intel NUC 14 (NVMe) | /dev/nvme0n1 |
radiantwave (tag 1.8) |
radiantwavekiosk-commercial |
Commercial HW (SATA) | /dev/sda |
radiantwave (tag 1.8) |
radiantwavekiosk-android |
Intel NUC 14, rotated display | /dev/nvme0n1 |
radiantwave (tag 1.8) |
radiantwavekiosk-rpi |
Raspberry Pi 5 (SD image) | — | radiantwave (tag 1.8) |
On the dev branch, the same four configs use radiantwave-dev and track ?ref=dev.
Every variant sets its own networking.hostName (same as the config name) and system.configurationRevision, so tools/find_systems.zsh can identify fleet members.
USB images (zero-touch installers)
Each ISO installs exactly one closure using that closure's disk layout (mkIso in flake.nix derives both from the target config name):
| ISO | Installs | Disk layout |
|---|---|---|
kiosk-iso |
radiantwavekiosk |
NVMe (disk-config.nix) |
commercial-iso |
radiantwavekiosk-commercial |
SATA (commercial-disk-config.nix) |
android-iso |
radiantwavekiosk-android |
NVMe |
These same names build dev-flavored images when the flake is checked out on the dev branch (the channel block selects the dev app, attic repo, and upgrade ref), so there are no separate -dev ISOs.
The installer lives once in zero-touch-installer.nix and is parameterized by targetConfig. Adding a new variant = add the nixosConfigurations.* entry, a matching packages.x86_64-linux.* line, and an ISO if needed.
Raspberry Pi systems are built as SD images, not ISOs:
nix build .#nixosConfigurations.radiantwavekiosk-rpi.config.system.build.sdImage (see build-rpi.sh).
Hardware facter reports
facter.json is a hardware report captured on a NUC 14. It is currently applied to every x86 variant, including the commercial SATA hardware — acceptable until per-model reports exist.
To generate a report for a hardware model:
- Boot that hardware from any NixOS live image (the kiosk installers work).
- With real peripherals attached (display(s), audio), run:
sudo nix shell nixpkgs#nixos-facter -c nixos-facter -o facter-<model>.json - Commit
facter-<model>.jsonto the repo root. - Wire it up at the marked spot in
flake.nix(facter.reportPathinsidemkKioskSystem) by selecting the right file per variant.
Regenerate when major hardware changes, not on a schedule — reports are snapshots. RPi configs don't use the facter module, so no report is needed there.
CI (Gitea)
.gitea/workflows/build-*.yml — all triggered by workflow_dispatch on the build-server runner. They only build and publish what is committed. flake.lock is updated manually: commit an updated lock (e.g. after nix flake update radiantwave-dev) and dispatching the workflow is the release trigger. The prod workflows run a "Verify baked baseline matches the published channel" preflight that fails the build if the baked closure diverges from the channel manifest or is missing from Attic (see the channel block and baselineOutPaths in flake.nix).
Important gotchas
- Disk device differs by variant:
disk-config.nixuses/dev/nvme0n1,commercial-disk-config.nixuses/dev/sda.mkIsopairs each ISO with the correct layout automatically — keep new ISOs paired correctly. - Changing a config name requires updating that config's
upgradeFlakeRefinflake.nix(it embeds the name). - The
channelblock is the dev/prod toggle inflake.nix—appInput,atticRepo,gitRef,appVersion. When mergingdev→main, keep this block's values for the target branch; only this block differs between branches. rotated_display.nixusesmkForceonloginShellInit— it replaces the entire kiosk startup script fromconfiguration.nix; don't edit both.- Auto-update:
system.autoUpgraderuns every 10 min (*:00/10) with--refresh, operationswitch, no auto-reboot. Aradiantwave-backuprsync runs before each attempt. The health-check/rollback service is written but commented out inconfiguration.nix. - Live app updater: each config's
radiantwaveUpdaterModulepoints at a per-arch manifest — x86 uses<repo>/manifest.json, RPi uses<repo>/aarch64-manifest.json(via themanifestFilearg). A 2-min timer polls it and fetches the announced closure from the channel's Attic repo. RPi live updates require RadiantWave CI to publish both an aarch64 closure andaarch64-manifest.json; until then the RPi stays on the baked baseline. - No binary cache: kiosks fetch from cache.nixos.org only and compile RadiantWave itself from source during upgrades. Attic integration was planned (see
PLAN-update-system.md) but never wired; the old netrc/JWT setup has been removed. - Secrets: none in-tree anymore (gitea repos are public). If either repo goes private, re-add auth via
nix.settings.access-tokensinconfiguration.nix. - Flake inputs:
radiantwaveis pinned to tag1.8(?ref=refs/tags/1.8);radiantwave-devtracks?ref=dev. Thechannelblock picks which one the configs consume. Bump the pinned tag andchannel.appVersiontogether. Tag refs are live: if RadiantWave force-moves a tag, the lock keeps the old commit untilnix flake update radiantwave, so the baked baseline silently diverges from the published channel (the CI preflight catches this). - SSH keys on machines are inbound-only (
authorizedKeysfor admin access); outbound git is HTTPS. The root key inzero-touch-installer.nixmust stay in sync withmyKeyinconfiguration.nix.
File map
flake.nix— entry point: inputs, thechanneldev/prod selector, builders (mkKioskSystem,mkRpiKioskSystem,mkIso), all system definitions, facter report wiringconfiguration.nix— shared NixOS config (packages, services, kiosk user, auto-upgrade, health check, VM variant)zero-touch-installer.nix— parameterized USB installer module used by all three ISOsdisk-config.nix— NVMe disk layout (NUC)commercial-disk-config.nix— SATA disk layout (commercial HW)rotated_display.nix— portrait display override for Android variantfacter.json— hardware facter report (NUC 14)assets/— logo and splash imageplymouth-theme/— boot splash themetools/find_systems.zsh— subnet sweep for fleet inventory over SSHbuild-rpi.sh— containerized RPi SD-image build helperPLAN-*.md,report.md,timeline.csv,kiosk-url-changes.csv— planning/history notes, not configuration