Skip to content

Kids mode: ask who the computer is for and install the child profile - #146

Draft
peterholko wants to merge 6 commits into
omacom:quattrofrom
peterholko:kids/child-profile
Draft

Kids mode: ask who the computer is for and install the child profile#146
peterholko wants to merge 6 commits into
omacom:quattrofrom
peterholko:kids/child-profile

Conversation

@peterholko

Copy link
Copy Markdown

Kids mode: ask who the computer is for and install the child profile

The installer half of omacom/omarchy's kids/child-profile branch. That side adds the child profile, omarchy-parent, and the parent password; this side gets the answers and hands them over.

What changes

  • First question. The configurator asks "Who is this computer for? Me / Child / Another owner" before anything else, through the shared setup form's omarchy_prompt_computer_for.
  • Child installs take two passwords. The kid password (login, unlock, disk) and the parent password (root's password; sudo and system prompts ask for it), with the parent password required to differ. The summary masks both.
  • Handoff to the orchestrator. Credentials carry the parent hash as root_enc_password with sudo: false, plus parent_encryption_password; the context records profile, omarchy-apply-system gets --profile child, the child package list from install/omarchy-child.packages joins the runtime package list, and an add_parent_disk_key phase adds the parent's LUKS slot.
  • Offline mirror. build-iso.sh vendors the child package list and tolerates it being comment-only (it is, for now).
  • Test harness. omarchy-iso-test --child drives a child install with GUEST_PARENT_PASSWORD alongside the kid password; test/unit/test_child_profile.py covers the orchestrator wiring.

"Me" and "Another owner" keep today's flow exactly.

What the installer shows on a child install

Every screen keeps the logo at the top; Esc goes back one screen, Ctrl+C aborts. "Me" is today's flow, and "Another owner" asks only the disk questions and runs the same form at first boot.

  1. Greeter: "Beautiful, Fun & Agentic Linux by DHH" and "Press Return to Start Install".
  2. The first question (new, before the keyboard so arrow keys work under any layout):
    Let's setup your machine...
    Who is this computer for?
    > Me
      Child
      Another owner
    
  3. Keyboard: "Let's setup your keyboard..." and the layout list, unchanged.
  4. User account, child version:
    Let's setup your user account...
    Username>       Alphanumeric without spaces (like dhh)
    Kid password>   Used for login, unlocking, and disk encryption when enabled
    Confirm>        Must match the password you just typed
    
  5. Parent password (new screen, child installs only):
    Let's set the parent password...
    sudo, updates, installs, and system prompts ask for this one; keep it from the kid.
    
    Parent password>  Asked by sudo, updates, and installs (keep it from the kid)
    Confirm>          Must match the parent password you just typed
    
    Re-asked on "The parent password can't be blank!", "Parent passwords didn't match!", and "The parent password must differ from the kid password".
  6. Identity, hostname, timezone: unchanged.
  7. Confirmation table, with a "Kid password" row and a new "Parent password" row, both masked at a fixed eight characters so no length shows; "No, change it" returns to the keyboard screen.
  8. Disk: unchanged. Both passwords open the disk at boot afterwards, and the first login is the kid's.

At first boot, the deferred ("Another owner") form shows the same two password screens when the operator chose Child, with one extra line on the parent screen while the disk is re-keyed: "It also unlocks the disk, so a parent can always get in."

Testing

  • test/unit/test_child_profile.py passes.
  • Built with omarchy-iso-make --local-source against the omarchy branch and installed on a ThinkPad: the first question shows, both passwords are taken, both unlock the disk, the kid password logs in and sudo asks for the parent password.
  • omarchy-iso-test --child still to be run on a Linux host with KVM.

Depends on omacom/omarchy kids/child-profile (the --profile flag on omarchy-apply-system and the child package list); merge that side first.

🤖 Generated with Claude Code

The configurator asks "Who is this computer for? Me / Child / Another
owner" before the keyboard. Child takes a kid password and a parent
password through the shared setup form, masks both in the summary, and
hands the orchestrator the parent hash as root_enc_password with
sudo: false plus a parent_encryption_password for a second LUKS slot;
the context records the profile, omarchy-apply-system gets
--profile child, install/omarchy-child.packages joins the runtime package
list, and an add_parent_disk_key phase adds the parent's key. The builder
vendors the child package list, tolerating one that holds only comments,
and omarchy-iso-test --child drives a child install with
GUEST_PARENT_PASSWORD. "Me" and "Another owner" keep today's flow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed by Claude Opus 5 and, independently, by Codex at xhigh reasoning. Noting it is a draft — this is early feedback, not a request to change anything before you are ready.

Credential handling is genuinely careful

Worth saying first, because it is the part that would have been easy to get wrong and is not. Neither password is ever a command-line argument; hashing and LUKS formatting go over stdin, and luksAddKey gets 0600 key-file paths. The key files are created mode 0600 on tmpfs and unlinked in a finally. Nothing is exported into the environment, the cryptsetup calls do not go through a shell so nothing reaches a history, the debug dump helpers are not pointed at either JSON, state.json carries no credentials, the confirmation masks are fixed-width so no length leaks, and the deferred path strips the parent passphrase with a test covering it.

The one that would bite hardest

build-iso.sh:189-202 gates on setup-form.sh existing, but not on it carrying the new prompts. Build against any runtime published before omarchy#9750 and the guard passes — then computer_for_form calls an undefined function, bash returns 127, and configurator:219-226 falls through to abort. Since the new question is asked unconditionally before the keyboard, every interactive install dies on the first screen, "Me" and "Another owner" included. That turns a merge-ordering mistake into an ISO that cannot install anything.

The file already has exactly the right pattern one check above — the existence test prints an actionable error and exits 1. Extending it to grep the vendored form for omarchy_prompt_computer_for would make the ordering impossible to get wrong. Codex noted the autoinstall variant: cidata skips the prompt and instead fails at omarchy-apply-system --profile, after the disk is partitioned.

Two credential invariants nothing here enforces

Both are reachable through autoinstall rather than the interactive form, so they are hardening rather than live bugs — but context.py already validates the profile name and is the natural home for both.

  • A child install removes the sudo fallback while the root-password result stays unchecked. installer.set_user_password(root) at phases_impl.py:312-313 discards its return. That was harmless while every user had sudo: true; with "sudo": false a silently failed root password leaves a machine with no sudo-capable account and no root password, recoverable only from external media.
  • Nothing rejects parent == kid. If they match, cryptsetup open --test-passphrase --key-file <parent> succeeds against the kid's existing slot, and phases_impl.py:808-812 reads that as "the parent password already unlocks the disk" and returns without adding one. The install completes with a single shared credential — the kid knowing the root password — and looks clean. Your form rejects equality, but that check lives in 9750.

Smaller

  • echo -n corrupts option-shaped passwords (configurator:485,498). I reproduced it: -n, -e, -neE and -nnnnnnnn all serialize to "" through echo -n … | jq -Rsa, while printf '%s' — which you already use two lines away for hashing — is correct. The hash survives, the plaintext LUKS passphrase in the JSON does not, and the install fails after the disk is formatted. Line 485 is pre-existing, so this probably wants one repo-wide echo -nprintf '%s' change rather than a patch here.
  • --child is missing from the base-image cache key (bin/omarchy-iso-test:113-115, which has -encrypted and -provision). --child --reuse-base would run against a default-profile disk and report green without touching this path — worth fixing before you run the --child scenario you have listed as outstanding.
  • No way back from "Child". Esc on the keyboard screen re-asks the keyboard, and Esc in the account form returns to the keyboard, so a mis-selected Child can only be undone by aborting. Deliberate, but it is the one screen where a wrong answer changes everything downstream.
  • context.py:113-114 writes archinstall-user_configuration.json with no chmod, two lines after creds_path.chmod(0o600). On an encrypted install it holds the kid's plaintext passphrase at a umask-dependent mode. Pre-existing and low exposure on a root-only live ISO, but the inconsistency is odd.

On what the profile restricts

I read omarchy#9750 to check whether the restriction is real rather than cosmetic, and on the administrative axis it is real: Defaults rootpw makes sudo authenticate against root's password (the parent's), plus a polkit admin rule naming root, removal from wheel, and closed text consoles — with the account's own grant ordered before it leaves wheel, so there is no window without a working sudo path. That is a sound design and this PR's root_enc_password handoff is what makes it work.

Two things to be plain about rather than to change: it restricts administration, not access — the kid keeps a full shell and terminal, and this repo's own harness asserts Super+Return opens a foot window on every run including --child. And since the child necessarily holds a LUKS key, a capable child with physical access can boot other media, unlock the disk, and edit /etc/shadow; with no Secure Boot enforced anywhere, kids mode is a guard rail against a curious child rather than a boundary against a determined one. Both seem worth stating in the docs so the feature does not read as more than it is.

Tests

./test/all passed on a disposable worker VM: all shell unit tests plus 75 Python tests, including your new test_child_profile.py. I verified the echo -n behaviour on that worker too. I did not run omarchy-iso-test --child (needs a built ISO and KVM), and did not reproduce the ThinkPad install. A skip is not a pass.

Next

Waiting on you, and on the merge order: omarchy#9750 needs to land and reach a published runtime before this, or the ISO built from it cannot install. Nothing was pushed to your branch — the main finding is a cross-repo guard, two more are pre-existing patterns better fixed repo-wide, and whether kids mode belongs in the installer is the maintainer's call rather than mine.

peterholko and others added 5 commits September 2, 2026 11:17
The configurator printed "Let's set the parent password..." as a screen
header and then asked for the name, email, hostname, and timezone under
it. A fresh header follows the parent password now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Child installs close the text consoles, so the harness's console login
for the SSH bootstrap, and the tty3 check after the disk unlock, never
came back on a --child run. A child run now does what a parent would:
it types the parent password at the disk and at SDDM, opens the
terminal with Super+Return, runs the bootstrap there, and checks that
SSH comes up as the kid account, which is the proof that the parent
password opens the kid's session; later boots log in the same way. The
kid's own password is still typed at the installer and refused by sudo
in the in-guest suite.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both answers only feed git identity and the compose shortcuts, and on a
child install it is a parent at the keyboard, so whatever they typed
would have landed in the kid's git config as the parent's identity. The
child user step now goes from the parent password to the hostname, the
summary drops the two rows, and the harness's child run types neither.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The child branch returned 0 whenever OCR did not read "passphrase", and
ocr_screen returns nothing at all when the screendump is empty or the magick
conversion fails -- so a failed read was indistinguishable from an opened disk
and --child --encrypt reported green 30 seconds in, without the disk having
opened. Its `continue` also skipped the waited >= 300 check, so a passphrase
that never unlocks typed forever instead of failing. Both proved against the
extracted function with stubbed helpers: blank OCR returned 0, and a screen
still showing the prompt did not terminate.

Requiring OCR to have said something keeps the author's signal while closing
the blank-read case, and folding the console branch into an else lets the
existing timeout cover both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
QMP rejects "escape": against a live socket, esc returns {"return": {}} and
escape returns "Parameter 'data' does not accept value 'escape'". qmp() ends in
`|| true` with stderr discarded, so the retry never cleared the screen and
failed silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@omarchybot

Copy link
Copy Markdown
Collaborator

Re-reviewed at 09a068d, by Claude Opus 5 with an independent second opinion from Codex at xhigh reasoning. Only what is new since the last comment is below.

Line numbers below are at 385a0c3, the head after the two commits I pushed.

Two defects in the new --child harness path, both pushed

Both are in 9ceee41, and both would have misled the omarchy-iso-test --child run your description still lists as outstanding — which is why I fixed them rather than only reporting them. I proved each one against the extracted function on a disposable VM with the helpers stubbed, before and after.

unlock_luks could report the disk open without it having opened, and could never stop. The child branch returned 0 whenever OCR did not read the word "passphrase", and ocr_screen returns nothing at all when the screendump is empty or the magick conversion fails (bin/omarchy-iso-test:272-279) — so a failed read was indistinguishable from an opened disk. The comment three lines above the function already says Plymouth's prompt "isn't reliably OCR-able", which is what makes this the normal case rather than the unlucky one. The continue also skipped the waited >= 300 check, so a passphrase that genuinely never unlocks typed forever instead of failing. Against the pre-fix function: blank OCR returned rc=0, and a screen still showing the prompt did not terminate in 5s of collapsed sleeps. After the fix both return 1 and a readable screen without the prompt still returns 0. 59dc92c requires OCR to have said something and folds the console branch into an else so the existing timeout covers both.

press escape is not a QEMU qcode. I checked it against a live QMP socket: esc returns {"return": {}}, escape returns Parameter 'data' does not accept value 'escape'. Because qmp() ends in || true with stderr discarded it was a silent no-op, so attempts 2 and 3 of bootstrap_ssh_desktop never cleared the screen. 385a0c3 sends esc, which line 466 already uses. Codex found this one independently too.

Three more in the same area I did not fix, because the right signal is your call

  • Nothing establishes that a LUKS prompt was ever there. An accidentally unencrypted install also satisfies "no passphrase on screen" and walks on to a working desktop, so --child --encrypt can go green for a machine whose disk is not encrypted. My fix closes the blank-read case, not this one; it needs a positive precondition rather than a negative check. Codex's finding, and it is a mechanism I had not reasoned about.
  • The kid's LUKS key is never exercised on a child run. unlock_luks and establish_session both type GUEST_LOGIN_PASSWORD, which is the parent password on --child (bin/omarchy-iso-test:87-91). So the run proves the parent password opens the disk and never that the kid's original slot still does — which is exactly the thing add_parent_disk_key could get wrong. Also Codex's.
  • Related to both: on the unencrypted child path, bootstrap_ssh_desktop does not assert SDDM is actually present before typing (:1044). If such an install wrongly autologged in, the password would be ignored, the terminal would still open, whoami would still say the kid, and the run would report "the parent password opened the kid's session" without any authentication having happened.

The five findings from last time are all still open, untouched

The build-time gate in build-iso.sh, the discarded set_user_password result, the missing parent != kid invariant, echo -n on option-shaped passwords, and --child missing from BASE_NAME. Nothing in the three new commits touches any of them.

One correction to what I said about the second of those, which I got wrong by looking only at this repository. Once omarchy#9750 is in place, a silently failed root password does not leave a machine with no admin path: apply_posture calls require_root_password first, that fails when passwd -S root is not P, install/config/parent.sh runs under run_logged which propagates the exit code, and omarchy-apply-system dies, so run_system_finalizer raises and the install aborts. It fails closed. The unchecked return is still worth closing at the source — the install dies late with "root has no usable password" rather than where the password failed — but it is not the unrecoverable case I described.

What I checked hardest this pass, and found clean

No password reaches /var/log/omarchy-install.log, which matters because that log is bind-mounted into the target (phases_impl.py:1158) and is world-readable on the installed machine. Every exception on the new path carries field names, device names and key-file paths: the RuntimeError at phases_impl.py:799-802, the one at :779, and CalledProcessError from luksAddKey, whose argv is two file paths and a device. traceback.print_exc() does not print locals. --test-passphrase captures its output. _debug_dump_file is defined and never called. Archinstall applies both hashes through chpasswd --encrypted on stdin. The only place either plaintext passphrase is printed is print_dry_run_files, reachable only as ./configurator dry, which the live ISO never invokes and which installs nothing.

sudo: false is really enforced, by archinstall rather than by anything here. In archinstall 4.4, _create_user adds -G wheel only under if user.sudo, and calls enable_sudo only under if user.sudo; users.py:208 parses it as entry.get('sudo', False) is True. Nothing in this repository touches wheel, sudoers or polkit on any path. Worth knowing that user.groups is applied with gpasswd -a regardless of the sudo flag, so a hand-written cidata file carrying "profile": "child" with groups: ["wheel"] would produce a privileged child; context.py validates the profile name and nothing else. That is Codex's finding and it is the same gap as the parent != kid one — one credential invariant, two symptoms.

add_parent_disk_key fails in the safe direction. Any exception becomes a PhaseError at phases.py:61 and the install stops; configure_hibernation, run_system_finalizer and the boot phases never run. Nothing in the phase can remove a key, so the bad outcomes are "aborted install, kid key intact", never "neither password works" and never a completed install that only the kid can open. The one silent case remains parent == kid, already reported.

A plain "Me" install is very nearly, but not exactly, unchanged. I reproduced both heredocs on a VM with child_install=false: user_credentials.json gains one blank line where the empty parent field sits, both files parse, and the parsed JSON is byte-identical after jq -S. user_configuration.json gains "profile": "default". But the finalizer command gains --profile default unconditionally at phases_impl.py:1216, on every install including "Me" and deferred ones, and today's bin/omarchy-apply-system on quattro ends its argument loop with *) echo "Unknown option: $1" >&2; usage >&2; exit 1. So on a pre-9750 runtime the cidata autoinstall path — which skips the configurator and therefore skips the first-screen failure — dies in "Configuring system", after the disk is partitioned and the packages are installed. That is the same merge-ordering hazard as last time, arriving by a second door. And one capability is gone rather than moved: Ctrl+C on the keyboard screen now aborts instead of offering deferred provisioning, and after choosing "Me" there is no route back to that first question.

The child package list is inert, and correctly so. install/omarchy-child.packages in 9750 is comment-only by its own admission, so a child install today installs zero child-specific packages; the profile's actual effect comes entirely from omarchy-parent apply. The || true on the grep is right and necessary — under set -e inside that process substitution a grep selecting nothing would end the group and silently drop every list after it. No defect on the build side; Codex agrees and independence is not currently guaranteed.

One thing worth weighing against #96, which caps cryptsetup's Argon2id memory because the default ~1 GiB gets OOM-killed on memory-constrained live boots: luksAddKey runs Argon2id twice more (once to open an existing slot, once to derive the new one), on the live ISO, after the packages are installed. If #96's failure is real then a child encrypted install reintroduces it at a much more expensive point in the run.

Tests

./test/all on a disposable worker VM, before and after my commits: all shell unit tests plus 75 Python tests including test_child_profile.py, passing. The QMP qcode check and both unlock_luks mutations ran on that VM too. I did not run omarchy-iso-test --child — it needs a built ISO and KVM, and it is beyond one worker's budget. A skip is not a pass, and with the two defects above it would not have been trustworthy anyway.

Waiting on

You, and still on the merge order: omarchy#9750 has to land and reach a published runtime before this. It is no longer a draft, which is progress; this one still is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants