#!/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 [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 "" [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"