--- Build: FAIL | Tests: FAIL
26 KiB
CLAWDIE-ISO — USB Installer
Status: Active — bare-metal deployment path — 16.apr.2026
Target repo: git@codeberg.org:Clawdie/Clawdie-ISO.git
Purpose
Create a bootable USB image that reduces a complete Clawdie-AI deployment to a single physical step: boot USB, answer a few screens, unplug USB, done.
The installed system is a plain FreeBSD + desktop + Clawdie-AI — nothing exotic, nothing persistent on the USB. The USB is a deployment vehicle only.
The install combines three procedures that currently run separately:
| # | Path | Tool | What it does |
|---|---|---|---|
| 1 | FreeBSD base | bsdinstall | Disk layout, ZFS, base OS, bootloader, root/user accounts |
| 2 | Desktop setup | desktop-installer | GPU detection, Xorg/Wayland, DE packages, login manager, audio |
| 3 | Clawdie-AI | firstboot → just install | Bastille jails, PostgreSQL, nginx/CMS, rc.d service, bot config |
Current state: the ISO repo is live at Codeberg with a working build pipeline,
firstboot wizard (bsddialog), and 3-mode installer (Fresh / Upgrade / Repair).
See INSTALLER-PLAN.md in the ISO repo for the full architecture.
Target state: one ISO, one wizard, one reboot. Three modes auto-detected from pool state. Secrets preserved on upgrade. ZFS snapshots before any changes.
Desktop-Installer Reference
outpaddling/desktop-installer
is a FreeBSD ports package (pkg install desktop-installer) that automates the
post-install desktop configuration step:
- Detects GPU via
pciconf -lv - Installs the right kld modules (i915kms, nvidia-modeset, drm, vboxguest, etc.)
- Writes
kld_listto/etc/rc.conffor persistent driver loading - Installs the chosen DE (Lumina — FreeBSD-native)
- Configures dbus, CUPS, sound (OSS/PulseAudio), login manager (lightdm)
- Handles VirtualBox, VMware, Parallels, bhyve guest additions automatically
- Does NOT typically require a reboot — kld can be loaded live or on next boot
- Respects quarterly vs latest pkg repository configured on the system
We borrow the detection and configuration logic from desktop-installer but do not run the interactive script. Our build process will vendor the GPU detection function and call it from our own bsddialog wizard.
Reboot Analysis — Can We Do 1 Reboot?
Three potential reboot triggers:
Reboot 1 (mandatory): bsdinstall completes bsdinstall always reboots after base install. Can't be avoided. The system must boot from HDD before userspace can run.
Reboot 2 (optional): GPU driver loading
Most GPU drivers on FreeBSD are kernel modules (klds). desktop-installer writes
them to kld_list in rc.conf, but they can also be kldloaded into a live system.
The key question: can we detect the GPU and write correct config WITHOUT having
the module loaded yet?
Answer: yes. GPU detection uses pciconf -lv (reads PCI device tree, no kld
needed). The output gives us PCI device ID and vendor ID. We match against a
known table to select the right kld and write it to rc.conf. On first boot from
HDD, the kld loads automatically. X11/Wayland starts clean. No intermediate reboot
is needed to make this work.
VirtualBox is explicitly out of scope for this installer. Target: real hardware and bhyve only. VBox guest packages are not bundled on the USB image.
Reboot 3 (not needed): Clawdie setup
setup.sh and all jail provisioning steps run in userspace. No reboot needed.
rc.d service starts without reboot via service clawdie onestart.
Verdict: 1 reboot is achievable for all common hardware. Two reboots may still be the safer default for first release if GPU detection reliability is uncertain. See Options section.
Pkg Branch Selection
FreeBSD ships two pkg repository branches:
| Branch | URL suffix | Update cadence | Best for |
|---|---|---|---|
quarterly |
/quarterly/ |
Stable snapshots, security fixes only | Servers, production |
latest |
/latest/ |
Tracks ports HEAD, newest versions | Desktops, GPU drivers, Node24 |
Recommendation: default to latest. GPU drivers (drm-kmod, nvidia-driver),
Node 24, and many Bastille-related packages are updated sooner in latest.
Quarterly has caused GPU driver installation failures on new hardware.
How to implement the toggle:
During the install wizard (bsddialog), present a radiolist before any packages are installed:
Package repository branch:
(*) Latest — newest packages, best driver support [recommended]
( ) Quarterly — stable branch, security updates only
The selected branch sets /usr/local/etc/pkg/repos/FreeBSD.conf:
FreeBSD: {
url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest",
mirror_type: "srv",
enabled: yes
}
For offline install from USB, the bundled pkg repo must match the selected branch.
Build the USB with latest packages. Add a note: if the user selects quarterly,
the offline install falls back to the USB-bundled packages (which are latest
builds) and the system upgrades to quarterly on first internet connection.
Simpler alternative: only support latest offline, add quarterly as a post-install
option in the wizard.
Options
Option A — Pre-configured silent install (1 reboot, no interaction)
All choices are baked into a config file on the USB (clawdie.conf). The operator
edits this file before booting:
PKG_BRANCH=latest
DESKTOP=lumina
AGENT_NAME=clawdie
ASSISTANT_NAME="Clawdie"
TZ=Europe/Ljubljana
bsdinstall runs from installerconfig. After base install, a post-install script
reads clawdie.conf, installs everything offline, writes all config. Reboot once
into a fully configured system.
Pro: Fully automated, reproducible, CI-friendly, no bsddialog needed Con: Requires USB preparation before boot. No interactive personalization. Reboots: 1 Best for: Reproducible deploys, CI testing, pre-provisioned hardware
Option B — Interactive bsddialog wizard in installerconfig (1 reboot, guided)
After bsdinstall's base install completes but before the system reboots,
installerconfig runs a bsddialog wizard. The wizard covers:
- Pkg branch (latest / quarterly)
- Desktop: Lumina (fixed, no choice)
- Agent name + assistant name
- Telegram bot token (optional, can be skipped)
- Summary screen → confirm → install
GPU is detected from the live USB environment via pciconf -lv before chroot.
Detection result is written into the target system's rc.conf. All packages are
installed offline from the USB repo. Clawdie setup runs in chroot.
Single reboot into configured system.
Pro: Best UX, matches existing bsddialog pattern, interactive but 1-step Con: bsddialog in post-install chroot requires careful path setup Reboots: 1 Best for: First install on new hardware, operator-configured deployments
Option C — rc.firstboot script (2 reboots, most reliable)
Standard bsdinstall base install. Minimal installerconfig just copies the
wizard script and enables a firstboot rc.d service. On first real boot from HDD
(reboot 1), the firstboot service runs the bsddialog wizard on the live system.
Packages install, GPU loads live via kldload, DE and Clawdie come up. Optional
second reboot (reboot 2) for clean DM start.
Pro: Runs on real hardware (not in chroot), GPU kldload works immediately, most reliable path, easier debugging Con: 2 reboots minimum. Wizard runs after first reboot, which can feel slow. Reboots: 2 Best for: Conservative first release, maximum hardware compatibility
Option D — Extended bsdinstall menus (1 reboot, most native)
Modify bsdinstall to add extra menu screens: pkg branch, DE selection, Clawdie agent config. The installer feels like a fully native FreeBSD install with Clawdie options integrated into the standard installer flow. Post-install runs silently based on selections.
Pro: Most seamless UX — everything happens inside one familiar installer Con: Requires forking/patching bsdinstall (sh scripts in /usr/libexec/bsdinstall). Fragile against FreeBSD updates. Significant maintenance burden. Reboots: 1 Best for: Polished v2.0+ release, not first iteration
Option E — Live USB + install-to-disk (2+ reboots, most impressive)
The USB boots as a live Clawdie desktop system. User can interact with a running agent demo before committing to install. An "Install Clawdie to Disk" button/command runs the installer. After install + reboot, the HDD system is fully configured.
Pro: Demo capability, validate hardware/agent before committing, impressive Con: Most complex build (~2–3GB image), live persistence layer, 2 reboots, significant extra build infrastructure Reboots: 2+ Best for: Demo/showcase events, evaluators who want to try before installing
Recommendation
Start with Option C (rc.firstboot, 2 reboots) for v1 of the repo.
Reason: GPU detection running on the real live system (not in chroot) eliminates the most common failure mode. The 2-reboot tradeoff is acceptable for a v1 that works reliably. The wizard runs on boot from HDD, using the same bsddialog infrastructure already in Clawdie's setup.sh — no new tooling needed.
Upgrade path to Option B (1 reboot) for v1.1:
Once we have Option C working and tested on multiple hardware configs, we port the
wizard to run in the installerconfig post-install chroot. At that point we drop
the firstboot service and the second reboot.
Skip Option D and E for now. Neither is worth the complexity at this stage.
Option A can be added as a --unattended flag on top of Option B/C.
Install Flow (Option C — rc.firstboot, implemented)
USB boot
└─ bsdinstall
├─ Disk partitioning (ZFS, GPT+EFI)
├─ Base system distribution
├─ Bootloader
├─ Root password
├─ User account (clawdie)
└─ installerconfig:
├─ Copy /usr/local/share/clawdie-iso/ firstboot tree to HDD
├─ Install clawdie-iso rc.d firstboot service
└─ Reboot from HDD
First boot from HDD (reboot 1)
└─ rc.firstboot (clawdie-firstboot service)
├─ shell-zfs.sh: detect existing pool, auto-select mode (install/upgrade/repair)
│ ├─ Fresh: clear stale ZFS labels, continue
│ ├─ Upgrade: take pre-upgrade snapshot @pre-upgrade-{timestamp}
│ └─ Repair: import pool, show diagnostic report
├─ bsddialog wizard (mode confirmation, identity, hardware, summary)
│
├─ [gpu] pciconf → detect GPU, write kld_list to rc.conf
├─ [pkg] Write pkg repo config, install from USB offline repo
├─ [ssh] Generate SSH keys, set passwords (fresh only)
├─ [env] Generate .env — append-only on upgrade, full on fresh
├─ [system] Hostname, rc.conf, services (fresh only)
├─ [pf] PF firewall + jail NAT (fresh only)
├─ [tailscale] Remote access setup (fresh only)
├─ [npm-globals] Install bundled agent CLIs (pi, claude, gemini)
├─ [deploy] Extract clawdie-ai.tar.gz (includes node_modules for offline)
│ └─ just install
│ ├─ environment (host pkg baseline)
│ ├─ jails (db + git + cms via bastille)
│ ├─ db (PostgreSQL 18 + pgvector)
│ ├─ skills-memory (if artifact present)
│ ├─ cms (nginx + Astro)
│ ├─ service (rc.d, privilege drop to agent user)
│ └─ verify
│
└─ Disable clawdie-firstboot service (runs once only)
Running system
└─ Agent service starts as agent user (not root)
Module Execution Matrix
| Module | Fresh | Upgrade | Repair |
|---|---|---|---|
| gpu | run | skip | skip |
| pkg | run | run | skip |
| ssh | run | preserve | preserve |
| env | full generate | append-only | append-only |
| system | run | skip | service repair only |
| pf | run | skip | skip |
| tailscale | run | skip | skip |
| npm-globals | run | run | skip |
| deploy | run | run | selective |
Repo Structure (clawdie-iso)
clawdie-iso/
├── build.sh ← main build script (bundles node_modules for offline)
├── build.cfg ← version pins, image size, pkg list
├── INSTALLER-PLAN.md ← 3-mode installer architecture (Fresh/Upgrade/Repair)
├── installerconfig ← bsdinstall post-install hook
├── firstboot/
│ ├── firstboot.sh ← module runner with run_step_if() mode matrix
│ ├── shell-zfs.sh ← pool detection, label cleanup, pre-upgrade snapshots
│ ├── shell-env.sh ← .env generation (full fresh, append-only upgrade)
│ ├── shell-deploy.sh ← tarball extract + just install
│ ├── shell-npm-globals.sh ← bundled pi/claude/gemini CLI install
│ ├── MODULE-MANIFEST.md ← module documentation
│ └── integration-test.sh ← VPS-path test harness
├── packages/ ← pre-fetched .pkg files
└── docs/ ← VPS migration, installer docs
build.sh responsibilities:
- Fetch FreeBSD memstick installer image (verified checksum)
- Fetch all required .pkg files to
packages/(node24, bastille, desktop-installer deps, etc.) - Run
pkg repo packages/to generate repo metadata - Unpack memstick image
- Inject into memstick:
installerconfig,firstboot/tree,packages/repo,clawdie-ai.tar.gz - Repack image
- Output:
clawdie-iso-YYYYMMDD.img
USB Package Manifest
Derived from setup/environment.ts host baseline + desktop-installer + DE packages:
Clawdie host baseline:
node24, npm, bsddialog, bastille, git, tmux, python312, uv,
ripgrep, fd-find, rsync, postgresql18-client, dejavu
Xorg baseline:
xorg-minimal, xf86-video-intel, drm-kmod,
xf86-input-libinput, dbus, hal
Note: xf86-video-vboxvideo and VirtualBox guest additions are excluded.
Target hardware is real machines and bhyve only. VirtualBox is out of scope.
Lumina (sole supported DE):
lumina-core, lumina-themes, lumina-calculator, lumina-archiver,
lumina-filemanager, lumina-screenshot, lumina-open, openbox,
libxcb, libxdg-basedir, lightdm, lightdm-gtk-greeter
NVIDIA support (optional):
nvidia-driver, nvidia-settings
Total estimated size (Lumina path, latest): ~1.5 GB packages + ~600 MB FreeBSD base
Node.js Shared Modules Setup
A critical user-space tweak that must be reproduced exactly on the installed
system: npm's global package prefix must be set to the clawdie user's home
directory, not to /usr/local.
Why it matters: On FreeBSD, /usr/local is owned by root. Running
npm install -g as the clawdie user fails without this. The prefix setting
redirects globals to a user-owned path that is in PATH.
Current production setup (from /home/clawdie/.npmrc):
prefix=/opt/clawdie/npm-global
PATH addition (must be in /home/clawdie/.profile or .shrc):
export PATH="/opt/clawdie/npm-global/bin:$PATH"
firstboot.sh must:
- Write
/home/clawdie/.npmrcwithprefix=/opt/clawdie/npm-global - Create
/opt/clawdie/npm-global/(with correct ownership) - Symlink
/home/clawdie/.npm-global→/opt/clawdie/npm-global - Append PATH export to
/home/clawdie/.profile - Pre-populate the global npm cache from USB-bundled packages so that
npm installinside clawdie-ai runs fully offline
Build-time: build.sh must pre-fetch npm packages and bundle them as a
local npm cache tarball (npm-cache.tar.gz) to be extracted to
/home/clawdie/.npm during firstboot. This eliminates network calls to
registry.npmjs.org during install.
Bundled Agent CLIs (npm-globals)
In addition to the offline npm cache, the ISO ships three pre-packed agent
CLIs as standalone tarballs so the firstboot environment satisfies the
setup onboard agent-CLI prereq gate without network access:
@anthropic-ai/claude-code→claude@google/gemini-cli→gemini@earendil-works/pi-coding-agent→pi
Build path:
clawdie-iso/scripts/fetch-npm-globals.shrunsnpm packfor each package intotmp/npm-globals/build.shcopies that directory to${USB_SHARE}/npm-globals/firstboot/shell-npm-globals.sh(functionclawdie_shell_npm_globals_install) runsnpm install -g <tgz>as the clawdie user withnpm_config_prefix=/opt/clawdie/npm-globalso the binaries land in the user-owned prefix
codex is not in the npm bundle — it ships as the FreeBSD codex pkg
and is installed via pkg install -y codex. End-to-end validation of all
four CLIs in a fresh worker jail is in doc/AGENT-CLI-VALIDATION.md.
AGENTS.md Seeding (Planning)
AGENTS.md is the agent's identity and behavior file. It must be seeded during firstboot with values derived from the install wizard (assistant name, locale, agent domain, etc.).
Plan (not yet implemented):
- A template
AGENTS.md.tplis bundled on the USB inside the clawdie-ai tarball atgroups/global/AGENTS.md.tpl(orgroups/main/AGENTS.md.tpl) - During firstboot, after
.envis written,firstboot.shrenders the template by substituting:{{ASSISTANT_NAME}}→ value from wizard{{AGENT_NAME}}→ derived system name{{AGENT_DOMAIN}}→ configured domain{{ASSISTANT_LOCALE}}→ selected locale{{TZ}}→ selected timezone
- Output written to
groups/global/AGENTS.mdbeforejust installruns - This ensures the agent has a fully personalized identity from first boot
Scope: Template design and rendering are handled in the clawdie-ai repo.
The clawdie-iso repo only needs to call the render step during firstboot.
.env Generation Strategy
The installed system requires 65 environment variables to be set before Clawdie-AI can start. They fall into four categories:
| Category | Count | Source |
|---|---|---|
| Identity / locale | 8 | Human provides via wizard |
| API keys / tokens | 14 | Human provides (all optional / deferrable) |
| Auto-generated secrets | 9 | openssl rand -base64 32 at install time |
| Derived structural | 34 | Calculated from identity (jail IPs, names, etc.) |
Three options for how the wizard handles this:
.env Option 1 — Minimal wizard (5 questions, everything else silent)
The wizard asks only the irreducible human questions:
- Assistant name → derives
AGENT_NAMEautomatically - Timezone
- Domain name (default:
<agentname>.local) - Primary LLM provider + key (Anthropic / OpenAI / skip)
- Telegram bot token (or skip)
All 9 secrets: auto-generated silently with openssl rand.
All 34 structural vars: derived from identity choices (fixed subnet, fixed jail
IPs, names from AGENT_NAME, etc.).
API keys not asked: left blank in .env, operator fills post-install.
Pro: Fastest path. 5 screens. Least friction. Works completely offline.
Con: No visibility into generated credentials during install. Operator must
read .env to find database passwords. No way to customize subnet or features.
Best for: First-time users, fast re-deploys, CI/automated testing.
LLM Provider Auth — Deferred to pi first-run (design decision)
LLM provider credentials are not collected by the ISO installer wizard. This is intentional. The installer is decoupled from provider choice — providers can rotate, subscription auth can be used instead of API keys, and secrets should not be baked into USB media.
Instead the wizard collects only the pi profile. The provider auth is connected the first time pi is used:
Path A — subscription login (/login):
For OAuth-backed providers such as OpenAI Codex, the operator starts pi,
runs /login, and completes the browser flow. Pi stores the credential in
~/.pi/agent/auth.json.
Path B — manual .env edit (API-key providers / operator preference):
Operator edits /home/clawdie/clawdie-ai/.env directly, sets the appropriate
key var (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.), then runs
service clawdie restart. pi-config detects the key is present and skips
future warnings.
Path C — local Ollama:
Operator sets OLLAMA_HOST in .env. pi-config treats that as the local
provider credential source.
Current detection logic in clawdie-ai:
pi-config runs →
provider entry present in ~/.pi/agent/auth.json? → success, continue
else provider env var present in .env? → success, continue
else OLLAMA_HOST present for ollama? → success, continue
else log instruction, continue (non-fatal)
.env Option 2 — Tiered wizard with credentials summary (recommended)
Tier 1 (always shown, 4 screens):
- Name + timezone + domain → derive/generate everything → write
.env
Tier 2 (shown with "Configure now? [Y/n]"):
- LLM provider selection radiolist → key input
- Telegram bot token input
Tier 3 (shown only if operator presses "Advanced"):
- Custom subnet base (default 10.0.0.0/24)
- Feature flags: Ollama (local inference), Gitea (self-hosted git), Management jail
- ZFS dataset layout (default vs Clawdie-optimized)
End screen — Credentials Summary:
┌─ Clawdie Installation Complete ──────────────────────────┐
│ │
│ Agent: Clawdie (clawdie) │
│ Domain: clawdie.local │
│ Timezone: Europe/Ljubljana │
│ │
│ DB password: Xy7qP2mR... (see /home/clawdie/.env) │
│ Telegram: ✓ configured │
│ LLM: Anthropic ✓ │
│ │
│ Press Enter to start installation. │
└───────────────────────────────────────────────────────────┘
Pro: Best balance. Guided but not overwhelming. Summary screen prevents lost credentials. Advanced tier serves power users without cluttering the default path. Con: More wizard screens to implement. Summary screen needs careful design. Best for: Standard production deployments, first installs on new hardware.
.env Option 3 — Pre-boot config file on USB (clawdie.conf)
The USB image contains a clawdie.conf template file on a FAT32 partition that
is readable/writable from any OS (Windows, macOS, Linux, FreeBSD). Operator
edits it before booting:
# Edit this file on the USB before booting.
# Leave a field blank to be prompted during install.
ASSISTANT_NAME=Clawdie
TZ=Europe/Ljubljana
AGENT_DOMAIN=clawdie.local
PKG_BRANCH=latest # latest or quarterly
DESKTOP=lumina # lumina only
TELEGRAM_BOT_TOKEN= # optional, leave blank to skip
ANTHROPIC_API_KEY= # optional
Firstboot reads clawdie.conf first. Blank required fields trigger bsddialog
prompts. Blank optional fields are silently skipped. All secrets still
auto-generated regardless. A fully pre-filled clawdie.conf = zero-interaction install.
Pro: Supports fully automated reproducible deploys without modifying ISO. Familiar to sysadmins (edit a config file → boot). Works offline. Can be scripted or templated externally. Con: Operator must mount USB on another machine before install (FAT32 partition is accessible but requires extra step). Blank fields still need wizard. Best for: Reproducible testing, staging/production fleet deployments, CI.
Open Questions
-
GPU detection table: desktop-installer uses a shell function with a PCI ID lookup table. We should vendor
gpu-detect.shfrom desktop-installer source rather than reimplementing it. Verify license (BSD 2-clause — fine). -
pkg offline vs online: If target machine has internet,
pkg installwill upgrade from online repo after we've pointed at USB. That's fine and expected. The USB repo is only for air-gapped or offline installs. -
ZFS layout: Should we mirror bsdinstall's default ZFS layout or enforce a Clawdie-specific layout (separate datasets for bastille jails, var/db)? Decision needed before writing
installerconfig. -
ARM64 / Raspberry Pi: build.sh should detect host arch and fetch the matching FreeBSD image. For Pi, memstick is replaced with a different image type. Scope for v1: amd64 only, Pi deferred.
-
clawdie user UID: bsdinstall creates the user interactively. Should we override/enforce UID 1001 for
clawdie? Or let operator choose during bsdinstall and then reference$CLAWDIE_USERin firstboot.sh? -
Clawdie-AI source bundling: Bundle a tagged release tarball (clawdie-ai
vX.Y.Z) or pull from Codeberg during firstboot? Offline-first → bundle. build.sh should accept--clawdie-version X.Y.Zand fetch the tarball.
Status (16.apr.2026)
Done:
- ISO repo created and live at Codeberg
- Build pipeline working (build.sh)
- Firstboot wizard with bsddialog
- 12 shell modules implemented
- Phase A fixes: upgrade secret preservation, module execution matrix, ZFS label cleanup, pre-upgrade snapshots
- Offline node_modules bundling in build.sh
just installintegration- Agent CLIs bundled (pi, claude, gemini)
In progress (ISO repo):
- Phase B: Auto-detect mode from pool state
- Phase C: Boot environments (v1.1.0)
- Phase D: Repair mode (v1.1.0)
See also: INSTALLER-PLAN.md in the ISO repo for the full 5-phase implementation plan.