Wire live installer commit path (Sam & Codex)

Live GUI installs now write runtime handoff files under /var/run/clawdie-installer, invoke bsdinstall script through a dedicated commit helper, persist the installed handoff for first HDD boot, and point the operator at /setup after reboot.

The live autologin user is restricted to a narrow sudoers rule for the commit helper and reboot only.

Build: pass
Tests: pass — sh -n + QML build + config-format + stubbed live-commit dry-run
Real-disk / bhyve install: NOT YET TESTED
This commit is contained in:
Sam & Claude 2026-05-12 14:34:42 +02:00 committed by 123kupola
parent 3a9954f9ec
commit 835074ab8d
8 changed files with 253 additions and 40 deletions

View file

@ -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"'

View file

@ -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

View file

@ -10,6 +10,30 @@
#include <QTextStream>
#include <QRegularExpression>
#include <QVariant>
#include <QDir>
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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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();

View file

@ -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://<agent-domain>/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"])
}
}

View file

@ -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"

View file

@ -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..."

View file

@ -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}"

View file

@ -1,3 +1,4 @@
# Live installer runtime — needed on the USB image itself
qt6-base
qt6-declarative
sudo