19 KiB
Clawdie Shell Modules — Implementation Reference
Version: v0.9.0 Implementation Date: 26.mar.2026 Status: ✅ Complete (7/7 modules)
Overview
The Clawdie Shell comprises 7 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-gpu.sh | clawdie_shell_gpu_detect() |
235 | GPU detection + kld_list | ✅ Complete |
| shell-nvidia.sh | clawdie_shell_nvidia_detect() |
265 | NVIDIA driver versioning | ✅ Complete |
| shell-pkg.sh | clawdie_shell_pkg_setup() |
189 | Package repos + USB cache | ✅ Complete |
| shell-env.sh | clawdie_shell_env_generate() |
256 | .env + secrets generation | ✅ Complete |
| shell-system.sh | clawdie_shell_system_config() |
180+ | rc.conf + hostname + services | ✅ Complete |
| shell-ssh.sh | clawdie_shell_ssh_setup() |
160+ | SSH keys + system passwords | ✅ Complete |
| shell-tailscale.sh | clawdie_shell_tailscale_setup() |
140+ | Tailscale remote access (optional) | ✅ Complete |
| shell-deploy.sh | clawdie_shell_deploy() |
225 | Tarball extract + install-all | ✅ Complete |
Total: ~1,650 lines of production code (POSIX shell, no bash-isms)
Module 1: 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
. 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
# 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 2: 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
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 nvidiaat 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
# 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 3: 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
. 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
# 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 4: 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
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_CHAT_ID (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 agentAGENT_DOMAIN— FQDN for internal networkTZ— 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
# 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 5: shell-deploy.sh
File: firstboot/shell-deploy.sh
Main Function: clawdie_shell_deploy()
Purpose
Extract Clawdie-AI tarball to /home/clawdie/ and run npm run install-all, which provisions jails, installs databases, configures services.
Entry Point
. firstboot/shell-deploy.sh
clawdie_shell_deploy
Functions
clawdie_shell_deploy()
Main orchestrator. Extracts tarball, runs npm install, 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 npm run install-all from /home/clawdie/clawdie-ai/.
What it does (from Clawdie-AI's package.json):
- Installs npm dependencies
- 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
- npm run install-all 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
# 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-all":"echo 'Mock install'"}}
EOF
. firstboot/shell-deploy.sh
clawdie_shell_deploy
# Should complete without errors
Module 6: 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 7: 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
Execution Order (firstboot.sh)
The modules execute in this sequence (see firstboot.sh line 55–61):
1. clawdie_shell_gpu_detect → Sets DETECTED_GPU
2. clawdie_shell_nvidia_detect → Refines if NVIDIA
3. clawdie_shell_pkg_setup → Configures repos
4. clawdie_shell_ssh_setup → SSH + passwords
5. clawdie_shell_env_generate → Generates .env
6. clawdie_shell_system_config → Configures system
7. clawdie_shell_deploy → Extracts + installs
State handoff:
- 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
functionkeyword (usename()) - ✅ No
local(use plain variables in functions, prefix with_to avoid collisions) - ✅ No
>&2redirects without careful quoting
This ensures compatibility with FreeBSD /bin/sh.
Error Handling Pattern
All modules follow this pattern:
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:
# 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–64):
- Sources all 7 shell-*.sh modules
- Collects wizard inputs (or uses pre-baked config)
- Exports inputs as environment variables
- Calls each module's main function in sequence
- Checks progress file for [MODULE] COMPLETE entries
- On success, disables clawdie-firstboot service
See firstboot/firstboot.sh for full flow.
References
- TESTING.md — Testing procedures
- IMPLEMENTATION-PLAN.md — Task status
- BUILD.md — Build process