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

@@ -11,6 +11,9 @@
# Anything less than the pair means this isn't an autoinstall drive, and the
# caller falls back to the wizard.
#
# An `oem` marker file stands in for user_credentials.json: OEM installs defer
# user creation to first boot, so imaging rigs ship no credentials at all.
#
# The optional argument prefixes every path the script touches; tests use it to
# run against a sandbox instead of the live system.
@@ -36,9 +39,9 @@ mkdir -p "$mnt"
mount -o ro "$device" "$mnt" || exit 1
loaded=1
if [[ -f $mnt/user_configuration.json && -f $mnt/user_credentials.json ]]; then
files=("$mnt"/user_configuration.json "$mnt"/user_credentials.json)
for file in user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey; do
if [[ -f $mnt/user_configuration.json ]] && [[ -f $mnt/user_credentials.json || -f $mnt/oem ]]; then
files=("$mnt"/user_configuration.json)
for file in user_credentials.json oem user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey; do
[[ -f $mnt/$file ]] && files+=("$mnt/$file")
done
if cp "${files[@]}" "$dest"/; then
@@ -321,11 +321,13 @@ install_progress() {
"Installing Arch + Omarchy") lo=65 hi=700 tau=150 pkg_signal=1 ;;
"Configuring hibernation") lo=700 hi=720 tau=10 ;;
"Configuring system") lo=720 hi=850 tau=120 ;;
"Finalizing Limine boot") lo=850 hi=950 tau=90 ;;
"Staging OEM state") lo=850 hi=855 tau=5 ;;
"Finalizing Limine boot") lo=855 hi=950 tau=90 ;;
"Finalizing user") lo=950 hi=978 tau=30 ;;
"Configuring login") lo=978 hi=988 tau=5 ;;
"Configuring DNS resolver") lo=988 hi=994 tau=5 ;;
"Validating boot setup") lo=994 hi=1000 tau=5 ;;
"Validating boot setup") lo=994 hi=998 tau=5 ;;
"Creating factory snapshot") lo=998 hi=1000 tau=5 ;;
*)
# Unknown names used to inherit a 1->99 sweep. Derive an even band from
# the published index instead; with no index, a bounded slice, so a
@@ -18,6 +18,7 @@ while [[ $# -gt 0 ]]; do
--encrypt-file) export OMARCHY_INSTALL_ENCRYPT_FILE="$2"; shift 2 ;;
--authorized-keys-file) export OMARCHY_INSTALL_AUTHORIZED_KEYS_FILE="$2"; shift 2 ;;
--tailscale-authkey-file) export OMARCHY_INSTALL_TAILSCALE_AUTHKEY_FILE="$2"; shift 2 ;;
--oem-file) export OMARCHY_INSTALL_OEM_FILE="$2"; shift 2 ;;
*) echo "omarchy-iso-install: unknown arg $1" >&2; exit 2 ;;
esac
done