Los tests no eran el punto: el punto era que un servicio DECLARADO en una receta
termine supervisado por PID 1. `product-boot-test.sh` sobre el product-rootfs de
la ruta real da `ok card sshd en la seed` y `PRODUCT_SSH_OK uid=0`, sin ningún
eslabón escrito a mano:
recipes/openssh.toml [[service]] → sidecar dentro del artefacto sellado →
service_cards() → genesis de /ente/seed.card.json → arje-zero encarna sshd →
la sesión SSH responde.
Y HABÍA QUE DESCARTAR QUE LO MOVIERA ESTE CAMBIO: el product-rootfs salió con
hash distinto (5011955a… → 8639894332…). No fue la seed — las dos son
BYTE-IDÉNTICAS (diff del JSON, cero diferencias). Cambiaron `netup`, que es otro
artefacto que cuando se selló el viejo, y por arrastre `ente/attest.json`, que
registra su BLAKE3. O sea: la constante y la receta producen la misma seed,
comprobado sobre el árbol real y no sólo en un test.
Y queda escrito por qué el ESCRITORIO no se puede arrancar supervisado todavía,
que es corpus y no diseño: gnome-shell en deuda (bloqueado sólo por
evolution-data-server, que tiene blocked_by vacío) y sway sin sellar. Sin
compositor no hay sesión. NO se cablearon los scripts de imagen a ciegas: inyectar
cards en un genesis que no se puede bootear es escribir código que nadie puede
contradecir, y este mismo doc ya tiene el ejemplo de qué pasa entonces (el SDD 06
afirmando meses un lector que no existía).
Primera prueba de `atuq` dentro de una imagen de disco arrancada, y el resultado no es el de la
jaula. La cadena previa funcionó, y eso también es medición: el perfil `escritorio-cosmic` hidratado
con el cierre de hoy (275 nodos, 8,4 G) trae `/usr/bin/atuq`, `/usr/bin/llama-server` y el modelo —
declarar en el perfil SÍ pone las cosas en la imagen—, la imagen EFI de 12 G arranca en QEMU y COSMIC
pinta panel y dock en ~2 min.
Y `atuq` arranca —sus extensiones inician, la del foco sondea cada minuto, WebRender inicializa— sin
que la ventana aparezca nunca. Descartado, para que nadie lo repita: no es el sandbox de Gecko
(relanzado con los cinco MOZ_DISABLE_* puestos, idéntico), no es que el proceso muera (sigue
ejecutando el JS de las extensiones), y no es la IA (pasa con about:blank).
La pista: bajo sway headless el MISMO artefacto pinta perfecto y hay capturas del panel contestando.
La diferencia son el compositor (cosmic-comp+llvmpipe contra sway+pixman) y el arranque real contra
bwrap. Es su propia unidad de trabajo, no un parche apurado.
⚠ La lección del método: quince guardianes en verde, `vigia-imagen.py` en ✓, y el navegador igual no
se puede usar en la imagen. «Sella», «hidrata» y «los tests pasan» son tres cosas distintas de
«arranca y se ve».
De paso, `metal-iso.sh` acepta ahora STORE y AUG por entorno, porque en esta máquina no funcionaba:
arma el rootfs con `cp -al` desde el store y el store es un BIND-MOUNT del volumen — `linkat` rechaza
cruzar mounts aunque sea el mismo disco, así que hay que nombrar los dos lados dentro del mismo
mount. Es la cuarta vez que aparece el mismo EXDEV hoy.
`scripts/atuq-captura-ia.py`: el arnés de sway headless de los guardianes + `grim` + un PNG. No es un
guardián (los veredictos siguen saliendo del `dump`); es la evidencia que ningún log puede dar: que
el panel se pinta y que la respuesta está ahí.
Encontró tres cosas, ninguna visible en un log:
1. **la barra lateral se abría SOLA** en el primer arranque, ocupando un tercio de la ventana. Gecko
lo hace al instalar una extensión con `sidebar_action`, y para un navegador que la trae de fábrica
eso es imponerle un panel a todo el mundo. `"open_at_install": false`, con captura del después;
2. el diálogo «Close Firefox» en la segunda corrida — el arnés mataba el navegador sin despedirse y
quedaba el `.parentlock`. Arreglado en el arnés;
3. dos barras de notificación VACÍAS en el arranque. El atajo era reportarlas como fuga de marca, y
era falso: instrumentando una copia del artefacto para volcar el DOM salieron
`sandbox-content-disabled` (lo apaga el propio arnés) y `startup-restore-session-suggestion` (por
el cierre abrupto anterior). El texto está en el DOM y no en los píxeles — comprobado además
quitando nuestro CSS, con el mismo resultado ⇒ es el render por software de la jaula.
⇒ Una captura muestra síntomas; el DOM dice de quién son. Sin ese segundo paso, dos de los tres
habrían entrado al SDD como bugs nuestros.
El log del navegador viaja junto a la foto, siempre: una captura muestra que algo se ve raro y no por
qué. Y `test-atuq-ia.py` + `test-atuq-archivo-semantico.py` siguen verdes tras el cambio.
── 1. Se RETIRA incoming-kde/networkmanager.toml ───────────────────────────────────────────────
networkmanager-qt b3:78bbdcda00d758fb3cc3a0be7da6a67395dfbe0e4d55c5d8e5cf8afaf728ff72
plasma-nm b3:496a9ff7794ae5d89ac2842bdfbca38c25c91df37465a049db7cf75ee0041b82
Ayer se promovió NetworkManager al corpus CON WiFi porque la variante de KDE iba con -Dwifi=false,
y quedaron dos. Esto cierra el arco: retirada la de la cola, `networkmanager-qt` resuelve al PADRE
y la distro tiene UNO solo. Consecuencia concreta: **KDE pasa a tener WiFi**, porque su applet
hablaba con un demonio construido sin soporte inalámbrico.
Medido con `yupana radio` antes de borrar: 2 rebuilds (networkmanager-qt, plasma-nm), los dos en
la cola KDE. Y comprobado sobre el ARTEFACTO del corpus que publica lo que nm-qt pide —`libnm.pc`
y `usr/include/libnm`— antes de quitarle el suelo. Los dos sellan.
── 2. upower al corpus: COSMIC dibujaba una batería sin nadie que se la contara ────────────────
libgudev b3:87ca5e73… · udev-pc b3:fa880b5c… (hash IDÉNTICO desde las dos partes ⇒ cache-hit)
upower b3:c82e655ff7011150f21f9413433c518f12c827c52ecf6f9fc3484f75d85c155c
Ayer escribí que esta cadena eran «3 promociones, una de ellas polkit, decisión de arquitectura».
Fui a mirar y polkit SALE de la ecuación con una perilla: la variante de KDE va `-Dpolkit=enabled`
«porque polkit YA está sellada» —cierto EN ESA COLA y falso desde el corpus—, y apagarlo cuesta
poco medido en FUNCIÓN: polkit en upower sólo gobierna las acciones privilegiadas (suspender e
hibernar por org.freedesktop.UPower, además deprecadas: hoy eso lo hace logind). Reportar batería,
carga, tiempo restante y línea de corriente NO pasa por polkit, que es justo lo que COSMIC quiere.
Se corrigió además la frase del comentario heredado que decía `-Dpolkit=enabled` al lado de un flag
que ahora dice `disabled`: una nota que desmiente al código de al lado es peor que no tener nota.
Y se le añadió su [[service]] (el corpus no lo traía; el label/id son los mismos que en GNOME a
propósito: el ULID identifica al SERVICIO, no al artefacto). No mueve el hash.
Comprobadas las NEEDED de upowerd con provee.py: libupower-glib, libffi.so.8, libz.so.1,
libudev.so.1 — las cuatro las publica el corpus y ya estaban en [deps]. Sin NEEDED colgante.
── 3. targets.toml ─────────────────────────────────────────────────────────────────────────────
escritorio-kde servicios += NetworkManager (llegaba por clausura de plasma-nm y NADIE lo
arrancaba: el applet sobre un demonio apagado)
escritorio-cosmic paquetes += upower · servicios += upowerd
⚠ Y se CORRIGE la nota de ayer que decía «NO declarar networkmanager en escritorio-kde porque
plasma-nm ya arrastra la variante de la cola». Ya no hay variante de cola.
El emisor lee la receta que viaja DENTRO del artefacto sellado (el sidecar
.hammer/recipe.toml) y no de recipes/, porque seal_product_rootfs sirve a las
DOS vetas —product desde recetas locales y product_from_repo desde el repo
firmado— y sólo una tiene recipes/ a mano. El sidecar es la única fuente que
ambas comparten, que es lo que sostiene que converjan al mismo product-rootfs.
LA TRAMPA QUEDA ARMADA, NO CERRADA: el sidecar no entra al ArtifactHash, así que
un artefacto anterior al bloque [[service]] es un cache-hit válido que no se
reconstruye solo. Si el emisor devolviera lista vacía sin protestar, el producto
saldría booteando, verde y SIN SSHD. Falla ruidosamente, y hay un test que exige
que el mensaje siga explicando la causa Y dando el arreglo (regla 3 del
CLAUDE.md: un ausente falla ruidosamente, un vacío llega hasta el final diciendo
que todo fue bien).
LA MIGRACIÓN, CON CONTROL, y el hecho nuevo que dejó: openssh no estaba
certificado como reproducible ("el criterio es construye y corre", dice su
receta). Manifiesto de 33 sha256+modos ANTES de tocar nada, respaldo por
hardlink, borrado, rebuild bajo flock -o, comparación. Resultado: 33/33
ficheros, modos idénticos, y la ÚNICA diferencia en todo el árbol es
.hammer/recipe.toml. openssh REPRODUCE BIT A BIT.
Gotcha que costó el primer intento: store/ es bind-mount de /dev/sdb y work/
vive en /dev/sdc ⇒ `cp -al` cruza mounts y muere con EXDEV. El respaldo va
DENTRO del mount del store; un dot-dir ahí es invisible para store-gc.sh, que
sólo mira dirs ^[0-9a-f]{64}-.
`vigia-sonames.py` corría por defecto SÓLO sobre los cinco perfiles de escritorio
(`p.startswith("escritorio")`). O sea que `base`, `cli` y `servidor` —el objetivo de la mudanza—
**nunca se miraron**. Apuntándolo a ellos a mano:
== servidor 147 nodos · 183 sonames provistos · 1 sin proveedor
FALTA libffi.so.8 ← lo piden: python3
`python3` no arrancaba en las tres imágenes, por el mecanismo EXACTO que `musl-shared` y
`zlib-shared` arreglaron dos días antes: la receta canónica de libffi es `--disable-shared`, ningún
artefacto del cierre publica ese soname, y dentro del lab el agujero lo tapa el rootfs de Alpine —
así que el build pasa y la imagen sale rota. `libffi-shared` ya existía en el corpus y ya estaba
declarada en otros dos perfiles; sólo faltaba en `base`.
servidor 148 nodos · 186 sonames · **0 sin proveedor** (base y cli, igual)
## Y el vigía pasa a mirar TODOS los perfiles
⚠ **Es la tercera vez en dos días que un guardián de este repo mide menos de lo que su resumen
afirma**: `static-audit.sh` sólo globeaba `recipes/` y no las 5 colas (3 mentiras ocultas); el bloque
de raíces sucias sólo corría al hidratar un perfil y no veía las 649 recetas fuera de toda imagen
(qdrant con 69 M de basura); y éste dejaba fuera medio sistema. **Un «0 huecos» sobre la mitad del
sistema se imprime igual que uno sobre todo él.**
⇒ Al leer un guardián, leer su SCOPE antes que su veredicto: el glob, el bucle, de dónde saca la
lista. Los ocho perfiles dan 0 sin proveedor, así que ampliar el alcance no mete ruido — sólo deja de
esconder.
Tras devolver protobuf y abseil a `zig-cc`, qdrant cayó a deuda y se reconstruyó en el worker:
2 ficheros, sólo `usr/`, 70 M, `qdrant 1.19.1`. La limpieza del `/out/src` —el DESTDIR global que
heredaba el `make install` anidado de protobuf-src— aguanta el cambio de dep. REPRODUCE bit a bit.
El corpus queda en 907/907 sellado con deuda 0.