327 lines
14 KiB
Bash
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"
|