.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>
264 lines
9.4 KiB
Bash
264 lines
9.4 KiB
Bash
#!/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 15–30 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 "============================================"
|