tawasuyu

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 1373Shell 116Rust 74Markdown 641915 Sutichaykuna

feat(firmware): el firmware de una PC x86 COMPLETO, y el hotplug declarado en base

Retira `firmware-intel` (publicada unas horas antes, hoy mismo) y la reemplaza por
`firmware-metal`. El operador dijo por que: «que ni ethernet funciona. no sé por qué me lo pones
tan pobre, como si no supieras lo que necesita un sistema». Tenia razon y el error era de metodo:
recorte por tamanio ANTES de medir. Con linux-firmware ya sellado se puede medir, y lo que una PC
puede necesitar es mas de la mitad del tarball.

firmware-metal, con el peso medido familia por familia (du -sh sobre el artefacto):
  WIFI 641M      iwlwifi 287 · mrvl 81 · mediatek 71 · ath11k 70 · ath12k 47 · rtw89 34
                 ath10k 20 · brcm 12 · ti-connectivity 9 · cypress 6 · libertas 2 · rtlwifi 1
                 rtw88 0,9 · ath9k_htc 0,1
  BLUETOOTH 33M  intel/ibt-* 27 · qca 5 · rtl_bt 1,5
  ETHERNET 8M    cxgb4 4,6 · bnx2x 2,6 · rtl_nic 0,3 · bnx2 · cxgb3 · qlogic · tigon
  GRAFICOS 309M  nvidia 154 · amdgpu 115 · i915 27 · radeon 7,4 · xe 5,4
  PORTATIL 49M   intel/ipu (camara IPU6) · intel/vsc · intel/ish (por el ISH pasa el touchpad
                 I2C de muchos portatiles nuevos)
  + cirrus, el SOF de la dep, y regulatory.db
  SELLADO: 5041 entradas, 1047 MiB, 203 enlaces de raiz reconstruidos.

Y corrige el error mas caro de `firmware-intel`, que decia que amdgpu/radeon quedaban afuera
«por un motivo tecnico: takana pinta por mesa-swrast, software puro». Eso esta MAL: sin su
firmware amdgpu NO INICIALIZA el dispositivo, y sin dispositivo no hay KMS ni /dev/dri/card0 — el
compositor no tiene a que abrirle nada y la pantalla queda muerta. Pintar por software no evita
necesitar que la GPU encienda. Idem nouveau con el GSP de las NVIDIA nuevas.

Lo que queda afuera y no es olvido: qcom 525M (SoC ARM, ninguna PC x86 lo pide), netronome 140M
+ mellanox 107M + qed 20M + liquidio 5M (NIC de datacenter de 25/100G), intel/qat + intel/vpu
(cripto y NPU: no hacen arrancar nada). Cada una es otra derivada de dos lineas sobre la misma
linux-firmware — asi esta pensado el mecanismo.

uevent-relay: la receta del reenviador de uevents (ver 5b761c17) con su [[service]], para que
arje lo levante y lo vuelva a levantar. Backoff corto a proposito: si muere, el hotplug de la
maquina muere con el y el sintoma es intermitente, que es peor que roto.

Los dos en [perfil.base] y no en un perfil de escritorio: las dos cosas son de la MAQUINA — un
servidor tambien necesita su NIC, y el greeter necesita que un teclado enchufado aparezca antes
de que haya nadie dentro. base pasa de 49 a 51 paquetes, verificado con targets.py.

⚠ firmware-metal multiplica por cuatro el medio (el product-rootfs pesa 264-358 MiB). Es una
decision tomada con el peso delante: la alternativa dejaba sin red a cualquier maquina con wifi
Realtek o Atheros, y sin pantalla a cualquiera con GPU AMD o NVIDIA. Para una imagen que ya sabe
en que maquina corre siguen estando `firmware-tigerlake` y su perfil `metal-tigerlake`.

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

0c84ec1f587481eb12fbbd9795f8248158f57eb4 · ruwaq sergio · (7 p'unchaw ñawpaq) · tayta 5b761c175 · kay commitpi sach'ata qhaway

Semántico sayk'uy

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

6 qillqakuna · +237 −138

diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index aca1e766..7241ce2d 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -286,6 +286,21 @@ paquetes = [
   # sellaron con akllay como /usr/bin/awk del lab **con el hash idéntico** al vigente. Brush hacía
   # 0 de 3. Ver `docs/plan-botar-busybox.md` §akllay.
   "akllay",
+
+  # ── QUE LA MAQUINA TENGA RED Y VEA LO QUE SE ENCHUFA (2026-10-15) ──────────────────────────────
+  # Las dos salen del mismo reporte en metal: «que ni ethernet funciona» y «los accesorios no
+  # aparecen cuando se conectan despues de iniciar». Van en `base` y no en un perfil de escritorio
+  # porque las dos son de la MAQUINA: un servidor tambien necesita su NIC, y el greeter necesita
+  # que un teclado enchufado aparezca antes de que haya nadie dentro.
+  #
+  # ⚠ `firmware-metal` pesa 1047 MiB y el product-rootfs pesa 264-358: esto multiplica el medio
+  # por cuatro. Es una decision tomada con el peso medido delante, no por descuido — la alternativa
+  # era `firmware-intel` (120 MB, solo Intel) y dejaba sin red a cualquier maquina con wifi Realtek
+  # o Atheros, y sin pantalla a cualquiera con GPU AMD o NVIDIA (sin su firmware el driver no
+  # inicializa el dispositivo y no hay KMS). Para una imagen que ya sabe en que maquina corre esta
+  # `firmware-tigerlake` y su perfil `metal-tigerlake`.
+  "firmware-metal",      # el firmware de una PC x86 COMPLETO (1047 MiB medidos): wifi, bluetooth, ethernet, graficos Intel/AMD/NVIDIA, audio
+  "uevent-relay",        # sin el, un accesorio enchufado DESPUES de arrancar no aparece nunca: libudev-zero escucha un grupo netlink al que nadie emite
 ]
 
 [perfil.cli]
diff --git a/recipes/firmware-intel.toml b/recipes/firmware-intel.toml
deleted file mode 100644
index 0802dd2c..00000000
--- a/recipes/firmware-intel.toml
+++ /dev/null
@@ -1,130 +0,0 @@
-# firmware-intel — los blobs que pide UN PORTÁTIL INTEL CUALQUIERA, no una laptop en particular.
-#
-# ══ POR QUÉ, EN PALABRAS DEL OPERADOR ══════════════════════════════════════════════════════════
-# *«que no sea particularizado tampoco, que voy a instalar a una i5 random y debe funcionar
-# también»* (2026-10-15). `firmware-tigerlake` es, por diseño y por su propio encabezado, «los blobs
-# que pide EXACTAMENTE la laptop del usuario, y nada más»: `iwlwifi-QuZ-a0-hr-b0-*.ucode` y
-# `i915/tgl_*.bin`. En otra máquina —otra generación de Core, otro módulo Wi-Fi— esos dos globs dan
-# CERO ficheros y la receta aborta, o peor: entra sin el blob que esa máquina necesita y el síntoma
-# es «no hay red», que es lo que costó la noche del 14 de octubre.
-#
-# Ésta es la misma idea una vuelta más arriba: **todas** las generaciones de iwlwifi y **todas** las
-# de i915, que es lo que convierte «funciona en mi laptop» en «funciona en un portátil Intel».
-#
-# ══ QUÉ CUBRE, Y QUÉ NO ════════════════════════════════════════════════════════════════════════
-#   · `intel/iwlwifi/*.ucode` — TODA la familia: 7260/7265, 8000, 9000, AX200/201/210, BE200…
-#     Es el módulo Wi-Fi del grueso del parque de portátiles Intel de los últimos diez años.
-#   · `i915/*` — el DMC de todas las generaciones (skl, kbl, cml, icl, tgl, adl, mtl…) y el GuC.
-#     Sin el DMC el display enciende pero **el ahorro de energía en runtime queda deshabilitado**,
-#     que en un portátil se nota en la batería y no en la pantalla (por eso nadie lo ve).
-#   · `regulatory.db` + `.p7s` — sin la base regulatoria el supplicant no puede elegir canal.
-#   · SOF genérico (`sof-hda-generic*.tplg` + los `.ri` que haya) — el audio de las Intel con HDA.
-#
-# **NO cubre** Realtek (`rtw88`/`rtw89`), Qualcomm (`ath10k`/`ath11k`), Broadcom (`brcm`) ni
-# MediaTek, y eso es una decisión de TAMAÑO escrita a propósito: sumarlas pasa de ~120 MB a ~310 MB
-# en el medio de instalación. Cada una es otra receta derivada de dos líneas sobre la misma
-# `linux-firmware` —así está pensado el mecanismo— y el día que alguien instale en una máquina con
-# wifi Realtek, la salida es `firmware-realtek`, no inflar ésta.
-#
-# **Tampoco cubre `amdgpu`/`radeon`** (son ~300 MB solos) y acá sí hay un motivo técnico y no de
-# peso: takana pinta por `mesa-swrast`, software puro. Una GPU sin firmware no acelera, y acá no se
-# acelera de todos modos.
-#
-# ══ SIN COMPRIMIR, igual que la derivada de tigerlake ══════════════════════════════════════════
-# El kernel `linux-metal` **no habilita `FW_LOADER_COMPRESS`**, así que un `.zst` no se carga y el
-# fallo se lee exactamente igual que un fichero ausente (`Direct firmware load for … failed with
-# error -2`). Medido en el dmesg del metal el 2026-10-14.
-#
-# ⚠️ **Y FALTA LA OTRA MITAD, que no es de esta receta:** el `iwlwifi` de estos kernels va
-# COMPILADO EN EL KERNEL y pide su `.ucode` a los 0,49 s de arranque, cuando la raíz todavía no está
-# montada. Poner los blobs en `/lib/firmware` de la raíz **no alcanza para el arranque**: el único
-# sitio donde el kernel los puede encontrar a esa altura es el INITRAMFS. Está anotado en la cola:
-# el initramfs que escribe el instalador tiene que llevar `lib/firmware`. Sin eso esta receta sirve
-# para un `rescan` en caliente y no para que la máquina arranque con red.
-name    = "firmware-intel"
-version = "20260910"          # sigue a linux-firmware: no tiene versión propia
-license = "LicenseRef-linux-firmware-various"
-
-[source]
-dir = "firmware-intel"
-
-[build]
-compiler = "zig-cc"
-target   = "x86_64-linux-musl"
-link     = "static"
-strip_debug = false
-flags   = []
-
-[build.phases]
-configure = "echo 'firmware-intel: nada que configurar'"
-compile   = "echo 'firmware-intel: nada que compilar — son blobs de la dep'"
-install = '''
-set -e
-SRC=/lib/firmware
-DST=/out/lib/firmware
-[ -d "$SRC" ] || { echo "firmware-intel: no está el árbol de linux-firmware en $SRC" >&2; exit 1; }
-[ -f "$SRC/WHENCE" ] || { echo "firmware-intel: falta WHENCE — la dep no se materializó entera" >&2; exit 1; }
-mkdir -p "$DST/intel/iwlwifi" "$DST/i915" "$DST/intel/sof/intel-signed" "$DST/intel/sof-tplg"
-
-# ── 1. Wi-Fi: TODA la familia iwlwifi ─────────────────────────────────────────────────────────
-# Sin glob de generación: el nombre que pide el driver sale del chip Y de la versión de API que ese
-# kernel soporta (`iwlwifi-<chip>-<N>.ucode`, probando de N hacia abajo), así que recortar por
-# generación es adivinar dos cosas a la vez. Son ~100 MB y es lo que compra «funciona en un portátil
-# Intel cualquiera».
-wifi=0
-for d in "$SRC/intel/iwlwifi" "$SRC"; do
-  [ -d "$d" ] || continue
-  for f in "$d"/iwlwifi-*.ucode; do
-    [ -f "$f" ] || continue
-    cp -L "$f" "$DST/intel/iwlwifi/" 2>/dev/null && wifi=$((wifi+1))
-  done
-done
-# El loader busca en `/lib/firmware` Y en `/lib/firmware/intel/iwlwifi` según la versión del driver;
-# los enlaces duros no cuestan disco y tapan las dos convenciones.
-for f in "$DST"/intel/iwlwifi/*.ucode; do
-  [ -f "$f" ] || continue
-  ln "$f" "$DST/$(basename "$f")" 2>/dev/null || cp -L "$f" "$DST/" || true
-done
-[ "$wifi" -gt 40 ] || { echo "firmware-intel: sólo $wifi blobs de iwlwifi — esperaba la familia entera (>40); ¿se materializó linux-firmware?" >&2; exit 1; }
-
-# ── 2. La base regulatoria: sin ella el supplicant no elige canal ─────────────────────────────
-for f in regulatory.db regulatory.db.p7s; do
-  [ -f "$SRC/$f" ] && cp -L "$SRC/$f" "$DST/"
-done
-[ -f "$DST/regulatory.db" ] || { echo "firmware-intel: falta regulatory.db" >&2; exit 1; }
-
-# ── 3. Gráficos Intel: el DMC de TODAS las generaciones, y el GuC/HuC ─────────────────────────
-gfx=0
-for f in "$SRC"/i915/*.bin; do
-  [ -f "$f" ] || continue
-  cp -L "$f" "$DST/i915/" && gfx=$((gfx+1))
-done
-[ "$gfx" -gt 10 ] || { echo "firmware-intel: sólo $gfx blobs de i915 — esperaba todas las generaciones (>10)" >&2; exit 1; }
-
-# ── 4. Audio SOF genérico ─────────────────────────────────────────────────────────────────────
-# El topology `sof-hda-generic` es el que sirve a cualquier Intel con HDA; los `.ri` firmados van
-# todos porque el que carga lo decide el PCI id de la máquina, no nosotros.
-snd=0
-if [ -d "$SRC/intel/sof/intel-signed" ]; then
-  for f in "$SRC"/intel/sof/intel-signed/sof-*.ri; do
-    [ -f "$f" ] || continue
-    cp "$f" "$DST/intel/sof/intel-signed/"
-    ln -sf "intel-signed/$(basename "$f")" "$DST/intel/sof/$(basename "$f")"
-    snd=$((snd+1))
-  done
-fi
-if [ -d "$SRC/intel/sof-tplg" ]; then
-  for f in "$SRC"/intel/sof-tplg/sof-hda-generic*.tplg; do
-    [ -f "$f" ] && cp -L "$f" "$DST/intel/sof-tplg/" && snd=$((snd+1))
-  done
-fi
-# El audio NO aborta la receta: una máquina sin sonido es usable y una sin red no. Se dice y sigue.
-[ "$snd" -gt 0 ] || echo "firmware-intel: 0 blobs de SOF — esta imagen va a quedar sin audio HDA" >&2
-
-n=$(find "$DST" -type f | wc -l)
-k=$(du -sk "$DST" | cut -f1)
-echo "firmware-intel: $n ficheros, ${k} KiB (wifi=$wifi i915=$gfx sof=$snd)"
-'''
-
-[deps]
-build = ["linux-firmware", "sof-firmware", "wireless-regdb"]
diff --git a/recipes/firmware-intel/README.md b/recipes/firmware-intel/README.md
deleted file mode 100644
index fbeb69f8..00000000
--- a/recipes/firmware-intel/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-Esta receta no tiene fuente propia: su contenido sale enteramente de sus deps
-(linux-firmware + sof-firmware + wireless-regdb). El directorio existe porque `source.dir`
-exige un árbol que hashear — el mismo patrón que usan `recipes/firmware-tigerlake/` y
-`recipes/atuq/`. Ver el encabezado de ../firmware-intel.toml.
-
-A diferencia de `firmware-tigerlake`, que lleva los blobs de UNA laptop medida, ésta lleva
-la familia iwlwifi entera y el DMC de todas las generaciones de i915: es la que hace que la
-imagen tenga red en un portátil Intel que nadie midió antes.
diff --git a/recipes/firmware-metal.toml b/recipes/firmware-metal.toml
new file mode 100644
index 00000000..d97140a1
--- /dev/null
+++ b/recipes/firmware-metal.toml
@@ -0,0 +1,172 @@
+# firmware-metal — el firmware de UNA MÁQUINA PERSONAL x86, completo.
+#
+# ══ POR QUÉ ESTA RECETA REEMPLAZA A `firmware-intel` ═══════════════════════════════════════════
+# `firmware-intel` (publicada unas horas antes, el 2026-10-15) llevaba 120 MB: iwlwifi, i915,
+# regulatory y SOF. Dejaba afuera **el ethernet, el bluetooth y las GPU que no son Intel**, y el
+# operador lo dijo sin anestesia:
+#
+#   *«que ni ethernet funciona. no sé por qué me lo pones tan pobre, como si no supieras lo que
+#    necesita un sistema»*
+#
+# Tenía razón, y el error era de método: recorté por tamaño **antes de medir**. Con el árbol ya
+# sellado se puede medir, y lo que una PC puede necesitar es más de la mitad del tarball. La lista de
+# abajo sale de `du -sh` sobre el artefacto de `linux-firmware`, familia por familia, no de criterio.
+#
+# ══ LO QUE ENTRA, CON SU PESO MEDIDO (2026-10-15, linux-firmware 20260910) ═════════════════════
+#
+#   WIFI ─ 641 MB
+#     intel/iwlwifi 287M · mrvl 81M · mediatek 71M · ath11k 70M · ath12k 47M · rtw89 34M
+#     ath10k 20M · brcm 12M · ti-connectivity 9M · cypress 6M · libertas 2M · rtlwifi 1M
+#     rtw88 0,9M · ath9k_htc 0,1M
+#   BLUETOOTH ─ 33 MB   intel/ibt-* 27M · qca 5M · rtl_bt 1,5M
+#   ETHERNET ─ 8 MB     cxgb4 4,6M · bnx2x 2,6M · rtl_nic 0,3M · bnx2 · cxgb3 · qlogic · tigon
+#   GRÁFICOS ─ 309 MB   nvidia 154M · amdgpu 115M · i915 27M · radeon 7,4M · xe 5,4M
+#   PORTÁTIL ─ 49 MB    intel/ipu 26M (cámara IPU6) · intel/vsc 11M · intel/ish 2,7M (sensor hub,
+#                       y por ahí pasa el touchpad I2C de muchos portátiles nuevos)
+#   AUDIO               cirrus 8,6M + el SOF que aporta la dep `sof-firmware`
+#   regulatory.db       sin él el supplicant no puede elegir canal
+#
+#   ≈ 1,05 GB. El `product-rootfs` pesa 264–358 MB, así que esto multiplica el medio por cuatro.
+#   Es el precio de que la imagen arranque con red en una máquina que nadie midió antes, y es la
+#   decisión que el operador tomó explícitamente.
+#
+# ══ LO QUE QUEDA AFUERA, Y NO ES OLVIDO ════════════════════════════════════════════════════════
+#   qcom 525M        SoC ARM (Adreno, Snapdragon). No hay PC x86 que lo pida.
+#   netronome 140M · mellanox 107M · qed 20M · liquidio 5M · intel/ice 5,4M
+#                    NIC de datacenter de 25/100 G. Si alguna vez takana corre en un servidor con
+#                    una de ésas, es otra derivada de dos líneas — así está pensado el mecanismo.
+#   intel/qat 3,7M · intel/vpu 4M   acelerador de cripto y NPU: no hacen arrancar nada.
+#
+# ══ POR QUÉ LAS GPU NO-INTEL **SÍ** VAN, corrigiendo lo que escribí en `firmware-intel` ════════
+# Ahí dije que `amdgpu`/`radeon` quedaban afuera «por un motivo técnico: takana pinta por
+# mesa-swrast, software puro». Eso estaba MAL y es el error más caro de los dos: sin su firmware
+# **`amdgpu` no inicializa el dispositivo**, y sin dispositivo no hay KMS ni `/dev/dri/card0` ⇒ el
+# compositor no tiene a qué abrirle nada y la pantalla queda muerta. Pintar por software no evita
+# necesitar que la GPU encienda. Lo mismo `nouveau` con el GSP de las NVIDIA nuevas.
+#
+# ══ SIN COMPRIMIR, Y CON LOS ENLACES ═══════════════════════════════════════════════════════════
+# El kernel `linux-metal` no habilita `FW_LOADER_COMPRESS`: un `.zst` se lee igual que un fichero
+# ausente. Y se copia **preservando los symlinks** (`cp -a`), porque el nombre que el driver pide es
+# muchas veces el del ENLACE y no el del blob: `iwlwifi-QuZ-a0-hr-b0-77.ucode` cuelga de la raíz y
+# apunta a `intel/iwlwifi/…`. Son los 2415 enlaces que `linux-firmware` reconstruye desde WHENCE;
+# copiar con `cp -L` los resolvería y el driver volvería a no encontrar su nombre.
+name    = "firmware-metal"
+version = "20260910"          # sigue a linux-firmware: no tiene versión propia
+license = "LicenseRef-linux-firmware-various"
+
+[source]
+dir = "firmware-metal"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+strip_debug = false
+flags   = []
+
+[build.phases]
+configure = "echo 'firmware-metal: nada que configurar'"
+compile   = "echo 'firmware-metal: nada que compilar — son blobs de la dep'"
+install = '''
+set -e
+SRC=/lib/firmware
+DST=/out/lib/firmware
+[ -d "$SRC" ] || { echo "firmware-metal: no está el árbol de linux-firmware en $SRC" >&2; exit 1; }
+[ -f "$SRC/WHENCE" ] || { echo "firmware-metal: falta WHENCE — la dep no se materializó entera" >&2; exit 1; }
+mkdir -p "$DST"
+
+# ── 1. Las familias, con sus enlaces internos intactos ────────────────────────────────────────
+FAMILIAS="intel/iwlwifi rtw88 rtw89 rtlwifi ath9k_htc ath10k ath11k ath12k brcm mediatek mrvl
+ti-connectivity cypress libertas rtl_bt qca rtl_nic bnx2 bnx2x tigon cxgb3 cxgb4 qlogic
+i915 xe amdgpu radeon nvidia intel/ish intel/vsc intel/ipu cirrus"
+puestas=0
+for d in $FAMILIAS; do
+	if [ -d "$SRC/$d" ]; then
+		mkdir -p "$DST/$(dirname "$d")"
+		cp -a "$SRC/$d" "$DST/$(dirname "$d")/"
+		puestas=$((puestas+1))
+	else
+		# Se dice, no se calla: una familia que desaparece entre versiones de linux-firmware es
+		# una capacidad que la imagen pierde en silencio.
+		echo "firmware-metal: ⚠ $SRC/$d no existe en esta versión del árbol" >&2
+	fi
+done
+[ "$puestas" -ge 25 ] || { echo "firmware-metal: sólo $puestas familias de las 32 declaradas — el árbol no es el que se espera" >&2; exit 1; }
+
+# ── 2. Bluetooth Intel y la base regulatoria: ficheros, no directorios ────────────────────────
+mkdir -p "$DST/intel"
+bt=0
+for f in "$SRC"/intel/ibt-*; do
+	[ -e "$f" ] || continue
+	cp -a "$f" "$DST/intel/" && bt=$((bt+1))
+done
+[ "$bt" -gt 0 ] || echo "firmware-metal: ⚠ 0 blobs de bluetooth Intel" >&2
+for f in regulatory.db regulatory.db.p7s; do
+	[ -e "$SRC/$f" ] && cp -a "$SRC/$f" "$DST/"
+done
+[ -e "$DST/regulatory.db" ] || { echo "firmware-metal: falta regulatory.db — ningún wifi podría elegir canal" >&2; exit 1; }
+
+# ── 3. Audio SOF (lo aporta la dep sof-firmware) ──────────────────────────────────────────────
+# Los `.ri` firmados van TODOS: el que carga lo decide el PCI id de la máquina, no nosotros. El
+# audio no aborta la receta —una máquina sin sonido es usable y una sin red no— pero se dice.
+snd=0
+if [ -d "$SRC/intel/sof" ]; then
+	cp -a "$SRC/intel/sof" "$DST/intel/" && snd=$((snd+1))
+fi
+if [ -d "$SRC/intel/sof-tplg" ]; then
+	cp -a "$SRC/intel/sof-tplg" "$DST/intel/" && snd=$((snd+1))
+fi
+[ "$snd" -gt 0 ] || echo "firmware-metal: ⚠ sin SOF — esta imagen va a quedar sin audio HDA" >&2
+
+# ── 4. LOS ENLACES DE LA RAÍZ: el nombre que el driver PIDE ───────────────────────────────────
+# `iwlwifi-QuZ-a0-hr-b0-77.ucode` vive en la raíz como ENLACE a `intel/iwlwifi/…`. Sin esta vuelta,
+# el blob está adentro y el driver —que pide por el nombre de la raíz— no lo encuentra: es
+# exactamente el fallo que `linux-firmware` documenta en su propio encabezado.
+raiz=0
+for l in "$SRC"/*; do
+	[ -L "$l" ] || continue
+	n=$(basename "$l")
+	destino=$(readlink "$l")
+	# Sólo si su destino cayó dentro de lo que copiamos: un enlace colgante es peor que no tenerlo,
+	# porque `request_firmware` abre y falla en vez de buscar el siguiente candidato.
+	if [ -e "$DST/$destino" ]; then
+		ln -sf "$destino" "$DST/$n"
+		raiz=$((raiz+1))
+	fi
+done
+echo "firmware-metal: $raiz enlaces de la raíz reconstruidos"
+
+# ── 5. Enlaces colgantes DENTRO de lo copiado: se resuelven desde el fuente ────────────────────
+# Una familia puede enlazar a un blob de otra que no copiamos (`ath11k/… -> ../ath10k/…`). Si el
+# destino existe en el árbol original, se trae el fichero; si no, se retira el enlace.
+colgantes=0; traidos=0
+for l in $(find "$DST" -type l); do
+	[ -e "$l" ] && continue
+	rel=$(echo "$l" | sed "s|^$DST/||")
+	if [ -e "$SRC/$rel" ]; then
+		rm -f "$l"
+		mkdir -p "$(dirname "$l")"
+		cp -L "$SRC/$rel" "$l" && traidos=$((traidos+1))
+	else
+		rm -f "$l"; colgantes=$((colgantes+1))
+	fi
+done
+echo "firmware-metal: $traidos enlaces colgantes resueltos copiando el blob, $colgantes retirados"
+
+# ── 6. Los controles que importan para el metal de un desconocido ─────────────────────────────
+falta=0
+for c in intel/iwlwifi rtw89 ath11k mediatek brcm rtl_nic i915 amdgpu nvidia regulatory.db; do
+	[ -e "$DST/$c" ] || { echo "firmware-metal: ✗ falta $c" >&2; falta=$((falta+1)); }
+done
+# El wifi Intel es el caso más probable de todos: si no quedaron sus .ucode, esto no sirve.
+w=$(find "$DST/intel/iwlwifi" -name "*.ucode" 2>/dev/null | wc -l)
+[ "$w" -ge 40 ] || { echo "firmware-metal: sólo $w .ucode de iwlwifi (esperaba >40)" >&2; falta=$((falta+1)); }
+[ "$falta" = 0 ] || { echo "firmware-metal: $falta control(es) en rojo" >&2; exit 1; }
+
+n=$(find "$DST" \( -type f -o -type l \) | wc -l)
+k=$(du -sk "$DST" | cut -f1)
+echo "firmware-metal: $n entradas, $((k/1024)) MiB — wifi/bluetooth/ethernet/gráficos/audio de una PC x86"
+'''
+
+[deps]
+build = ["linux-firmware", "sof-firmware", "wireless-regdb"]
diff --git a/recipes/firmware-metal/README.md b/recipes/firmware-metal/README.md
new file mode 100644
index 00000000..a01541ab
--- /dev/null
+++ b/recipes/firmware-metal/README.md
@@ -0,0 +1,7 @@
+Esta receta no tiene fuente propia: su contenido sale de sus deps (linux-firmware +
+sof-firmware + wireless-regdb). El directorio existe porque `source.dir` exige un árbol que
+hashear — el mismo patrón que `recipes/firmware-tigerlake/` y `recipes/atuq/`.
+
+Es la que lleva el firmware de UNA MÁQUINA PERSONAL x86 completa (wifi, bluetooth, ethernet,
+gráficos Intel/AMD/NVIDIA, audio): ~1 GB. `firmware-tigerlake` son los blobs de UNA laptop
+medida, y sirve para una imagen que ya sabe en qué máquina va a correr.
diff --git a/recipes/uevent-relay.toml b/recipes/uevent-relay.toml
new file mode 100644
index 00000000..442ac27f
--- /dev/null
+++ b/recipes/uevent-relay.toml
@@ -0,0 +1,43 @@
+# uevent-relay — la pieza que le faltaba al hotplug de la imagen.
+#
+# ══ QUÉ ARREGLA, MEDIDO EN METAL EL 2026-10-15 ═════════════════════════════════════════════════
+# *«los accesorios no aparecen cuando se conectan después de iniciar»*. El kernel emite sus uevents
+# al grupo netlink 1; `libudev-zero` abre su monitor `"udev"` —el que pide `libinput`— en el grupo
+# 0x4, que es donde `udevd` reenvía lo ya procesado. **Sin `udevd` nadie emite nunca al 0x4**, así
+# que el monitor no recibe un solo evento en toda la sesión y todo lo que se enchufa después de
+# arrancar es invisible. Es el contrato de libudev-zero, escrito en su README: hay que
+# rebroadcastear, y «hotplugging support is fully optional» — takana se salteó ese paso.
+#
+# El detalle completo (incluida la razón de que esto sea un reenviador y no un parche de una línea a
+# libudev-zero: la carrera que el propio autor advierte) está en el encabezado del crate.
+#
+# ⚠ **Va en TODOS los perfiles que lleven `libudev-zero`.** Sin él, el escritorio funciona con lo que
+# estaba enchufado al arrancar y con nada más: un ratón USB que se conecta después no mueve el
+# puntero, y el síntoma no nombra a ninguna de las tres piezas que participan.
+name    = "uevent-relay"
+version = "0.0.1"
+license = "MIT"
+
+[source]
+repo   = "https://git.tawasuyu.net/sergio/takana.git"
+commit = "5b761c17554ac412dde80ab2c2f77657fd1a90fe"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+flags    = ["-p", "uevent-relay"]
+
+# ── La Card: arje lo levanta y lo vuelve a levantar ───────────────────────────────────────────────
+# `scope = "system"` porque el reenvío es de la MÁQUINA y no de una sesión: el greeter todavía no
+# tiene a nadie dentro y ya hace falta que un teclado enchufado aparezca.
+#
+# El backoff es corto a propósito: si esto muere, el hotplug de la máquina muere con él, y el único
+# síntoma es que los accesorios dejan de aparecer — exactamente el defecto que viene a cerrar, pero
+# intermitente, que es peor. Que vuelva rápido.
+[[service]]
+label   = "uevent-relay"
+id      = "01M4ZQ8H3NKQ7VPT5WRXB4YJ6C"
+exec    = "/usr/bin/uevent-relay"
+scope   = "system"
+restart = { initial_ms = 200, max_ms = 5000 }