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
159 lines
6.6 KiB
Bash
Executable File
159 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The machine role: the one interview answer Panama keeps.
|
|
#
|
|
# What is pinned, and why each half matters:
|
|
#
|
|
# * The interview asks the role first and a server is never asked the
|
|
# desktop's questions -- hardware and extras feed stages the server path
|
|
# does not run, and a question nothing consumes is a control that lies.
|
|
# * `--server` presets the answer without a prompt, because the fresh-VPS
|
|
# path runs from a curl with nobody to interview twice.
|
|
# * setup/lib/machine-role reads env over file over default, and records
|
|
# only values it can read back -- an unreadable role file must degrade to
|
|
# desktop, never to an error, because every pre-role machine has none.
|
|
# * install runs different stages per role, and the server list must never
|
|
# silently reacquire a desktop stage (or the reverse).
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
interview="$repo_dir/setup/scripts/interview"
|
|
install_script="$repo_dir/install"
|
|
machine_role="$repo_dir/setup/lib/machine-role"
|
|
boot="$repo_dir/boot"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
# ── The helper, sandboxed ────────────────────────────────────────────────────
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
role_of() {
|
|
env -u PANAMA_ROLE XDG_STATE_HOME="$work/state" ${1:+PANAMA_ROLE="$1"} \
|
|
bash -c "source '$machine_role'; panama_role"
|
|
}
|
|
|
|
[[ "$(role_of "")" == desktop ]] \
|
|
|| note 'with no file and no env, the role is not desktop'
|
|
|
|
env XDG_STATE_HOME="$work/state" \
|
|
bash -c "source '$machine_role'; panama_role_record server"
|
|
[[ "$(role_of "")" == server ]] \
|
|
|| note 'a recorded server role does not read back'
|
|
|
|
[[ "$(role_of desktop)" == desktop ]] \
|
|
|| note 'an exported PANAMA_ROLE does not win over the recorded file'
|
|
|
|
printf 'gibberish\n' >"$work/state/panama/role"
|
|
[[ "$(role_of "")" == desktop ]] \
|
|
|| note 'a role file with an unknown value does not degrade to desktop'
|
|
|
|
env XDG_STATE_HOME="$work/state" \
|
|
bash -c "source '$machine_role'; panama_role_record nonsense"
|
|
[[ "$(role_of "")" == desktop ]] \
|
|
|| note 'recording an unknown value does not degrade to desktop'
|
|
|
|
# ── The interview, per role ──────────────────────────────────────────────────
|
|
#
|
|
# The same stub the interview contract stands up, plus `choose` so the role
|
|
# question is answerable. GUM_STUB_CHOOSE also feeds the extras checklist on
|
|
# a desktop run, which is why the desktop case leaves it empty.
|
|
|
|
stub_dir="$work/bin"
|
|
mkdir -p "$stub_dir"
|
|
cat >"$stub_dir/gum" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
case "$1" in
|
|
input) printf '%s\n' "$GUM_STUB_INPUT" ;;
|
|
confirm) [[ "$GUM_STUB_CONFIRM" == yes ]] ;;
|
|
choose) printf '%s\n' "$GUM_STUB_CHOOSE" ;;
|
|
style) shift; printf '%s\n' "${@: -1}" ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
STUB
|
|
chmod +x "$stub_dir/gum"
|
|
|
|
answers="$work/answers"
|
|
|
|
ask_interview() {
|
|
: >"$answers"
|
|
GUM_STUB_INPUT="x" GUM_STUB_CONFIRM=yes GUM_STUB_CHOOSE="${2:-}" \
|
|
PANAMA_ANSWERS="$answers" PANAMA_ROLE_PRESET="${1:-}" \
|
|
PATH="$stub_dir:$PATH" bash "$interview" >/dev/null 2>&1
|
|
}
|
|
|
|
answered() {
|
|
( # shellcheck source=/dev/null
|
|
source "$answers"; printf '%s' "${!1:-}" )
|
|
}
|
|
|
|
ask_interview "" server \
|
|
|| note 'the interview fails when the role question answers server'
|
|
[[ "$(answered PANAMA_ROLE)" == server ]] \
|
|
|| note 'choosing server does not record PANAMA_ROLE=server'
|
|
[[ "$(answered PANAMA_EXTRAS)" == "" ]] \
|
|
|| note 'a server was asked the extras question'
|
|
[[ "$(answered PANAMA_NVIDIA)" == no ]] \
|
|
|| note 'a server run does not record the hardware defaults'
|
|
|
|
ask_interview "" "" \
|
|
|| note 'the interview fails when the role question is escaped'
|
|
[[ "$(answered PANAMA_ROLE)" == desktop ]] \
|
|
|| note 'an escaped role question does not default to desktop'
|
|
|
|
ask_interview server desktop \
|
|
|| note 'the interview fails under a --server preset'
|
|
[[ "$(answered PANAMA_ROLE)" == server ]] \
|
|
|| note 'PANAMA_ROLE_PRESET=server does not win: the prompt was asked anyway'
|
|
|
|
# ── install runs the right stages ────────────────────────────────────────────
|
|
#
|
|
# Read from the source rather than run: the stage lists are data, and what
|
|
# matters is which names each role's list carries.
|
|
|
|
# Only the two literal lists: the upgrade filter reassigns STAGES from a
|
|
# variable, which is not a role's stage list.
|
|
server_stages="$(sed -n 's/^ STAGES=(\(.*\))$/\1/p' "$install_script" | grep '^install-packages' | head -1)"
|
|
desktop_stages="$(sed -n 's/^ STAGES=(\(.*\))$/\1/p' "$install_script" | grep '^install-packages' | tail -1)"
|
|
|
|
for stage in install-packages link-dotfiles link-user setup-server link-server setup-identity; do
|
|
grep -qw "$stage" <<<"$server_stages" \
|
|
|| note "the server stage list is missing $stage"
|
|
done
|
|
for stage in install-hardware change-settings link-skills link-vicinae-scripts; do
|
|
grep -qw "$stage" <<<"$server_stages" \
|
|
&& note "the server stage list acquired the desktop stage $stage"
|
|
done
|
|
for stage in install-packages link-dotfiles link-skills link-user change-settings link-vicinae-scripts setup-identity install-hardware; do
|
|
grep -qw "$stage" <<<"$desktop_stages" \
|
|
|| note "the desktop stage list lost $stage"
|
|
done
|
|
|
|
grep -q -- '--server)' "$install_script" \
|
|
|| note 'install does not accept --server'
|
|
grep -q 'panama_role_record' "$install_script" \
|
|
|| note 'install never records the role, so panama update cannot know it'
|
|
|
|
# ── boot's front door ────────────────────────────────────────────────────────
|
|
|
|
grep -q -- '--server) SERVER=1' "$boot" \
|
|
|| note 'boot does not accept --server'
|
|
grep -q 'not root' "$boot" \
|
|
|| note 'boot no longer refuses a root run without --server'
|
|
grep -q 'runuser' "$boot" \
|
|
|| note 'the root bootstrap does not hand off to the created user'
|
|
|
|
# ── Report ───────────────────────────────────────────────────────────────────
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
|
|
printf 'role contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'role contract: PASS\n'
|