Files
Panama/tests/setup/interview-contract
T
Gabriel Brown f33da41cc6 Panama learns what a server is: from a root login to running containers
A machine's role is now the interview's first question and the one answer
Panama records. Servers get the same shell minus the screen: core packages,
nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless
ports from 80, firewalld, the nginx-bridge network, and a nightly image
updater that replaced watchtower for cause.

server/containers/ carries junior's 23 compose services -- secrets moved to
per-machine .env files that never enter this public repo, every transformed
compose proven to render byte-identical to what is live. 'panama server'
enables, disables and relinks them; nothing here restarts a running service.
'boot --server' walks a fresh VPS from its root login to a normal install.

Five new contracts pin the secrets rule, the catalog's shape, panama-server's
behavior, the role plumbing, and the dotfile classification.

Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
2026-08-25 23:11:49 -04:00

131 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# The interview asks, the stages consume, and nothing survives the run.
#
# Three properties matter enough to pin:
#
# 1. Every question maps to a stage that reads its answer. A prompt whose
# answer nothing consumes is a control that lies -- the same defect this
# repository refused to ship on the SSH Keys page -- and it is an easy one
# to introduce, because asking is cheap and wiring up is not.
# 2. Every answer a stage reads is one the interview asks. The reverse gap is
# quieter and worse: the stage silently takes its fallback forever.
# 3. The answers file is deleted on every exit path. It carries an email
# address, and it is transient by design -- there is deliberately no
# remembered state between runs.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
interview="$repo_dir/setup/scripts/interview"
install_script="$repo_dir/install"
findings=()
note() { findings+=("$1"); }
# ── 1 & 2. Questions and consumers agree ─────────────────────────────────────
asked="$(grep -oE '^record [A-Z_]+' "$interview" | awk '{print $2}' | sort -u)"
# Stages read answers as ${PANAMA_FOO:-default}; install re-exports them.
consumed="$(grep -rhoE '\$\{PANAMA_[A-Z_]+' "$repo_dir"/setup/scripts/* "$install_script" 2>/dev/null \
| sed 's/^\${//' | sort -u)"
# Not answers: paths the installer sets up for itself, plus PANAMA_MOK_CERT,
# which is the akmods certificate path with an override on it. Nothing sets that
# override outside the hardware contract, which needs a certificate it is allowed
# to create -- enrolling the real one to find out what happens is not a test.
# PANAMA_ROLE_PRESET is the --server flag on its way INTO the interview, not an
# answer out of it -- the answer it produces is PANAMA_ROLE, which is checked.
INFRASTRUCTURE='^(PANAMA_PATH|PANAMA_ANSWERS|PANAMA_BASH|PANAMA_DOT|PANAMA_OLD|PANAMA_APPLICATION_DIR|PANAMA_ICON_DIR|PANAMA_UNIT_DIR|PANAMA_CURSOR_DIR|PANAMA_WALLPAPER_DIR|PANAMA_MOK_CERT|PANAMA_ROLE_PRESET)$'
while read -r key; do
[[ -n "$key" ]] || continue
grep -qx "$key" <<<"$consumed" \
|| note "the interview asks for $key, but no stage ever reads it"
done <<<"$asked"
while read -r key; do
[[ -n "$key" ]] || continue
[[ "$key" =~ $INFRASTRUCTURE ]] && continue
grep -qx "$key" <<<"$asked" \
|| note "a stage reads $key, but the interview never asks for it"
done <<<"$consumed"
# ── 3. Nothing is left behind ────────────────────────────────────────────────
# Cleanup rides the EXIT trap; INT and TERM must exit explicitly, because a
# trap handler that merely cleans up lets bash carry on with the remaining
# stages after a Ctrl-C -- MOK enrollment and firmware included.
grep -q 'trap cleanup EXIT' "$install_script" \
|| note 'install does not arm a cleanup trap on EXIT'
grep -qE "trap 'exit [0-9]+' INT" "$install_script" \
|| note 'install does not exit on SIGINT, so Ctrl-C would keep installing'
grep -qE "trap 'exit [0-9]+' TERM" "$install_script" \
|| note 'install does not exit on SIGTERM, so a kill would keep installing'
grep -q 'rm -f "$PANAMA_ANSWERS"' "$install_script" \
|| note 'the cleanup trap does not delete the answers file'
grep -qE 'mktemp' "$install_script" \
|| note 'install does not create the answers file with mktemp'
# Declining must stop the run rather than count as one failed stage among five.
grep -qE 'if ! [A-Z_="$ ]*"\$PANAMA_PATH/setup/scripts/interview"; then' "$install_script" \
|| note 'install does not treat a declined interview as fatal'
# ── 4. A real run, with gum stubbed ──────────────────────────────────────────
#
# The interview is built on gum, which needs a terminal. Standing in a stub on
# PATH exercises the actual script -- its ordering, its quoting, and the file it
# writes -- rather than asserting things about its source text.
stub_dir="$(mktemp -d)"
answers_file="$(mktemp)"
trap 'rm -rf "$stub_dir" "$answers_file"' EXIT
cat >"$stub_dir/gum" <<'STUB'
#!/usr/bin/env bash
case "$1" in
input) printf '%s\n' "$GUM_STUB_INPUT" ;;
confirm) [[ "$GUM_STUB_CONFIRM" == yes ]] ;;
style) shift; printf '%s\n' "${@: -1}" ;;
*) exit 0 ;;
esac
STUB
chmod +x "$stub_dir/gum"
# A value containing a space and a quote, to prove %q survives being sourced.
GUM_STUB_INPUT="O'Brien Test" GUM_STUB_CONFIRM=yes \
PANAMA_ANSWERS="$answers_file" PATH="$stub_dir:$PATH" \
bash "$interview" >/dev/null 2>&1
interview_status=$?
(( interview_status == 0 )) || note "the interview exited $interview_status on a run that answered everything"
# Sourcing it back must reproduce the value exactly, not a mangled fragment.
(
# shellcheck source=/dev/null
source "$answers_file"
[[ "${PANAMA_GIT_NAME:-}" == "O'Brien Test" ]]
) || note 'an answer containing a quote and a space does not survive being sourced'
# Declining at the confirmation must fail, so install stops.
GUM_STUB_INPUT="x" GUM_STUB_CONFIRM=no \
PANAMA_ANSWERS="$answers_file" PATH="$stub_dir:$PATH" \
bash "$interview" >/dev/null 2>&1 \
&& note 'declining the final confirmation still exits zero, so install would proceed'
# Refusing to invent an answers path keeps the file where the caller can delete it.
PATH="$stub_dir:$PATH" bash "$interview" >/dev/null 2>&1 \
&& note 'the interview runs without PANAMA_ANSWERS instead of refusing'
# ── Report ───────────────────────────────────────────────────────────────────
if (( ${#findings[@]} > 0 )); then
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
printf 'interview contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'interview contract: PASS\n'