docs: SDD 22 — respuesta verificada al handoff de kernel-config de tawasuyu
El handoff avisa que nada suyo está verificado contra hammer («si un dato de hammer lo
contradice, manda hammer»). Esto es esa verificación: contesta sus seis preguntas del §9 con
evidencia del código, corrige lo que estaba mal y añade dos objeciones que cambian el orden.
NO es aprobación ni plan de implementación.
EL HECHO QUE REORDENA TODO, y que el handoff no podía conocer: las FASES entran en
hash_inputs, y el config del kernel vive en la fase `configure` ⇒ **el config ES la identidad
del artefacto**. Corta en los dos sentidos. A favor: §2.2 (atestar un config por huella de
hardware) sale casi gratis, porque cada config ya es un artefacto direccionado por contenido,
reproducible y firmable — no hay que inventar el mecanismo. En contra: la explosión de builds
no es hipotética, es aritmética visible en el disco, que ya crece 24 G por ciclo. Y una
«perilla» de la UI no es un parámetro de runtime: es una edición de receta.
LAS SEIS RESPUESTAS:
P1 fragmentos — YA es como §6 prescribe: defconfig + `scripts/config -e/-d` + olddefconfig,
o sea que la app nunca escribe un .config. Media implementación está en producción. Falta
el diff-back, sin el cual un símbolo pedido que no sobrevive se pierde EN SILENCIO.
P2 repro con config variable — sí, por construcción: no hay un kernel cuya reproducibilidad
dependa de congelar el config; hay N kernels, cada uno congelado. Se pueden PREFIRMAR
artefactos, no sólo configs.
P3 tiempo — 35/45/60 min según receta ⇒ la bisección de §5 son ~4,5 h de la máquina que
acaba de no arrancar. Mata la bisección ingenua.
P4 harkaq — el patrón sí, el código no: su clausura enumera fichero a fichero sobre
artefactos del store, semántica de rutas. Kconfig es otro grafo.
P5 eje huella en el índice — sí y compatible hacia atrás: PackageEntry usa campos serde
opcionales. Mismo truco que hizo pagable el campo `license` hoy.
P6 PLAN-KIKIN §4.bis — DADO POR PENDIENTE Y ESTÁ HECHO EN DOS DE TRES: linux-metal y
linux-generic ya declaran los cuatro símbolos; sólo `linux` deja IO_URING y BPF_SYSCALL
al azar del defconfig. Y ojo al método: `grep IO_URING` da positivo por un COMENTARIO;
hay que mirar los -e/-d dentro del bloque configure. Mismo error que infló el conteo de
licencias hoy y que hace mentir la regla del `.a` no-PIC.
OBJECIÓN a §2.1: «clausura» sobre Kconfig no está definida todavía. Hay tres tipos de arista
(`depends on`, `select`, `imply`) con semánticas incompatibles, y `select` fuerza símbolos
IGNORANDO los `depends on` del destino. «Alcanzable desde CONFIG_WIRELESS» da conjuntos
distintos según la arista y ninguno es la respuesta. No lo invalida: lo convierte en la parte
con contenido técnico real. Y hay con qué validarlo GRATIS: el config actual de linux.toml
YA es un juego de bundles N1 hecho a mano («-d WLAN -d WIRELESS -d CFG80211 -d MAC80211
-d RFKILL» es literalmente «no necesito wifi») sobre un kernel que arranca.
LO QUE AGREGO:
· Bisección SIN recompilar: kernel «superset» con lo bisecable como módulo ⇒ los bundles de
hardware se prueban con lista de bloqueo en la línea de arranque. 6 reinicios en vez de 6
rebuilds. No cubre lo que debe ser =y (buena parte de N2), y el superset es el kernel gordo
que N1 quiere evitar: es diagnóstico, no el de todos los días.
· El gate de no-regresión debe ser POR OBJETIVO: linux.toml apaga USB/HID/INPUT a propósito
(es el kernel de QEMU con consola serie) y un gate global rechazaría una receta sana.
· «Booteó» no basta para atestar: un kernel arranca y te deja sin red, que es el desastre que
el propio gate quiere evitar. La atestación debe llevar QUÉ se comprobó — el mismo dato que
calcula el gate: una implementación, dos usos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Semantic delta
No definition changed: this change is text, documentation or data.
1 files · +223 −0
- docs/22-configurador-kernel.md +223 −0
diff --git a/docs/22-configurador-kernel.md b/docs/22-configurador-kernel.md
new file mode 100644
index 00000000..61662ad2
--- /dev/null
+++ b/docs/22-configurador-kernel.md
@@ -0,0 +1,223 @@
+# SDD 22 — Configurador de kernel por niveles: respuesta al handoff de tawasuyu
+
+Escrito 2026-08-07, en respuesta a `tawasuyu/HANDOFF-KERNEL-CONFIG-A-HAMMER.md`.
+
+El handoff avisa de su propio método: *«nada de lo que sigue está verificado contra el código de
+hammer — si un dato de hammer lo contradice, manda hammer»*. Este documento es esa verificación. **No
+es una aprobación ni un plan de implementación**: contesta las seis preguntas de su §9 con evidencia,
+corrige lo que estaba mal, y señala dos objeciones técnicas que cambian el diseño.
+
+**Veredicto corto:** la idea se sostiene y el sustrato está más listo de lo que el handoff supone —
+el mecanismo de §6 (*«la app nunca escribe un `.config`»*) **ya es exactamente como funciona hoy**.
+Pero hay **un hecho estructural que el handoff no podía conocer y que reordena todo**, y **una
+objeción técnica a §2.1** que hay que resolver antes de escribir una línea.
+
+---
+
+## 1. El hecho que reordena todo: **el config ES la identidad del artefacto**
+
+En hammer, `Recipe::hash_inputs` es una lista blanca, y **las fases de build entran en ella**. El
+config del kernel vive dentro de la fase `configure` de `recipes/linux*.toml`. Por lo tanto:
+
+> **Cambiar un solo símbolo de config cambia el `ArtifactHash` del kernel.**
+
+Esto no es un detalle de implementación: es la bisagra del diseño, y corta en los dos sentidos.
+
+**A favor, y fuerte:** §2.2 (config atestado por huella de hardware) **sale casi gratis**. No hay que
+inventar un mecanismo para «atestar un config»: cada config ya *es* un artefacto direccionado por
+contenido, reproducible bit a bit y firmable con la maquinaria que existe. Un «config que booteó en
+esta huella» es, literalmente, un `ArtifactHash` + una firma. El handoff intuyó que hammer tenía el
+sustrato; lo tiene más de lo que creía.
+
+**En contra, y también fuerte:** la explosión de builds que §2.2 quiere evitar **no es hipotética,
+es aritmética visible en el disco**. Cada variante de config es un artefacto nuevo en el store, y el
+store ya crece 24 G por ciclo de trabajo sólo con las variantes que producimos hoy sin querer. Con
+N1 (~40–60 bundles) el espacio combinatorio es 2⁴⁰; con «perfiles publicados» —que es lo que el
+handoff propone en §8— es un puñado. **La restricción de §8 no es una preferencia de diseño: es una
+condición de supervivencia del disco**, y conviene decirlo así en la UI.
+
+**Consecuencia práctica que hay que aceptar desde el día uno:** una «perilla» de la UI **no es un
+parámetro de runtime, es una edición de receta**. No hay forma de tener un config variable sin
+generar recetas. El diseño correcto es que `hammer kernel plan` **emita una receta derivada** (con su
+propio hash) en vez de fingir que el kernel es un binario parametrizable.
+
+---
+
+## 2. Las seis preguntas del §9, contestadas con evidencia
+
+### P1 — ¿Las recetas de kernel aceptan fragmentos o sólo defconfig completo?
+**Ya son fragmentos, y con la forma que §6 prescribe.** `recipes/linux.toml`, fase `configure`:
+
+```
+make ARCH=x86_64 defconfig && \
+scripts/config -d MODULE_SIG -d DEBUG_INFO_BTF -e SECURITY_LANDLOCK -e AUDIT \
+ -d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL … && \
+make ARCH=x86_64 olddefconfig
+```
+
+Base + deltas por símbolo + resolución por el `olddefconfig` **del propio kernel**. La app nunca
+escribe un `.config`. La única diferencia con el handoff es el vehículo: `scripts/config -e/-d` en
+vez de ficheros de fragmento y `merge_config.sh`. Son equivalentes y `scripts/config` es más fácil de
+generar programáticamente. **Media implementación de §6 ya está en producción.**
+
+Falta la otra mitad, que es la que da la UI veraz: **el diff-back**. Hoy nada comprueba que los
+símbolos pedidos hayan sobrevivido a `olddefconfig`. Un `-e FOO` cuya dependencia no se cumple se
+pierde en silencio — el mismo patrón de fallo que el `license` que caía dentro de `[deps]`.
+
+### P2 — ¿El build es bit-reproducible con config variable?
+**Sí, y por construcción.** El config no es un parámetro externo: es parte de la entrada hasheada
+(§1). Cada config es su propia receta con su propio hash y su propia garantía de reproducción. No hay
+un «kernel» cuya reproducibilidad dependa de congelar el config; hay N kernels, cada uno congelado.
+⇒ **§2.2 puede prefirmar artefactos, no sólo configs.**
+
+### P3 — ¿Cuánto tarda un kernel completo? 🚨
+Declarado en las propias recetas: **`linux` ~35 min, `linux-metal` ~45 min, `linux-generic` ~60 min**
+en la máquina de referencia. En la más lenta del parque, más.
+
+**Esto mata la bisección ingenua de §5.** ~6 rebuilds × 45 min = **4,5 horas** de la máquina del
+usuario para encontrar un bundle culpable. Nadie espera eso, y menos con la máquina que acaba de no
+arrancar. El handoff ya preveía el riesgo («si es caro, tope duro de iteraciones»); la respuesta es
+que **es caro**. Ver §4, donde propongo una bisección que no recompila.
+
+### P4 — ¿harkaq puede prestar su motor de clausura?
+**El patrón sí; el código no.** `crates/hammer-build/src/harkaq.rs` deriva su política enumerando
+**fichero a fichero** los artefactos de las deps declaradas — es una clausura sobre el grafo de
+paquetes materializado en el store, con semántica de rutas de filesystem. Kconfig es otro grafo, con
+otra semántica y ~15 000 símbolos. Reusar la *idea* («política = clausura, no lista») es correcto y
+está validado; reusar la *implementación* no. Hace falta un lector de Kconfig — y ahí está la
+objeción del §3.
+
+### P5 — ¿El índice firmado admite un eje más (la huella)?
+**Sí, y sin romper nada.** `PackageEntry` (`crates/hammer-core/src/repo.rs`) es un struct serde cuyos
+campos opcionales llevan `#[serde(default, skip_serializing_if = ...)]`. Añadir `fingerprint:
+Option<String>` es compatible hacia atrás: los índices viejos siguen parseando y los clientes viejos
+ignoran el campo. **Es exactamente el truco que hizo pagable el campo `license` hoy mismo.**
+
+### P6 — El prerrequisito de PLAN-KIKIN §4.bis
+**Estaba dado por pendiente y está hecho en dos de tres.** Medido:
+
+| receta | `SECURITY_LANDLOCK` | `AUDIT` | `IO_URING` | `BPF_SYSCALL` |
+|---|---|---|---|---|
+| `linux-metal` | ✅ `-e` | ✅ `-e` | ✅ `-e` | ✅ `-e` |
+| `linux-generic` | ✅ `-e` | ✅ `-e` | ✅ `-e` | ✅ `-e` |
+| `linux` | ✅ `-e` | ✅ `-e` | ❌ sólo en un comentario | ❌ sólo en un comentario |
+
+⚠️ Y la forma de medirlo importa: `grep IO_URING recipes/linux.toml` **da positivo** porque la palabra
+aparece en un comentario. Hay que mirar los `-e`/`-d` **dentro del bloque `configure`**. Es el mismo
+error que hoy infló el conteo de licencias (`grep -l license` contaba comentarios y nombres de
+paquete) y el que hace mentir a la regla del `.a` no-PIC. **En este repo, `grep` sobre ficheros con
+comentarios densos no es una medición.**
+
+⇒ Queda pendiente sólo `linux`, que es el kernel de QEMU/serial.
+
+---
+
+## 3. Objeción técnica a §2.1: «clausura» sobre Kconfig no está bien definida todavía
+
+La propuesta —«todo lo alcanzable desde `CONFIG_WIRELESS` menos lo alcanzable desde lo que sí
+quiero»— asume un grafo con aristas de un solo tipo. Kconfig tiene **al menos tres, con semánticas
+incompatibles**:
+
+- `depends on` — arista hacia arriba: *no puedo existir sin X*.
+- `select` — arista hacia abajo y **forzada**: *si yo entro, X entra*, ignorando los `depends on` de
+ X. Es la fuente clásica de configs inválidos.
+- `imply` — sugerencia blanda.
+
+«Alcanzable» por `depends on` y «alcanzable» por `select` dan conjuntos distintos y **ninguno de los
+dos es la respuesta**. Un driver de otro subsistema puede `select` un helper de wireless sin ser
+wireless: cae dentro de la clausura ingenua y apagarlo rompe algo que el usuario sí quería. Y al
+revés, `select` puede meter símbolos wireless desde fuera de la clausura.
+
+**Esto no invalida §2.1 — lo convierte en la parte con contenido técnico real**, tal como el propio
+handoff dice en su §10. Pero hay que definir la semántica de arista antes de escribir el predicado, y
+la definición hay que **validarla contra algo**.
+
+### Y tenemos contra qué validarla, hoy y gratis
+El config actual de `linux.toml` **ya es un juego de bundles N1 escrito a mano**. Literalmente:
+
+```
+-d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL ← el bundle «no necesito wifi»
+-d SOUND -d SND ← «no necesito audio»
+-d DRM -d DRM_I915 -d DRM_AMDGPU -d DRM_NOUVEAU -d FB ← «sin gráficos»
+-d XFS_FS -d BTRFS_FS -d F2FS_FS -d JFS_FS -d REISERFS_FS ← «sólo ext4»
+```
+
+Es un corpus de verdad-de-campo: alguien decidió a mano qué símbolos componen «no necesito wifi», y
+ese kernel **arranca**. La prueba de §2.1 es directa y barata: *¿la clausura calculada de
+`CONFIG_WIRELESS` reproduce ese conjunto?* Si sobra o falta mucho, la semántica de arista está mal y
+nos enteramos en una tarde, sin compilar nada. **Refuerza el §10 del handoff: eso se hace antes que
+nada, junto al modo reversa.**
+
+---
+
+## 4. Lo que agrego: bisección **sin recompilar**
+
+Dado P3 (35–60 min por kernel), la bisección de §5 cuesta horas. Pero la mayoría de los bundles N1
+son **drivers**, y un driver puede ser `=m` en vez de `=y`.
+
+Propuesta: **un kernel «superset» con todo lo bisecable como módulo**. Los bundles N1 de tipo
+hardware no se resuelven entonces recompilando, sino con una **lista de bloqueo de módulos en la
+línea de arranque**. La bisección deja de ser 6 rebuilds (4,5 h) y pasa a ser **6 reinicios**
+(minutos), sobre un kernel que ya está en el árbol de generaciones.
+
+Consecuencias:
+- El «kernel a medida» pasa a ser una optimización de tamaño/arranque, no un requisito de
+ funcionalidad ⇒ el usuario puede probar bundles antes de pagar un build.
+- Encaja con el árbol de generaciones y el rollback E4, que ya existen.
+- **Limitación honesta:** no cubre lo que debe ser `=y` (LSM, preempción, unwinder, `-march`, LTO,
+ o sea buena parte de N2). Para esos sí hay rebuild y sí hay que topar iteraciones.
+- **Y un coste que hay que decir:** el superset es exactamente el kernel gordo que el nivel 1 quiere
+ evitar. Es una herramienta de diagnóstico, no el kernel de todos los días.
+
+## 5. Lo que agrego: el gate de no-regresión (§6) debe ser **por objetivo**, no global
+
+El handoff lo llama la mejor relación valor/esfuerzo y estoy de acuerdo. Pero el config actual de
+`linux.toml` apaga `USB_SUPPORT`, `HID`, `HID_SUPPORT`, `INPUT_MOUSE`, `INPUT_TOUCHSCREEN`. Ese
+kernel **no puede recibir teclado en metal** — y es correcto, porque es el kernel de QEMU con consola
+serie. Un gate global «todo dispositivo en uso debe seguir teniendo driver» **rechazaría una receta
+sana**.
+
+⇒ El gate necesita el objetivo declarado (`qemu-serial` / `metal` / `servidor`) y una noción de
+«dispositivos en uso **de la máquina destino**», que no siempre es la máquina donde se construye.
+Sigue siendo la propuesta más barata de las nueve; sólo que el portón se pone en el sitio correcto.
+
+## 6. Dos huecos del handoff que conviene cerrar antes
+
+**«Booteó» no es una aserción suficiente para atestar (§2.2).** Un kernel puede arrancar y dejarte
+sin red, sin wifi o sin teclado — que es justo el desastre que §6 quiere evitar. Si la atestación
+dice sólo «booteó en esta huella», estamos firmando la clase de fallo que más duele. La atestación
+debería llevar **qué se comprobó**: qué dispositivos de la huella quedaron con driver bindeado
+después de arrancar. Es el mismo dato que ya calcula el gate de §6 ⇒ una implementación, dos usos.
+
+**El catálogo de bundles necesita las tres etiquetas de nivel, no dos.** El handoff avisa en §5 que
+«la mitad de N2 no es Kconfig sino variables de receta». Con §1 en la mano eso es más grave de lo que
+parece: las perillas de receta (LTO, `-march`, `zig_version`) **cambian el hash igual que los
+símbolos**, pero se aplican en otra fase y fallan de otra forma. La UI tiene que saber de qué lado
+cae cada perilla o prometerá diffs que no puede explicar.
+
+---
+
+## 7. Qué contesto al reparto (§7)
+
+De acuerdo con el reparto tal como está. Dos precisiones desde este lado:
+
+- El contrato JSON: de acuerdo, y el precedente que cita (`/run/hammer/boot-graph.json`) es el
+ correcto. Añadiría que el JSON de `hammer kernel plan` **debe incluir el `ArtifactHash` resultante**,
+ porque por §1 el plan *determina* el artefacto: la UI puede decir «esto ya está construido y
+ firmado» sin construir nada.
+- Los verbos propuestos me parecen bien salvo `hammer kernel build --plan`, que esconde dos cosas muy
+ distintas (construir, y dar de alta en el árbol de generaciones). Separarlos deja que la sonda en
+ VM de §4 se meta entre medio como un paso propio y auditable.
+
+## 8. Orden que propongo (modifica el §10 del handoff)
+
+1. **Modo reversa (#9)** — tal cual lo propone: cero riesgo, valida el catálogo contra un kernel real.
+2. **Validar la clausura contra el config actual** (§3 de este documento) — sale gratis, usa un corpus
+ que ya existe, y decide si §2.1 es viable. **Va antes que el gate**, porque si la clausura no
+ reproduce los bundles hechos a mano, el diseño de N1 cambia.
+3. **Gate de no-regresión (#6), por objetivo** (§5 de este documento).
+4. **Diff-back** (la mitad que falta de §6 del handoff): barato, y sin él la UI miente.
+5. Recién después, #1 (clausuras) con la semántica ya decidida.
+
+**No implementar todavía.** Este documento existe para que la decisión se tome con los seis datos
+verificados, no con las suposiciones del handoff — que su autor, correctamente, marcó como tales.