clawdie-iso/firstboot/firstboot.sh
Sam & Claude 3d21e5fa36 feat: CI/CD pipeline, package lists, offline pkg-cache seeding
.forgejo/workflows/build.yml:
- Forgejo Actions pipeline: push to main + weekly cron + manual dispatch
- Two-stage: fetch-only (no root) → assemble ISO (root via sudo)
- Publishes ISO to CMS nginx downloads; Codeberg release entry (metadata only)
- Uploads packages/ as workflow artifact for pkg-cache seeding

packages/:
- pkg-list-host.txt     — host baseline (mirrors clawdie-ai infra/packages/)
- pkg-list-jails.txt    — union of all jail package lists
- pkg-list-desktop-base.txt — Xorg + drm base for all DEs
- pkg-list-xfce.txt / kde.txt / mate.txt / nvidia.txt — per-DE packages

build.sh:
- --fetch-only flag: downloads packages + memstick, no root, CI step 1
- Real pkg fetch loop: reads all pkg-list-*.txt, deduplicates, runs pkg fetch
- pkg repo step: generates offline repo metadata after fetch
- Resolves "latest" Clawdie version via Codeberg API

firstboot/firstboot.sh:
- Seeds zroot/pkg-cache from USB packages/ after desktop install
- npm run install-all runs fully offline — no internet needed for jails
- Creates ZFS dataset if not present, falls back to plain directory

runner/README.md:
- forgejo-runner install + register on FreeBSD
- Scoped sudoers entry (build.sh + publish.sh only)
- rc.d service setup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 20:04:21 +02:00

264 lines
9.4 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# firstboot.sh — Clawdie-AI first-boot setup wizard
#
# Runs once on first HDD boot via clawdie-firstboot rc.d service.
# Uses bsddialog for all interactive prompts.
#
# Flow:
# Tier 1 (always): pkg branch → DE selection → agent identity → domain
# Tier 2 (optional): pi provider profile selection
# Tier 3 (advanced): subnet, feature flags (press 'A' on summary screen)
# Secrets: all DB/service passwords auto-generated
# LLM keys: deferred — entered via pi on first agent run
# Summary screen: shows generated credentials before install starts
# Then: install packages → GPU setup → desktop → Clawdie-AI
set -e
SHARE="/usr/local/share/clawdie-iso"
CLAWDIE_HOME="/home/clawdie"
CLAWDIE_AI="${CLAWDIE_HOME}/clawdie-ai"
LOG="/var/log/clawdie-firstboot.log"
ENV_FILE="${CLAWDIE_AI}/.env"
. "${SHARE}/build.cfg"
# --- helpers ---
dialog() { bsddialog --backtitle "Clawdie-AI Setup" "$@" ; }
die() { echo "ERROR: $1" >&2; exit 1; }
gen_secret() {
openssl rand -base64 32 | tr -d '\n/+=' | head -c 32
}
write_env() {
KEY="$1"; VALUE="$2"
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
sed -i '' "s|^${KEY}=.*|${KEY}=${VALUE}|" "$ENV_FILE"
else
echo "${KEY}=${VALUE}" >> "$ENV_FILE"
fi
}
# --- screen: welcome ---
dialog --msgbox \
"\nWelcome to Clawdie-AI Setup.\n\nThis wizard will configure your system.\nAll packages install offline from this USB.\n\nPress Enter to begin." \
12 60
# --- tier 1: pkg branch ---
PKG_BRANCH=$(dialog --radiolist \
"Package repository branch:" 12 60 2 \
"latest" "Newest packages — best GPU/driver support [recommended]" on \
"quarterly" "Stable branch — security updates only" off \
3>&1 1>&2 2>&3) || die "Cancelled."
# Write pkg repo config
mkdir -p /usr/local/etc/pkg/repos
ABI=$(pkg config abi 2>/dev/null || echo "FreeBSD:15:amd64")
cat > /usr/local/etc/pkg/repos/FreeBSD.conf <<EOF
FreeBSD: {
url: "pkg+https://pkg.FreeBSD.org/${ABI}/${PKG_BRANCH}",
mirror_type: "srv",
enabled: yes
}
EOF
# Point pkg at offline USB repo for this install
cat > /usr/local/etc/pkg/repos/Clawdie-USB.conf <<EOF
Clawdie-USB: {
url: "file://${SHARE}/packages",
enabled: yes,
priority: 100
}
EOF
# --- tier 1: desktop environment ---
DESKTOP=$(dialog --radiolist \
"Desktop environment:" 14 60 4 \
"xfce" "XFCE — lightweight, recommended" on \
"kde" "KDE Plasma — full-featured, needs 8GB+" off \
"mate" "MATE — classic desktop, mid-weight" off \
"headless" "Headless — no desktop, server only" off \
3>&1 1>&2 2>&3) || die "Cancelled."
# --- tier 1: agent identity ---
ASSISTANT_NAME=$(dialog --inputbox \
"Assistant name (displayed to users):" 8 50 "Clawdie" \
3>&1 1>&2 2>&3) || die "Cancelled."
# Derive AGENT_NAME: lowercase, alphanumeric + hyphen only
AGENT_NAME=$(echo "$ASSISTANT_NAME" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/-*$//')
AGENT_DOMAIN=$(dialog --inputbox \
"Domain name for this agent:" 8 50 "${AGENT_NAME}.local" \
3>&1 1>&2 2>&3) || die "Cancelled."
TZ=$(dialog --inputbox \
"Timezone (IANA format):" 8 50 "UTC" \
3>&1 1>&2 2>&3) || die "Cancelled."
# --- tier 2: pi provider profile (optional) ---
DO_PI=$(dialog --yesno \
"Configure LLM provider profile now?\n\n(You can also set this up later — the agent will prompt you on first run.)" \
9 60 3>&1 1>&2 2>&3 && echo yes || echo no)
PI_PROFILE="operator"
PI_PROVIDER="anthropic"
if [ "$DO_PI" = "yes" ]; then
PI_PROVIDER=$(dialog --radiolist \
"LLM provider:" 16 60 6 \
"anthropic" "Anthropic (Claude)" on \
"openai" "OpenAI (GPT-4)" off \
"openrouter" "OpenRouter" off \
"groq" "Groq" off \
"ollama" "Ollama (local)" off \
"zai" "ZAI / GLM" off \
3>&1 1>&2 2>&3) || PI_PROVIDER="anthropic"
# Note: key entry is deferred to pi first-run
dialog --msgbox \
"\nProvider set to: ${PI_PROVIDER}\n\nYour API key will be requested the first time the agent runs.\nYou can also add it manually to:\n ${ENV_FILE}\n\nThen restart: service ${AGENT_NAME} restart" \
13 60
fi
# --- auto-generate secrets ---
POSTGRES_ADMIN_PASSWORD=$(gen_secret)
SKILLS_DB_PASSWORD=$(gen_secret)
MEMORY_DB_PASSWORD=$(gen_secret)
STRAPI_DB_PASSWORD=$(gen_secret)
STRAPI_APP_KEYS="$(gen_secret),$(gen_secret)"
STRAPI_API_TOKEN_SALT=$(gen_secret)
STRAPI_ADMIN_JWT_SECRET=$(gen_secret)
STRAPI_TRANSFER_TOKEN_SALT=$(gen_secret)
STRAPI_JWT_SECRET=$(gen_secret)
SCREENSHOTS_PASSWORD=$(gen_secret)
# --- summary screen ---
dialog --msgbox \
"\nReady to install. Summary:\n\
\n Agent : ${ASSISTANT_NAME} (${AGENT_NAME})\
\n Domain : ${AGENT_DOMAIN}\
\n Timezone : ${TZ}\
\n Desktop : ${DESKTOP}\
\n Pkg : ${PKG_BRANCH}\
\n Provider : ${PI_PROVIDER}\
\n\nGenerated credentials written to:\n ${ENV_FILE}\n\nInstallation will take 1530 minutes.\nPress Enter to begin." \
20 65
# --- write .env ---
mkdir -p "$CLAWDIE_AI"
touch "$ENV_FILE"
chmod 600 "$ENV_FILE"
write_env "AGENT_NAME" "$AGENT_NAME"
write_env "ASSISTANT_NAME" "$ASSISTANT_NAME"
write_env "AGENT_DOMAIN" "$AGENT_DOMAIN"
write_env "AGENT_INTERNAL_DOMAIN" "${AGENT_NAME}.home.arpa"
write_env "TZ" "$TZ"
write_env "SETUP_LOCALE" "en-US"
write_env "DISPLAY_LOCALE" "en-US"
write_env "ASSISTANT_LOCALE" "en-US"
write_env "SYSTEM_LOCALE" "en_US.UTF-8"
write_env "PI_TUI_PROVIDER" "$PI_PROVIDER"
write_env "PI_TUI_PROFILE" "$PI_PROFILE"
write_env "AGENT_SUBNET_BASE" "10.0.0"
write_env "WARDEN_SUBNET" "10.0.0.0/24"
write_env "WARDEN_GATEWAY" "10.0.0.1"
write_env "WARDEN_DB_IP" "10.0.0.3"
write_env "WARDEN_GIT_IP" "10.0.0.4"
write_env "POSTGRES_ADMIN_PASSWORD" "$POSTGRES_ADMIN_PASSWORD"
write_env "SKILLS_DB_PASSWORD" "$SKILLS_DB_PASSWORD"
write_env "MEMORY_DB_PASSWORD" "$MEMORY_DB_PASSWORD"
write_env "STRAPI_DB_PASSWORD" "$STRAPI_DB_PASSWORD"
write_env "STRAPI_APP_KEYS" "$STRAPI_APP_KEYS"
write_env "STRAPI_API_TOKEN_SALT" "$STRAPI_API_TOKEN_SALT"
write_env "STRAPI_ADMIN_JWT_SECRET" "$STRAPI_ADMIN_JWT_SECRET"
write_env "STRAPI_TRANSFER_TOKEN_SALT" "$STRAPI_TRANSFER_TOKEN_SALT"
write_env "STRAPI_JWT_SECRET" "$STRAPI_JWT_SECRET"
write_env "SCREENSHOTS_PASSWORD" "$SCREENSHOTS_PASSWORD"
write_env "SCREENSHOTS_USER" "$AGENT_NAME"
# --- node.js: set up npm prefix for clawdie user ---
echo "prefix=${CLAWDIE_HOME}/.npm-global" > "${CLAWDIE_HOME}/.npmrc"
mkdir -p "${CLAWDIE_HOME}/.npm-global/bin"
if ! grep -q '\.npm-global' "${CLAWDIE_HOME}/.profile" 2>/dev/null; then
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "${CLAWDIE_HOME}/.profile"
fi
chown -R clawdie:clawdie "${CLAWDIE_HOME}/.npmrc" "${CLAWDIE_HOME}/.npm-global" "${CLAWDIE_HOME}/.profile"
# --- gpu detection ---
echo "Detecting GPU..."
eval $(sh "${SHARE}/firstboot/gpu-detect.sh")
echo " GPU: ${GPU_VENDOR} → kld: ${GPU_KLD}"
for kld in $GPU_KLD; do
kldload "$kld" 2>/dev/null || true
done
sysrc kld_list+="${GPU_KLD}"
[ -n "$GPU_NOTES" ] && echo " Note: ${GPU_NOTES}"
# --- pkg install: desktop packages ---
echo "Installing packages offline..."
# TODO: install DE-specific package list
# pkg install -y $(cat "${SHARE}/packages/pkg-list-${DESKTOP}.txt")
# --- seed zroot/pkg-cache from USB packages ---
# The packages/ directory on the USB is dual-purpose:
# 1. Used above to install desktop + host packages offline
# 2. Seeded into zroot/pkg-cache so npm run install-all can provision
# all Bastille jails fully offline — no internet needed for jail setup
echo "Seeding jail pkg cache from USB..."
if zfs list zroot/pkg-cache >/dev/null 2>&1; then
PKG_CACHE_MOUNT=$(zfs get -H -o value mountpoint zroot/pkg-cache)
elif zfs list zroot >/dev/null 2>&1; then
zfs create -o mountpoint=/var/cache/pkg/bastille zroot/pkg-cache
PKG_CACHE_MOUNT="/var/cache/pkg/bastille"
else
PKG_CACHE_MOUNT="/var/cache/pkg/bastille"
mkdir -p "$PKG_CACHE_MOUNT"
fi
rsync -a --ignore-existing "${SHARE}/packages/All/" "${PKG_CACHE_MOUNT}/"
pkg repo "$PKG_CACHE_MOUNT"
echo " pkg cache seeded at ${PKG_CACHE_MOUNT}"
# --- extract clawdie-ai ---
echo "Extracting Clawdie-AI..."
tar -xzf "${SHARE}/clawdie-ai.tar.gz" -C "$CLAWDIE_HOME"
chown -R clawdie:clawdie "$CLAWDIE_AI"
# --- seed AGENTS.md from template ---
AGENTS_TPL="${CLAWDIE_AI}/groups/global/AGENTS.md.tpl"
AGENTS_OUT="${CLAWDIE_AI}/groups/global/AGENTS.md"
if [ -f "$AGENTS_TPL" ]; then
sed \
-e "s|{{ASSISTANT_NAME}}|${ASSISTANT_NAME}|g" \
-e "s|{{AGENT_NAME}}|${AGENT_NAME}|g" \
-e "s|{{AGENT_DOMAIN}}|${AGENT_DOMAIN}|g" \
-e "s|{{ASSISTANT_LOCALE}}|en-US|g" \
-e "s|{{TZ}}|${TZ}|g" \
"$AGENTS_TPL" > "$AGENTS_OUT"
chown clawdie:clawdie "$AGENTS_OUT"
fi
# --- run clawdie-ai setup ---
echo "Running Clawdie-AI install..."
cd "$CLAWDIE_AI"
su -m clawdie -c "npm install 2>&1" | tee -a "$LOG"
su -m clawdie -c "npm run install-all 2>&1" | tee -a "$LOG"
echo ""
echo "============================================"
echo " Clawdie-AI setup complete."
echo " Agent : ${ASSISTANT_NAME}"
echo " Domain: ${AGENT_DOMAIN}"
echo " Logs : ${LOG}"
echo "============================================"