Add OEM install mode and factory snapshots

OEM mode installs the entire system but defers user creation to first boot,
where the runtime's omarchy-oem-setup asks for the owner and lands them in the
already-installed system — and the same machinery arms omarchy-reset-computer.

The configurator's install-mode step gains an OEM choice, which required
moving the user step after mode selection (OEM skips it entirely). Autoinstall
rigs select OEM with an `oem` marker file on the cidata drive, which also
replaces the user_credentials.json requirement.

In the orchestrator, OEM swaps the user phases for a stage_oem_state phase:
it stashes the bundled Node tarball (every install, for offline factory
resets), arms omarchy-oem-setup.service in the target, and on encrypted
installs stages the throwaway LUKS passphrase — generated by InstallContext
when the input carries none — with a keyfile embedded in the UKI so boot
auto-unlocks until first-boot setup re-keys to the owner's password. The
install fails with a clear error if the bundled runtime lacks OEM support.

Every install now also ends with a read-only @factory snapshot of @ at the
btrfs top level — outside snapper's reach — which is what makes
omarchy-reset-computer a true factory reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Fable 5 committed 2026-08-07 15:41:04 -07:00
1 parent 6004e80485
commit 679edce2e3
12 files changed
+927 -108

No files matched your search

+2 -1
View File
@@ -117,4 +117,5 @@ rm -f /run/omarchy-install/state.json
--email-file /root/user_email_address.txt \
--encrypt-file /root/user_encrypt_installation.txt \
--authorized-keys-file /root/authorized_keys \
--tailscale-authkey-file /root/tailscale_authkey
--tailscale-authkey-file /root/tailscale_authkey \
--oem-file /root/oem
+105 -45
View File
@@ -136,7 +136,7 @@ Ukrainian|ua'
keyboard_form
# STEP 2: USER
# STEP 2: USER (run after the install mode is chosen — OEM installs skip it)
user_form() {
step "Let's setup your user account..."
@@ -197,11 +197,12 @@ user_form() {
fi
}
user_form
user_step() {
user_form
while true; do
# Add manual padding since gum table -p doesn't respect padding
echo -e "Field,Value
while true; do
# Add manual padding since gum table -p doesn't respect padding
echo -e "Field,Value
Username,$username
Password,$(printf "%${#password}s" | tr ' ' '*')
Full name,${full_name:-[Skipped]}
@@ -209,16 +210,17 @@ Email address,${email_address:-[Skipped]}
Hostname,$hostname
Timezone,$timezone
Keyboard,$keyboard" |
gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/"
gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/"
echo
if gum confirm --negative "No, change it" "Does this look right?"; then
break
else
keyboard_form
user_form
fi
done
echo
if gum confirm --negative "No, change it" "Does this look right?"; then
break
else
keyboard_form
user_form
fi
done
}
# STEP 3: DISK
@@ -347,11 +349,22 @@ wait_for_device() {
# Save the configurator answers the installer consumes: git identity and
# encryption choice as plain files, account details as archinstall credentials.
# OEM installs defer the user to first boot: no credentials, and on encrypted
# targets the orchestrator generates a throwaway LUKS passphrase itself.
write_user_files() {
echo "$full_name" >user_full_name.txt
echo "$email_address" >user_email_address.txt
printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt
if $oem_install; then
cat <<-_EOF_ >user_credentials.json
{
"users": []
}
_EOF_
return
fi
local password_escaped password_hash_escaped username_escaped
password_escaped=$(echo -n "$password" | jq -Rsa)
password_hash_escaped=$(echo -n "$password_hash" | jq -Rsa)
@@ -413,10 +426,10 @@ not_enough_space() {
return 1
}
run_partition_install() {
local dry=false
[[ "${1:-}" == "dry" ]] && dry=true
# Decide half of the free-space install: checks, free-space analysis, and the
# encryption confirm. Sets the globals run_partition_execute needs. Returns 1
# to fall back to the install-mode picker.
run_partition_decide() {
step "Checking for BitLocker on $disk"
local bl_part
bl_part=$(detect_bitlocker || true)
@@ -448,7 +461,8 @@ run_partition_install() {
# An unlabeled disk has no partition table for parted to scan, so we
# synthesize a whole-disk free region and mklabel gpt before mkpart.
local FREE_SPACE_INFO needs_mklabel=false pt_type disk_size_b
local FREE_SPACE_INFO pt_type disk_size_b
needs_mklabel=false
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
if [[ -z "$pt_type" ]]; then
disk_size_b=$(lsblk -bdno SIZE "$disk" 2>/dev/null)
@@ -491,7 +505,6 @@ run_partition_install() {
local EFI_SIZE_B=$((2 * gib))
local MIN_INSTALL_B=$((32 * gib)) # Minimum total Omarchy allocation
local EFI_START_B EFI_END_B ROOT_START_B ROOT_END_B
EFI_START_B=$(( (FREE_START_B + ALIGN - 1) / ALIGN * ALIGN ))
EFI_END_B=$((EFI_START_B + EFI_SIZE_B))
ROOT_START_B=$(( (EFI_END_B + 1 + ALIGN - 1) / ALIGN * ALIGN ))
@@ -554,7 +567,6 @@ run_partition_install() {
esac
done
local esp_mount_in_target
if [[ "$encrypt_installation" == "true" ]]; then
esp_mount_in_target=/boot
else
@@ -563,12 +575,11 @@ run_partition_install() {
kernel_choice=$(detect_kernel)
local last_part efi_part_num root_part_num
local last_part
last_part=$(lsblk -n -o PARTN "$disk" 2>/dev/null | grep -E '^[0-9]+$' | sort -n | tail -1)
efi_part_num=$(( ${last_part:-0} + 1 ))
root_part_num=$(( efi_part_num + 1 ))
local efi_dev root_partition_device root_mapper
efi_dev=$(partition_path "$disk" "$efi_part_num")
root_partition_device=$(partition_path "$disk" "$root_part_num")
if [[ "$encrypt_installation" == "true" ]]; then
@@ -576,6 +587,14 @@ run_partition_install() {
else
root_mapper="$root_partition_device"
fi
}
# Execute half of the free-space install: partition, format, mount, and write
# the pre-mounted (protected) install configuration. Runs after the user step
# so LUKS can be formatted with the user's password.
run_partition_execute() {
local dry=false
[[ "${1:-}" == "dry" ]] && dry=true
if $dry; then
$needs_mklabel && say "[dry] would initialize GPT on $disk"
@@ -634,10 +653,6 @@ run_partition_install() {
mount "$efi_dev" /mnt"$esp_mount_in_target"
fi
clear
write_user_files
local luks_uuid_json="null" luks_uuid
if [[ "$encrypt_installation" == "true" ]]; then
if $dry; then
@@ -716,10 +731,6 @@ run_partition_install() {
"version": "3.0.9"
}
_EOF_
if $dry; then
print_dry_run_files
fi
}
disk_form() {
@@ -771,8 +782,7 @@ open_partition_tool() {
}
# If the disk has no partition table or no unallocated free space, the only
# viable path is a full-disk install. Skip the install-mode picker and go
# straight to confirmation.
# viable paths write the whole disk, so the free-space mode is not offered.
requires_full_disk_install() {
local pt_type free_space_b
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
@@ -799,9 +809,12 @@ install_mode_form() {
# The free-space/protected path registers an EFI boot entry and is UEFI-only.
# Full-disk installs still support BIOS through the orchestrator's BIOS branch.
if [[ -d /sys/firmware/efi ]]; then
if [[ -d /sys/firmware/efi ]] && ! $full_disk_only; then
choices+=("Free space install (alongside existing data)")
fi
# OEM: install the whole system now, defer user setup to the machine's
# first boot — for machines built to be sold or handed on.
choices+=("OEM install (new owner creates their user on first boot)")
choices+=("Choose a different disk")
step "Let's select how to install Omarchy..."
@@ -846,30 +859,41 @@ confirm_disk_overwrite() {
done
}
# Decide what and how to install: sets install_target (full_disk|free_space),
# oem_install, and encrypt_installation. Nothing is written to disk yet — the
# user step (skipped for OEM) runs between this and the execution below.
select_installation() {
local full_disk_only
while true; do
if requires_full_disk_install; then
full_disk_only=true
install_mode="Full disk install"
else
full_disk_only=false
install_mode_form
fi
install_mode_form
case "$install_mode" in
"Full disk install")
oem_install=false
if confirm_disk_overwrite; then
install_target="full_disk"
return 0
fi
if $full_disk_only; then
disk_form
fi
;;
"Free space install (alongside existing data)")
if run_partition_install "$@"; then
exit 0
"OEM install"*)
oem_install=true
if confirm_disk_overwrite; then
install_target="full_disk"
return 0
fi
oem_install=false
;;
"Free space install"*)
oem_install=false
if run_partition_decide; then
install_target="free_space"
return 0
fi
;;
"Choose a different disk")
@@ -879,13 +903,41 @@ select_installation() {
done
}
# STEP 3: WHERE AND HOW TO INSTALL
oem_install=false
install_target="full_disk"
disk_form
select_installation "$@"
select_installation
# STEP 4: USER (OEM installs defer this to the machine's first boot)
if $oem_install; then
username=""
password=""
password_hash=""
full_name=""
email_address=""
hostname="omarchy"
timezone="UTC"
else
user_step
fi
clear
write_user_files
if [[ $install_target == "free_space" ]]; then
run_partition_execute "$@"
if [[ ${1:-} == "dry" ]]; then
print_dry_run_files
fi
exit 0
fi
# Setup partition layout
disk_size=$(lsblk -bdno SIZE "$disk")
mib=$((1024 * 1024))
@@ -902,14 +954,21 @@ main_partition_size=$((disk_size_in_mib - main_partition_start - gpt_backup_rese
kernel_choice=$(detect_kernel)
if [[ $encrypt_installation == true ]]; then
# OEM installs carry no password: the orchestrator generates a throwaway
# LUKS passphrase and stages it for the first-boot re-key.
if $oem_install; then
encryption_password_line=""
else
encryption_password_line=",
\"encryption_password\": $(echo -n "$password" | jq -Rsa)"
fi
disk_encryption_config=$(cat <<-_EOF_
,
"disk_encryption": {
"encryption_type": "luks",
"lvm_volumes": [],
"iter_time": 2000,
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ],
"encryption_password": $(echo -n "$password" | jq -Rsa)
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ]$encryption_password_line
}
_EOF_
)
@@ -927,6 +986,7 @@ cat <<-_EOF_ >user_configuration.json
"custom_commands": [],
"omarchy_install": {
"mode": "full_disk",
"oem": $oem_install,
"target_mount": "/mnt",
"boot": {
"esp_mount": "/boot",