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
This commit is contained in:
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/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'
|
||||
Reference in New Issue
Block a user