gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

fix(instalador): la cuenta de quien va a usar la maquina se pregunta antes de instalar

Una takana instalada no dejaba entrar ni administrar a nadie. Medido en metal el 14-oct:
`sergio:!` + `root:!` + ningun `/etc/doas.conf`, y `su` contestando «must be suid to work
properly». Son cuatro huecos distintos del camino de instalacion, no uno:

1. NADIE PREGUNTABA POR UNA CUENTA. `takana-install-partition` (el camino de --keep/--adopt,
   el mas comun) no pregunta nada: los usuarios salen de lib/usuarios-del-home.sh, que los
   cosecha del /home conservado con `!` en shadow — correcto de su parte, inventar una
   contrasena seria peor — y nadie, despues, la pedia. Y `takana-live-install` solo preguntaba
   por root, o sea una imagen de escritorio donde el greeter no puede autenticar a nadie.
   => scripts/lib/cuenta.sh, compartida por los dos instaladores (son DOS caminos y UNA
   pregunta, el mismo argumento que escribe usuarios-del-home.sh). Usuario, contrasena
   SHA-512, hogar, grupos video/input, `permit persist` y /etc/profile. Idempotente: a un
   usuario ya cosechado solo le pone su contrasena, SIN tocarle el uid — si se lo cambia, sus
   archivos dejan de ser suyos. Root queda cerrado si alguien puede escalar, y se pide
   obligatorio si no: una maquina sin ninguna de las dos no se administra.

2. NINGUN PERFIL DECLARABA EL BIT DE PRIVILEGIO de busybox, y once applets instalados lo
   necesitan: su, login, passwd, mount, umount, ping, ping6, traceroute, traceroute6, crontab
   y vlock. El encabezado de takana-core/src/setuid.rs lo habia previsto textualmente («hoy es
   LATENTE: el instalador solo pone contrasena de root, asi que no hay un humano no-root que
   lo choque») y el 14-oct lo hubo. El build trae CONFIG_FEATURE_SUID (medido con strings: la
   cadena «must be suid to work properly» solo existe si esta compilada), asi que los applets
   que no lo piden sueltan el privilegio en check_suid(). Metadato => el hash queda SELLADO,
   cero reforja de las 24 recetas que declaran busybox build-dep.

3. LA CARD QUE ARRANCA EL ESCRITORIO no nacia de ninguna receta: existia porque un script de
   parche la inyectaba en la semilla a mano, o sea que una instalacion de cero arranca el
   sistema y no arranca el escritorio. [[service]] en mirada-compositor (seatd + mirada, con
   backoff de arje en vez de un `sh -c` que contaba intentos), y el PATH de la Card incluye
   /usr/local/bin — sin eso el operador tenia claude instalado y la consola contestaba
   `claude: not found`. Los bin del hogar NO van en la Card: los agrega el compositor para
   quien inicia sesion, y clavarlos apuntaria al hogar de UNA persona en una imagen que
   instala cualquiera.

4. TRECE PAQUETES que el escritorio necesita no los declaraba ningun perfil, y vivian en una
   variable EXTRA de un script de otro repo: dejavu-fonts (sin un .ttf el compositor no pinta
   un solo rotulo, y se calla), fontconfig/freetype/libpng (su cadena), alsa-lib (pata-llimphi
   pide libasound: sin ella el riel no arranca), xkeyboard-config (la Card exporta
   XKB_CONFIG_ROOT a un directorio vacio), ncurses/bzip2, foot (ninguna terminal) y shuma-*.
   Es la misma fuga que cerraron las tres -shared: mirada-usb.sh cierra las librerias copiando
   del devfs de Alpine del lab, asi que el medio arranca y el hueco no se ve hasta que la
   imagen vive sola.

Y de paso: los generadores del instalador copiaban UNA de las CUATRO librerias que
takana-install-partition sourcea. Ahora se derivan del propio script — una lista a mano ya
habia quedado corta tres veces, y una lib que no viaja no da un error claro: falla al
llamarla, ya con el disco en la mano.

Certificado: install-tui-test.sh, 6/6 verdes, con dos casos nuevos y un control nuevo —
la cuenta completa con cryptpw de verdad (hash SHA-512, nada en claro, permit persist 640,
grupos video+input), el usuario cosechado (uid 1047 intacto, sin duplicar, /home sin tocar)
y que /etc/profile no lleve el hogar de nadie escrito, porque esto se instala en la maquina
de otro. Mas `takana setuid --apply` y `takana service-cards` sobre este mismo arbol, y
`takana hash --check` antes y despues: identico.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

0f66c9973305b15da33e86b6f5bb2eb2bab9f3ad · por sergio · (hace 7 d) · padre 8fe27e882 · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

10 archivos · +650 −3

diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index 368a8849..aca1e766 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -409,6 +409,28 @@ paquetes = [
   "pata-notify", "pata-notify-panel", "pata-notify-triage",
   # el instalador: las dos caras que ya tienen receta, y la de ventana que la espera
   "churay-tui", "churay-metal", "churay",
+
+  # ── LO QUE EL ESCRITORIO NECESITA Y NADIE DECLARABA (2026-10-15) ──────────────────────────
+  # Salieron de armar el escritorio del operador a mano y CONTAR lo que quedaba adentro, uno por
+  # uno. Hasta hoy vivian en la variable `EXTRA` de `tawasuyu:scripts/armar-escritorio-de-takana.sh`
+  # ⇒ el escritorio que el operador usa no lo reproducia una instalacion de cero. Es la MISMA fuga
+  # que cerraron las tres `-shared` de arriba: `mirada-usb.sh` cierra las librerias copiando del
+  # devfs de Alpine DEL LAB, asi que el medio arranca y el hueco no se ve hasta que la imagen vive
+  # sola. El precio medido en metal el 14-oct: un escritorio que no pinta un solo rotulo, sin riel
+  # y sin ninguna terminal, en una maquina donde claude ya estaba instalado.
+  "dejavu-fonts",          # el compositor NO embute fuentes: sin un .ttf no pinta un solo rotulo, y se calla
+  "fontconfig-shared",     # /etc/fonts/fonts.conf y el soname que enlaza el riel
+  "freetype-shared",       # libfontconfig.so.1 pide libfreetype.so.6: sin ella el riel muere al cargar
+  "libpng-shared",         # y freetype pide libpng16.so.16 — la cadena entera entro por querer letras
+  "alsa-lib",              # pata-llimphi pide libasound.so.2: sin ella EL RIEL NO ARRANCA
+  "xkeyboard-config",      # la Card exporta XKB_CONFIG_ROOT a /usr/share/X11/xkb, que estaba VACIO
+  "ncurses-shared",        # soname de los de arriba y de la TUI
+  "bzip2-shared",          # idem
+  "foot",                  # NINGUNA terminal en una maquina donde ya esta instalado claude
+  "shuma-tui",             # el terminal de la suite: corre DENTRO de foot, no lo reemplaza
+  "shuma-daemon",          # su cadena
+  "shuma-gateway",         # su cadena
+  "shuma-pregunta",        # su cadena
 ]
 
 [perfil.escritorio-kde]
diff --git a/recipes/busybox.toml b/recipes/busybox.toml
index 1cd00007..6c594af3 100644
--- a/recipes/busybox.toml
+++ b/recipes/busybox.toml
@@ -86,3 +86,37 @@ configure = "make CC='zig cc -mcpu=baseline' HOSTCC='zig cc -mcpu=baseline' defc
 compile   = "make -j\"$(nproc)\" CC='zig cc -mcpu=baseline' HOSTCC='zig cc -mcpu=baseline'"
 # busybox instala en CONFIG_PREFIX (no DESTDIR): bin/busybox + symlinks de cada applet.
 install   = "make CC='zig cc -mcpu=baseline' HOSTCC='zig cc -mcpu=baseline' CONFIG_PREFIX=/out install"
+
+# ── EL BIT DE PRIVILEGIO (SDD 28 §5.11, `[[setuid]]`) ─────────────────────────────────────────────
+# Medido en metal el 2026-10-15, en la máquina del operador: `su` contesta
+#
+#     su: must be suid to work properly
+#
+# y era **la única escalada que le quedaba** (root tenía la cuenta cerrada y `sudo`/`doas` no
+# estaban proyectados). El encabezado de `crates/takana-core/src/setuid.rs` lo había previsto
+# textualmente —«hoy es LATENTE: el instalador sólo pone contraseña de root, así que no hay un
+# humano no-root que lo choque»— y ése fue el día que lo hubo.
+#
+# **ONCE applets instalados dependen de este bit** (contados en el rootfs instalado, todos symlink a
+# `bin/busybox`): `su` `login` `passwd` `mount` `umount` `ping` `ping6` `traceroute` `traceroute6`
+# `crontab` `vlock`. Sin él fallan los once, y ninguno nombra el privilegio al fallar: `ping 8.8.8.8`
+# sale 1 al instante y se lee como «no hay red».
+#
+# **Por qué el bit es seguro en ESTE build, y no es una opinión:** el binario trae
+# `CONFIG_FEATURE_SUID` (medido con `strings -a`: la cadena «must be suid to work properly» sólo
+# existe si está compilada, y es la que el operador vio). Con esa opción busybox **suelta el
+# privilegio en `check_suid()`** para todo applet que no lo declara `BB_SUID_REQUIRE` ⇒ `ls`, `sh` y
+# `cat` corren con el uid real aunque el multi-call sea setuid. También trae
+# `CONFIG_FEATURE_SUID_CONFIG` (la cadena `busybox.conf` está en el binario), así que un
+# `/etc/busybox.conf` 0600 root:root puede acotar la lista applet por applet el día que se quiera —
+# si el fichero no existe, manda la tabla compilada, que es este comportamiento.
+#
+# ⚠ Lo que NO se hace acá, y el motivo: Alpine pone el bit en un binario APARTE (`busybox.suid`) y
+# reapunta sólo los symlinks privilegiados, para que el multi-call que corre todo el sistema no sea
+# setuid. Es mejor, y acá cuesta tocar el `install` de esta receta ⇒ **re-hashea busybox y con él las
+# 24 recetas que lo declaran build-dep**. Esta declaración es metadato (fuera de `hash_inputs`) y no
+# mueve un solo `ArtifactHash`, así que entra hoy; el split queda anotado como lo que sigue.
+[[setuid]]
+path = "bin/busybox"
+mode = "4755"
+why  = "once applets instalados lo necesitan (su, login, passwd, mount, umount, ping, ping6, traceroute, traceroute6, crontab, vlock) y sin el bit fallan los once sin nombrar el privilegio — `su` dice «must be suid to work properly» y `ping` sale 1 al instante, que se lee como «no hay red». El build trae CONFIG_FEATURE_SUID, así que busybox suelta el privilegio para todo applet que no lo pide."
diff --git a/recipes/mirada-compositor.toml b/recipes/mirada-compositor.toml
index 09dc9b48..07039615 100644
--- a/recipes/mirada-compositor.toml
+++ b/recipes/mirada-compositor.toml
@@ -37,3 +37,60 @@ flags    = ["-p", "mirada-compositor"]
 [deps]
 # libxkbcommon (smithay lo enlaza, no sólo dlopen) + pixman (composición CPU de smithay).
 build = ["mesa", "wayland", "wayland-protocols", "libdrm", "seatd", "linux-pam", "libinput", "libudev-zero", "libevdev", "mtdev", "libxkbcommon", "pixman", "expat", "zlib", "zstd", "libffi", "pkgconf"]
+
+# ── LA CARD QUE ARRANCA EL ESCRITORIO (`[[service]]`, 2026-10-15) ─────────────────────────────────
+# Hasta hoy esta receta no declaraba servicio, y la consecuencia estaba MEDIDA y no escrita: la Card
+# que lanza el compositor en la máquina del operador existe porque un script de parche
+# (`tawasuyu:scripts/declarar-mirada-en-la-semilla.sh`) la inyectó en la semilla a mano. O sea que
+# **una instalación de cero arranca el sistema y no arranca el escritorio**, y nada lo avisa: el
+# génesis no se queja de lo que nadie le declaró. Los otros cuatro escritorios ya pasan por
+# `takana service-cards` (lo usan `servidor-image.sh` y los `*-image.sh` de gnome/kde/cosmic);
+# mirada se armaba con un `/sbin/init` escrito a mano por `mirada-usb.sh`, que sirve para un medio
+# live y no deja nada en el disco.
+#
+# La declaración es metadato —fuera de `hash_inputs`, como `[[setuid]]`, `[[user]]` y `license`— así
+# que entra en una receta YA SELLADA sin mover un solo `ArtifactHash`.
+#
+# ⚠ **`seatd` va en la MISMA receta y no en la suya**, por el invariante de `validate_all`: dos
+# recetas no pueden reclamar lo mismo, y acá lo que se declara es el ORDEN de arranque de un
+# escritorio —seatd primero, el compositor después— que es una propiedad de mirada y no de seatd.
+# Sin seatd vivo el compositor no obtiene el seat y muere con el DRM ocupado.
+[[service]]
+label   = "seatd"
+id      = "01M4ZQ2GXTPZ8RVZRBVZCBE94G"
+exec    = "/usr/bin/seatd"
+scope   = "system"
+restart = { initial_ms = 500, max_ms = 20000 }
+
+[[service]]
+label   = "mirada"
+id      = "01M4ZQ2GXTQJ26NFQGXEE1D7BX"
+exec    = "/usr/bin/mirada-compositor --drm --greeter"
+scope   = "system"
+# El backoff lo hace arje: la versión a mano de esta Card llevaba un `sh -c` de quince líneas que
+# contaba intentos en `/run/mirada-intentos` y se quedaba quieto al tercero. Eso es exactamente lo
+# que `restart` hace, con el tope arriba — un compositor que no abre y reintenta cada 100 ms deja la
+# máquina inservible, y el techo de 30 s es lo que la vuelve diagnosticable.
+restart = { initial_ms = 1000, max_ms = 30000 }
+# ⚠ Un Ente NO hereda el PATH de ningún shell, y el compositor LANZA hijos (el greeter, el riel, la
+# terminal del menú). El PATH de esta Card es el que hereda la sesión entera, y hasta el 2026-10-14
+# decía `/usr/bin:/bin:/usr/sbin:/sbin` — sin `/usr/local/bin`, que es donde vive todo lo instalado
+# a mano. El operador tenía `claude` instalado y la máquina contestaba `claude: not found`.
+#
+# Los bin del hogar NO van acá a propósito: los agrega el propio compositor para el usuario que
+# inicia sesión (`utilidades::path_de_usuario`), y clavarlos en la Card los dejaría apuntando al
+# hogar de UNA persona en una imagen que instala cualquiera.
+envp = [
+  ["PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
+  ["HOME", "/root"],
+  ["XDG_RUNTIME_DIR", "/run/arje"],
+  ["XDG_SEAT", "seat0"],
+  ["XDG_SESSION_TYPE", "wayland"],
+  # El layout por defecto; el instalador pregunta el teclado y lo puede reescribir en la Card.
+  ["XKB_DEFAULT_LAYOUT", "us"],
+  # Sin esto libxkbcommon no encuentra las reglas y el teclado no responde una tecla. Lo aporta
+  # `xkeyboard-config`, declarado en el perfil el mismo día que esto.
+  ["XKB_CONFIG_ROOT", "/usr/share/X11/xkb"],
+  ["MIRADA_GREETER_BIN", "/usr/bin/mirada-greeter"],
+  ["RUST_LOG", "info"],
+]
diff --git a/scripts/instalador-initramfs.sh b/scripts/instalador-initramfs.sh
index 2b16de06..9fed0a30 100755
--- a/scripts/instalador-initramfs.sh
+++ b/scripts/instalador-initramfs.sh
@@ -102,7 +102,17 @@ TAKANA_MUSL="${TAKANA_MUSL:-$ROOT/target/x86_64-unknown-linux-musl/release/takan
 [ -x "$TAKANA_MUSL" ] || TAKANA_MUSL="$ROOT/target/release/takana"
 cp "$TAKANA_MUSL" "$IRD_TREE/bin/takana"; chmod 0755 "$IRD_TREE/bin/takana"
 cp scripts/takana-install-partition.sh "$IRD_TREE/instalador/"
-cp scripts/lib/herramientas-desde-store.sh "$IRD_TREE/instalador/lib/"
+# ── LAS LIBRERÍAS QUE EL INSTALADOR SOURCEA, DERIVADAS DEL PROPIO SCRIPT ──────────────────────────
+# Acá había UNA copia a mano (`herramientas-desde-store.sh`) y el script sourcea CUATRO:
+# `usuarios-del-home.sh`, `adoptar.sh`, `cuenta.sh` y ésa. Una librería que no viaja no da un error
+# claro: el `.` falla y el instalador sigue hasta que llama a la función, ya con el disco en la mano.
+# Derivarlas del script es lo que hace que agregar la quinta no exija acordarse de este sitio — la
+# lista a mano ya había quedado corta tres veces.
+for _lib in $(sed -n 's|^\. "$(dirname "$0")/lib/\([a-z-]*\.sh\)".*|\1|p' scripts/takana-install-partition.sh); do
+	[ -r "scripts/lib/$_lib" ] || { echo "falta scripts/lib/$_lib, que el instalador sourcea" >&2; exit 1; }
+	cp "scripts/lib/$_lib" "$IRD_TREE/instalador/lib/"
+done
+echo "   libs del instalador: $(sed -n 's|^\. "$(dirname "$0")/lib/\([a-z-]*\.sh\)".*|\1|p' scripts/takana-install-partition.sh | tr '\n' ' ')"
 cp "$KERNEL" "$IRD_TREE/payload/bzImage"
 mkdir -p "$IRD_TREE/payload/stage"
 cp -a "$STAGE/root" "$IRD_TREE/payload/stage/root"
diff --git a/scripts/instalador-paquete.sh b/scripts/instalador-paquete.sh
index 5864dcfe..6a3c36d5 100755
--- a/scripts/instalador-paquete.sh
+++ b/scripts/instalador-paquete.sh
@@ -57,7 +57,17 @@ poner() { rm -f "$2"; cp "$1" "$2"; chmod "${3:-0644}" "$2"; }
 poner "$TAKANA" "$SALIDA/takana" 0755
 poner "$KERNEL" "$SALIDA/kernel/boot/bzImage"
 poner scripts/takana-install-partition.sh "$SALIDA/takana-install-partition.sh" 0755
-poner scripts/lib/herramientas-desde-store.sh "$SALIDA/lib/herramientas-desde-store.sh"
+# ── LAS LIBRERÍAS QUE EL INSTALADOR SOURCEA, DERIVADAS DEL PROPIO SCRIPT ──────────────────────────
+# Acá había UNA copia a mano (`herramientas-desde-store.sh`) y el script sourcea CUATRO:
+# `usuarios-del-home.sh`, `adoptar.sh`, `cuenta.sh` y ésa. Una librería que no viaja no da un error
+# claro: el `.` falla y el instalador sigue hasta que llama a la función, ya con el disco en la mano.
+# Derivarlas del script es lo que hace que agregar la quinta no exija acordarse de este sitio — la
+# lista a mano ya había quedado corta tres veces.
+for _lib in $(sed -n 's|^\. "$(dirname "$0")/lib/\([a-z-]*\.sh\)".*|\1|p' scripts/takana-install-partition.sh); do
+	[ -r "scripts/lib/$_lib" ] || { echo "falta scripts/lib/$_lib, que el instalador sourcea" >&2; exit 1; }
+	poner "scripts/lib/$_lib" "$SALIDA/lib/$_lib"
+done
+echo "   libs del instalador: $(sed -n 's|^\. "$(dirname "$0")/lib/\([a-z-]*\.sh\)".*|\1|p' scripts/takana-install-partition.sh | tr '\n' ' ')"
 # `busybox` viaja suelto en el paquete —2 MiB— y no es redundante: es con lo que se desempaqueta el
 # árbol a instalar cuando NO hace falta saltar. El host puede no tener `cpio` ni `gzip` (en las tres
 # máquinas de este proyecto no hay `cpio`), así que depender de los del sistema ajeno es depender de
diff --git a/scripts/install-tui-test.sh b/scripts/install-tui-test.sh
index 88eeaa40..85b103fc 100755
--- a/scripts/install-tui-test.sh
+++ b/scripts/install-tui-test.sh
@@ -31,7 +31,11 @@ printf '127.0.0.1\tlocalhost\n' > "$R1/etc/hosts"
 
 echo "== test 1: modo INTERACTIVO (TUI dry-run) =="
 # Respuestas: disco #2 (vdb) · root 2048M · store 512M · hostname midistro · cred 2 (ssh) · authkeys ·
-#             zona America/La_Paz · teclado (default us) · confirmación "/dev/vdb"
+#             USUARIO ana · zona America/La_Paz · teclado (default us) · confirmación "/dev/vdb"
+#
+# ⚠ `ana` es la respuesta de la SECCIÓN DE CUENTA (lib/cuenta.sh, 2026-10-15). Este rootfs de
+# scratch no trae `usr/bin/doas`, así que la librería NO pregunta por administración —y eso es
+# determinista a propósito: mira el rootfs del test y no el de la máquina que corre el test—.
 OUT1="$W/out1.txt"
 BB=/nonexistent SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$ZI" \
   TAKANA_INSTALL_INTERACTIVE=1 TAKANA_INSTALL_DRYRUN=1 TAKANA_INSTALL_TESTROOT="$R1" \
@@ -42,6 +46,7 @@ BB=/nonexistent SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$ZI" \
 midistro
 2
 ssh-ed25519 AAAATESTKEY user@host
+ana
 America/La_Paz
 
 /dev/vdb
@@ -60,7 +65,29 @@ grep -q 'HOSTNAME=midistro' "$R1/etc/hammer/install.conf" || fail "install.conf
 grep -q 'KEYMAP=us'         "$R1/etc/hammer/install.conf" || fail "install.conf sin keymap default"
 # root en shadow debe seguir intacto (interactivo eligió ssh, no tocó la contraseña)
 grep -q '^root:!:' "$R1/etc/shadow" || fail "shadow de root alterado sin querer"
+# ── la cuenta (lib/cuenta.sh) ──────────────────────────────────────────────────────────────────
+# Sin `cryptpw` (BB=/nonexistent) no se puede hashear nada, así que lo que se exige acá es que la
+# cuenta EXISTA con su hogar y que el instalador DIGA que queda cerrada. Un instalador que la deja
+# cerrada en silencio es el defecto que esta sección cierra.
+grep -q "^ana:x:1000:1000:" "$R1/etc/passwd" || { cat "$R1/etc/passwd"; fail "el usuario ana no quedó en passwd con uid 1000"; }
+grep -q "^ana:x:1000:" "$R1/etc/group"       || { cat "$R1/etc/group"; fail "el grupo ana no quedó en group"; }
+[ -d "$R1/home/ana" ] || fail "/home/ana no se creó"
+grep -q '^ana:!:' "$R1/etc/shadow" || { cat "$R1/etc/shadow"; fail "ana debía quedar con la cuenta cerrada (no hay cryptpw)"; }
+grep -q 'SIN contraseña posible' "$OUT1" || { cat "$OUT1"; fail "no avisó que la cuenta queda sin contraseña"; }
+grep -q 'INADMINISTRABLE' "$OUT1" || { cat "$OUT1"; fail "no avisó que la máquina queda inadministrable (root cerrado y nadie escala)"; }
+[ ! -e "$R1/etc/doas.conf" ] || fail "escribió doas.conf en una imagen que no trae doas"
 echo "  ✓ interactivo: disco elegido + confirmado, config escrita (hostname/hosts/ssh/tz/keymap)"
+# ── /etc/profile: que exista, y que NO sea de NADIE EN PARTICULAR ─────────────────────────────
+# El segundo control es el que importa para una imagen que instala un desconocido: la primera
+# versión de este PATH llevaba `/home/sergio/.local/bin` escrito a mano, porque nació de arreglar
+# UNA máquina. Un fichero de `/etc` con el hogar de una persona adentro es un defecto que no se ve
+# hasta que lo instala otra.
+[ -f "$R1/etc/profile" ] || fail "/etc/profile no se escribió (sin él nadie lee /etc/profile.d y /usr/local/bin no existe para el shell)"
+grep -q '/usr/local/bin' "$R1/etc/profile" || fail "/etc/profile no garantiza /usr/local/bin"
+grep -q '/etc/profile.d' "$R1/etc/profile" || fail "/etc/profile no sourcea profile.d"
+grep -q '/home/[a-z]' "$R1/etc/profile" && { grep -n '/home/[a-z]' "$R1/etc/profile"; fail "/etc/profile lleva el hogar de UNA persona: no sirve para instalar en otra máquina"; }
+echo "  ✓ la cuenta: usuario creado con uid 1000, su hogar, y los DOS avisos (cerrada / inadministrable)"
+echo "  ✓ /etc/profile: los seis directorios, profile.d sourceado, y SIN el hogar de nadie escrito"
 
 echo "== test 2: confirmación ERRÓNEA cancela sin tocar nada =="
 OUT2="$W/out2.txt"
@@ -72,6 +99,7 @@ if BB=/nonexistent SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$ZI" \
 512
 h
 3
+
 us
 sda-equivocado
 ANSWERS
@@ -97,5 +125,83 @@ echo "== test 4: sin device y sin TTY ⇒ error de uso (no cuelga) =="
 if BB=/nonexistent sh "$INST" </dev/null >/dev/null 2>&1; then fail "debió fallar sin device ni TTY"; fi
 echo "  ✓ uso inválido reportado"
 
+echo "== test 5: LA CUENTA COMPLETA, con cryptpw de verdad =="
+# Lo que los tests 1-4 no pueden mostrar: que la contraseña se hashea, que la regla de doas se
+# escribe, y que el usuario entra a los grupos del hardware. Necesita un busybox con `cryptpw`; si
+# no hay, se SALTA diciéndolo — un test que se calla cuando no puede medir es peor que ninguno.
+BBREAL=$(command -v busybox 2>/dev/null || true)
+if [ -n "$BBREAL" ] && printf x | "$BBREAL" cryptpw -m sha512 >/dev/null 2>&1; then
+  R5="$W/root5"; mkdir -p "$R5/etc" "$R5/usr/bin" "$R5/usr/share/zoneinfo"
+  printf 'root:!:19000:0:99999:7:::\n'  > "$R5/etc/shadow"
+  printf 'root:x:0:0:root:/root:/bin/sh\n' > "$R5/etc/passwd"
+  printf 'root:x:0:\nvideo:x:27:\ninput:x:97:\n'  > "$R5/etc/group"
+  printf '127.0.0.1\tlocalhost\n'       > "$R5/etc/hosts"
+  : > "$R5/usr/bin/doas"; chmod 755 "$R5/usr/bin/doas"   # con doas, la librería SÍ pregunta por admin
+  OUT5="$W/out5.txt"
+  # Respuestas: disco 2 · 2048 · 512 · hostname · cred 3 (root sin cambios) · usuario ana ·
+  #             contraseña ×2 · admin 1 · zona (vacío) · teclado (default) · confirmación
+  BB="$BBREAL" SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$R5/usr/share/zoneinfo" \
+    TAKANA_INSTALL_INTERACTIVE=1 TAKANA_INSTALL_DRYRUN=1 TAKANA_INSTALL_TESTROOT="$R5" \
+    sh "$INST" >"$OUT5" 2>&1 <<'ANSWERS' || { cat "$OUT5"; fail "test 5 salió con error"; }
+2
+2048
+512
+midistro
+3
+ana
+clave-de-prueba
+clave-de-prueba
+1
+
+
+/dev/vdb
+ANSWERS
+  grep -qE '^ana:\$6\$' "$R5/etc/shadow" || { cat "$R5/etc/shadow"; fail "la contraseña de ana no quedó hasheada en SHA-512"; }
+  grep -q 'clave-de-prueba' "$R5/etc/shadow" "$OUT5" && fail "LA CONTRASEÑA EN CLARO quedó escrita en algún sitio"
+  grep -q '^permit persist ana$' "$R5/etc/doas.conf" || { cat "$R5/etc/doas.conf" 2>/dev/null; fail "la regla de doas no se escribió"; }
+  [ "$(stat -c%a "$R5/etc/doas.conf")" = 640 ] || fail "doas.conf no quedó 640"
+  grep -q '^video:x:27:ana$' "$R5/etc/group" || { grep '^video' "$R5/etc/group"; fail "ana no entró al grupo video (seatd lo exige por nombre)"; }
+  grep -q '^input:x:97:ana$' "$R5/etc/group" || fail "ana no entró al grupo input"
+  grep -q 'INADMINISTRABLE' "$OUT5" && fail "avisó inadministrable cuando ana SÍ puede escalar"
+  echo "  ✓ cuenta completa: hash SHA-512, nada en claro, permit persist 640, grupos video+input"
+
+  echo "== test 6: el usuario YA EXISTE (cosechado del /home conservado) ⇒ sólo su contraseña =="
+  # Éste es el caso que vivió el operador: `--keep` cosecha `sergio` del /home con el uid de sus
+  # archivos y `!` por contraseña. La sección NO debe crear un segundo usuario ni cambiarle el uid:
+  # si le cambia el uid, sus archivos dejan de ser suyos — el único daño irreparable que hay acá.
+  R6="$W/root6"; mkdir -p "$R6/etc" "$R6/usr/bin" "$R6/home/ana" "$R6/usr/share/zoneinfo"
+  printf 'root:x:0:0:root:/root:/bin/sh\nana:x:1047:1047:ana:/home/ana:/bin/sh\n' > "$R6/etc/passwd"
+  printf 'root:x:0:\nana:x:1047:\n' > "$R6/etc/group"
+  printf 'root:!:19000:0:99999:7:::\nana:!:20000:0:99999:7:::\n' > "$R6/etc/shadow"
+  printf '127.0.0.1\tlocalhost\n' > "$R6/etc/hosts"
+  : > "$R6/usr/bin/doas"; chmod 755 "$R6/usr/bin/doas"
+  : > "$R6/home/ana/lo-que-ya-tenia"
+  OUT6="$W/out6.txt"
+  BB="$BBREAL" SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$R6/usr/share/zoneinfo" \
+    TAKANA_INSTALL_INTERACTIVE=1 TAKANA_INSTALL_DRYRUN=1 TAKANA_INSTALL_TESTROOT="$R6" \
+    sh "$INST" >"$OUT6" 2>&1 <<'ANSWERS' || { cat "$OUT6"; fail "test 6 salió con error"; }
+2
+2048
+512
+midistro
+3
+ana
+otra-clave
+otra-clave
+1
+
+
+/dev/vdb
+ANSWERS
+  [ "$(grep -c '^ana:' "$R6/etc/passwd")" = 1 ] || { grep '^ana:' "$R6/etc/passwd"; fail "duplicó el usuario ana"; }
+  grep -q '^ana:x:1047:1047:' "$R6/etc/passwd" || { grep '^ana:' "$R6/etc/passwd"; fail "le CAMBIÓ el uid: sus archivos dejarían de ser suyos"; }
+  grep -qE '^ana:\$6\$' "$R6/etc/shadow" || fail "no le puso contraseña al usuario cosechado"
+  [ -f "$R6/home/ana/lo-que-ya-tenia" ] || fail "tocó el /home que ya existía"
+  grep -q 'ya existe con uid 1047' "$OUT6" || { cat "$OUT6"; fail "no dijo que reusaba el usuario cosechado"; }
+  echo "  ✓ usuario cosechado: uid 1047 intacto, sin duplicar, /home sin tocar, contraseña puesta"
+else
+  echo "  ⊘ SALTADO: no hay un busybox con cryptpw en esta máquina (los tests 5 y 6 no se midieron)"
+fi
+
 echo
 echo "✓✓ install-tui-test: TODOS los casos verdes"
diff --git a/scripts/iso-image.sh b/scripts/iso-image.sh
index 0ebd701a..9fee54a5 100755
--- a/scripts/iso-image.sh
+++ b/scripts/iso-image.sh
@@ -125,6 +125,12 @@ if [ "${INSTALLER:-0}" = 1 ]; then
     grub-mkimage -O i386-pc -p '(hd0,msdos1)/boot/grub' -o "$IPL/grub/core.img" \
         biosdisk part_msdos ext2 normal configfile linux echo ls boot search
     install -m 0755 "$ROOT/scripts/takana-live-install.sh" "$RFS/root/usr/bin/takana-install"
+    # La sección de la cuenta viaja CON el instalador: `scripts/lib/` no existe dentro del producto,
+    # y sin la librería el instalador no pregunta por ningún usuario — que es el defecto que esa
+    # librería existe para cerrar (medido en metal el 2026-10-14: máquina instalada e
+    # inadministrable). El instalador la busca acá y avisa en el resumen si no la encuentra.
+    install -d -m 0755 "$RFS/root/usr/lib/takana"
+    install -m 0644 "$ROOT/scripts/lib/cuenta.sh" "$RFS/root/usr/lib/takana/cuenta.sh"
     # hammer-recover (static musl): auto-sana un upgrade interrumpido al arranque del sistema instalado.
     RECOVER_BIN="$ROOT/target/x86_64-unknown-linux-musl/release/hammer-recover"
     if [ ! -x "$RECOVER_BIN" ]; then
diff --git a/scripts/lib/cuenta.sh b/scripts/lib/cuenta.sh
new file mode 100644
index 00000000..29be5e0c
--- /dev/null
+++ b/scripts/lib/cuenta.sh
@@ -0,0 +1,352 @@
+#!/bin/sh
+# lib/cuenta.sh — LA CUENTA DE QUIEN VA A USAR LA MÁQUINA: se pregunta antes de instalar y se
+# escribe al terminar. Es la sección que faltaba en los dos instaladores.
+#
+# ── El defecto, medido en metal el 2026-10-14/15 ─────────────────────────────────────────────────
+# El operador instaló, arrancó, y **no podía hacer nada**: ni entrar como root, ni escalar. Lo dijo
+# así: *«al correr su me dice que debe ser suid, así que se cagó ese boot»* y *«¿de dónde salió el
+# usuario sergio? me di cuenta que debe ser una sección antes de instalar»*.
+#
+# Las dos preguntas tienen la misma respuesta, y es ésta: **nadie preguntó nunca por una cuenta.**
+#
+#   · `takana-live-install.sh` (disco entero) pregunta hostname, zona, teclado y la credencial de
+#     ROOT — y ningún usuario. En una imagen de escritorio eso es una máquina donde el greeter no
+#     puede autenticar a nadie, porque no hay nadie.
+#   · `takana-install-partition.sh` (el camino de `--keep`/`--adopt`, el que usó él) **no pregunta
+#     NADA**. Los usuarios salen de `lib/usuarios-del-home.sh`, que los cosecha del `/home` que se
+#     conserva —un usuario por directorio, con el uid de sus archivos— y les pone `!` en
+#     `/etc/shadow`, que es «cuenta sin contraseña posible». Correcto de su parte: inventar una
+#     contraseña sería peor. Pero nadie, después, la pedía.
+#
+# Resultado medido: `sergio:!` + `root:!` + ningún `doas.conf` = una máquina instalada, arrancada y
+# **inadministrable**. El encabezado de `takana-core/src/setuid.rs` lo había previsto textualmente
+# («hoy es LATENTE: el instalador sólo pone contraseña de root, así que no hay un humano no-root que
+# lo choque») y el 14 de octubre lo hubo.
+#
+# ── Por qué una librería y no dos secciones ──────────────────────────────────────────────────────
+# El mismo argumento que `lib/usuarios-del-home.sh` escribe para sí: son DOS caminos y UNA pregunta,
+# y dos implementaciones divergen en el caso raro. Acá el caso raro es justo el del operador —el
+# usuario YA existe, cosechado del `/home` conservado, y lo único que falta es su contraseña— y una
+# de las dos copias se lo habría perdido.
+#
+# ── La regla de root, sin criterio ───────────────────────────────────────────────────────────────
+# Root queda **cerrado** si el usuario puede escalar (hay `doas` en la imagen y se pidió que
+# administre): es lo correcto, y es lo que hacen las distros desde hace una década. Si NO puede
+# escalar, entonces se pide contraseña de root **obligatoriamente** — porque una máquina sin
+# ninguna de las dos no se administra, y eso es exactamente lo que acabamos de medir.
+#
+# Se usa así (los dos instaladores):
+#
+#     . "$(dirname "$0")/lib/cuenta.sh"
+#     cuenta_preguntar "$RAIZ_DEL_MEDIO"   # TUI; no pregunta si no hay tty o si es desatendido
+#     ...
+#     cuenta_aplicar "$ARBOL"              # escribe passwd/group/shadow/home/doas.conf
+#
+# Desatendido (arneses y `--unattended`): `TAKANA_USER`, `TAKANA_USER_HASH` (un hash, NO una
+# contraseña), `TAKANA_USER_PW` (sólo donde no haya más remedio), `TAKANA_ADMIN=1|0`,
+# `TAKANA_ROOT_PW` / `TAKANA_ROOT_HASH` como ya existían.
+#
+# ⚠ **La contraseña NO viaja por la cmdline del kernel.** `/proc/cmdline` lo lee cualquiera, así que
+# un hash ahí es un ataque de diccionario offline servido en bandeja. Por eso esto pregunta en el
+# instalador —que tiene tty— en vez de recibirlo del que hizo el kexec.
+
+CUENTA_USER="${TAKANA_USER:-}"
+CUENTA_HASH="${TAKANA_USER_HASH:-}"
+CUENTA_ADMIN="${TAKANA_ADMIN:-1}"
+CUENTA_ROOT_HASH="${TAKANA_ROOT_HASH:-${HAMMER_ROOT_HASH:-}}"
+
+# Los dos instaladores ya traen sus propios `ask`/`read_secret`/`bb`; si falta alguno (al usar la
+# librería desde un arnés) se definen acá. Un `command -v` sobre una función de shell la encuentra.
+command -v bb >/dev/null 2>&1 || bb() { busybox "$@" 2>/dev/null || command "$@"; }
+command -v ask >/dev/null 2>&1 || ask() {
+	_a_v="$1"; printf '%s%s: ' "$2" "$([ -n "${3:-}" ] && printf ' [%s]' "$3")"
+	IFS= read -r _a_r || _a_r=""
+	[ -n "$_a_r" ] || _a_r="${3:-}"
+	eval "$_a_v=\$_a_r"
+}
+command -v read_secret >/dev/null 2>&1 || read_secret() {
+	printf '%s: ' "$2"; stty -echo 2>/dev/null || true
+	IFS= read -r _rs || _rs=""; stty echo 2>/dev/null || true; echo
+	eval "$1=\$_rs"
+}
+
+# cuenta_hash <contraseña> — SHA-512 por `cryptpw`. Devuelve vacío si este busybox no lo trae, y el
+# llamador lo dice: un hash vacío escribiría una cuenta SIN CONTRASEÑA, que es peor que no tenerla.
+cuenta_hash() {
+	printf '%s' "$1" | bb cryptpw -m sha512 2>/dev/null || true
+}
+
+# cuenta_login_valido <nombre> — el mismo criterio que `lib/usuarios-del-home.sh` usa al cosechar,
+# porque los dos juegos de usuarios tienen que poder convivir en el mismo `/etc/passwd`.
+cuenta_login_valido() {
+	case "$1" in
+		"" | -* | *[!a-z0-9_-]*) return 1 ;;
+		root | sshd | bin | daemon | nobody | messagebus) return 1 ;;
+		*) [ "${#1}" -le 32 ] ;;
+	esac
+}
+
+# cuenta_preguntar [raíz-del-medio] — la sección. No pregunta si no hay tty, si es desatendido, o si
+# ya vino todo por el ambiente.
+cuenta_preguntar() {
+	_cp_raiz="${1:-/}"
+	[ "${TAKANA_UNATTENDED:-0}" = 1 ] && return 0
+	# ⚠ La condición es la MISMA que usa el instalador para decidir si es interactivo, y no sólo
+	# `[ -t 0 ]`: con stdin alimentado por un heredoc —que es como se prueba una TUI sin una persona
+	# delante— no hay tty, y la sección se saltaba entera dejando el arnés en verde sobre un
+	# instalador que no preguntaba nada. Medido escribiéndolo.
+	[ -t 0 ] || [ "${TAKANA_INSTALL_INTERACTIVE:-${HAMMER_INSTALL_INTERACTIVE:-}}" = 1 ] || return 0
+	[ -n "$CUENTA_USER" ] && [ -n "$CUENTA_HASH" ] && return 0
+
+	# ¿Hay con qué escalar en la imagen que se está instalando? La respuesta cambia la regla de root,
+	# así que se MIRA y no se supone.
+	_cp_doas=no
+	for _d in usr/bin/doas bin/doas usr/local/bin/doas usr/bin/sudo; do
+		[ -e "$_cp_raiz/$_d" ] && { _cp_doas=sí; break; }
+	done
+
+	echo
+	echo "Tu cuenta en esta máquina"
+	echo "  ─────────────────────────"
+	echo "  Es la que vas a usar para entrar al escritorio. Sin una cuenta con contraseña,"
+	echo "  el escritorio arranca y no deja entrar a nadie."
+	while :; do
+		ask CUENTA_USER "Nombre de usuario" "${CUENTA_USER:-}"
+		[ -z "$CUENTA_USER" ] && { echo "  (sin usuario: la máquina queda sólo con root)"; break; }
+		cuenta_login_valido "$CUENTA_USER" && break
+		echo "  ✗ «$CUENTA_USER» no sirve de login: minúsculas, dígitos, «_» y «-», sin empezar con «-»,"
+		echo "    y no puede ser un usuario de sistema (root, sshd, nobody…)."
+		CUENTA_USER=""
+	done
+
+	if [ -n "$CUENTA_USER" ]; then
+		if [ -z "$CUENTA_HASH" ]; then
+			if [ -n "$(cuenta_hash x)" ]; then
+				while :; do
+					read_secret _cp_p1 "Contraseña para $CUENTA_USER"
+					read_secret _cp_p2 "Repetir"
+					[ "$_cp_p1" = "$_cp_p2" ] && [ -n "$_cp_p1" ] || { echo "  no coinciden (o vacía) — reintentá"; continue; }
+					CUENTA_HASH=$(cuenta_hash "$_cp_p1")
+					[ -n "$CUENTA_HASH" ] || { echo "  ⚠ no pude generar el hash"; break; }
+					break
+				done
+				_cp_p1=; _cp_p2=
+			else
+				echo "  ⚠ este busybox no trae cryptpw: no puedo poner contraseña y la cuenta queda cerrada."
+			fi
+		fi
+		if [ "$_cp_doas" = sí ]; then
+			ask CUENTA_ADMIN "¿$CUENTA_USER administra la máquina (doas)? 1=sí 0=no" "1"
+		else
+			echo "  ⚠ esta imagen no trae doas ni sudo: nadie puede escalar, así que root necesita contraseña."
+			CUENTA_ADMIN=0
+		fi
+	fi
+
+	# La regla de root, aplicada: cerrada si hay quien escale, obligatoria si no.
+	if [ -n "$CUENTA_USER" ] && [ "$CUENTA_ADMIN" = 1 ] && [ -n "$CUENTA_HASH" ]; then
+		[ -n "$CUENTA_ROOT_HASH" ] || echo "  · root queda CERRADO: se administra con «doas» desde $CUENTA_USER."
+	elif [ -z "$CUENTA_ROOT_HASH" ]; then
+		echo
+		echo "  Nadie puede escalar todavía ⇒ root necesita contraseña, o la máquina queda inadministrable."
+		if [ -n "$(cuenta_hash x)" ]; then
+			while :; do
+				read_secret _cp_r1 "Contraseña de root"
+				read_secret _cp_r2 "Repetir"
+				[ "$_cp_r1" = "$_cp_r2" ] && [ -n "$_cp_r1" ] || { echo "  no coinciden (o vacía) — reintentá"; continue; }
+				CUENTA_ROOT_HASH=$(cuenta_hash "$_cp_r1"); break
+			done
+			_cp_r1=; _cp_r2=
+		fi
+	fi
+}
+
+# cuenta_resumen — una línea para el resumen de confirmación del instalador.
+cuenta_resumen() {
+	if [ -z "$CUENTA_USER" ]; then printf 'sin usuario (sólo root)'; return; fi
+	printf '%s' "$CUENTA_USER"
+	[ -n "$CUENTA_HASH" ] && printf ' con contraseña' || printf ' SIN contraseña (cuenta cerrada)'
+	[ "$CUENTA_ADMIN" = 1 ] && printf ', administra (doas)'
+}
+
+# _cuenta_romper_hardlink <fichero> — el árbol del stage son HARDLINKS del rootfs sellado (ADR
+# 0005): un `>>` sobre `etc/passwd` escribiría DENTRO del artefacto del store y lo envenenaría para
+# todas las imágenes, sin que nada falle. Es la misma precaución que toma `usuarios_aplicar`, y la
+# razón de que acá no haya un solo `>>` directo.
+_cuenta_romper_hardlink() {
+	[ -e "$1" ] || return 0
+	cp "$1" "$1.cuenta-tmp" && mv "$1.cuenta-tmp" "$1"
+}
+
+# cuenta_aplicar <árbol> — escribe la cuenta en la raíz nueva. Idempotente: si el usuario ya existe
+# (cosechado del `/home` conservado) NO lo vuelve a crear y sólo le pone su contraseña, que es
+# exactamente el caso del operador.
+cuenta_aplicar() {
+	_ca_t="$1"
+	bb mkdir -p "$_ca_t/etc"
+	for _f in etc/passwd etc/group etc/shadow; do _cuenta_romper_hardlink "$_ca_t/$_f"; done
+	[ -e "$_ca_t/etc/passwd" ] || : > "$_ca_t/etc/passwd"
+	[ -e "$_ca_t/etc/group" ]  || : > "$_ca_t/etc/group"
+	[ -e "$_ca_t/etc/shadow" ] || { : > "$_ca_t/etc/shadow"; bb chmod 600 "$_ca_t/etc/shadow"; }
+
+	if [ -n "$CUENTA_USER" ]; then
+		if bb awk -F: -v n="$CUENTA_USER" '$1==n{f=1} END{exit !f}' "$_ca_t/etc/passwd"; then
+			_ca_uid=$(bb awk -F: -v n="$CUENTA_USER" '$1==n{print $3; exit}' "$_ca_t/etc/passwd")
+			echo "    · «$CUENTA_USER» ya existe con uid $_ca_uid (del /home conservado): le pongo su contraseña"
+		else
+			# El primer uid libre desde 1000. Se cuenta sobre el passwd que YA tiene los usuarios
+			# cosechados, así que no puede chocar con ellos.
+			_ca_uid=1000
+			while bb awk -F: -v u="$_ca_uid" '$3==u{f=1} END{exit !f}' "$_ca_t/etc/passwd"; do
+				_ca_uid=$((_ca_uid + 1))
+			done
+			printf '%s:x:%s:%s:%s:/home/%s:/bin/sh\n' \
+				"$CUENTA_USER" "$_ca_uid" "$_ca_uid" "$CUENTA_USER" "$CUENTA_USER" >> "$_ca_t/etc/passwd"
+			bb awk -F: -v g="$_ca_uid" '$3==g{f=1} END{exit !f}' "$_ca_t/etc/group" \
+				|| printf '%s:x:%s:\n' "$CUENTA_USER" "$_ca_uid" >> "$_ca_t/etc/group"
+			echo "    · usuario «$CUENTA_USER» creado con uid $_ca_uid"
+		fi
+		# El hogar: sólo si NO existe. Con `--keep` o `--adopt` ya está ahí con sus archivos y sus
+		# dueños, y pisarlo sería el único daño irreparable que puede hacer un instalador.
+		if [ ! -e "$_ca_t/home/$CUENTA_USER" ]; then
+			bb mkdir -p "$_ca_t/home/$CUENTA_USER"
+			bb chown "$_ca_uid:$_ca_uid" "$_ca_t/home/$CUENTA_USER" 2>/dev/null || true
+			bb chmod 700 "$_ca_t/home/$CUENTA_USER"
+			echo "    · /home/$CUENTA_USER creado (uid $_ca_uid)"
+		else
+			echo "    · /home/$CUENTA_USER ya existe: NO lo toco"
+		fi
+		# Los grupos del hardware: seatd exige `video` POR NOMBRE, y sin `input` libinput no abre los
+		# dispositivos. Sólo si el grupo existe en la imagen — inventar un gid acá sería adivinar.
+		for _g in video input audio seat; do
+			bb awk -F: -v g="$_g" -v u="$CUENTA_USER" '
+				BEGIN{OFS=":"}
+				$1==g { if ($4=="") $4=u; else if ($4 !~ ("(^|,)" u "(,|$)")) $4=$4","u; f=1 }
+				{print}
+				END{exit !f}' "$_ca_t/etc/group" > "$_ca_t/etc/group.cuenta-tmp" \
+				&& mv "$_ca_t/etc/group.cuenta-tmp" "$_ca_t/etc/group" \
+				|| rm -f "$_ca_t/etc/group.cuenta-tmp"
+		done
+		if [ -n "$CUENTA_HASH" ]; then
+			_cuenta_set_shadow "$CUENTA_USER" "$CUENTA_HASH" "$_ca_t"
+		else
+			_cuenta_set_shadow "$CUENTA_USER" '!' "$_ca_t"
+			echo "    ⚠ «$CUENTA_USER» queda SIN contraseña posible: nadie va a poder entrar con esa cuenta"
+		fi
+		# La regla de escalada. Sin esto doas RECHAZA TODO («no rule matched») aunque esté instalado y
+		# con su bit: quién escala no lo puede decidir el paquete, lo decide esta máquina.
+		if [ "$CUENTA_ADMIN" = 1 ]; then
+			if [ -e "$_ca_t/usr/bin/doas" ] || [ -e "$_ca_t/bin/doas" ]; then
+				if [ -e "$_ca_t/etc/doas.conf" ]; then
+					echo "    · /etc/doas.conf ya existe: no lo toco"
+				else
+					printf '# Quién puede ser root en esta máquina. La ÚLTIMA regla que casa es la que vale.\npermit persist %s\n' \
+						"$CUENTA_USER" > "$_ca_t/etc/doas.conf"
+					bb chown 0:0 "$_ca_t/etc/doas.conf" 2>/dev/null || true
+					bb chmod 640 "$_ca_t/etc/doas.conf"
+					echo "    · /etc/doas.conf: permit persist $CUENTA_USER"
+				fi
+			else
+				echo "    ⚠ se pidió que «$CUENTA_USER» administre y esta imagen NO trae doas: no hay escalada"
+			fi
+		fi
+	fi
+
+	if [ -n "$CUENTA_ROOT_HASH" ]; then
+		_cuenta_set_shadow root "$CUENTA_ROOT_HASH" "$_ca_t"
+		echo "    · root con contraseña"
+	fi
+
+	# El entorno de login de esa cuenta. Va acá y no en otra sección porque una cuenta cuyo shell no
+	# encuentra `/usr/local/bin` es media cuenta: es lo que dejó al operador con `claude` instalado y
+	# `claude: not found` en la consola.
+	cuenta_perfil_de_login "$_ca_t"
+
+	# ── El brazo de control, ACÁ y no en un test aparte ───────────────────────────────────────────
+	# Una cuenta mal escrita no se nota hasta el primer arranque, y arreglarla cuesta montar el disco
+	# desde otro sistema. Se comprueba lo que se acaba de escribir, y se dice en voz alta.
+	_ca_mal=0
+	if [ -n "$CUENTA_USER" ]; then
+		bb awk -F: -v n="$CUENTA_USER" '$1==n{f=1} END{exit !f}' "$_ca_t/etc/passwd" \
+			|| { echo "    ✗ «$CUENTA_USER» NO quedó en /etc/passwd"; _ca_mal=$((_ca_mal+1)); }
+		if [ -n "$CUENTA_HASH" ]; then
+			bb awk -F: -v n="$CUENTA_USER" '$1==n && $2 ~ /^\$/{f=1} END{exit !f}' "$_ca_t/etc/shadow" \
+				|| { echo "    ✗ «$CUENTA_USER» no quedó con un hash en /etc/shadow"; _ca_mal=$((_ca_mal+1)); }
+		fi
+	fi
+	# El aviso cubre las TRES formas de quedar sin nadie que administre: sin contraseña de root y
+	# (sin usuario | con usuario que no escala | con usuario sin contraseña). La primera versión de
+	# esta línea sólo miraba `ADMIN != 1`, y con el default `ADMIN=1` y usuario VACÍO se callaba —
+	# que es exactamente la combinación con la que arrancó la máquina del operador.
+	if [ -z "$CUENTA_ROOT_HASH" ] \
+		&& { [ -z "$CUENTA_USER" ] || [ "$CUENTA_ADMIN" != 1 ] || [ -z "$CUENTA_HASH" ]; }; then
+		echo "    ⚠ root cerrado y nadie escala: esta máquina va a arrancar INADMINISTRABLE"
+	fi
+	[ "$_ca_mal" = 0 ]
+}
+
+# cuenta_perfil_de_login <árbol> — `/etc/profile`, que es lo que lee el shell de esa cuenta.
+#
+# ── Por qué está acá y no en una receta ──────────────────────────────────────────────────────────
+# Medido en la máquina del operador: **`/etc/profile` NO EXISTÍA**. Y como el `ash` de busybox
+# sourcea `/etc/profile.d/*` sólo si ese archivo lo hace, `/etc/profile.d/tawasuyu-dev.sh` —que
+# exportaba el PATH del toolchain— estuvo inerte desde el primer arranque: `claude` instalado en
+# `/usr/local/bin` y la consola contestando `claude: not found`. Las recetas no tienen mecanismo
+# para aportar un fichero de `/etc` (no hay `[[file]]`: lo que entra sale del `install` de la
+# receta, y tocar eso re-hashea), así que el sitio donde nace un `/etc` de ESTA máquina es el
+# instalador — el mismo que escribe `hostname`, `hosts`, `localtime` y `shadow`.
+#
+# **Los seis directorios se garantizan uno por uno** en vez de agregar sólo `/usr/local`: un PATH
+# heredado puede traer unos y no otros, y entonces `su` o `wpa_supplicant` «no existen» porque
+# faltaba `/bin` o `/usr/sbin`. Medido dentro de un chroot de la imagen: con el PATH de la otra
+# máquina, `claude` se encontraba y `su` no.
+#
+# Y los del hogar van con `$HOME`, no con una ruta escrita: este fichero viaja a la máquina de
+# cualquiera. La primera versión de este PATH decía `/home/sergio/...` y eso es justo lo que no
+# puede quedar en algo que instala un desconocido.
+cuenta_perfil_de_login() {
+	_cp_t="$1"
+	[ -e "$_cp_t/etc/profile" ] && { echo "    · /etc/profile ya existe: no lo toco"; return 0; }
+	bb mkdir -p "$_cp_t/etc"
+	cat > "$_cp_t/etc/profile" <<'PERFIL'
+# /etc/profile — lo que lee un login shell.
+#
+# El `ash` de busybox sourcea ESTE fichero y nada más: `/etc/profile.d/*` no se lee si acá no se
+# lee. Sin esto, la consola de rescate tampoco tiene PATH.
+for dir in /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin; do
+	case ":${PATH:-}:" in
+	*":$dir:"*) ;;
+	*) PATH="${PATH:+$PATH:}$dir" ;;
+	esac
+done
+# Y los del hogar, al final: lo de la persona no debe tapar lo del sistema.
+for dir in "$HOME/.local/bin" "$HOME/bin"; do
+	[ -d "$dir" ] || continue
+	case ":$PATH:" in *":$dir:"*) ;; *) PATH="$PATH:$dir" ;; esac
+done
+export PATH
+umask 022
+
+for perfil in /etc/profile.d/*.sh; do
+	[ -r "$perfil" ] && . "$perfil"
+done
+unset perfil dir
+PERFIL
+	bb chmod 644 "$_cp_t/etc/profile"
+	echo "    · /etc/profile escrito (los seis directorios + los del hogar al final)"
+}
+
+# El mismo `_set_shadow` que ya tenía `takana-live-install.sh`, con otro nombre para no pisárselo.
+_cuenta_set_shadow() {
+	_cs_u="$1"; _cs_h="$2"; _cs_f="$3/etc/shadow"
+	if [ ! -f "$_cs_f" ]; then
+		printf '%s:%s:20000:0:99999:7:::\n' "$_cs_u" "$_cs_h" > "$_cs_f"
+		bb chmod 600 "$_cs_f"
+		return
+	fi
+	bb awk -F: -v u="$_cs_u" -v h="$_cs_h" 'BEGIN{OFS=":"}
+		{ if ($1==u){ $2=h; seen=1 } print }
+		END{ if(!seen) print u,h,"20000","0","99999","7","","","" }' "$_cs_f" > "$_cs_f.cuenta-tmp" \
+		&& mv "$_cs_f.cuenta-tmp" "$_cs_f"
+	bb chmod 600 "$_cs_f"
+}
diff --git a/scripts/takana-install-partition.sh b/scripts/takana-install-partition.sh
index 5c16148e..ab149c8b 100755
--- a/scripts/takana-install-partition.sh
+++ b/scripts/takana-install-partition.sh
@@ -118,6 +118,10 @@ case "$ON_ACTIVE" in auto|kexec|free-space|abort) ;; *) echo "--on-active-root:
 # un disco; estas funciones, sobre directorios, se prueban enteras en un segundo.
 . "$(dirname "$0")/lib/usuarios-del-home.sh"
 . "$(dirname "$0")/lib/adoptar.sh"
+# La cuenta de quien va a usar la máquina. Este camino —`--keep`/`--adopt`, el más común de todos—
+# no preguntaba NADA: los usuarios salían del `/home` conservado con `!` en shadow y root quedaba
+# cerrado, o sea una máquina instalada e inadministrable. Medido en metal el 2026-10-14.
+. "$(dirname "$0")/lib/cuenta.sh"
 SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
 # `e2label` sólo hace falta para `--adopt`, que no formatea: ahí la etiqueta por la que el arranque
 # encuentra la raíz hay que ponerla a mano. Se resuelve acá arriba —con el resto— y sólo cuando se
@@ -1139,6 +1143,16 @@ if [ -n "$KEEP_SPEC" ]; then keep_aplicar "$ROOT_TREE"; fi
 # dentro de la raíz. Lo único que hace falta es que sus dueños existan en el sistema nuevo.
 if [ -n "$ADOPT" ] && [ -s "$ADOPT_USUARIOS" ]; then usuarios_aplicar "$ROOT_TREE" "$ADOPT_USUARIOS"; fi
 
+# ── LA CUENTA: se pregunta acá y se escribe acá, con el destino TODAVÍA SIN TOCAR ─────────────────
+# El orden importa en las dos direcciones. Después de `usuarios_aplicar`, para que un usuario ya
+# cosechado del `/home` conservado sólo reciba su contraseña en vez de crearse dos veces (ése es el
+# caso del operador: `sergio` existía con el uid de sus archivos y `!` por contraseña). Y antes del
+# `mke2fs`, por el mismo motivo que `keep_aplicar`: si esto falla, nada se destruyó.
+echo "══ la cuenta de quien va a usar la máquina ═══════════════════════════════════"
+cuenta_preguntar "$ROOT_TREE"
+cuenta_aplicar "$ROOT_TREE" \
+    || { echo "takana-install-partition: la cuenta NO quedó escrita; no sigo y NADA se formateó." >&2; exit 1; }
+
 # ── 2) Formatear SÓLO la partición destino, y poblarla SIN MONTARLA ────────────────────────────
 # `mke2fs -d` escribe el árbol dentro del filesystem nuevo sin `mount` y sin loop. Que no haya
 # montaje no es elegancia: un `mount` mal apuntado en un disco ajeno es justamente el accidente que
diff --git a/scripts/takana-live-install.sh b/scripts/takana-live-install.sh
index 1cedd01a..04af8f62 100755
--- a/scripts/takana-live-install.sh
+++ b/scripts/takana-live-install.sh
@@ -88,6 +88,25 @@ read_secret() {  # read_secret VAR "prompt"  (sin eco si hay stty)
 
 have_cryptpw() { bb 2>&1 | grep -qw cryptpw; }
 
+# ── LA CUENTA DE QUIEN VA A USAR LA MÁQUINA (lib/cuenta.sh) ───────────────────────────────────────
+# Este instalador preguntaba hostname, zona, teclado y la credencial de ROOT — y ningún usuario. En
+# una imagen de escritorio eso es una máquina donde el greeter no puede autenticar a nadie, porque
+# no hay nadie. La sección es compartida con `takana-install-partition.sh` y vive en la librería:
+# son DOS caminos y UNA pregunta, el mismo argumento que escribe `lib/usuarios-del-home.sh`.
+#
+# ⚠ Se busca en dos sitios porque este script **se inyecta como `/usr/bin/takana-install`** dentro
+# del producto (`iso-image.sh`), donde `scripts/lib/` no existe: ahí la librería viaja a
+# `/usr/lib/takana/cuenta.sh`. Y si no viajó, no se muere el instalador — se dice en el resumen,
+# que es donde alguien lo va a leer antes de apretar.
+for _lib_cuenta in "$(dirname "$0")/lib/cuenta.sh" /usr/lib/takana/cuenta.sh; do
+    [ -r "$_lib_cuenta" ] && { . "$_lib_cuenta"; break; }
+done
+if ! command -v cuenta_preguntar >/dev/null 2>&1; then
+    cuenta_preguntar() { :; }
+    cuenta_aplicar() { :; }
+    cuenta_resumen() { printf 'NINGUNA — lib/cuenta.sh no viajó en este medio'; }
+fi
+
 # Reescribe el campo de contraseña de un usuario en $mnt/etc/shadow (o crea el fichero).
 _set_shadow() {
     _u="$1"; _h="$2"; _f="$3/etc/shadow"
@@ -113,6 +132,11 @@ apply_post_install_config() {
         fi
     fi
     if [ -n "${CFG_ROOT_HASH:-}" ]; then _set_shadow root "$CFG_ROOT_HASH" "$_mnt"; fi
+    # La cuenta: usuario, su contraseña, su hogar, los grupos del hardware y la regla de doas. Lleva
+    # su propio brazo de control adentro y devuelve ≠0 si no quedó escrita — se dice fuerte, porque
+    # una cuenta que no quedó sólo se descubre al primer arranque, y arreglarla desde ahí pide montar
+    # el disco desde otro sistema.
+    cuenta_aplicar "$_mnt" || echo "  ⚠ LA CUENTA NO QUEDÓ ESCRITA: revisá /etc/passwd y /etc/shadow antes de reiniciar" >&2
     if [ -n "${CFG_ROOT_AUTHKEYS:-}" ]; then
         bb mkdir -p "$_mnt/root/.ssh"; bb chmod 700 "$_mnt/root/.ssh"
         printf '%s\n' "$CFG_ROOT_AUTHKEYS" >> "$_mnt/root/.ssh/authorized_keys"
@@ -212,6 +236,17 @@ interactive_setup() {
         *)  echo "  (root sin credencial nueva)" ;;
     esac
 
+    # --- la cuenta de quien va a usar la máquina ---
+    # Va después de la credencial de root y le pasa lo ya decidido, para no preguntar dos veces por
+    # lo mismo: la librería sólo insiste con root si NADIE puede escalar, que es la única
+    # combinación que deja una máquina inadministrable.
+    CUENTA_ROOT_HASH="$CFG_ROOT_HASH"
+    # La raíz que se MIRA para saber si hay con qué escalar. En producción es "/" —el medio live es
+    # el sistema que se copia—; en el arnés es el rootfs de scratch, porque si no el test
+    # dependería de si la máquina que lo corre tiene sudo instalado.
+    cuenta_preguntar "${TAKANA_INSTALL_TESTROOT:-${HAMMER_INSTALL_TESTROOT:-/}}"
+    CFG_ROOT_HASH="$CUENTA_ROOT_HASH"
+
     # --- locale (best-effort) ---
     _zi="${ZONEINFO:-/usr/share/zoneinfo}"
     if [ -d "$_zi" ]; then
@@ -232,6 +267,7 @@ interactive_setup() {
     echo "  / = ${ROOT_MB} MiB    /store = ${STORE_MB} MiB    /var/lib/hammer = resto"
     echo "  hostname: $CFG_HOSTNAME"
     echo "  root    : $_cd"
+    echo "  usuario : $(cuenta_resumen)"
     echo "  zona    : ${CFG_TZ:-UTC}    teclado: $CFG_KEYMAP"
     echo
     echo "  ⚠  ESTO BORRA TODO EN $DEV."