#!/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" </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 # Start SSH agent on login (FreeBSD: no systemd, no X11 agent launcher). # Non-login shells (tmux windows) use ~/.bashrc fallback instead. if [ -z "$SSH_AUTH_SOCK" ]; then eval $(ssh-agent -s) > /dev/null 2>&1 fi 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 <<'BASHRC' # Clawdie operator interactive bash shell. # Non-interactive shells stop here. case $- in *i*) ;; *) return ;; esac # ── PATH (FreeBSD: /usr/local first) ────────────────────────── export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" [ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh # ── Privilege escalation ────────────────────────────────────── # Clawdie ISO: no sudo pkg. Use FreeBSD mac_do instead. # mac_do rules (gid=0>uid=0) allow wheel→root transitions. # ZFS auto-snapshot provides safety net — no confirmation prompts. sudo() { local pool pool=$(zpool list -H -o name 2>/dev/null | head -1) if [ -n "$pool" ]; then (zfs snapshot "${pool}@cli-$(date +%s)" 2>/dev/null &) fi mdo -u root "$@" } # ── SSH agent (fallback for non-login shells) ───────────────── # .profile starts the agent for login shells; tmux windows # (non-login) inherit via SSH_AUTH_SOCK if the parent had one, # but if not, try a stored env file from a prior login. if [ -z "$SSH_AUTH_SOCK" ] && [ -f ~/.ssh-agent-env ]; then . ~/.ssh-agent-env 2>/dev/null || true fi # ── Prompt ──────────────────────────────────────────────────── PS1='\h:\w\$ ' # ── History ─────────────────────────────────────────────────── export HISTFILE="${HISTFILE:-/tmp/clawdie/bash_history}" export HISTSIZE=5000 export HISTFILESIZE=10000 mkdir -p /tmp/clawdie 2>/dev/null || true # ── Aliases ─────────────────────────────────────────────────── alias ll='ls -lah' alias la='ls -A' alias lt='ls -laht' BASHRC 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 if [ -f /usr/local/etc/xdg/tumbler/tumbler.rc ]; then mkdir -p /home/clawdie/.config/tumbler cp /usr/local/etc/xdg/tumbler/tumbler.rc /home/clawdie/.config/tumbler/tumbler.rc fi chown -R clawdie:clawdie /home/clawdie/.config/tumbler 2>/dev/null || true 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 chmod 644 /home/clawdie/.config/tumbler/tumbler.rc 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" </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