aacee245d63a789009844ead49b21492960f996b
478
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aacee245d6 |
takana: el worker pasa a /opt/takana y takana-farm.service
Renombrado el directorio del worker y la unit, con las tres units del repo (farm, deadman service y timer). Todas las rutas /opt/hammer del hub pasan a /opt/takana: eran defaults REMOTE= y HAMMER_DIR=, mas los systemctl/journalctl que nombraban la unit sin el sufijo .service, que el primer sed no casaba. Orden, para que no quedara partido: el hub siembra por rsync a una ruta fija, asi que se saco el cron ANTES de mover. Si se movia el worker con el hub apuntando a la ruta vieja, la siguiente cosecha recreaba /opt/hammer y quedaban dos arboles. Verificado de punta a punta: - el worker cerro un ciclo de build REAL desde /opt/takana (dunst sellado, 1/1) - una cosecha completa del hub contra la ruta nueva: siembra, manifiesto, los nueve grafos, static-audit (691 estaticos, MIENTEN 0) y estado pusheado - la unit vieja quedo deshabilitada y borrada; /opt tiene una sola entrada NO se tocan dos referencias a /opt/hammer que siguen siendo CORRECTAS: docs/23-plan-rehasheo.md describe rutas EMBEBIDAS en artefactos ya construidos —que literalmente dicen /opt/hammer/work en sus secciones .debug— y el HANDOFF de la noche de KDE es registro. Cambiarlas haria que los documentos mientan. |
||
|
|
17f8f4fb80 |
takana: la ruta del repo es /mnt/vvv/takana
El directorio se movio de verdad, asi que las referencias absolutas dentro del repo ya eran incorrectas. Tambien los ejemplos que usaban ~/hammer. NO se tocan docs/evidencia/: son REGISTRO de lo que se corrio ese dia, y reescribir una ruta ahi adentro falsifica la evidencia. Que nombren una ruta que ya no existe es correcto: existia cuando se midio. |
||
|
|
eb8f217245 |
gnome: el perfil y el hidratador decían cosas distintas — 21 paquetes no llegaban a la imagen
Verificando que las extensiones de la tanda anterior llegaran al rootfs apareció algo bastante peor que lo que fui a buscar: **`scripts/gnome/hydrate-gnome.sh` no lee `targets.toml`**. Arma el rootfs desde una lista de 14 raíces escrita a mano dentro del propio script, y de las 28 raíces que el perfil declaraba sólo DOS coincidían. MEDIDO recorriendo la clausura de esa lista (123 recetas) contra la membresía del perfil: **21 paquetes declarados en `escritorio-gnome` no llegaban al rootfs hidratado**. No son accesorios — son las hojas, que es justo lo que un perfil declara porque no es dep de build de nadie: atuq (el navegador) · foot (la terminal) · helix (el editor) · mpv · swayimg (el visor) zathura + zathura-pdf-poppler (el lector de PDF) · dejavu-fonts y dejavu-fonts-nerd (las FUENTES) xdg-desktop-portal-gnome · libnotify · desktop-file-utils bzip2-shared, expat-shared, libffi-shared, ncurses-shared, xz-shared (las .so que se cazaron el 2026-09-03 recorriendo NEEDED, justamente para que el rootfs no resolviera contra el lab) y las cuatro de ayer: las tres extensiones y gnome-tweaks O sea que el trabajo de descubrir que la imagen no tenía terminal, ni editor, ni una sola fuente —con su análisis largo escrito en este mismo fichero— estaba DECLARADO y no llegaba. Y la divergencia iba en los dos sentidos: 11 raíces de RUNTIME vivían sólo dentro del script y el perfil no las conocía (accountsservice, geoclue, libgdm, libgweather, upower, librsvg, ibus, los dos gi-*-typelibs, gsd-schemas y wireplumber). Sin ellas el shell no arranca: las carga por `imports.gi.*`, que es invisible para la clausura de deps. ARREGLO: las 11 se mueven a `targets.toml` con la justificación que el script ya traía bien escrita, y el script queda marcado como SUPERADO por `scripts/hydrate-profile.py`, que hidrata desde el perfil — la única declaración que existe. Su propia cabecera ya advertía de este modo de fallo («DOS fuentes de verdad para lo mismo, y divergen sin que nada lo diga»); esto es esa advertencia cumpliéndose. El perfil pasa de 28 a 39 raíces y de 190 a 204 nodos, deuda 0. VERIFICADO HIDRATANDO DE VERDAD, no leyendo: `hydrate-profile.py escritorio-gnome` proyecta 210/210 nodos (27993 ficheros, 3,5 G) y ahí están las tres extensiones, `gnome-tweaks` con su módulo `gtweak`, y los tres gschemas en el directorio GLOBAL. Después se corrió el bloque nuevo del lanzador contra ESE árbol: enumeró las tres, escribió el override y `glib-compile-schemas` salió 0 con stderr vacío. ⚠ DOS COSAS QUE APRENDÍ MIDIENDO Y CONVIENE QUE ESTÉN DICHAS: 1. **El hub NO puede hidratar GNOME hoy.** `gjs`, `gnome-shell` y `spidermonkey` salen «no sellado» localmente. No es deriva ni un grafo mintiendo: sus hashes vigentes SÍ están en `work/farm-sellados.txt` — o sea sellados EN EL WORKER — y la cosecha baja el manifiesto y no el store, a propósito (bajarlo deshacía la poda). `build-state` los cuenta bien porque mira también el manifiesto; `hydrate-profile.py` mira sólo el disco local. Los dos tienen razón. 2. **EXDEV entre bind-mounts, otra vez.** Hidratar a `work/` o a `work/out` falla con «Invalid cross-device link» AUNQUE `work/out` esté en el mismo /dev/sdb que el store: son bind-mounts distintos y `linkat()` no cruza montajes. Hay que hidratar accediendo al store por su montaje raíz (`--store /mnt/cosecha/store --into /mnt/cosecha/escritorios/…`). El mensaje de error del script lo dice y aun así me costó dos intentos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV |
||
|
|
e20a5f44d8 |
takana: --takana-bin, con --hammer-bin como alias
Era la ultima superficie de CLI que quedaba con el nombre viejo. Mismo patron que forja: el canonico es el nuevo, el viejo sigue aceptandose como alias. El default pasa a .../release/takana. El DESTINO no se toca: el binario se sigue instalando en /usr/bin/hammer dentro del rootfs del producto, porque ese rootfs se hashea. Moverlo cambia el hash del producto y obliga a rehacer el baseline del selfhost — es etapa 6 y decision aparte, no efecto colateral de renombrar un flag. Verificado en el subcomando correcto (bootstrap builder, no product): las dos formas se aceptan. 605 tests en verde. |
||
|
|
4e4a2c8d2a |
gnome: dash-to-panel, appindicator y gnome-tweaks — y la lista de activas sale de las recetas
GNOME deja de ser la cáscara: tres extensiones y la app que el proyecto no instala y todo el mundo
termina instalando. El perfil queda 190/190 con 28 raíces, deuda 0.
dash-to-panel v73 junta dash, ventanas y bandeja en una sola barra (lo que más cambia el uso)
appindicator v64 devuelve los ICONOS DE BANDEJA que GNOME quitó; sin ella las apps que usan
StatusNotifierItem corren y no tienen dónde mostrarse
blur-my-shell v72 (ya estaba; se le saca el override, ver abajo)
gnome-tweaks 46.1 tema, iconos, cursor, tipografías, los botones de minimizar/maximizar que
GNOME esconde, y el interruptor de las extensiones
⚠ LA LISTA DE ACTIVAS SE MUEVE A UN SOLO SITIO, y esto era una bomba de relojería. `enabled-extensions`
es un ARRAY: un override de GSettings lo escribe ENTERO. Con la primera extensión el override vivía
en su receta y funcionaba; con la segunda NO se habrían sumado — ganaría la que compile última
(orden alfabético) y las otras quedarían instaladas y MUERTAS, sin un solo error. Ahora la arma
`scripts/gnome/gnome-start-qemu.sh`, que ya compilaba los esquemas al arrancar, enumerando los uuid
presentes en /usr/share/gnome-shell/extensions. La política es «lo que la imagen instala, se
activa»; el dconf del usuario gana sobre el default.
Una receta NO puede ser ese sitio: hammer exige `[source]` en toda receta («missing field `source`»),
así que no hay forma de escribir una receta de pura política. Se probó el bloque contra un árbol de
juguete: lista las dos extensiones, salta un directorio sin metadata.json, el override compila con
stderr vacío y `gsettings get` devuelve las dos. Y si la lista cambia borra `gschemas.compiled` para
forzar recompilación — sin eso, una imagen ya arrancada ignoraría la extensión nueva en silencio.
⚠ EL HALLAZGO CARO: `msgfmt` de gettext-tiny REVIENTA con las formas plurales del árabe. SIGILL,
exit 132, «index 6 out of bounds for type 'size_t[6]'», y deja un .mo de 0 bytes. Reproducido FUERA
de la receta con el binario sellado. No es el idioma sino el mensaje: blur-my-shell compila su
ar.po sin problema porque no tiene NINGÚN plural (msgstr[5] = 0); el de tweaks tiene exactamente
uno. Acá se saca `ar` del LINGUAS y se pierde la traducción de UNA app, con el diagnóstico escrito
en la receta. ARREGLARLO ES OTRA UNIDAD DE TRABAJO: gettext-tiny la usan 108 recetas.
Cada receta usa el mecanismo de SU upstream, que no es el mismo en las tres:
blur-my-shell `make build` llama a `gnome-extensions pack` —el CLI que nuestro shell apaga— así
que se copia el árbol a mano, con el layout LEÍDO del zip de EGO y comparado
contra él (idéntico, sin sobras ni faltantes)
dash-to-panel su Makefile tiene un `install` que honra DESTDIR y hace lo de un paquete de
distro: gschema y locale a los directorios GLOBALES. ⚠ `VERSION=73` se pasa a
mano porque sin ella el Makefile hace `git describe` y el árbol viene por
`git archive`, sin `.git` ⇒ metadata quedaría con el marcador "version": 9999
appindicator meson. ⚠ `-Dlocal_install=disabled` explícito: en `auto` decide mirando si el uid
es 0, o sea que hoy acierta POR ACCIDENTE — y si el lab dejara de correr como
root instalaría en $HOME/.local y el artefacto sellaría vacío. Y `jq` va en deps
aunque sea JavaScript puro: su meson saca el uuid del metadata.json con jq
⚠ gnome-tweaks NO costó ninguna receta nueva, y eso hubo que medirlo: su meson declara siete deps
mínimas de runtime y las siete ya estaban selladas. La que parecía traer una cadena de Python
entera —`pygobject-3.0`— la provee `py3-gobject`, que YA ESTABA en el corpus con otro nombre y
publica `pygobject-3.0.pc`. El nombre no es el hecho, otra vez.
Sí costó dos deps que el configure reclamó y que son el patrón `.pc Requires` → `[deps].build`:
`appstream` (la pide `libadwaita-1.pc` y el error culpa a libadwaita, que está bien instalada) y
`desktop-file-utils` (meson la busca como PROGRAMA para `update-desktop-database`).
Sus deps de RUNTIME —python3 y py3-gobject— van declaradas: es una app de Python, si no están en la
imagen se instala y no arranca, y ningún build lo delata.
Las cuatro licencias, verificadas en la fuente pineada y NINGUNA leída del COPYING —cuyo apéndice
«How to Apply» trae siempre la frase «or any later version»—: blur-my-shell del README del commit,
dash-to-panel y appindicator de la cabecera de sus fuentes, gnome-tweaks de su etiqueta SPDX.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
|
||
|
|
a37b47a004 |
takana: la unit del worker fija TAKANA_DIR ademas de HAMMER_DIR
Medido: esta unit era el UNICO sitio fuera del repo que traia una variable vieja puesta (Environment=HAMMER_DIR=/opt/hammer), y por eso es lo que impide retirar la caida del codigo. Ahora fija las dos. Se ponen las dos y no solo la nueva porque un script viejo —o una copia del repo que todavia no sincronizo— solo mira HAMMER_DIR. Desplegada y verificada en dev.gioser.net: daemon-reload + restart, servicio active, y el entorno del servicio muestra las dos. |
||
|
|
476168bb07 |
takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa. El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces: las tres eran el MOTD que el script escribe DENTRO de la imagen construida — texto del producto, no comentario del script. Se cambiaron aparte y a propósito, que es rebranding, no limpieza. Y el hallazgo caro: casaba contra , que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate. La etapa 4 lo movió a y el script quedó casando NADA. No fallaba: imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja. Además 14 rutas de módulo en docs, que el barrido anterior no tocó porque no es frontera de palabra. |
||
|
|
1c3e185167 |
takana: las variables de entorno leen las dos formas, gana la nueva
TAKANA_X con caída a HAMMER_X (ADR 0016). No es un sed: estas variables son
contrato de usuario —knobs del instalador, el entorno del worker, mirror-env.sh—
y viven en perfiles de shell y units FUERA del repo, así que un renombre duro no
falla ruidosamente: la variable no aparece, se toma el default y el build se
comporta distinto sin que nada lo diga.
Rust: takana_core::env::{var,var_os} toma el nombre canónico y deriva el viejo
cambiando el prefijo — se le pasa TAKANA_* para que un grep del nombre nuevo
encuentre todas las lecturas. 5 tests, con DOS controles negativos: sin ninguna
de las dos no hay valor, y un nombre sin prefijo no inventa una caída.
Migrados los 17 sitios directos y los indirectos que el grep no mostraba
(bases_de_mirror, las constantes de kernel_cmd, ROOT_ENV de qorpa, env_path de
recover). takana-recover lleva la caída inline: es un mini-binario que se copia
a /usr/sbin y no vale arrastrarle una dep entera por dos líneas.
Scripts: 30 lecturas pasan a default, manteniendo el
nombre INTERNO de la variable para no tocar sus 190 usos.
Y donde el script EXPORTA en vez de leer, se ponen LAS DOS (mirror-env.sh y el
fragmento in-VM de bootstrap): ahí el lector puede ser un binario viejo —worker
sin recompilar, el /usr/bin/hammer pinado del baseline— que sólo conoce HAMMER_*.
La caída sirve para lectores nuevos; los viejos necesitan que la vieja siga puesta.
Verificado de punta a punta con el binario, no sólo con unit tests: HAMMER_LAB
sigue surtiendo efecto, TAKANA_LAB hace lo mismo, y con las dos gana TAKANA_LAB.
605 tests en verde y el hash de zlib sigue en b3:dc363f26… , intacto.
Cambio de comportamiento que va aparte y hay que decir: el hostname por defecto
de una instalación nueva pasa de 'hammer' a 'takana' (sólo si no se fija ninguna
de las dos variables).
NO se tocan: las rutas /var/lib/hammer de sistemas instalados, el volid
HAMMER_LIVE del ISO, ni el namespace HARKAQ_*, que es de otro subsistema.
|
||
|
|
24bcf1783c |
takana etapa 4: los 10 crates de librería y el CLI pasan a takana-*
hammer-{core,build,bootstrap,overlay,journal,mirror,upgrade,agent,recover,cli}
→ takana-*, con sus deps de workspace, sus identificadores en el fuente y las
referencias -p de los scripts.
VERIFICADO que no mueve nada del corpus: `takana hash recipes/zlib.toml`
devuelve b3:dc363f26… , idéntico a antes del renombre. Los nombres de crate no
entran en hash_inputs, pero eso se comprueba, no se supone. 600 tests en verde.
DOS BINARIOS SE CONGELAN, y no por prolijidad:
- `hammerd` — paquete Y binario. Es componente de Stage 1 de la distro (musl,
busybox, hammerd, arje-zero), lo supervisa arje-zero en el sistema arrancado,
`PRESEED=hammerd` lo nombra en selfhost-verify y sus bytes anclan el baseline
of_tree. El nombre del crate va en los símbolos ⇒ renombrarlo mueve los bytes.
- `hammer-recover` — el PAQUETE se renombra a takana-recover, el BINARIO no.
hammer-live-install.sh lo copia a /usr/sbin/hammer-recover en sistemas ya
instalados y hornea un hook de arranque que lo invoca por ese nombre:
renombrarlo rompe máquinas instaladas, no el repo.
Consecuencia que hay que anotar igual: al renombrar hammer-core, los bytes de
hammerd cambian de todos modos porque linkea contra un crate con otro nombre.
El baseline of_tree del selfhost hay que rehacerlo — es efecto de la etapa 4,
no de un cambio de hammerd.
Las referencias en comentarios de recetas y docs (rutas hammer-core/src/…)
quedan para la etapa 5: son texto, no mueven hash.
|
||
|
|
8730aad34e |
takana etapa 3b: las 49 invocaciones pasan a ./target/release/takana
Los llamadores EJECUTABLES: scripts/ (incluida toda la granja), los runbooks y CLAUDE.md. Seguro porque la 3a ya garantiza que el worker emite los dos binarios, y porque en farm-lab-sync.sh el cargo build remoto precede a la invocación remota en el mismo script. Verificado: sintaxis de los 49 (bash -n / py_compile — ojo que why-differs-barrido.sh es Python con extensión .sh) y `takana hash` devuelve hash real sobre el store. NO se toca en esta etapa, a propósito: - La variable de entorno HAMMER=. Es interfaz de los scripts entre sí y hay llamadores que la fijan; renombrarla va con la etapa 4. - docs/evidencia/ y los HANDOFF: son REGISTRO de lo que se corrió ese día. Reescribir un comando dentro de una evidencia la falsifica. - docs/state/: es generado, se regenera solo. - Los ADR y los docs de diseño: texto, y `hammer` sigue funcionando. Van con la etapa 5, que es la de churn de texto. |
||
|
|
7b6da600d6 |
takana etapa 3a: los cargo build emiten los DOS binarios
Va solo y ANTES de cambiar ninguna invocación, a propósito. El worker compila desde fuente (farm-worker-loop.sh) y la siembra excluye /target: si primero cambiara las llamadas a `takana` y después el build, habría una ventana en la que el worker sincroniza scripts nuevos y sigue teniendo sólo el binario viejo — y eso no falla ruidosamente, deja de cosechar en silencio. Con los dos emitidos, cualquier orden de sincronización queda sano. |
||
|
|
3379a1f170 |
licencias: las 26 que faltaban, y el guardián que las contaba mal
CAMPAÑA CERRADA: 1166/1166 recetas declaran `license`. Ninguna adivinada — cada una sale del
fichero de licencia de su fuente PINEADA (tarball del sha256 de la receta, o el commit exacto en
la forja), y la cita queda como comentario en la propia receta.
⚠ Y EL GUARDIÁN ESTABA MAL, que es el hallazgo que vale más que las 26. `licencias-rootfs.sh`
resolvía la receta por NOMBRE DE FICHERO (`ls recipes/$pkg.toml`), y el paquete se llama por su
campo `name`, que en 34 recetas NO coincide: nu.toml→`nushell`, dust.toml→`du-dust`,
incoming-kde/qtbase.toml→`qt6-qtbase`… Medido: **14 paquetes que SÍ declaran licencia salían como
«licencia desconocida»** y el guardián vetaba una imagen perfectamente publicable.
El falso veto se nota; el hermano silencioso NO: si existe un `<pkg>.toml` que pertenece a OTRO
paquete, la versión vieja reportaba la licencia EQUIVOCADA sin decir nada. Hoy no pasa —medido,
0 casos, y de los 34 nombres duplicados CERO declaran licencias distintas—, pero ahora es
imposible en vez de improbable.
El arreglo tuvo que ser por LOS DOS lados, y el primer intento rompió el otro: el grafo de estado
nombra sus nodos por el fichero (`dust`) y el artefacto del store por `name` (`du-dust`), así que
resolver sólo por `name` dejaba a `dust` sin licencia. Ahora busca por `name` y cae al fichero.
Medido en los dos sentidos: corpus entero 1128/1128, perfil base+cli 81/81, cero sin licencia.
Y probado CON ROTURA A PROPÓSITO además del control, que es lo único que distingue a un guardián
que sirve de uno que nunca salta:
paquete inexistente en la lista → exit 1 y «ESTA IMAGEN NO SE PUEDE PUBLICAR»
control (zlib nushell qt6-qtbase lsof tzdata) → exit 0, y escribe los textos
(⚠ ojo al medir: `script | tail` devuelve el exit de `tail`. La primera corrida dijo exit=0 sobre
la rotura y no era el guardián, era el pipe.)
SE LEVANTA EL VETO QUE SDD 20 DEJÓ ESCRITO. Decía que `base` y `cli` iban con 2 paquetes cada una
con binarios y licencia desconocida: `lsof` y `tzdata`, «que necesitan la vía LicenseRef- y siguen
vetando a propósito». Hechos los dos, con su texto real en licenses/:
lsof → LicenseRef-lsof (licencia propia de Purdue, sin identificador SPDX)
tzdata → LicenseRef-tz-public-domain (su LICENSE: «all files in the tz code and data … are in
the public domain»; los tres ficheros BSD-3-Clause que menciona NO se instalan — la
receta sólo compila zic y deja /usr/share/zoneinfo)
⚠ DOS QUE NO SE PUEDEN REDISTRIBUIR, y ahora el veto los ve:
duplicacy NO ES LIBRE. Su LICENSE.md: «Free for personal use or commercial trial; non-trial
commercial use requires per-computer CLI licenses … $50 per year»
waybackurls NO DECLARA LICENCIA: en el commit pineado la raíz es .gitignore, README.mkd,
go.mod, main.go y script/ — sin LICENSE ni COPYING, y el README no la menciona. Sin
concesión expresa, el defecto es «todos los derechos reservados»
Los dos con LicenseRef y un texto en licenses/ que explica qué hay, en vez de dejar el campo vacío,
que se lee como «todavía no lo poblamos». Ninguno está hoy en un perfil de imagen; si alguien los
mete, el guardián corta.
De paso queda escrito el texto de `LicenseRef-qorpa-ajena-no-enumerable`, que ya se usaba en
steam-runtime-sniper y no tenía fichero; y `licencias-textos.sh` bajó los canónicos nuevos
(BSL-1.0 para boost, GCC-exception-3.1 que ya hacía falta).
Hueco conocido y anotado en la receta: `XFree86-1.0` (rama del OR de hwdata) se queda sin texto —
SPDX no publica ese identificador, sólo XFree86-1.1, que es otra licencia, y el tarball lo nombra
sin incluirlo. El guardián avisa y no veta, que es correcto: la otra rama del OR es la GPL y su
texto sí está.
NADA SE RE-HASHEA: `license` está fuera de `hash_inputs`. Verificado, no supuesto — `hammer hash`
sobre pigz, lsof y boost después de editarlas devuelve el hash cuyo artefacto YA está en el store.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
|
||
|
|
f984cbf9e7 |
frontera: derivar el grafo de la COLA — sway y cosmic eran invisibles
La lista de grafos que consulta 'frontera' estaba escrita a mano y sólo tenía corpus, kde y gnome. Consecuencia: 'yupana frontera escritorio-sway' y '... escritorio-cosmic' fallaban con 'perfil sin nodos en ningún grafo de estado (¿regeneraste con build-state.py?)' — culpando a un grafo viejo con los cinco frescos de hacía minutos. Dos escritorios enteros sin poder responder '¿qué me falta?'. ⚠ QUINTA vez que cae el mismo cable en este repo: GNOME (2026-07-27), COSMIC (2026-08-03) y wlr/sway (2026-08-26) en cosecha-cron.sh, keystones y duplicados (2026-09-08) en el mismo sitio, y ahora acá. Las cuatro anteriores se arreglaron AGREGANDO una línea, que arregla el caso y deja la trampa puesta. Esta vez el fichero se DERIVA de la cola que el perfil declara: corpus → build-state.json, incoming-<x> → build-state-<x>.json. Una cola nueva trae su grafo sola. Verificado con control: 'base' sigue usando build-state.json y dando los mismos 41 candidatos. Y lo que estaba tapado aparece — sway 175, cosmic 148, gnome 191, kde 276. El triaje pasa de 172 a 365 candidatos (193 nuevos). No es deuda nueva: es deuda que no se podía ver. Los que más recetas piden encabezan la cola — gi-docgen y vala con 9 cada uno. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
bdb5f7df9a |
nix instalado en gioser: la cadena frontera→triaje vuelve a correr
'yupana frontera' llevaba desde el 2026-08-10 sin poder correr acá porque el hub no tenía nix.
Instalado desde el repo de Artix (extra/nix 2.35.2). Dos cosas que costaron medirse:
· El paquete quedó ROTO al instalarse: le faltaban libboost_{context,iostreams,url}.so.1.92.0
porque el sistema tiene boost 1.91 y 280 paquetes sin actualizar. NO se hizo 'pacman -Syu':
280 paquetes en un server protegido, sin backup, que hospeda la granja, gitea y el runner de
CI no es una decisión que tome un arreglo de tooling. El ensayo en seco mostró que la
reparación acotada eran TRES paquetes exactos (boost-libs, source-highlight, gdb) sin
cascada, y eso sí se hizo. Control después: gdb 17.2 sigue arrancando.
· El store por defecto iba a ~/.nixstore, o sea a '/', que tiene 21 G libres frente a los 126
del volumen. Llenar '/' no rompe una tanda, rompe la máquina. NIX_ROOT pasa a apuntar a
work/nixstore, que además ya es el sitio de lo pesado-y-regenerable. La caché git de nixpkgs
(~69 M) NO la gobierna esa variable —nix la pone en XDG_CACHE_HOME— así que en gioser está
mudada al volumen con un enlace: ~/.cache/nix -> /mnt/vvv/nix-cache.
Con eso, la frontera vuelve a dar respuesta, y es pequeña como corresponde a un corpus cerrado:
172 candidatos → 150 opcionales, 11 provistos, 5 nix-ismos, 4 HUECOS reales (polkit-qt-1,
qqc2-breeze-style, shared-mime-info, xwayland) y 2 pendientes de clasificar (mesa-libclc,
python3-3.14.7-env).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
|
||
|
|
a19fc8bcf0 |
seed-graph: decir que falta nix, en vez de reventar con un traceback
'yupana frontera <perfil>' —el verbo que responde qué pide upstream y no tenemos— moría con
veinte líneas de traceback terminadas en FileNotFoundError: 'nix'. Eso tiene la cara de un bug
de código y manda a depurar el script, cuando lo único que falta es una herramienta que en esta
máquina nunca se instaló: el hub gioser no trae nix, y los docs/state/seed-*.json del repo se
generaron en otra máquina el 2026-08-10. El coste no es el susto, es el desvío.
Ahora es una precondición explícita que dice qué falta y qué hacer, y sale 2.
⚠ Dos errores míos que los controles cazaron en el acto, y por eso van escritos:
· Eximí a --calibrar de la comprobación 'porque no usa nix'. Lo usa: el control lo mostró
imprimiendo 'nix eval lote 1 (3 nombres)…' y muriendo igual. La exención era una suposición
con forma de hecho.
· Con la precondición delante del uso, 'seed-graph.py' a secas pasó a quejarse de nix en vez
de explicarse. Un fallo de entorno va DESPUÉS del uso.
Verificado en los tres caminos: sin args → uso; --frontera → mensaje de entorno; --calibrar →
mensaje de entorno.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
|
||
|
|
29b0d99e9d |
latido: regenerar keystones y duplicados — llevaban TRES DÍAS congelados
Cuarta vez que cae el mismo cable. Los comentarios del propio fichero ya lo cuentan para GNOME (2026-07-27), COSMIC (2026-08-03) y wlr/sway (2026-08-26): un frente que el latido no regenera envejece en silencio, y un grafo viejo miente con la misma cara que uno fresco. keystones.json y duplicados.json estaban parados en 2026-09-05. Con un agravante que los otros tres no tenían: keystones es el fichero que responde '¿qué construyo después?', así que rancio no se limita a envejecer — DESINFORMA. Medido: decía '1 nudo en deuda, camino crítico corpus/firefox → corpus/atuq' con los dos sellados hacía horas. Regenerado dice 0 nudos en deuda, que es la verdad. Van también al 'git add' Y al commit acotado por pathspec: regenerar sin publicar deja el fichero fresco en el hub y viejo para todos los demás. Probado con un ciclo real: 'keystones.json ✓ duplicados.json ✓ estado commiteado+pusheado'. ⚠ De paso, dos cosas que este rato dejó claras sobre mis propias comprobaciones: 'pgrep' y 'grep [c]osecha-cron' se autodetectan cuando MI comando menciona el fichero, así que la guarda '¿está corriendo?' dio dos falsos positivos. La comprobación fiable mira /proc/*/cmdline y pide que argv[0] sea un shell y argv[1] el script. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
381780aab4 |
fuentes: tapar el agujero de libtool y que el vigía distinga noticia de emergencia
El vigía decía 5 fuentes muertas y sólo UNA estaba en riesgo de verdad: busybox y freetype tenían el tarball en la caché local Y en el mirror, y firefox-pgo-profile apunta a .invalid a propósito. La que no tenía copia en ningún lado era libtool (incoming-kde), con ftpmirror.gnu.org devolviendo 502 — o sea una receta que ya no se podía reconstruir y nadie lo sabía. Arreglado: el tarball se bajó de ftp.gnu.org, se verificó contra el sha256 PINEADO (coincide byte a byte), y está en la caché local y en el mirror. La URL de la receta pasa al espejo que responde, y se midió que eso no re-hashea nada — ANTES y DESPUÉS dan el mismo b3:5c09bff5..., que es el ADR 0013 comprobado en vez de citado. Y el vigía ahora cruza cada fuente muerta con la caché local y el mirror, porque mezclar 'upstream caído con el blob guardado' (noticia) con 'caído y sin copia' (emergencia) hace que el número suba, nadie lo mire, y el día que uno importe esté enterrado entre cuatro que no. A un guardián lo mata el ruido. Ahora informa EN RIESGO aparte: == vivas 1175 / 1179 muertas arriba 4 EN RIESGO 0 ⚠ Y si el mirror no se puede consultar —el worker no tiene la clave del Storage Box— dice 'indeterminado', NUNCA 'EN RIESGO': un instrumento que no pudo correr no puede parecer un hallazgo. Probado con rotura a propósito y con los dos controles, que es lo que separa un guardián que sirve de uno que nunca saltó: sha inexistente + upstream muerto → EN RIESGO ✓ salta blob en caché + upstream muerto → cache-local ✓ no salta mirror inalcanzable → indeterminado ✓ no alarma en falso Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
f1d69f4f99 |
granja: la siembra de la flota fallaba justo en el caso que existe para arreglar
Con '.fleet' ausente —el hub recién clonado, o sea el único caso que importa— el 'cat' de un
fichero inexistente devuelve 1, y como el script corre con 'set -o pipefail' la tubería hereda
ese estado y el '&& mv' no dispara: el fichero se generaba correctamente y se quedaba sin
mover. Se añaden los '|| true' que faltaban.
⚠ Y por qué mi prueba no lo vio, que es lo que vale anotar: extraje el bloque real del script
para probarlo, pero le puse 'set -u' en vez del 'set -uo pipefail' que el script trae. Copié
el código y no el ENTORNO en que corre, y sin pipefail el caso pasaba. El extractor ahora lee
la línea 'set -' del propio script en vez de que yo la escriba de memoria.
Verificado con un ciclo REAL tras borrar .fleet a mano:
── cosecha-cron arranca
==> dev.gioser.net (154.197.1.13)
siembra ✓
manifiesto ✓ (worker: 764 · total: 4743)
.fleet quedó: dev.gioser.net 154.197.1.13
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
|
||
|
|
4db7d1e7ce |
granja: versionar la flota permanente, y acotar el commit del cron
'.fleet' está gitignored con razón —los workers efímeros entran y salen, y el reaper lo reescribe en cada ciclo— pero eso tenía un coste que nadie había pagado: un hub recién clonado nacía con la flota VACÍA y la granja quedaba desconectada EN SILENCIO. La cosecha decía 'flota vacía' cada 30 min, los artefactos del worker no volvían, estado-granja.sh reportaba 'no hay worker vivo' con el worker compilando, y nada fallaba. Así se descubrió esto hoy, por casualidad. Se separa lo que debe sobrevivir a un clon (scripts/farm/flota-permanente, versionado) de lo que es estado de ejecución (.fleet). cosecha-cron siembra .fleet desde el fichero fijo al empezar cada ciclo, uniendo por nombre. Probado con la siembra extraída del script real: hub recién clonado (.fleet ausente) → queda dev.gioser.net ← el caso que rompía con un hworker-3 efímero ya dentro → conviven los dos, sin duplicar ejecutada dos veces más → idempotente, sigue 1 línea por worker comentarios del fichero versionado → 0 se cuelan Y de paso el commit del propio cron pasa a ir acotado por pathspec. Hacía 'git add <rutas>' + 'git commit' a secas, que es justo lo que la regla 2 de CLAUDE.md declara insuficiente: el índice es compartido y el commit se lleva el índice entero. Pesa más acá que en ningún sitio porque corre desatendido cada 30 min mientras hay agentes trabajando. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
d45c0688e7 |
granja: el worker es el LXC PRESTADO (€0) — que no se vuelva a perder
El dato ya estaba en memoria y aun así se perdió dos veces, así que ahora vive en los sitios que se leen sin buscarlos: regla 1 bis de CLAUDE.md (que carga todo agente en cada sesión) y la skill 'granja' del repo. Se compila en dev.gioser.net PARA NO GASTAR HETZNER; no se levantan cajas hcloud salvo petición explícita. Y se arregla la fragilidad que salió al mirar: el reaper de cosecha-cron expulsa de .fleet todo lo que no esté en hcloud, y el LXC se salvaba SÓLO porque su nombre contiene la subcadena 'gioser' y caía en una lista negra que existe para proteger al hub de Hetzner — no tiene nada que ver con él. Medido con el bloque real del reaper contra un .fleet de juguete: nombre CON 'gioser' → 'en LISTA NEGRA ⇒ intocable' sobrevive el MISMO host como 'pruebasia-lxc' → 'ya no existe en hcloud' .fleet VACÍO O sea que la granja se mantenía conectada por una casualidad de nombre, y con otro nombre volvía a 'flota vacía' en el primer ciclo sin que nada fallara. Ahora el reaper pregunta por SSH si el host responde, que es preguntarle a la máquina en vez de al nombre. Verificado en las dos direcciones, con control: host vivo, nombre sin 'gioser' → 'no es de hcloud pero RESPONDE ⇒ se queda (€0)' host que no responde → 'no responde ⇒ lo saco de .fleet' (intención original) Queda anotado también que .fleet está gitignored: un hub recién clonado nace con la flota vacía y la granja queda desconectada en silencio — la cosecha dice 'flota vacía', los artefactos no vuelven, y estado-granja.sh reporta 'no hay worker vivo' con el worker compilando. Es como se descubrió esto hoy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
0d7beefb60 |
cazadores: setsid + matar el GRUPO — mataban el bwrap y el firefox sobrevivía
Segunda vez el mismo día: un firefox colgado de la verificación A/B se quedó con el lock de build de la granja. La causa es del arnés, no de firefox: 'kill $BW' mata el bwrap EXTERIOR, que no es el init del namespace de PID, así que el proceso de dentro sigue vivo — y si el arnés corría bajo flock, hereda el fd del lock y deja a la granja muda hasta que alguien mire. Medido con control negativo y positivo: matar sólo el bwrap → queda 1 firefox vivo (reproduce la fuga) setsid + matar grupo → quedan 0 (arreglado) Se documenta además que el 'flock' del uso lleva '-o', que no es opcional. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
20a719f9c0 |
cuelgue headless: verificación A/B y evidencia — 98 segfaults contra 0
arnés viejo (sólo CONTENT) 98 segfaults · 1 colgada · 49/50 screenshots arnés nuevo (los cinco) 0 segfaults · 0 colgadas · 50/50 screenshots Queda dicho en la cabecera qué prueba esto y qué no. El MECANISMO sí: 98 → 0, exactamente 2 por corrida, en el 100% de las corridas; un efecto determinista se refuta con pocas muestras. El CUELGUE no: 1 contra 0 en 50 pares no es significativo — con la tasa medida (3 en 130 ≈ 2,3%) ver cero en 50 tiene ~31% de probabilidad aunque nada hubiera cambiado, y harían falta ~200 corridas para un negativo convincente. Lo que sí sostiene: las tres colgadas observadas tienen la MISMA huella exacta (7 fallos de lanzamiento de pestaña, 1 de rdd, 6 messageManager is null, cero screenshot) y las tres cayeron donde los ayudantes revientan. Ninguna apareció sin el mecanismo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
5247821455 |
granja: el lock ya no se lo puede quedar un proceso fugado
El lock lo sostiene la descripción de fichero abierta y los hijos la heredan: con 'exec 9>' +
'flock 9' cualquier descendiente que sobreviva al script retiene el lock para siempre, y el
siguiente que lo pida espera sin que nada falle. Medido hoy: dos firefox colgados de una caza
sobrevivieron al kill de su bwrap y dejaron a la granja hora y media sin poder compilar.
Dos formas de defecto ⇒ dos arreglos, y no son intercambiables:
· cosecha-cron, campana-deuda, harvest-harkaq — el lock cubre el script ENTERO, así que se
re-ejecutan bajo 'flock -o' (cierra el fd antes de ejecutar). Ir poniendo '9>&-' comando a
comando ahí es jugar a los topos. '-E 77' separa 'estaba ocupado' de 'el trabajo falló',
que con el 9> no se distinguían.
· farm-worker-loop, latido — el lock vive en un subshell / es a propósito el mecanismo de
vida, así que basta '9>&-' en los hijos. En latido NO se puede usar -o: ahí el fd retenido
ES como --status sabe que el latido vive sin pidfile. Su modo de fallo era el peor de
todos: un nieto fugado dejaba el latido MUERTO PERO APARENTANDO ESTAR VIVO.
Medido antes de elegir, no deducido del manual:
exec 9> + flock 9 → el nieto RETIENE (control negativo: reproduce el fallo)
exec {L}> (fd auto) → el nieto RETIENE (bash NO lo marca close-on-exec)
flock -o / 9>&- hijo → lock LIBRE
Y probados los cinco sobre el fichero real, no sobre una maqueta: copias truncadas justo tras
el bloque del lock, invocadas con RUTA RELATIVA DESDE OTRO DIRECTORIO (que es lo que rompe un
$0 sin resolver — de hecho la primera versión de harvest-harkaq calculaba YO después del cd y
habría fallado ahí). Los tres entran, ninguno deja el lock tomado por el nieto, y la exclusión
mutua sigue funcionando con su mensaje y su exit 0 — que es lo que un arreglo de locks puede
romper en silencio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
|
||
|
|
b221f06c01 |
arneses: los CINCO sandboxes de Gecko apagados, no un subconjunto a ojo
Cada arnés headless tenía su propio subconjunto de MOZ_DISABLE_*_SANDBOX, elegido en su momento sin medir: banco 1 de 5, perfilar 4, codecs 3, ruteo 3, nativo 2, inicio 2. Con cualquier subconjunto incompleto el sandbox de Firefox sigue intentando montar su user-namespace dentro del de bwrap, falla con 'uid_map: EPERM' y deja un ayudante 'Sandbox Forked' muerto de SIGSEGV por cada intento — en el 100% de las corridas, después de escribir el PNG y por eso invisible. Medido: 2 cadáveres por corrida con sólo CONTENT apagado, 0 con los cinco. Verificado sobre el arnés ya editado: segv=0 EPERM=0 screenshot=sí. Acá no se pierde seguridad: bwrap ya es la jaula, el sandbox de Gecko es redundante y lo único que hace dentro es fallar. ⚠ Cambia las condiciones de medición del banco de PGO: las cifras de docs/26 se tomaron con el arnés viejo. Los cocientes del PGO comparan dos firefox bajo el mismo arnés y deberían aguantar; la línea base absoluta de arranque no tiene por qué. Anotado en la cabecera. No se tocan scripts/wlr/dunst-headless.sh (otro agente trabajando en él) ni los cazadores, que conservan el entorno viejo a propósito para poder reproducir la condición original. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
b405fc77df |
cuelgue headless RESUELTO: el sandbox de Firefox no puede montar su userns dentro de bwrap
No era la presión de memoria. La caza bajo presión (4 niveles × 20 corridas, con el nivel de
presión MEDIDO por MemAvailable y PSI) reprodujo el cuelgue por primera vez en 282 corridas
—2 de 80, 2,5%, compatible con el 3,5% original— pero SIN dosis-respuesta: los dos cuelgues
cayeron en el nivel con PSI 0,00 y los dos niveles apretados dieron cero.
Tres hipótesis muertas, cada una con su medición:
· OOM se lleva al hijo → oom_kill de /proc/vmstat no se movió (delta=0) y los hijos vivían
· presión de memoria → sin dosis-respuesta
· fork server de Gecko → A/B del pref: el proceso forkserver desaparece (35 muestras → 0,
o sea que el pref hizo efecto) y siguen los mismos 2 segfaults
La causa apareció comparando el log de una colgada con el de una BUENA, que es lo que faltaba:
en las 6 buenas TAMBIÉN revientan 2 hijos con SIGSEGV, después de escribir el PNG y por eso
invisibles. Muestreando /proc adentro, se llaman 'Sandbox Forked': los ayudantes que el sandbox
propio de Firefox forkea para montar su user-namespace, que no puede montar porque ya estamos
dentro del de bwrap ('writing /proc/self/uid_map: EPERM').
Las tres cantidades bajan juntas hasta cero, que es forma de cadena causal:
sandbox completo segv=7 SIN screenshot EPERM=9 SandboxForked=725
content.level=0 segv=2 con screenshot EPERM=2 SandboxForked=68
los 3 prefs .level a 0 segv=1 con screenshot EPERM=1 SandboxForked=33
las 5 MOZ_DISABLE_* segv=0 con screenshot EPERM=0 SandboxForked=0
Y el sandbox completo cuelga DETERMINISTA (rc=124 a los 180 s). Comparte familia con el
intermitente pero no está probado que sean el mismo: al determinista le faltan los 'Failed to
launch'. Queda dicho como lo que es.
Arreglo para cualquier firefox headless en bwrap (donde el sandbox de Gecko no aporta nada,
porque bwrap ya es la jaula):
MOZ_DISABLE_{CONTENT,GMP,RDD,SOCKET_PROCESS,UTILITY}_SANDBOX=1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
|
||
|
|
8ff6ae7b3a |
pgo: el control que faltaba — el arranque eran 16 s y diluía todos los porcentajes
El banco medía el ciclo COMPLETO del proceso (arrancar → renderizar → capturar → salir) y nunca se midió cuánto de eso era arranque. Con una página vacía como control, y las 4 cargas × 2 variantes en UNA sola sesión intercalada (restar entre corridas de días distintos no vale: la misma variante deriva ~1%): maquetación trabajo 7281 → 4744 ms -34,8% (publicado: -10,9%) carga ajena trabajo 4550 → 4634 ms +1,8% = cero, y es buena señal SunSpider trabajo -34 → -6 ms bajo el ruido Dos correcciones al documento: 1. El -10,9% es correcto para el ciclo completo pero se leía como 'firefox 11% más rápido'. Sobre el trabajo de la página el PGO rinde -34,8%: tres veces más. El error subestimaba el propio resultado. 2. La explicación que di del SunSpider era aire. Su trabajo mide -34 ms, NEGATIVO: la página con el benchmark tardó menos que la vacía. No había nada que medir, y yo le colgué encima una teoría sobre el JIT. La teoría puede ser cierta; esta medición nunca la probó. Las tablas viejas se dejan sin retocar, con un aviso arriba: el error de método enseña más que el número corregido. Todo el rigor estadístico estaba puesto sobre una cantidad que no era la que yo creía medir, y ninguna repetición lo habría revelado — sólo un control, que costó siete corridas de una página vacía. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
8c1378cb1f |
cazador: evidencia a favor de la hipótesis de presión de memoria
Al montar otro banco, la carga estaba en 3,34 (contra 0,13 de las cuatro cazas) porque otro agente arrancó un cargo, y kswapd0 consumía CPU con CERO memoria libre: recuperación de páginas bajo presión. Cada corrida mapea un libxul de 227 MB. Es exactamente la condición que las cazas NO tenían y que los dos bancos con cuelgues SÍ. No es prueba —el estado de aquellas ventanas no se reconstruye— pero es la primera pista concreta, y cambia dónde buscar: cazar BAJO PRESIÓN de memoria, no con la máquina ociosa. |
||
|
|
11e2c5f3a2 |
cazador del cuelgue en headless: NO reproducido en 202 corridas, y qué descarta
Dos corridas del banco de PGO se colgaron en 120.034 y 120.030 ms — exactamente el timeout del arnés, o sea bloqueos y no lentitud. Dos de 56 (~3,5%). NO SE REPRODUJO. 202 corridas en cuatro condiciones, cero cuelgues: un binario, un sandbox compartido ......... 30 · 0 4 binarios alternando, un sandbox ......... 60 · 0 (descarta churn de caché) 4 binarios, UN SANDBOX NUEVO POR CORRIDA .. 56 · 0 (descarta los namespaces) ídem con LAS PÁGINAS EXACTAS del banco .... 56 · 0 (descarta la página) Si la tasa fuera 3,5%, ver cero en 202 tendría probabilidad ~0,06%. La tasa real bajo estas condiciones no es ésa, y lo que falta está fuera de ellas. UN ERROR DE MÉTODO QUE COSTÓ 146 CORRIDAS, anotado en el script para no repetirlo: las tres primeras cazas usaron flex.html y tablas.html porque las tenía a mano, cuando las colgadas habían sido en fuera-del-corpus.html y del-corpus.html. Estuve probando una condición que NO era la observada, creyendo que sí. Antes de concluir «no se reproduce», comprobar que el instrumento reproduce lo que dice reproducir. Lo que sí se aprendió, y acota: es TODO O NADA. En 202 corridas ninguna pasó siquiera de 45 s — o terminan en ~20 s o se bloquean hasta el timeout. No es degradación, es bloqueo. Sobrevive la hipótesis que no se puede probar retroactivamente: contención transitoria de otra cosa en la máquina —que es compartida con otros agentes— durante esas dos ventanas. El script queda para que la PRÓXIMA vez se capture en el acto (wchan, syscall, state, memoria y carga del host, y el log del navegador de esa corrida) en vez de empezar de cero. |
||
|
|
639948f2cd |
atuq: la página de inicio y la pestaña nueva, verificadas contra el propio motor
La v0.3 las puso por extensión de sistema y quedó como afirmación. Que el XPI
esté en el artefacto no dice nada: el override lo puede rechazar el gestor de
extensiones, lo puede pisar una política, o el navegador puede arrancar con el
chrome viejo cacheado. Ninguna de las tres falla ruidosamente — se ve una pestaña
nueva perfectamente normal, que es de otro.
Se mide preguntándole AL MOTOR, no mirando el disco: una sonda con permiso
`browserSettings` lee `homepageOverride` y `newTabPageOverride`.
positivo moz-extension://…/inicio.html (las dos)
control about:home · about:newtab
El control negativo borra el XPI de `inicio` dentro del overlay temporal —el
rootfs real no se toca— y exige el resultado contrario.
Y una escotilla `ATUQ_DIR` en las dos sondas nuevas, con su aviso a gritos: el
corpus es compartido y hoy mismo el perfil PGO v2 de otro frente re-hasheó
`firefox` y con él `atuq`, así que el artefacto VIGENTE no existe en ningún store
hasta que alguien pague un build de horas. Sin escotilla no se puede correr una
sola prueba de atuq en esa ventana; con ella se corre contra un artefacto viejo A
SABIENDAS, y por eso el resultado no se puede citar como «atuq de hoy pasa».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
e74d449655 |
perfilar.sh: la corrida de entrenamiento PGO, con las tres defensas puestas
Estaba en un scratchpad y se perdía; ahora vive en el repo con el porqué de cada guarda, porque las tres se pagaron durante la primera corrida: 1. --unshare-pid en el bwrap. Sin él los hijos SOBREVIVEN al sandbox: un http.server quedó vivo 19 HORAS, retuvo el puerto y envenenó las corridas siguientes con «Address in use». 2. Puerto aleatorio por corrida. Elimina la colisión de raíz en vez de detectarla: si otro proceso tiene un puerto, este arranque usa otro. 3. Marcador único por corrida, servido desde una copia ESCRIBIBLE del corpus. La versión anterior horneaba el marcador como constante del script, y eso rompía justo lo que el chequeo existe para probar: el servidor huérfano servía el MISMO fichero de marca y el chequeo lo daba por bueno. Verificaba «alguien sirve este contenido», no «este servidor es el mío». Con el marcador por corrida, un servidor ajeno devuelve otra cosa y se aborta. Y queda escrito por qué los sandboxes de Firefox van apagados en el entrenamiento: con ellos los hijos mueren con signal 11, el proceso que renderiza no nace, el servidor registra 0 GET y el único .profraw es el del padre arrancando — un perfil de nada, con todo en verde. Es lo que hace el profileserver.py de Mozilla. Sólo aplica al entrenamiento. La espera al servidor va con reintento y no con un sleep fijo: 2 s concluían «no responde» sobre uno que sí iba a responder. |
||
|
|
c13be792ab |
atuq: el camino de native messaging EXISTE — sonda, y tres cosas que no eran obvias
Todo el §6 del SDD 26 —sct, descargas al CAS, archivo con RAG, torrent— pasa por
un solo mecanismo: un proceso Rust hablando native messaging con la extensión.
Antes de escribir ese crate en tawasuyu conviene saber si el camino existe en
NUESTRO build, que es propio, rebrandeado y con MOZ_REQUIRE_SIGNING vacío.
Existe. Una extensión de diez líneas y un host de tres:
positivo MENSAJE {"ok":true}
control DESCONECTADO No such native application puente_atuq
El control no sólo falla: NOMBRA la causa, que es lo que confirma que la ruta del
manifiesto es la que se probó.
TRES COSAS MEDIDAS QUE NO ERAN OBVIAS:
1. El manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/`, NO en el
appdir. Gecko lo busca por `XRESysNativeManifests`, que en Linux sale de un
`/usr/lib/mozilla` compilado, y el rebranding a atuq no lo mueve.
2. Un host que escribe y SALE pierde el mensaje. La primera sonda hacía printf y
terminaba: el puerto llegaba a onDisconnect «sin error y sin mensaje», o sea
el peor informe posible — parece que el camino no existe. Con un sleep detrás
del printf, el mensaje aparece. El host de verdad es un proceso largo, así que
en producción no se nota; en una prueba, sí.
3. `ExtensionSettings.install_url` con `file://` NO instala nada, y sin una línea
de log. Probado con normal_installed y con force_installed, y con el XPI dentro
y fuera del appdir: ninguna instala. Lo que instala las extensiones de atuq es
el ESCANEO de `distribution/extensions/`. La política sirve para fijarlas y
configurarlas; leerla como «esto es lo que las instala» es un error fácil,
porque los dos mecanismos apuntan a los mismos ficheros y se tapan uno al otro.
`sendNativeMessage` no sirve para diagnosticar: devuelve «An unexpected error
occurred» para todo. El error del puerto de `connectNative` es el único que
nombra la causa.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
8a0496dbb3 |
corpus de maquetación para el PGO: 10 páginas nuestras
Medida la ganancia del PGO ayer: -9,4% en una página de DOM/maquetación y -1,0% en 3d-raytrace de SunSpider. La razón de la diferencia es que un benchmark JIT-bound es casi CIEGO al PGO: el bucle caliente no lo ejecuta el C++ de SpiderMonkey sino el código máquina que el JIT emite en runtime, y el PGO optimiza el intérprete, el GC y el propio JIT — no lo que el JIT produce. El corpus de Mozilla está dominado en número de páginas por SunSpider, o sea que entrenaba mucho justo donde el PGO menos rinde. Estas diez ejercitan el camino que sí es C++ de punta a punta: flex flexbox anidado, wrap, alineaciones rejilla CSS Grid: pistas, áreas nombradas, auto-fit tablas tablas grandes con table-layout FIJO y AUTO (dos algoritmos) texto columnas, shaping, reflujo por cambio de ancho selectores DOM profundo contra 600 reglas, muchas que NO casan pintado degradados, sombras, opacidad, mix-blend-mode transformar transforms y contextos de apilamiento svg paths, degradados, clip desbordes overflow anidado, position:sticky, scroll programático reflujo layout thrashing: leer y escribir geometría alternadamente Reglas que cumplen todas, y no son de estilo: · DETERMINISTAS (LCG propio, ni Math.random ni Date ni red) — dos corridas hacen lo mismo, así que dos perfiles difieren por el timing de los contadores y no por haber visitado caminos distintos. · AUTOCONTENIDAS — el perfilado corre sin red. · NUESTRAS — nada de páginas ajenas capturadas, que traerían licencia y fragilidad. · ACOTADAS — 16 a 32 s cada una; el perfilado arranca un navegador POR PÁGINA. Validadas las diez contra el firefox sellado: todas renderizan y producen captura. `flex` hubo que acortarla de 40 a 12 cajas raíz: con 40 la página medía 68.241 px de alto y la captura moría con «Failed to allocate a surface due to invalid size». El layout ocurría igual, pero sin captura no se ejercita el pintado, que es justo la mitad que se venía a entrenar. |
||
|
|
7d0dc72de0 |
test-atuq-ruteo: los puertos fijos daban un fallo del producto que no existía
Esta prueba dio «NO concluyente — la pestaña sin contenedor no salió directa»
dos veces seguidas, y la causa no tenía nada que ver con atuq: **otro frente de
esta misma máquina tenía levantado un `python3 -m http.server 8099`** desde hacía
dos horas. La oreja del destino no podía atarse, no llegaba nada, y el informe
publicaba un fallo inexistente. En un repo que comparten varios agentes, un
puerto fijo es estado compartido sin dueño.
Y había un segundo bug que es el que lo hizo dañino: el `bind` vivía DENTRO del
hilo de la oreja, donde `fatal()` no puede matar el proceso — `SystemExit` en un
hilo secundario sólo termina ese hilo. Así que la prueba imprimía «el puerto está
ocupado, la medición no valdría nada» y **seguía adelante hasta publicar un
veredicto**. Un guardián que avisa de que no puede medir y mide igual es peor que
uno que no mide.
Ahora las dos orejas se atan en el hilo principal, con el puerto 0: lo elige el
kernel. Si no hay puertos, no hay prueba.
Con eso, y contra el atuq de hoy (d36ae188, el del arreglo del LD_LIBRARY_PATH):
positivo destino 40843 · proxy 38503 → GET /directo al destino, SOCKS5 al proxy
control destino 40917 · proxy 37113 → las dos directas, cero al proxy
O sea que el ruteo por contenedor del §6.8 sigue en pie y no lo rompió nada de
hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
ec45c3fa97 |
dunst-headless: --via-atuq — la cadena entera, empezando en una página web
El modo por defecto prueba la cadena del SISTEMA (`notify-send` → bus → dunst).
Éste prueba la que motivó todo el hilo del §6.10: una página llama a
`new Notification(...)`, `libxul` hace `dlopen("libnotify.so.4")` —que no es
NEEDED de ningún ELF, o sea invisible para cualquier auditor—, eso habla D-Bus,
el bus ACTIVA dunst y dunst dibuja. Cinco piezas, y la única forma de saber que
están las cinco es verlo.
El veredicto acá no puede ser el diff de píxeles solo: el navegador ocupa la
pantalla y repinta por su cuenta. Lo decisivo es el `onshow` del objeto
Notification, que es el motor diciendo que el sistema ACEPTÓ la notificación; el
diff queda como corroboración y la captura «antes» se toma con atuq ya pintado.
--via-atuq MOSTRADA #33 · 151.174 píxeles
--via-atuq --negative-control «ERROR al mostrar» · 0 píxeles
El control negativo también se lee distinto según el modo, y no por comodidad:
sin el .service, lo que tiene que faltar en el modo navegador es el onshow.
Captura en docs/evidencia/atuq-notificacion-web-sway-2026-09-07.png: dos globos
de dunst sobre la ventana de atuq, en sway headless, sin que nadie haya lanzado
el daemon a mano.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
4f9ee5430f |
dunst: sway ya atiende las notificaciones, y hay un píxel que lo prueba
Era la única mitad que quedaba abierta del mapa del §6.10: KDE atiende
`org.freedesktop.Notifications` con plasma-workspace, GNOME con gnome-shell y
COSMIC con cosmic-notifications; sway no tenía a NADIE, así que una página que
pedía notificar mandaba el mensaje al bus y se lo comía el silencio.
`dunst` 1.12.2 y no `mako` —que es el de la casa wlroots— por dos razones
medidas: el nombre `mako` YA ESTÁ OCUPADO en el corpus por el motor de plantillas
de mesa, y el cierre del perfil incluye las herramientas de build, así que la
imagen terminaría con dos artefactos homónimos peleando por las mismas rutas; y
mako habla D-Bus por sd-bus, que en musl es una receta nueva (basu), mientras
dunst habla por GDBus, que ya está sellado. Cero dependencias nuevas.
DOS GUARDIANES EN LA PROPIA RECETA, porque las dos cosas que importan no se ven
en que compile:
- `X11=0` es una intención; el hecho se lee en el ELF. dunst compila los dos
backends por defecto y su config.mk avisa: sin wayland «forzará xwayland». En
esta distro eso sella un binario que arranca, toma el bus y NO PINTA NUNCA. La
fase exige `libwayland-client` entre los NEEDED y prohíbe `libX11`.
- El `.service` de D-Bus es la mitad que importa: sin systemd, al daemon lo
levanta EL BUS. Se comprueba que reclame `org.freedesktop.Notifications` y se
le saca la línea `SystemdService=`, que acá no puede significar nada.
Y `DUNSTIFY=0`: entró primero en 1 y el binario **segfaultea hasta en
`--help`** (rc=139 en las tres pruebas). La causa es el cuadro que este repo ya
tiene escrito: nuestro `libnotify.so.4` es compartido y arrastra la cadena glib
`.so`, mientras esta cola enlaza la GLib estática ⇒ dos copias de GObject en un
proceso. No se pierde nada: el emisor de la casa es `notify-send`, que viene
DENTRO del artefacto de libnotify y ya está en las cuatro imágenes. Hay un
guardián que frena el día que alguien vuelva a poner DUNSTIFY=1 sin leer.
LA PRUEBA, en `scripts/wlr/dunst-headless.sh`: sway headless + bus de sesión +
`notify-send`, y NADIE lanza dunst a mano — lo activa el bus por el .service,
que es el mismo camino de una página web en atuq. La evidencia es un diff de
píxeles antes/después acotado al cuadrante donde dunst dibuja, porque un log en
verde es compatible con una pantalla vacía y el color no distingue (swaynag pinta
su barra de error en el mismo rojo).
positivo 14832 píxeles cambiados · servidor: dunst knopwob 1.12.2 1.2
control 0 píxeles · ServiceUnknown · notify-send rc=1
El control negativo borra el .service dentro del overlay temporal y EXIGE que no
se dibuje nada: probado en los dos sentidos.
Captura en docs/evidencia/dunst-sway-notificacion-2026-09-07.png.
Vigía de sonames: escritorio-sway 203 → 204 nodos, 512 sonames, 0 sin proveedor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
f3aaab75c6 |
test-atuq-rootfs: el triaje decía «sin receta de ffmpeg» y era falso
La familia `libavcodec.so.*` estaba clasificada como hueco con el motivo «sin receta de ffmpeg en el corpus». `recipes/ffmpeg.toml` existe, está sellada, publica `libavcodec.so.61` —uno de los once sonames que sondea libxul— y ya viajaba en la clausura de los cuatro escritorios arrastrada por `mpv`. Pasa a ruido: lo que falta son las OTRAS versiones del soname, y Firefox recorre la lista hasta que una carga. `libva` igual: la receta está y ahora también en el rootfs del runner. Sigue siendo hueco, pero por la otra mitad —las tres mesa van con `-Dgallium-va=disabled` y `-Dvideo-codecs=` vacío, así que no hay un solo `*_drv_video.so` que cargar—, y el motivo ahora lo dice. Con eso el mapa pasa de 47 cadenas sin proveedor a 7 huecos reales. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
a44fc4f06a |
atuq: AV1 no reproducía, y la causa era UN elemento de LD_LIBRARY_PATH
Gecko SE COME EL PRIMER ELEMENTO de `LD_LIBRARY_PATH` al lanzar el proceso RDD
—el que decodifica vídeo—. El lanzador ponía `/usr/lib/atuq` una sola vez, o sea
primero, así que el RDD arrancaba sin él, no encontraba `libmozavcodec.so` /
`libmozavutil.so` (el ffvpx bundleado, donde vive dav1d) y anotaba:
PlatformDecoderModule FFVPX: Link result: NoProvidedLib
El navegador NO falla ahí: se cae al ffmpeg del sistema, que cubre
H.264/AAC/VP8/VP9/MP3/FLAC/Opus. Pero AV1 por software NO lo cubre —el
decodificador `av1` de ffmpeg es sólo hwaccel y nuestra receta va sin dav1d—, así
que un vídeo AV1 se quedaba en `readyState=1` para siempre, sin un error, sin un
NEEDED faltante y sin una cadena ausente. Media función apagada en silencio, que
es la forma de fallo de esta casa.
Tres corridas que sólo cambian esa variable, con el mismo artefacto:
LD=/usr/lib/atuq:/usr/lib:/lib → RDD NoProvidedLib AV1 ✗
LD=/usr/lib/atuq:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓
LD=/relleno:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓
El arreglo es repetir el appdir. Feo y correcto mientras no haya `patchelf` en el
corpus para grabar `RUNPATH=$ORIGIN`, que borraría la variable entera.
Y el guardián que sale de este punto ciego, `scripts/test-atuq-codecs.sh`: abre
cinco muestras versionadas en `scripts/fixtures/codecs/` y mira si `currentTime`
AVANZA — «se creó el decodificador» no es «decodifica». El veredicto sale por
`dump()` al stdout, así que no necesita ni red ni servidor, y corre headless para
que sirva en el worker. Con `--negative-control` se saltea el lanzador y EXIGE
que AV1 falle: probado en los dos sentidos, 5/5 y control ✓.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
b6d000eca3 |
atuq-nested: el runner medía un rootfs MÁS POBRE que la imagen, y su caché no cacheaba
Tres cosas en el mismo fichero, las tres medidas hoy: 1. `ffmpeg` y `libva` entran a las raíces. NO son recetas nuevas: las dos están selladas en el corpus y ya viven en la clausura de los cuatro escritorios, arrastradas por `mpv` (verificado con `yupana.membresia`, no leyendo el TOML). Faltaban acá, o sea que el runner abría un rootfs sin libavcodec y desde ahí se concluía que la imagen no tiene códecs. El instrumento otra vez, no el artefacto. 2. El bucle de hidratación salía 1 SIEMPRE: `$VIGENTES` termina en `\n`, `echo` agrega otro, la última vuelta lee la línea vacía, `[ -n "$d" ]` da 1 y con `set -e` el script moría sin imprimir una sola línea, justo después de hidratar bien las 41 raíces. Ahora `continue` en la vacía y un `hydrate` que falla grita. 3. La comparación del sello NUNCA daba igual: el lado izquierdo lleva su `\n` final y `$(cat …)` lo recorta ⇒ «DESACTUALIZADO» en cada corrida y 3013 ficheros rehidratados de más. Los dos lados pasan ahora por la misma sustitución de comandos. Y `ROOTFS_ONLY=1`, que corta después de hidratar: lo necesita el guardián de códecs, que corre headless y tiene que usar ESTA lista de raíces y no una copia. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
6886b1541c |
libnotify: el primer hueco que el mapa del §6.10 encontró Y cerró
`libxul.so` lleva la cadena `libnotify.so.4` adentro y la abre por dlopen cuando
una página pide permiso para notificar. Nadie en el corpus la proveía, así que la
función quedaba apagada SIN UN SOLO MENSAJE: el navegador arranca, la web pide
notificaciones, y no pasa nada. No lo veía ninguna herramienta porque no hay
NEEDED en ningún ELF — es el tercer escalón, y sobrevivió a vigia-sonames con los
cinco perfiles en CERO.
Receta nueva, 0.8.8, SÓLO en variante compartida y eso no es un olvido: a un
`dlopen("libnotify.so.4")` una `.a` no le sirve de nada, así que una receta
estática sería un artefacto que nadie puede consumir.
Dos cosas que la receta se comió y quedan escritas:
1. `libpng-shared` hace falta porque las deps de hammer NO son transitivas: el
`.pc` de gdk-pixbuf-2.0 declara `Requires: libpng` y el meson muere con un
mensaje que nombra a gdk-pixbuf —que sí está— en vez de a lo que falta.
2. El guardián informaba «18 bytes» y parecía una librería vacía: `stat -c%s` no
sigue el symlink, y `libnotify.so.4` apunta a `libnotify.so.4.0.0`. El
artefacto estaba bien y el MENSAJE mentía. Con `-L` son 162.928 bytes. Se
arregla el mensaje porque es lo que alguien va a leer a las tres de la mañana,
y de paso el guardián exige el fichero real, no sólo el nombre.
⚠ Y lo que NO arregla, escrito en la receta para que nadie lea de más: libnotify
no trae daemon, manda org.freedesktop.Notifications por D-Bus. Tenerla resuelve
la mitad —que firefox la encuentre—; la otra mitad es que en la imagen haya
alguien escuchando ese nombre.
Verificado con el propio mapa: `--dlopen` pasa de 25 huecos a 24 y libnotify.so.4
desaparece; el soname viejo `.so.1` se reclasifica de «hueco» a «ruido», que es lo
que ahora es. El guardián normal sigue en CERO con un soname más pedido (66).
Queda pendiente la membresía de perfil en `docs/state/targets.toml`, que es
catálogo compartido y no lo toco sin decidirlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
7a81b6480c |
atuq §6.10: el tercer escalón — qué NO puede hacer el navegador, y por qué
hammer-9f nombró el punto ciego que ni su vigía ni mi guardián podían cubrir:
una librería que sólo aparece como CADENA LITERAL dentro de un dlopen(). No hay
NEEDED en ningún ELF, así que ningún auditor de readelf la encuentra. La
jerarquía queda:
NEEDED del ejecutable -> lo vemos los dos
NEEDED de un .so dlopeado -> lo vemos los dos
dlopen("libfoo.so.1") literal -> NO LO VE NINGUNO
Y un navegador vive de eso. Firefox sondea ffmpeg, VA-API, vulkan y libnotify
por nombre, y cuando no están NO FALLA: apaga la función y sigue. No hay línea
roja; hay una función que nadie ofrece y nadie reclama. Es la forma que ya costó
caro con OBS y su dlopen("libGL.so.1").
`--dlopen` busca esas cadenas y las cruza contra el rootfs. Es un HEURÍSTICO y se
declara como tal —una cadena no prueba un dlopen y su ausencia no prueba que no
lo haya—, así que no falla nunca: imprime un mapa triado. Lo afirmable es lo
contrario, que es lo útil: si la cadena está y el fichero no, esa función no
existe en esta imagen.
De 85 cadenas, 47 sin proveedor. Siete son huecos de verdad:
códecs del sistema (H.264/AAC) sin receta de ffmpeg — el más caro
notificaciones web sin receta
llavero (libsecret) RECETA YA EXISTE en incoming-gnome
sonidos (libcanberra) receta en incoming-kde
WebGPU (vulkan-loader) receta en incoming-kde
vídeo por hardware (VA-API) sin receta
lectura en voz alta sin receta
Tres de los siete son promoción, no autoría. Ocho son decisiones ya tomadas
(libGL por Wayland-only sin GLX; libcurl porque sólo lo usa el pingsender de
telemetría) y siete son ruido de musl o sonames viejos. El triaje va en una tabla
del propio script, no escondido en un `if`, porque es criterio y no medición: ahí
se puede discutir.
Sin triar: 0. Si aparece una cadena nueva, el informe la marca «SIN TRIAR» en vez
de tragársela.
Lo que esto cambia de fondo: hasta hoy la pregunta era «¿arranca?» y la respuesta
era sí. La que faltaba era «¿y qué NO puede hacer?», que ninguna métrica del repo
respondía porque todas miran presencia y ésta mira ausencia declarada por el
propio binario.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
a20d1b7453 |
atuq: un guardián para el rootfs del runner — y encontró SIETE sonames más cayendo al lab
La lista de raíces de `atuq-nested.sh` se mantiene A MANO y la jaula monta
`.dev-fs/alpine` como capa de abajo. Esas dos cosas juntas hacen que una raíz que
falte NO se note: el navegador arranca igual, resolviendo contra el lab. Ayer eso
costó `gcc-libs`. La regla del repo es que cada punto ciego se convierte en un
guardián, así que acá está el guardián en vez del parche.
`scripts/test-atuq-rootfs.py` mira el objeto que la jaula monta de verdad —el
directorio HIDRATADO— y no el grafo de recetas, que es lo que ya cubre
`vigia-sonames.py`. Son preguntas distintas: la lista del runner no sale del
grafo, así que el grafo no puede auditarla.
Lo primero que hizo fue encontrar SIETE sonames más que se estaban resolviendo
contra el lab, y no son cosmética:
libexpat.so.1, libzstd.so.1 <- los pide mesa (iris_dri, libEGL, libgbm)
libdbus-1.so.3 <- lo pide pipewire; lo trae `dbus-shared`, no `dbus`
libbz2.so.1 <- freetype
libudev.so.1 <- libspa-alsa
libsndfile.so.1, libncursesw.so.6
Los siete tienen proveedor en el corpus. Agregados a las raíces: el rootfs pasa
de 7 huecos a CERO, y la única excepción que queda es `libc.so`, que va en una
lista explícita porque ningún artefacto lo provee — las imágenes lo copian del
devfs. Si algún día hay receta que lo provea, esa lista se achica y el guardián
se vuelve más estricto solo.
CONTROL NEGATIVO incluido, que sin él esto no probaría nada:
`--negative-control` esconde libstdc++.so.6 y exige que el guardián lo cace.
Corrido: lo caza, y nombra a quién lo pide (atuq, atuq-bin).
Y `scripts/test-atuq-ruteo.py` vuelve a pasar entero contra el rootfs completo,
o sea que el ruteo por contenedor está probado ahora sobre un rootfs que no le
pide nada al lab salvo el intérprete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
7389d44569 |
atuq-nested: faltaba gcc-libs, y su ausencia hacía MENTIR a la prueba entera
`atuq` declara `NEEDED libstdc++.so.6` y `libgcc_s.so.1` —lo hereda de firefox, que va con clang++ y la libstdc++ COMPARTIDA— y su clausura ya estaba bien: `recipes/atuq.toml` declara `runtime = ["gcc-libs"]` desde |
||
|
|
2e093f9cab |
atuq §6.8: probado que el paquete sale por el proxy del contenedor — y con control negativo
Era lo único que la v0.5 dejó sin probar, y las tres puertas que había
encontrado siguen cerradas: no hay flag de línea de comandos para el
userContextId, Marionette no toma `-remote-allow-system-access` en la jaula, y
`tabs.create({cookieStoreId})` exige el permiso `cookies` — que no se le da a la
extensión del proxy sólo para que pueda probarse a sí misma.
La cuarta puerta estaba abierta: el fichero de SESIÓN guarda el userContextId de
cada pestaña y Gecko lo restaura. Se fabrica a mano — `mozLz40\0` + tamaño + un
bloque LZ4, y un bloque de sólo literales es LZ4 válido: veinte líneas, sin
librería.
Y la medición no le pregunta nada al navegador: le pone dos oídos en la red y
mira a cuál llama. Dos pestañas piden la MISMA url, una sin contenedor y otra en
«Banco»:
al destino (8099) GET /directo <- la de sin contenedor, directa
al proxy (9099) \x05\x01\x00 <- saludo SOCKS5 de la de «Banco»
Ese saludo sólo aparece si Gecko decidió hablar con un proxy para esa petición, y
el único que se lo pudo indicar es proxy.onRequest mirando el cookieStoreId. La
pestaña sin contenedor no es decorativa: sin ella, «todo fue por el proxy» y «el
ruteo anda» se verían iguales.
CONTROL NEGATIVO, que es lo que este repo se exige desde hoy: con
`--negative-control` no se configura el proxy y se exige lo contrario — las dos
pestañas directas y nadie llamando al proxy. Corrido: pasa. La única diferencia
entre las dos corridas es una línea de configuración y el observable se da vuelta
entero. Sin ese modo, una prueba que se hubiera vuelto ciega se vería idéntica a
una que funciona.
Tres detalles sin los cuales la prueba mide un silencio y se lee como fallo:
restore_on_demand=false, allow_hijacking_localhost=true, y el
triggeringPrincipal_base64 en cada entrada de sesión.
El artefacto se resuelve por `hammer hash` y no por glob, que es la lección de
hammer-03 de esta madrugada: con dos artefactos de la misma receta en el store,
`ls | head -1` es una ruleta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
44b6032522 |
vigia-sonames: dos falsos positivos menos — los cinco perfiles en CERO
NO hacía falta una receta de perl. `libperl.so` está en el artefacto, son 9,6 MB
en usr/lib/perl5/5.40.2/x86_64-linux/CORE/, y el binario la encuentra por su
RUNPATH, que apunta justo ahí. El vigía no la veía porque perl la construye con
-Duseshrplib y NO le pone SONAME: el código sólo registraba el nombre del fichero
`if son:`. Un ELF compartido sin SONAME se resuelve POR NOMBRE DE FICHERO, y eso
es exactamente lo que había que indexar.
El otro falso positivo era el último «hueco» del informe:
usr/lib/go/src/debug/elf/testdata/libtiffxx.so_ — un ELF de MUESTRA que Go
shipea para los tests de su propio paquete debug/elf, pidiendo libc.so.6 (glibc)
en una distro musl. Un binario que nadie ejecuta. `testdata/` no es cierre.
Importa arreglar un falso positivo aunque «sólo» sea ruido: un vigía que grita
en falso enseña a ignorarlo, y así es como libstdc++.so.6 —que SÍ rompía el
navegador en los cuatro escritorios— estuvo en esta misma salida sin que nadie
la mirara.
escritorio-mirada 41 nodos · 231 sonames · 0 sin proveedor
escritorio-kde 306 nodos · 2083 sonames · 0 sin proveedor
escritorio-gnome 190 nodos · 657 sonames · 0 sin proveedor
escritorio-cosmic 161 nodos · 498 sonames · 0 sin proveedor
escritorio-sway 201 nodos · 509 sonames · 0 sin proveedor
PROBADO CON UNA ROTURA A PROPÓSITO, porque un vigía todo-verde se ve igual que
uno roto. Quitando `runtime = ["gcc-libs"]` de firefox SOLO no pasa nada —atuq
lo declara también y es raíz de los mismos perfiles, así que la librería entra
igual; el primer control estaba mal montado—. Quitándolo de las DOS, kde y
cosmic vuelven a reportar libstdc++.so.6 y libgcc_s.so.1 exactamente. Restaurado
después.
|
||
|
|
02facebe28 |
deps.runtime: que ALGUIEN las lea — de 29 huecos de soname a 6
Tres arreglos que son el mismo: un campo que nadie lee es un campo que miente. 1. yupana._deps() leía SÓLO `deps.build`. Como yupana es la base de todas las herramientas de grafo, una dep de EJECUCIÓN no existía para ninguna: ni el vigía de sonames, ni la membresía de perfiles, ni el rootfs hidratado. Medido: firefox declaraba `runtime = ["gcc-libs"]` y el cierre de las cuatro imágenes seguía sin libstdc++.so.6, así que el navegador no arrancaba y el vigía lo seguía reportando como hueco DESPUÉS de haberlo arreglado. `deps.runtime` está en el esquema de hammer desde siempre. La unión es la definición de cierre: para CORRER hacen falta las dos. 2. build-state.py, lo mismo y por lo mismo. 3. El vigía entra en el LATIDO y deja docs/state/sonames.txt. Existía desde antes y contesta la pregunta que el grafo no contesta —no «¿está sellado?» sino «¿arranca?»— pero NADIE LO CORRÍA: no estaba en cosecha-cron y no dejaba fichero de estado. Por eso libstdc++.so.6, que rompía el navegador en los CUATRO perfiles, estuvo en su salida sin que nadie lo leyera, y se redescubrió arrancando atuq a mano. Un vigía que hay que acordarse de invocar no se distingue de no tenerlo. Y se BORRA scripts/audit-needed.sh, que escribí ayer sin ver que vigia-sonames.py ya hacía exactamente esto, con la misma frase en la cabecera. Dos herramientas que miden lo mismo divergen y la que nadie mira es la que miente; la que se queda es la que ya existía, que además reporta POR PERFIL y encontró más cosas. python3 declara sus cuatro deps de ejecución (readline/sqlite/lzma/bz2): son módulos de la stdlib que se cargan por dlopen, así que no rompen el arranque sino un `import` — un fallo que aparece lejos y no menciona a python. Resultado, con todo aplicado: 29 huecos -> 6, y los que quedan son otra clase. `libperl.so` es empaquetado de la receta perl; `libc.so.6` lo pide el `go` prebuilt y es un soname de GLIBC en una distro musl, que es un síntoma distinto. Los dos quedan anotados en docs/state/sonames.txt, que ahora se regenera solo. |
||
|
|
5b9777bdbd |
declarar gcc-libs como dep de EJECUCIÓN en las seis — y el navegador arranca
Las seis recetas que el audit daba por colgantes declaran ahora
`runtime = ["gcc-libs"]`. `deps.runtime` NO entra en el ArtifactHash —medido:
firefox sigue en b3:8116bdec, atuq en b3:f2960991, waterfox en b3:88b5a762—
así que esto NO reconstruye nada. Lo que cambia es la CLAUSURA, que es lo que se
hidrata en la imagen.
EVIDENCIA DE QUE ARREGLA EL FALLO REAL, no de que compila: `atuq` bajo sway
headless, sin GPU, renderizando about:buildconfig con sus tablas y la barra de
contenedores de la v0.5. Antes moría con decenas de «Error relocating:
_ZNKSt5ctypeIcE13_M_widen_initEv: symbol not found».
docs/evidencia/atuq-arranca-con-gcc-libs-2026-09-06.png
Y el audit se corrige, porque SUB-REPORTABA en dos formas:
· Miraba sólo ejecutables y `head -12` por artefacto. El siguiente NEEDED que
faltaba estaba en una LIBRERÍA (libmozsandbox.so pedía libnspr4.so), así que
daba «6 recetas» cuando el artefacto de firefox declara 30 sonames. Ahora
recorre TODOS los ELF.
· No contaba lo que el propio artefacto TRAE. Firefox bundlea su nspr/nss y
las encuentra por el LD_LIBRARY_PATH que fija su lanzador; contarlas como
colgantes era un falso positivo.
Con las dos correcciones y gcc-libs en el store, el corpus baja de 25 artefactos
colgantes a TRES, y son otra cosa:
sqlite-shared libreadline.so.8
python3 libreadline.so.8
naabu libdl.so.2 libpthread.so.0 libc.so.6
Los dos primeros piden una receta que falta (readline). El tercero pide sonames
de GLIBC, que en una distro musl es un síntoma distinto y merece su propia
mirada. Quedan anotados, no arreglados.
|
||
|
|
2be672415c |
audit-needed: cazar deps de ejecución colgantes — firefox no arranca en la distro
Encontrado corriendo el navegador, no leyendo. Lancé `atuq` bajo el sway
headless y murió con decenas de «Error relocating: _ZNKSt5ctypeIcE...: symbol
not found». El binario SELLADO de firefox declara:
NEEDED libstdc++.so.6 NEEDED libgcc_s.so.1 NEEDED libc.so
y las dos primeras NO EXISTEN en el store ni en ningún cierre hidratado: sólo en
.dev-fs/alpine, o sea en el LAB. Es exactamente la lección que la memoria
`quitar-dep-no-apaga-funcion` describe: el proyecto la encuentra en el sysroot
del lab y el artefacto sella con un NEEDED que nadie provee.
LO QUE LO HACE CARO ES QUE NINGUNA MÉTRICA LO VE. build-state.json dice
`sealed`; verificar-repro.sh dice que REPRODUCE bit a bit; los tres guardianes de
la receta pasan (RLBox en el binario, MOZ_REQUIRE_SIGNING, BuildID determinista)
— y el binario no arranca fuera del lab. El verde responde «¿construye?», nunca
«¿corre?».
El audit recorre el store, indexa qué sonames PROVEE el corpus y resta los NEEDED
de cada ELF. No mira el lab a propósito: mirarlo diría que todo está bien, que es
justo la ilusión que causó el problema.
Resultado de hoy: 25 artefactos colgantes, 6 recetas distintas —atuq, firefox,
waterfox, librsvg, spidermonkey, mesa-llvmpipe— y sólo DOS sonames faltan en todo
el corpus. O sea que falta UNA receta que provea las libs de runtime de gcc.
Gravedad: firefox y atuq son raíces de los CUATRO perfiles de escritorio
(cosmic, gnome, kde, sway), así que hoy las cuatro imágenes llevan un navegador
que no puede arrancar. La receta de firefox razona sobre esto en su comentario
—«clang++ de Alpine usa la libstdc++ COMPARTIDA ⇒ el NEEDED existe»— pero cierra
el requisito de BUILD y deja abierto el de EJECUCIÓN. hammer tiene
`[deps] runtime` y ninguna de las seis lo declara.
|
||
|
|
dc1391e611 |
siembra: anclar la exclusión de PNG — sin anclar rompe las recetas que traen PNG
La siembra excluía `*.png` sin anclar, o sea PNG en CUALQUIER sitio, incluido
recipes/. Dos consecuencias y las dos malas:
a) rsync PROTEGE DE BORRADO lo que excluye. Un directorio cuyo único resto es
un .png no se puede vaciar y por lo tanto no se puede borrar nunca, aunque
el hub lo haya eliminado. Medido: recipes/atuq/extension/atuq128.png
sobrevivió a un rename hecho en el hub, y el log del latido venía repitiendo
`cannot delete non-empty directory: recipes/atuq/extension` sin que nadie lo
leyera como un error.
b) Un PNG NUEVO de una receta tampoco viaja. Y hay recetas cuyo `[source] dir`
lleva PNG dentro: los 9 iconos de recipes/atuq/branding/icons/.
El efecto medido: `atuq` daba b3:f2960991 en el hub y b3:b33e81a5 en el worker,
por ESE único fichero de más. El worker no podía coincidir con el hub ni
construyendo bien.
Lo bueno es que no era silencioso, y por diseño: hammer SÍ hashea el árbol de un
`[source] dir` (recipe.rs, `dir:<hash>`), así que la divergencia sale como otro
ArtifactHash en vez de como dos bytes distintos en la misma dirección. Es
exactamente la propiedad que le falta al lab y a la versión de hammer.
Lo que se quería ahorrar eran las capturas: docs/evidencia/ (4,4 M) y el PNG
suelto de la raíz (1 M). Eso ahora se excluye POR RUTA. Los PNG de recipes/
viajan, que es lo correcto: ahí son fuente, no adorno.
Borrado el fichero rancio del worker; hub y worker vuelven a coincidir en
b3:f2960991.
|
||
|
|
b6a35bc2bc |
worker-loop: que el watchdog no se lleve el post-mortem
El watchdog barre work/sources/* cada 180 s con un suelo de 2 minutos, así que
el árbol de una receta que ACABA DE FALLAR se destruye antes de que nadie lo
mire. Medido hoy: waterfox murió a las 03:20:15 tras 36 minutos y 23.980 pasos,
enlazando libxul.so con un `collect2: error: ld returned 1 exit status` SIN
mensaje del linker delante — o sea justo el caso en que hace falta el objdir. A
las 03:22 work/sources/ estaba vacío. Reproducirlo cuesta otros 36 minutos.
Lo que más molesta es que la doctrina ya estaba escrita en el repo, en dos
sitios, y este watchdog la contradecía sin que nadie lo notara:
campana-deuda.sh:138 «NO se poda tras un ✗: el árbol de una receta que falló
es el post-mortem»
poda-fuentes.sh suelo de 24 h, «deja el post-mortem del día»
No se arregla subiendo el suelo a secas: este watchdog existe para que una tanda
Go no llene el disco vendoreando, y ahí los minutos importan de verdad (80 G en
una tanda, con el I/O-wait disparando el load). Se arregla siendo agresivo SÓLO
cuando el recurso escasea: con el disco por debajo de DISK_HIGH el suelo pasa a
SUELO_FRIO_MIN (120 min por defecto, configurable); en cuanto el disco aprieta
vuelve a los 2 minutos de siempre. Un árbol frío con disco al 37% no le hace
daño a nadie.
Probado en los dos sentidos: árbol de 5 min -> se borra con suelo 2, protegido
con suelo 120; árbol de 3 h -> se borra con los dos (ya no es post-mortem).
Ojo al leerlo: `df --output=pcent` da el porcentaje USADO, no el libre. La
primera versión de este parche llamaba `libre` a esa variable.
|
||
|
|
7638e4e2be |
worker-loop: recompilar hammer POR CICLO, no sólo al arrancar
El bloque de rebuild que ya existía corre una sola vez, al arrancar el loop, y
este loop vive días — hoy el worker llevaba 8 de uptime. El source SÍ sigue
llegando fresco cada 30 min (la siembra rsync-ea el repo), así que el worker
acaba con FUENTE NUEVO y BINARIO VIEJO.
Eso es peor que el fósil de la golden que motivó el bloque original, porque nada
lo delata: la versión de hammer NO entra en el ArtifactHash, así que el build se
comporta distinto y build-state.json sigue en verde. Misma familia que el lab
fuera de hash_inputs, sin siquiera el aviso del lock.
Caso real que lo motiva, de hoy: el worker corría un hammer de las 04:54 y el
arreglo que materializa los submódulos git había entrado a las 15:11 (
|