#!/bin/sh # clawdie-iso operator USB build script # Produces a bootable FreeBSD XFCE operator USB image with Clawdie-AI pre-bundled. # All packages are fetched and bundled for fully offline installation. # # Runtime detection handles configuration (display, GPU, etc.) # # Usage: # ./build.sh # full build (fetch + assemble) # ./build.sh --fetch-only # fetch packages/memstick only (no root needed) # ./build.sh --skip-fetch # assemble only (use cached packages) # ./build.sh --skip-memstick-fetch # fetch packages, reuse cached FreeBSD memstick # ./build.sh --live-default-password # set live clawdie password to quindecim # ./build.sh --clawdie-password ... # set an explicit live clawdie password # ./build.sh --clawdie-version 1.0.2 # pin Clawdie-AI release tag v1.0.2 # ./build.sh --clawdie-ref main # bundle a branch/tag/commit ref # # Requirements (run on FreeBSD host): # pkg install curl # for fetching # pkg install node24 npm-node24 # to bundle clawdie-ai node_modules for offline firstboot # pkg install (root) # for step 5-6 (mdconfig, mount) # # The tmp/packages/ directory produced here is dual-purpose: # 1. Bundled into the ISO for offline installation # 2. Used to seed zroot/pkg-cache on the installed system for offline jail provisioning set -e SCRIPT_DIR="$(dirname "$(realpath "$0")")" # Make build host command lookup independent from the invoking user's login PATH. PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" export PATH NPM_CONFIG_UPDATE_NOTIFIER=false export NPM_CONFIG_UPDATE_NOTIFIER TMP_DIR="${SCRIPT_DIR}/tmp" NPM_CONFIG_GLOBALCONFIG="${TMP_DIR}/npm-globalconfig" NPM_CONFIG_USERCONFIG="${TMP_DIR}/npm-userconfig" export NPM_CONFIG_GLOBALCONFIG NPM_CONFIG_USERCONFIG PKG_LIST_DIR="${SCRIPT_DIR}/packages" PKG_REPO_DIR="${TMP_DIR}/packages" NPM_GLOBALS_DIR="${TMP_DIR}/npm-globals" CACHE_DIR="${TMP_DIR}/cache" OUTPUT_DIR="${TMP_DIR}/output" LIVE_SESSION_DIR="${SCRIPT_DIR}/live/operator-session" mkdir -p "$TMP_DIR" . "${SCRIPT_DIR}/build.cfg" BUILD_HOST_USER="" BUILD_HOST_HOME="" if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then BUILD_HOST_USER="$SUDO_USER" BUILD_HOST_HOME="$(getent passwd "$BUILD_HOST_USER" | cut -d: -f6)" fi if [ "$(id -u)" -eq 0 ] && [ -z "${BUILD_HOST_USER}" ]; then echo "WARN: build is running as root without SUDO_USER; npm stages cannot drop to a non-root build user." fi # --- argument parsing --- SKIP_FETCH=0 SKIP_MEMSTICK_FETCH=0 FETCH_ONLY=0 LIVE_DEFAULT_PASSWORD=0 while [ "$#" -gt 0 ]; do case "$1" in --clawdie-version) CLAWDIE_VERSION="$2"; CLAWDIE_REF="v$2"; shift 2 ;; --clawdie-ref) CLAWDIE_REF="$2"; CLAWDIE_VERSION="$2"; shift 2 ;; --skip-fetch) SKIP_FETCH=1; shift ;; --skip-memstick-fetch) SKIP_MEMSTICK_FETCH=1; shift ;; --fetch-only) FETCH_ONLY=1; shift ;; --live-default-password) LIVE_DEFAULT_PASSWORD=1; shift ;; --assistant-name) ASSISTANT_NAME="$2"; shift 2 ;; --domain) AGENT_DOMAIN="$2"; shift 2 ;; --tz) TZ="$2"; shift 2 ;; --ssh-key) SSH_PUBLIC_KEY="$2"; shift 2 ;; --tailscale-auth-key) TAILSCALE_AUTHKEY="$2"; shift 2 ;; --root-password) ROOT_PASSWORD="$2"; shift 2 ;; --clawdie-password) CLAWDIE_USER_PASSWORD="$2"; shift 2 ;; *) echo "Unknown arg: $1"; exit 1 ;; esac done if [ "$LIVE_DEFAULT_PASSWORD" -eq 1 ] && [ -z "${CLAWDIE_USER_PASSWORD:-}" ]; then CLAWDIE_USER_PASSWORD="quindecim" fi TAILSCALE_AUTH_KEY_BAKED=false if [ -n "${TAILSCALE_AUTHKEY:-}" ]; then TAILSCALE_AUTH_KEY_BAKED=true fi LIVE_SSH_PUBKEY_FP="" if [ -n "${SSH_PUBLIC_KEY:-}" ]; then _ssh_fp_tmp=$(mktemp "${TMP_DIR}/live-ssh-pubkey.XXXXXX") printf '%s\n' "${SSH_PUBLIC_KEY}" > "${_ssh_fp_tmp}" LIVE_SSH_PUBKEY_FP=$(ssh-keygen -lf "${_ssh_fp_tmp}" 2>/dev/null | awk '{print $2}') rm -f "${_ssh_fp_tmp}" if [ -z "${LIVE_SSH_PUBKEY_FP:-}" ]; then echo "ERROR: could not derive fingerprint from --ssh-key input" exit 1 fi fi if [ "${BUILD_CHANNEL}" = "release" ]; then case "${CLAWDIE_REF}" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "ERROR: release builds must pin a Clawdie-AI tag with --clawdie-version X.Y.Z" echo " Current Clawdie ref: ${CLAWDIE_REF}" exit 1 ;; esac fi echo "==> clawdie-iso build" echo " ISO : ${ISO_VERSION}-${BUILD_CHANNEL}" echo " FreeBSD : ${FREEBSD_VERSION} ${FREEBSD_ARCH}" echo " Clawdie : ${CLAWDIE_REF}" echo " Desktop : ${DEFAULT_DESKTOP}" echo " Pkg : ${DEFAULT_PKG_BRANCH}" echo " GPU : ${GPU_DRIVER:-auto-detect}" echo " NVIDIA universal : ${NVIDIA_UNIVERSAL:-NO}" echo " Target : ${TARGET:-baremetal}" echo " Colibri : ${FEATURE_COLIBRI:-NO}" echo " zot agent : ${FEATURE_ZOT:-NO} (${ZOT_VERSION:-})" echo "" # Name the output for the thing we are actually building. # Keep the date short and operator-facing. Use a recognizable codename for the # FreeBSD major release when we have one, otherwise fall back to the old # fbsd stamp. BUILD_DATE_STAMP="$(date +%d.%m.%y)" BUILD_REPO_SHORT_COMMIT="$(git -C "${SCRIPT_DIR}" rev-parse --short=7 HEAD 2>/dev/null || echo unknown)" BUILD_FREEBSD_MAJOR="${FREEBSD_RELEASE_SERIES%%.*}" BUILD_FREEBSD_STAMP="$(printf '%s' "${FREEBSD_RELEASE_SERIES}" | tr 'A-Z' 'a-z')" case "${BUILD_FREEBSD_MAJOR}" in 15) BUILD_FREEBSD_CODENAME="quindecim" ;; *) BUILD_FREEBSD_CODENAME="fbsd${BUILD_FREEBSD_STAMP}" ;; esac IMAGE_NAME="clawdie-${DEFAULT_DESKTOP}-${BUILD_FREEBSD_CODENAME}-usb-${BUILD_DATE_STAMP}-${BUILD_REPO_SHORT_COMMIT}.img" : > "$NPM_CONFIG_GLOBALCONFIG" : > "$NPM_CONFIG_USERCONFIG" # --- helper: read package lists into a single deduplicated list --- pkg_list_all() { # Operator USB: include host, jail, desktop, GPU, and disk-install extras # for offline use. Runtime detection handles configuration. echo "==> Operator USB: including packages (host + jails + XFCE desktop + GPU drivers + disk extras)" >&2 cat \ "${PKG_LIST_DIR}/pkg-list-host.txt" \ "${PKG_LIST_DIR}/pkg-list-jails.txt" \ "${PKG_LIST_DIR}/pkg-list-desktop-base.txt" \ "${PKG_LIST_DIR}/pkg-list-xfce.txt" \ "${PKG_LIST_DIR}/pkg-list-xfce-theming.txt" \ "${PKG_LIST_DIR}/pkg-list-live-operator.txt" \ "${PKG_LIST_DIR}/pkg-list-disk-install-extras.txt" \ "${PKG_LIST_DIR}/pkg-list-nvidia-all.txt" \ | grep -v '^#' | grep -v '^$' | sort -u } pkg_list_live_installer() { cat \ "${PKG_LIST_DIR}/pkg-list-desktop-base.txt" \ "${PKG_LIST_DIR}/pkg-list-xfce.txt" \ "${PKG_LIST_DIR}/pkg-list-xfce-theming.txt" \ "${PKG_LIST_DIR}/pkg-list-live-operator.txt" \ | grep -v '^#' \ | grep -v '^$' \ | sort -u } pkg_list_live_nvidia() { case "${GPU_DRIVER:-}" in nvidia-390) cat "${PKG_LIST_DIR}/pkg-list-nvidia-390.txt" ;; nvidia-470) cat "${PKG_LIST_DIR}/pkg-list-nvidia-470.txt" ;; nvidia-590) cat "${PKG_LIST_DIR}/pkg-list-nvidia-590.txt" ;; *) return 0 ;; esac | grep -v '^#' | grep -v '^$' | sort -u } set_config_line() { _file="$1" _assignment="$2" _name=$(echo "$_assignment" | cut -d= -f1) mkdir -p "$(dirname "$_file")" touch "$_file" if grep -q "^${_name}=" "$_file" 2>/dev/null; then sed -i '' "s|^${_name}=.*|${_assignment}|" "$_file" else echo "$_assignment" >> "$_file" fi } append_rc_list_values() { _file="$1" _name="$2" shift 2 mkdir -p "$(dirname "$_file")" touch "$_file" _current="" if grep -q "^${_name}=" "$_file" 2>/dev/null; then _current=$(sed -n "s/^${_name}=\"\\(.*\\)\"/\\1/p" "$_file" | head -1) fi _merged="$_current" for _value in "$@"; do if [ -z "$_merged" ]; then _merged="$_value" continue fi case " ${_merged} " in *" ${_value} "*) ;; *) _merged="${_merged} ${_value}" ;; esac done set_config_line "$_file" "${_name}=\"${_merged}\"" } ensure_fstab_line() { _file="$1" _pattern="$2" _line="$3" mkdir -p "$(dirname "$_file")" touch "$_file" if ! grep -Eq "$_pattern" "$_file" 2>/dev/null; then echo "$_line" >> "$_file" fi } set_fstab_line() { _file="$1" _pattern="$2" _line="$3" mkdir -p "$(dirname "$_file")" touch "$_file" _tmp="${_file}.tmp.$$" awk -v pattern="$_pattern" -v line="$_line" ' BEGIN { replaced = 0 } $0 ~ pattern { if (!replaced) { print line replaced = 1 } next } { print } END { if (!replaced) { print line } } ' "$_file" > "$_tmp" mv "$_tmp" "$_file" } pkg_archive_for() { _pkg_name="$1" # Match exact package names only. A loose `${name}-*.pkg` pattern lets a # missing package such as `xfce` resolve to `xfce-icons-elementary`, which # silently builds an image without the requested desktop session. { find "${PKG_REPO_DIR}/All" -type f -name "${_pkg_name}-[0-9]*.pkg" 2>/dev/null find "${PKG_REPO_DIR}" -maxdepth 2 -type f -name "${_pkg_name}-[0-9]*.pkg" 2>/dev/null } | sort -u | tail -1 } resolve_colibri_paths() { _resolved_colibri_repo="${COLIBRI_REPO:-/home/clawdie/ai/colibri}" case "${_resolved_colibri_repo}" in /*) ;; *) _resolved_colibri_repo="${SCRIPT_DIR}/${_resolved_colibri_repo}" ;; esac if [ -n "${COLIBRI_ARTIFACT_DIR:-}" ]; then _resolved_colibri_artifact_dir="${COLIBRI_ARTIFACT_DIR}" case "${_resolved_colibri_artifact_dir}" in /*) ;; *) _resolved_colibri_artifact_dir="${SCRIPT_DIR}/${_resolved_colibri_artifact_dir}" ;; esac else _resolved_colibri_artifact_dir="${_resolved_colibri_repo}/target/release" fi } resolve_clawdie_ai_repo() { _resolved_clawdie_ai_repo="${CLAWDIE_AI_REPO:-/home/clawdie/ai/clawdie-ai}" case "${_resolved_clawdie_ai_repo}" in /*) ;; *) _resolved_clawdie_ai_repo="${SCRIPT_DIR}/${_resolved_clawdie_ai_repo}" ;; esac } preflight_colibri_artifacts() { [ "${FEATURE_COLIBRI:-NO}" = "YES" ] || return 0 [ "${FETCH_ONLY:-0}" -eq 0 ] || return 0 resolve_colibri_paths _colibri_rc="${_resolved_colibri_repo}/packaging/freebsd/colibri_daemon.in" _colibri_newsyslog="${_resolved_colibri_repo}/packaging/freebsd/newsyslog-colibri.conf" if [ ! -f "${_colibri_rc}" ]; then echo "ERROR: Colibri rc.d source missing: ${_colibri_rc}" echo " Set COLIBRI_REPO=/path/to/colibri or FEATURE_COLIBRI=NO." exit 1 fi if ! grep -q '^command="/usr/sbin/daemon"' "${_colibri_rc}"; then echo "ERROR: Colibri rc.d source does not supervise with daemon(8): ${_colibri_rc}" echo " Update the Colibri checkout before building; a foreground daemon blocks live boot." exit 1 fi if [ ! -f "${_colibri_newsyslog}" ]; then echo "ERROR: Colibri newsyslog source missing: ${_colibri_newsyslog}" echo " Set COLIBRI_REPO=/path/to/colibri or FEATURE_COLIBRI=NO." exit 1 fi for _colibri_bin in colibri-daemon colibri colibri-smoke-agent; do if [ ! -x "${_resolved_colibri_artifact_dir}/${_colibri_bin}" ]; then echo "ERROR: Colibri release binary missing: ${_resolved_colibri_artifact_dir}/${_colibri_bin}" echo " Build first: (cd ${_resolved_colibri_repo} && cargo build --workspace --release)" echo " Or set FEATURE_COLIBRI=NO to skip Colibri staging." exit 1 fi done } resolve_zot_paths() { _resolved_zot_repo="${ZOT_REPO:-${SCRIPT_DIR}/../zot}" case "${_resolved_zot_repo}" in /*) ;; *) _resolved_zot_repo="${SCRIPT_DIR}/${_resolved_zot_repo}" ;; esac if [ -n "${ZOT_ARTIFACT_DIR:-}" ]; then _resolved_zot_artifact_dir="${ZOT_ARTIFACT_DIR}" case "${_resolved_zot_artifact_dir}" in /*) ;; *) _resolved_zot_artifact_dir="${SCRIPT_DIR}/${_resolved_zot_artifact_dir}" ;; esac else _resolved_zot_artifact_dir="${_resolved_zot_repo}/bin" fi } preflight_zot_artifacts() { [ "${FEATURE_ZOT:-NO}" = "YES" ] || return 0 [ "${FETCH_ONLY:-0}" -eq 0 ] || return 0 resolve_zot_paths if [ ! -x "${_resolved_zot_artifact_dir}/zot" ]; then echo "ERROR: zot binary missing: ${_resolved_zot_artifact_dir}/zot" echo " zot has no FreeBSD release — build it first:" echo " (cd ${_resolved_zot_repo} && git checkout ${ZOT_VERSION:-v0.2.29} && \\" echo " GOOS=freebsd GOARCH=amd64 go build -trimpath -o bin/zot ./cmd/zot)" echo " Or set FEATURE_ZOT=NO to skip zot staging." exit 1 fi } override_networkmgr_package() { _networkmgr_pkg=$(find "${PKG_REPO_DIR}/All/Hashed" -maxdepth 1 -type f -name 'networkmgr-*.pkg' | sort | tail -1) if [ -z "${_networkmgr_pkg:-}" ]; then echo "ERROR: networkmgr package not found under ${PKG_REPO_DIR}/All/Hashed" exit 1 fi echo "==> [2c/7] Repacking NetworkMgr for mac_do/mdo policy..." _networkmgr_stage=$(mktemp -d "${TMP_DIR}/networkmgr-override.XXXXXX") _networkmgr_log="${TMP_DIR}/networkmgr-repack.log" if ! node "${SCRIPT_DIR}/scripts/repack-networkmgr-for-mdo.mjs" "${_networkmgr_pkg}" "${_networkmgr_stage}" >"${_networkmgr_log}" 2>&1; then cat "${_networkmgr_log}" rm -rf "${_networkmgr_stage}" echo "ERROR: failed to rebuild local networkmgr package" exit 1 fi rm -f "${PKG_REPO_DIR}/All/Hashed"/networkmgr-*.pkg mv "${_networkmgr_stage}"/networkmgr-*.pkg "${PKG_REPO_DIR}/All/Hashed/" # After replacing NetworkMgr's sudo dependency with the local mac_do/mdo # package, remove any fetched sudo archive from the offline repo that will # be copied onto the USB image. find "${PKG_REPO_DIR}/All" -type f -name 'sudo-*.pkg' -delete rm -rf "${_networkmgr_stage}" } json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' } resolve_clawdie_commit() { _ref="$1" _repo="https://code.smilepowered.org/clawdie/clawdie-ai.git" if printf '%s' "$_ref" | grep -Eq '^[0-9a-fA-F]{40}$'; then printf '%s\n' "$_ref" return 0 fi if command -v git >/dev/null 2>&1; then git ls-remote "$_repo" \ "refs/heads/${_ref}" \ "refs/tags/${_ref}^{}" \ "refs/tags/${_ref}" 2>/dev/null \ | awk ' $2 ~ /\^\{\}$/ { print $1; found = 1; exit } first == "" { first = $1 } END { if (!found && first != "") print first } ' fi } resolve_latest_clawdie_tag() { _repo_api="https://code.smilepowered.org/api/v1/repos/clawdie/clawdie-ai" _repo_git="https://code.smilepowered.org/clawdie/clawdie-ai.git" _tag=$( curl -fsS "${_repo_api}/releases?limit=20" 2>/dev/null \ | grep -o '"tag_name":"[^"]*"' \ | head -1 \ | cut -d'"' -f4 ) if [ -n "$_tag" ]; then printf '%s\n' "$_tag" return 0 fi git ls-remote --tags "$_repo_git" 2>/dev/null \ | awk -F/ '$2 == "tags" && $3 ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ { print $3 }' \ | sed 's/^v//' \ | sort -t. -k1,1n -k2,2n -k3,3n \ | tail -n 1 \ | sed 's/^/v/' } is_pinned_clawdie_ref() { _ref="$1" printf '%s' "$_ref" | grep -Eq '^[0-9a-fA-F]{40}$|^v[0-9]+\.[0-9]+\.[0-9]+$' } write_build_manifest() { _manifest_path="$1" _iso_repo_commit="unknown" _iso_repo_dirty="null" _live_ssh_pubkey_fp_json="null" _tailscale_auth_key_baked="${TAILSCALE_AUTH_KEY_BAKED:-false}" if command -v git >/dev/null 2>&1 && git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1; then _iso_repo_commit=$(git -C "$SCRIPT_DIR" rev-parse HEAD 2>/dev/null || echo unknown) if git -C "$SCRIPT_DIR" diff --quiet 2>/dev/null && git -C "$SCRIPT_DIR" diff --cached --quiet 2>/dev/null; then _iso_repo_dirty="false" else _iso_repo_dirty="true" fi fi if [ -n "${LIVE_SSH_PUBKEY_FP:-}" ]; then _live_ssh_pubkey_fp_json="\"$(json_escape "${LIVE_SSH_PUBKEY_FP}")\"" fi mkdir -p "$(dirname "$_manifest_path")" cat > "$_manifest_path" </dev/null || true exit 1 fi truncate -s "$((_slice_size * 512))" "$_memstick_slice_img" dd if="$MEMSTICK" of="$_memstick_slice_img" bs=512 skip="$_slice_start" conv=sparse status=none MD_SLICE=$(mdconfig -a -t vnode -f "$_memstick_slice_img") _ufs_meta=$( bsdlabel "/dev/${MD_SLICE}" 2>/dev/null \ | awk '$1 == "a:" { print $2 " " $3; exit }' ) _ufs_size=$(echo "$_ufs_meta" | awk '{print $1}') _ufs_offset=$(echo "$_ufs_meta" | awk '{print $2}') if [ -z "${_ufs_size:-}" ] || [ -z "${_ufs_offset:-}" ]; then echo "ERROR: could not determine UFS root partition offset in ${MEMSTICK}" mdconfig -d -u "${MD_SLICE}" 2>/dev/null || true mdconfig -d -u "${MD_SRC}" 2>/dev/null || true exit 1 fi truncate -s "$((_ufs_size * 512))" "$_memstick_ufs_img" dd if="$_memstick_slice_img" of="$_memstick_ufs_img" bs=512 skip="$_ufs_offset" conv=sparse status=none MD_ROOTFS=$(mdconfig -a -t vnode -f "$_memstick_ufs_img") mount -r -t ufs "/dev/${MD_ROOTFS}" "$_memstick_mount" } cleanup_memstick_rootfs() { _memstick_mount="$1" umount "$_memstick_mount" 2>/dev/null || true [ -n "${MD_ROOTFS:-}" ] && mdconfig -d -u "${MD_ROOTFS}" 2>/dev/null || true [ -n "${MD_SLICE:-}" ] && mdconfig -d -u "${MD_SLICE}" 2>/dev/null || true [ -n "${MD_SRC:-}" ] && mdconfig -d -u "${MD_SRC}" 2>/dev/null || true rm -f "${CACHE_DIR}/memstick-freebsd-slice.img" "${CACHE_DIR}/memstick-rootfs.img" MD_ROOTFS="" MD_SLICE="" MD_SRC="" } verify_memstick_cache() { if [ ! -f "$MEMSTICK" ] || [ ! -f "${MEMSTICK}.SHA256" ]; then return 1 fi _expected_sha=$( awk -v image="$(basename "$MEMSTICK")" ' $2 == "(" image ")" { print $4; exit } ' "${MEMSTICK}.SHA256" ) [ -n "${_expected_sha:-}" ] || return 1 [ "$(/sbin/sha256 -q "$MEMSTICK")" = "$_expected_sha" ] } install_image_bootcode() { _image_md="$1" _image_root="$2" _freebsd_slice_index="${3:-}" if [ -z "${_freebsd_slice_index}" ]; then if [ -e "/dev/${_image_md}s2" ]; then _freebsd_slice_index="2" else _freebsd_slice_index="1" fi fi if [ ! -f "${_image_root}/boot/mbr" ] || [ ! -f "${_image_root}/boot/boot" ]; then echo "ERROR: missing bootcode files in ${_image_root}/boot" exit 1 fi gpart bootcode -b "${_image_root}/boot/mbr" "/dev/${_image_md}" gpart bootcode -b "${_image_root}/boot/boot" "/dev/${_image_md}s${_freebsd_slice_index}" } install_image_uefi_bootcode() { _image_md="$1" _image_root="$2" _efi_slice_index="${3:-1}" _efi_mount="${CACHE_DIR}/efi-mnt" if [ ! -f "${_image_root}/boot/loader.efi" ]; then echo "ERROR: missing UEFI loader at ${_image_root}/boot/loader.efi" exit 1 fi newfs_msdos -L EFISYS "/dev/${_image_md}s${_efi_slice_index}" mkdir -p "${_efi_mount}" mount_msdosfs "/dev/${_image_md}s${_efi_slice_index}" "${_efi_mount}" mkdir -p "${_efi_mount}/EFI/BOOT" cp "${_image_root}/boot/loader.efi" "${_efi_mount}/EFI/BOOT/BOOTX64.EFI" sync umount "${_efi_mount}" rmdir "${_efi_mount}" 2>/dev/null || true } run_live_chroot() { chroot "$MOUNT_POINT" /usr/bin/env \ PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ LD_LIBRARY_PATH="/usr/local/lib:/usr/local/lib/compat:/lib:/usr/lib" \ "$@" } refresh_live_desktop_caches() { echo " Refreshing live desktop schema/icon/mime caches..." if [ -x "${MOUNT_POINT}/usr/local/bin/glib-compile-schemas" ]; then run_live_chroot /usr/local/bin/glib-compile-schemas /usr/local/share/glib-2.0/schemas fi if [ ! -f "${MOUNT_POINT}/usr/local/share/glib-2.0/schemas/gschemas.compiled" ]; then echo "ERROR: GLib GSettings schema cache missing from live image" exit 1 fi if [ -x "${MOUNT_POINT}/usr/local/bin/gdk-pixbuf-query-loaders" ]; then run_live_chroot /usr/local/bin/gdk-pixbuf-query-loaders --update-cache fi if [ ! -f "${MOUNT_POINT}/usr/local/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" ]; then echo "ERROR: gdk-pixbuf loader cache missing from live image" exit 1 fi if ! grep -qi 'png' "${MOUNT_POINT}/usr/local/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"; then echo "ERROR: gdk-pixbuf PNG loader missing from live image cache" exit 1 fi if [ -x "${MOUNT_POINT}/usr/local/bin/update-mime-database" ] && [ -d "${MOUNT_POINT}/usr/local/share/mime" ]; then run_live_chroot /usr/local/bin/update-mime-database /usr/local/share/mime fi if [ -x "${MOUNT_POINT}/usr/local/bin/update-desktop-database" ] && [ -d "${MOUNT_POINT}/usr/local/share/applications" ]; then run_live_chroot /usr/local/bin/update-desktop-database /usr/local/share/applications fi if [ -x "${MOUNT_POINT}/usr/local/bin/gtk-update-icon-cache" ]; then # Regenerate every installed icon theme cache. FreeBSD packages and # Clawdie branding install icons into hicolor and other theme # directories; missed caches show up as blank/dot panel icons. for _icon_theme_dir in "${MOUNT_POINT}/usr/local/share/icons"/*/; do [ -d "$_icon_theme_dir" ] || continue _theme_name=$(basename "$_icon_theme_dir") run_live_chroot /usr/local/bin/gtk-update-icon-cache -f -t "/usr/local/share/icons/${_theme_name}" || true done fi if [ -x "${MOUNT_POINT}/usr/local/bin/fc-cache" ] && [ -d "${MOUNT_POINT}/usr/local/share/fonts" ]; then run_live_chroot /usr/local/bin/fc-cache -f fi } ensure_sshd_include_line() { _sshd_config="$1" _include_line='Include /etc/ssh/sshd_config.d/*.conf' if [ ! -f "$_sshd_config" ]; then echo "ERROR: sshd_config missing from live image" exit 1 fi if grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "$_sshd_config"; then return 0 fi _tmp="${_sshd_config}.tmp.$$" { printf '%s\n' "${_include_line}" cat "$_sshd_config" } > "$_tmp" mv "$_tmp" "$_sshd_config" } configure_nsswitch_hosts_line() { _file="$1" _line='hosts: files mdns_minimal [NOTFOUND=return] dns mdns' if [ ! -f "$_file" ]; then echo "ERROR: nsswitch.conf missing from live image" exit 1 fi set_fstab_line "$_file" '^[[:space:]]*hosts:' "$_line" } install_live_runtime_packages() { echo " Installing live operator runtime packages into image..." _pkg_files="" for _pkg in $(pkg_list_live_installer); do _archive=$(pkg_archive_for "$_pkg") if [ -z "${_archive:-}" ]; then echo "ERROR: missing package archive for ${_pkg}" exit 1 fi _pkg_files="${_pkg_files} ${_archive}" done for _pkg in $(pkg_list_live_nvidia); do _archive=$(pkg_archive_for "$_pkg") if [ -z "${_archive:-}" ]; then echo "ERROR: missing NVIDIA package archive for ${_pkg}" exit 1 fi _pkg_files="${_pkg_files} ${_archive}" done mkdir -p "${MOUNT_POINT}/dev" "${MOUNT_POINT}/proc" _mounted_devfs=0 _mounted_procfs=0 if mount -t devfs devfs "${MOUNT_POINT}/dev"; then _mounted_devfs=1 fi if mount -t procfs proc "${MOUNT_POINT}/proc"; then _mounted_procfs=1 fi if ! env ASSUME_ALWAYS_YES=yes HANDLE_RC_SCRIPTS=no \ /usr/local/sbin/pkg-static -o PKG_TRIGGERS_ENABLE=false -r "$MOUNT_POINT" add -f ${_pkg_files}; then [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true echo "ERROR: failed to install live GUI runtime packages into image" exit 1 fi if ! chroot "${MOUNT_POINT}" /usr/sbin/pw usershow sddm >/dev/null 2>&1; then [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true echo "ERROR: sddm user missing from live image after package install" exit 1 fi # webcamd creates /dev/videoN with mode 0660 root:webcamd. Add clawdie # to that group so the operator can open the camera without sudo. if chroot "${MOUNT_POINT}" /usr/sbin/pw groupshow webcamd >/dev/null 2>&1; then chroot "${MOUNT_POINT}" /usr/sbin/pw groupmod webcamd -m clawdie 2>/dev/null || true else [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true echo "ERROR: webcamd group missing from live image after package install" exit 1 fi refresh_live_desktop_caches [ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true [ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true } install_colibri_service() { [ "${FEATURE_COLIBRI:-NO}" = "YES" ] || { echo " Colibri service staging disabled (FEATURE_COLIBRI=${FEATURE_COLIBRI:-NO})" return 0 } echo " Staging Colibri service..." resolve_colibri_paths env \ COLIBRI_REPO="${_resolved_colibri_repo}" \ COLIBRI_ARTIFACT_DIR="${_resolved_colibri_artifact_dir}" \ COLIBRI_STAGE_ENABLE="${COLIBRI_DAEMON_ENABLE:-YES}" \ COLIBRI_COST_MODE="${COLIBRI_COST_MODE:-smart}" \ "${SCRIPT_DIR}/scripts/stage-colibri-iso.sh" "${MOUNT_POINT}" if ! /usr/sbin/pw -R "${MOUNT_POINT}" groupshow colibri >/dev/null 2>&1; then /usr/sbin/pw -R "${MOUNT_POINT}" groupadd colibri fi if ! /usr/sbin/pw -R "${MOUNT_POINT}" usershow colibri >/dev/null 2>&1; then /usr/sbin/pw -R "${MOUNT_POINT}" useradd colibri \ -g colibri \ -d /var/db/colibri \ -s /usr/sbin/nologin \ -c "Colibri Daemon" fi mkdir -p \ "${MOUNT_POINT}/var/db/colibri" \ "${MOUNT_POINT}/var/run/colibri" \ "${MOUNT_POINT}/var/log/colibri" chroot "${MOUNT_POINT}" chown -R colibri:colibri \ /var/db/colibri \ /var/run/colibri \ /var/log/colibri chmod 0755 \ "${MOUNT_POINT}/var/db/colibri" \ "${MOUNT_POINT}/var/run/colibri" \ "${MOUNT_POINT}/var/log/colibri" # Allow operator to use 'colibri' CLI without root if /usr/sbin/pw -R "${MOUNT_POINT}" usershow clawdie >/dev/null 2>&1; then /usr/sbin/pw -R "${MOUNT_POINT}" groupmod colibri -m clawdie fi set_config_line "${MOUNT_POINT}/etc/rc.conf" "colibri_daemon_enable=\"${COLIBRI_DAEMON_ENABLE:-YES}\"" set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_user="colibri"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_group="colibri"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_data_dir="/var/db/colibri"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_run_dir="/var/run/colibri"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_socket="/var/run/colibri/colibri.sock"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_db_path="/var/db/colibri/colibri.sqlite"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_logfile="/var/log/colibri/daemon.log"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_host="$(hostname)"' set_config_line "${MOUNT_POINT}/etc/rc.conf" "colibri_cost_mode=\"${COLIBRI_COST_MODE:-smart}\"" if [ ! -x "${MOUNT_POINT}/usr/local/bin/colibri-daemon" ] || [ ! -x "${MOUNT_POINT}/usr/local/bin/colibri" ]; then echo "ERROR: Colibri binaries missing from live image" exit 1 fi if [ ! -x "${MOUNT_POINT}/usr/local/etc/rc.d/colibri_daemon" ]; then echo "ERROR: Colibri rc.d script missing from live image" exit 1 fi if ! /usr/sbin/pw -R "${MOUNT_POINT}" usershow colibri >/dev/null 2>&1; then echo "ERROR: colibri service user missing from live image" exit 1 fi # Seed the skills catalog with operator-useful entries. The daemon # creates the SQLite DB on first start; pre-populate it so skills # are available immediately without a first-boot script. _colibri_db="${MOUNT_POINT}/var/db/colibri/colibri.sqlite" if command -v sqlite3 >/dev/null 2>&1; then sqlite3 "${_colibri_db}" "CREATE TABLE IF NOT EXISTS skills ( id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL UNIQUE, description TEXT, category TEXT, created_at TEXT NOT NULL );" 2>/dev/null || true _now=$(date -u +%Y-%m-%dT%H:%M:%SZ) sqlite3 "${_colibri_db}" "INSERT OR IGNORE INTO skills (id, name, description, category, created_at) VALUES ('$(uuidgen || echo 00000000-0000-0000-0000-000000000001)', 'freebsd-live-usb', 'FreeBSD live USB operator workstation procedures', 'freebsd', '${_now}'), ('$(uuidgen || echo 00000000-0000-0000-0000-000000000002)', 'colibri-smoke', 'Colibri daemon smoke test and validation', 'colibri', '${_now}'), ('$(uuidgen || echo 00000000-0000-0000-0000-000000000003)', 'iso-build', 'Clawdie ISO build and staging workflow', 'iso', '${_now}'), ('$(uuidgen || echo 00000000-0000-0000-0000-000000000004)', 'tailscale-join', 'Tailscale mesh join procedure for operator USB', 'networking', '${_now}'), ('$(uuidgen || echo 00000000-0000-0000-0000-000000000005)', 'disk-deploy', 'Deploy from USB live to permanent disk install. Provisions ZFS pool, installs FreeBSD boot environment, migrates config, and prepares for the future deployed-system clawdie service.', 'clawdie', '${_now}'), ('$(uuidgen || echo 00000000-0000-0000-0000-000000000006)', 'deployed-clawdie-health', 'Future post-deploy health check for service clawdie once the deployed-system service implementation lands.', 'clawdie', '${_now}');" 2>/dev/null || true chroot "${MOUNT_POINT}" chown colibri:colibri /var/db/colibri/colibri.sqlite 2>/dev/null || true echo " colibri skills seeded: 6 entries" fi # Import clawdie-ai skill definitions into the catalog. # Reads .agent/skills/*/SKILL.md and registers name + description. resolve_clawdie_ai_repo _clawdie_ai_dir="${_resolved_clawdie_ai_repo}" if [ -d "${_clawdie_ai_dir}/.agent/skills" ]; then "${SCRIPT_DIR}/scripts/import-clawdie-skills.sh" \ "${_clawdie_ai_dir}" "${MOUNT_POINT}" else echo " clawdie-ai checkout not found, skipping skill import" fi } install_zot_agent() { [ "${FEATURE_ZOT:-NO}" = "YES" ] || { echo " zot agent staging disabled (FEATURE_ZOT=${FEATURE_ZOT:-NO})" return 0 } echo " Staging zot agent (${ZOT_VERSION:-})..." resolve_zot_paths env \ ZOT_ARTIFACT_DIR="${_resolved_zot_artifact_dir}" \ ZOT_OPERATOR="clawdie" \ ZOT_DEEPSEEK_KEY="${ZOT_DEEPSEEK_KEY:-}" \ "${SCRIPT_DIR}/scripts/stage-zot-iso.sh" "${MOUNT_POINT}" # zot state lives in the operator's home; own it as the operator. if /usr/sbin/pw -R "${MOUNT_POINT}" usershow clawdie >/dev/null 2>&1; then chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/.local 2>/dev/null || true fi if [ ! -x "${MOUNT_POINT}/usr/local/bin/zot" ]; then echo "ERROR: zot binary missing from live image" exit 1 fi } # Stage an on-image NVIDIA pkg repo (all branches) so clawdie_live_gpu can # `pkg install` the detected branch at boot (NVIDIA_UNIVERSAL lane). # # FreeBSD-build-host step (authored on Linux; runs + must be validated on # FreeBSD). Verify on the build host: (1) the `pkg fetch -o` layout matches what # `pkg repo` expects, (2) the dependency closure is complete for offline boot # install, (3) image size headroom, (4) `pkg install -r clawdie-nvidia` resolves # from file:// at boot. See doc handoff. install_nvidia_universal_repo() { [ "${NVIDIA_UNIVERSAL:-NO}" = "YES" ] || { echo " NVIDIA universal repo staging disabled (NVIDIA_UNIVERSAL=${NVIDIA_UNIVERSAL:-NO})" return 0 } echo " Staging universal NVIDIA repo (all branches) into image..." _nv_repo="${MOUNT_POINT}/usr/local/share/clawdie/nvidia-repo" mkdir -p "${_nv_repo}" # Fetch all three branches + deps into the on-image repo dir, then build repo # metadata. Uses pkg's own dependency resolution so the closure is complete. if ! env ASSUME_ALWAYS_YES=yes pkg fetch -y -o "${_nv_repo}" -d \ nvidia-driver-390 nvidia-driver-470 nvidia-driver-580 nvidia-settings nvidia-xconfig; then echo "ERROR: failed to fetch NVIDIA packages for universal repo" exit 1 fi if ! pkg repo "${_nv_repo}"; then echo "ERROR: failed to generate NVIDIA universal repo metadata" exit 1 fi mkdir -p "${MOUNT_POINT}/usr/local/etc/pkg/repos" cat > "${MOUNT_POINT}/usr/local/etc/pkg/repos/clawdie-nvidia.conf" <<'EOF' # On-image NVIDIA package repo for the universal GPU lane. Consumed at boot by # clawdie_live_gpu (nvidia-auto mode) via: pkg install -r clawdie-nvidia ... clawdie-nvidia: { url: "file:///usr/local/share/clawdie/nvidia-repo", enabled: yes } EOF echo " NVIDIA universal repo staged at /usr/local/share/clawdie/nvidia-repo" } install_live_npm_globals() { echo " Installing bundled npm globals into live image..." if [ ! -d "$NPM_GLOBALS_DIR" ]; then echo " WARN: ${NPM_GLOBALS_DIR} not found — skipping live npm globals" return 0 fi _live_prefix="${MOUNT_POINT}/opt/clawdie/npm-global" mkdir -p "${_live_prefix}/bin" "${_live_prefix}/lib" "${MOUNT_POINT}/usr/local/bin" _live_npm_home="${CACHE_DIR}/npm-live-home" rm -rf "${_live_npm_home}" mkdir -p "${_live_npm_home}" for _tgz in "${NPM_GLOBALS_DIR}"/*.tgz; do [ -f "${_tgz}" ] || continue # Live bundle policy: ship pi only via npm tarball. codex is covered # separately via the FreeBSD pkg in pkg-list-live-operator.txt. case "$(basename "${_tgz}")" in *claude-code*) echo " skip $(basename "${_tgz}") (broken on FreeBSD live image)" continue ;; *gemini-cli*) echo " skip $(basename "${_tgz}") (not part of current live CLI policy)" continue ;; esac echo " npm install -g $(basename "${_tgz}")" env HOME="${_live_npm_home}" npm_config_prefix="${_live_prefix}" npm install -g --ignore-scripts --no-audit --no-fund "${_tgz}" >/dev/null done for _bin in "${_live_prefix}/bin"/*; do [ -e "${_bin}" ] || continue ln -sf "/opt/clawdie/npm-global/bin/$(basename "${_bin}")" "${MOUNT_POINT}/usr/local/bin/$(basename "${_bin}")" done patch_live_pi_footer_hostname "${_live_prefix}" # npm runs as root on the build host and writes everything as # root:wheel. configure_live_operator_session() used to do this # chown but ran BEFORE install_live_npm_globals (caller order at # bottom of build.sh), so it was chowning two empty directories # before the package tree was written. Do it here, after the files # exist, then validate — fail the build if ownership didn't take # so this regression cannot ship silently again. if ! chroot "${MOUNT_POINT}" /usr/sbin/pw usershow clawdie >/dev/null 2>&1; then echo "ERROR: clawdie user missing — cannot chown npm-global tree" exit 1 fi chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /opt/clawdie _npm_owner=$(chroot "${MOUNT_POINT}" /usr/bin/stat -f '%Su:%Sg' /opt/clawdie/npm-global) if [ "${_npm_owner}" != "clawdie:clawdie" ]; then echo "ERROR: /opt/clawdie/npm-global ownership is ${_npm_owner}, expected clawdie:clawdie" exit 1 fi # Fix pi HTTP/2: the bundled http-dispatcher sets allowH2:false to # work around a Node 26.0 undici decompression bug. We run Node 24 # with undici 8.3, where H2 works correctly. Enabling HTTP/2 gives # HPACK header compression and multiplexing for API calls. _pi_dispatcher="${_live_prefix}/lib/node_modules/@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js" if [ -f "${_pi_dispatcher}" ]; then sed -i '' 's/allowH2: false/allowH2: true/' "${_pi_dispatcher}" echo " pi http-dispatcher: allowH2 → true" fi } patch_live_pi_footer_hostname() { _npm_prefix="$1" _patch_script="${SCRIPT_DIR}/firstboot/patch-pi-footer-hostname.js" _patched=0 if [ ! -f "${_patch_script}" ]; then echo "ERROR: Pi footer patch script missing: ${_patch_script}" exit 1 fi for _footer in \ "${_npm_prefix}/lib/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js" \ "${_npm_prefix}/lib/node_modules/@mariozechner/pi-coding-agent/dist/modes/interactive/components/footer.js" do [ -f "${_footer}" ] || continue echo " patch Pi footer hostname: ${_footer}" node "${_patch_script}" "${_footer}" node --check "${_footer}" _patched=1 done if [ "${_patched}" -ne 1 ]; then echo "ERROR: Pi footer.js not found under ${_npm_prefix}" exit 1 fi } seed_live_ai_source_repo() { _repo_src="$1" _repo_name="$2" _repo_dest="${MOUNT_POINT}/home/clawdie/ai/${_repo_name}" if [ ! -d "${_repo_src}" ]; then echo " Skipping AI source seed ${_repo_name}: ${_repo_src} not found" return 0 fi if ! command -v git >/dev/null 2>&1 || ! git -C "${_repo_src}" rev-parse --git-dir >/dev/null 2>&1; then echo " Skipping AI source seed ${_repo_name}: not a git worktree" return 0 fi echo " Seeding AI source snapshot: ${_repo_name}" rm -rf "${_repo_dest}" mkdir -p "${_repo_dest}" git -C "${_repo_src}" archive --format=tar HEAD | tar -C "${_repo_dest}" -xf - _repo_branch=$(git -C "${_repo_src}" symbolic-ref --short -q HEAD 2>/dev/null || echo detached) _repo_commit=$(git -C "${_repo_src}" rev-parse HEAD 2>/dev/null || echo unknown) _repo_origin=$(git -C "${_repo_src}" remote get-url origin 2>/dev/null || echo unknown) _repo_dirty=false if ! git -C "${_repo_src}" diff --quiet 2>/dev/null || ! git -C "${_repo_src}" diff --cached --quiet 2>/dev/null; then _repo_dirty=true fi cat > "${_repo_dest}/.clawdie-source.json" < "${MOUNT_POINT}/home/clawdie/ai/README.txt" <<'EOF' Clawdie live AI source snapshots These directories are included so the operator can start a local provider-backed Pi session from the live XFCE desktop and inspect the shipped source beside the running system. No API keys, .env files, SSH private keys, build caches, package caches, tmp/ directories, or uncommitted worktree changes are included. Each snapshot has a .clawdie-source.json file recording the source remote, branch, commit, and dirty state at image build time. EOF seed_live_ai_source_repo "${SCRIPT_DIR}" "clawdie-iso" seed_live_ai_source_repo "${_resolved_clawdie_ai_repo}" "clawdie-ai" seed_live_ai_source_repo "${_resolved_colibri_repo}" "colibri" chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/ai } configure_live_operator_session() { echo " Configuring live operator session..." mkdir -p "${MOUNT_POINT}/usr/local/bin" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-bootstrap-launch.sh" \ "${MOUNT_POINT}/usr/local/bin/clawdie-bootstrap-launch.sh" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-noblank-guard.sh" \ "${MOUNT_POINT}/usr/local/bin/clawdie-noblank-guard.sh" install -m 0755 "${LIVE_SESSION_DIR}/hw-report" \ "${MOUNT_POINT}/usr/local/bin/hw-report" # The stock FreeBSD memstick starts bsdinstall from /etc/rc.local before # our graphical live session can own the USB workflow. Preserve a copy for # debugging, but disable the automatic text installer. if [ -f "${MOUNT_POINT}/etc/rc.local" ]; then cp "${MOUNT_POINT}/etc/rc.local" "${MOUNT_POINT}/etc/rc.local.freebsd-installer" rm -f "${MOUNT_POINT}/etc/rc.local" fi # The installer memstick may leave /etc/resolv.conf as a symlink into # /tmp/bsdinstall_etc. The live image mounts /tmp as tmpfs, so that link # becomes dangling on boot and DNS fails despite working DHCP/routing. # Ship a real file, then let DHCP/NetworkMgr replace it at runtime. if [ -L "${MOUNT_POINT}/etc/resolv.conf" ] && \ readlink "${MOUNT_POINT}/etc/resolv.conf" | grep -q '^/tmp/bsdinstall_etc/'; then rm -f "${MOUNT_POINT}/etc/resolv.conf" fi if [ ! -e "${MOUNT_POINT}/etc/resolv.conf" ]; then { echo '# Created by the Clawdie live image build.' echo '# DHCP/NetworkMgr may replace this file after interfaces come up.' } > "${MOUNT_POINT}/etc/resolv.conf" chmod 0644 "${MOUNT_POINT}/etc/resolv.conf" fi # FreeBSD packages ship python3.11 but not a bare python3 symlink. # Scripts that use /usr/bin/env python3 break without this. if [ -x "${MOUNT_POINT}/usr/local/bin/python3.11" ] && \ [ ! -e "${MOUNT_POINT}/usr/local/bin/python3" ]; then ln -sf python3.11 "${MOUNT_POINT}/usr/local/bin/python3" ln -sf python3.11 "${MOUNT_POINT}/usr/local/bin/python" fi mkdir -p "${MOUNT_POINT}/usr/local/etc/sddm.conf.d" install -m 0644 "${LIVE_SESSION_DIR}/sddm.conf" \ "${MOUNT_POINT}/usr/local/etc/sddm.conf.d/50-clawdie-live.conf" mkdir -p "${MOUNT_POINT}/var/lib/sddm" "${MOUNT_POINT}/var/log" cat > "${MOUNT_POINT}/var/lib/sddm/state.conf" <<'EOF' [Last] Session=clawdie-xfce.desktop User=clawdie EOF chmod 0755 "${MOUNT_POINT}/var/lib/sddm" chmod 0644 "${MOUNT_POINT}/var/lib/sddm/state.conf" if chroot "${MOUNT_POINT}" /usr/sbin/pw usershow sddm >/dev/null 2>&1; then chroot "${MOUNT_POINT}" chown -R sddm:sddm /var/lib/sddm fi mkdir -p "${MOUNT_POINT}/usr/local/share/xsessions" install -m 0644 "${LIVE_SESSION_DIR}/clawdie-xfce.desktop" \ "${MOUNT_POINT}/usr/local/share/xsessions/clawdie-xfce.desktop" # Strip any Wayland session .desktop files dropped by transitive # packages. The operator USB has no working Wayland compositor # (XFCE on FreeBSD is X11-only here), so offering a Wayland entry # in the SDDM greeter just hands the operator a non-booting session. if [ -d "${MOUNT_POINT}/usr/local/share/wayland-sessions" ]; then rm -f "${MOUNT_POINT}/usr/local/share/wayland-sessions/"*.desktop rmdir "${MOUNT_POINT}/usr/local/share/wayland-sessions" 2>/dev/null || true fi mkdir -p "${MOUNT_POINT}/usr/local/etc/xdg/autostart" install -m 0644 "${LIVE_SESSION_DIR}/autostart/clawdie-bootstrap.desktop" \ "${MOUNT_POINT}/usr/local/etc/xdg/autostart/clawdie-bootstrap.desktop" install -m 0644 "${LIVE_SESSION_DIR}/autostart/clawdie-noblank-guard.desktop" \ "${MOUNT_POINT}/usr/local/etc/xdg/autostart/clawdie-noblank-guard.desktop" install -m 0644 "${LIVE_SESSION_DIR}/autostart/volumeicon.desktop" \ "${MOUNT_POINT}/usr/local/etc/xdg/autostart/volumeicon.desktop" mkdir -p "${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d" install -m 0644 "${LIVE_SESSION_DIR}/xorg.conf.d/30-keyboard.conf" \ "${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d/30-keyboard.conf" install -m 0644 "${LIVE_SESSION_DIR}/xorg.conf.d/40-clawdie-noblank.conf" \ "${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d/40-clawdie-noblank.conf" mkdir -p "${MOUNT_POINT}/usr/local/etc/rc.d" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-gpu" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_gpu" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-wifi" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_wifi" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-seed" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_seed" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-resolver" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_resolver" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-audio" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_audio" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-power" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_power" mkdir -p "${MOUNT_POINT}/usr/local/etc/polkit-1/rules.d" install -m 0644 "${LIVE_SESSION_DIR}/49-clawdie-power.rules" \ "${MOUNT_POINT}/usr/local/etc/polkit-1/rules.d/49-clawdie-power.rules" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-xfce-session" \ "${MOUNT_POINT}/usr/local/bin/clawdie-xfce-session" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-xfce-session-inner" \ "${MOUNT_POINT}/usr/local/bin/clawdie-xfce-session-inner" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-touchpad-guard" \ "${MOUNT_POINT}/usr/local/bin/clawdie-live-touchpad-guard" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-startx" \ "${MOUNT_POINT}/usr/local/bin/clawdie-startx" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-startx" \ "${MOUNT_POINT}/usr/local/bin/clawdie-gui" for _xfce_xdg_dir in \ "${MOUNT_POINT}/usr/local/etc" \ "${MOUNT_POINT}/usr/local/etc/xdg" \ "${MOUNT_POINT}/usr/local/etc/xdg/xfce4"; do if [ ! -d "$_xfce_xdg_dir" ]; then echo "ERROR: XFCE XDG directory missing from live image: ${_xfce_xdg_dir#${MOUNT_POINT}}" exit 1 fi chmod 0755 "$_xfce_xdg_dir" done if [ -f "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xinitrc" ]; then chmod 0755 "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xinitrc" else echo "ERROR: XFCE xinitrc missing from live image" exit 1 fi if [ ! -x "${MOUNT_POINT}/usr/local/bin/xinit" ] || [ ! -x "${MOUNT_POINT}/usr/local/bin/startx" ]; then echo "ERROR: xinit/startx missing from live image" exit 1 fi if [ ! -x "${MOUNT_POINT}/usr/local/bin/clawdie-startx" ]; then echo "ERROR: clawdie-startx rescue launcher missing from live image" exit 1 fi if [ ! -x "${MOUNT_POINT}/usr/local/bin/bash" ]; then echo "ERROR: bash missing from live image" exit 1 fi if [ ! -x "${MOUNT_POINT}/usr/local/bin/zsh" ]; then echo "ERROR: zsh missing from live image" exit 1 fi if ! grep -qx '/usr/local/bin/bash' "${MOUNT_POINT}/etc/shells" 2>/dev/null; then printf '%s\n' '/usr/local/bin/bash' >> "${MOUNT_POINT}/etc/shells" fi if ! grep -qx '/usr/local/bin/zsh' "${MOUNT_POINT}/etc/shells" 2>/dev/null; then printf '%s\n' '/usr/local/bin/zsh' >> "${MOUNT_POINT}/etc/shells" fi if ! /usr/sbin/pw -R "$MOUNT_POINT" usershow clawdie >/dev/null 2>&1; then /usr/sbin/pw -R "$MOUNT_POINT" useradd clawdie \ -m \ -s /usr/local/bin/bash \ -c "Clawdie Operator" fi /usr/sbin/pw -R "$MOUNT_POINT" usermod clawdie -s /usr/local/bin/bash /usr/sbin/pw -R "$MOUNT_POINT" groupmod wheel -m clawdie if /usr/sbin/pw -R "$MOUNT_POINT" groupshow video >/dev/null 2>&1; then /usr/sbin/pw -R "$MOUNT_POINT" groupmod video -m clawdie fi CLAWDIE_UID="$(/usr/sbin/pw -R "$MOUNT_POINT" usershow clawdie | cut -d: -f3)" # NOTE: chown of /opt/clawdie/npm-global was moved into # install_live_npm_globals (called later in the build). The previous # chown here ran before npm globals were installed and silently # chowned two empty directories, leaving the package tree root:wheel # at runtime. Do not re-introduce a chown here. if [ -n "${CLAWDIE_USER_PASSWORD:-}" ]; then printf '%s\n' "${CLAWDIE_USER_PASSWORD}" | /usr/sbin/pw -R "$MOUNT_POINT" usermod clawdie -h 0 fi # Do not force vendor-specific DRM firmware or KMS drivers through # loader.conf. The live image must boot Intel, AMD, VMware, NVIDIA and # fallback framebuffer hosts from the same rootfs. GPU KMS is selected at # boot by /usr/local/etc/rc.d/clawdie_live_gpu after inspecting display PCI # vendor IDs. # Live root is on USB. Avoid USB autosuspend/resume surprises while the # root filesystem is attached to the same bus. set_config_line "${MOUNT_POINT}/boot/loader.conf" 'hw.usb.no_suspend="1"' # The stock installer memstick brands the loader menu as "Installer". # This artifact is an operator live USB, so use generic FreeBSD branding # plus an explicit Clawdie live prompt. set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_brand="orb"' set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_menu_title="Clawdie Operator USB"' set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_menu_multi_user_prompt="Start Clawdie Operator USB"' # Enable FreeBSD mac_do for kernel-enforced privilege escalation. # Base system only — no sudo package needed. Wheel group members can # become root via mdo(1). mdo -u root changes the primary gid to wheel, # so the target rule must explicitly allow gid=0 as well as uid=0. set_config_line "${MOUNT_POINT}/boot/loader.conf" 'mac_do_load="YES"' set_config_line "${MOUNT_POINT}/etc/sysctl.conf" 'security.mac.do.rules=gid=0>uid=0,gid=0,+gid=*' if ! grep -q '^\[clawdie_live=' "${MOUNT_POINT}/etc/devfs.rules" 2>/dev/null; then cat >> "${MOUNT_POINT}/etc/devfs.rules" <<'EOF' [clawdie_live=10] add path 'dri' mode 0755 group video add path 'dri/*' mode 0660 group video add path 'drm/*' mode 0660 group video EOF fi set_config_line "${MOUNT_POINT}/etc/rc.conf" 'devfs_system_ruleset="clawdie_live"' _nm_autostart="${MOUNT_POINT}/usr/local/etc/xdg/autostart/networkmgr.desktop" if [ ! -f "$_nm_autostart" ]; then echo "ERROR: networkmgr.desktop missing from live image" exit 1 fi if /usr/local/sbin/pkg -r "$MOUNT_POINT" info -e sudo 2>/dev/null; then echo "ERROR: sudo is installed in the live image" exit 1 fi if [ -e "${MOUNT_POINT}/usr/local/etc/sudoers.d/networkmgr" ]; then echo "ERROR: NetworkMgr sudoers policy is still present in the live image" exit 1 fi if ! grep -q 'Exec=mdo -u root networkmgr' "$_nm_autostart"; then echo "ERROR: networkmgr.desktop does not use mdo -u root networkmgr" exit 1 fi if /usr/local/sbin/pkg -r "$MOUNT_POINT" info -d networkmgr 2>/dev/null | grep -q '^.*sudo-'; then echo "ERROR: networkmgr still depends on sudo in the live image" exit 1 fi mkdir -p "${MOUNT_POINT}/home/clawdie" "${MOUNT_POINT}/etc/profile.d" "${MOUNT_POINT}/etc/skel" cat > "${MOUNT_POINT}/etc/profile.d/clawdie.sh" <<'EOF' # Clawdie live operator environment. _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 export LANG="${LANG:-en_US.UTF-8}" export LC_ALL="${LC_ALL:-en_US.UTF-8}" 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 # Colibri daemon socket — needed for 'colibri' CLI without --socket export COLIBRI_DAEMON_SOCKET="/var/run/colibri/colibri.sock" unset _clawdie_npm_prefix unset _clawdie_base_path unset -f _clawdie_path_has 2>/dev/null || true EOF chmod 0644 "${MOUNT_POINT}/etc/profile.d/clawdie.sh" cat > "${MOUNT_POINT}/home/clawdie/.profile" <<'EOF' # Clawdie operator POSIX shell profile. [ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh EOF cat > "${MOUNT_POINT}/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 > "${MOUNT_POINT}/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 > "${MOUNT_POINT}/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 > "${MOUNT_POINT}/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 cat > "${MOUNT_POINT}/home/clawdie/.tmux.conf" <<'EOF' # Clawdie operator tmux defaults. # Pi uses modified Enter/key chords; tmux must pass CSI-u extended keys or Pi # warns that modified Enter keys may not work. set -g extended-keys on set -g extended-keys-format csi-u set -g escape-time 10 set -g mouse on set -g base-index 1 setw -g pane-base-index 1 set -g renumber-windows on EOF mkdir -p "${MOUNT_POINT}/usr/local/etc" cp "${MOUNT_POINT}/home/clawdie/.tmux.conf" "${MOUNT_POINT}/usr/local/etc/tmux.conf" cp "${MOUNT_POINT}/home/clawdie/.profile" "${MOUNT_POINT}/etc/skel/.profile" cp "${MOUNT_POINT}/home/clawdie/.bash_profile" "${MOUNT_POINT}/etc/skel/.bash_profile" cp "${MOUNT_POINT}/home/clawdie/.bashrc" "${MOUNT_POINT}/etc/skel/.bashrc" cp "${MOUNT_POINT}/home/clawdie/.zprofile" "${MOUNT_POINT}/etc/skel/.zprofile" cp "${MOUNT_POINT}/home/clawdie/.zshrc" "${MOUNT_POINT}/etc/skel/.zshrc" cp "${MOUNT_POINT}/home/clawdie/.tmux.conf" "${MOUNT_POINT}/etc/skel/.tmux.conf" chmod 0644 \ "${MOUNT_POINT}/home/clawdie/.profile" \ "${MOUNT_POINT}/home/clawdie/.bash_profile" \ "${MOUNT_POINT}/home/clawdie/.bashrc" \ "${MOUNT_POINT}/home/clawdie/.zprofile" \ "${MOUNT_POINT}/home/clawdie/.zshrc" \ "${MOUNT_POINT}/home/clawdie/.tmux.conf" \ "${MOUNT_POINT}/usr/local/etc/tmux.conf" \ "${MOUNT_POINT}/etc/skel/.profile" \ "${MOUNT_POINT}/etc/skel/.bash_profile" \ "${MOUNT_POINT}/etc/skel/.bashrc" \ "${MOUNT_POINT}/etc/skel/.zprofile" \ "${MOUNT_POINT}/etc/skel/.zshrc" \ "${MOUNT_POINT}/etc/skel/.tmux.conf" install -m 0755 "${LIVE_SESSION_DIR}/xprofile" \ "${MOUNT_POINT}/home/clawdie/.xprofile" cat > "${MOUNT_POINT}/home/clawdie/.xinitrc" <<'EOF' #!/bin/sh exec /usr/local/bin/clawdie-xfce-session EOF mkdir -p "${MOUNT_POINT}/home/clawdie/.config/xfce4" "${MOUNT_POINT}/etc/skel/.config/xfce4" cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/home/clawdie/.config/xfce4/xinitrc" cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/etc/skel/.xinitrc" cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/etc/skel/.config/xfce4/xinitrc" chmod 0755 \ "${MOUNT_POINT}/home/clawdie/.xinitrc" \ "${MOUNT_POINT}/home/clawdie/.config/xfce4/xinitrc" \ "${MOUNT_POINT}/etc/skel/.xinitrc" \ "${MOUNT_POINT}/etc/skel/.config/xfce4/xinitrc" if [ -f "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" ]; then mkdir -p "${MOUNT_POINT}/home/clawdie/.config/tumbler" "${MOUNT_POINT}/etc/skel/.config/tumbler" install -m 0644 "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" \ "${MOUNT_POINT}/home/clawdie/.config/tumbler/tumbler.rc" install -m 0644 "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" \ "${MOUNT_POINT}/etc/skel/.config/tumbler/tumbler.rc" fi chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie mkdir -p "${MOUNT_POINT}/usr/local/share/applications" install -m 0644 "${LIVE_SESSION_DIR}/clawdie-bootstrap.desktop" \ "${MOUNT_POINT}/usr/local/share/applications/Clawdie Bootstrap.desktop" install -m 0644 "${LIVE_SESSION_DIR}/hw-report.desktop" \ "${MOUNT_POINT}/usr/local/share/applications/Clawdie Hardware Report.desktop" mkdir -p "${MOUNT_POINT}/home/clawdie/Desktop" install -m 0644 "${LIVE_SESSION_DIR}/clawdie-bootstrap.desktop" \ "${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Bootstrap.desktop" install -m 0644 "${LIVE_SESSION_DIR}/hw-report.desktop" \ "${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Hardware Report.desktop" mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/bootstrap" install -m 0644 "${LIVE_SESSION_DIR}/bootstrap.html" \ "${MOUNT_POINT}/usr/local/share/clawdie-iso/bootstrap/index.html" chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/Desktop chmod 0755 "${MOUNT_POINT}/home/clawdie/Desktop" chmod 0644 \ "${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Bootstrap.desktop" \ "${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Hardware Report.desktop" install_live_ai_source_snapshots # The stock FreeBSD install memstick is intentionally read-only. This live # operator USB needs a writable root so SDDM, Xorg, NetworkMgr, logs, and # operator diagnostics can create runtime state on the flashed stick. set_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*/dev/ufs/FreeBSD_Install[[:space:]]+/[[:space:]]+ufs[[:space:]]' '/dev/ufs/FreeBSD_Install / ufs rw,noatime 1 1' if ! grep -Eq '^[[:space:]]*/dev/ufs/FreeBSD_Install[[:space:]]+/[[:space:]]+ufs[[:space:]]+([^[:space:]]*,)?rw(,|[[:space:]])' "${MOUNT_POINT}/etc/fstab"; then echo "ERROR: live USB root filesystem is not configured read-write in /etc/fstab" exit 1 fi ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*proc[[:space:]]+/proc[[:space:]]+procfs[[:space:]]' 'proc /proc procfs rw 0 0' ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*tmpfs[[:space:]]+/tmp[[:space:]]+tmpfs[[:space:]]' 'tmpfs /tmp tmpfs rw,mode=1777 0 0' ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*tmpfs[[:space:]]+/var/log[[:space:]]+tmpfs[[:space:]]' 'tmpfs /var/log tmpfs rw,mode=755 0 0' # No swap on the live USB. USB write wear, unpredictable behaviour, and # it would defeat the tmpfs work above (under memory pressure the kernel # would page tmpfs contents to swap, putting writes back on the stick). # On an 8 GB RAM baseline, "fail visibly under memory pressure" is the # right behaviour, not "silently grind the USB". Strip any inherited # swap entries and assert clean. sed -i '' '/^[[:space:]]*[^#].*[[:space:]]swap[[:space:]]/d' "${MOUNT_POINT}/etc/fstab" if grep -qE '^[[:space:]]*[^#].*[[:space:]]swap[[:space:]]' "${MOUNT_POINT}/etc/fstab"; then echo "ERROR: swap entry remains in live image /etc/fstab" exit 1 fi set_config_line "${MOUNT_POINT}/etc/rc.conf" 'root_rw_mount="YES"' # FreeBSD default dumpdev_enable="AUTO" scans for swap on boot to use as # the kernel crash-dump device. With no swap and no desire to crash-dump # to the USB anyway, set NO explicitly so the scan never runs. set_config_line "${MOUNT_POINT}/etc/rc.conf" 'dumpdev_enable="NO"' # The stock installer memstick overlays /tmp and /var with tiny tmpfs # filesystems. That hides image content such as /var/lib/xkb and leaves # too little runtime space for a graphical desktop. The operator USB has a # writable root filesystem, so keep /tmp and /var on the root UFS image. set_config_line "${MOUNT_POINT}/etc/rc.conf" 'tmpmfs="NO"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'varmfs="NO"' if ! grep -Eq '^root_rw_mount="YES"' "${MOUNT_POINT}/etc/rc.conf"; then echo "ERROR: live USB root_rw_mount is not enabled in /etc/rc.conf" exit 1 fi if ! grep -Eq '^tmpmfs="NO"' "${MOUNT_POINT}/etc/rc.conf" || ! grep -Eq '^varmfs="NO"' "${MOUNT_POINT}/etc/rc.conf"; then echo "ERROR: live USB tmpmfs/varmfs overlays are not disabled in /etc/rc.conf" exit 1 fi # Keep a real hostname so startx/xauth do not derive invalid display names # such as bare :0 or /unix:0 on the installer-derived "Amnesiac" profile. set_config_line "${MOUNT_POINT}/etc/rc.conf" 'hostname="usb.clawdie.home.arpa"' if ! grep -Eq '^hostname="usb.clawdie.home.arpa"' "${MOUNT_POINT}/etc/rc.conf"; then echo "ERROR: live USB hostname is not configured in /etc/rc.conf" exit 1 fi ensure_fstab_line "${MOUNT_POINT}/etc/hosts" '^[[:space:]]*127\.0\.0\.1[[:space:]].*clawdie-live' '127.0.0.1 clawdie-live' ensure_fstab_line "${MOUNT_POINT}/etc/hosts" '^[[:space:]]*::1[[:space:]].*clawdie-live' '::1 clawdie-live' configure_nsswitch_hosts_line "${MOUNT_POINT}/etc/nsswitch.conf" set_config_line "${MOUNT_POINT}/etc/rc.conf" 'dbus_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'sshd_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'tailscaled_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'avahi_daemon_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'sddm_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'display_manager="sddm"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="auto"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_nvidia_branch=""' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_wifi_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_seed_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_resolver_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_audio_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_power_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'webcamd_enable="YES"' # CPU power management. powerdxx is a drop-in replacement for base # powerd with moving-average load sampling and better multi-core # behavior (matters on Ryzen 7 5700U-class chips). Do NOT also set # powerd_enable — both daemons use the same pidfile and one would # fail to start. Base powerd defaults to NO when unset, so leaving # it unset is the right way to keep it off. # The bigger battery lever is the C-state pair. Use C3 rather than Cmax # on the live USB because root is USB-backed; deeper C6/C7-style states # can expose USB controller resume bugs on some Ryzen mobile systems. # Installed systems can choose Cmax once root is on internal storage. set_config_line "${MOUNT_POINT}/etc/rc.conf" 'powerdxx_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'powerdxx_flags="-a hiadaptive -b adaptive -n adaptive"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'performance_cx_lowest="C3"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'economy_cx_lowest="C3"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'linux_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'zfs_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'pf_enable="YES"' # GPU KMS is selected at boot by /usr/local/etc/rc.d/clawdie_live_gpu so # the live USB does not force the Intel driver on non-Intel hardware. # kld_list breakdown: # linux / linux64 — Linuxulator binary compatibility. # zfs — runtime ZFS support (pool import on demand). # cuse — userspace character devices; required by webcamd. # hidbus / iichid — HID-over-I²C bridge for modern touchpads / keyboards. # hms / hmt / hkbd — HID mouse / multitouch / keyboard class drivers. # acpi_video — backlight control sysctls (hw.acpi.video.lcd0.brightness). append_rc_list_values "${MOUNT_POINT}/etc/rc.conf" kld_list \ linux linux64 zfs \ cuse \ hidbus iichid hms hmt hkbd \ acpi_video case "${GPU_DRIVER:-}" in nvidia-390|nvidia-470|nvidia-590) set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="nvidia"' set_config_line "${MOUNT_POINT}/etc/rc.conf" "clawdie_live_gpu_nvidia_branch=\"${GPU_DRIVER#nvidia-}\"" append_rc_list_values "${MOUNT_POINT}/etc/rc.conf" kld_list \ nvidia-modeset nvidia ;; esac # Universal NVIDIA lane: no branch is baked; clawdie_live_gpu installs the # matching one from the on-image repo at boot. Overrides the mode set above. # kld_list intentionally does NOT preload nvidia (it is not installed yet at # first boot); the rc.d service kldloads it after install. if [ "${NVIDIA_UNIVERSAL:-NO}" = "YES" ]; then set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="nvidia-auto"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_nvidia_branch=""' fi mkdir -p "${MOUNT_POINT}/etc/ssh/sshd_config.d" install -m 0644 "${LIVE_SESSION_DIR}/sshd-live.conf" \ "${MOUNT_POINT}/etc/ssh/sshd_config.d/clawdie-live.conf" ensure_sshd_include_line "${MOUNT_POINT}/etc/ssh/sshd_config" rm -rf "${MOUNT_POINT}/home/clawdie/.ssh" if [ -n "${SSH_PUBLIC_KEY:-}" ]; then mkdir -p "${MOUNT_POINT}/home/clawdie/.ssh" chmod 0700 "${MOUNT_POINT}/home/clawdie/.ssh" printf '%s\n' "${SSH_PUBLIC_KEY}" > "${MOUNT_POINT}/home/clawdie/.ssh/authorized_keys" chmod 0600 "${MOUNT_POINT}/home/clawdie/.ssh/authorized_keys" chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/.ssh fi if grep -q '^sshd_enable="YES"' "${MOUNT_POINT}/etc/rc.conf"; then _drop_in="${MOUNT_POINT}/etc/ssh/sshd_config.d/clawdie-live.conf" if [ ! -s "$_drop_in" ]; then echo "ERROR: sshd_enable=YES but ${_drop_in#${MOUNT_POINT}} missing or empty" exit 1 fi if ! grep -qE '^PasswordAuthentication[[:space:]]+no' "$_drop_in" \ || ! grep -qE '^PermitRootLogin[[:space:]]+no' "$_drop_in" \ || ! grep -qE '^KbdInteractiveAuthentication[[:space:]]+no' "$_drop_in"; then echo "ERROR: sshd drop-in does not disable password, keyboard-interactive, and root login" exit 1 fi if ! grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "${MOUNT_POINT}/etc/ssh/sshd_config"; then echo "ERROR: sshd_config does not include /etc/ssh/sshd_config.d/*.conf" exit 1 fi fi install -m 0644 "${LIVE_SESSION_DIR}/pf-live.conf" "${MOUNT_POINT}/etc/pf.conf" if grep -q '^[^#].*\' "${MOUNT_POINT}/etc/pf.conf"; then echo "ERROR: live PF ruleset unexpectedly enables logging" exit 1 fi install -m 0755 "${LIVE_SESSION_DIR}/clawdie-tailscale-up" \ "${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_tailscale_up" rm -f "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey" if [ -n "${TAILSCALE_AUTHKEY:-}" ]; then mkdir -p "${MOUNT_POINT}/var/lib/clawdie-iso" printf '%s\n' "${TAILSCALE_AUTHKEY}" > "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey" chmod 0600 "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey" set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_tailscale_up_enable="YES"' else set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_tailscale_up_enable="NO"' fi # Keep ZFS tools/modules ready on the live USB, but do not let a copied or # stale zpool cache auto-import disks from the machine being rescued. The # operator can still import pools explicitly with mdo when needed. mkdir -p "${MOUNT_POINT}/etc/zfs" "${MOUNT_POINT}/boot/zfs" rm -f "${MOUNT_POINT}/etc/zfs/zpool.cache" "${MOUNT_POINT}/boot/zfs/zpool.cache" # Xorg's XKB compiler must be able to chdir into the XKB root and write a # compiled keymap cache. The stock memstick/rootless-X path is unforgiving: # if /tmp, /var/tmp, /var/lib/xkb, or any XKB parent directory is missing or # not traversable, xkbcomp reports a misleading missing keycodes file. mkdir -p "${MOUNT_POINT}/tmp" "${MOUNT_POINT}/var/tmp" "${MOUNT_POINT}/var/lib/xkb" chmod 1777 "${MOUNT_POINT}/tmp" "${MOUNT_POINT}/var/tmp" "${MOUNT_POINT}/var/lib/xkb" mkdir -p "${MOUNT_POINT}/var/run/user" "${MOUNT_POINT}/var/run/user/${CLAWDIE_UID}" chroot "${MOUNT_POINT}" chown clawdie:clawdie "/var/run/user/${CLAWDIE_UID}" chmod 0755 "${MOUNT_POINT}/var/run/user" chmod 0700 "${MOUNT_POINT}/var/run/user/${CLAWDIE_UID}" for _xkb_dir in \ "${MOUNT_POINT}/usr" \ "${MOUNT_POINT}/usr/local" \ "${MOUNT_POINT}/usr/local/share" \ "${MOUNT_POINT}/usr/local/share/X11" \ "${MOUNT_POINT}/usr/local/share/X11/xkb" \ "${MOUNT_POINT}/usr/local/share/X11/xkb/keycodes" \ "${MOUNT_POINT}/usr/local/share/X11/xkb/rules" \ "${MOUNT_POINT}/usr/local/share/X11/xkb/symbols" \ "${MOUNT_POINT}/usr/local/share/X11/xkb/types" \ "${MOUNT_POINT}/usr/local/share/X11/xkb/compat"; do if [ ! -d "$_xkb_dir" ]; then echo "ERROR: XKB directory missing from live image: ${_xkb_dir#${MOUNT_POINT}}" exit 1 fi chmod 0755 "$_xkb_dir" done if [ ! -f "${MOUNT_POINT}/usr/local/share/X11/xkb/keycodes/xfree86" ]; then echo "ERROR: XKB keycodes missing from live image" exit 1 fi if [ ! -x "${MOUNT_POINT}/usr/local/bin/xkbcomp" ]; then echo "ERROR: xkbcomp missing from live image" exit 1 fi _xkb_test="${CACHE_DIR}/xkb-default-test.xkb" _xkb_out="${CACHE_DIR}/xkb-default-test.xkm" cat > "$_xkb_test" <<'EOF' xkb_keymap { xkb_keycodes { include "xfree86+aliases(qwerty)" }; xkb_types { include "complete" }; xkb_compat { include "complete" }; xkb_symbols { include "pc+us+inet(evdev)" }; xkb_geometry { include "pc(pc105)" }; }; EOF if ! "${MOUNT_POINT}/usr/local/bin/xkbcomp" -w 0 -R"${MOUNT_POINT}/usr/local/share/X11/xkb" "$_xkb_test" "$_xkb_out" >/dev/null 2>&1; then echo "ERROR: XKB default keymap does not compile against the live image xkeyboard-config" exit 1 fi rm -f "$_xkb_test" "$_xkb_out" _xkb_si_test="${CACHE_DIR}/xkb-si-test.xkb" _xkb_si_out="${CACHE_DIR}/xkb-si-test.xkm" cat > "$_xkb_si_test" <<'EOF' xkb_keymap { xkb_keycodes { include "xfree86+aliases(qwerty)" }; xkb_types { include "complete" }; xkb_compat { include "complete" }; xkb_symbols { include "pc+si+inet(evdev)" }; xkb_geometry { include "pc(pc105)" }; }; EOF if ! "${MOUNT_POINT}/usr/local/bin/xkbcomp" -w 0 -R"${MOUNT_POINT}/usr/local/share/X11/xkb" "$_xkb_si_test" "$_xkb_si_out" >/dev/null 2>&1; then echo "ERROR: Slovenian (si) XKB keymap does not compile — missing xkeyboard-config layout?" exit 1 fi rm -f "$_xkb_si_test" "$_xkb_si_out" # Preseed XFCE panel layout into /etc/skel so the clawdie user gets it # on first login. pw useradd -m copies /etc/skel into /home/clawdie, but # the user was already created above, so copy directly into both locations. PANEL_SKEL="${LIVE_SESSION_DIR}/panel-skel" if [ -d "$PANEL_SKEL" ]; then echo " Seeding XFCE panel layout..." mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/panel-skel" cp -R "${PANEL_SKEL}/." "${MOUNT_POINT}/usr/local/share/clawdie-iso/panel-skel/" mkdir -p "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml" cp "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml" \ "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml" for _xfconf_xml in xsettings.xml xfwm4.xml xfce4-desktop.xml xfce4-power-manager.xml displays.xml; do if [ -f "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}" ]; then cp "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}" \ "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}" fi done # Install into /etc/skel for any future users _skel_etc="${MOUNT_POINT}/etc/skel" mkdir -p "${_skel_etc}/.config" cp -R "${PANEL_SKEL}/.config/." "${_skel_etc}/.config/" if [ -d "${PANEL_SKEL}/.local/share/applications" ]; then mkdir -p "${_skel_etc}/.local/share/applications" cp "${PANEL_SKEL}/.local/share/applications/mimeapps.list" \ "${_skel_etc}/.local/share/applications/" 2>/dev/null || true fi # Also copy directly into clawdie's home (user already exists) mkdir -p "${MOUNT_POINT}/home/clawdie/.config" cp -R "${PANEL_SKEL}/.config/." "${MOUNT_POINT}/home/clawdie/.config/" if [ -d "${PANEL_SKEL}/.local/share/applications" ]; then mkdir -p "${MOUNT_POINT}/home/clawdie/.local/share/applications" cp "${PANEL_SKEL}/.local/share/applications/mimeapps.list" \ "${MOUNT_POINT}/home/clawdie/.local/share/applications/" 2>/dev/null || true fi chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/.config chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/.local chroot "$MOUNT_POINT" chown -R root:wheel /etc/skel fi _wallpapers_dir="${LIVE_SESSION_DIR}/wallpapers" if [ -d "$_wallpapers_dir" ]; then echo " Installing wallpapers..." mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/wallpapers" cp "${_wallpapers_dir}"/* "${MOUNT_POINT}/usr/local/share/clawdie-iso/wallpapers/" 2>/dev/null || true fi # Brand icons (e.g. the Whisker Start-button triangle). Install both the # raw files for direct diagnostics and hicolor theme names for XFCE plugins # that do not reliably load absolute icon paths. _icons_dir="${LIVE_SESSION_DIR}/icons" if [ -d "$_icons_dir" ]; then echo " Installing brand icons..." mkdir -p \ "${MOUNT_POINT}/usr/local/share/clawdie-iso/icons" \ "${MOUNT_POINT}/usr/local/share/icons/hicolor/48x48/apps" \ "${MOUNT_POINT}/usr/local/share/icons/hicolor/64x64/apps" \ "${MOUNT_POINT}/usr/local/share/icons/hicolor/scalable/apps" cp "${_icons_dir}"/* "${MOUNT_POINT}/usr/local/share/clawdie-iso/icons/" 2>/dev/null || true if [ -f "${_icons_dir}/clawdie-start-48.png" ]; then install -m 0644 "${_icons_dir}/clawdie-start-48.png" \ "${MOUNT_POINT}/usr/local/share/icons/hicolor/48x48/apps/clawdie-start.png" fi if [ -f "${_icons_dir}/clawdie-start.png" ]; then install -m 0644 "${_icons_dir}/clawdie-start.png" \ "${MOUNT_POINT}/usr/local/share/icons/hicolor/64x64/apps/clawdie-start.png" fi if [ -f "${_icons_dir}/clawdie-start.svg" ]; then install -m 0644 "${_icons_dir}/clawdie-start.svg" \ "${MOUNT_POINT}/usr/local/share/icons/hicolor/scalable/apps/clawdie-start.svg" fi if [ -x "${MOUNT_POINT}/usr/local/bin/gtk-update-icon-cache" ]; then run_live_chroot /usr/local/bin/gtk-update-icon-cache -f -t /usr/local/share/icons/hicolor || true fi fi rm -rf "${MOUNT_POINT}/home/clawdie/.cache" "${MOUNT_POINT}/etc/skel/.cache" ln -s /tmp/clawdie/cache "${MOUNT_POINT}/home/clawdie/.cache" ln -s /tmp/clawdie/cache "${MOUNT_POINT}/etc/skel/.cache" chroot "${MOUNT_POINT}" chown -h clawdie:clawdie /home/clawdie/.cache } preflight_colibri_artifacts preflight_zot_artifacts # --- step 1: fetch FreeBSD memstick --- MEMSTICK="${CACHE_DIR}/FreeBSD-${FREEBSD_VERSION}-${FREEBSD_ARCH}-memstick.img" if [ "$SKIP_MEMSTICK_FETCH" -eq 1 ]; then if [ ! -f "$MEMSTICK" ]; then echo "ERROR: cached FreeBSD memstick not found: ${MEMSTICK}" echo "Rerun without --skip-memstick-fetch to download it." exit 1 fi if ! verify_memstick_cache; then echo "ERROR: cached FreeBSD memstick failed checksum verification." echo "Remove ${MEMSTICK} and rerun without --skip-memstick-fetch." exit 1 fi echo "==> [1/7] FreeBSD memstick cached; fetch skipped." elif [ "$SKIP_FETCH" -eq 0 ] || [ ! -f "$MEMSTICK" ]; then echo "==> [1/7] Fetching FreeBSD memstick..." mkdir -p "$CACHE_DIR" curl -L --progress-bar -o "$MEMSTICK" "$FREEBSD_MEMSTICK_URL" curl -L -o "${MEMSTICK}.SHA256" "$FREEBSD_MEMSTICK_SHA256_URL" verify_memstick_cache || { echo "ERROR: checksum mismatch on memstick"; exit 1; } else if ! verify_memstick_cache; then echo "ERROR: cached FreeBSD memstick failed checksum verification." echo "Remove ${MEMSTICK} and rerun without --skip-fetch." exit 1 fi echo "==> [1/7] FreeBSD memstick cached." fi # --- step 2: fetch all packages (no root needed) --- if [ "$SKIP_FETCH" -eq 0 ]; then echo "==> [2/7] Fetching packages to tmp/packages/..." mkdir -p "$PKG_REPO_DIR" # Set pkg repo to configured branch before fetching # Use temporary user-level config to avoid requiring root PKG_CONFIG_DIR=$(mktemp -d "${TMP_DIR}/pkg-repo.XXXXXX") trap "rm -rf $PKG_CONFIG_DIR" EXIT # Avoid calling pkg config (needs privilege) — hardcode ABI and use cache ABI="FreeBSD:15:amd64" PKG_CACHE_HOME="${HOME}/.pkg-cache" mkdir -p "$PKG_CONFIG_DIR" "$PKG_CACHE_HOME" cat > "$PKG_CONFIG_DIR/FreeBSD.conf" <"$_PKG_FETCH_ERROR"; then # If privilege error, offer to re-run with sudo if grep -q "Insufficient privileges" "$_PKG_FETCH_ERROR" 2>/dev/null; then echo " ⚠️ pkg fetch requires elevated privileges." echo " Re-running with sudo..." sudo sh -c "export PKG_REPOS_DIR='$PKG_CONFIG_DIR' PKG_CACHEDIR='$PKG_CACHE_HOME'; echo '$PKGS' | xargs pkg fetch --yes --dependencies --output '$PKG_REPO_DIR'" || { echo " ERROR: sudo pkg fetch failed" exit 1 } else cat "$_PKG_FETCH_ERROR" exit 1 fi fi echo " Fetch complete." # Fetch pinned npm-global CLI tarballs for the live image bundle. echo "==> [2b/7] Fetching npm-global CLI tarballs..." OUT_DIR="$NPM_GLOBALS_DIR" "${SCRIPT_DIR}/scripts/fetch-npm-globals.sh" || { echo " ERROR: fetch-npm-globals.sh failed" exit 1 } else echo "==> [2/7] Skipping package fetch." fi if [ -d "${PKG_REPO_DIR}/Hashed" ]; then mkdir -p "${PKG_REPO_DIR}/All/Hashed" find "${PKG_REPO_DIR}/Hashed" -maxdepth 1 -type f -name '*.pkg' -exec mv {} "${PKG_REPO_DIR}/All/Hashed/" \; rmdir "${PKG_REPO_DIR}/Hashed" 2>/dev/null || true fi if [ -d "${PKG_REPO_DIR}/All/Hashed" ]; then override_networkmgr_package fi # --- step 3: generate offline pkg repo metadata --- echo "==> [3/7] Generating offline pkg repo metadata..." if [ -d "$PKG_REPO_DIR/All" ]; then # pkg repo may also need privilege — try unprivileged first _PKG_REPO_ERROR="${TMP_DIR}/pkg-repo-error.log" if ! pkg repo "$PKG_REPO_DIR" 2>"$_PKG_REPO_ERROR"; then if grep -q "Insufficient privileges" "$_PKG_REPO_ERROR" 2>/dev/null; then echo " ⚠️ pkg repo requires elevated privileges. Re-running with sudo..." sudo pkg repo "$PKG_REPO_DIR" || { echo " ERROR: sudo pkg repo failed" exit 1 } else cat "$_PKG_REPO_ERROR" exit 1 fi fi echo " Repo metadata written to tmp/packages/" else echo " WARN: tmp/packages/All/ not found — run without --skip-fetch first" fi # --- step 4: fetch + prepare Clawdie-AI tarball (offline-ready) --- # Resolve "latest" from Forgejo releases first, then tags. if [ "${CLAWDIE_REF:-${CLAWDIE_VERSION:-}}" = "latest" ] || [ -z "${CLAWDIE_REF:-}" ]; then echo "==> [4/7] Resolving latest Clawdie-AI version..." CLAWDIE_VERSION=$(resolve_latest_clawdie_tag | sed 's/^v//') if [ -z "$CLAWDIE_VERSION" ]; then echo "ERROR: could not resolve latest Clawdie-AI release/tag from Forgejo." echo " Pin --clawdie-ref main or --clawdie-version X.Y.Z explicitly." exit 1 fi CLAWDIE_REF="v${CLAWDIE_VERSION}" echo " Resolved: ${CLAWDIE_REF}" fi CLAWDIE_AI_COMMIT=$(resolve_clawdie_commit "$CLAWDIE_REF" | head -n 1) CLAWDIE_AI_COMMIT="${CLAWDIE_AI_COMMIT:-unknown}" echo " Clawdie commit: ${CLAWDIE_AI_COMMIT}" if [ "$CLAWDIE_AI_COMMIT" = "unknown" ] && [ "$SKIP_FETCH" -eq 1 ] && ! is_pinned_clawdie_ref "$CLAWDIE_REF"; then echo "ERROR: cannot safely use --skip-fetch for moving Clawdie-AI ref '${CLAWDIE_REF}' without resolving its commit." echo " Run without --skip-fetch or pin --clawdie-version / --clawdie-ref to a commit." exit 1 fi if [ "$CLAWDIE_AI_COMMIT" != "unknown" ]; then CLAWDIE_ARCHIVE_REF="$CLAWDIE_AI_COMMIT" CLAWDIE_CACHE_KEY="$CLAWDIE_AI_COMMIT" else CLAWDIE_ARCHIVE_REF="$CLAWDIE_REF" CLAWDIE_CACHE_KEY=$(printf '%s' "$CLAWDIE_REF" | tr -c 'A-Za-z0-9._-' '_') fi CLAWDIE_TARBALL="${CACHE_DIR}/clawdie-ai-${CLAWDIE_CACHE_KEY}.tar.gz" CLAWDIE_TARBALL_URL="https://code.smilepowered.org/clawdie/clawdie-ai/archive/${CLAWDIE_ARCHIVE_REF}.tar.gz" if [ "$SKIP_FETCH" -eq 0 ] || [ ! -f "$CLAWDIE_TARBALL" ]; then echo "==> [4/7] Fetching Clawdie-AI ${CLAWDIE_REF} (${CLAWDIE_ARCHIVE_REF})..." mkdir -p "$CACHE_DIR" curl -L --progress-bar -o "$CLAWDIE_TARBALL" "$CLAWDIE_TARBALL_URL" else echo "==> [4/7] Clawdie-AI ${CLAWDIE_REF} (${CLAWDIE_ARCHIVE_REF}) cached." fi # Build an ISO-ready tarball that includes node_modules for offline firstboot. CLAWDIE_TARBALL_ISO="${CACHE_DIR}/clawdie-ai-${CLAWDIE_CACHE_KEY}-iso.tar.gz" if [ "$SKIP_FETCH" -eq 0 ] || [ ! -f "$CLAWDIE_TARBALL_ISO" ]; then echo "==> [4/7] Preparing Clawdie-AI offline tarball (node_modules)..." if ! command -v npm >/dev/null 2>&1; then echo "ERROR: npm not found on build host." echo "Install Node.js + npm on the ISO build machine, then rerun build.sh." echo "Example (FreeBSD): sudo pkg install -y node24 npm-node24" exit 1 fi BUILD_AI_DIR="${CACHE_DIR}/clawdie-ai-build" STAGE_AI_DIR="${CACHE_DIR}/clawdie-ai-stage" rm -rf "$BUILD_AI_DIR" rm -rf "$STAGE_AI_DIR" mkdir -p "$BUILD_AI_DIR" mkdir -p "$STAGE_AI_DIR" tar -xzf "$CLAWDIE_TARBALL" -C "$BUILD_AI_DIR" AI_SRC_DIR="$(find "$BUILD_AI_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)" if [ -z "${AI_SRC_DIR:-}" ]; then echo "ERROR: Could not locate extracted Clawdie-AI directory." exit 1 fi _bundle_script="${CACHE_DIR}/bundle-clawdie-ai.sh" cat > "$_bundle_script" <<'EOF' #!/bin/sh set -eu cd "$BUILD_AI_SRC_DIR" # Never let package-manager lifecycle hooks run the Clawdie installer on # the ISO build host. Older Clawdie-AI refs used an npm "install" script # as an operator shortcut; remove only that root lifecycle hook in the # staged copy while still allowing dependency install scripts. node -e ' const fs = require("fs"); const path = "package.json"; const pkg = JSON.parse(fs.readFileSync(path, "utf8")); if (pkg.scripts?.install) delete pkg.scripts.install; fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); ' if ! npm ci --no-audit --no-fund --legacy-peer-deps; then echo " WARN: npm ci failed for Clawdie-AI; falling back to npm install." echo " WARN: This usually means the published release tarball is out of sync with package-lock.json." npm install --no-audit --no-fund --legacy-peer-deps fi EOF chmod 700 "$_bundle_script" if [ -n "${BUILD_HOST_USER}" ]; then chown -R "$BUILD_HOST_USER":"$BUILD_HOST_USER" "$BUILD_AI_DIR" "$STAGE_AI_DIR" "$_bundle_script" 2>/dev/null || true su -m "$BUILD_HOST_USER" -c "env HOME='${BUILD_HOST_HOME}' PATH='${PATH}' BUILD_AI_SRC_DIR='${AI_SRC_DIR}' sh '${_bundle_script}'" else env BUILD_AI_SRC_DIR="${AI_SRC_DIR}" sh "$_bundle_script" fi mv "$AI_SRC_DIR" "${STAGE_AI_DIR}/Clawdie-AI" tar -czf "$CLAWDIE_TARBALL_ISO" -C "$STAGE_AI_DIR" Clawdie-AI else echo "==> [4/7] Clawdie-AI offline tarball cached." fi # Exit here if --fetch-only (CI package pre-fetch step, no root required) if [ "$FETCH_ONLY" -eq 1 ]; then echo "" echo "==> Fetch complete. Run ./build.sh --skip-fetch to assemble ISO." exit 0 fi # --- step 5: prepare working image (requires root) --- echo "==> [5/7] Preparing working image (${IMAGE_SIZE} for offline packages)..." if [ "$(id -u)" -ne 0 ]; then echo "ERROR: steps 5-7 require root (mdconfig/mount)" exit 1 fi WORK_IMG="${CACHE_DIR}/work.img" # Recreate the working image on every assembly. Package/profile changes must not # inherit stale packages or desktop files from a previous branch build. rm -f "$WORK_IMG" # Create image with configured size for offline packages if [ ! -f "$WORK_IMG" ]; then echo " Creating ${IMAGE_SIZE} image..." truncate -s "${IMAGE_SIZE}" "$WORK_IMG" # Attach to mdconfig and partition MD=$(mdconfig -a -t vnode -f "$WORK_IMG") echo " Attached as /dev/${MD}" # Initialize an MBR layout compatible with modern UEFI removable boot: # s1: EFI System Partition (FAT, contains /EFI/BOOT/BOOTX64.EFI) # s2: FreeBSD slice with BSD label + UFS root # s3: CLAWDIESEED FAT32 partition for operator seed files (SSH keys, # future hostname / Tailscale / Wi-Fi overrides). Lives at the # tail of the disk so Linux pre-flash tooling can mount and edit # it without touching the UFS root. Imported on every boot by # /usr/local/etc/rc.d/clawdie_live_seed. _seed_size_m=64 _efi_size_m=64 _image_size_m=$(( $(stat -f %z "$WORK_IMG") / 1024 / 1024 )) # 4 MiB headroom covers MBR overhead + alignment rounding. Without this # the trailing 'gpart add -s ${_seed_size_m}M' can fail when the EFI # slice rounds up past the requested 64M. _freebsd_size_m=$(( _image_size_m - _efi_size_m - _seed_size_m - 4 )) gpart create -s MBR /dev/${MD} gpart add -t efi -s ${_efi_size_m}M /dev/${MD} gpart add -t freebsd -s ${_freebsd_size_m}M /dev/${MD} gpart add -t '!12' -s ${_seed_size_m}M /dev/${MD} # !12 = FAT32 LBA (0x0c) gpart set -a active -i 2 /dev/${MD} # Create BSD label (partition a) in the FreeBSD slice gpart create -s BSD /dev/${MD}s2 gpart add -t freebsd-ufs /dev/${MD}s2 # Create UFS filesystem on partition a. Keep the label expected by the # stock FreeBSD memstick loader configuration. newfs -U -L FreeBSD_Install /dev/${MD}s2a # Format and seed the CLAWDIESEED partition. Operators mount this on # Linux (sudo mount -t vfat /dev/sdX3 /mnt/...) pre-flash to drop # authorized_keys. The README.txt explains the allowlisted contract. # # -c 1 forces 1 sector/cluster (512 B). FAT32 requires >=65525 clusters # by spec; at default cluster size (4 KiB+) a 64 MiB partition falls # well below that and newfs_msdos rejects it ("too few clusters for # FAT32, need 65525"). 512 B clusters give us 131072 clusters with a # ~512 KiB FAT table — <1% overhead, invisible for the few-KiB text # files this partition is meant to hold. newfs_msdos -F 32 -c 1 -L CLAWDIESEED /dev/${MD}s3 _seed_mount="${CACHE_DIR}/seed-mnt" mkdir -p "${_seed_mount}" mount_msdosfs /dev/${MD}s3 "${_seed_mount}" install -m 0644 "${LIVE_SESSION_DIR}/clawdie-live-seed.README.txt" \ "${_seed_mount}/README.txt" sync umount "${_seed_mount}" rmdir "${_seed_mount}" 2>/dev/null || true # Mount the new filesystem MOUNT_POINT="${CACHE_DIR}/mnt" mkdir -p "$MOUNT_POINT" mount /dev/${MD}s2a "$MOUNT_POINT" echo " Mounted /dev/${MD}s2a at ${MOUNT_POINT}" # Mount memstick read-only to extract base system MEMSTICK_MNT="${CACHE_DIR}/memstick-src" mkdir -p "$MEMSTICK_MNT" mount_memstick_rootfs "$MEMSTICK_MNT" echo " Copying base system from memstick..." # Copy all files from memstick to new image (excluding package cache) ( cd "$MEMSTICK_MNT" pax -rw -pe . "$MOUNT_POINT" ) # Cleanup memstick mount cleanup_memstick_rootfs "$MEMSTICK_MNT" rm -rf "$MEMSTICK_MNT" install_image_bootcode "$MD" "$MOUNT_POINT" "2" install_image_uefi_bootcode "$MD" "$MOUNT_POINT" "1" # Store MD device for later cleanup echo "$MD" > "${CACHE_DIR}/.md_device" else # Reattach existing image (but extract base system if empty) MD=$(mdconfig -a -t vnode -f "$WORK_IMG") echo " Reattached as /dev/${MD}" MOUNT_POINT="${CACHE_DIR}/mnt" mkdir -p "$MOUNT_POINT" if [ -e "/dev/${MD}s2a" ]; then ROOT_PART="/dev/${MD}s2a" ROOT_SLICE_INDEX="2" else ROOT_PART="/dev/${MD}s1a" ROOT_SLICE_INDEX="1" fi mount "$ROOT_PART" "$MOUNT_POINT" echo " Mounted ${ROOT_PART} at ${MOUNT_POINT}" # Check if base system is missing and extract if needed if [ ! -d "$MOUNT_POINT/etc" ]; then echo " Extracting base system from memstick..." MEMSTICK_MNT="${CACHE_DIR}/memstick-src" mkdir -p "$MEMSTICK_MNT" mount_memstick_rootfs "$MEMSTICK_MNT" ( cd "$MEMSTICK_MNT" pax -rw -pe . "$MOUNT_POINT" ) cleanup_memstick_rootfs "$MEMSTICK_MNT" rm -rf "$MEMSTICK_MNT" install_image_bootcode "$MD" "$MOUNT_POINT" "$ROOT_SLICE_INDEX" if [ "$ROOT_SLICE_INDEX" = "2" ]; then install_image_uefi_bootcode "$MD" "$MOUNT_POINT" "1" fi fi # Store MD device for later cleanup echo "$MD" > "${CACHE_DIR}/.md_device" fi # --- step 6: inject payload --- echo "==> [6/7] Injecting payload..." # Create share directory on USB USB_SHARE="${MOUNT_POINT}/usr/local/share/clawdie-iso" mkdir -p "$USB_SHARE" # Step 1 switches the live boot entrypoint away from auto-running # /etc/installerconfig. Keep the script in USB_SHARE for later explicit # bsdinstall invocation, but remove any legacy boot-time auto script. rm -f "${MOUNT_POINT}/etc/installerconfig" install_live_runtime_packages configure_live_operator_session install_colibri_service install_zot_agent install_nvidia_universal_repo # Copy payload # Rebuild payload paths from scratch inside the reusable work image. A failed # prior build can leave a partial package tree behind, and overlaying a new # hashed pkg repo onto that stale path produces "Not a directory" copy errors. rm -rf "${USB_SHARE}/firstboot" "${USB_SHARE}/packages" "${USB_SHARE}/npm-globals" rm -f "${USB_SHARE}/installerconfig" \ "${USB_SHARE}/clawdie-ai.tar.gz" \ "${USB_SHARE}/build.cfg" \ "${USB_SHARE}/build-manifest.json" cp "${SCRIPT_DIR}/installerconfig" "${USB_SHARE}/installerconfig" mkdir -p "${USB_SHARE}/firstboot" ( cd "${SCRIPT_DIR}/firstboot" pax -rw -pe . "${USB_SHARE}/firstboot" ) mkdir -p "${USB_SHARE}/packages" ( cd "${PKG_REPO_DIR}" pax -rw -pe . "${USB_SHARE}/packages" ) if [ -d "$NPM_GLOBALS_DIR" ]; then mkdir -p "${USB_SHARE}/npm-globals" ( cd "${NPM_GLOBALS_DIR}" pax -rw -pe . "${USB_SHARE}/npm-globals" ) NPM_GLOBAL_TARBALL_COUNT=$(find "${NPM_GLOBALS_DIR}" -maxdepth 1 -type f -name '*.tgz' | wc -l | tr -d ' ') echo " Bundled npm-globals: ${NPM_GLOBAL_TARBALL_COUNT} tarballs" fi install_live_npm_globals cp "${CLAWDIE_TARBALL_ISO}" "${USB_SHARE}/clawdie-ai.tar.gz" cp "${SCRIPT_DIR}/build.cfg" "${USB_SHARE}/" write_build_manifest "${USB_SHARE}/build-manifest.json" # Bake runtime vars so firstboot reads the right target config { echo "ISO_VERSION=\"${ISO_VERSION}\"" echo "BUILD_CHANNEL=\"${BUILD_CHANNEL}\"" echo "CLAWDIE_VERSION=\"${CLAWDIE_VERSION}\"" echo "CLAWDIE_REF=\"${CLAWDIE_REF}\"" echo "CLAWDIE_AI_COMMIT=\"${CLAWDIE_AI_COMMIT}\"" echo "TARGET=\"${TARGET:-baremetal}\"" [ -n "${GPU_DRIVER:-}" ] && echo "GPU_DRIVER=\"${GPU_DRIVER}\"" [ -n "${ASSISTANT_NAME:-}" ] && echo "ASSISTANT_NAME=\"${ASSISTANT_NAME}\"" [ -n "${AGENT_GENDER:-}" ] && echo "AGENT_GENDER=\"${AGENT_GENDER}\"" [ -n "${AGENT_DOMAIN:-}" ] && echo "AGENT_DOMAIN=\"${AGENT_DOMAIN}\"" [ -n "${TZ:-}" ] && echo "TZ=\"${TZ}\"" [ -n "${PI_TUI_PROVIDER:-}" ] && echo "PI_TUI_PROVIDER=\"${PI_TUI_PROVIDER}\"" [ -n "${PI_TUI_MODEL:-}" ] && echo "PI_TUI_MODEL=\"${PI_TUI_MODEL}\"" [ -n "${ZAI_API_KEY:-}" ] && echo "ZAI_API_KEY=\"${ZAI_API_KEY}\"" [ -n "${OPENROUTER_API_KEY:-}" ] && echo "OPENROUTER_API_KEY=\"${OPENROUTER_API_KEY}\"" [ -n "${ANTHROPIC_API_KEY:-}" ] && echo "ANTHROPIC_API_KEY=\"${ANTHROPIC_API_KEY}\"" [ -n "${EMBED_BASE_URL:-}" ] && echo "EMBED_BASE_URL=\"${EMBED_BASE_URL}\"" [ -n "${EMBED_MODEL:-}" ] && echo "EMBED_MODEL=\"${EMBED_MODEL}\"" [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && echo "TELEGRAM_BOT_TOKEN=\"${TELEGRAM_BOT_TOKEN}\"" [ -n "${TELEGRAM_ADMIN_IDS:-}" ] && echo "TELEGRAM_ADMIN_IDS=\"${TELEGRAM_ADMIN_IDS}\"" [ -n "${FEATURE_TELEGRAM:-}" ] && echo "FEATURE_TELEGRAM=\"${FEATURE_TELEGRAM}\"" [ -n "${FEATURE_COLIBRI:-}" ] && echo "FEATURE_COLIBRI=\"${FEATURE_COLIBRI}\"" [ -n "${COLIBRI_DAEMON_ENABLE:-}" ] && echo "COLIBRI_DAEMON_ENABLE=\"${COLIBRI_DAEMON_ENABLE}\"" [ -n "${COLIBRI_COST_MODE:-}" ] && echo "COLIBRI_COST_MODE=\"${COLIBRI_COST_MODE}\"" [ -n "${SSH_PUBLIC_KEY:-}" ] && echo "SSH_PUBLIC_KEY=\"${SSH_PUBLIC_KEY}\"" [ -n "${ROOT_PASSWORD:-}" ] && echo "ROOT_PASSWORD=\"${ROOT_PASSWORD}\"" [ -n "${CLAWDIE_USER_PASSWORD:-}" ] && echo "CLAWDIE_USER_PASSWORD=\"${CLAWDIE_USER_PASSWORD}\"" } >> "${USB_SHARE}/build.cfg" echo " Payload injected." # Unmount and detach umount "$MOUNT_POINT" if [ -f "${CACHE_DIR}/.md_device" ]; then MD=$(cat "${CACHE_DIR}/.md_device") mdconfig -d -u "$MD" rm "${CACHE_DIR}/.md_device" fi # --- step 7: write output --- echo "==> [7/7] Writing output image..." mkdir -p "$OUTPUT_DIR" cp "$WORK_IMG" "${OUTPUT_DIR}/${IMAGE_NAME}" sync echo "" OUTPUT_IMAGE="${OUTPUT_DIR}/${IMAGE_NAME}" IMAGE_LOGICAL_SIZE=$(ls -lh "$OUTPUT_IMAGE" | awk '{print $5}') IMAGE_ALLOCATED_SIZE=$(du -sh "$OUTPUT_IMAGE" | awk '{print $1}') echo " Done : ${OUTPUT_IMAGE}" echo " Image size : ${IMAGE_LOGICAL_SIZE}" echo " Allocated : ${IMAGE_ALLOCATED_SIZE} (sparse on build host)" echo "" echo " Write to USB:" echo " dd if=${OUTPUT_IMAGE} of=/dev/daX bs=1M status=progress"