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
131 lines
6.0 KiB
Bash
Executable File
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'
|