Files
Panama/docs/superpowers/specs/2026-08-20-panama-fresh-install-design.md
T
Gabriel Brown 13f3648e4d Install the driver, enrol the key, and still never ask twice
Phase 3 of the fresh-install spec: the parts of a run that depend on what the
machine actually is. NVIDIA, Secure Boot, Fedora's preinstalled extras, firmware.

Two of these looked like they would force a compromise, and neither did.

sunhat opened an editor in the middle of its run so grub could be hand-corrected,
and that single step is why walking away from an install did not work. The step
existed to delete duplicated kernel arguments -- and grubby replaces an argument
that already exists rather than appending a second copy, so the duplicates cannot
accumulate and there is nothing to correct. The editor was load-bearing for a
problem that a different tool does not have.

MOK enrolment needs a password now and the same password at the next boot's blue
screen, which reads like a prompt that has to happen mid-run. mokutil has
--generate-hash and --hash-file for exactly this: the interview asks, hashes it
on the spot, and records only the hash. The plaintext never reaches the answers
file, the environment, or a command line, and the stage runs without asking.

The stage runs last rather than fourth as the spec's table had it. The constraint
was always "late" and fourth of eight is not late: enrolment arms a prompt for the
next boot and firmware may want a reboot, so a machine that reboots out of this
stage should already be completely configured.

Every question names what was found -- the card, the packages actually installed
-- and is not asked at all on a machine it would do nothing to. sunhat's debloat
list no longer describes Fedora 44: totem became showtime and LibreOffice is not
preinstalled, so the list is curated and a package that is not installed is never
passed to dnf, which is what lets it outlive a release.

This stage cannot be verified by running it. It installs a proprietary driver and
queues a Secure Boot enrolment, and this machine is an AMD desktop. So every
privileged command is stood in on PATH and the contract asserts which answer led
to which call: that no answers means no commands, that a failed driver install is
not followed by arguments and services for a driver that is not there, that the
hash reaches mokutil through a file and never a command line, and that removal is
offered only for packages that are installed. The contract was checked by breaking
the stage three ways and confirming it caught each. It does not verify that
akmod-nvidia builds, and says so where a reader would otherwise assume it did.

The README's stage table listed three of seven stages; the interview and identity
work never reached it. Corrected rather than extended, since a table that lists
three of seven is worse than one that lists none. The Desktops section still
describes a GNOME session nothing installs -- that is phase 5.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 19:36:53 -04:00

24 KiB

Panama: fresh-install correctness and the sunhat deprecation

Panama installs a desktop it does not fully provision. The Quickshell shell names fonts nothing installs, a cursor theme nothing installs, and a wallpaper that does not exist; four settings pages shell out to packages no list declares. None of this shows up on this machine, because this machine accumulated those pieces under sunhat and never lost them. It shows up on the next machine.

This spec closes that gap, moves every question the installer needs to the front of the run, and ports the parts of sunhat that still matter — so sunhat can be deleted.

Why now

Recent work built a large Quickshell desktop: a bar, dock, overview, lock screen, notification server, and a Settings application with thirty-odd pages. Almost all of it was verified against a running system. Almost none of it was verified against a system where Panama had just been installed for the first time.

The existing tests/quickshell/declared-dependencies-contract.sh was written for exactly this class of bug and reports PASS, because it checks only commands that scripts invoke. Fonts, cursor themes, wallpapers, and packages consumed by GUI handoffs are invisible to it. A dependency checker with a blind spot is worse than none, because it reports PASS — that argument is already in that file's own header comment, and it applies to the checker itself.

What is broken

Verified against the repository at 3b01f1e.

Gap Evidence
No fonts installed setup/scripts/change-settings:32 sets VictorMono Nerd Font 10. kitty, ghostty, hyprlock.conf.template, Theme.qml and Fonts.qml all name Nerd Font families. No stage installs a font. Fonts.qml documents that a mono family without Nerd glyphs replaces every icon in the shell with tofu.
No cursor theme installed oreo_blue_cursors is named in change-settings:33, both GTK settings.ini.template files, config/dot/uwsm/env:23, config/dot/hypr/env.lua:52, and PreferenceSchema.qml:985. It exists in ~/.local/share/icons on this machine only.
No wallpaper, though the code says one ships Wallpaper.qml:32 declares shippedPath as ~/Pictures/Wallpapers/faroe_islands.jpg. panama-lock:139 falls back to the same path, hyprpaper.conf names it twice, and lock-screen-settings-harness.qml uses it as a fixture. Nothing ships it. PreferenceSchema.qml:826 defaults wallpaperPath to "".
gnome-control-center undeclared SystemSettings.qml:599 allow-lists 23 GNOME panels; 15+ rows across OnlineAccountsPage, RegionPage, AccessibilityPage, ConnectivityPage, ShortcutsPage, SoundPage, PrivacyPage and HealthPage call openGnomePanel. No list declares the package.
snapper undeclared panama-snapshots drives the entire Snapshots page through snapper.
Four more undeclared, found by the new contract Crawling QML for launched commands — which the dependency contract never reads — also caught gnome-calendar (CalendarAgenda.qml:180), podman (Containers.qml:149, the Containers page backend), pipewire-utils for pw-dump/pw-play (PrivacyState.qml:169, SoundTest.qml:35), and flatpak, which install-packages itself uses.
espanso, ghostty undeclared Both have configs symlinked by link-dotfiles; neither is in a package list. ghostty is in Terra and is now declared. espanso is in no enabled repository — sunhat built it from source — so under the dnf/flatpak rule it stays undeclared. Its config remains linked, which costs nothing, and installing it is a separate decision.
.bashrc breaks on a fresh box config/bash/.bashrc ends with an unguarded . "$HOME/.cargo/env", which does not exist until rustup-init has run, so every shell start emits an error. The file also carries a committed # >>> Codex installer >>> block.
README describes a desktop that is not installed The Desktops section advertises GNOME with Forge, Dash-to-Dock, Openbar and Vitals. Nothing in setup/ installs or configures any of it.

Decisions

These were settled during design and are recorded so the plan does not relitigate them.

The GNOME session is cut; gnome-control-center stays. These are different things and only the first is free. The session — Forge, Dash-to-Dock, Openbar, Vitals, the README's parity claims — is dead weight and goes. The control center is load-bearing: it is where Panama's own Settings sends you for Online Accounts, Region, Accessibility, Color, Wellbeing and Wacom, and commit 0c364f3 shows the handoff list was deliberately narrowed to what Panama does not own. It becomes a declared dependency rather than an accident of this machine's history.

The installer asks everything up front, then runs untouched. sunhat's failure mode was a prompt or a failure twenty minutes in, with a person needed at the keyboard to get past it. Panama's answer is not "no prompts" — some of what the installer needs genuinely changes between machines and between years, and a stale hardcoded answer is worse than a question. The answer is that every question is asked before any package work begins, and nothing asks again afterwards.

Answers are not persisted. Each run asks fresh. The only default shown is the current hostname, which today's installer already shows. This is a deliberate simplicity choice: no answers file to keep in sync, nothing personal on disk, and re-answering six questions costs less than maintaining state that can go stale.

Hardware is detected, then confirmed. Gib's machines span AMD, Intel and NVIDIA in both laptops and desktops. The installer probes — lspci for GPUs, mokutil --sb-state for secure boot, DMI for chassis — and asks a question that names what it found: "Found NVIDIA RTX 4070 — install akmod-nvidia and enroll a secure boot key? [Y/n]". Detection alone would remove the ability to decline a proprietary driver on a machine that has the card; asking alone would make a person recite their own hardware.

Nothing personal is committed. Panama should be cloneable by someone else and yield a working desktop. Identity, hostnames and machine layout come from prompts and from gitignored local state — ~/.config/panama/settings.json is already exactly this — never from files in the repository.

Applications are admitted one at a time, and dnf or flatpak only. A short always-installed core, plus categories chosen from a gum checklist during the interview, so a work laptop does not acquire emulators and a desktop does not skip Steam. But the menu starts nearly empty and grows by explicit decision, one application per decision — it is not seeded by porting sunhat's list.

The rule is that every application Panama installs must come from dnf or from Flathub, with flatpak preferred where both exist. Anything requiring a pinned RPM URL, a curl | bash, or a source build is admitted only case by case, with a stated reason why no packaged form exists.

sunhat is the argument for this rule, not a counterexample to it. Its sources/dotenv.sh hardcodes upscayl-2.11.5, lact-0.5.4-0.x86_64.fedora-40.rpm and jdk-22 — URLs that were stale within a release cycle and that turn a re-run into a 404. Every application Panama declares is one it has to keep installable, and the cost of that promise is only bearable if a package manager is keeping it.

Panama already carries three exceptions: the oh-my-posh and bun install scripts in install-packages, and helium-browser-bin from the lionheartp/Hyprland COPR. They stay — each is idempotent and guarded — but they are the ceiling, not a precedent. New exceptions need the same standard.

Bash scripts carry no .sh extension. A shebang and the executable bit select the interpreter; the extension is redundant. This applies to the 159 existing files as well as new ones.

Design

The installer

./install becomes four movements:

ascii  →  interview  →  stages (uninterrupted)  →  panama-doctor summary

The interview is a new script, setup/scripts/interview, that runs before any other stage and asks, in order:

  • Hostname — defaulting to the current one, as today
  • Git identity: name, email, editor
  • Run gh auth login? — skipped without asking when gh auth status already passes
  • Generate an SSH key? — skipped without asking when one already exists
  • Hardware confirmations, each naming what was detected
  • Remove Fedora's preinstalled bloat?
  • Update firmware with fwupdmgr?
  • Which application extras to install — a gum checklist

Answers reach the stages as PANAMA_* environment variables, but not by the obvious route. Stages run as child processes of install, and a child cannot export into its parent or its siblings — so the interview cannot simply set variables and expect the next stage to see them. Instead it writes KEY=value lines to a mktemp file whose path install chose; install sources that file, exports the names, and deletes it on the existing trap ... EXIT INT TERM that already restores the idle settings. The answers exist for the length of one run and never touch a durable path, which is what keeps "no persisted answers" honest.

The interview is therefore listed in STAGES like any other stage, but install treats it specially: it is the only stage whose output the installer reads back.

A stage that needs an answer reads its variable and treats absent as "no". This matters for re-running an individual stage by hand, which is a debugging path that must keep working, and it means the interview is never a prerequisite for repairing one piece of a machine.

The run ends with panama-doctor, printing its summary. Today the only signal that an install worked is a failed-stage count; a failure that produced no non-zero exit — a font that did not land, a service that did not start — scrolls past unnoticed. docs/UPSTREAM-INSPIRATION.md already lists a first-run health check as a candidate; the health check now exists, so this is wiring rather than building.

Stages

The STAGES array in install is explicit rather than glob-ordered, and stays that way. The new order:

Stage Status Responsibility
interview new Every prompt, before any work
install-packages edited Repos, package lists, extras
link-dotfiles edited Adds the wallpaper and the cursor theme; drops forge
change-settings edited Unchanged in substance
link-vicinae-scripts unchanged
setup-identity new git config, gh auth login, SSH key
install-hardware new NVIDIA, akmods, secure boot, debloat, firmware — all conditional

Two things in this table changed while building it, and the reasons are worth keeping.

There is no install-cursors. The cursor theme is vendored into the repository and exposed by the same narrow per-directory symlink link-dotfiles already uses for icons and Vicinae themes, so it needs no stage of its own — and because link-dotfiles precedes change-settings, the ordering constraint that stage was invented to satisfy holds anyway.

install-hardware runs last, not fourth. The constraint was always "late", and fourth of eight is not late: MOK enrolment arms a prompt consumed at the next boot and a firmware update may ask for a reboot, so a machine that reboots out of this stage should already be completely configured — dotfiles linked, settings applied, identity set. Placing it after setup-identity costs nothing, since nothing later depends on it.

Ordering constraints that are not obvious and must not be broken:

  • link-dotfiles precedes change-settings, because change-settings sets a gsettings value naming a cursor theme. Setting it before the theme exists is not an error, but it does mean the first session renders with a fallback. Fonts are installed by install-packages, which already runs first.
  • setup-identity follows install-packages, because gh is installed there.
  • install-hardware is last, for the reason above. It also has to follow its own driver install: akmods generates the certificate that MOK enrolment enrols, so there is nothing to enrol until akmod-nvidia is on the machine.

Every stage stays independently re-runnable and independently failable, which is the existing contract and the reason Panama exists.

Fonts

There is no font stage. Terra — which install-packages already enables — packages every Nerd Font as an RPM (victormono-nerd-fonts and siblings, 3.5.0-1.fc44), and Fedora now packages the Microsoft core fonts as mscore-fonts. So the entire gap closes by adding five lines to desktop-packages:

  • victormono-nerd-fonts — required. It is what change-settings sets as monospace-font-name and what the shell draws every icon with.
  • jetbrainsmono-nerd-fonts, firamono-nerd-fonts, cascadiamono-nerd-fonts — Panama ships a font picker; a picker with one option is not a picker.
  • mscore-fonts — web development means checking pages that assume Arial and Georgia resolve.

This supersedes sunhat's approach, which downloaded release zips from GitHub with wget and unpacked them into ~/.local/share/fonts. That is precisely the install method this repository is trying to stop using, and it turned out to be unnecessary.

Cursors

oreo_blue_cursors is packaged nowhere — not Fedora, not RPM Fusion, not Terra. It comes from varlesh/oreo-cursors on GitHub, so the dnf/flatpak rule cannot apply and one of its exceptions has to.

The theme is vendored into the repository at config/local/share/icons/oreo_blue_cursors (3.3MB, 63 files, no symlinks) and exposed by link-dotfiles with the same narrow per-directory symlink it already uses for Panama's application icons and Vicinae themes. Downloading at install time was rejected: it would add a fourth network exception and a URL that can rot, which is the failure this spec exists to prevent. Vendoring costs 3.3MB in git once, and then the cursor is simply always there.

Only the blue variant ships, and nothing else needs changing to make that true: cursorTheme is a free string in the schema, and panama-desktop-style's catalog() enumerates the pointer themes actually present in the XDG icon directories. The picker therefore offers exactly what is installed, so shipping one theme instead of three removes options from the list rather than leaving it advertising themes that are not there.

Wallpaper

config/wallpapers/faroe_islands.jpg (4000x2444, 644K) is committed. link-dotfiles copies it to ~/Pictures/Wallpapers/ when absent, and seeds wallpaperPath in settings.json only when that key is still empty. An existing choice is never overwritten — the same rule every other seeding step in link-dotfiles already follows for kitty, tmux, GTK and hyprlock.

The assets contract

A new tests/quickshell/declared-assets-contract covers what the dependency contract structurally cannot. It fails when:

  • A font family named in config/ or in a gsettings set call is not installed by install-fonts
  • A cursor theme named in config/ is not installed by install-cursors
  • The default wallpaperPath names a file the repository does not ship
  • A package invoked by a settings-page helper or a GUI handoff is undeclared
  • A directory under config/dot/ is neither in link-dotfiles's dirs array nor given explicit handling

All four of today's gaps fail this test. That is the acceptance criterion for the test itself: it must be written against the broken tree and observed to fail before the fixes land.

It is named without a .sh extension from the start, while its siblings still carry one until phase 6. That inconsistency is deliberate and temporary — new files follow the convention immediately rather than being written wrong and renamed later.

GNOME excision

config/dot/forge is deleted and forge removed from link-dotfiles's dirs. The README's Desktops section is rewritten to describe one desktop. The comment in link-dotfiles explaining why GNOME and Hyprland coexist goes with it.

gnome-control-center is added to desktop-packages with a comment naming what depends on it, so a future reader does not mistake it for GNOME-session residue and remove it. gnome-settings-hyprland.desktop stays.

The panama command

Three subcommands are added to the existing dispatcher, which was written to grow this way:

  • panama doctor — front panama-doctor from a terminal, not only from Settings
  • panama test — run every contract under tests/, with a pass/fail summary. The suite is 119 files with no entry point and no mention in the README; it is the main safety net and it is currently invisible.
  • panama upgrade — re-run ./install from anywhere

Dropping the .sh extension

159 files: 110 under tests/quickshell, 47 under config/local/share/vicinae/scripts, 2 under tests/hypr. All are already executable and all already carry a bash shebang, so the rename is git mv plus three comment references and two lines in panama-settings-commands, which generates the Vicinae command files and globs for settings-*.sh.

The one real risk is Vicinae's script discovery. Its documentation states a script command needs a plain text file, the three @vicinae.* directives, a shebang, and the executable bit — no extension requirement. That is close enough to a guarantee to act on, but not close enough to skip verifying: after the rename, reload with vicinae cmd launch core:reload-scripts and confirm all 47 commands still resolve before the change is considered done. If discovery does key off the extension, the Vicinae directory is exempted and the reason recorded here.

Machine-specific configuration

Monitor layout, keybinds and input differences stay in gitignored local state — ~/.config/panama/settings.json, written by the Settings application. Panama ships defaults; a machine deviates locally. No per-hostname directories enter the repository, because that is what "cloneable by someone else" costs.

This is a decision to not build something. sunhat branched its Hyprland config on hostname and it was one of the things that made sunhat unmaintainable.

Ported from sunhat

Everything below blocks sunhat's deletion.

  • NVIDIA and secure bootakmod-nvidia, xorg-x11-drv-nvidia-cuda and MOK enrolment via mokutil --import. sunhat's version opened an editor to hand-edit grub; the replacement does not, because a step that requires a human mid-run is the failure this repository exists to avoid. It turned out not to need one: sunhat's editor step existed to delete duplicated kernel arguments, and grubby --update-kernel=ALL --args= replaces an argument that already exists rather than appending a second copy, so the duplicates cannot accumulate.

    Enrolment is likewise non-interactive, and did not need a compromise either. mokutil --generate-hash and --hash-file are exactly the pair for this: the interview asks for the password, hashes it immediately, and records only the hash — so the plaintext never reaches the answers file, the environment, or a command line, and the stage still runs without prompting.

    rebuild_nvidia_kmod was not ported. akmods already rebuilds on kernel updates through its own systemd unit, and a helper that duplicates that is one more thing to keep true.

  • Fedora debloat — behind an interview question. sunhat's list was gnome-contacts, gnome-tour, gnome-maps, totem and three LibreOffice components; on Fedora 44 totem has been replaced by showtime and LibreOffice is not preinstalled at all, so the list is gnome-contacts, gnome-tour, gnome-maps and showtime. A package that is not installed is never passed to dnf, which is what lets the list outlive a Fedora release rather than starting to fail on one.

  • Firmwarefwupdmgr refresh and update, behind an interview question. Panama's Updates page covers this after installation; the installer covers the first run.

  • Applications — the extras mechanism, and only the applications explicitly agreed. sunhat's ~70 installers are a list of candidates to review, not a manifest to port; most of them install by methods that no longer work. Reviewing them is its own task, done one application at a time against the dnf/flatpak rule above, and it does not block anything else in this spec.

  • Firefox — the package itself, which Panama does not currently install, plus sunhat's themes/firefox userChrome. Helium is the daily driver; Firefox stays because web development means checking a second engine. If Helium supports userChrome, it gets the equivalent treatment.

Deliberately not ported: sunhat's configs/docker/compose_files. Those are server workloads — caddy, gitea, homeassistant, nextcloud_aio, pihole, vaultwarden, wireguard, searxng — and belong to whatever runs them, not to a workstation installer. They remain recoverable from sunhat's git history.

Repository hygiene

  • Done. The seven branches that appeared unmerged were audited with git cherry -v main <branch>. Six of them — feat/panama-displays, feat/panama-settings-pages, feat/settings-home-completion, feat/settings-sound, fix/settings-backup-complete and fix/settings-sidebar-scroll — had every commit marked patch-equivalent to one already in main, and every file they added was verified present in main. They looked unmerged only because the work re-landed through a different route. Deleted, along with their worktrees.
  • feat/roadmap-completion is the sole branch with unlanded work: 5 commits, 13 files absent from main, 62 commits behind. It carries a theme-profile system, a dock context menu, overview-thumbnail capture reliability, and wallpaper/lock hardening, with its own spec and plan. It does not conflict with this work — it depends on it, since its lock generator falls back to the shipped wallpaper this spec makes exist. Rebasing it is its own task.
  • Six worktrees remain on branches already merged to main (home-accessories-customization, panama-commands, panama-displays-review, panama-settings-home-phone, settings-notification-rules, settings-ownership). They are clutter, not risk; pruning them stays in phase 6.
  • Guard the .cargo/env source in config/bash/.bashrc and remove the Codex block.
  • Rewrite the README: one desktop, the real install stages, and the test suite.
  • Audit docs/settings.md (21K) for claims that no longer hold.

Phases

Each phase is independently shippable and independently useful.

  1. Install correctness — fonts and the four other undeclared packages, the vendored cursor theme, the shipped wallpaper, the .bashrc fixes, and the assets contract. Closes every gap that would visibly break a fresh machine.
  2. The interview — up-front prompts, hardware detection, panama-doctor at the end of the run.
  3. Hardware flows — NVIDIA, secure boot, debloat, firmware.
  4. Applications and theming — the extras menu mechanism, Firefox and its theme. The menu ships with the categories agreed at the time and nothing more; filling it is ongoing work, one application per decision, not part of this phase.
  5. GNOME excision and documentation — Forge removal, README rewrite, docs/settings.md audit.
  6. Hygiene — the .sh rename, worktree pruning, the branch audit, and the three new panama subcommands.

Verification

Panama is not going to be tested by reinstalling Fedora. Confidence comes from three places instead:

  • The assets contract, which encodes each gap found here as a test that fails on the broken tree.
  • The hardware contract, for the one stage that cannot be verified by running it. install-hardware installs a proprietary driver and queues a Secure Boot enrolment, and the machine Panama is developed on is an AMD desktop; so every privileged command is stood in on PATH and the contract asserts which answer led to which call. That covers the decisions, which is the part that can be wrong. It does not cover whether akmod-nvidia builds, and does not claim to — the first real NVIDIA machine is still the first real test.
  • panama doctor at the end of every install, which reports what is actually running rather than what was attempted.
  • panama test, which makes the existing 119 contracts runnable in one command for the first time.

The open risk this leaves is anything that breaks only on a genuinely clean system and that none of the three notices — a repository that fails to enable, a package renamed between Fedora releases. That risk is accepted; a VM run can settle it later if the fixes here prove insufficient.