clawdie-iso/firstboot/firstboot.sh

327 lines
14 KiB
Bash

#!/bin/sh
# Clawdie-AI Firstboot Orchestrator
# Runs once on first boot (rc.d/clawdie-firstboot, REQUIRE: NETWORKING LOGIN)
# Dispatches to clawdie-shell-*.sh modules for cloud or baremetal path
#
# Usage:
# firstboot.sh — Normal run (wizard on baremetal, pre-baked on cloud)
# firstboot.sh --resume — Skip already-completed steps, continue from last failure
# firstboot.sh --reset — Clear progress and start over from the beginning
set -eu
SHARE="${SHARE:-/usr/local/share/clawdie-iso}"
LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}"
PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}"
RC_CONF="${RC_CONF:-/etc/rc.conf}"
# ── Arg parsing ────────────────────────────────────────────────────────────
RESUME=0
case "${1:-}" in
--resume) RESUME=1 ;;
--reset)
rm -f "$PROGRESS_FILE"
echo "$(date '+%H:%M:%S') [firstboot] Progress reset — starting over" | tee -a "$LOG_FILE"
;;
--help|-h)
echo "Usage: firstboot.sh [--resume|--reset]"
echo " --resume Skip completed steps, continue from last failure"
echo " --reset Clear progress file and start from the beginning"
exit 0
;;
esac
log_msg() { echo "$(date '+%H:%M:%S') $1" | tee -a "$LOG_FILE"; }
# ── Checkpoint helpers ─────────────────────────────────────────────────────
# Mark a step done in the progress file
step_done() {
echo "$1" >> "$PROGRESS_FILE"
}
# Return 0 (true) if the step was already completed
step_completed() {
[ "$RESUME" -eq 1 ] && grep -qx "$1" "$PROGRESS_FILE" 2>/dev/null
}
# Run a module function with checkpoint guard.
# Usage: run_step <step_name> <function> [description] [step_num]
run_step() {
_step="$1"
_fn="$2"
_desc="${3:-$_fn}"
_step_num="${4:-0}" # Optional: step number for progress tracking
if step_completed "$_step"; then
log_msg "[firstboot] Skipping $_step (already completed)"
[ "$_step_num" -gt 0 ] && echo "PROGRESS=$_step_num" >> "$PROGRESS_FILE"
return 0
fi
log_msg "[firstboot] Running: $_desc"
"$_fn"
step_done "$_step"
[ "$_step_num" -gt 0 ] && echo "PROGRESS=$_step_num" >> "$PROGRESS_FILE"
}
# Run a step only when the current boot mode is in the allowed list.
# Usage: run_step_if "<modes>" <step_name> <function> [description] [step_num]
# Modes: space-separated list of CLAWDIE_BOOT_MODE values (install upgrade repair)
# Steps not applicable to the current mode are marked done (so --resume works).
run_step_if() {
_allowed="$1"; shift
_step="$1"
_mode="${CLAWDIE_BOOT_MODE:-install}"
if echo "$_allowed" | grep -qw "$_mode"; then
run_step "$@"
else
if ! step_completed "$_step"; then
log_msg "[firstboot] Skipping $_step (not applicable in $_mode mode)"
step_done "$_step"
fi
fi
}
# ── Set package path to bundled packages on HDD ──────────────────────────────
# After bsdinstall, packages live at SHARE/packages (not /mnt/media)
export USB_PKG_PATH="${SHARE}/packages"
# ── Prevent modules from auto-running when sourced ─────────────────────────
export SHELL_GPU_TEST=1
export SHELL_NVIDIA_TEST=1
export SHELL_PKG_TEST=1
export SHELL_ENV_TEST=1
export SHELL_DEPLOY_TEST=1
export SHELL_NPM_GLOBALS_TEST=1
export SHELL_TAILSCALE_TEST=1
export SHELL_ZFS_TEST=1
export SHELL_PF_TEST=1
export SHELL_DESKTOP_TEST=1
export SETUP_IMPORT_TEST=1
# shell-ssh.sh and shell-system.sh use case/${0##*/} — no flag needed
# ── Source modules (functions only, nothing runs yet) ─────────────────────────
. "${SHARE}/build.cfg"
. "${SHARE}/firstboot/setup-import.sh"
. "${SHARE}/firstboot/shell-zfs.sh"
. "${SHARE}/firstboot/shell-gpu.sh"
. "${SHARE}/firstboot/shell-nvidia.sh"
. "${SHARE}/firstboot/shell-pkg.sh"
. "${SHARE}/firstboot/shell-ssh.sh"
. "${SHARE}/firstboot/shell-env.sh"
. "${SHARE}/firstboot/shell-system.sh"
. "${SHARE}/firstboot/shell-desktop.sh"
. "${SHARE}/firstboot/shell-pf.sh"
. "${SHARE}/firstboot/shell-tailscale.sh"
. "${SHARE}/firstboot/shell-npm-globals.sh"
. "${SHARE}/firstboot/shell-deploy.sh"
# ── Load GUI config if present ───────────────────────────────────────────────
SETUP_HANDOFF_LOADED=0
if [ -f "/tmp/clawdie-install.conf" ]; then
log_msg "[firstboot] Loading GUI installer configuration"
. "/tmp/clawdie-install.conf"
step_done "wizard"
SETUP_HANDOFF_LOADED=1
elif [ -f "/var/db/clawdie-installer/clawdie-handoff.sealed" ]; then
log_msg "[firstboot] Loading installed handoff payload"
. "/var/db/clawdie-installer/clawdie-handoff.sealed"
step_done "wizard"
SETUP_HANDOFF_LOADED=1
fi
log_msg "[firstboot] Starting — target: ${TARGET:-baremetal}${RESUME:+, resume mode}"
log_msg "[firstboot] ISO ${ISO_VERSION:-unknown}-${BUILD_CHANNEL:-unknown}; Clawdie-AI ${CLAWDIE_REF:-unknown}@${CLAWDIE_AI_COMMIT:-unknown}"
# ── Import setup.txt/system.env from USB config partition ───────────────────
if [ "$SETUP_HANDOFF_LOADED" -eq 1 ]; then
log_msg "[firstboot] Skipping setup-import (installer handoff already loaded)"
step_done "setup-import"
else
run_step "setup-import" clawdie_setup_import_load "Load first-boot setup from USB partition"
fi
# ── ZFS pool detection (baremetal only) ───────────────────────────────────
# Runs early to decide boot mode: install | upgrade | maintenance
# Maintenance mode exec's away and never returns.
CLAWDIE_BOOT_MODE="${CLAWDIE_BOOT_MODE:-install}"
if [ "${TARGET:-baremetal}" != "vps" ]; then
run_step "zfs" clawdie_shell_zfs_detect "ZFS pool detection"
fi
export CLAWDIE_BOOT_MODE
# ── Collect configuration ──────────────────────────────────────────────────
if step_completed "wizard"; then
log_msg "[firstboot] Skipping wizard (already completed)"
elif [ "$CLAWDIE_BOOT_MODE" = "upgrade" ]; then
# Upgrade: import existing pool, load .env from previous install
log_msg "[firstboot] Upgrade mode — loading existing configuration"
kldload zfs 2>/dev/null || true
zpool import "$POOL_NAME" 2>/dev/null || true
_existing_env="/home/clawdie/clawdie-ai/.env"
if [ -f "$_existing_env" ]; then
ENV_FILE="${ENV_FILE:-/home/clawdie/.env}"
cp "$_existing_env" "$ENV_FILE"
log_msg "[firstboot] Loaded existing .env from previous install"
else
log_msg "[firstboot] WARNING: No existing .env found — falling through to wizard"
fi
step_done "wizard"
elif [ "${TARGET:-baremetal}" = "vps" ]; then
# VPS: all values must be pre-baked in build.cfg — validate
[ -z "${ASSISTANT_NAME:-}" ] && log_msg "ERROR: ASSISTANT_NAME not baked" && exit 1
[ -z "${AGENT_DOMAIN:-}" ] && log_msg "ERROR: AGENT_DOMAIN not baked" && exit 1
[ -z "${TZ:-}" ] && log_msg "ERROR: TZ not baked" && exit 1
log_msg "[firstboot] VPS — pre-baked config OK"
step_done "wizard"
else
# Baremetal: minimal wizard — identity, network, keys only
# All jails (db, git/forgejo, cms) are provisioned by default.
# API keys are deferred to web UI on first desktop login.
_dialog() { bsddialog --backtitle "Clawdie-AI Setup" "$@" 2>&1; }
_dialog --msgbox "\
EXPERIMENTAL BUILD
This is pre-release software.
Not recommended for production use.
Data loss or service interruption possible.
By continuing, you assume all risks." 12 60
# Tailscale (recommended, but optional)
if _dialog --yesno \
"Enable Tailscale for secure remote access?\n\n" \
"Tailscale creates a private network for SSH access.\n" \
"Without it, SSH will be exposed on public port 22.\n\n" \
"Recommended: Yes (you can add auth key later if needed)" 14 70; then
FEATURE_TAILSCALE="YES"
TAILSCALE_AUTHKEY=$(_dialog --passwordbox \
"Tailscale device auth key (tskey-...).\n\n" \
"Leave blank to skip auth (you can run 'tailscale up' later).\n" \
"Generate at: https://login.tailscale.com/admin/settings/keys" 13 72 "")
if [ -z "$TAILSCALE_AUTHKEY" ]; then
_dialog --msgbox "No auth key provided.\n\nTailscale will be installed but not authenticated.\nRun 'tailscale up' after first boot to connect." 10 60
fi
else
FEATURE_TAILSCALE="NO"
TAILSCALE_AUTHKEY=""
_dialog --msgbox "WARNING: SSH will be publicly accessible on port 22.\n\nYou are responsible for securing network access." 10 60
fi
ASSISTANT_NAME=$(_dialog --inputbox "Assistant name:" 8 50 "Clawdie")
# Derive default domain from assistant name (e.g., Clawdie → clawdie.home.arpa)
_agent_name_lower=$(echo "$ASSISTANT_NAME" | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9]//g')
_default_domain="${_agent_name_lower}.home.arpa"
AGENT_DOMAIN=$(_dialog --inputbox \
"Domain zone (public or local):" 8 60 "$_default_domain")
if command -v route >/dev/null 2>&1 && command -v ifconfig >/dev/null 2>&1; then
HOST_IF="$(route -n get default 2>/dev/null | awk '/interface:/ { print $2; exit }')"
HOST_IPS=""
if [ -n "$HOST_IF" ]; then
HOST_IPS="$(ifconfig "$HOST_IF" 2>/dev/null | awk '/inet / { print $2 }')"
fi
if [ -z "$HOST_IPS" ]; then
HOST_IPS="$(ifconfig 2>/dev/null | awk '/inet / && $2 != "127.0.0.1" { print $2 }')"
fi
if [ -n "$HOST_IPS" ]; then
HOST_IPS_LINE="$(echo "$HOST_IPS" | tr '\n' ' ' | sed 's/ $//')"
_dialog --msgbox "\
DNS note: If you use *.home.arpa, create an A record for
${AGENT_DOMAIN} pointing to this host IP.
Detected IP(s): ${HOST_IPS_LINE}" 10 70
fi
fi
TZ=$(_dialog --inputbox \
"Timezone (e.g. Europe/Ljubljana):" 8 50 "UTC")
SYSTEM_LOCALE=$(_dialog --inputbox \
"Locale (e.g. en_US.UTF-8):" 8 50 "en_US.UTF-8")
KEYMAP=$(_dialog --inputbox \
"Console keymap (e.g. us):" 8 50 "us")
[ -z "${SYSTEM_LOCALE:-}" ] && SYSTEM_LOCALE="en_US.UTF-8"
DISPLAY_LOCALE="${SYSTEM_LOCALE}"
ASSISTANT_LOCALE="${SYSTEM_LOCALE}"
SSH_PUBLIC_KEY=$(_dialog --inputbox \
"SSH public key (optional — paste ssh-ed25519 or ssh-rsa):" 12 70 "")
# Defaults: all jails enabled, no local LLM (can be enabled post-install)
: "${AGENT_GENDER:=f}"
FEATURE_GIT="YES"
FEATURE_GITEA="NO"
CODE_HOSTING_MODE="git"
LOCAL_LLM_PROVIDER="none"
FEATURE_OLLAMA="NO"
FEATURE_LLAMA_CPP="NO"
FEATURE_OLLAMA_HPP="NO"
# Summary screen
SUMMARY_MSG="Configuration Summary:\n\n"
SUMMARY_MSG+="Name: ${ASSISTANT_NAME}\n"
SUMMARY_MSG+="Domain: ${AGENT_DOMAIN}\n"
SUMMARY_MSG+="Timezone: ${TZ}\n"
SUMMARY_MSG+="Locale: ${SYSTEM_LOCALE}\n"
SUMMARY_MSG+="Keymap: ${KEYMAP}\n"
SUMMARY_MSG+="SSH key: $([ -n "$SSH_PUBLIC_KEY" ] && echo "✓ Provided" || echo "✗ None")\n"
if [ "${FEATURE_TAILSCALE}" = "YES" ]; then
if [ -n "$TAILSCALE_AUTHKEY" ]; then
SUMMARY_MSG+="Tailscale: ✓ Enabled (auth key provided)\n"
else
SUMMARY_MSG+="Tailscale: ⚠ Enabled (no auth key - run 'tailscale up' later)\n"
fi
else
SUMMARY_MSG+="Tailscale: ✗ Disabled (SSH on public port 22)\n"
fi
SUMMARY_MSG+="Provider keys: configure after first boot in the controlplane\n"
SUMMARY_MSG+="Telegram: configure after first boot in the controlplane\n"
SUMMARY_MSG+="\nProceed with installation?"
if ! _dialog --yesno "$SUMMARY_MSG" 16 70; then
_dialog --msgbox "Installation cancelled. Rebooting..." 6 40
reboot
fi
step_done "wizard"
fi
export CLAWDIE_BOOT_MODE POOL_NAME
export ASSISTANT_NAME AGENT_GENDER AGENT_DOMAIN TZ SSH_PUBLIC_KEY
export HOSTNAME INSTALL_MODE PROFILE
export SYSTEM_LOCALE DISPLAY_LOCALE ASSISTANT_LOCALE KEYMAP
export EMBED_BASE_URL EMBED_MODEL EMBED_API_KEY EMBED_DIMENSIONS
export FEATURE_TAILSCALE TAILSCALE_AUTHKEY
export CODE_HOSTING_MODE FEATURE_GIT FEATURE_GITEA FORGEJO_DISK_ESTIMATE
export LOCAL_LLM_PROVIDER FEATURE_OLLAMA FEATURE_LLAMA_CPP FEATURE_OLLAMA_HPP
export OLLAMA_RAM_ESTIMATE OLLAMA_DISK_ESTIMATE LLAMA_CPP_RAM_ESTIMATE LLAMA_CPP_DISK_ESTIMATE
export USB_LLM_MODELS_PATH
export ZFS_POOL ZFS_LAYOUT ZFS_DATA_DISKS ZFS_HOT_SPARES ZFS_PREFIX
export NETWORK_EXTERNAL_IF NETWORK_INTERNAL_IF TAILSCALE_IF ZFS_DISKS ZFS_SPARE_DISKS GPU_DEVICE SND_DEVICE
# ── Run modules ────────────────────────────────────────────────────────────
log_msg "[firstboot] Running modules..."
# Module execution matrix — controls which modules run in each mode.
# fresh install: all modules
# upgrade: pkg + env (append-only) + npm-globals + deploy only
# repair: deploy only (targeted repair actions inside shell-deploy)
#
# GPU, SSH, system, desktop, PF, Tailscale are skipped on upgrade/repair —
# they would reset keys, overwrite rc.conf, and undo operator customisations.
run_step_if "install" "gpu" clawdie_shell_gpu_detect "GPU driver detection" 1
run_step_if "install" "nvidia" clawdie_shell_nvidia_detect "NVIDIA version selection" 2
run_step_if "install upgrade" "pkg" clawdie_shell_pkg_setup "Package repo configuration" 3
run_step_if "install" "ssh" clawdie_shell_ssh_setup "SSH keys + system passwords" 4
run_step_if "install upgrade" "env" clawdie_shell_env_generate "Generate .env with secrets" 5
run_step_if "install" "system" clawdie_shell_system_config "Hostname, rc.conf, services" 6
run_step_if "install" "desktop" clawdie_shell_desktop_detect "Desktop enablement" 7
run_step_if "install" "pf" clawdie_shell_pf "PF firewall + jail NAT" 8
run_step_if "install" "tailscale" clawdie_shell_tailscale_setup "Tailscale remote access" 8
run_step_if "install upgrade" "npm-globals" clawdie_shell_npm_globals_install "Install bundled npm CLIs (pi)" 8
run_step_if "install upgrade" "deploy" clawdie_shell_deploy "Extract tarball + just install" 8
log_msg "[firstboot] Complete."
log_msg "[firstboot] Aider (primary harness): aider --help"
log_msg "[firstboot] Pi (primary harness): pi --help"
log_msg "[firstboot] Optional: Codex CLI (headless): codex login --device-auth"
log_msg "[firstboot] Optional: Codex CLI (API key): printenv OPENAI_API_KEY | codex login --with-api-key"