diff --git a/build.sh b/build.sh index 02788d96..e08d3b30 100755 --- a/build.sh +++ b/build.sh @@ -295,6 +295,8 @@ configure_live_installer_session() { "${MOUNT_POINT}/usr/local/bin/clawdie-qml-installer" install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-installer-launch.sh" \ "${MOUNT_POINT}/usr/local/bin/clawdie-live-installer-launch.sh" + install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-commit.sh" \ + "${MOUNT_POINT}/usr/local/bin/clawdie-live-commit.sh" mkdir -p "${MOUNT_POINT}/usr/local/etc/lightdm/lightdm.conf.d" install -m 0644 "${LIVE_SESSION_DIR}/lightdm-live.conf" \ @@ -316,6 +318,15 @@ Session=lumina EOF chroot "$MOUNT_POINT" chown -R clawdie-installer:clawdie-installer /home/clawdie-installer + mkdir -p "${MOUNT_POINT}/usr/local/etc/sudoers.d" + cat > "${MOUNT_POINT}/usr/local/etc/sudoers.d/clawdie-live-installer" <<'EOF' +# Allow the live autologin user to run only the installer commit helper and +# reboot from the success screen, without a password prompt. +Defaults:clawdie-installer !requiretty +clawdie-installer ALL=(root) NOPASSWD: /usr/local/bin/clawdie-live-commit.sh, /sbin/reboot +EOF + chmod 0440 "${MOUNT_POINT}/usr/local/etc/sudoers.d/clawdie-live-installer" + set_config_line "${MOUNT_POINT}/etc/rc.conf" 'dbus_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'seatd_enable="YES"' set_config_line "${MOUNT_POINT}/etc/rc.conf" 'lightdm_enable="YES"' diff --git a/firstboot/firstboot.sh b/firstboot/firstboot.sh index 3ff4bcdc..c514168e 100644 --- a/firstboot/firstboot.sh +++ b/firstboot/firstboot.sh @@ -117,16 +117,28 @@ export SETUP_IMPORT_TEST=1 . "${SHARE}/firstboot/shell-deploy.sh" # ── Load GUI config if present ─────────────────────────────────────────────── +SETUP_HANDOFF_LOADED=0 if [ -f "/tmp/clawdie-install.conf" ]; then log_msg "[firstboot] Loading GUI installer configuration" . "/tmp/clawdie-install.conf" step_done "wizard" + SETUP_HANDOFF_LOADED=1 +elif [ -f "/var/db/clawdie-installer/clawdie-handoff.sealed" ]; then + log_msg "[firstboot] Loading installed handoff payload" + . "/var/db/clawdie-installer/clawdie-handoff.sealed" + step_done "wizard" + SETUP_HANDOFF_LOADED=1 fi log_msg "[firstboot] Starting — target: ${TARGET:-baremetal}${RESUME:+, resume mode}" # ── Import setup.txt/system.env from USB config partition ─────────────────── -run_step "setup-import" clawdie_setup_import_load "Load first-boot setup from USB partition" +if [ "$SETUP_HANDOFF_LOADED" -eq 1 ]; then + log_msg "[firstboot] Skipping setup-import (installer handoff already loaded)" + step_done "setup-import" +else + run_step "setup-import" clawdie_setup_import_load "Load first-boot setup from USB partition" +fi # ── ZFS pool detection (baremetal only) ─────────────────────────────────── # Runs early to decide boot mode: install | upgrade | maintenance diff --git a/firstboot/gui/qml-installer/main.cpp b/firstboot/gui/qml-installer/main.cpp index 518137f4..a1e9ecac 100644 --- a/firstboot/gui/qml-installer/main.cpp +++ b/firstboot/gui/qml-installer/main.cpp @@ -10,6 +10,30 @@ #include #include #include +#include + +static QString shellEscape(const QString &value) { + QString escaped = value; + escaped.replace('\\', "\\\\"); + escaped.replace('"', "\\\""); + escaped.replace('$', "\\$"); + escaped.replace('`', "\\`"); + escaped.replace('\n', " "); + escaped.replace('\r', " "); + return escaped; +} + +static void writeShellAssignment(QTextStream &out, const QString &key, const QString &value) { + out << key << "=\"" << shellEscape(value) << "\"\n"; +} + +static QString normalizeHostLabel(const QString &value) { + QString label = value.toLower(); + label.remove(QRegularExpression("[^a-z0-9-]")); + label.remove(QRegularExpression("^-+")); + label.remove(QRegularExpression("-+$")); + return label.isEmpty() ? QStringLiteral("clawdie") : label; +} // ============================================================================ // DiskModel — lists available disks for installation target selection @@ -165,6 +189,7 @@ signals: private: void parseNvidiaBusId(const QString &output) { + Q_UNUSED(output); // Recommend driver version based on device ID // For now, recommend 590 (most modern) // In production: parse device ID and map to version @@ -400,21 +425,62 @@ public: return false; if (qEnvironmentVariableIsSet("CLAWDIE_LIVE_SESSION")) { - QFile logFile("/var/log/clawdie-firstboot.log"); - if (logFile.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream log(&logFile); - log << "[live-installer] Live GUI session bootstrapped successfully.\n"; - log << "[live-installer] Commit path is not implemented in this build yet.\n"; - log << "[live-installer] Stop here and continue with Step 2/3 implementation.\n"; - logFile.close(); - } + const QString runtimeDir = QStringLiteral("/var/run/clawdie-installer"); + QDir().mkpath(runtimeDir); - QFile progressFile("/var/log/clawdie-firstboot.progress"); - if (progressFile.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream progress(&progressFile); - progress << "ERROR=live-session-bootstrap-only\n"; - progressFile.close(); + const QString hostLabel = normalizeHostLabel(m_username); + const QString handoffPath = runtimeDir + QStringLiteral("/clawdie-handoff.sealed"); + QFile handoffFile(handoffPath); + if (!handoffFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + qWarning() << "Failed to create handoff file:" << handoffFile.errorString(); + return false; } + QTextStream handoff(&handoffFile); + handoff << "# Clawdie live installer handoff\n"; + writeShellAssignment(handoff, QStringLiteral("ASSISTANT_NAME"), m_username); + writeShellAssignment(handoff, QStringLiteral("HOSTNAME"), hostLabel); + writeShellAssignment(handoff, QStringLiteral("AGENT_DOMAIN"), hostLabel + QStringLiteral(".home.arpa")); + writeShellAssignment(handoff, QStringLiteral("CLAWDIE_USER_PASSWORD"), m_password); + handoff << "\n"; + writeShellAssignment(handoff, QStringLiteral("AGENT_GENDER"), QStringLiteral("f")); + writeShellAssignment(handoff, QStringLiteral("TZ"), QStringLiteral("UTC")); + writeShellAssignment(handoff, QStringLiteral("SYSTEM_LOCALE"), QStringLiteral("sl_SI.UTF-8")); + writeShellAssignment(handoff, QStringLiteral("DISPLAY_LOCALE"), QStringLiteral("sl_SI.UTF-8")); + writeShellAssignment(handoff, QStringLiteral("ASSISTANT_LOCALE"), QStringLiteral("sl_SI.UTF-8")); + writeShellAssignment(handoff, QStringLiteral("KEYMAP"), QStringLiteral("sl.kbd")); + writeShellAssignment(handoff, QStringLiteral("FEATURE_TAILSCALE"), QStringLiteral("YES")); + writeShellAssignment(handoff, QStringLiteral("TAILSCALE_AUTHKEY"), QStringLiteral("")); + writeShellAssignment(handoff, QStringLiteral("FEATURE_GIT"), QStringLiteral("YES")); + writeShellAssignment(handoff, QStringLiteral("FEATURE_GITEA"), QStringLiteral("NO")); + writeShellAssignment(handoff, QStringLiteral("CODE_HOSTING_MODE"), QStringLiteral("git")); + handoffFile.close(); + + const QString envPath = runtimeDir + QStringLiteral("/live-install.env"); + QFile envFile(envPath); + if (!envFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + qWarning() << "Failed to create live install env:" << envFile.errorString(); + return false; + } + QTextStream env(&envFile); + env << "# Clawdie live installer environment\n"; + writeShellAssignment(env, QStringLiteral("INSTALL_DISK"), m_selectedDisk); + envFile.close(); + + QProcess *installProcess = new QProcess(this); + connect(installProcess, QOverload::of(&QProcess::finished), + this, [](int exitCode, QProcess::ExitStatus exitStatus) { + Q_UNUSED(exitStatus); + if (exitCode != 0) { + qWarning() << "Live installation process failed with exit code:" << exitCode; + } + }); + + installProcess->start(QStringLiteral("sudo"), + QStringList() << QStringLiteral("-n") + << QStringLiteral("/usr/local/bin/clawdie-live-commit.sh")); + + if (!installProcess->waitForStarted(5000)) + return false; m_installationStarted = true; emit installationStartedChanged(); @@ -430,34 +496,39 @@ public: QTextStream out(&configFile); out << "# Clawdie GUI Installer Configuration\n"; out << "# Generated by QML installer\n\n"; + const QString hostLabel = normalizeHostLabel(m_username); out << "# User settings\n"; - out << "ASSISTANT_NAME=\"" << m_username << "\"\n"; - out << "AGENT_DOMAIN=\"" << m_username.toLower() << ".home.arpa\"\n"; - out << "CLAWDIE_USER_PASSWORD=\"" << m_password << "\"\n\n"; + writeShellAssignment(out, QStringLiteral("ASSISTANT_NAME"), m_username); + writeShellAssignment(out, QStringLiteral("HOSTNAME"), hostLabel); + writeShellAssignment(out, QStringLiteral("AGENT_DOMAIN"), hostLabel + QStringLiteral(".home.arpa")); + writeShellAssignment(out, QStringLiteral("CLAWDIE_USER_PASSWORD"), m_password); + out << "\n"; out << "# Installation settings\n"; - out << "INSTALL_DISK=\"" << m_selectedDisk << "\"\n"; - out << "GPU_DRIVER_VERSION=\"" << m_gpuDriverVersion << "\"\n\n"; + writeShellAssignment(out, QStringLiteral("INSTALL_DISK"), m_selectedDisk); + writeShellAssignment(out, QStringLiteral("GPU_DRIVER_VERSION"), QString::number(m_gpuDriverVersion)); + out << "\n"; out << "# Feature flags\n"; - out << "FEATURE_DESKTOP=\"" << (m_installDesktop ? "YES" : "NO") << "\"\n"; - out << "FEATURE_DEVTOOLS=\"" << (m_installDevTools ? "YES" : "NO") << "\"\n"; - out << "FEATURE_NVIDIA=\"" << (m_installNvidia ? "YES" : "NO") << "\"\n"; - out << "LOCAL_LLM_PROVIDER=\"" << (m_installLLM ? "llama.cpp" : "none") << "\"\n\n"; + writeShellAssignment(out, QStringLiteral("FEATURE_DESKTOP"), m_installDesktop ? QStringLiteral("YES") : QStringLiteral("NO")); + writeShellAssignment(out, QStringLiteral("FEATURE_DEVTOOLS"), m_installDevTools ? QStringLiteral("YES") : QStringLiteral("NO")); + writeShellAssignment(out, QStringLiteral("FEATURE_NVIDIA"), m_installNvidia ? QStringLiteral("YES") : QStringLiteral("NO")); + writeShellAssignment(out, QStringLiteral("LOCAL_LLM_PROVIDER"), m_installLLM ? QStringLiteral("llama.cpp") : QStringLiteral("none")); + out << "\n"; out << "# Defaults from firstboot.sh\n"; - out << "AGENT_GENDER=\"f\"\n"; - out << "TZ=\"UTC\"\n"; - out << "SYSTEM_LOCALE=\"sl_SI.UTF-8\"\n"; - out << "DISPLAY_LOCALE=\"sl_SI.UTF-8\"\n"; - out << "ASSISTANT_LOCALE=\"sl_SI.UTF-8\"\n"; - out << "KEYMAP=\"sl.kbd\"\n"; - out << "FEATURE_TAILSCALE=\"YES\"\n"; - out << "TAILSCALE_AUTHKEY=\"\"\n"; - out << "FEATURE_GIT=\"YES\"\n"; - out << "FEATURE_GITEA=\"NO\"\n"; - out << "CODE_HOSTING_MODE=\"git\"\n"; + writeShellAssignment(out, QStringLiteral("AGENT_GENDER"), QStringLiteral("f")); + writeShellAssignment(out, QStringLiteral("TZ"), QStringLiteral("UTC")); + writeShellAssignment(out, QStringLiteral("SYSTEM_LOCALE"), QStringLiteral("sl_SI.UTF-8")); + writeShellAssignment(out, QStringLiteral("DISPLAY_LOCALE"), QStringLiteral("sl_SI.UTF-8")); + writeShellAssignment(out, QStringLiteral("ASSISTANT_LOCALE"), QStringLiteral("sl_SI.UTF-8")); + writeShellAssignment(out, QStringLiteral("KEYMAP"), QStringLiteral("sl.kbd")); + writeShellAssignment(out, QStringLiteral("FEATURE_TAILSCALE"), QStringLiteral("YES")); + writeShellAssignment(out, QStringLiteral("TAILSCALE_AUTHKEY"), QStringLiteral("")); + writeShellAssignment(out, QStringLiteral("FEATURE_GIT"), QStringLiteral("YES")); + writeShellAssignment(out, QStringLiteral("FEATURE_GITEA"), QStringLiteral("NO")); + writeShellAssignment(out, QStringLiteral("CODE_HOSTING_MODE"), QStringLiteral("git")); configFile.close(); @@ -474,16 +545,21 @@ public: QProcess *installProcess = new QProcess(this); connect(installProcess, QOverload::of(&QProcess::finished), - this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + this, [](int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitStatus); if (exitCode != 0) { qWarning() << "Installation process failed with exit code:" << exitCode; } }); - installProcess->start("/bin/sh", QStringList() << "-c" - << "export $(cat /tmp/clawdie-install.conf | xargs) && " - "sudo -p '[sudo] password: ' " + firstbootPath); + installProcess->start(QStringLiteral("sudo"), + QStringList() << QStringLiteral("-n") + << QStringLiteral("/bin/sh") + << QStringLiteral("-c") + << QStringLiteral(". /tmp/clawdie-install.conf; exec \"%1\"").arg(firstbootPath)); + + if (!installProcess->waitForStarted(5000)) + return false; m_installationStarted = true; emit installationStartedChanged(); diff --git a/firstboot/gui/qml-installer/pages/CompletePage.qml b/firstboot/gui/qml-installer/pages/CompletePage.qml index 6c8e32c1..10e74ea2 100644 --- a/firstboot/gui/qml-installer/pages/CompletePage.qml +++ b/firstboot/gui/qml-installer/pages/CompletePage.qml @@ -70,6 +70,23 @@ ColumnLayout { text: tracker.success ? "Finish machine bootstrap first. Provider keys, Telegram, and browser sign-in are configured after boot." : "You may retry the installation or seek support." font.pixelSize: 12 color: "#666666" + wrapMode: Text.WordWrap + } + + Text { + text: tracker.success ? "After reboot, open http://127.0.0.1:3100/setup on the Clawdie host or local console. Remote use of https:///setup is only safe after TLS and PF/reverse-proxy or tailnet/LAN allowlisting are in place." : "" + font.pixelSize: 12 + color: "#555555" + wrapMode: Text.WordWrap + visible: tracker.success + } + + Text { + text: tracker.success ? "If no setup token is shown after boot, run 'npm run setup-token -- rotate' on the installed host and use that token at /setup. Do not expose /setup directly to the public internet before setup completes." : "" + font.pixelSize: 12 + color: "#8a4f00" + wrapMode: Text.WordWrap + visible: tracker.success } Item { @@ -110,8 +127,7 @@ ColumnLayout { enabled: tracker.finished onClicked: { - // Call reboot command - backend.runCommand("reboot", []) + backend.runCommand("sudo", ["-n", "/sbin/reboot"]) } } diff --git a/firstboot/rc.d/clawdie-firstboot b/firstboot/rc.d/clawdie-firstboot index 87610ef6..eb5b1632 100644 --- a/firstboot/rc.d/clawdie-firstboot +++ b/firstboot/rc.d/clawdie-firstboot @@ -17,6 +17,7 @@ stop_cmd=":" SHARE="/usr/local/share/clawdie-iso" LOG="/var/log/clawdie-firstboot.log" +HANDOFF_FILE="/var/db/clawdie-installer/clawdie-handoff.sealed" clawdie_firstboot_start() { @@ -31,6 +32,8 @@ clawdie_firstboot_start() if [ "$RC" -eq 0 ]; then echo "Clawdie first-boot setup complete. Disabling service." sysrc -x clawdie_firstboot_enable + rm -f "$HANDOFF_FILE" + rmdir /var/db/clawdie-installer 2>/dev/null || true rm -rf "$SHARE" else echo "Clawdie first-boot setup failed (exit $RC). Check $LOG" diff --git a/installerconfig b/installerconfig index 293d34b1..3485d731 100644 --- a/installerconfig +++ b/installerconfig @@ -38,8 +38,18 @@ set_config_line() { USB_SHARE="/usr/local/share/clawdie-iso" HDD_SHARE="/mnt/usr/local/share/clawdie-iso" HDD_RCD="/mnt/usr/local/etc/rc.d" +LIVE_INSTALLER_RUNTIME_DIR="${LIVE_INSTALLER_RUNTIME_DIR:-/var/run/clawdie-installer}" +LIVE_INSTALLER_PERSIST_DIR="/mnt/var/db/clawdie-installer" +LIVE_INSTALLER_PERSIST_HANDOFF="${LIVE_INSTALLER_PERSIST_DIR}/clawdie-handoff.sealed" +LIVE_INSTALLER_PROGRESS_FILE="${LIVE_INSTALLER_PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}" + +set_progress() { + [ -n "${LIVE_INSTALLER_PROGRESS_FILE:-}" ] || return 0 + echo "PROGRESS=$1" >> "$LIVE_INSTALLER_PROGRESS_FILE" +} echo "clawdie-iso: injecting firstboot payload..." +set_progress 4 # Copy firstboot scripts mkdir -p "$HDD_SHARE" @@ -61,6 +71,14 @@ chmod +x "${HDD_SHARE}/firstboot/maintenance-mode.sh" 2>/dev/null || true mkdir -p "$HDD_RCD" cp "${USB_SHARE}/firstboot/rc.d/clawdie-firstboot" "${HDD_RCD}/clawdie-firstboot" chmod +x "${HDD_RCD}/clawdie-firstboot" +set_progress 5 + +if [ -f "${LIVE_INSTALLER_RUNTIME_DIR}/clawdie-handoff.sealed" ]; then + mkdir -p "$LIVE_INSTALLER_PERSIST_DIR" + cp "${LIVE_INSTALLER_RUNTIME_DIR}/clawdie-handoff.sealed" "$LIVE_INSTALLER_PERSIST_HANDOFF" + chmod 0600 "$LIVE_INSTALLER_PERSIST_HANDOFF" + set_progress 6 +fi # Enable mac_do framework at first HDD boot with no credential grants yet. set_config_line /mnt/boot/loader.conf 'mac_do_load="YES"' @@ -68,5 +86,6 @@ set_config_line /mnt/etc/sysctl.conf 'security.mac.do.rules=' # Enable service in rc.conf on HDD echo 'clawdie_firstboot_enable="YES"' >> /mnt/etc/rc.conf +set_progress 7 echo "clawdie-iso: firstboot payload installed. Rebooting to HDD..." diff --git a/live/installer-session/clawdie-live-commit.sh b/live/installer-session/clawdie-live-commit.sh new file mode 100644 index 00000000..8981976f --- /dev/null +++ b/live/installer-session/clawdie-live-commit.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +set -eu + +RUNTIME_DIR="${RUNTIME_DIR:-/var/run/clawdie-installer}" +LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}" +PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}" +USB_SHARE="${USB_SHARE:-/usr/local/share/clawdie-iso}" +ENV_FILE="${RUNTIME_DIR}/live-install.env" +HANDOFF_FILE="${RUNTIME_DIR}/clawdie-handoff.sealed" +INSTALLERCONFIG_FILE="${RUNTIME_DIR}/freebsd-installerconfig" +INSTALLERCONFIG_TEMPLATE="${USB_SHARE}/installerconfig" + +log_msg() { + echo "$(date '+%H:%M:%S') [live-installer] $1" | tee -a "$LOG_FILE" +} + +set_progress() { + echo "PROGRESS=$1" >> "$PROGRESS_FILE" +} + +fail() { + echo "ERROR=$1" >> "$PROGRESS_FILE" + log_msg "$2" + exit 1 +} + +mkdir -p "$RUNTIME_DIR" +: > "$LOG_FILE" +: > "$PROGRESS_FILE" + +[ -r "$ENV_FILE" ] || fail "missing-live-install-env" "Missing $ENV_FILE" +[ -r "$HANDOFF_FILE" ] || fail "missing-live-handoff" "Missing $HANDOFF_FILE" +[ -r "$INSTALLERCONFIG_TEMPLATE" ] || fail "missing-installer-template" "Missing $INSTALLERCONFIG_TEMPLATE" + +. "$ENV_FILE" + +[ -n "${INSTALL_DISK:-}" ] || fail "missing-install-disk" "INSTALL_DISK is required" +case "$INSTALL_DISK" in + *[!A-Za-z0-9._/-]*) + fail "invalid-install-disk" "INSTALL_DISK contains unsupported characters: $INSTALL_DISK" + ;; +esac + +set_progress 1 +log_msg "Preparing live install for disk ${INSTALL_DISK}" + +{ + echo 'DISTRIBUTIONS="kernel.txz base.txz"' + echo 'export nonInteractive="YES"' + echo 'export ZFSBOOT_POOL_NAME="clawdie"' + echo 'export ZFSBOOT_VDEV_TYPE="stripe"' + echo "export ZFSBOOT_DISKS=\"${INSTALL_DISK}\"" + echo 'export ZFSBOOT_FORCE_4K_SECTORS="1"' + echo "export LIVE_INSTALLER_RUNTIME_DIR=\"${RUNTIME_DIR}\"" + echo "export LIVE_INSTALLER_PROGRESS_FILE=\"${PROGRESS_FILE}\"" + awk 'found || /^#!/ { found = 1; print }' "$INSTALLERCONFIG_TEMPLATE" +} > "$INSTALLERCONFIG_FILE" +chmod 0700 "$INSTALLERCONFIG_FILE" +chmod 0600 "$HANDOFF_FILE" + +set_progress 2 +log_msg "Generated ${INSTALLERCONFIG_FILE}" + +set_progress 3 +log_msg "Starting scripted bsdinstall" + +if BSDINSTALL_LOG="$LOG_FILE" bsdinstall script "$INSTALLERCONFIG_FILE" >> "$LOG_FILE" 2>&1; then + set_progress 8 + log_msg "bsdinstall completed successfully" + exit 0 +fi + +_rc=$? +fail "bsdinstall-failed" "bsdinstall exited with status ${_rc}" diff --git a/packages/pkg-list-live-installer.txt b/packages/pkg-list-live-installer.txt index ec93fe5a..5ca4635b 100644 --- a/packages/pkg-list-live-installer.txt +++ b/packages/pkg-list-live-installer.txt @@ -1,3 +1,4 @@ # Live installer runtime — needed on the USB image itself qt6-base qt6-declarative +sudo