# Firstboot — Modular Shell Pipeline

**Audience:** developers, operators, contributors maintaining the installed-system firstboot path.

**Scope:** this document describes the **installed-system firstboot shell
pipeline** in `firstboot/`. The current live operator USB graphical boot path
runs a separate pre-SDDM rc.d service (`/usr/local/etc/rc.d/clawdie_live_gpu`)
and does not exercise this pipeline. The `shell-gpu.sh` examples below describe
the installed-target firstboot/module flow.

This file consolidates and supersedes the earlier `SHELL-ARCHITECTURE.md`,
`SHELL-MODULES.md`, and `firstboot/MODULE-MANIFEST.md`.

---

## Design Philosophy

Traditional installers do one of two things:

1. **Monolithic:** single script that does everything — hard to test, debug, or reuse.
2. **Heavy framework:** external runtime (Python, Perl, Ansible) with VM overhead and dependencies.

**Clawdie Shell chooses a third path:** modular POSIX-shell functions
organised by concern, sourceable independently, with no runtime dependencies
beyond `/bin/sh`.

Each module is:

- **Standalone:** can be sourced without others.
- **Idempotent:** safe to run twice — checks state first.
- **Testable:** functions have clear inputs/outputs and can be invoked in isolation.
- **Debuggable:** `clawdie_shell_gpu_detect` can be run from a shell to reproduce a step.

### Why POSIX over Bash

| Aspect                  | POSIX `/bin/sh` | Bash                        |
| ----------------------- | --------------- | --------------------------- |
| Default on FreeBSD      | yes             | requires `pkg install bash` |
| First-boot availability | always          | needs install first         |
| Dependencies            | none            | libreadline, ncurses        |
| Startup overhead        | ~5 ms           | ~50 ms                      |

All modules use POSIX shell only. No `[[ ]]`, no `${var#pattern}` outside POSIX-defined cases, no `function` keyword, no `local` (use `_`-prefixed plain variables instead), no `set -o pipefail`.

### Why shell over Python or Node

| Aspect              | Shell      | Python    | Node        |
| ------------------- | ---------- | --------- | ----------- |
| Startup time        | 5 ms       | 100 ms    | 300 ms      |
| Footprint           | one file   | ~50 MB    | ~100 MB     |
| External deps       | none       | venv, pip | npm modules |
| First-boot overhead | negligible | 100+ ms   | 300+ ms     |

For a one-time installer, shell wins on speed and simplicity.

---

## Architecture Overview

```
firstboot.sh
  ├─ Load GUI config (/tmp/clawdie-install.conf) or wizard input
  ├─ Source: shell-zfs.sh
  ├─ Source: shell-gpu.sh
  ├─ Source: shell-nvidia.sh
  ├─ Source: shell-pkg.sh
  ├─ Source: shell-ssh.sh
  ├─ Source: shell-env.sh
  ├─ Source: shell-system.sh
  ├─ Source: shell-desktop.sh
  ├─ Source: shell-pf.sh
  ├─ Source: shell-tailscale.sh
  ├─ Source: shell-npm-globals.sh
  ├─ Source: shell-deploy.sh
  └─ Call functions in sequence:
      ├─ clawdie_shell_zfs_detect              (baremetal only)
      ├─ clawdie_shell_gpu_detect
      ├─ clawdie_shell_nvidia_detect           (only if GPU=nvidia)
      ├─ clawdie_shell_pkg_setup
      ├─ clawdie_shell_ssh_setup
      ├─ clawdie_shell_env_generate
      ├─ clawdie_shell_system_config
      ├─ clawdie_shell_desktop_detect
      ├─ clawdie_shell_pf
      ├─ clawdie_shell_tailscale_setup         (optional)
      ├─ clawdie_shell_npm_globals_install
      └─ clawdie_shell_deploy
```

---

## Dependency Graph

```
[0.0 setup-import] (optional) → setup.txt + system.env import
    ↓
[0.1 zfs] (baremetal only) → CLAWDIE_BOOT_MODE
    ↓
[User Input]
    ↓
[1.1 env] → ASSISTANT_NAME, AGENT_DOMAIN, TZ, LLM_PROVIDER
    ↓
    ├→ [1.2 pkg]                            (optional: can run any time)
    ├→ [1.3 gpu]                            (optional: hardware detection)
    ├→ [1.3b nvidia]                        ← REQUIRES: DETECTED_GPU from [1.3]
    ├→ [1.3c ssh]                           ← REQUIRES: ASSISTANT_NAME from [1.1]
    ├→ [1.4 system]                         ← REQUIRES: TZ, AGENT_DOMAIN from [1.1]
    ├→ [1.4.1 pf]                           ← REQUIRES: ASSISTANT_NAME from [1.1]
    ├→ [1.4.2 desktop]                      ← PREFERS: GPU config from [1.3]
    ├→ [1.5 tailscale]    (optional)        ← PREFERS: repos from [1.2], pf from [1.4.1]
    ├→ [1.5.1 npm-globals]                  ← PREFERS: repos from [1.2]
    └→ [1.6 deploy]                         ← REQUIRES: .env from [1.1], repos from [1.2]
                                              ← PREFERS: gpu config from [1.3]
```

### Hard dependencies

- **1.1 must complete before 1.6** — deploy sources `.env`.
- **1.3 must complete before 1.3b** — nvidia module checks `DETECTED_GPU`.
- **1.4 should run before 1.4.2 and 1.4.1** — system config writes rc.conf that affects desktop + PF.
- **1.4.1 should run before 1.5** — Tailscale updates PF rules.

---

## Module Reference

### 0.0 — `setup-import.sh`

Read `setup.txt` and `system.env` from the writable USB config partition
before the wizard, for unattended installs.

**Wizard inputs:** none.
**Outputs (exports):** `ASSISTANT_NAME`, `HOSTNAME`, `AGENT_DOMAIN`, `TZ`,
optional pre-baked provider/channel values, `ZFS_POOL`, `ZFS_LAYOUT`,
`ZFS_DATA_DISKS`, `ZFS_HOT_SPARES`, `ZFS_PREFIX`, hardware hints.
**Checkpoint:** `setup-import`.
**Skip:** no `CLAWDIE` FAT32 partition, or `setup.txt` missing, or values
incomplete for non-interactive path.
**Error handling:** safe-fail back to wizard; never blocks an interactive
install on a missing partition.

---

### 0.1 — `shell-zfs.sh`

Detect existing ZFS pools and select boot mode.

**Main function:** `clawdie_shell_zfs_detect()`.
**Wizard inputs:** none — automatic detection.
**Outputs (exports):**

- `CLAWDIE_BOOT_MODE` — `install` | `upgrade` | `maintenance`.
- `POOL_NAME` — detected pool name when present.

**Checkpoint:** `[ZFS]`.
**Skip:** `TARGET=vps` (ZFS not used).
**Error handling:** safe-fail to `install` if no pools found. If maintenance
mode selected, execs `maintenance-mode.sh` and exits.

---

### 1.1 — `shell-env.sh`

Generate a minimal `.env` seed (identity + feature flags).

**Main function:** `clawdie_shell_env_generate()`.

**Wizard inputs (Tier 1, required):**

- `ASSISTANT_NAME` — human name (e.g., "Clawdie Smith").
- `AGENT_DOMAIN` — public or local domain zone (default: `${agentname}.home.arpa`).
- `TZ` — timezone (e.g., `Europe/Ljubljana`).

**Wizard inputs (Tier 2, optional):** provider/model values, provider API keys,
Telegram credentials — normally configured after first boot in `/setup`.

**Outputs:**

- `$ENV_FILE` — `/home/clawdie/.env` (mode 0600), identity + feature flags.
- Copied into `/home/clawdie/clawdie-ai/.env` by shell-deploy (1.6).
- Completed by Clawdie-AI onboarding (secrets + derived defaults).

**Exports for downstream:** `AGENT_NAME` (derived from `ASSISTANT_NAME`),
`DETECTED_ABI` (optional, from `pkg config abi`).

**Functions:**

```sh
clawdie_shell_env_generate     # Main entry point
clawdie_shell_env_gen_secret   # openssl rand -base64 32 (with /dev/urandom fallback)
clawdie_shell_env_derive_names # ASSISTANT_NAME → AGENT_NAME
clawdie_shell_env_derive_ips   # Allocate jail IPs from AGENT_SUBNET_BASE
clawdie_shell_env_validate     # Verify .env has required vars
```

**Jail IP allocation** (from `AGENT_SUBNET_BASE`, default `10.0.0`):

| Slot  | Service                   | Default IP  |
| ----- | ------------------------- | ----------- |
| .1    | Gateway (host bridge)     | 10.0.0.1    |
| .2    | Management jail           | 10.0.0.2    |
| .3    | Database (PostgreSQL)     | 10.0.0.3    |
| .4    | Code service (git mirror) | 10.0.0.4    |
| .5    | Web service (nginx)       | 10.0.0.5    |
| .101+ | Worker jails (dynamic)    | 10.0.0.101+ |

**Checkpoint:** `[ENV]`.
**Skip:** `.env` exists AND contains `AGENT_DOMAIN`.
**Error handling:** fails if `ASSISTANT_NAME`, `AGENT_DOMAIN`, or `TZ` missing.
Validates `.env` has minimal required vars before marking complete.
**Recovery:** `clawdie-firstboot --resume` skips if `[ENV] COMPLETE` in progress
file. Re-runnable (idempotent via sysrc pattern).

---

### 1.2 — `shell-pkg.sh`

Configure FreeBSD package repositories (online + offline USB) and seed Bastille jail cache.

**Main function:** `clawdie_shell_pkg_setup()`.
**Wizard inputs:** none — automatic from `[1.1]`.
**Inputs from `[1.1]`:** `DETECTED_ABI` (optional, auto-detected if missing).

**Outputs:**

- `/etc/pkg/repos/FreeBSD.conf` — online fallback, priority 10.
  - URL: `pkg+https://pkg.FreeBSD.org/${ABI}/latest`.
- `/etc/pkg/repos/Clawdie-USB.conf` — offline USB repo, priority 100 (preferred).
  - URL: `file:///mnt/media/packages`.
- `/var/cache/pkg/bastille/` — seeded with .pkg files from USB for offline jail provisioning.

**Functions:**

```sh
clawdie_shell_pkg_setup
clawdie_shell_pkg_detect_abi          # pkg config abi or derive from uname
clawdie_shell_pkg_write_freebsd_conf  # writes FreeBSD.conf
clawdie_shell_pkg_write_clawdie_conf  # writes Clawdie-USB.conf
clawdie_shell_pkg_seed_cache          # copies USB pkgs to bastille cache
```

**Checkpoint:** `[PKG]`.
**Skip:** if both `FreeBSD.conf` AND `Clawdie-USB.conf` exist.
**Error handling:** safe-fail — missing USB packages → continues online only;
`pkg update` may fail in chroot (expected, logged). Cache seeding errors are non-fatal.
**Recovery:** re-runnable to refresh cache or update repo configs.

---

### 1.3 — `shell-gpu.sh`

Detect GPU hardware and select driver kernel module.

**Scope:** installed-target firstboot path. The live USB boot path uses
`clawdie_live_gpu` (an rc.d service) instead.

**Main function:** `clawdie_shell_gpu_detect()`.
**Wizard inputs:** none.

**Hardware detection (via `pciconf -lv`, class `0x030000`):**

| Vendor    | Module(s)                          | Note                               |
| --------- | ---------------------------------- | ---------------------------------- |
| Intel     | `i915kms`                          | Intel integrated (iGPU)            |
| AMD       | `amdgpu` (or `radeon` on older HW) | Radeon / RDNA                      |
| NVIDIA    | `nvidia-modeset nvidia`            | branch chosen by `shell-nvidia.sh` |
| VMware    | `vmwgfx`                           | VMware SVGA 3D                     |
| (unknown) | (empty)                            | VESA software fallback             |

**Outputs:**

- `/etc/rc.conf` — `kld_list="..."` (idempotent; replaces existing line).

**Exports for downstream:** `DETECTED_GPU` — `intel` | `amd` | `nvidia` | `vmware` | `vesa`. Used by `[1.3b]` to decide run or skip.

**Functions:**

```sh
clawdie_shell_gpu_detect
clawdie_shell_gpu_detect_pci    # pciconf -lv → vendor:device
clawdie_shell_gpu_match_driver  # vendor → kld list
clawdie_shell_gpu_write_rcconf  # rc.conf kld_list
clawdie_shell_gpu_load_live     # optional kldload (may fail in chroot)
clawdie_shell_gpu_validate
```

**Checkpoint:** `[GPU]`.
**Skip:** `kld_list=` already in rc.conf.
**Error handling:** `pciconf` may fail outside chroot (expected). Falls back
silently to `vesa` if detection fails. `kldload` live fails gracefully in chroot.
**Recovery:** re-runnable. Must run before `[1.4 system]` (which reads rc.conf)
and is required before `[1.3b nvidia]`.

---

### 1.3b — `shell-nvidia.sh`

NVIDIA driver version selection (PC-BSD heritage).

**Main function:** `clawdie_shell_nvidia_detect()`.
**Skip if:** `DETECTED_GPU != "nvidia"`.

**Wizard inputs (Tier 2, optional):**

- `NVIDIA_DRIVER_VERSION` — `590` | `470` | `390` (env var for unattended), or interactive `bsddialog` menu.

**Device ranges → driver:**

| Range         | Era                           | Driver             |
| ------------- | ----------------------------- | ------------------ |
| ≥0x2700       | Ada (RTX 40xx)                | 590                |
| 0x2060–0x2500 | Turing/Ampere (RTX 20xx/30xx) | 590                |
| 0x1340–0x2186 | Maxwell (GTX 750–980 Ti)      | 470                |
| 0x1180–0x139F | Kepler (GTX 650–780 Ti)       | 390                |
| (unknown)     | —                             | 590 (safe default) |

**Outputs:**

- `/etc/rc.conf` — `nvidia_driver_version="590"` (or `"470"`/`"390"`), idempotent.
- `/var/run/nvidia_driver_version.txt` — optional marker file (safe-fail).

**Exports:** `NVIDIA_DRIVER_VERSION`, used during package selection in `[1.6]`.

**Checkpoint:** `[NVIDIA]`.
**Error handling:** invalid version → warning + default `590`. `bsddialog`
unavailable → silent default. `/var/run` write failure → non-fatal.

---

### 1.3c — `shell-ssh.sh`

Configure SSH access and system passwords.

**Main function:** `clawdie_shell_ssh_setup()`.

**Wizard inputs (Tier 1, required):**

- `ASSISTANT_NAME` — used for default user naming.
- `ASSISTANT_PASSWORD` — operator password.
- `ROOT_PASSWORD` — root password (optional if disabled).

**Outputs:**

- `/etc/master.passwd` updates via `pw`.
- `/home/clawdie/.ssh/authorized_keys` if SSH key provided.
- `/etc/ssh/sshd_config` — hardened defaults.

**Checkpoint:** `[SSH]`.
**Error handling:** password prompts must succeed; missing SSH key is
non-fatal (password login allowed).

---

### 1.4 — `shell-system.sh`

System configuration (hostname, timezone, rc.conf, services).

**Main function:** `clawdie_shell_system_config()`.

**Inputs (from `[1.1]`):**

- `TZ`, `AGENT_DOMAIN`.

**Outputs:**

- `/etc/rc.conf` — `timezone`, `dbus_enable=YES`, `display_manager=sddm`, `sddm_enable=YES`, `linux_enable`, `zfs_enable`, `kld_list` (idempotent via sysrc).
- `/etc/hostname` — single line: `home.arpa` (or `AGENT_DOMAIN`).
- `/etc/profile.d/clawdie.sh` — npm `PATH`, `npm_config_prefix`.

**Functions:**

```sh
clawdie_shell_system_config         # orchestrator
clawdie_shell_system_write_rcconf
clawdie_shell_system_set_hostname
clawdie_shell_system_setup_env
clawdie_shell_system_enable_services
```

**Checkpoint:** `[SYSTEM]`.
**Skip:** `timezone=` AND `dbus_enable=` already in rc.conf.
**Error handling:** `hostname` may fail in chroot (safe-fail). `service`
onestart fails gracefully in chroot (expected).

---

### 1.4.2 — `shell-desktop.sh`

Detect display hardware and enable the desktop stack when appropriate.

**Main function:** `clawdie_shell_desktop_detect()`.
**Inputs from `[1.3]`:** `DETECTED_GPU` (used to decide which desktop bits to enable).

**Outputs:**

- `/etc/rc.conf` — desktop service flags (e.g., `sddm_enable=YES` when display
  detected; `sddm_enable=NO` for headless VPS/cloud).

**Checkpoint:** `[DESKTOP]`.
**Skip:** headless/VPS scenario detected, or desktop already enabled.
**Error handling:** detection failures fall back to headless mode.

---

### 1.4.1 — `shell-pf.sh`

Configure PF firewall with block-all default, SSH protection, jail NAT, glasspane VNC.

**Main function:** `clawdie_shell_pf()`.
**Wizard inputs:** none.
**Inputs from `[1.1]`:** `ASSISTANT_NAME`; `AGENT_NET` (optional, default `192.168.100.0/24`).

**Outputs:**

- `/etc/pf.conf`:
  - block-all default
  - SSH brute-force protection (`max-src-conn-rate 3/60`)
  - NAT for agent jails (`192.168.0.0/16` supernet)
  - HTTP/HTTPS pass rules
  - commented Tailscale + glasspane VNC block (agent uncomments later)
- `/etc/rc.conf` appends:
  - `cloned_interfaces="bridge0"`
  - `ifconfig_bridge0_name="warden0"`
  - `ifconfig_${BRIDGE}="inet 192.168.100.1/24 up"`
  - `gateway_enable="YES"`
  - `pf_enable="YES"`
  - `pf_reload_enable="YES"`
- `/usr/local/etc/rc.d/pf_reload`:
  - `REQUIRE: tailscaled`
  - Fixes PF/Tailscale cold-boot race (resolves `tailscale0` interface index).

**Glasspane VNC:** port 5900 (`wayvnc` → `cage` → browser), blocked on
`ext_if`, passed on `tailscale0`. Uncommented by agent when Tailscale enabled.

**Checkpoint:** `[PF]`.
**Skip:** `pf_enable="YES"` already in rc.conf.
**Error handling:** fails if external interface cannot be detected (no
default route). Bridge creation and PF load may fail in chroot (expected,
non-fatal; loads on reboot).
**Recovery:** `pf_reload` pre-installed even without Tailscale — harmless
now, essential later. Must run before `[1.5 tailscale]`.

---

### 1.5 — `shell-tailscale.sh`

Optional Tailscale enablement for secure remote access.

**Main function:** `clawdie_shell_tailscale_setup()`.
**Wizard inputs (Tier 2):** `FEATURE_TAILSCALE` — `YES`/`NO` (default `NO`).

**Outputs:**

- `/etc/rc.conf` — `tailscaled_enable="YES"`.
- Tailscale left unauthenticated; operator runs `tailscale up` from the running system.

**Checkpoint:** `[TAILSCALE]`.
**Skip:** `FEATURE_TAILSCALE != YES`.
**Error handling:** safe-fail — network or daemon issues are logged and do
not abort install. `pkg install tailscale` failure → warning + continue.
**Recovery:** `service tailscaled start && tailscale up`.

---

### 1.5.1 — `shell-npm-globals.sh`

Install bundled npm CLI tools (`claude`, `gemini`, `pi`) from ISO cache.

**Main function:** `clawdie_shell_npm_globals_install()`.

**Inputs preferred:** offline repo cache from `[1.2]` (fully offline install).

**Outputs:**

- `/opt/clawdie/npm-global/bin/*` — bundled CLI binaries.
- `/etc/profile.d/clawdie.sh` — `PATH` update if missing.

**Checkpoint:** `[NPM-GLOBALS]`.
**Skip:** npm globals already installed in `/opt/clawdie/npm-global`.
**Error handling:** missing cache → warning + skip (non-fatal).

---

### 1.6 — `shell-deploy.sh`

Extract Clawdie-AI (offline tarball incl. `node_modules`), run installer, start services.

**Main function:** `clawdie_shell_deploy()`.

**Hard inputs from `[1.1]`:** `.env` at `$ENV_FILE` — sourced for
`ASSISTANT_NAME`, `AGENT_DOMAIN`, jail IPs, DB config. Missing `.env` →
**hard exit**.

**Preferred (not required) inputs:**

- `[1.2]` — repos + pkg cache (speeds offline provisioning).
- `[1.3]`/`[1.3b]` — `kld_list`, `nvidia_driver_version` (used for jail pkg selection).

**Outputs:**

- `/home/clawdie/clawdie-ai/` — extracted from `/usr/local/share/clawdie-iso/clawdie-ai.tar.gz`.
- `/home/clawdie/clawdie-ai/.env` — seeded from `[1.1]`.
- `node_modules/` bundled into the ISO tarball at build time.
- Bastille jails created: worker, db, cms (optional mgmt).
- `just install` runs as the `clawdie` user.
- PostgreSQL seeded in db jail; nginx configured in cms jail; `rc.d` `clawdie` service installed and enabled.

**Functions:**

```sh
clawdie_shell_deploy
clawdie_shell_deploy_extract_tarball    # tar xzf → /home/clawdie, chown
clawdie_shell_deploy_run_install_all    # just install
clawdie_shell_deploy_verify             # jails up, service present, .env ok
```

**Checkpoint:** `[DEPLOY]`.
**Skip:** `package.json` AND `node_modules/` present → skip extraction; always run `just install`.
**Error handling:** missing `.env` → hard exit. Missing tarball → assume
already extracted (warning). Missing `node_modules` → fall back to `npm ci`
(needs network), fails hard if deps cannot install. Jail creation failures →
warning + continue. DB connectivity → deferred to runtime.
**Recovery:** idempotent re-run; failures block full system setup.

---

## Execution Order

```
User input (wizard Tier 1 + optional Tier 2)
    ↓
1. zfs           CONDITIONAL — baremetal only, sets boot mode
2. gpu           RECOMMENDED — hardware detection
3. nvidia        CONDITIONAL — only if GPU=nvidia
4. pkg           OPTIONAL    — early for offline support
5. ssh           REQUIRED    — user + password provisioning
6. env           REQUIRED    — creates .env with identity
7. system        REQUIRED    — system-level rc.conf
8. desktop       CONDITIONAL — display detection + enablement
9. pf            REQUIRED    — firewall + jail NAT
10. tailscale    OPTIONAL    — remote access, modifies PF rules
11. npm-globals  OPTIONAL    — offline npm CLIs
12. deploy       REQUIRED    — depends on .env from env
    ↓
SUCCESS: Clawdie-AI ready for first boot
```

---

## Error Handling and Recovery

### Module pattern

```sh
#!/bin/sh
set -eu  # exit on error and undefined vars (POSIX)
trap 'echo "ERROR in clawdie_shell_FUNCTION at line $LINENO" >&2; exit 1' ERR

clawdie_shell_FUNCTION() {
  _status_file="/var/log/clawdie-firstboot.progress"
  echo "[MODULE] FUNCTION_START" >> "$_status_file"

  # validate inputs
  if [ -z "${VAR:-}" ]; then
    log_msg "ERROR: VAR not set"
    return 1
  fi

  # do work
  if ! some_command; then
    log_msg "ERROR: some_command failed"
    return 1
  fi

  echo "[MODULE] FUNCTION_COMPLETE" >> "$_status_file"
  return 0
}
```

### Logging

All errors land in `/var/log/clawdie-firstboot.log`:

```
[2026-03-23 15:34:12] [pkg] Starting package setup
[2026-03-23 15:34:15] [pkg] Detected ABI: FreeBSD:15:amd64
[2026-03-23 15:34:18] [pkg] ERROR at line 42: repo config failed
[2026-03-23 15:34:18] ERROR in clawdie_shell_pkg_setup at line 42
```

Operator can `tail -100 /var/log/clawdie-firstboot.log`, SSH in to debug, or
`clawdie-firstboot --resume` to skip completed checkpoints.

### Resume

`--resume` parses the progress file and skips modules that logged
`[MODULE] COMPLETE`. Skip logic (pseudo-code):

```sh
grep -q "\[ENV\] COMPLETE"    "$PROGRESS_FILE" && skip_env=1
grep -q "\[PKG\] COMPLETE"    "$PROGRESS_FILE" && skip_pkg=1
grep -q "\[GPU\] COMPLETE"    "$PROGRESS_FILE" && skip_gpu=1
grep -q "\[NVIDIA\] COMPLETE" "$PROGRESS_FILE" || [ "$DETECTED_GPU" != nvidia ] && skip_nvidia=1
# ...
```

### Failure classes

- **Soft failure (warning):** module logs and continues. Examples: GPU
  live-load fails, `pkg update` fails. Action on resume: re-run module (idempotent).
- **Hard failure (`exit 1`):** module exits shell. Examples: `.env` missing
  in 1.6, `AGENT_DOMAIN` not set. Action on resume: fix input, re-run from
  start.

---

## Testing

Each module is sourceable and runnable standalone:

```sh
# GPU detection
export RC_CONF=/tmp/test-rc.conf
. firstboot/shell-gpu.sh
clawdie_shell_gpu_detect
cat /tmp/test-rc.conf       # should have kld_list

# Env generation
export ASSISTANT_NAME="Test"
export AGENT_DOMAIN=test.local
export TZ=UTC
export CLAWDIE_HOME=/tmp/test-clawdie
. firstboot/shell-env.sh
clawdie_shell_env_generate
cat /tmp/test-clawdie/.env  # should have ~45 lines incl. JWT_SECRET

# Pkg setup
export PKG_CONF_DIR=/tmp/test-repos
export USB_PKG_PATH=/tmp/fake-usb/packages
mkdir -p "$USB_PKG_PATH" && touch "$USB_PKG_PATH/test.pkg"
. firstboot/shell-pkg.sh
clawdie_shell_pkg_setup
cat /tmp/test-repos/FreeBSD.conf
cat /tmp/test-repos/Clawdie-USB.conf
```

After install, the same modules can be re-sourced from
`/usr/local/share/clawdie-iso/firstboot/` to troubleshoot or rerun a phase
without full reinstall.

### Verifying module output

```sh
# After env module
grep ASSISTANT_NAME /home/clawdie/clawdie-ai/.env

# After pkg module
cat /etc/pkg/repos/Clawdie-USB.conf

# After gpu module
sysctl hw.pci.dump | grep -i vga
grep kld_list /etc/rc.conf

# After deploy module
jls -N                    # list jails
# service clawdie status  # future deployed-system service acceptance
```

---

## Adding a New Module

To add a new setup phase (e.g., `shell-ollama.sh` for local AI):

1. Create `firstboot/shell-ollama.sh` with a `clawdie_shell_ollama_setup()` function and the standard error/log/progress scaffolding.
2. Add a corresponding entry to this manifest: purpose, inputs, outputs, checkpoint, skip condition, error handling, recovery.
3. Source and call it from `firstboot.sh` in the appropriate slot:
   ```sh
   . /usr/local/share/clawdie-iso/firstboot/shell-ollama.sh
   clawdie_shell_ollama_setup
   ```
4. Add a checkpoint to the resume table.

No framework changes needed.

---

## POSIX Compliance — Rules

```sh
# Required
set -eu                      # POSIX: exit on error + undefined vars
trap '...' ERR               # POSIX: error trap

# Forbidden (bash-only)
set -o pipefail              # bash-only
[[ -f foo ]]                 # bash-only; use [ -f foo ]
function name { ... }        # bash-only; use name() { ... }
local var=...                # bash-only; use plain _var (prefixed)
```

### FreeBSD vs Linux idioms

| Wrong (Linux)             | Right (FreeBSD)         |
| ------------------------- | ----------------------- |
| `systemctl start service` | `service clawdie start` |
| `apt-get install package` | `pkg install package`   |
| `/dev/sda1`               | `/dev/da0s1`            |

---

## Version History

- **current dev ISO:** live XFCE operator USB via SDDM, interactive login, 12 shell modules, ZFS/desktop/npm-globals.
- **v0.9.0:** 8 modules, runtime GPU detection, no ZFS/desktop/npm-globals.
- **v0.5.0:** 6 modules, PF firewall, glasspane VNC support.
- **v1.1 (planned):** add `shell-gpu-passthrough.sh`, `shell-upgrade.sh`.

---

## References

- `firstboot/firstboot.sh` — orchestrator (sources all modules and runs the sequence).
- `TESTING.md` — bhyve + hardware validation procedures.
- `BUILD.md` — build flags and cache behaviour.
- [POSIX Shell Reference](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html)
- [FreeBSD rc.d Services](https://docs.freebsd.org/en/books/handbook/config-tuning/#config-daemons)
- [Bastille Jails Documentation](https://bastille.readthedocs.io/)
