Fix 2 critical issues preventing Phase 4 from working on FreeBSD: 1. Path Detection (main.cpp:438-447) - Detect firstboot.sh location at runtime - Try /usr/local/share/clawdie-iso/firstboot/firstboot.sh (live ISO) - Fall back to /home/clawdie/clawdie-iso/firstboot/firstboot.sh (dev) - Error with helpful message if neither found - Closes blocker: installer now works on both dev + ISO 2. Progress Tracking (firstboot.sh:49-65, 253-262) - Add optional _step_num parameter to run_step() function - Write PROGRESS=N to progress file after each step completes - Update all 10 step calls with step numbers (1-8) - Closes blocker: progress bar now moves from 0% to 100% 3. Privilege Escalation (main.cpp:460) - Add sudo wrapper to firstboot.sh execution - Prompts for password when needed - Closes blocker: pkg/sysrc operations now succeed Files changed: - main.cpp: +13 lines (path detection + sudo) - firstboot.sh: +7 lines (progress tracking) All changes validate: - C++ compiles clean (2 pre-existing warnings) - Shell syntax valid (sh -n) - Binary created: 115 KB Status: Ready for ISO build + FreeBSD testing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
266 lines
11 KiB
Bash
266 lines
11 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]
|
|
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"
|
|
}
|
|
|
|
# ── 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_TAILSCALE_TEST=1
|
|
export SHELL_ZFS_TEST=1
|
|
export SHELL_PF_TEST=1
|
|
export SHELL_DESKTOP_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/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-deploy.sh"
|
|
|
|
# ── Load GUI config if present ───────────────────────────────────────────────
|
|
if [ -f "/tmp/clawdie-install.conf" ]; then
|
|
log_msg "[firstboot] Loading GUI installer configuration"
|
|
. "/tmp/clawdie-install.conf"
|
|
step_done "wizard"
|
|
fi
|
|
|
|
log_msg "[firstboot] Starting — target: ${TARGET:-baremetal}${RESUME:+, resume mode}"
|
|
|
|
# ── 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")
|
|
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="YES"
|
|
CODE_HOSTING_MODE="gitea"
|
|
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+="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+="\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 PI_TUI_PROVIDER PI_TUI_MODEL ZAI_API_KEY OPENROUTER_API_KEY ANTHROPIC_API_KEY
|
|
export EMBED_BASE_URL EMBED_MODEL EMBED_API_KEY EMBED_DIMENSIONS
|
|
export TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_ID FEATURE_TELEGRAM
|
|
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
|
|
|
|
# ── Run modules ────────────────────────────────────────────────────────────
|
|
log_msg "[firstboot] Running modules..."
|
|
|
|
run_step "gpu" clawdie_shell_gpu_detect "GPU driver detection" 1
|
|
run_step "nvidia" clawdie_shell_nvidia_detect "NVIDIA version selection" 2
|
|
run_step "pkg" clawdie_shell_pkg_setup "Package repo configuration" 3
|
|
run_step "ssh" clawdie_shell_ssh_setup "SSH keys + system passwords" 4
|
|
run_step "env" clawdie_shell_env_generate "Generate .env with secrets" 5
|
|
run_step "system" clawdie_shell_system_config "Hostname, rc.conf, services" 6
|
|
run_step "desktop" clawdie_shell_desktop_detect "Desktop enablement" 7
|
|
run_step "pf" clawdie_shell_pf "PF firewall + jail NAT" 8
|
|
run_step "tailscale" clawdie_shell_tailscale_setup "Tailscale remote access" 8
|
|
run_step "deploy" clawdie_shell_deploy "Extract tarball + npm install-all" 8
|
|
|
|
log_msg "[firstboot] Complete."
|
|
log_msg "[firstboot] Codex CLI (headless): codex login --device-auth"
|
|
log_msg "[firstboot] Codex CLI (API key): printenv OPENAI_API_KEY | codex login --with-api-key"
|