Files
NeilClack d54637d249 Bake the channel manifest's closure instead of a pinned app version
Production builds failed CI's baseline preflight because flake.nix pinned
the app to tag 1.8 (channel.appVersion = "1.8") while the published
manifest had moved on to 1.9.

Stop pinning the app anywhere. flake.nix now fetches the channel's
published manifest at eval time (builtins.fetchurl) and bakes its
storePath verbatim, so every image carries exactly the closure RadiantWave
published to Attic and never compiles the app from source.

- flake.nix: drop the radiantwave/radiantwave-dev inputs, appInput,
  appVersion, channelVersion and baselineOutPaths; add channelAtticUrl,
  channelManifestDir and publishedApp; builders consume the published path.
- configuration.nix: wrap the published store path in a symlinkJoin so it
  can live in environment.systemPackages; add --impure to autoUpgrade
  flags and set tarball-ttl = 0 so eval reads the current manifest.
- radiantwave-updater.nix: baselinePackage (package) -> baselinePath (str),
  seeded from the exact manifest path.
- CI (build.yml): replace the version-equality preflight with a fail-fast
  check that the announced closure is in the channel's Attic; set
  tarball-ttl = 0.
- build-rpi.sh: pass --impure.
- AGENTS.md: document the manifest-driven baseline.
2026-10-07 10:28:12 -05:00

122 lines
9.0 KiB
Markdown

# 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 app itself is not pinned or built here: each channel's **published manifest** (served from `files.radiantwavetech.com`, pushed by RadiantWave CI to Attic) is the single source of truth for the baked closure.
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`: `atticRepo = "radiantwave"`, `gitRef = ""` (default branch).
- `dev`: `atticRepo = "radiantwave-dev"`, `gitRef = "?ref=dev"`.
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 two `channel` values. When merging, reconcile just this one block.
**No app version is pinned anywhere.** At eval time `flake.nix` fetches the channel's published manifest (`https://files.radiantwavetech.com/<atticRepo>/<manifestFile>`) with `builtins.fetchurl` and bakes its `storePath` verbatim as the app closure. Nix substitutes that exact closure from the channel's Attic cache, so images always match what RadiantWave published and never compile the app from source. Because that fetch is impure (no hash), **every eval that reaches the app needs `--impure`**: CI, local `build-rpi.sh`, and the kiosk's `system.autoUpgrade` (which passes `--impure`). `tarball-ttl = 0` is set in CI and on the kiosks so a rebuild never reuses a stale manifest.
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
```bash
# Build a system closure (any config name from flake.nix).
# --impure is required: the app closure comes from the channel manifest,
# fetched at eval time (see the prod/dev split above).
nix build --impure .#nixosConfigurations.<name>.config.system.build.toplevel
# Build a bootable ISO
nix build --impure .#nixosConfigurations.<name>.config.system.build.isoImage
# Deploy to a remote kiosk over SSH
nixos-rebuild switch --impure --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 (pure-safe: does not force the app fetch)
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 closure |
|---|---|---|---|
| `radiantwavekiosk` | Intel NUC 14 (NVMe) | `/dev/nvme0n1` | channel manifest `manifest.json` |
| `radiantwavekiosk-commercial` | Commercial HW (SATA) | `/dev/sda` | channel manifest `manifest.json` |
| `radiantwavekiosk-android` | Intel NUC 14, rotated display | `/dev/nvme0n1` | channel manifest `manifest.json` |
| `radiantwavekiosk-rpi` | Raspberry Pi 5 (SD image) | — | channel manifest `aarch64-manifest.json` |
On the `dev` branch, the same four configs read the `radiantwave-dev` manifests (`?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 manifest, 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 --impure .#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:
1. Boot that hardware from any NixOS live image (the kiosk installers work).
2. With real peripherals attached (display(s), audio), run:
```bash
sudo nix shell nixpkgs#nixos-facter -c nixos-facter -o facter-<model>.json
```
3. Commit `facter-<model>.json` to the repo root.
4. Wire it up at the marked spot in `flake.nix` (`facter.reportPath` inside `mkKioskSystem`) 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` and `build-rpi.yml` — triggered on pushes to `dev`, on new tags (production), and by `workflow_dispatch`, all on the `orion-lxc` runner. They build and publish exactly the closure the channel manifest announces, so a release is published by RadiantWave CI alone; there is no `flake.lock` bump to make here. Both workflows set `tarball-ttl = 0` and fail fast if the announced closure is not in the channel's Attic cache.
## Important gotchas
- **Disk device differs by variant**: `disk-config.nix` uses `/dev/nvme0n1`, `commercial-disk-config.nix` uses `/dev/sda`. `mkIso` pairs each ISO with the correct layout automatically — keep new ISOs paired correctly.
- **Changing a config name** requires updating that config's `upgradeFlakeRef` in `flake.nix` (it embeds the name).
- **The `channel` block is the dev/prod toggle** in `flake.nix` — `atticRepo` and `gitRef`. When merging `dev`→`main`, keep this block's values for the target branch; only this block differs between branches.
- **`rotated_display.nix` uses `mkForce`** on `loginShellInit` — it replaces the entire kiosk startup script from `configuration.nix`; don't edit both.
- **Auto-update**: `system.autoUpgrade` runs every 10 min (`*:00/10`) with `--refresh --impure`, operation `switch`, no auto-reboot. A `radiantwave-backup` rsync runs before each attempt. The health-check/rollback service is written but commented out in `configuration.nix`.
- **Live app updater**: each config's `radiantwaveUpdaterModule` points at a per-arch manifest — x86 uses `<repo>/manifest.json`, RPi uses `<repo>/aarch64-manifest.json` (via the `manifestFile` arg). 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 and `aarch64-manifest.json`; until then the RPi stays on the baked baseline.
- **App closure comes from Attic**: both channels (`radiantwave`, `radiantwave-dev`) are configured as trusted substituters in `configuration.nix`, so the baked app and its updates are substituted, never compiled, on the kiosks. nixpkgs deps still come from cache.nixos.org.
- **Secrets**: none in-tree anymore (gitea repos are public). If either repo goes private, re-add auth via `nix.settings.access-tokens` in `configuration.nix`.
- **App inputs**: there are none. Do not re-add a pinned `radiantwave` flake input or an `appVersion`; that is exactly what drifted from the published releases. The manifest is the source of truth.
- **SSH keys on machines are inbound-only** (`authorizedKeys` for admin access); outbound git is HTTPS. The root key in `zero-touch-installer.nix` must stay in sync with `myKey` in `configuration.nix`.
## File map
- `flake.nix` — entry point: inputs, the `channel` dev/prod selector, builders (`mkKioskSystem`, `mkRpiKioskSystem`, `mkIso`), all system definitions, facter report wiring
- `configuration.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 ISOs
- `disk-config.nix` — NVMe disk layout (NUC)
- `commercial-disk-config.nix` — SATA disk layout (commercial HW)
- `rotated_display.nix` — portrait display override for Android variant
- `facter.json` — hardware facter report (NUC 14)
- `assets/` — logo and splash image
- `plymouth-theme/` — boot splash theme
- `tools/find_systems.zsh` — subnet sweep for fleet inventory over SSH
- `build-rpi.sh` — containerized RPi SD-image build helper
- `PLAN-*.md`, `report.md`, `timeline.csv`, `kiosk-url-changes.csv` — planning/history notes, not configuration