clawdie-iso/firstboot/shell-system.sh

556 lines
17 KiB
Bash
Executable file

#!/bin/sh
# Clawdie Shell — System Configuration Module
# Purpose: System-level config (rc.conf, hostname, services, environment)
# Author: Clawdie Project
# POSIX-compliant (no bash-isms)
set -eu
# FreeBSD /bin/sh doesn't support trap ERR
# Configuration (can be overridden for testing)
RC_CONF="${RC_CONF:-/etc/rc.conf}"
LOADER_CONF="${LOADER_CONF:-/boot/loader.conf}"
SYSCTL_CONF="${SYSCTL_CONF:-/etc/sysctl.conf}"
HOSTNAME_FILE="${HOSTNAME_FILE:-/etc/hostname}"
PROFILE_DIR="${PROFILE_DIR:-/etc/profile.d}"
LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}"
PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}"
# Derived from wizard inputs (caller sets these)
# TZ - Timezone (e.g., "Europe/Ljubljana")
# AGENT_DOMAIN - FQDN (e.g., "clawdie.home.arpa" for local, or public domain)
# DETECTED_GPU - GPU vendor from gpu module (intel, amd, nvidia, vmware, vesa)
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================
clawdie_shell_system_config() {
# Main orchestrator
log_msg "[system] Starting system configuration"
if [ -z "${TZ:-}" ]; then
log_msg "[system] ERROR: TZ not set"
return 1
fi
if [ -z "${AGENT_DOMAIN:-}" ]; then
log_msg "[system] ERROR: AGENT_DOMAIN not set"
return 1
fi
if [ -z "${SYSTEM_LOCALE:-}" ]; then
log_msg "[system] ERROR: SYSTEM_LOCALE not set"
return 1
fi
# Write rc.conf with timezone and services
clawdie_shell_system_write_rcconf
log_msg "[system] Updated rc.conf"
# Set hostname
clawdie_shell_system_set_hostname
log_msg "[system] Set hostname"
# Setup environment
clawdie_shell_system_setup_env
log_msg "[system] Setup environment"
# Setup locale + keymap
clawdie_shell_system_setup_locale
log_msg "[system] Setup locale"
# Enable FreeBSD mac_do framework with no credential grants yet
clawdie_shell_system_setup_mac_do
log_msg "[system] Setup mac_do framework"
# Enable evdev input routing for Xorg/libinput touchpads and mice
clawdie_shell_system_setup_evdev
log_msg "[system] Setup evdev input routing"
# Enable Linux compatibility at boot and best-effort for the current boot
clawdie_shell_system_setup_linuxulator
log_msg "[system] Setup Linux compatibility"
# Enable a baseline bhyve host configuration on the target
clawdie_shell_system_setup_bhyve
log_msg "[system] Setup bhyve host baseline"
# Enable services
clawdie_shell_system_enable_services
log_msg "[system] Enabled services"
echo "[SYSTEM] COMPLETE" >> "$PROGRESS_FILE"
log_msg "[system] System configuration complete"
}
# ============================================================================
# RC.CONF CONFIGURATION
# ============================================================================
clawdie_shell_system_write_rcconf() {
# Update /etc/rc.conf with:
# - timezone
# - service configurations (dbus, sddm)
# - desktop settings
if [ ! -f "$RC_CONF" ]; then
log_msg "[system] Creating $RC_CONF"
touch "$RC_CONF"
fi
# Helper to set or update rc.conf variable
clawdie_shell_system_sysrc "timezone=$TZ"
if [ -n "${KEYMAP:-}" ]; then
clawdie_shell_system_sysrc "keymap=$KEYMAP"
fi
clawdie_shell_system_sysrc "dbus_enable=YES"
clawdie_shell_system_sysrc "display_manager=sddm"
clawdie_shell_system_sysrc "sddm_enable=YES"
clawdie_shell_system_sysrc "linux_enable=YES"
clawdie_shell_system_sysrc "zfs_enable=YES"
clawdie_shell_system_sysrc "vm_enable=YES"
clawdie_shell_system_append_rc_list kld_list linux linux64 zfs vmm nmdm if_tap if_bridge
log_msg "[system] Wrote rc.conf configuration"
}
clawdie_shell_system_sysrc() {
# Add or update a variable in rc.conf
# Input: VAR=VALUE
clawdie_shell_system_set_config_line "$RC_CONF" "$1"
}
clawdie_shell_system_set_config_line() {
# Add or update KEY=VALUE in a config file.
# Input: FILE KEY=VALUE
local config_file="$1"
local var_assignment="$2"
local var_name
var_name=$(echo "$var_assignment" | cut -d= -f1)
if [ ! -f "$config_file" ]; then
mkdir -p "$(dirname "$config_file")"
touch "$config_file"
fi
if grep -q "^${var_name}=" "$config_file" 2>/dev/null; then
sed -i '' "s|^${var_name}=.*|${var_assignment}|" "$config_file"
else
echo "$var_assignment" >> "$config_file"
fi
}
clawdie_shell_system_append_rc_list() {
# Append values to a quoted rc.conf list variable without duplicating entries.
# Input: VAR VALUE [VALUE...]
local var_name="$1"
shift
local current merged value
current=$(sed -n "s/^${var_name}=\"\\(.*\\)\"/\\1/p" "$RC_CONF" 2>/dev/null | head -1)
merged="$current"
for value in "$@"; do
if [ -z "$merged" ]; then
merged="$value"
continue
fi
case " ${merged} " in
*" ${value} "*) ;;
*)
merged="${merged} ${value}"
;;
esac
done
clawdie_shell_system_set_config_line "$RC_CONF" "${var_name}=\"${merged}\""
}
# ============================================================================
# MAC_DO FRAMEWORK
# ============================================================================
clawdie_shell_system_setup_mac_do() {
# Load FreeBSD mac_do at boot, but grant no credential transitions yet.
# FreeBSD 15 accepts an empty rule list as no-op policy: security.mac.do.rules=
clawdie_shell_system_set_config_line "$LOADER_CONF" 'mac_do_load="YES"'
clawdie_shell_system_set_config_line "$SYSCTL_CONF" 'security.mac.do.rules='
# Best-effort live activation for the firstboot session. The persistent
# loader/sysctl files above are the source of truth after reboot.
if command -v kldload >/dev/null 2>&1; then
kldload mac_do 2>/dev/null || true
elif [ -x /sbin/kldload ]; then
/sbin/kldload mac_do 2>/dev/null || true
fi
if command -v sysctl >/dev/null 2>&1; then
sysctl security.mac.do.rules= >/dev/null 2>&1 || true
elif [ -x /sbin/sysctl ]; then
/sbin/sysctl security.mac.do.rules= >/dev/null 2>&1 || true
fi
}
# ============================================================================
# EVDEV INPUT ROUTING
# ============================================================================
clawdie_shell_system_setup_evdev() {
# FreeBSD's xorg/libinput stack recommends routing native evdev events for
# ums(4)/psm(4). This improves touchpad and mouse support and avoids PS/2
# detection issues when moused is disabled.
clawdie_shell_system_set_config_line "$SYSCTL_CONF" 'kern.evdev.rcpt_mask=6'
if command -v sysctl >/dev/null 2>&1; then
sysctl kern.evdev.rcpt_mask=6 >/dev/null 2>&1 || true
elif [ -x /sbin/sysctl ]; then
/sbin/sysctl kern.evdev.rcpt_mask=6 >/dev/null 2>&1 || true
fi
}
# ============================================================================
# LINUX COMPATIBILITY
# ============================================================================
clawdie_shell_system_setup_linuxulator() {
# Persist Linux compatibility in rc.conf and try to activate it immediately.
# This runs on the target system during firstboot, not on the ISO build host.
local module
for module in linux64 linux; do
if command -v kldload >/dev/null 2>&1; then
kldload "$module" >/dev/null 2>&1 || true
elif [ -x /sbin/kldload ]; then
/sbin/kldload "$module" >/dev/null 2>&1 || true
fi
done
if command -v service >/dev/null 2>&1; then
service linux onestart >/dev/null 2>&1 || true
elif [ -x /usr/sbin/service ]; then
/usr/sbin/service linux onestart >/dev/null 2>&1 || true
fi
}
# ============================================================================
# BHYVE HOST BASELINE
# ============================================================================
clawdie_shell_system_setup_bhyve() {
# Persist core bhyve modules and tap behavior. Leave bridge/vm_dir creation
# to later host-specific setup because the correct NIC and storage dataset are
# machine-dependent.
local module
clawdie_shell_system_set_config_line "$SYSCTL_CONF" 'net.link.tap.up_on_open=1'
if command -v sysctl >/dev/null 2>&1; then
sysctl net.link.tap.up_on_open=1 >/dev/null 2>&1 || true
elif [ -x /sbin/sysctl ]; then
/sbin/sysctl net.link.tap.up_on_open=1 >/dev/null 2>&1 || true
fi
for module in vmm nmdm if_tap if_bridge; do
if command -v kldload >/dev/null 2>&1; then
kldload "$module" >/dev/null 2>&1 || true
elif [ -x /sbin/kldload ]; then
/sbin/kldload "$module" >/dev/null 2>&1 || true
fi
done
}
# ============================================================================
# HOSTNAME CONFIGURATION
# ============================================================================
clawdie_shell_system_set_hostname() {
# Set /etc/hostname and apply live
if [ ! -f "$HOSTNAME_FILE" ]; then
touch "$HOSTNAME_FILE"
fi
# Write to file
echo "$AGENT_DOMAIN" > "$HOSTNAME_FILE"
# Apply live (if not in chroot)
if command -v hostname >/dev/null 2>&1; then
hostname "$AGENT_DOMAIN" 2>/dev/null || true
fi
log_msg "[system] Set hostname to $AGENT_DOMAIN"
}
# ============================================================================
# ENVIRONMENT SETUP
# ============================================================================
clawdie_shell_system_setup_env() {
# Create /etc/profile.d/clawdie.sh for environment variables
# Sets up npm global paths and other Clawdie-specific variables
if [ ! -d "$PROFILE_DIR" ]; then
mkdir -p "$PROFILE_DIR"
fi
local clawdie_profile="$PROFILE_DIR/clawdie.sh"
local normalized_locale
normalized_locale=$(clawdie_shell_system_normalize_locale "$SYSTEM_LOCALE")
cat > "$clawdie_profile" <<EOF
# Clawdie-AI environment setup
# Adds npm global bin directory to PATH
_clawdie_npm_prefix="/opt/clawdie/npm-global"
_clawdie_base_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
_clawdie_path_has() {
case ":\${PATH:-}:" in
*:"\$1":*) return 0 ;;
*) return 1 ;;
esac
}
export npm_config_prefix="\${_clawdie_npm_prefix}"
export NPM_CONFIG_PREFIX="\${_clawdie_npm_prefix}"
export NPM_CONFIG_UPDATE_NOTIFIER=false
export NO_UPDATE_NOTIFIER=1
if [ -z "\${PATH:-}" ]; then
PATH="\${_clawdie_base_path}"
else
for _clawdie_dir in /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin; do
_clawdie_path_has "\${_clawdie_dir}" || PATH="\${PATH}:\${_clawdie_dir}"
done
fi
_clawdie_path_has "\${_clawdie_npm_prefix}/bin" || PATH="\${_clawdie_npm_prefix}/bin:\${PATH}"
export PATH
unset _clawdie_npm_prefix
unset _clawdie_base_path
unset -f _clawdie_path_has 2>/dev/null || true
export LANG="${normalized_locale}"
export LC_ALL="${normalized_locale}"
EOF
chmod 644 "$clawdie_profile"
if id clawdie >/dev/null 2>&1; then
if [ -x /usr/local/bin/bash ]; then
if ! grep -qx '/usr/local/bin/bash' /etc/shells 2>/dev/null; then
echo '/usr/local/bin/bash' >> /etc/shells 2>/dev/null || true
fi
pw usermod clawdie -s /usr/local/bin/bash 2>/dev/null || true
fi
if [ -x /usr/local/bin/zsh ] && ! grep -qx '/usr/local/bin/zsh' /etc/shells 2>/dev/null; then
echo '/usr/local/bin/zsh' >> /etc/shells 2>/dev/null || true
fi
cat > /home/clawdie/.profile <<'EOF'
# Clawdie operator POSIX shell profile.
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
EOF
cat > /home/clawdie/.bash_profile <<'EOF'
# Clawdie operator bash login profile.
[ -r /etc/profile ] && . /etc/profile
[ -L "${HOME}/.cache" ] && {
mkdir -p /tmp/clawdie/cache 2>/dev/null || true
chown "$(id -u):$(id -g)" /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
chmod 0700 /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
}
[ -r "${HOME}/.bashrc" ] && . "${HOME}/.bashrc"
EOF
cat > /home/clawdie/.bashrc <<'EOF'
# Clawdie operator interactive bash profile.
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
if [ -n "${PS1:-}" ]; then
export HISTFILE="${HISTFILE:-/tmp/clawdie/bash_history}"
mkdir -p /tmp/clawdie 2>/dev/null || true
fi
EOF
cat > /home/clawdie/.zprofile <<'EOF'
# Clawdie operator zsh login profile.
[ -r /etc/profile ] && . /etc/profile
[ -L "${HOME}/.cache" ] && {
mkdir -p /tmp/clawdie/cache 2>/dev/null || true
chown "$(id -u):$(id -g)" /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
chmod 0700 /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
}
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
[ -r "${HOME}/.zshrc" ] && . "${HOME}/.zshrc"
EOF
cat > /home/clawdie/.zshrc <<'EOF'
# Clawdie operator interactive zsh profile.
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
export HISTFILE="${HISTFILE:-/tmp/clawdie/zsh_history}"
mkdir -p /tmp/clawdie 2>/dev/null || true
# Keep zsh optional, but ready: use packaged oh-my-zsh when present.
if [ -d /usr/local/share/ohmyzsh ]; then
export ZSH="/usr/local/share/ohmyzsh"
elif [ -d /usr/local/share/oh-my-zsh ]; then
export ZSH="/usr/local/share/oh-my-zsh"
fi
if [ -n "${ZSH:-}" ] && [ -r "${ZSH}/oh-my-zsh.sh" ]; then
ZSH_THEME="${ZSH_THEME:-robbyrussell}"
plugins=(git)
source "${ZSH}/oh-my-zsh.sh"
fi
EOF
chown clawdie:clawdie /home/clawdie/.profile /home/clawdie/.bash_profile /home/clawdie/.bashrc /home/clawdie/.zprofile /home/clawdie/.zshrc 2>/dev/null || true
chmod 644 /home/clawdie/.profile /home/clawdie/.bash_profile /home/clawdie/.bashrc /home/clawdie/.zprofile /home/clawdie/.zshrc 2>/dev/null || true
fi
log_msg "[system] Created $clawdie_profile"
}
# ============================================================================
# SERVICE ENABLEMENT
# ============================================================================
clawdie_shell_system_enable_services() {
# Enable and start required services
# Safe to fail if running in chroot (first boot)
local services="dbus sddm"
for service in $services; do
if command -v service >/dev/null 2>&1; then
# Try to start service
service "$service" onestart 2>/dev/null || {
log_msg "[system] Could not start $service (expected in chroot)"
}
fi
done
log_msg "[system] Service enablement complete"
}
# ============================================================================
# LOCALE + KEYMAP SETUP
# ============================================================================
clawdie_shell_system_setup_locale() {
local normalized_locale
normalized_locale=$(clawdie_shell_system_normalize_locale "$SYSTEM_LOCALE")
# Write per-user login_conf (must be UTF-8).
if id clawdie >/dev/null 2>&1; then
local login_conf="/home/clawdie/.login_conf"
cat > "$login_conf" <<EOF
me:\\
:charset=UTF-8:\\
:lang=${normalized_locale}:
EOF
chown clawdie:clawdie "$login_conf" 2>/dev/null || true
chmod 644 "$login_conf" 2>/dev/null || true
if command -v cap_mkdb >/dev/null 2>&1; then
cap_mkdb "$login_conf" 2>/dev/null || true
fi
fi
}
clawdie_shell_system_normalize_locale() {
local raw="$1"
raw=$(echo "$raw" | tr '-' '_')
raw=${raw%%.*}
raw=${raw%%@*}
if [ -z "$raw" ]; then
raw="C"
fi
echo "${raw}.UTF-8"
}
# ============================================================================
# VALIDATION
# ============================================================================
clawdie_shell_system_validate() {
# Verify system configuration completed
if [ ! -f "$RC_CONF" ]; then
echo "ERROR: rc.conf not found" >&2
return 1
fi
# Check timezone is set
if ! grep -q "^timezone=" "$RC_CONF"; then
echo "ERROR: timezone not set in rc.conf" >&2
return 1
fi
# Check hostname file exists
if [ ! -f "$HOSTNAME_FILE" ]; then
echo "ERROR: $HOSTNAME_FILE not created" >&2
return 1
fi
# Check mac_do framework config exists. Empty rules intentionally grant no
# credential transitions.
if ! grep -q '^mac_do_load="YES"' "$LOADER_CONF" 2>/dev/null; then
echo "ERROR: mac_do_load not set in $LOADER_CONF" >&2
return 1
fi
if ! grep -q '^security.mac.do.rules=' "$SYSCTL_CONF" 2>/dev/null; then
echo "ERROR: security.mac.do.rules not set in $SYSCTL_CONF" >&2
return 1
fi
if ! grep -q '^net.link.tap.up_on_open=1$' "$SYSCTL_CONF" 2>/dev/null; then
echo "ERROR: net.link.tap.up_on_open not set in $SYSCTL_CONF" >&2
return 1
fi
# Check environment profile exists
if [ ! -f "$PROFILE_DIR/clawdie.sh" ]; then
echo "ERROR: $PROFILE_DIR/clawdie.sh not created" >&2
return 1
fi
if ! grep -q '^linux_enable=YES$' "$RC_CONF" 2>/dev/null; then
echo "ERROR: linux_enable not set in $RC_CONF" >&2
return 1
fi
if ! grep -q '^vm_enable=YES$' "$RC_CONF" 2>/dev/null; then
echo "ERROR: vm_enable not set in $RC_CONF" >&2
return 1
fi
if ! grep -q '^kld_list=' "$RC_CONF" 2>/dev/null; then
echo "ERROR: kld_list not set in $RC_CONF" >&2
return 1
fi
log_msg "[system] Validation passed"
return 0
}
# ============================================================================
# UTILITY: Logging
# ============================================================================
log_msg() {
local msg="$1"
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
}
# ============================================================================
# Export for use by firstboot.sh
# ============================================================================
case "${0##*/}" in
clawdie-shell-system.sh)
# Direct execution (for testing)
clawdie_shell_system_config
clawdie_shell_system_validate
;;
*)
# Sourced from another script — functions available
;;
esac