# Clawdie Shell Modules — Implementation Reference **Version:** v0.9.0 **Implementation Date:** 26.mar.2026 **Status:** ✅ Complete (12/12 modules) --- ## Overview The Clawdie Shell comprises 12 POSIX-compliant shell modules that orchestrate the complete installation and configuration of Clawdie-AI from ISO boot to running system. Each module is independent and can be sourced individually for testing. Together, they form the firstboot pipeline that executes on first HDD boot. --- ## Module Index | Module | Function | Lines | Purpose | Status | |--------|----------|-------|---------|--------| | shell-zfs.sh | `clawdie_shell_zfs_detect()` | 192 | ZFS pool detection | ✅ Complete | | shell-gpu.sh | `clawdie_shell_gpu_detect()` | 184 | GPU detection + kld_list | ✅ Complete | | shell-nvidia.sh | `clawdie_shell_nvidia_detect()` | 229 | NVIDIA driver versioning | ✅ Complete | | shell-pkg.sh | `clawdie_shell_pkg_setup()` | 199 | Package repos + USB cache | ✅ Complete | | shell-ssh.sh | `clawdie_shell_ssh_setup()` | 260 | SSH keys + system passwords | ✅ Complete | | shell-env.sh | `clawdie_shell_env_generate()` | 192 | .env + secrets generation | ✅ Complete | | shell-system.sh | `clawdie_shell_system_config()` | 233 | rc.conf + hostname + services | ✅ Complete | | shell-desktop.sh | `clawdie_shell_desktop_detect()` | 101 | Desktop detection + enablement | ✅ Complete | | shell-pf.sh | `clawdie_shell_pf()` | 148 | PF firewall + jail NAT | ✅ Complete | | shell-tailscale.sh | `clawdie_shell_tailscale_setup()` | 121 | Tailscale remote access (optional) | ✅ Complete | | shell-npm-globals.sh | `clawdie_shell_npm_globals_install()` | 101 | Bundled npm CLIs | ✅ Complete | | shell-deploy.sh | `clawdie_shell_deploy()` | 424 | Tarball extract + just install | ✅ Complete | **Total:** 2,384 lines of production code (POSIX shell, no bash-isms) --- ## Module: shell-gpu.sh **File:** `firstboot/shell-gpu.sh` **Main Function:** `clawdie_shell_gpu_detect()` ### Purpose Detect the system's GPU (if any) and configure appropriate kernel modules for X11/Wayland rendering. Writes `kld_list` to `/etc/rc.conf`. ### Entry Point ```bash . firstboot/shell-gpu.sh clawdie_shell_gpu_detect ``` ### Functions #### `clawdie_shell_gpu_detect()` Main orchestrator. Detects GPU vendor via `pciconf`, matches to driver, writes rc.conf. **Sets:** `DETECTED_GPU` (intel, amd, nvidia, vmware, or vesa) **Side effects:** Writes to rc.conf, progress file, log file #### `clawdie_shell_gpu_detect_pci()` Queries `pciconf -lv` to find VGA device (class 0x030000). **Returns:** GPU vendor string (intel, amd, nvidia, vmware, or empty) #### `clawdie_shell_gpu_match_driver(vendor)` Maps vendor to FreeBSD kernel module list. **Mappings:** | Vendor | Module | Note | |--------|--------|------| | intel | i915kms | Intel integrated GPU (iGPU) | | amd | amdgpu | AMD Radeon / RDNA | | nvidia | nvidia-modeset nvidia | NVIDIA discrete GPU (see shell-nvidia.sh for version) | | vmware | vmwgfx | VMware SVGA 3D | | (other) | (empty) | VESA fallback, no kld needed | #### `clawdie_shell_gpu_write_rcconf(kld_list)` Writes `kld_list="..."` to `/etc/rc.conf`, replacing any existing kld_list. **Side effects:** - Removes existing kld_list (idempotent) - Appends new kld_list entry - Skips if kld_list is empty (VESA case) #### `clawdie_shell_gpu_validate()` Verifies rc.conf has valid kld_list. ### Environment Variables **Input (from caller):** - (none required; uses pciconf on system) **Output (sets for downstream modules):** - `DETECTED_GPU` — GPU vendor (used by shell-nvidia.sh) **Configuration (can override for testing):** - `RC_CONF` (default: `/etc/rc.conf`) - `LOG_FILE` (default: `/var/log/clawdie-firstboot.log`) - `PROGRESS_FILE` (default: `/var/log/clawdie-firstboot.progress`) ### Error Handling Returns 0 on success. Returns 1 on critical failure (e.g., can't write rc.conf). Gracefully handles: - Missing pciconf output → defaults to VESA - Unknown GPU vendor → defaults to VESA - Read-only /etc/rc.conf → returns error ### Testing ```bash # Quick test export RC_CONF="/tmp/test-rc.conf" export LOG_FILE="/tmp/test.log" . firstboot/shell-gpu.sh clawdie_shell_gpu_detect cat /tmp/test-rc.conf # Should have kld_list ``` --- ## Module: shell-nvidia.sh **File:** `firstboot/shell-nvidia.sh` **Main Function:** `clawdie_shell_nvidia_detect()` ### Purpose For NVIDIA GPUs, detect the specific GPU model and select the correct driver version (590, 470, or 390). Modern GPUs use 590; Maxwell-era use 470; Kepler and older use 390. ### Entry Point ```bash export DETECTED_GPU="nvidia" # Set by shell-gpu.sh . firstboot/shell-nvidia.sh clawdie_shell_nvidia_detect ``` ### Functions #### `clawdie_shell_nvidia_detect()` Main orchestrator. Only executes if `DETECTED_GPU=nvidia`. **Sets:** `NVIDIA_DRIVER` (590, 470, or 390) **Side effects:** Writes updated kld_list to rc.conf #### `clawdie_shell_nvidia_get_device_id()` Queries pciconf for NVIDIA GPU device ID (PCI vendor 0x10de). **Returns:** 4-digit hex device ID (e.g., "2388" for RTX 4090) #### `clawdie_shell_nvidia_select_driver(device_id)` Maps device ID to driver version using lookup table. **Device ranges:** | Range | GPU Era | Driver | |-------|---------|--------| | 0x2700+ (≥9984) | Ada (RTX 40xx) | 590 | | 0x2060–0x2500 (8288–9472) | Turing/Ampere (RTX 20xx/30xx) | 590 | | 0x1340–0x2186 (4928–8582) | Maxwell (GTX 750–980 Ti) | 470 | | 0x1180–0x139F (4480–5023) | Kepler (GTX 650–780 Ti) | 390 | | (unknown) | — | 590 (safe default) | #### `clawdie_shell_nvidia_write_rcconf(nvidia_version)` Updates rc.conf with correct nvidia-driver and nvidia-modeset modules. **Side effects:** - Removes any existing nvidia entries from kld_list - Adds `nvidia-modeset nvidia` at front of kld_list - Preserves other kld modules (e.g., i915kms from dual GPU) #### `clawdie_shell_nvidia_validate()` Checks that nvidia is in rc.conf. ### Environment Variables **Input (required):** - `DETECTED_GPU` — Must equal "nvidia" to execute **Output (sets):** - `NVIDIA_DRIVER` — Selected version (590, 470, or 390) **Configuration (can override):** - `RC_CONF` (default: `/etc/rc.conf`) - `LOG_FILE` (default: `/var/log/clawdie-firstboot.log`) - `PROGRESS_FILE` (default: `/var/log/clawdie-firstboot.progress`) ### Fallback Behavior - No GPU detected → Silently returns 0 (NVIDIA module skipped) - Unknown device ID → Defaults to driver 590 ### Testing ```bash # Test with simulated NVIDIA GPU export DETECTED_GPU="nvidia" export RC_CONF="/tmp/test-rc.conf" . firstboot/shell-nvidia.sh clawdie_shell_nvidia_detect cat /tmp/test-rc.conf # Should have nvidia-modeset nvidia ``` --- ## Module: shell-pkg.sh **File:** `firstboot/shell-pkg.sh` **Main Function:** `clawdie_shell_pkg_setup()` ### Purpose Configure FreeBSD package repositories (online and offline USB) and seed Bastille jail cache with pre-downloaded packages for offline provisioning. ### Entry Point ```bash . firstboot/shell-pkg.sh clawdie_shell_pkg_setup ``` ### Functions #### `clawdie_shell_pkg_setup()` Main orchestrator. Detects ABI, writes repo configs, seeds cache. **Returns:** 0 on success, 1 on critical error **Side effects:** Creates /etc/pkg/repos, /var/cache/pkg/bastille directories #### `clawdie_shell_pkg_detect_abi()` Queries `pkg config abi` or derives from `uname`. **Returns:** ABI string like "FreeBSD:15:amd64" #### `clawdie_shell_pkg_write_freebsd_conf(abi)` Writes `/etc/pkg/repos/FreeBSD.conf` pointing to pkg.FreeBSD.org. **Uses:** `DEFAULT_PKG_BRANCH` from build.cfg (latest or quarterly) **Output file format:** ``` FreeBSD: { url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest", mirror_type: "srv", enabled: yes } ``` #### `clawdie_shell_pkg_write_clawdie_conf()` Writes `/etc/pkg/repos/Clawdie-USB.conf` pointing to bundled USB packages. **Output file format:** ``` Clawdie-USB: { url: "file:///usr/local/share/clawdie-iso/packages", mirror_type: "none", enabled: yes, priority: 100 } ``` Priority 100 ensures USB packages take precedence if online repo is also available. #### `clawdie_shell_pkg_seed_cache()` Copies all .pkg files from USB to `/var/cache/pkg/bastille`. **Purpose:** Allows jails to install packages offline after first boot **Side effects:** - Creates /var/cache/pkg/bastille if missing - Copies *.pkg files (non-fatal if USB path missing) ### Environment Variables **Input (optional):** - `DEFAULT_PKG_BRANCH` — "latest" or "quarterly" (from build.cfg) **Configuration (can override):** - `PKG_CONF_DIR` (default: `/etc/pkg/repos`) - `FREEBSD_REPO_CONF` (default: `/etc/pkg/repos/FreeBSD.conf`) - `CLAWDIE_USB_REPO_CONF` (default: `/etc/pkg/repos/Clawdie-USB.conf`) - `USB_PKG_PATH` (default: `/usr/local/share/clawdie-iso/packages`) - `BASTILLE_PKG_CACHE` (default: `/var/cache/pkg/bastille`) - `LOG_FILE`, `PROGRESS_FILE` ### Error Handling Non-fatal errors: - Missing /etc/pkg/repos directory → created - Missing USB packages → skipped with warning - FreeBSD repo already exists → replaced Returns 1 only if critical failure (e.g., can't write config). ### Testing ```bash # Test with mock directories 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 # Verify cat /tmp/test-repos/FreeBSD.conf cat /tmp/test-repos/Clawdie-USB.conf ``` --- ## Module: shell-env.sh **File:** `firstboot/shell-env.sh` **Main Function:** `clawdie_shell_env_generate()` ### Purpose Generate `.env` configuration file with secrets, identity variables, and jail IP allocation. This file is used by Clawdie-AI at runtime. ### Entry Point ```bash export ASSISTANT_NAME="MyAssistant" export AGENT_DOMAIN="clawdie.local" export TZ="Europe/Ljubljana" . firstboot/shell-env.sh clawdie_shell_env_generate ``` ### Functions #### `clawdie_shell_env_generate()` Main orchestrator. Validates inputs, generates secrets, writes .env file. **Requires:** - `ASSISTANT_NAME` (from wizard) - `AGENT_DOMAIN` (from wizard) - `TZ` (from wizard, defaults to UTC) **Side effects:** - Creates /home/clawdie/.env - Sets permissions 600 - Changes owner to clawdie:clawdie #### `clawdie_shell_env_write_file()` Generates complete .env file with: - Identity (ASSISTANT_NAME, AGENT_DOMAIN, TZ) - Auto-generated secrets (JWT_SECRET, API_KEY, DB_PASSWORD, REDIS_PASSWORD) - Jail IP allocation (derived from AGENT_SUBNET_BASE) - Feature flags (FEATURE_MANAGEMENT_JAIL, FEATURE_OLLAMA, FEATURE_TELEGRAM) - LLM provider config - System metadata **File format:** Shell-compatible KEY="value" (can be sourced in other scripts) **Variables generated:** - Identity: ASSISTANT_NAME, AGENT_NAME, AGENT_DOMAIN, TZ - Secrets: JWT_SECRET, ANTHROPIC_API_KEY, DB_PASSWORD, REDIS_PASSWORD (32-char base64 each) - Network: AGENT_SUBNET_BASE, AGENT_GATEWAY_IP, MGMT_JAIL_IP, DB_JAIL_IP, GIT_JAIL_IP, CMS_JAIL_IP, WORKER_JAIL_IP_START - Database: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD - Features: FEATURE_MANAGEMENT_JAIL, FEATURE_OLLAMA, FEATURE_TELEGRAM - Telegram: TELEGRAM_BOT_TOKEN, TELEGRAM_ADMIN_IDS (optional, blank initially) - LLM: LLM_PROVIDER - SSH: SSH_PUBLIC_KEY (if provided at install) - System: FREEBSD_VERSION, INSTALL_DATE #### `clawdie_shell_env_gen_secret()` Generates random 32-character secrets suitable for JWT/API keys. **Method:** `openssl rand -base64 32` with fallback to /dev/urandom **Returns:** 32-char base64 string (alphanumeric + +/) #### `clawdie_shell_env_validate()` Verifies .env file has: - Correct permissions (600) - All required variables - Non-empty secret values ### Environment Variables **Input (required):** - `ASSISTANT_NAME` — Display name for agent - `AGENT_DOMAIN` — FQDN for internal network - `TZ` — Timezone (e.g., Europe/Ljubljana) **Input (optional):** - `SSH_PUBLIC_KEY` — User's SSH public key (if provided at install) **Configuration (can override):** - `CLAWDIE_HOME` (default: `/home/clawdie`) - `ENV_FILE` (default: `/home/clawdie/.env`) - `AGENT_SUBNET_BASE` (default: `10.0.0`) - `LOG_FILE`, `PROGRESS_FILE` ### Jail IP Allocation IPs are auto-derived from AGENT_SUBNET_BASE: | Slot | Service | 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+ | ### Testing ```bash # Test .env generation export ASSISTANT_NAME="TestAgent" export AGENT_DOMAIN="test.local" export TZ="UTC" export CLAWDIE_HOME="/tmp/test-clawdie" . firstboot/shell-env.sh clawdie_shell_env_generate # Verify cat /tmp/test-clawdie/.env wc -l /tmp/test-clawdie/.env # Should have ~45 lines grep "JWT_SECRET" /tmp/test-clawdie/.env ``` --- ## Module: shell-deploy.sh **File:** `firstboot/shell-deploy.sh` **Main Function:** `clawdie_shell_deploy()` ### Purpose Extract Clawdie-AI tarball to /home/clawdie/ and run `just install`, which provisions jails, installs databases, configures services. ### Entry Point ```bash . firstboot/shell-deploy.sh clawdie_shell_deploy ``` ### Functions #### `clawdie_shell_deploy()` Main orchestrator. Extracts tarball, ensures dependencies exist (offline tarball includes `node_modules`), runs installer, verifies services. **Requires:** - `ASSISTANT_NAME`, `AGENT_DOMAIN`, `TZ` (from env module) **Returns:** 0 on success, 1 on failure **Side effects:** Creates /home/clawdie/clawdie-ai, provisions jails, starts services #### `clawdie_shell_deploy_extract_tarball()` Extracts clawdie-ai.tar.gz to /home/clawdie/. **Tarball location:** `/usr/local/share/clawdie-iso/clawdie-ai.tar.gz` **Side effects:** - Creates /home/clawdie/clawdie-ai - Renames Clawdie-AI/ → clawdie-ai/ if needed - Fixes ownership to clawdie:clawdie #### `clawdie_shell_deploy_run_install_all()` Executes `just install` from /home/clawdie/clawdie-ai/. **What it does (from Clawdie-AI's package.json):** - Uses bundled `node_modules` (offline); if missing, deploy falls back to `npm ci` (network required) - Creates and starts Bastille jails (worker, db, git, cms) - Sets up PostgreSQL in db jail - Configures nginx in cms jail - Installs rc.d service script for clawdie agent - Enables service **Runs as:** clawdie user (drops from root if needed via `su - clawdie`) #### `clawdie_shell_deploy_verify()` Checks post-install state: - At least one jail running (jls -N) - clawdie service enabled in rc.conf - .env file exists **Warnings:** Non-fatal if any check fails (jails may still be starting) ### Environment Variables **Input (optional):** - `ASSISTANT_NAME`, `AGENT_DOMAIN`, `TZ` (from env module if available) **Configuration (can override):** - `CLAWDIE_HOME` (default: `/home/clawdie`) - `CLAWDIE_AI_DIR` (default: `/home/clawdie/clawdie-ai`) - `CLAWDIE_TARBALL` (default: `/usr/local/share/clawdie-iso/clawdie-ai.tar.gz`) - `ENV_FILE` (default: `/home/clawdie/.env`) - `LOG_FILE`, `PROGRESS_FILE` ### Error Handling Returns 1 (fails) if: - Tarball not found - Extraction fails - package.json not found after extraction - just install returns error Returns 0 (succeeds) even if: - Verification finds missing jails (they may still be starting) - .env file missing (it's created by env module, but this module doesn't require it) ### Testing ```bash # Minimal test (mock) export CLAWDIE_HOME="/tmp/test-clawdie" export CLAWDIE_AI_DIR="$CLAWDIE_HOME/clawdie-ai" mkdir -p "$CLAWDIE_AI_DIR" # Create mock package.json cat > "$CLAWDIE_AI_DIR/package.json" <<'EOF' {"name":"clawdie-ai","version":"0.9.0","scripts":{"install":"echo 'Mock install'"}} EOF . firstboot/shell-deploy.sh clawdie_shell_deploy # Should complete without errors ``` --- ## Module: shell-system.sh **File:** `firstboot/shell-system.sh` **Main Function:** `clawdie_shell_system_config()` ### Purpose Configure system-level settings: timezone, hostname, rc.conf services (dbus, hald, seatd, lightdm), and Lumina desktop environment. ### Functions #### `clawdie_shell_system_config()` Main orchestrator. **Requires:** - `TZ` (timezone from wizard) - `AGENT_DOMAIN` (from wizard, used as hostname) **Side effects:** - Writes /etc/rc.conf - Writes /etc/hostname - Creates /etc/profile.d/clawdie.sh - Enables services #### `clawdie_shell_system_write_rcconf()` Configures rc.conf with: - Timezone - Service enables (dbus, hald, seatd, lightdm) - Lumina desktop settings #### `clawdie_shell_system_set_hostname()` Sets hostname from AGENT_DOMAIN #### `clawdie_shell_system_setup_env()` Creates /etc/profile.d/clawdie.sh with environment variables #### `clawdie_shell_system_enable_services()` Enables required services in rc.conf: - dbus (message bus) - hald (hardware abstraction) - seatd (seat management for Wayland) - lightdm (display manager for Lumina) --- ## Module: shell-ssh.sh **File:** `firstboot/shell-ssh.sh` **Main Function:** `clawdie_shell_ssh_setup()` ### Purpose Configure SSH access and system user passwords. If SSH key is provided, disables password auth. Otherwise, generates secure password for clawdie user. ### Functions #### `clawdie_shell_ssh_setup()` Main orchestrator. **Optional input:** - `SSH_PUBLIC_KEY` (from install, if provided) **Configures:** - SSH keys for clawdie user - System user passwords (root + clawdie) - rc.conf for sshd --- ## Module: shell-zfs.sh **Purpose:** Detect existing ZFS pools and choose boot mode **Main Function:** `clawdie_shell_zfs_detect()` **Outputs:** - `CLAWDIE_BOOT_MODE` — install | upgrade | maintenance - `POOL_NAME` — detected pool name (if present) --- ## Module: shell-desktop.sh **Purpose:** Detect display hardware and enable desktop stack when appropriate **Main Function:** `clawdie_shell_desktop_detect()` **Outputs:** - Desktop enablement flags in rc.conf (headless when skipped) --- ## Module: shell-pf.sh **Purpose:** Configure PF firewall, NAT, and glasspane VNC rules **Main Function:** `clawdie_shell_pf()` **Outputs:** - `/etc/pf.conf`, `/usr/local/etc/rc.d/pf_reload`, rc.conf PF settings --- ## Module: shell-tailscale.sh **Purpose:** Optional Tailscale install + enablement for remote access **Main Function:** `clawdie_shell_tailscale_setup()` **Outputs:** - tailscaled enablement in rc.conf, optional auth flow --- ## Module: shell-npm-globals.sh **Purpose:** Install bundled npm CLI tools from ISO cache **Main Function:** `clawdie_shell_npm_globals_install()` **Outputs:** - `/opt/clawdie/npm-global/bin` populated with CLI binaries --- ## Execution Order (firstboot.sh) The modules execute in this sequence (see firstboot.sh run_step block): ``` 1. clawdie_shell_zfs_detect → Sets CLAWDIE_BOOT_MODE (baremetal only) 2. clawdie_shell_gpu_detect → Sets DETECTED_GPU 3. clawdie_shell_nvidia_detect → Sets NVIDIA_DRIVER_VERSION 4. clawdie_shell_pkg_setup → Configures repos 5. clawdie_shell_ssh_setup → SSH + passwords 6. clawdie_shell_env_generate → Generates .env 7. clawdie_shell_system_config → Configures system 8. clawdie_shell_desktop_detect → Desktop enablement 9. clawdie_shell_pf → PF firewall + NAT 10. clawdie_shell_tailscale_setup → Tailscale remote access 11. clawdie_shell_npm_globals_install → Bundled npm CLIs 12. clawdie_shell_deploy → Extracts + installs ``` **State handoff:** - ZFS module → sets CLAWDIE_BOOT_MODE + POOL_NAME - GPU module → sets DETECTED_GPU - env module → creates .env (used by deploy) - deploy module → provisions jails + services --- ## POSIX Compliance All modules use POSIX shell features only (no bash-isms): - ✅ No `[[ ]]` (use `[ ]`) - ✅ No `${var#pattern}` (avoid) - ✅ No `function` keyword (use `name()`) - ✅ No `local` (use plain variables in functions, prefix with `_` to avoid collisions) - ✅ No `>&2` redirects without careful quoting This ensures compatibility with FreeBSD /bin/sh. --- ## Error Handling Pattern All modules follow this pattern: ```bash function_name() { # 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 # Log success log_msg "Task complete" return 0 } ``` Each module defines its own `log_msg()` function to write to `LOG_FILE`. --- ## Testing Individual Modules Each module can be tested standalone: ```bash # Test GPU detection export RC_CONF="/tmp/rc.conf" . firstboot/shell-gpu.sh clawdie_shell_gpu_detect # Test environment generation export ASSISTANT_NAME="Test" export AGENT_DOMAIN="test.local" export TZ="UTC" . firstboot/shell-env.sh clawdie_shell_env_generate ``` --- ## Integration with firstboot.sh The orchestrator `firstboot.sh` (lines 1–282): 1. Sources all 12 shell-*.sh modules 2. Collects wizard inputs (or uses pre-baked config) 3. Exports inputs as environment variables 4. Calls each module's main function in sequence 5. Checks progress file for [MODULE] COMPLETE entries 6. On success, disables clawdie-firstboot service See [firstboot/firstboot.sh](firstboot/firstboot.sh) for full flow. --- ## References - [TESTING.md](TESTING.md) — Testing procedures - [IMPLEMENTATION-PLAN.md](IMPLEMENTATION-PLAN.md) — Task status - [BUILD.md](BUILD.md) — Build process