# Clawdie ISO Builder

On `xfce-operator-usb`, builds a bootable FreeBSD 15.x operator USB image with:

- XFCE desktop
- pre-SDDM live GPU detection
- Firefox browser
- Tailscale
- NetworkMgr launched via `mdo`, not `sudo`
- native Wi-Fi firmware bundle for NetworkMgr-managed wireless adapters
- bundled npm globals (`pi` only); `codex` ships as a FreeBSD pkg
  instead of npm; `claude-code` is skipped on the live image because
  its native deps have no FreeBSD binary (see `install_live_npm_globals`)
- bash as the default operator shell, with zsh + packaged oh-my-zsh available as optional user shell tooling
- Blender for Python-capable 3D/operator workflows
- offline package repository
- bundled Clawdie-AI tarball for later phases
- Colibri Rust control-plane service for the live USB
- deployed-system `clawdie` service lane for future disk/server installs

The ISO carries its own product version, independent of any component (zot,
Colibri, Clawdie-AI). Component versions are recorded in `build-manifest.json`.

```sh
ISO_VERSION="0.11.0"     # explicit product version (required; no zot-tracking)
BUILD_CHANNEL="dev"      # dev | release
CLAWDIE_REF="main"       # validation default
```

Release builds must pin a Clawdie-AI tag with `--clawdie-version X.Y.Z`.

---

## Prerequisites

On the FreeBSD build host:

```sh
sudo pkg install -y curl node24 npm-node24 sudo
```

Also required:

- FreeBSD 15.x
- 150 GB free build space recommended
- root or `sudo` for image assembly
- 32 GB USB key minimum for the current default `IMAGE_SIZE=28G`
- 64 GB or larger recommended when you want more offline growth headroom
- prebuilt Colibri release artifacts for the live USB control plane

Tailscale is packaged for live operator access. You can either bake
`--tailscale-auth-key` for one-shot first-boot autojoin or authenticate later
from the running USB with `mdo -u root tailscale up`.

### Colibri artifacts

The operator USB stages Colibri as the live control plane. It expects prebuilt
FreeBSD release binaries; Rust compilation takes place before image mounting,
and only prebuilt binaries land on the mounted image.

Default adjacent-checkout flow:

```sh
(cd /home/clawdie/ai/colibri && cargo build --workspace --release)
sudo ./build.sh --skip-fetch
```

Override locations when needed:

```sh
COLIBRI_REPO=/home/clawdie/ai/colibri sudo ./build.sh --skip-fetch
COLIBRI_ARTIFACT_DIR=/path/to/colibri-target/release sudo ./build.sh --skip-fetch
```

Temporary debugging escape hatch while the flag still exists:

```sh
FEATURE_COLIBRI=NO sudo ./build.sh --skip-fetch
```

Operator-USB validation builds use the Colibri-backed default lane. Set
`FEATURE_COLIBRI=NO` only for focused staging/debug work. The image includes:

```text
/usr/local/bin/colibri-daemon      # always-on Colibri control-plane daemon
/usr/local/bin/colibri             # operator CLI for status, tasks, skills, and spawning
/usr/local/bin/colibri-mcp         # MCP bridge for Zed/Claude Code/Cursor
/usr/local/bin/colibri-tui         # optional terminal dashboard if present in artifacts
/usr/local/bin/colibri-test-agent  # dev/validation only when COLIBRI_STAGE_TEST_AGENT=YES
/usr/local/etc/rc.d/colibri_daemon # FreeBSD service wrapper for colibri-daemon
/var/db/colibri                    # persistent SQLite/state directory
/var/run/colibri                   # runtime socket and PID directory
/var/log/colibri                   # daemon logs
```

The build also creates the `colibri` service user/group and writes rc.conf
values for `colibri_daemon_enable`, paths, and `colibri_daemon_cost_mode`. Operator USB
validation builds enable `colibri_daemon_enable=YES`; the service starts after
the login milestone and is the lightweight control plane for the live USB.

`colibri-mcp` is staged out of the box for MCP-capable editors and assistants.
It defaults to read-only tools; launch it with `COLIBRI_MCP_WRITE=1` only for a
trusted write-capable MCP profile. Example MCP client configs are installed at
`/usr/local/share/clawdie-iso/mcp-examples/`.

### Colibri vs. Clawdie service names

```text
Live USB image
  rc.conf: colibri_daemon_enable="YES"
  service: colibri_daemon
  purpose: lightweight live control plane for the operator desktop

Installed/deployed system
  rc.conf: clawdie_enable="YES"
  service: clawdie
  purpose: persistent host agent/facade for health, inventory, watchdog,
           credentials, and Colibri-backed orchestration
```

No current ISO build flag stages `service clawdie`. That name is reserved for
installed/deployed systems until the real persistent host service is chosen. The
baseline live USB ships Colibri directly and excludes the old mini-binary
intended for the final deployed service.

---

## Quick Start

```sh
# Confirm you are building the intended branch/head first.
git status --short --branch
git log --oneline -5

# Full build: fetch + assemble.
sudo ./build.sh
```

Output:

```text
tmp/output/clawdie-quindecim-0.11.0.img
```

Published/downloaded artifacts are compressed as `.img.xz`. Stream the
compressed image directly into `dd`:

```sh
xz -dc clawdie-quindecim-0.11.0.img.xz | dd of=/dev/daX bs=1M status=progress conv=fsync && sync
sync
```

For Linux or FreeBSD downloads from the published HTTPS path, prefer resumable
`curl` with retries before flashing:

```sh
curl -fL --continue-at - --retry 5 --retry-delay 5 --progress-bar -O \
  https://osa.smilepowered.org/downloads/iso/clawdie-quindecim-0.11.0.img.xz
curl -fL --retry 5 --retry-delay 5 -O \
  https://osa.smilepowered.org/downloads/iso/clawdie-quindecim-0.11.0.img.xz.sha256
```

For a build-local uncompressed image, plain `dd` is also fine:

```sh
dd if=tmp/output/clawdie-quindecim-0.11.0.img of=/dev/daX bs=1M status=progress conv=fsync && sync
sync
```

Target the whole USB device (`/dev/daX`); flashing to a partition is unsafe and
unsupported. See [FLASHING.md](FLASHING.md)
for Linux commands, checksum verification, and stale-label cleanup.

---

## Useful Build Modes

```sh
# Download/cache FreeBSD, packages, npm globals, and Clawdie-AI only.
./build.sh --fetch-only

# Assemble using cached inputs.
sudo ./build.sh --skip-fetch

# Fetch packages but reuse the cached FreeBSD memstick image.
sudo ./build.sh --skip-memstick-fetch

# Dev/test image: set live user clawdie password to quindecim.
sudo ./build.sh --live-default-password

# Bake an SSH public key for pubkey-only live USB access.
sudo ./build.sh --ssh-key "$(cat ~/.ssh/id_ed25519.pub)"

# Autojoin a tailnet on first boot via a one-shot self-deleting service.
sudo ./build.sh --tailscale-auth-key "$TAILSCALE_AUTH_KEY"

# Bundle a specific branch/tag/commit ref.
sudo ./build.sh --clawdie-ref main
sudo ./build.sh --clawdie-ref f04f35eb4e16b50150ae73bad2e271388dc88f82

# Release build from a pinned Clawdie-AI tag.
BUILD_CHANNEL=release sudo ./build.sh --clawdie-version 0.10.0
```

`--skip-fetch` is provenance-safe for Clawdie-AI: moving refs are resolved to a
commit and cached by that commit. If the commit cannot be resolved safely, the
script refuses a moving-ref skip-fetch build rather than producing misleading
manifest data.

### SSH key note for live-debug builds

`build.sh` accepts `--ssh-key` for the live USB. The key is installed into
`/home/clawdie/.ssh/authorized_keys` so the operator can pull logs without
hand-transcribing them from a tty.

Find an existing public key:

```sh
ls -l ~/.ssh/*.pub
cat ~/.ssh/id_ed25519.pub
```

Create a dedicated key for distributable images:

```sh
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/clawdie-live-usb -C "clawdie-live-usb"
sudo ./build.sh --ssh-key "$(cat ~/.ssh/clawdie-live-usb.pub)"
```

**Listening policy on the live USB:** `sshd` runs unconditionally and listens
on all interfaces — both the LAN path and the tailnet path, so an operator
without Tailscale can still SSH in from the local network. Auth is restrictive
regardless: pubkey only, no passwords, no root. The daemon runs even when
`--ssh-key` is not provided (no authorized key → no one can authenticate;
harmless but predictable).

**Tailscale coupling:** when the build also receives a Tailscale auth key, the
live USB autojoins the tailnet on first boot and is reachable by MagicDNS
hostname (`ssh clawdie@clawdie-live`). That's the preferred path; the LAN
path remains a fallback. Recommend passing both for hardware testing:

```sh
sudo ./build.sh \
  --live-default-password \
  --ssh-key "$(cat ~/.ssh/clawdie-live-usb.pub)" \
  --tailscale-auth-key "$TAILSCALE_AUTH_KEY"
```

Current implementation:

- `--ssh-key` prepopulates `/home/clawdie/.ssh/authorized_keys` (mode 0600,
  `.ssh/` mode 0700, owned `clawdie:clawdie`).
- `sshd_enable="YES"` in the live rc.conf, unconditionally.
- Policy lives in a drop-in at
  `/etc/ssh/sshd_config.d/clawdie-live.conf`, with the base
  `/etc/ssh/sshd_config` including that directory. Contents:
  `PubkeyAuthentication yes`, `PasswordAuthentication no`,
  `KbdInteractiveAuthentication no`, `ChallengeResponseAuthentication no`,
  `PermitRootLogin no`.
- `build-manifest.json` records the baked-in key fingerprint
  (`ssh-keygen -lf <pubkey>`) so the operator can verify the running image
  matches expectation.

**Distribution-safety note:** public keys are public, so baking one in is
not a confidentiality issue — but anyone with the image file can SSH into a
live boot of it. For dev/test builds that's the right tradeoff. For public
release images, omit `--ssh-key` entirely; operators add a key
themselves after first boot or build their own image. The dedicated
`clawdie-live-usb` key avoids leaking personal keys into shareable images.

See `doc/LIVE-SESSION-REVIEW.md` for the full pre-build plan and TESTING
hooks.

### LAN discovery (mDNS / Avahi)

The live USB advertises itself on the local network as
`clawdie-live.local` so the operator can SSH/`scp` without finding the
DHCP-assigned IP each boot. Two access paths in priority order:

```sh
# Preferred when Tailscale was passed at build time:
ssh clawdie@clawdie-live          # MagicDNS

# Always-on LAN discovery — no Tailscale required:
ssh clawdie@clawdie-live.local    # mDNS / Avahi

# Last-resort fallback if multicast is blocked on the network:
ssh clawdie@<dhcp-ip-from-router>
```

The implementation contract:

- Explicit packages in `packages/pkg-list-live-operator.txt`:
  `avahi-app` (already transitive in the closure, list explicitly to
  pin the contract) and `nss_mdns`.
- `avahi_daemon_enable="YES"` in live rc.conf. `dbus_enable="YES"` is
  already set; Avahi depends on it.
- `/etc/nsswitch.conf` `hosts:` line set to
  `files mdns_minimal [NOTFOUND=return] dns mdns` so `.local` names
  resolve from the live USB itself.
- No config changes to the packaged Avahi `ssh.service` — its
  `_ssh._tcp` advertisement is what we want.

**Scope discipline:** mDNS is **only** for LAN discovery of the live
USB itself. Clawdie's internal service names continue to live under
`home.arpa` (`ai.home.arpa`, `cms.home.arpa`, `git.home.arpa`,
`<tenant>.home.arpa`). Keep internal service names under `home.arpa`;
reserve `.local` exclusively for mDNS — RFC 6762 reserves `.local` for
mDNS, and mixing the two namespaces breaks both.

**Network caveat:** some corporate networks, hotel Wi-Fi, and isolated
guest VLANs block multicast traffic. In those environments
`clawdie-live.local` won't resolve and the operator falls back to the
Tailscale path (if configured) or the DHCP IP path. Worth knowing
before debugging "mDNS doesn't work" on a hostile network.

### PF firewall on the live USB

PF is enabled by default with a minimal, permissive baseline: outbound
open, inbound limited to SSH, mDNS, ICMP, DHCP-client, and Tailscale's
UDP 41641. The actual access restriction on SSH is carried by `sshd`
auth policy (pubkey only), not PF interface scoping.

**PF logging stack is opt-in by default.** `pflog_enable`
and `pflogd_enable` remain at their `NO` defaults. The ruleset
has no `log` keywords, so nothing is captured by default. When
debugging from a booted live USB, the operator enables the stack by
hand:

```sh
mdo -u root kldload pflog
mdo -u root ifconfig pflog0 create
mdo -u root tcpdump -ni pflog0
```

Post-disk-install both knobs can be flipped to `YES` from `sysrc` for
forensic log capture. Not the live USB default.

The ruleset lives at `live/operator-session/pf-live.conf` (separate
from the installed-system policy in `firstboot/shell-pf.sh`, which has
different threat-model assumptions).

See `doc/LIVE-SESSION-REVIEW.md` for the full ruleset and TESTING hooks.

### Targeted tmpfs for write-heavy paths

The live USB targets 8 GB RAM minimum hardware. Mirror the proven
NomadBSD desktop-live pattern for the system paths, then add one
Clawdie-specific Firefox cache optimization:

| Path                   | Backing                         | Why                                                                                               |
| ---------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------- |
| `/tmp`                 | tmpfs                           | X sockets, screenshot tools, browser/app temp files; NomadBSD ships this for SDDM/XFCE live media |
| `/var/log`             | tmpfs                           | avoid steady-state USB writes from syslog, sshd, Avahi, dbus, and desktop services                |
| `/home/clawdie/.cache` | symlink to `/tmp/clawdie/cache` | Firefox cache2, GTK icon cache, thumbnails — dominant desktop-session USB writer                  |

Keep `tmpmfs="NO"` and `varmfs="NO"`. The historical Clawdie breakage came
from the stock memstick's blanket rc.d `tmpmfs`/`varmfs` overlays, which hid
important `/var` content. Controlled fstab tmpfs entries for only `/tmp` and
`/var/log` are a different mechanism and match NomadBSD's long-running live
desktop design.

`/var/tmp` stays on disk. NomadBSD does not tmpfs it, and applications often
use `/var/tmp` for longer-lived crash-recovery state.

**Not adopted in this phase:** unionfs root / "XFCE from RAM". The
FreeBSD UFS buffer cache already keeps hot reads in RAM after first
load, and a full unionfs overlay would regress SDDM/SSH-host-key
persistence. Revisit when disk install + USB-removable mode become a
real requirement.

See `doc/LIVE-SESSION-REVIEW.md` for the rationale, the explicit
"what stays on disk" list, and TESTING hooks.

### Tailscale auth-key build flag

`build.sh` accepts `--tailscale-auth-key` for the live USB. When present, the
key is written to a root-only secret file inside the image and consumed by a
one-shot rc.d service on first boot. The service runs:

```text
tailscale up --auth-key=... --hostname=clawdie-live --ssh=false
```

Then it deletes the key file and disables itself. The operator can still join
manually after boot when no auth key was baked:

```sh
mdo -u root tailscale up
```

The auth key never lands in `build-manifest.json` or `build.cfg` — only the
boolean `tailscale_auth_key_baked: true|false` is recorded in the manifest.

---

## Build Output and Provenance

The build header shows:

```text
ISO     : <iso-version>-dev
FreeBSD : 15.x-RELEASE amd64
Clawdie : main
Clawdie commit: <resolved-sha>
```

The image contains:

```text
/usr/local/share/clawdie-iso/build-manifest.json
```

The installed system receives the same manifest. It records:

- ISO version and build channel
- FreeBSD version/arch
- bundled Clawdie-AI ref and commit
- ISO repo commit and modified state
- UTC build timestamp

The final size output distinguishes:

- **Image size** — logical size that must fit on the USB key
- **Allocated** — sparse bytes used on the build host

---

## Build Configuration

Edit `build.cfg` for persistent defaults:

```sh
ISO_VERSION="${ISO_VERSION:-0.11.0}"
BUILD_CHANNEL="${BUILD_CHANNEL:-dev}"
IMAGE_SIZE="28G"
CLAWDIE_REF="${CLAWDIE_REF:-main}"
DEFAULT_PKG_BRANCH="latest"
FEATURE_TAILSCALE="${FEATURE_TAILSCALE:-YES}"
```

Notes:

- Host pkg repo branch and ISO package branch are independent.
- `DEFAULT_PKG_BRANCH=latest` is the ISO package source by default.
- Provider keys, Telegram, and disk deployment are deferred on this branch.

---

## Packages Deferred to Disk Install

The live operator USB is intentionally a lean substrate: bring up the
desktop, the browser, the agent CLIs, networking, and jails — nothing
more. Packages listed below are fetched into the offline repository via
`packages/pkg-list-disk-install-extras.txt`, but are intentionally not
installed onto the live rootfs.

**Two distinct categories live in the same file** with the same code
path (fetched, not installed on live), but different long-term homes:

1. **Desktop-spin leftovers** (telegram-desktop, mpv, abiword,
   simplescreenrecorder, …). Inherited from the earlier all-in-one
   desktop image. Don't match the operator-USB role. **Long-term home:
   disk-install only.** Not returning to the live USB.

2. **Roadmap-essential, deferred for stabilization** (`blender`).
   First-class operator capability, not a leftover. Clawdie's product
   scope explicitly covers parametric design → CAD/CAM → CNC fabrication
   for OSA-style geodesic work
   ([osa.smilepowered.org](https://osa.smilepowered.org/)), and
   Blender's bundled `bpy` Python module is the skill substrate for
   those workflows. Held off the live USB for now only because its
   ffmpeg/libpulse/mesa-libs/boost surface would partially un-do the
   lean-rootfs payoff we want during the early hardware-validation
   cycle. **Long-term home: back on the live USB** once dependency
   surface is audited and operator workflow exists. Classify it as
   roadmap-essential in all future audits, not desktop-spin cruft.

**Intent:** keep this table as the authoritative "what we will deploy
to disk" reference so the work is not lost between commits. The
disk-install consumer still needs to install
`pkg-list-disk-install-extras.txt` when that path lands.

| Package                | Role                                                   |                Direct pkg size | Installed size | Category          |
| ---------------------- | ------------------------------------------------------ | -----------------------------: | -------------: | ----------------- |
| `blender`              | Parametric 3D modelling + Python `bpy` skill substrate | (large; measure on next image) |        (large) | roadmap-essential |
| `telegram-desktop`     | Messaging                                              |                      52.08 MiB |     236.90 MiB | leftover          |
| `abiword`              | Word processor                                         |                       4.75 MiB |      21.60 MiB | leftover          |
| `mpv`                  | Media player                                           |                       1.60 MiB |       6.96 MiB | leftover          |
| `simplescreenrecorder` | Screen/audio capture                                   |                       1.31 MiB |       3.83 MiB | leftover          |
| `xls2txt`              | Spreadsheet text converter                             |                       0.55 MiB |       2.14 MiB | leftover          |
| `antiword`             | `.doc` text converter                                  |                       0.15 MiB |       0.65 MiB | leftover          |
| `epdfview`             | PDF viewer                                             |                       0.12 MiB |       0.40 MiB | leftover          |
| `catdoc`               | `.doc` / `.xls` converter                              |                       0.08 MiB |       0.60 MiB | leftover          |
| `p5-docx2txt`          | `.docx` converter                                      |                       0.02 MiB |       0.06 MiB | leftover          |
| `odt2txt`              | `.odt` converter                                       |                       0.02 MiB |       0.04 MiB | leftover          |

Removing the leftover bundle from the live USB shrinks the image and
drops the PulseAudio + PipeWire transitive surface — see "Audio surface
implications" below. The bundle is still useful on a full installed
operator desktop, hence **deferred** rather than **deleted**.

Blender's deferral is different: it's a _commitment_ to the
CNC/parametric-fabrication roadmap, parked off the live USB only until
the dep-set audit completes.

### Audio surface implications

The audio stack is the main architectural payoff of the bundle move and
Firefox browser swap, separate from the size win. Confirmed by direct
package-dep inspection. The `qt6-*`/`kf6-*` entries below are transitive package
dependencies from desktop applications, not an installer direction:

| Package                | Pulls in (audio-relevant)                                                          |
| ---------------------- | ---------------------------------------------------------------------------------- |
| `telegram-desktop`     | `qt6-declarative`, `qt6-wayland`, `qt6-svg`, `qt6-lottie`, `kf6-*`, **`pipewire`** |
| `simplescreenrecorder` | **`pulseaudio`**, **`pipewire`**, `qt6-base`, `ffmpeg`                             |
| `mpv`                  | `ffmpeg`, `libplacebo`, `mesa-libs`, `wayland`, `vulkan-loader`                    |
| `abiword`              | `goffice`, `libgsf`, `wv`                                                          |

`simplescreenrecorder` is the worst offender — a single niche tool drags
in both the PulseAudio daemon _and_ the PipeWire daemon. Removing the
bundle from the live rootfs restores the FreeBSD-native OSS default by
construction, not by config-tuning.

Replacing Chromium with Firefox also removes the Chromium →
`speech-dispatcher` → `pulseaudio` dependency chain. Firefox keeps a
modern browser on the live USB without installing the PulseAudio daemon.

`xfce4-mixer`'s GStreamer-plugin chain typically pulls
`gstreamer1-plugins-pulse`. This is the remaining live-USB libpulse
puller after the bundle move and is tracked under "Audit candidates"
below.

### Audit candidates (live USB, keep for now)

These stay on the live USB, but they're flagged for a second-pass audit
once the desktop is proven stable on hardware:

| Package       | Reason to revisit                                                                                                                                                                                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `xfce4-mixer` | wraps GStreamer; the GStreamer plugin bundle on FreeBSD typically pulls libpulse. The OSS-native intent of picking xfce4-mixer is partially undermined by this. Replacement candidates: `mixer(8)` from a terminal launcher, or a tiny OSS-only mixer applet. |
| `pcmanfm`     | file-manager fallback while `xfdesktop` reliability is being validated. Drop once `xfdesktop` is the proven path.                                                                                                                                             |
| `bitchx`      | IRC client; desktop-era leftover. Kept on the live USB for now — size impact is negligible (~few hundred KiB) and the operator may want a quick chat tool on a fresh boot.                                                                                    |

### Currently essential (both live and disk)

Listed for completeness so the lean/full split stays honest:

- **Desktop session:** `xfce4-desktop`, `xfce4-panel`, `xfce4-session`,
  `xfce4-settings`, `xfce4-wm`, `xfce4-terminal`,
  `xfce4-whiskermenu-plugin`, `sddm`, `xinit`, `xterm`.
- **GTK / desktop integration:** `gsettings-desktop-schemas`,
  `adwaita-icon-theme`, `adwaita-icon-theme-legacy`,
  `gtk-update-icon-cache`, `shared-mime-info`, `desktop-file-utils`.
- **Browser + agent runtime:** `firefox`, `node24`, `npm-node24` (with
  bundled `pi` npm globals). `codex` is included as a FreeBSD
  pkg, not via npm. `claude-code` is excluded from the live image —
  its native deps have no FreeBSD binary.
- **Networking:** `tailscale`, `networkmgr`, `wifi-firmware-kmod`,
  `FreeBSD-fwget`.
- **Jails:** `bastille`.
- **Operator diagnostics:** `tmux`, `screen`, `mc`, `zip`, `unzip`,
  `7-zip`, `python312`, `dejavu`, `py311-pillow`.

  **Python:** `python3` is **3.11** (FreeBSD's `PYTHON_DEFAULT`). `python312`
  is also installed and available as `python3.12` for anything that needs
  newer. The image-render / screenshot skill uses `py311-pillow` (works on
  `python3`); `clawdie-join-hive.sh` advertises `image-render` when Pillow
  imports and `screenshot` when a display is present.

- **Xorg base:** `xorg-minimal`, `xkeyboard-config`, `xkbcomp`,
  `xf86-input-libinput`, `xf86-video-scfb`, `xf86-video-intel`,
  `xf86-video-amdgpu`, `xf86-video-ati`, `drm-kmod`,
  `gpu-firmware-intel-kmod-geminilake`,
  `gpu-firmware-intel-kmod-kabylake`.
- **System bus:** `dbus`.

### Decisions implemented for next build

These decisions are now reflected in `packages/pkg-list-*.txt` and the
live XFCE launcher/mime defaults. The next build should validate the new
closure and hardware behavior.

| Decision                                                               | Status                              | Notes                                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------------------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Drop `chromium`, add `firefox`                                         | **implemented**                     | Browser swap applied in `packages/pkg-list-live-operator.txt`, `packages/pkg-list-jails.txt`, README, the bootstrap page, the launch helper, and the panel launcher. Firefox on FreeBSD avoids the Chromium → `speech-dispatcher` → `pulseaudio` chain.                                                                                                                                 |
| Split deferred bundle into `packages/pkg-list-disk-install-extras.txt` | **implemented**                     | The list remains in `pkg_list_all()` so archives land in the offline repo, but is absent from `pkg_list_live_installer()` so it stays off the live rootfs. The future disk-install consumer still needs to install it explicitly.                                                                                                                                                       |
| `simplescreenrecorder`: defer or drop outright?                        | **deferred to disk extras for now** | Kept in `pkg-list-disk-install-extras.txt` with the rest of the desktop bundle so the package remains available offline; revisit before implementing the disk-install consumer.                                                                                                                                                                                                         |
| PulseAudio installable as an option on disk-install target?            | **open**                            | On the live USB the answer is "no daemon, OSS default." On the _installed_ operator desktop, do we ship `pulseaudio` as an opt-in install (e.g. via a setup-time toggle, or as part of `pkg-list-disk-install-extras.txt`), or commit to FreeBSD-native OSS/sndio across the board? Decision affects whether the deferred-bundle file should include `pulseaudio` as an explicit entry. |

---

## Build Process

1. Fetch FreeBSD memstick and verify checksum.
2. Fetch all package archives for offline install.
3. Build local pkg repository metadata.
4. Fetch Clawdie-AI by resolved ref and prepare offline `node_modules` tarball.
5. Create/attach the working image.
6. Inject firstboot scripts, packages, XFCE live-session assets, bundled npm
   globals, Clawdie-AI tarball, build config, and manifest.
7. Copy the final sparse image into `tmp/output/`.

The build is intentionally cache-friendly. If in doubt before validation, run the
full `sudo ./build.sh` once after pulling current `main`.

### Build-host disk policy

When OSA gets tight on disk, prefer deleting **repo-local build artifacts**
(`tmp/packages`, `tmp/cache/work.img`, cached FreeBSD memstick images, old output
artifacts) over pruning host packages.

Preserve the remaining host-side graphics/media stack; it supports current
operator tooling and validation lanes, not an old X11 desktop:

- `ffmpeg` + `py311-pillow` back the Hermes/image-render and screenshot path.
- `gtk3`, `cairo`, `pango`, `fontconfig`, `libX11`, `mesa-*`, `wayland`,
  `wlroots`, and related libs are shared by Codex-adjacent tooling, VNC/screenshot
  helpers, media/rendering packages, and Python operator tools.
- Dry-run package removal on OSA showed that reclaiming this subtree would remove
  far more than a stale desktop: `Codex`, `ffmpeg`, `py311-pillow`, ImageMagick,
  graphviz, aider-adjacent Python packages, and remote-display helpers all rode on
  the same dependency closure.

In practice: if you need gigabytes quickly, clear build caches first:

    sudo rm -rf tmp/packages tmp/cache tmp/output

These caches typically reclaim 3-8 GB. If host pkg pressure becomes chronic,
that is a signal to move more build/package work onto
`mother-build` (PLANNED — see docs/POUDRIERE-BUILD-SERVER.md), not to hollow
out the FreeBSD operator host.

Note: OSA's `/tmp` is a ZFS dataset separate from build caches. If shell
commands fail with "No space left on device" during agent operations, clear
`/tmp` independently:

    sudo rm -rf /tmp/*

---

## Boot Flow Produced by the Image

1. USB boots to XFCE live session.
2. `/usr/local/etc/rc.d/clawdie_live_gpu` runs before SDDM and selects a conservative GPU path.
3. SDDM presents the greeter; the operator logs in as `clawdie` to launch the Clawdie XFCE session.
4. A desktop launcher opens the static Clawdie bootstrap page.
5. The operator verifies browser, `pi`, Tailscale, and local networking.
6. Later phases will add persistence, disk deployment, and upgrade/rescue flows.

---

## Testing

Default verification order for the operator USB is now:

1. static artifact verification on the build host
2. bhyve boot verification on the ML350p virtualization lane
3. physical hardware validation for the final acceptance pass

Use bhyve before writing to hardware whenever the lane is available:

```sh
sudo ./scripts/bhyve-pf-allow.sh
sudo ./scripts/bhyve-test.sh
```

Current ML350p bhyve plan:

- host RAM budget: ZFS ARC 6G + Poudriere tmpfs 4G + headroom 6G
- bhyve RAM budget: FreeBSD ISO test 4G + Linux cross-compile 4G + FreeBSD builder 4G + spare 4G
- intended VM roles:
  - ISO boot verification after each build
  - Linux target validation
  - FreeBSD/Poudriere test lane

Use bhyve to catch boot, SDDM/XFCE, and service-start regressions early. Keep
real hardware as the final proof for GPU quirks, Wi-Fi, audio, touchpad, and
display/panel behavior.

See [TESTING.md](TESTING.md) for the full validation checklist.

---

## Troubleshooting

**`ERROR: missing package archive for <pkg>`**

A package was added after your cache was created. Run a full fetch/build:

```sh
sudo ./build.sh
```

**`pkg` not found during unprivileged fetch**

`build.sh` now sets a known FreeBSD tool PATH internally. If your shell still
cannot find pkg manually, add `/usr/local/sbin` to your login PATH.

**`npm ci` falls back to `npm install`**

The bundled Clawdie-AI ref has a package-lock mismatch. The build warns and
falls back so validation can continue, but release refs should have a clean lock.

**Image says small allocated size**

The image is sparse on the build host. Use the logical image size when choosing
a USB key.

---

**Last updated:** 16.maj.2026
