Files
Panama/setup/scripts/interview
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

188 lines
8.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Everything Panama needs to be told, asked before anything is installed.
#
# sunhat's failure mode was a question -- or a failure -- twenty minutes into a
# run, with a person needed at the keyboard to get past it. Walking away from an
# install meant coming back to a prompt that had been waiting an hour.
#
# So Panama asks first and then runs untouched. Everything interactive lives
# here, at the front, where the answers are cheap to change and nothing has been
# installed yet.
#
# Answers are NOT remembered between runs. There is no state file to go stale, and
# nothing personal is committed, which is what keeps this repository something
# somebody else could clone. Re-answering a handful of questions costs less than
# maintaining an answers file that drifts out of date.
#
# This asks only what a stage in this repository actually consumes. The extras
# checklist arrives with the stage that acts on it; a prompt whose answer nothing
# reads is a control that lies.
#
# The hardware questions name what was found rather than asking a person to
# recite their own machine, and they are not asked at all on a machine they
# would do nothing to. Detection alone would be worse: it would remove the
# ability to decline a proprietary driver on a machine that has the card.
set -uo pipefail
# install passes the path. Refusing to guess one keeps the answers where the
# caller can delete them, rather than somewhere this script invented.
answers="${PANAMA_ANSWERS:-}"
[[ -n "$answers" ]] || { printf 'interview: PANAMA_ANSWERS is not set; run this through ./install\n' >&2; exit 1; }
: >"$answers"
# %q so a value containing a space, a quote or a dollar sign survives being
# sourced by install exactly as it was typed.
record() { printf '%s=%q\n' "$1" "$2" >>"$answers"; }
heading() { gum style --bold --foreground 4 "$1"; }
ask() { gum input --header "$1" --placeholder "${2:-}"; }
yes_no() { gum confirm --default=false "$1"; }
# ── Machine ──────────────────────────────────────────────────────────────────
heading "This machine"
current_hostname="$(hostname)"
printf 'Current hostname: %s\n' "$current_hostname"
new_hostname=""
if yes_no "Change the hostname?"; then
new_hostname="$(ask "Hostname" "$current_hostname")"
fi
record PANAMA_HOSTNAME "$new_hostname"
# ── Identity ─────────────────────────────────────────────────────────────────
#
# Left blank, each of these keeps whatever git already has. That matters on a
# re-run: the prompts start empty every time by design, and an empty answer must
# not wipe a name that was already correct.
heading "Git identity"
printf 'Leave any of these blank to keep the current setting.\n'
git_name="$(ask "Git user.name")"
git_email="$(ask "Git user.email")"
git_editor="$(ask "Git editor" "nvim")"
record PANAMA_GIT_NAME "$git_name"
record PANAMA_GIT_EMAIL "$git_email"
record PANAMA_GIT_EDITOR "$git_editor"
# ── Accounts and keys ────────────────────────────────────────────────────────
#
# These two are asked only when they would do something. Checking whether a
# credential already exists is not the same as remembering a previous answer --
# it is refusing to ask a question whose answer is already on the machine.
heading "Accounts"
gh_login=no
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
printf 'GitHub CLI is already signed in.\n'
elif yes_no "Sign in to GitHub after packages are installed?"; then
gh_login=yes
fi
record PANAMA_GH_LOGIN "$gh_login"
ssh_key=no
if compgen -G "$HOME/.ssh/id_*.pub" >/dev/null 2>&1; then
printf 'An SSH key already exists.\n'
elif yes_no "Generate an SSH key?"; then
ssh_key=yes
fi
record PANAMA_SSH_KEY "$ssh_key"
# ── Hardware ─────────────────────────────────────────────────────────────────
#
# Each question names what was detected, so declining is a decision about this
# machine rather than an answer to a hypothetical. A machine with no NVIDIA card
# is never asked about drivers, and one with nothing to remove is never asked
# about removing it.
heading "Hardware"
nvidia=no
mok_hash=""
nvidia_card="$(lspci 2>/dev/null | grep -iE 'vga compatible|3d controller' | grep -i nvidia | sed 's/.*: //' | head -1)"
if [[ -n "$nvidia_card" ]]; then
if yes_no "Found $nvidia_card — install the NVIDIA driver?"; then
nvidia=yes
# Only asked where it does something. On a machine with Secure Boot off,
# akmods' signature is never checked and enrolling a key is ceremony.
if mokutil --sb-state 2>/dev/null | grep -qi 'secureboot enabled'; then
printf 'Secure Boot is on, so the driver must be signed with a key you enrol.\n'
printf 'The next boot will ask for this password on a blue screen.\n'
if yes_no "Enrol a machine owner key?"; then
# Hashed here and only the hash recorded. The password never
# reaches the answers file, the environment, or a command line
# -- mokutil takes a hash file precisely so it does not have to.
while :; do
first="$(gum input --password --header "MOK password")"
if [[ -z "$first" ]]; then
printf 'No password given; skipping enrolment.\n'
break
fi
second="$(gum input --password --header "MOK password again")"
if [[ "$first" == "$second" ]]; then
mok_hash="$(mokutil --generate-hash="$first")"
break
fi
printf 'Those did not match.\n'
done
unset first second
fi
fi
fi
else
printf 'No NVIDIA card found.\n'
fi
record PANAMA_NVIDIA "$nvidia"
record PANAMA_MOK_HASH "$mok_hash"
# The stage that removes them owns the list, so there is one copy of it.
debloat=no
mapfile -t removable < <("$(dirname "${BASH_SOURCE[0]}")/install-hardware" --debloat-list)
installed=()
for package in "${removable[@]}"; do
rpm -q "$package" >/dev/null 2>&1 && installed+=("$package")
done
if (( ${#installed[@]} > 0 )); then
if yes_no "Remove Fedora's preinstalled extras (${installed[*]})?"; then
debloat=yes
fi
fi
record PANAMA_DEBLOAT "$debloat"
firmware=no
if command -v fwupdmgr >/dev/null 2>&1; then
if yes_no "Update firmware with fwupdmgr?"; then
firmware=yes
fi
fi
record PANAMA_FIRMWARE "$firmware"
# ── Confirm ──────────────────────────────────────────────────────────────────
#
# The last chance to catch a typo before twenty minutes of package work that
# nobody is watching.
shown() { [[ -n "$1" ]] && printf '%s' "$1" || printf 'unchanged'; }
heading "Ready"
gum style --border rounded --padding "0 1" "$(
printf 'Hostname %s\n' "${new_hostname:-"$current_hostname (unchanged)"}"
printf 'Git name %s\n' "$(shown "$git_name")"
printf 'Git email %s\n' "$(shown "$git_email")"
printf 'Git editor %s\n' "$(shown "$git_editor")"
printf 'GitHub %s\n' "$([[ "$gh_login" == yes ]] && echo "sign in" || echo "no change")"
printf 'SSH key %s\n' "$([[ "$ssh_key" == yes ]] && echo "generate" || echo "no change")"
printf 'NVIDIA %s\n' "$([[ "$nvidia" == yes ]] && echo "install driver" || echo "no")"
printf 'Secure Boot %s\n' "$([[ -n "$mok_hash" ]] && echo "enrol a key" || echo "no change")"
printf 'Extras %s\n' "$([[ "$debloat" == yes ]] && echo "remove ${installed[*]}" || echo "keep")"
printf 'Firmware %s' "$([[ "$firmware" == yes ]] && echo "update" || echo "no")"
)"
if ! gum confirm --default=true "Install with these answers?"; then
printf 'interview: cancelled; nothing was installed.\n' >&2
exit 1
fi