Validado por captura de framebuffer OVMF-GOP: el kernel metal con deferred- takeover + quiet no toca el framebuffer (Δ=0). Falta sólo el seamless i915 y el metal real. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
162 lines
11 KiB
Markdown
162 lines
11 KiB
Markdown
# ADR 0010 — Arranque por grafo: el menú de boot como navegación del grafo de estados (mirada + hammer)
|
||
|
||
**Estado:** aceptado; lado hammer implementado (pasos 1–5 ✅). Instalador live EFI validado 2-stage en
|
||
OVMF ✅; cero-parpadeo validado por captura de framebuffer ✅ (salvo el *seamless* i915, metal-only).
|
||
Falta metal real y el lado mirada. **Fecha:** 2026-07-06 (impl. EFI-stub + cero-parpadeo 2026-07-10).
|
||
**Contexto cruzado:** hammer (arranque/kernel/generaciones) + tawasuyu/mirada (render). El
|
||
espejo de este ADR para el lado mirada vive en `tawasuyu/HANDOFF-arranque-grafo.md`.
|
||
|
||
## Contexto
|
||
|
||
hammer quiere instalarse en metal real con un arranque **soberano**: nada de `systemd-boot`
|
||
(sigue siendo systemd) y GRUB-EFI está roto en este stack (GRUB 2.14 cuelga la rama UEFI, ver
|
||
[etapa-metal-usb]). El compositor **mirada** (tawasuyu, `02_ruway/mirada`) ya es "lo que ves al
|
||
arrancar": compositor Wayland + greeter + launcher sobre DRM/KMS. La idea del usuario: que el
|
||
**menú de arranque sea de mirada**, no de un bootloader ajeno — "sólo falta ponerle su menú tipo
|
||
GRUB".
|
||
|
||
Al mirarlo, el menú no es un port de GRUB. En un Linux normal el menú es una **lista** de kernels.
|
||
En hammer, donde el sistema es un **grafo content-addressed** de estados (SDD 15: config = paquete =
|
||
función = proceso), el menú puede ser la **navegación de ese grafo**: elegir un nodo = activar un
|
||
estado del sistema. Eso cierra el eslabón **proceso** que SDD 15 §H4 dejó abierto (las tres primeras
|
||
lentes ya coinciden en el hash; faltaba el lado proceso = replay de un log monótono de estados).
|
||
|
||
## Decisión
|
||
|
||
El menú de arranque es un **selector gráfico sobre el grafo de estados del sistema**, renderizado por
|
||
**mirada** sobre KMS, respaldado por el modelo de generaciones/DAG de **hammer**. La frontera entre
|
||
los dos agentes es un **contrato de datos** (hammer describe el grafo; mirada lo dibuja y pide activar
|
||
un nodo). Invariante de UX compartido: **cero parpadeo** de DRM desde la firmware hasta el escritorio.
|
||
|
||
### Quién reside sobre qué
|
||
|
||
| Capa | Reside en | Notas |
|
||
|---|---|---|
|
||
| Cargador EFI + entrada en el disco instalado | **hammer** | install-to-disk EFI; ver "loader" abajo. |
|
||
| Kernel con KMS sin costura (simpledrm→i915) | **hammer** | `linux-generic.toml` ya trae `SIMPLEDRM+SYSFB_SIMPLEFB+FB_EFI+I915+FBDEV_EMULATION`. |
|
||
| El grafo de estados (nodos que el menú ofrece) + activar nodo | **hammer** | generaciones (hammer-upgrade/recover), overlay, DAG de wawa-memo. |
|
||
| Render + navegación del menú sobre KMS | **tawasuyu/mirada** | reusa Prezi espacial / Zoom-Z (árbol fractal) / constelaciones. |
|
||
| Sostener el DRM master sin parpadeo menú→escritorio | **mirada** (con el kernel de hammer) | un solo framebuffer, abierto una vez, nunca re-modeset. |
|
||
|
||
### El contrato (la frontera limpia)
|
||
|
||
**hammer → mirada** — hammer escribe el grafo en un path bien conocido (propuesta:
|
||
`/run/hammer/boot-graph.json`, regenerable, world-readable):
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"current": "<blake3-del-nodo-vivo>",
|
||
"default": "<blake3-a-arrancar-si-no-hay-elección>",
|
||
"nodes": [
|
||
{
|
||
"id": "<blake3>",
|
||
"label": "texto corto para el usuario",
|
||
"kind": "generation | snapshot | base | recovery",
|
||
"created": "2026-07-06T12:00:00Z",
|
||
"parents": ["<blake3>", "..."],
|
||
"bootable": true,
|
||
"summary": "qué cambió respecto del padre (opcional)"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**mirada → hammer** — mirada pide activar el nodo elegido (propuesta: escribir el id en
|
||
`/run/hammer/boot-select` **o** invocar `hammer boot activate <id>`). hammer valida el id contra el
|
||
grafo, activa la generación (pivota overlay/root) y sigue el arranque. Un nodo `recovery` mapea al
|
||
`hammer-recover --rollback` que ya existe.
|
||
|
||
> El grafo es content-addressed: cada `id` es un hash, `parents` forma un DAG (no una lista lineal).
|
||
> El modelo in-place actual de hammer (`hammer-recover`: "no hay menú de generaciones tipo NixOS")
|
||
> se **sube** a un grafo navegable — es trabajo nuevo de hammer, no un refactor cosmético.
|
||
|
||
### Cero parpadeo (invariante compartido)
|
||
|
||
Un solo modo de video desde la firmware hasta el compositor:
|
||
|
||
1. Firmware GOP framebuffer → **`SYSFB_SIMPLEFB`** toma ese mismo FB → **i915 hace *seamless
|
||
takeover*** sin cambio de modo. (El kernel ya tiene los tres bits `=y`.)
|
||
2. Sin texto de fbcon ni cursor durante el arranque, sin getty en el VT gráfico. **Diagnóstico
|
||
preciso (2026-07-10, evidencia por captura de framebuffer en OVMF-GOP, `scripts/`):** hoy el kernel
|
||
metal pinta TODO el log de boot sobre el framebuffer gráfico (simpledrm "switching to colour frame
|
||
buffer device 160x50" + decenas de líneas). Causa: `CONFIG_VT_CONSOLE=y` registra el VT como consola
|
||
automáticamente (quitar `console=tty0` NO alcanza — probado), y `quiet loglevel=0` por LoadOptions NO
|
||
lo frena (fbcon redibuja el ring buffer al tomar el FB a ~0.6s; además el `ignore_loglevel earlyprintk`
|
||
HORNEADO en `CONFIG_CMDLINE` fuerza la salida). **Fix (requiere rebuild del kernel metal, cambia el
|
||
hash firmado):** (a) `CONFIG_FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER=y` — fbcon no toca el FB hasta el
|
||
primer output ⇒ queda negro; (b) cmdline horneada → quitar `earlyprintk=efi,keep ignore_loglevel`,
|
||
añadir `quiet loglevel=3 vt.global_cursor_default=0`; (c) `CONFIG_LOGO` ya está off ✓. GOTCHA de
|
||
validación: con `quiet` los marcadores serie kernel-side desaparecen ⇒ los harness EFI deben apoyarse
|
||
en los marcadores userspace tee'ados a `/dev/ttyS0` (que sobreviven `quiet`). El *seamless* i915 (1)
|
||
sólo se valida en metal Intel real (OVMF no trae i915).
|
||
3. **mirada abre el DRM master directo** y lo sostiene desde el menú hasta el escritorio — nunca
|
||
suelta, nunca re-modeset.
|
||
|
||
### El loader (cómo se lanza el kernel sin systemd-boot ni GRUB)
|
||
|
||
El cuelgue del EFI-stub directo en el metal del usuario era por el **initrd de 100MB** (medición TPM
|
||
+ quirk de LoadOptions del firmware). `linux-generic.toml` ya trae **squashfs+overlay** ⇒ el
|
||
initramfs se vuelve **minúsculo** (sólo monta el squashfs). Con initrd chico, **EFI-stub directo
|
||
vuelve a ser viable y soberano** — sin GRUB ni systemd. Es decir: squashfs+overlay no es un desvío,
|
||
es lo que *destraba* el arranque EFI soberano y de paso el cero-parpadeo (initrd chico = menos tiempo
|
||
muerto antes del takeover).
|
||
|
||
## Plan (lado hammer)
|
||
|
||
1. **Este ADR + el handoff a tawasuyu** con el contrato (hecho en este commit).
|
||
2. **initrd chico → EFI-stub directo reprobado en OVMF** ✅ (2026-07-10): el disco instalado NO usa un
|
||
rootfs en RAM sino un **initramfs mínimo de pivote** (~770K) que resuelve `hammer-root` por LABEL y
|
||
hace `switch_root` a la ext4 real. Ese initrd chico es lo que destraba EFI-stub directo (el cuelgue
|
||
del metal era por un initrd de 100MB). Validado en OVMF por `scripts/efi-disk-boot-test.sh`. (La vía
|
||
squashfs+overlay del [post-etapa-e] queda como optimización futura; el pivote a ext4 ya cumple.)
|
||
3. **Contrato de generaciones-grafo** ✅ (módulo `hammer_upgrade::boot_graph` + subcomando `hammer
|
||
boot`): el modelo in-place de generaciones se lee como un **grafo de estados** navegable (id =
|
||
`of_tree`, DAG por `parent`), `hammer boot graph` emite `/run/hammer/boot-graph.json` con el
|
||
formato del contrato, y `hammer boot activate <id>` (o `--from-select`) deja el sistema en un nodo
|
||
(no-op si ya vivo · rollback paso a paso a un ancestro · nodo `recovery` sintético · error honesto
|
||
ante un "redo" hacia una generación huérfana). Validado e2e por el CLI. **mirada ya puede maquetar
|
||
contra el grafo real.**
|
||
4. **Afinar kernel/cmdline para el cero-parpadeo** ✅ (2026-07-10): `linux-metal` con
|
||
`CONFIG_FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER=y` + cmdline horneada `quiet loglevel=3
|
||
vt.global_cursor_default=0` (sin `earlyprintk/ignore_loglevel`) + ttyS0 último (la shell de arje-zero
|
||
sale por serial, no pinta tty0). **Validado por captura de framebuffer en OVMF-GOP**
|
||
(`scripts/efi-flicker-test.sh`): el kernel NO toca el FB (Δ media de brillo = 0.000 entre handoff y
|
||
post-boot; el viejo saltaba ~3→~11) ⇒ el contenido de la firmware sobrevive intacto hasta que mirada
|
||
abra el DRM. Los harness EFI pasan a marcadores userspace tee'ados a `/dev/ttyS0` (sobreviven `quiet`).
|
||
El *seamless* i915 (firmware GOP → simplefb → i915 sin re-modeset) sólo se valida en metal Intel real.
|
||
5. **install-to-disk EFI** ✅ (2026-07-10): el bzImage `linux-metal` ES el binario EFI
|
||
(`\EFI\BOOT\BOOTX64.EFI`, ruta fallback removible, sin GRUB ni systemd-boot). Como no hay LoadOptions
|
||
en esa ruta, el `CONFIG_CMDLINE` horneado (`… initrd=/initramfs.cpio.gz rdinit=/init`) activa el
|
||
pivote. Dos caminos, mismos artefactos:
|
||
- **host-side** `scripts/install-image-efi.sh` — GPT+ESP+root/store/state, ESP con mtools de hammer,
|
||
ext4 con `mke2fs -d` bajo `unshare -r`; **validado VERDE en OVMF** (`scripts/efi-disk-boot-test.sh`:
|
||
firmware → BOOTX64.EFI → pivote → arje-zero PID1).
|
||
- **live-side** `scripts/hammer-live-install.sh` rama EFI — detecta `/sys/firmware/efi` y hace
|
||
**MBR con ESP tipo 0xEF** (UEFI arranca una ESP MBR igual que GPT ⇒ alcanza busybox
|
||
fdisk/mkfs.vfat/mount, sin GPT-tool ni mtools en el live). El kernel EFI-stub viaja en el payload
|
||
(`iso-image.sh INSTALLER=1` bundlea `bzImage-efi`). **Validación 2-stage VERDE en OVMF**
|
||
(`scripts/efi-install-test.sh`): ISO live UEFI → `hammer-install` detecta `/sys/firmware/efi` y toma
|
||
la rama EFI → el disco instalado bootea SOLO por EFI-stub soberano (sin GRUB) → arje-zero PID1.
|
||
Falta sólo el metal real (quemar USB + bootear, Secure Boot OFF).
|
||
|
||
## Consecuencias
|
||
|
||
- La frontera es un **fichero + un comando**, no una API compartida ⇒ los dos agentes avanzan sin
|
||
bloquearse. hammer puede emitir el grafo y probar EFI-stub sin esperar a mirada; mirada puede
|
||
maquetar el menú contra un `boot-graph.json` de ejemplo sin esperar a hammer.
|
||
- El menú-grafo es la primera **manifestación del lado proceso** de SDD 15 §H4 — arrancar = activar
|
||
un nodo del DAG. No colapsa todo el modelo, pero lo hace visible.
|
||
- No mueve la frontera de confianza (ADR 0007/0009): activar un nodo es reproducir/hidratar un árbol
|
||
sellado del store, no ejecutar algo nuevo.
|
||
|
||
## Referencias
|
||
|
||
- SDD 15 §H3/§H4 (`docs/15-frontier-ai-native.md`) — config = paquete = función = proceso; el lado
|
||
proceso = replay del MonotonicLog.
|
||
- ADR 0007 (arje-zero como init propio), ADR 0009 (código content-addressed).
|
||
- Memoria de proyecto: etapa-metal-usb (GRUB 2.14 roto, EFI-stub colgó por initrd 100MB, metal
|
||
booteado vía systemd-boot como crutch descartable), post-etapa-e-refinamientos (squashfs+overlay,
|
||
rollback-al-boot).
|
||
- `tawasuyu/HANDOFF-arranque-grafo.md` — el espejo, lado mirada.
|