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…

TOML 1471Shell 173Rust 105Markdown 943545 Sutichaykuna

paso 4: el ensamblado de imagen toma sus herramientas del STORE — y estaba roto en las 3 máquinas

La disyuntiva del paso 4 («receta de grub o retirar la rama BIOS») se cierra de un lado por medición:
Hetzner Cloud ARRANCA POR BIOS (medido en gioser, SDD 28 §3.1) ⇒ la imagen del servidor de producción
es justamente la que necesita esa rama, así que no se retira.

Y al mirar apareció algo más grande: los scripts tomaban sfdisk/mke2fs/cpio/gzip/mformat/xorriso del
PATH del host, y en el hub, en su anfitrión y en el worker NO EXISTE NINGUNO — el pipeline de imagen
estaba roto en las tres máquinas, en silencio, hasta que alguien lo corriera. Seis de las siete son
NUESTRAS y están selladas (util-linux, e2fsprogs, libarchive/bsdcpio, gzip, mtools, xorriso, más
dosfstools), estáticas musl, y corren tal cual en el host.

- `scripts/lib/herramientas-desde-store.sh`: resolvedor único, por HASH VIGENTE (iso-image.sh
  resolvía xorriso por el primer glob y install-image-efi.sh mtools por fecha: con dos artefactos
  conviviendo el más reciente puede ser el viejo). El host queda como caída RUIDOSA: un binario sin
  hash se anuncia en el log. El store se detecta, porque el ./store del clon está vacío en el hub.
- `scripts/vigia-ensamblado.sh`: 8 selladas · 0 del host · 2 ausentes (grub-mkimage, grub-bios-setup).
- Cableados install-image.sh, install-image-efi.sh, iso-image.sh y metal-usb-sdboot.sh.
- `sonda_hardlink`: el staging enlaza en duro desde el rootfs y el EXDEV llegaba como una PARED de
  `cp: Invalid cross-device link` sin decir la causa. Sonda funcional (st_dev no sirve: store y
  work/out son bind-mounts del mismo dispositivo) que nombra los dos montajes.

PROBADO de punta a punta: una imagen EFI armada EN EL HUB, que antes era imposible — rc=0, 297M
sparse, tabla GPT verificada con el sfdisk sellado y la ESP con las dos rutas del kernel.

Queda UNA receta: el gestor de arranque BIOS (grub, o limine que cubre BIOS y UEFI de una). Y queda
anotado que metal-usb-sdboot.sh arranca con un systemd-bootx64.efi sacado de un rootfs de Arch del
home del usuario: otro binario ajeno sin hash en el camino del metal.

e30ffbbf07685fc0c00ad1d6548a92e000d42051 · ruwaq Sergio · (1 killa ñawpaq) · tayta e679ec1ec · kay commitpi sach'ata qhaway

Semántico sayk'uy

Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.

7 qillqakuna · +385 −43

diff --git a/docs/plan-que-sigue-tras-busybox.md b/docs/plan-que-sigue-tras-busybox.md
index 02950e5b..acdbc0db 100644
--- a/docs/plan-que-sigue-tras-busybox.md
+++ b/docs/plan-que-sigue-tras-busybox.md
@@ -588,3 +588,97 @@ El detalle está en `docs/plan-botar-busybox.md` §7. En corto, y todo con `rc`
 
 **De 23 applets de trabajo real quedan 3**, y dos son el ADR de `getty`. El paso 3 del plan decía
 «23 → 11»; medido, es **23 → 3**.
+
+---
+
+# Paso 4 — HECHO: las herramientas del ensamblado salen del store; el gestor de arranque es el único hueco (2026-09-22)
+
+El paso 4 estaba planteado como una disyuntiva: «o `grub` entra al corpus como receta, o la rama BIOS
+se retira». **Medir la cerró de un lado y abrió algo más grande del otro.**
+
+## La rama BIOS no se puede retirar, y está medido en el repo
+
+> **Hetzner Cloud arranca por BIOS** — «medido en gioser: no existe `/sys/firmware/efi`; GPT + GRUB»
+> (SDD 28 §3.1).
+
+O sea que la imagen del **servidor de producción** es justamente la que necesita la rama BIOS. La
+disyuntiva se resuelve por el otro lado: el gestor de arranque tiene que entrar al corpus.
+
+## Lo que apareció al mirar: el pipeline de imagen estaba roto en las TRES máquinas
+
+Los scripts tomaban sus herramientas del `PATH` del host. Medido en el hub, en su anfitrión (`raiz`) y
+en el worker `dev.gioser.net`:
+
+```
+grub-mkimage AUSENTE · grub-bios-setup AUSENTE · cpio AUSENTE · mformat AUSENTE · xorriso AUSENTE
+```
+
+**Ninguna imagen se podía armar en ninguna de las tres**, y sólo se veía al intentarlo, con un
+`falta cpio` que se lee como un descuido de la máquina. Y lo absurdo: **seis de las siete son
+nuestras y están selladas** — `util-linux` (sfdisk), `e2fsprogs` (mke2fs), `libarchive` (bsdcpio),
+`gzip`, `mtools`, `xorriso`, más `dosfstools`. Todas estáticas musl; **corren tal cual en el host**,
+comprobado ejecutándolas.
+
+## Lo hecho
+
+**`scripts/lib/herramientas-desde-store.sh`** — resolvedor único: la herramienta sale del artefacto
+sellado **por hash vigente**, con el host como CAÍDA ruidosa (un binario del host no tiene hash ⇒ la
+imagen deja de ser reproducible por ahí, y eso se dice en el log) y abortando con las dos rutas
+nombradas si no está en ninguno.
+
+⚠ **Por hash, no por fecha ni por el primer glob**: `iso-image.sh` resolvía xorriso con
+`ls store/*-xorriso/…|head -1` y `install-image-efi.sh` mtools con `ls -dt …|head -1`. Con dos
+artefactos del mismo paquete conviviendo —lo normal tras cualquier re-hash— el más reciente puede ser
+el viejo. Es la lección que `hydrate-profile.py` y `libs-compartidas-desde-store.sh` ya habían pagado.
+Y el store por defecto se **detecta**: en este hub el `./store` del clon está vacío y el de verdad es
+`/store`, así que un default mudo manda a diagnosticar al lugar equivocado.
+
+Cableados: `install-image.sh`, `install-image-efi.sh`, `iso-image.sh`, `metal-usb-sdboot.sh`.
+
+**`scripts/vigia-ensamblado.sh`** — el guardián: de dónde sale cada herramienta.
+
+```
+✓ sfdisk util-linux · ✓ mke2fs e2fsprogs · ✓ bsdcpio libarchive · ✓ gzip · ✓ mformat/mcopy mtools
+✓ xorriso · ✓ mkfs.vfat dosfstools
+✗ grub (no hay receta) — grub-mkimage AUSENTE, grub-bios-setup AUSENTE
+== resumen: 8 sellada(s) · 0 del host (sin hash) · 2 ausente(s)
+```
+
+## La prueba: una imagen EFI armada EN EL HUB, que antes era imposible
+
+```
+ROOTFS=<product-rootfs> KERNEL=<linux-metal/bzImage> STAGE=/store/.efi-stage \
+  sh scripts/install-image-efi.sh        ⇒ rc=0
+==> imagen EFI creada: 297M en disco (sparse), 754M virtuales
+```
+
+Todas las herramientas del store (con su hash en el log), tabla GPT verificada con el `sfdisk`
+sellado (ESP 48M + root 512M + store 128M + estado 64M) y la ESP con las dos rutas del kernel más el
+initramfs, verificada por el propio script con `mdir`.
+
+⚠ **Y el primer intento falló con EXDEV**, que es el otro arreglo de este paso: el staging **enlaza en
+duro** desde el rootfs y un enlace no cruza un montaje aunque sea el mismo disco
+([[hardlink-cruza-mount-exdev]]). El fallo llegaba como una **pared de `cp: Invalid cross-device
+link`** —cientos de líneas— sin que ninguna dijera la causa. Ahora hay `sonda_hardlink`, FUNCIONAL
+(intenta un enlace de verdad, porque `store` y `work/out` son bind-mounts del mismo dispositivo y
+comparar `st_dev` no dispara), y dice qué montaje es cada uno:
+
+```
+✗ EXDEV: no se puede ENLAZAR de '/store/…-product-rootfs' a '/work/sergio/work/.efi-stage'.
+  origen  está en: /store       destino está en: /work/sergio
+  Poné STAGE= (y IMG=) bajo el MISMO montaje que el rootfs.
+```
+
+Es la misma sonda que `hydrate-profile.py` ya tenía; `install-image.sh` sólo tenía el comentario.
+
+## Lo que queda, y es UNA receta
+
+- **El gestor de arranque BIOS.** Dos salidas, ninguna gratis: **`grub`** (lo que los scripts ya
+  hablan) o **`limine`**, que cubre BIOS **y** UEFI de una y es mucho más chico. Es un build de
+  granja, no un cambio de script, y por eso no entra en este paso.
+- **`metal-usb-sdboot.sh` arranca con un `systemd-bootx64.efi` sacado de un rootfs de Arch del home
+  del usuario** — otro binario ajeno, sin hash y fuera del store, en el camino del metal. Anotado y
+  no tocado: esa ruta se usa para bringup con menú, y cambiarla por EFI-stub (que ya existe) es su
+  propia decisión.
+- Para cualquier objetivo **UEFI la cadena ya es soberana y no lleva gestor**: el bzImage *es* el
+  binario EFI (ADR 0018), y ahora además se puede armar acá.
diff --git a/scripts/install-image-efi.sh b/scripts/install-image-efi.sh
index 3641e34b..7763f502 100755
--- a/scripts/install-image-efi.sh
+++ b/scripts/install-image-efi.sh
@@ -60,7 +60,11 @@ ESP_MB="${ESP_MB:-64}"
 ROOT_SIZE="${ROOT_SIZE:-2048}"
 STORE_SIZE="${STORE_SIZE:-512}"
 STATE_SIZE="${STATE_SIZE:-512}"
-MTOOLS="${MTOOLS:-$(ls -dt store/*-mtools/usr/bin 2>/dev/null | head -1)}"
+# ⚠ Las herramientas del ensamblado salen del STORE, por HASH VIGENTE, no del PATH del host ni del
+# artefacto más reciente por fecha (2026-09-22). En este hub, en su anfitrión y en el worker NO existe
+# ninguna de `cpio mformat xorriso grub-mkimage`: el pipeline estaba roto en las tres máquinas y sólo
+# se veía al correrlo. Ver `scripts/lib/herramientas-desde-store.sh`.
+. "$(dirname "$0")/lib/herramientas-desde-store.sh"
 
 [ -n "${ROOTFS:-}" ] && [ -d "$ROOTFS" ] || { echo "no existe ROOTFS: '${ROOTFS:-}' (set ROOTFS=<dir product-rootfs>)" >&2; exit 1; }
 # ⚠ Esto decía `[ -e "$ROOTFS/sbin/init" ]` y era un chequeo ROTO para todo rootfs sano: `/sbin/init`
@@ -80,8 +84,21 @@ fi
 [ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (build recipes/linux-metal.toml o set KERNEL=)" >&2; exit 1; }
 BUSYBOX="${BUSYBOX:-$ROOTFS/bin/busybox}"
 [ -r "$BUSYBOX" ] || { echo "no encuentro busybox estático en $BUSYBOX (set BUSYBOX=)" >&2; exit 1; }
-[ -n "${MTOOLS:-}" ] && [ -x "$MTOOLS/mformat" ] || { echo "no encuentro mtools de hammer (build recipes/mtools.toml o set MTOOLS=)" >&2; exit 1; }
-for t in sfdisk mke2fs cpio gzip; do command -v "$t" >/dev/null 2>&1 || { echo "falta $t" >&2; exit 1; }; done
+echo "==> herramientas del ensamblado"
+MFORMAT=$(herramienta mtools usr/bin/mformat mformat) || exit 1
+MMD=$(herramienta     mtools usr/bin/mmd     mmd)     || exit 1
+MCOPY=$(herramienta   mtools usr/bin/mcopy   mcopy)   || exit 1
+MDIR=$(herramienta    mtools usr/bin/mdir    mdir)    || exit 1
+SFDISK=$(herramienta  util-linux sbin/sfdisk sfdisk)  || exit 1
+MKE2FS=$(herramienta  e2fsprogs  sbin/mke2fs mke2fs)  || exit 1
+CPIO=$(herramienta    libarchive usr/bin/bsdcpio cpio) || exit 1
+GZIP=$(herramienta    gzip bin/gzip gzip)             || exit 1
+# `MTOOLS=` sigue existiendo como override manual (un directorio con los binarios), que es como lo
+# documenta la cabecera; si se pasa, gana sobre el store.
+if [ -n "${MTOOLS:-}" ]; then
+    MFORMAT="${MTOOLS}/mformat"; MMD="${MTOOLS}/mmd"; MCOPY="${MTOOLS}/mcopy"; MDIR="${MTOOLS}/mdir"
+    [ -x "$MFORMAT" ] || { echo "MTOOLS=$MTOOLS no tiene mformat" >&2; exit 1; }
+fi
 unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; }
 
 STAGE="${STAGE:-work/.install-efi-stage}"
@@ -152,11 +169,14 @@ log "HAMMER-EFI-DISK-PIVOT-OK: switch_root → /sbin/init"
 exec switch_root /newroot /sbin/init
 INIT
 chmod +x "$IRD_TREE/init"
-( cd "$IRD_TREE" && find . -print0 | cpio --null -o -H newc --owner=root:root 2>/dev/null | gzip -1 ) > "$IRD"
+# `bsdcpio` de libarchive acepta el mismo `--null -o -H newc --owner=root:root` que el GNU cpio
+# (comprobado 2026-09-22 contra el artefacto sellado: mismo `newc`, dueños root).
+( cd "$IRD_TREE" && find . -print0 | "$CPIO" --null -o -H newc --owner=root:root 2>/dev/null | "$GZIP" -1 ) > "$IRD"
 echo "    initramfs: $(du -h "$IRD" | cut -f1)"
 
 # --- 2) árbol root: hardlinks del rootfs SIN store/estado (van en sus particiones dedicadas) -----------
 echo "==> staging root (hardlinks del rootfs, sin /store ni /var/lib/hammer)"
+sonda_hardlink "$ROOTFS" "$STAGE" || exit 1
 cp -al "$ROOTFS" "$ROOT_TREE"
 rm -rf "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
 mkdir -p "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
@@ -265,15 +285,15 @@ if [ "$_need_mb" -gt "$_esp_usable_mb" ]; then
 fi
 
 rm -f "$ESP_IMG"; truncate -s "$(( ESP_MB * 1024 * 1024 ))" "$ESP_IMG"
-"$MTOOLS/mformat" -F -v HAMMER -i "$ESP_IMG" ::
-"$MTOOLS/mmd" -i "$ESP_IMG" ::/EFI ::/EFI/BOOT ::/EFI/takana
-"$MTOOLS/mcopy" -i "$ESP_IMG" "$KERNEL" ::/EFI/takana/takanax64.efi
-"$MTOOLS/mcopy" -i "$ESP_IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
-"$MTOOLS/mcopy" -i "$ESP_IMG" "$IRD"    ::/initramfs.cpio.gz
+"$MFORMAT" -F -v HAMMER -i "$ESP_IMG" ::
+"$MMD" -i "$ESP_IMG" ::/EFI ::/EFI/BOOT ::/EFI/takana
+"$MCOPY" -i "$ESP_IMG" "$KERNEL" ::/EFI/takana/takanax64.efi
+"$MCOPY" -i "$ESP_IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
+"$MCOPY" -i "$ESP_IMG" "$IRD"    ::/initramfs.cpio.gz
 # Verificar las DOS rutas: que una exista no dice nada de la otra, y el modo de fallo que esto
 # previene es precisamente quedarse con una sola sin enterarse.
 for _p in ::/EFI/takana/takanax64.efi ::/EFI/BOOT/BOOTX64.EFI ::/initramfs.cpio.gz; do
-    "$MTOOLS/mdir" -i "$ESP_IMG" "$_p" >/dev/null 2>&1 || { echo "install-image-efi: falta $_p en la ESP" >&2; exit 1; }
+    "$MDIR" -i "$ESP_IMG" "$_p" >/dev/null 2>&1 || { echo "install-image-efi: falta $_p en la ESP" >&2; exit 1; }
 done
 echo "    ESP poblada y verificada: las dos rutas del kernel + el initramfs"
 
@@ -292,7 +312,7 @@ ESP_GUID="C12A7328-F81F-11D2-BA4B-00A0C93EC93B"   # GPT "EFI System Partition"
 
 echo "==> imagen: $IMG (GPT UEFI; ESP ${ESP_MB}M + root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)"
 rm -f "$IMG"; truncate -s "$(( TOTAL_SECT * 512 ))" "$IMG"
-sfdisk --quiet "$IMG" >/dev/null <<EOF
+"$SFDISK" --quiet "$IMG" >/dev/null <<EOF
 label: gpt
 start=$START1, size=$ESP_SECT,   type=$ESP_GUID, name="EFI System Partition"
 start=$START2, size=$ROOT_SECT,  type=linux,     name="hammer-root"
@@ -303,9 +323,9 @@ EOF
 # --- 5) poblar las 3 ext4 (root-owned vía unshare -r) y empalmar ESP + ext4 en sus offsets ----------
 echo "==> mke2fs -d + empalme (ESP con mtools ya lista; ext4 bajo unshare -r)"
 unshare -r sh -eu <<EOF
-mke2fs -q -t ext4 -L hammer-root  -E root_owner=0:0 -d "$ROOT_TREE"             "$ROOT_IMG"  ${ROOT_SIZE}M
-mke2fs -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store"          "$STORE_IMG" ${STORE_SIZE}M
-mke2fs -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer" "$STATE_IMG" ${STATE_SIZE}M
+"$MKE2FS" -q -t ext4 -L hammer-root  -E root_owner=0:0 -d "$ROOT_TREE"             "$ROOT_IMG"  ${ROOT_SIZE}M
+"$MKE2FS" -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store"          "$STORE_IMG" ${STORE_SIZE}M
+"$MKE2FS" -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer" "$STATE_IMG" ${STATE_SIZE}M
 EOF
 dd if="$ESP_IMG"   of="$IMG" bs=512 seek=$START1 conv=sparse,notrunc status=none
 dd if="$ROOT_IMG"  of="$IMG" bs=512 seek=$START2 conv=sparse,notrunc status=none
diff --git a/scripts/install-image.sh b/scripts/install-image.sh
index 7bb2307f..46a10fd4 100755
--- a/scripts/install-image.sh
+++ b/scripts/install-image.sh
@@ -57,8 +57,28 @@ GRUB_LIB="${GRUB_LIB:-/usr/lib/grub/i386-pc}"
 [ -e "$ROOTFS/sbin/init" ] || [ -L "$ROOTFS/sbin/init" ] || { echo "ROOTFS sin /sbin/init (arje-zero)" >&2; exit 1; }
 [ -n "$KERNEL" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel a embeber (set KERNEL=<bzImage>)" >&2; exit 1; }
 [ -d "$GRUB_LIB" ] && [ -r "$GRUB_LIB/boot.img" ] || { echo "faltan los módulos GRUB i386-pc en $GRUB_LIB" >&2; exit 1; }
-for t in sfdisk mke2fs grub-mkimage grub-bios-setup; do
-    command -v "$t" >/dev/null 2>&1 || { echo "falta $t" >&2; exit 1; }
+# ⚠ Las herramientas del ensamblado salen del STORE por HASH VIGENTE (2026-09-22). Medido: en este
+# hub, en su anfitrión y en el worker NO existe `grub-mkimage` ni `grub-bios-setup`, así que ESTA RAMA
+# no se podía correr en ninguna de las tres máquinas — y sólo se veía al intentarlo.
+. "$(dirname "$0")/lib/herramientas-desde-store.sh"
+echo "==> herramientas del ensamblado"
+SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
+MKE2FS=$(herramienta e2fsprogs  sbin/mke2fs mke2fs) || exit 1
+# GRUB es el ÚNICO que no puede salir del store: no hay `recipes/grub.toml`. Y no se puede retirar la
+# rama BIOS «porque ya tenemos EFI-stub»: **Hetzner Cloud arranca por BIOS** —medido en gioser, no
+# existe `/sys/firmware/efi` (SDD 28 §3.1)—, o sea que la imagen del servidor de producción es
+# justamente la que lo necesita. Mientras no haya receta, esta rama depende de un binario ajeno sin
+# hash y el aviso tiene que ser explícito, no un `falta grub-mkimage` que se lee como un olvido.
+for t in grub-mkimage grub-bios-setup; do
+    command -v "$t" >/dev/null 2>&1 || {
+        echo "   ✗ falta $t, y NO hay receta de grub en el corpus." >&2
+        echo "     Esta rama (GPT+BIOS) es la que necesita Hetzner Cloud, que arranca por BIOS." >&2
+        echo "     Alternativas, ninguna gratis: instalar grub en la máquina que ensambla, o escribir" >&2
+        echo "     la receta del gestor de arranque (grub, o limine que cubre BIOS y UEFI de una)." >&2
+        echo "     Para un objetivo UEFI la rama soberana ya existe: scripts/install-image-efi.sh." >&2
+        exit 1
+    }
+    echo "   ⚠ ${t} ← $(command -v "$t") DEL HOST, sin hash: el gestor de arranque no es nuestro" >&2
 done
 unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; }
 
@@ -77,6 +97,7 @@ GRUB_STAGE="$STAGE/grub"
 
 # --- árbol root: hardlinks del rootfs SIN store/estado + /boot (kernel + GRUB) + wrapper /sbin/init ---
 echo "==> staging root: rootfs (hardlinks) + /boot/bzImage + /boot/grub + wrapper /sbin/init"
+sonda_hardlink "$ROOTFS" "$STAGE" || exit 1
 cp -al "$ROOTFS" "$ROOT_TREE"
 rm -rf "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
 mkdir -p "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer" "$ROOT_TREE/boot/grub/i386-pc"
@@ -206,7 +227,7 @@ else
     rm -f "$IMG"
     truncate -s "$TOTAL_BYTES" "$IMG"
 fi
-sfdisk --quiet "$IMG" >/dev/null <<EOF
+"$SFDISK" --quiet "$IMG" >/dev/null <<EOF
 label: gpt
 start=$START1, size=$BIOS_SECT,  type=$BIOS_GUID, name="bios-boot"
 start=$START2, size=$ESP_SECT,   type=uefi,       name="hammer-esp"
@@ -218,9 +239,9 @@ EOF
 # --- poblar las 3 particiones ext4 (root-owned vía unshare -r) y empalmar en su offset ---
 echo "==> mke2fs + empalme de las particiones ext4 (unshare -r, sin sudo)"
 unshare -r sh -eu <<EOF
-mke2fs -q -t ext4 -L hammer-root  -E root_owner=0:0 -d "$ROOT_TREE"               "$ROOT_IMG"  ${ROOT_SIZE}M
-mke2fs -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store"            "$STORE_IMG" ${STORE_SIZE}M
-mke2fs -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer"   "$STATE_IMG" ${STATE_SIZE}M
+"$MKE2FS" -q -t ext4 -L hammer-root  -E root_owner=0:0 -d "$ROOT_TREE"               "$ROOT_IMG"  ${ROOT_SIZE}M
+"$MKE2FS" -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store"            "$STORE_IMG" ${STORE_SIZE}M
+"$MKE2FS" -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer"   "$STATE_IMG" ${STATE_SIZE}M
 dd if="$ROOT_IMG"  of="$IMG" bs=512 seek=$START3 conv=sparse,notrunc status=none
 dd if="$STATE_IMG" of="$IMG" bs=512 seek=$START4 conv=sparse,notrunc status=none
 dd if="$STORE_IMG" of="$IMG" bs=512 seek=$START5 conv=sparse,notrunc status=none
diff --git a/scripts/iso-image.sh b/scripts/iso-image.sh
index 3de3ecb0..0ebd701a 100755
--- a/scripts/iso-image.sh
+++ b/scripts/iso-image.sh
@@ -37,19 +37,27 @@ KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}"
 GRUB_LIB="${GRUB_LIB:-/usr/lib/grub/i386-pc}"
 GRUB_EFI_LIB="${GRUB_EFI_LIB:-/usr/lib/grub/x86_64-efi}"
 CMDLINE="${CMDLINE:-console=ttyS0 rdinit=/init}"
-# xorriso: preferimos el construido por takana (dogfooding de recipes/xorriso.toml); fallback al host.
-XORRISO="${XORRISO:-$(ls store/*-xorriso/usr/bin/xorriso 2>/dev/null | head -1)}"
-[ -n "$XORRISO" ] && [ -x "$XORRISO" ] || XORRISO="$(command -v xorriso 2>/dev/null || true)"
-# mtools (mformat/mcopy): para poblar la ESP FAT del arranque EFI. Preferimos el de takana (dogfooding).
-MTOOLS_DIR="${MTOOLS_DIR:-$(ls -d store/*-mtools/usr/bin 2>/dev/null | head -1)}"
+# ⚠ Las herramientas salen del STORE por HASH VIGENTE, no por el primer glob ni por fecha
+# (2026-09-22): con dos artefactos del mismo paquete conviviendo, el más reciente puede ser el viejo.
+# Ver `scripts/lib/herramientas-desde-store.sh`.
+. "$(dirname "$0")/lib/herramientas-desde-store.sh"
 
 [ -d "$ROOTFS" ] || { echo "no existe ROOTFS: $ROOTFS" >&2; exit 1; }
 [ -e "$ROOTFS/sbin/init" ] || { echo "ROOTFS sin /sbin/init" >&2; exit 1; }
 [ -n "$KERNEL" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel (set KERNEL=<bzImage>)" >&2; exit 1; }
-[ -n "$XORRISO" ] || { echo "no encuentro xorriso (build recipes/xorriso.toml)" >&2; exit 1; }
-[ -d "$GRUB_LIB" ] && [ -r "$GRUB_LIB/cdboot.img" ] || { echo "faltan módulos GRUB i386-pc en $GRUB_LIB" >&2; exit 1; }
-command -v grub-mkimage >/dev/null 2>&1 || { echo "falta grub-mkimage" >&2; exit 1; }
-command -v cpio >/dev/null 2>&1 || { echo "falta cpio" >&2; exit 1; }
+echo "==> herramientas del ensamblado"
+XORRISO="${XORRISO:-$(herramienta xorriso usr/bin/xorriso xorriso)}" || exit 1
+CPIO=$(herramienta libarchive usr/bin/bsdcpio cpio) || exit 1
+GZIP=$(herramienta gzip bin/gzip gzip) || exit 1
+MFORMAT=$(herramienta mtools usr/bin/mformat mformat) || exit 1
+MCOPY=$(herramienta   mtools usr/bin/mcopy   mcopy)   || exit 1
+MMD=$(herramienta     mtools usr/bin/mmd     mmd)     || exit 1
+# GRUB es el único que NO puede salir del store: no hay `recipes/grub.toml`. El ISO arranca por El
+# Torito (BIOS), así que sin él no hay ISO. La rama UEFI de este mismo script y `metal-iso.sh
+# EFI_STUB=1` no lo necesitan — ahí el bzImage ES el binario EFI (ADR 0018).
+[ -d "$GRUB_LIB" ] && [ -r "$GRUB_LIB/cdboot.img" ] || { echo "faltan módulos GRUB i386-pc en $GRUB_LIB — y NO hay receta de grub en el corpus (ver scripts/vigia-ensamblado.sh)" >&2; exit 1; }
+command -v grub-mkimage >/dev/null 2>&1 || { echo "falta grub-mkimage, y NO hay receta de grub: el ISO BIOS depende de un binario del host sin hash" >&2; exit 1; }
+echo "   ⚠ grub-mkimage ← $(command -v grub-mkimage) DEL HOST, sin hash: el gestor de arranque no es nuestro" >&2
 
 echo "==> xorriso: $XORRISO ($("$XORRISO" -version 2>&1 | grep -m1 'xorriso version' || echo '?'))"
 
@@ -138,7 +146,7 @@ if [ "${INSTALLER:-0}" = 1 ]; then
 fi
 
 echo "==> empaquetando initramfs.cpio.gz"
-( cd "$RFS/root" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$ISO_ROOT/boot/initramfs.cpio.gz"
+( cd "$RFS/root" && find . -print0 | "$CPIO" --null -o -H newc -R 0:0 2>/dev/null | "$GZIP" -1 ) > "$ISO_ROOT/boot/initramfs.cpio.gz"
 
 # --- kernel + módulos GRUB + grub.cfg en el árbol del ISO ---
 cp "$KERNEL" "$ISO_ROOT/boot/bzImage"; chmod 0644 "$ISO_ROOT/boot/bzImage"
@@ -167,7 +175,7 @@ grub-mkimage -O i386-pc-eltorito -p '/boot/grub' -o "$ISO_ROOT/boot/grub/i386-pc
 #     EFI_STUB; aquí GRUB EFI es el cargador (espeja la rama BIOS). xorriso añade un 2º El Torito (EFI). ---
 EFI_ARGS=""
 if [ "${EFI:-0}" = 1 ]; then
-    MFORMAT="$MTOOLS_DIR/mformat"; MCOPY="$MTOOLS_DIR/mcopy"; MMD="$MTOOLS_DIR/mmd"
+    [ -n "${MTOOLS_DIR:-}" ] && { MFORMAT="$MTOOLS_DIR/mformat"; MCOPY="$MTOOLS_DIR/mcopy"; MMD="$MTOOLS_DIR/mmd"; }
     [ -x "$MFORMAT" ] && [ -x "$MCOPY" ] || MFORMAT="$(command -v mformat 2>/dev/null || true)"
     [ -n "$MFORMAT" ] && [ -x "$MFORMAT" ] || { echo "no encuentro mtools (build recipes/mtools.toml)" >&2; exit 1; }
     [ -x "$MCOPY" ] || { MCOPY="$(command -v mcopy)"; MMD="$(command -v mmd)"; }
diff --git a/scripts/lib/herramientas-desde-store.sh b/scripts/lib/herramientas-desde-store.sh
new file mode 100644
index 00000000..5cbc3c2d
--- /dev/null
+++ b/scripts/lib/herramientas-desde-store.sh
@@ -0,0 +1,107 @@
+# herramientas-desde-store.sh — las herramientas que ENSAMBLAN una imagen salen del STORE, no del host.
+#
+# ══ POR QUÉ EXISTE (medido el 2026-09-22) ══════════════════════════════════════════════════════
+# Armar una imagen necesita `sfdisk mke2fs cpio gzip mformat mcopy xorriso` y, en la rama BIOS,
+# `grub-mkimage`/`grub-bios-setup`. Los scripts los tomaban del `PATH` del host — y **en este hub, en
+# su anfitrión y en el worker no existe NINGUNO de ellos**:
+#
+#   grub-mkimage AUSENTE · grub-bios-setup AUSENTE · cpio AUSENTE · mformat AUSENTE · xorriso AUSENTE
+#
+# O sea que el pipeline de imagen estaba roto en las tres máquinas que tenemos, en silencio, hasta que
+# alguien lo corriera. Y lo absurdo es que **seis de las siete son nuestras**: `util-linux` (sfdisk),
+# `e2fsprogs` (mke2fs), `libarchive` (bsdcpio), `gzip`, `mtools`, `xorriso` y `dosfstools` están
+# SELLADAS en el store, son estáticas musl y corren tal cual en el host. Lo único que no tenemos es
+# el gestor de arranque de la rama BIOS (ver `vigia-ensamblado.sh`).
+#
+# ── EL ARTEFACTO SE ELIGE POR HASH, NO POR FECHA ─────────────────────────────────────────────────
+# `iso-image.sh` resolvía `xorriso` con `ls store/*-xorriso/...|head -1` y `install-image-efi.sh`
+# mtools con `ls -dt ...|head -1`. Con dos artefactos del mismo paquete conviviendo —lo normal tras
+# cualquier re-hash— el más reciente puede ser el viejo: **la fecha miente; el hash no**. Es la misma
+# lección que `hydrate-profile.py` y `libs-compartidas-desde-store.sh` ya habían pagado.
+#
+# ── EL HOST ES CAÍDA, NO FUENTE ─────────────────────────────────────────────────────────────────
+# Si no está en el store se cae al host, pero **avisando**: un binario del host no tiene hash, así que
+# la imagen deja de ser reproducible por ese lado y eso tiene que verse en el log de la corrida, no
+# descubrirse después. Si no está en ninguno de los dos, se ABORTA con las dos rutas nombradas.
+#
+# Uso:  . scripts/lib/herramientas-desde-store.sh
+#       SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
+#       CPIO=$(herramienta libarchive usr/bin/bsdcpio cpio)  || exit 1
+# Vars: STORE_ART  (dónde buscar; por defecto se detecta) · TAKANA (binario; por defecto el del árbol)
+
+_herr_store() {
+    # Se detecta UNA vez y se dice cuál se eligió: en el hub el `./store` del clon está vacío y el
+    # store de verdad es `/store`, así que un default mudo manda a diagnosticar al lugar equivocado.
+    [ -n "${STORE_ART:-}" ] && { echo "$STORE_ART"; return; }
+    [ -n "${HAMMER_STORE:-}" ] && { echo "$HAMMER_STORE"; return; }
+    for c in store /store; do
+        if [ -d "$c" ] && [ -n "$(find "$c" -maxdepth 1 -name '*-*' -print -quit 2>/dev/null)" ]; then
+            echo "$c"; return
+        fi
+    done
+    echo store
+}
+
+_herr_takana() {
+    for c in "${TAKANA:-}" ./target/release/takana /usr/bin/takana; do
+        [ -n "$c" ] && [ -x "$c" ] && { echo "$c"; return; }
+    done
+    command -v takana 2>/dev/null || echo ./target/release/takana
+}
+
+# herramienta <receta> <ruta-relativa-en-el-artefacto> [nombre-en-el-host]
+herramienta() {
+    _hr_receta="$1"; _hr_rel="$2"; _hr_host="${3:-}"
+    _hr_store="$(_herr_store)"; _hr_tk="$(_herr_takana)"
+    _hr_hash=$("$_hr_tk" --store "$_hr_store" hash "recipes/$_hr_receta.toml" 2>/dev/null \
+               | tail -1 | sed 's/^b3://')
+    if [ -n "$_hr_hash" ]; then
+        _hr_p="$_hr_store/$_hr_hash-$_hr_receta/$_hr_rel"
+        if [ -x "$_hr_p" ]; then
+            echo "   · ${_hr_rel##*/} ← $_hr_receta $(echo "$_hr_hash" | cut -c1-12) (sellado)" >&2
+            echo "$_hr_p"; return 0
+        fi
+    fi
+    if [ -n "$_hr_host" ] && command -v "$_hr_host" >/dev/null 2>&1; then
+        echo "   ⚠ ${_hr_rel##*/} ← $(command -v "$_hr_host") DEL HOST, sin hash: por acá la imagen no es reproducible" >&2
+        command -v "$_hr_host"; return 0
+    fi
+    echo "   ✗ falta ${_hr_rel##*/}: ni sellado ($_hr_store/$_hr_hash-$_hr_receta/$_hr_rel) ni en el PATH ($_hr_host)" >&2
+    echo "     construilo con: flock work/.farm-build.lock ./target/release/takana --store $_hr_store build recipes/$_hr_receta.toml" >&2
+    return 1
+}
+
+# ── sonda_hardlink <dir-origen> <dir-destino> ───────────────────────────────────────────────────
+# El staging de una imagen ENLAZA (hardlink) desde el rootfs, y `linkat()` no cruza un punto de
+# montaje aunque sea el mismo disco ([[hardlink-cruza-mount-exdev]]). Sin sonda, el fallo llega como
+# una PARED de `cp: Invalid cross-device link` fichero por fichero —cientos de líneas— y el diagnóstico
+# real («el STAGE está en otro montaje que el ROOTFS») no aparece en ninguna de ellas. Medido el
+# 2026-09-22 armando una imagen EFI con el rootfs en /store y el stage en /work.
+#
+# La sonda es FUNCIONAL —se intenta un enlace de verdad— y no una comparación de `st_dev`: en este
+# repo `store` y `work/out` son bind-mounts del MISMO dispositivo y `st_dev` coincide, así que
+# comparar números no dispara. Es la misma sonda que `hydrate-profile.py` ya tenía.
+sonda_hardlink() {
+    _sh_src="$1"; _sh_dst="$2"
+    mkdir -p "$_sh_dst" 2>/dev/null || true
+    _sh_a="$_sh_src/.sonda-hardlink.$$"; _sh_b="$_sh_dst/.sonda-hardlink.$$"
+    if ! : > "$_sh_a" 2>/dev/null; then
+        # origen de sólo lectura (un artefacto del store): se prueba con un fichero que YA está
+        _sh_a=$(find "$_sh_src" -maxdepth 2 -type f -print -quit 2>/dev/null)
+        [ -n "$_sh_a" ] || { echo "sonda_hardlink: $_sh_src no tiene ningún fichero con el que probar" >&2; return 1; }
+        _sh_limpiar=no
+    else
+        _sh_limpiar=si
+    fi
+    if ln "$_sh_a" "$_sh_b" 2>/dev/null; then
+        rm -f "$_sh_b"; [ "$_sh_limpiar" = si ] && rm -f "$_sh_a"
+        return 0
+    fi
+    [ "$_sh_limpiar" = si ] && rm -f "$_sh_a"
+    echo "   ✗ EXDEV: no se puede ENLAZAR de '$_sh_src' a '$_sh_dst'." >&2
+    echo "     El staging usa hardlinks y un enlace no cruza un montaje aunque sea el mismo disco." >&2
+    echo "     origen  está en: $(findmnt -n -T "$_sh_src" -o TARGET 2>/dev/null || echo '?')" >&2
+    echo "     destino está en: $(findmnt -n -T "$_sh_dst" -o TARGET 2>/dev/null || echo '?')" >&2
+    echo "     Poné STAGE= (y IMG=) bajo el MISMO montaje que el rootfs." >&2
+    return 1
+}
diff --git a/scripts/metal-usb-sdboot.sh b/scripts/metal-usb-sdboot.sh
index d9a97c53..013c446f 100755
--- a/scripts/metal-usb-sdboot.sh
+++ b/scripts/metal-usb-sdboot.sh
@@ -25,8 +25,25 @@ KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage 2>/dev/null | head -
 [ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro kernel metal" >&2; exit 1; }
 SDBOOT="${SDBOOT:-/home/sergio/.local/share/shuma/rootfs/arch/usr/lib/systemd/boot/efi/systemd-bootx64.efi}"
 [ -r "$SDBOOT" ] || { echo "no encuentro systemd-bootx64.efi en $SDBOOT" >&2; exit 1; }
-MT=$(ls -d store/*-mtools/usr/bin 2>/dev/null | head -1)
-[ -n "$MT" ] && [ -x "$MT/mformat" ] || { echo "no encuentro mtools de hammer (build recipes/mtools.toml)" >&2; exit 1; }
+# Las herramientas salen del STORE por HASH VIGENTE (2026-09-22): `cpio` no existe en ninguna de las
+# tres máquinas del proyecto y mtools se resolvía por el primer glob, no por el hash de la receta de
+# hoy. Ver `scripts/lib/herramientas-desde-store.sh` y `scripts/vigia-ensamblado.sh`.
+. "$(dirname "$0")/lib/herramientas-desde-store.sh"
+MFORMAT=$(herramienta mtools usr/bin/mformat mformat) || exit 1
+MCOPY=$(herramienta   mtools usr/bin/mcopy   mcopy)   || exit 1
+MMD=$(herramienta     mtools usr/bin/mmd     mmd)     || exit 1
+MDIR=$(herramienta    mtools usr/bin/mdir    mdir)    || exit 1
+CPIO=$(herramienta libarchive usr/bin/bsdcpio cpio)   || exit 1
+GZIP=$(herramienta gzip bin/gzip gzip)                || exit 1
+if [ -n "${MT:-}" ]; then
+    MFORMAT="${MT}/mformat"; MCOPY="${MT}/mcopy"; MMD="${MT}/mmd"; MDIR="${MT}/mdir"
+    [ -x "$MFORMAT" ] || { echo "MT=$MT no tiene mformat" >&2; exit 1; }
+fi
+# ⚠ Y el GESTOR DE ARRANQUE de esta ruta tampoco es nuestro: `SDBOOT` apunta a un
+# `systemd-bootx64.efi` sacado de un rootfs de Arch en el home del usuario — un binario ajeno, sin
+# hash y fuera del store, en el camino de arranque del metal. La alternativa soberana ya existe y no
+# lleva gestor (`install-image-efi.sh`: el bzImage ES el binario EFI, ADR 0018). Anotado, no tocado:
+# esta ruta se usa para bringup con menú y cambiarla es su propia decisión.
 IMG="${IMG:-work/hammer-metal-usb.img}"
 # /dev/console=tty0 al FINAL ⇒ la getty de arje da shell en la PANTALLA. earlyprintk=efi para diagnóstico.
 OPTIONS="${OPTIONS:-earlyprintk=efi,keep ignore_loglevel efi=novamap console=ttyS0,115200 console=tty0}"
@@ -50,7 +67,7 @@ chmod +x "$STG/init"
 
 # 2) empaquetar initramfs.cpio.gz (root-owned, igual que iso-image.sh)
 echo "==> empaquetando initramfs.cpio.gz"
-( cd "$STG" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz"
+( cd "$STG" && find . -print0 | "$CPIO" --null -o -H newc -R 0:0 2>/dev/null | "$GZIP" -1 ) > "$WORK/initramfs.cpio.gz"
 IRD="$WORK/initramfs.cpio.gz"; echo "    initramfs: $(du -h "$IRD" | cut -f1)"
 
 # 3) ESP FAT32: BOOTX64.EFI=systemd-boot + loader.conf + entry + kernel + initrd
@@ -59,17 +76,17 @@ espkb=$(( ( $(stat -c%s "$KERNEL") + $(stat -c%s "$IRD") ) / 1024 + 16384 ))
 # no la lee ("Not Found"). Piso de 48 MiB para que el FAT32 siempre sea válido.
 [ "$espkb" -lt 49152 ] && espkb=49152
 ESP="$WORK/esp.img"; rm -f "$ESP"; truncate -s "$(( espkb * 1024 ))" "$ESP"
-"$MT/mformat" -F -v HAMMER -i "$ESP" ::
-"$MT/mmd" -i "$ESP" ::/EFI ::/EFI/BOOT ::/loader ::/loader/entries
-"$MT/mcopy" -i "$ESP" "$SDBOOT" ::/EFI/BOOT/BOOTX64.EFI
+"$MFORMAT" -F -v HAMMER -i "$ESP" ::
+"$MMD" -i "$ESP" ::/EFI ::/EFI/BOOT ::/loader ::/loader/entries
+"$MCOPY" -i "$ESP" "$SDBOOT" ::/EFI/BOOT/BOOTX64.EFI
 printf 'default hammer\ntimeout 8\neditor yes\n' > "$WORK/loader.conf"
 printf 'title   hammer (metal)\nlinux   /bzImage\ninitrd  /initramfs.cpio.gz\noptions %s\n' "$OPTIONS" > "$WORK/hammer.conf"
-"$MT/mcopy" -i "$ESP" "$WORK/loader.conf"   ::/loader/loader.conf
-"$MT/mcopy" -i "$ESP" "$WORK/hammer.conf"   ::/loader/entries/hammer.conf
-"$MT/mcopy" -i "$ESP" "$KERNEL"             ::/bzImage
-"$MT/mcopy" -i "$ESP" "$IRD"                ::/initramfs.cpio.gz
+"$MCOPY" -i "$ESP" "$WORK/loader.conf"   ::/loader/loader.conf
+"$MCOPY" -i "$ESP" "$WORK/hammer.conf"   ::/loader/entries/hammer.conf
+"$MCOPY" -i "$ESP" "$KERNEL"             ::/bzImage
+"$MCOPY" -i "$ESP" "$IRD"                ::/initramfs.cpio.gz
 echo "==> ESP poblada ($(du -h "$ESP" | cut -f1)):"
-"$MT/mdir" -i "$ESP" ::/ ::/EFI/BOOT ::/loader/entries 2>/dev/null | grep -iE 'BOOTX64|bzImage|initram|hammer|loader' || true
+"$MDIR" -i "$ESP" ::/ ::/EFI/BOOT ::/loader/entries 2>/dev/null | grep -iE 'BOOTX64|bzImage|initram|hammer|loader' || true
 
 # 4) imagen de disco GPT: GPT primaria (1MiB) + ESP (en sector 2048) + GPT backup
 esp_sectors=$(( espkb * 2 ))                       # KiB→sectores de 512
@@ -83,7 +100,7 @@ DATA_LABEL="${DATA_LABEL:-HAMMERLOG}"
 data_sectors=0
 if [ "$DATA_MB" -gt 0 ]; then
   DATA="$WORK/data.img"; rm -f "$DATA"; truncate -s "$(( DATA_MB * 1024 * 1024 ))" "$DATA"
-  "$MT/mformat" -F -v "$DATA_LABEL" -i "$DATA" ::
+  "$MFORMAT" -F -v "$DATA_LABEL" -i "$DATA" ::
   data_sectors=$(( DATA_MB * 2048 ))
   echo "==> partición de datos FAT32 ${DATA_MB}M etiqueta=$DATA_LABEL (telemetría persistente)"
 fi
diff --git a/scripts/vigia-ensamblado.sh b/scripts/vigia-ensamblado.sh
new file mode 100755
index 00000000..0983f8e4
--- /dev/null
+++ b/scripts/vigia-ensamblado.sh
@@ -0,0 +1,75 @@
+#!/bin/sh
+# vigia-ensamblado.sh — ¿de dónde sale cada herramienta que ARMA una imagen: del store o del host?
+#
+# ══ EL PUNTO CIEGO QUE CUBRE (medido el 2026-09-22) ═════════════════════════════════════════════
+# Los tres scripts de imagen tomaban sus herramientas del `PATH`, y en las TRES máquinas que tenemos
+# —este hub, su anfitrión y el worker `dev.gioser.net`— no existe ninguna de
+# `grub-mkimage grub-bios-setup cpio mformat xorriso`. O sea: **el pipeline de imagen estaba roto en
+# todas partes y sólo se veía al intentar correrlo**, con un `falta cpio` que se lee como un descuido
+# de la máquina y no como lo que era: la imagen dependiendo de software que nadie declaró.
+#
+# Lo absurdo del caso es que SEIS de las siete son nuestras y están selladas: `util-linux` (sfdisk),
+# `e2fsprogs` (mke2fs), `libarchive` (bsdcpio), `gzip`, `mtools`, `xorriso`. Todas estáticas musl, y
+# corren tal cual en el host — comprobado ejecutándolas, no supuesto.
+#
+# ⚠ **La séptima es el gestor de arranque, y no se puede resolver retirando la rama BIOS.** Hetzner
+# Cloud **arranca por BIOS** (medido en gioser: no existe `/sys/firmware/efi`, SDD 28 §3.1), así que
+# la imagen del servidor de producción es justamente la que necesita GRUB. Para objetivos UEFI la
+# rama soberana ya existe y no lleva gestor: el bzImage ES el binario EFI (ADR 0018).
+#
+#   uso:  scripts/vigia-ensamblado.sh [--fail]
+#         --fail  → exit 1 si alguna herramienta no sale del store (para CI)
+set -eu
+cd "$(dirname "$0")/.."
+. scripts/lib/herramientas-desde-store.sh
+
+FALLAR=0; [ "${1:-}" = "--fail" ] && FALLAR=1
+sellados=0; host=0; faltan=0
+
+mirar() {  # mirar <receta> <ruta-rel> <nombre-host> <para qué>
+    if p=$(herramienta "$1" "$2" "$3" 2>/dev/null); then
+        case "$p" in
+            /store/*|store/*|*/store/*) printf '  ✓ %-12s %-22s %s\n' "${2##*/}" "$1" "sellado"; sellados=$((sellados+1)) ;;
+            *)                          printf '  ⚠ %-12s %-22s %s\n' "${2##*/}" "$1" "DEL HOST ($p) — sin hash"; host=$((host+1)) ;;
+        esac
+    else
+        printf '  ✗ %-12s %-22s %s\n' "${2##*/}" "$1" "NO ESTÁ: ni sellado ni en el host"; faltan=$((faltan+1))
+    fi
+}
+
+echo "== herramientas del ensamblado de imágenes (store: $(_herr_store))"
+mirar util-linux sbin/sfdisk      sfdisk   "particionar"
+mirar e2fsprogs  sbin/mke2fs      mke2fs   "las ext4 con -d, sin root"
+mirar libarchive usr/bin/bsdcpio  cpio     "el initramfs newc"
+mirar gzip       bin/gzip         gzip     "comprimir el initramfs"
+mirar mtools     usr/bin/mformat  mformat  "poblar la ESP FAT sin montar"
+mirar mtools     usr/bin/mcopy    mcopy    "idem"
+mirar xorriso    usr/bin/xorriso  xorriso  "el ISO"
+mirar dosfstools sbin/mkfs.vfat   mkfs.vfat "vfat, cuando hace falta"
+
+echo
+echo "== el gestor de arranque, que es el hueco de verdad"
+if [ -f recipes/grub.toml ]; then
+    echo "  (hay recipes/grub.toml: actualizá este vigía)"
+else
+    printf '  ✗ %-12s %-22s %s\n' "grub" "(no hay receta)" "sólo del host"
+    for t in grub-mkimage grub-bios-setup; do
+        if command -v "$t" >/dev/null 2>&1; then
+            printf '    ⚠ %-18s %s DEL HOST, sin hash\n' "$t" "$(command -v "$t")"; host=$((host+1))
+        else
+            printf '    ✗ %-18s AUSENTE ⇒ la rama BIOS (install-image.sh, iso-image.sh) NO puede correr acá\n' "$t"
+            faltan=$((faltan+1))
+        fi
+    done
+    echo "    Hetzner Cloud arranca por BIOS (medido, SDD 28 §3.1) ⇒ la rama no se puede retirar."
+    echo "    Las dos salidas, ninguna gratis: receta de \`grub\`, o receta de \`limine\` (cubre BIOS"
+    echo "    y UEFI de una). Para un objetivo UEFI la rama soberana ya está: install-image-efi.sh."
+fi
+
+echo
+echo "== resumen: $sellados sellada(s) · $host del host (sin hash) · $faltan ausente(s)"
+[ "$faltan" -gt 0 ] && echo "   Una imagen que necesita algo ausente NO se puede armar en esta máquina."
+if [ "$FALLAR" = 1 ] && { [ "$faltan" -gt 0 ] || [ "$host" -gt 0 ]; }; then
+    exit 1
+fi
+exit 0