97ceb7241181ba5e2b586b6bbc1c490c334422f4
471
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 (
|
||
|
|
4fae3ee1ff |
prueba que los guardianes de la cadena wasm sirven — con CONTROL
Un guardián que nunca falló no se sabe si sirve, y uno que falla SIEMPRE se ve idéntico a uno que funciona. Por eso el control con el sysroot intacto —que tiene que PASAR— no es un extra: es la mitad de la prueba. El método lo aportó la sesión hammer-f8 (scripts/test-atuq-politica.py hace lo mismo con el cruce política<->XPI). Le pone delante a la cadena las cuatro formas conocidas de romperla —sin la sonda include/c++/v1, sin cstring, sin libc++.a, sin libc.a— y exige que las cuatro maten, usando la invocación EXACTA del configure de firefox dentro del lab. No construye nada, no toma work/.farm-build.lock y no escribe en el store: trabaja sobre copias temporales de artefactos ya sellados, así que corre con la granja a pleno. Y el control se ganó el sueldo en su primera corrida: salió en ROJO. La causa no era un guardián flojo sino este script, que resolvía el artefacto con y agarraba el VIEJO —el de antes de que la receta creara el directorio-sonda—. Misma trampa que el cache-hit que congela regresiones: un artefacto anterior se hace pasar por el actual y la prueba mide otra cosa. Sin el control se habría leído como «4 en verde, guardianes ok». Ahora el artefacto se resuelve por de la receta, nunca por glob. 5 en verde, 0 en rojo. |
||
|
|
c5ebbda933 |
atuq v0.5: proxy por contenedor — el primer diferenciador del §6 que se paga entero
Es el único de la lista del SDD 26 §6 que no pasa por el host de native
messaging del §7: es API de Firefox y nada más. Cada contenedor —Personal,
Trabajo, Banco, Compras— puede salir por su propio proxy.
Tres piezas y ninguna alcanza sola: `atuq.cfg` prende los contenedores (vienen
apagados), `policies.json` los CREA con `Containers.Default` —el único
mecanismo que los pone en un perfil NUEVO— y deja la config de fábrica en
`3rdparty.Extensions`, y `extensions/proxy/` los enruta con `proxy.onRequest`,
que es lo único que ve el `cookieStoreId` de la petición.
Las cuatro se comprobaron DENTRO del artefacto de firefox antes de escribir una
línea, no en la documentación de Mozilla: `Containers` y `3rdparty` en el
`policies-schema.json` de browser/omni.ja, `cookieStoreId` en el
`schemas/proxy.json` de omni.ja, y `storage.managed` leyendo
`getExtensionPolicy(id)` en ext-storage.js. Es la regla del §2.sexies: la
pregunta no es si Mozilla lo tiene, es si NUESTRO build lo tiene.
Tres decisiones que valen más que el código:
1. La config se indexa por NOMBRE de contenedor y no por `cookieStoreId`: el id
depende del orden en que se crearon, así que la misma configuración aplicada
a otro perfil apuntaría a otro contenedor.
2. FAIL CLOSED. Un contenedor con proxy configurado que no se pudo honrar no
sale directo: va a un destino cerrado y el navegador muestra el error. Salir
directo sería una fuga silenciosa — la misma familia que el artefacto vacío
de la regla 3, el fallo que llega hasta el final diciendo que todo fue bien.
3. `proxyDNS` PRENDIDO por defecto: sin él la consulta DNS sale por la línea que
se quería evitar. Es la fuga clásica de esta configuración.
Y lo que no promete está arriba de todo en la página de opciones, no en un pie:
separación de tráfico, NO anonimato; para anonimato, Tor Browser. El §4 cumplido
donde el usuario lo lee.
De paso, una verdad que estaba escrita en dos sitios pasa a tener un dueño: el
id de cada extensión sale ahora del `manifest.json` y no de una constante de
rebrand.py, el nombre del XPI se deriva de él, y el `install_url` de la política
se cruza contra la ruta donde el fichero quedó escrito de verdad. El icono se
inyecta desde branding/icons/ en vez de estar copiado byte a byte dentro de cada
extensión. Agregar una tercera extensión es ahora un directorio.
PROBADO, corriendo el árbol en la misma jaula que atuq-nested.sh:
· captura de about:preferences#containers con los cuatro contenedores y sus
iconos, más el containers.json del perfil;
· extensions.json del perfil nombra las dos extensiones;
· `console.info: "atuq/proxy: 4 contenedor(es) enrutado(s)"` — leyó la config
de fábrica por storage.managed Y la casó con los contenedores de la política;
· scripts/test-atuq-politica.py: cinco formas de desincronizar política y XPI,
las cinco matan el build, y el control con la política intacta pasa.
NO probado y dicho por su nombre: que una petición hecha en «Banco» salga por el
proxy de «Banco». Pide automatizar la UI y queda pendiente.
Dos obstáculos del método, que valen para la próxima. La consola de una
extensión es CONTENIDO: `devtools.console.stdout.chrome` (que viene en true) no
la incluye, hace falta `...stdout.content`. Y un `moz-extension://` NO se abre
desde la línea de comandos —muere con `NS_NOINTERFACE [nsIFileURL.file]` y abre
la home en su lugar—, además de que `--screenshot` dispara al `load`, que puede
ocurrir antes de que arranquen las extensiones. Eso último destapó un fallo real
y arreglado: la página de opciones confundía «no hay contenedores» con «la API
no está» y mostraba un mensaje FALSO.
Verificado contra firefox b3:352d7880; se reconstruye contra el firefox con
RLBox cuando selle. La otra mitad de esta unidad —el rename de
recipes/atuq/extension/ a extensions/inicio/— entró sin querer en
|
||
|
|
8a2c6fad39 |
repro: «no pude comparar» tampoco es no-determinismo — segundo sitio, misma lección
Una tanda volvió a llenar el libro de veredictos falsos: 47 recetas sanas anotadas como
NO-DETERMINISMO. Esta vez la cadena empezó antes de donde yo había mirado.
El `mv` que APARTA el artefacto falló (el directorio de apartado no estaba) y yo nunca comprobaba que
hubiera funcionado. A partir de ahí todo lo que sigue es basura: el artefacto se queda en su sitio,
`hammer why-differs` compara contra una ruta que no existe y devuelve `Error: store: no existe…`, y
mi código leía cualquier salida no-cero como «difieren». Un error de comparación disfrazado de
veredicto, 47 veces.
Es la MISMA lección que arreglé hace un rato para el build, en un segundo sitio que no miré:
**no poder medir no es un resultado negativo**. Dos guardas nuevas:
· el apartado se comprueba (`mv || SIN VEREDICTO`) y la receta se salta ruidosamente;
· `why-differs` se lee, no sólo su exit code: si dice `Error:`/`no existe`, es SIN VEREDICTO, se
restaura el artefacto y NO se anota nada.
Probadas las dos: con el apartadero sin permiso de escritura sale «no pude apartar el artefacto ⇒
SIN VEREDICTO», el artefacto queda intacto y el libro no crece.
Comprobado además que la tanda mala no perdió NADA: como el `mv` fallaba, los 47 artefactos nunca
salieron del store (`faltan: 0`). El daño fue sólo el registro, y está purgado — el libro queda con
51 verificaciones buenas.
La regla, ya por triplicado hoy: cuando un guardián no puede establecer algo, el estado es *sin
veredicto*, y eso no se escribe. Un libro que anota lo que no midió se cree igual que uno que sí.
|
||
|
|
a48885e143 |
repro: podar el árbol de fuentes tras cada veredicto sano — el barrido ya no llena el disco
Un barrido dejaba un árbol extraído por receta en `work/sources` y no limpiaba hasta el final. En el
hub eso llevó el disco del 95% al 98% DOS VECES hoy, y un disco lleno no se lee como disco lleno: se
lee como recetas rotas. Con la poda por veredicto, `work/sources` se queda en 3 MB durante toda la
tanda en vez de crecer hasta 7,7 G.
No cuesta nada porque `fetch.rs` borra y re-extrae el árbol en CADA build por diseño: guardarlo entre
recetas de un barrido no ahorra un segundo.
Dos límites deliberados:
· Sólo se poda cuando el veredicto es SANO. Si algo falló o divergió, el árbol es justamente lo que
hace falta para mirarlo — un verificador que limpia la escena del problema que acaba de encontrar
sirve para poco. Es la misma razón por la que los ejemplares divergentes se conservan.
· Sólo el árbol de ESA receta. Las deps extraídas se quedan: son compartidas, y borrarlas mientras
otra cosa las usa es exactamente el ADR 0012.
Con esto la cobertura se puede levantar en el HUB, que es donde están los 1157 artefactos. El worker
sólo tiene los que construyó —el último barrido allá reportó 47 de 60 «sin artefacto»— así que como
máquina de cobertura no sirve por más disco que tenga.
|
||
|
|
058162c6d3 |
licencias: la cola de pendientes también se escribe — «lo que falta y por qué» es dato
Las 33 que quedan sólo existían en la salida del script, o sea que se perdían al cerrar la terminal. Para quien tiene que decidirlas —y son decisiones legales, no mecánicas— «qué falta y POR QUÉ no se puede afirmar» es tan dato como los veredictos. `docs/licencias-pendientes.tsv` las deja listadas con su motivo: cuáles traen varios ficheros de licencia que dicen cosas distintas (gmp, ffmpeg, clang18, llvm18, los `gi-*`), cuáles tienen un LICENSE que no reconozco y con qué frase empieza (fuse3, lsof), y cuáles no traen nada en el árbol (boost, pigz, sqlite-shared). Al revés que `licencias-evidencia.tsv`, ésta se REGENERA entera en cada corrida. No es un registro de lo que se probó sino una foto de lo que queda, y una pendiente resuelta tiene que DESAPARECER de acá —si se acumulara, la lista de trabajo mentiría hacia arriba para siempre. |
||
|
|
d7d9cf8fd2 |
repro: «la 2ª reconstrucción no construyó» NO es no-determinismo — mi propio bug, y lo pagó el libro
Corriendo una tanda con `MAX_SECONDS=45` se llenó el disco a mitad, los rebuilds empezaron a morir por ENOSPC, y la rama que yo mismo había escrito para distinguir DERIVA de NO-DETERMINISMO metió «falló» y «difiere» en la misma condición ⇒ **21 recetas sanas quedaron anotadas como NO-DETERMINISMO** en un libro que pretende ser autoritativo. Un registro falso y persistente es peor que no tener registro: el propósito del libro es que nadie tenga que volver a mirar, así que una entrada equivocada se cree para siempre. Es la trampa de siempre —un disco lleno se lee como receta rota, y la pista es que mueren varias seguidas— y el script ORIGINAL ya la evitaba: «no es lo mismo «no reproduce» que «no construye acá». Distinguirlos importa: mezclarlos inventaría un problema de determinismo». Lo rompí al añadir la segunda reconstrucción. Restituido: si la 2ª no construye, se restaura el artefacto, se cuenta como fallo y **no se anota nada** — sin veredicto es un estado legítimo. Reparado además el daño: las 21 entradas falsas purgadas del libro (quedan 27 verificaciones buenas), y `angle-grinder` repuesto desde `store/.divergen/`, donde lo había dejado la clasificación errónea. Eso sí funcionó como debía: el ejemplar estaba guardado y entero, no perdido. Y queda anotado en el propio script el límite del filtro por tiempo que provocó todo: el sidecar de `store/.times/` lo escribe el WORKER, así que acota el build de esa receta EN LA MÁQUINA QUE LO MIDIÓ. Una receta Rust de 40 s allá con la caché de cargo caliente son muchos minutos acá en frío —`angle-grinder` y `amp` entraron por el filtro y se comieron el disco— y encima el número no cuenta la CASCADA de deps que falten en el store local, que es la misma sorpresa que dio `gjs`. Con disco justo conviene pasar las recetas a mano. |
||
|
|
efe6e001d2 |
repro: un libro de lo verificado, y por fin un número de cobertura — era 2 de 1157
`verificar-repro.sh` sorteaba una muestra y se olvidaba. Sin registro no había forma de contestar la
pregunta que importa —¿qué fracción del corpus se comprobó alguna vez que reproduce?— y encima las
mismas recetas salían sorteadas una y otra vez mientras otras no se miraban jamás. La primera medida
con el libro puesto: **2 de 1157**. El invariante central del proyecto no se había verificado de
forma acumulativa prácticamente en nada, y eso no se veía porque cada corrida daba «✅ PUERTA
SUPERADA» sobre su propia muestra.
`docs/state/repro-verificado.tsv` va con la misma clave auto-invalidante que el libro de fuzz:
(receta, **ArtifactHash**). El hash resume la receta MÁS su cierre de deps, así que una entrada deja
de casar en cuanto algo aguas arriba cambia. Un «ya lo verifiqué» que sobreviviera a un re-hash sería
una mentira; éste no puede serlo.
Con eso el muestreo pasa a sortear entre lo NO verificado, así que corridas sucesivas ACUMULAN
cobertura en vez de repetir (con `TODO=1` se sortea entre todas, para re-verificar a propósito), y
`--coverage` da el número sin construir nada.
Verificadas en esta tanda: itstool, markupsafe, musl-obstack, hicolor-icon-theme, musl-fts, npth,
poppler-render-check, doas, libffi y packaging REPRODUCEN; `when` derivaba y quedó al día. Cero
no-determinismos. Van 11 de 1157.
⚠ Y una trampa que casi me come, anotada acá porque el próximo la va a pisar: elegí candidatos «por
artefacto chico» y la lista empezó con **nodejs**, cuyo artefacto pesa 0,2 MB y cuyo build son horas
de V8. El tamaño del artefacto NO dice nada del costo de construirlo. Para elegir muestra barata hay
que mirar tiempos de build, no bytes de salida.
|