El PGO necesita correr el navegador para juntar el perfil, y las distros lo
hacen bajo xvfb-run. Nosotros no tenemos X11 (Wayland-only), así que el plan
decía «la salida es un sway headless». Ya no es plan: se corrió.
sway con WLR_BACKENDS=headless y WLR_RENDERER=pixman arranca en gioser —un LXC
SIN /dev/dri, o sea sin GPU ninguna— y dibuja píxeles reales. La captura va como
evidencia: sway 1.10 / wlroots 0.18.2 / foot 1.27.0, los tres binarios nuestros,
salida HEADLESS-1 a 1280x720. Ciclo completo ~2 min.
Dos cosas que costó y quedan escritas para no repetirlas:
· Hidratar BAJO el montaje del store. hydrate-profile.py enlaza, y un hardlink
no cruza montajes: con --into work/... sale EXDEV porque el store está en otro
disco. Con --into store/.rootfs/sway son 193/193 nodos y 4,9 G que no ocupan.
· Inyectar el loader de musl. sway salió DINÁMICO y pide
/lib/ld-musl-x86_64.so.1, que el cierre no trae. El error es
«exec: /usr/bin/sway: not found», que se lee como «falta el binario» cuando lo
que falta es su INTÉRPRETE. En musl el libc.so ES el loader, así que se
resuelve con un --ro-bind. Misma familia que el resto de la noche: el mensaje
nombra lo que buscó, no lo que falta.
Queda el segundo muro del PGO, que es de diseño y no de infra: el profdata no es
determinista, así que hay que generarlo UNA vez y sellarlo como artefacto propio
consumido por hash.
`scripts/verificar-repro.sh recipes/waterfox.toml`: REPRODUCEN 1, DERIVA 0,
NO-DETERMINISMO 0.
Con esto el guardián del BuildID queda probado en las DOS direcciones que exige
la doctrina del repo, y no por diseño sino porque los hechos cayeron así: cazó
una rotura real —el artefacto anterior selló en verde con BuildID=20260906062042,
la hora del build— y pasa un control positivo, que es el arreglado reproduciendo
de verdad. Un guardián con sólo la primera mitad no se distingue de uno que mata
siempre; con sólo la segunda, de uno que no mira nada.
Los dos Gecko del corpus (firefox con RLBox y waterfox) reproducen.
`scripts/verificar-repro.sh recipes/firefox.toml`: reconstruido y comparado
contra el sellado da REPRODUCEN 1, DERIVA 0, NO-DETERMINISMO 0.
No es un trámite. Encender RLBox mete una cadena de compilación ENTERA nueva
—wasi-libc, libc++ a wasm32, los builtins, wasm2c traduciendo a C— dentro del
build de firefox, y la duda razonable era si algo de eso metía una fecha, un
orden de tabla hash o una ruta absoluta. No lo hace.
Es además la distinción que esta misma noche costó cara en la receta de al lado:
waterfox SELLÓ EN VERDE sin reproducir, porque el ArtifactHash es
input-addressed y no se mueve por un BuildID que sea la hora del build. «Selló»
y «está bien» son dos preguntas distintas, y la segunda cuesta un comando.
b3:88b5a762, 377 M, 43 ficheros, BuildID=19700101000001 (determinista, con
guardián). Con esto el grafo queda 850/852 sellados y los dos que faltan son
`ajeno` (steam-runtime-sniper, xwayland), que son frontera y no deuda.
Los cuatro muros que costó, y ninguno era de la receta:
1. Moría en 4 s por un submódulo git — era el binario de hammer del worker,
13 h más viejo que el arreglo que los materializa.
2. El linker moría sin mensaje — era el watchdog del worker-loop barriendo el
objdir a los 2 minutos, antes de que nadie pudiera mirarlo.
3. `multiple definition` de cairo — la receta traía la lista de deps anterior
a la migración de firefox a las variantes -shared.
4. Iconos faltantes al empaquetar — inconsistencia interna de Waterfox: el
manifiesto pide ocho tamaños y branding-common.mozbuild instala cinco.
Rotura sólo de Linux.
Y una quinta que el sello NO habría delatado: selló primero con
BuildID=20260906062042, o sea sin reproducir, y en VERDE.
`llvm-nm --defined-only | grep -c w2c_` cuenta también los nombres C++
mangleados que llevan `w2c_` en el MEDIO —`_ZN5rlbox...PK16w2c_mem_capacity...`
son 913 de los 1547—, no sólo los símbolos de la jaula wasm. Anclado
(`grep -c " w2c_"`, o `awk '{print $3}' | grep -c "^w2c_"`) da 634, que es lo
que mide el guardián de recipes/firefox.toml y lo que el documento quería decir.
La conclusión no se mueve: la base sin RLBox da 0 con cualquiera de los dos
patrones, y el derivado hereda los mismos 634 que la base. Lo que se corrige es
la afirmación, que decía «símbolos w2c_*» y contaba otra cosa: un número sin su
patrón no es una medición.
Lo cazó hammer-03 al reproducir el número desde su lado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
`firefox` selló con RLBox (b3:8116bdec) y `atuq` se reconstruyó encima en 2
segundos, que es exactamente lo que el §2 prometía del camino derivado: el
overlay no cambió una línea por un rebuild del motor.
Comprobado que la base nueva es la que quedó DENTRO del derivado, no sólo
declarada: el `libxul.so` de atuq trae 1547 símbolos `w2c_*` donde la base
anterior (352d7880) traía 0. Y la capa de v0.5 sigue entera sobre ella — los
cuatro contenedores, las dos extensiones y el mapa del proxy en un arranque
real.
De paso, una promesa del documento que convenía no creer sin medir: que si sube
firefox, atuq se reconstruye solo. Se midió en un catálogo de sonda (una copia
de atuq.toml + su árbol + firefox.toml, resuelto por el fallback al catálogo
padre): con el firefox idéntico al del corpus, atuq da el mismo hash; cambiando
una bandera de ese firefox copiado, atuq pasa a b3:60ade76a. La dep entra en
hash_inputs, así que un atuq viejo no puede quedarse tapando un motor nuevo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
Las cinco recetas se construyeron por separado en las dos máquinas —gioser (4c)
y el LXC dev.gioser.net (6c)— y los cinco artefactos salen idénticos comparando
el árbol completo, no sólo el ArtifactHash.
No es un trámite. El lab NO entra en hash_inputs, así que dos labs distintos
pueden sellar bytes distintos en la MISMA dirección del store y nada lo detecta:
la igualdad de hashes no probaba nada por sí sola. Esto dice que para esta
cadena los dos labs coinciden de verdad.
Sale gratis de haber construido en las dos máquinas por otro motivo (el hub no
tenía wasi-libcxx y sin él no podía correr scripts/test-guardianes-wasm.sh).
La v0.5 dejó dicho que faltaba probar que una petición hecha en «Banco» sale
por el proxy de «Banco». Se intentó y no se llegó; queda escrito el intento
porque el muro es reusable y el que venga después no tiene por qué volver a
descubrirlo.
El diseño de la prueba sirve y no necesita red ni servidor: la MISMA url a un
puerto vacío desde dos contenedores, y comparar los errores. `connectionFailure`
contra `proxyConnectFailure`. Que sean DISTINTOS es la prueba, porque el segundo
sólo aparece si Gecko habló con un proxy y el único que se lo pudo indicar es
nuestro proxy.onRequest mirando el cookieStoreId.
Lo que falta es abrir una pestaña EN un contenedor, y las tres puertas están
cerradas:
1. Desde la línea de comandos no existe el flag.
2. Marionette SÍ está en el artefacto y responde —se le habló con un cliente
propio de 60 líneas, el protocolo es `<longitud>:<json>`, abrió sesión y
aceptó comandos—, pero el contexto chrome exige `-remote-allow-system-access`
y en la jaula no lo tomó ni por bandera, ni con --remote-debugging-port, ni
por MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1, que es lo que RemoteAgent.sys.mjs lee
en su constructor. Comprobado que la variable llega al proceso.
3. Desde la extensión, `tabs.create({cookieStoreId})` exige el permiso
`cookies` (ext-tabs-base.js:getUserContextIdForCookieStoreId). Darle a la
extensión del proxy acceso a TODAS las cookies del usuario para poder
probarse a sí misma sería pagar con la superficie de ataque del producto una
comodidad del test. No se hace, y queda dicho por qué.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
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 ff0b556, que
es de otro frente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
Deja escrito en el documento que se lee para reanudar: el orden obligatorio de
las cinco recetas, por qué wasi-libc-headers pinea un commit viejo a propósito, y
las tres trampas ya pagadas (sesgo de clang en check-symbols, el wasi-libcxx que
faltaba, y el directorio vacío que es una sonda del compilador).
Y el criterio de guardián que sale de todo esto, que es lo más reusable:
«existe» no es «se encuentra».
La deuda de BuildID queda cerrada con la misma prueba que se le exigió al
derivado: apartar el artefacto como .ref, construir de nuevo y comparar.
56 entradas idénticas, 0 divergen. Antes de MOZ_BUILD_DATE esto no podía
salir bien, y build-state.json no lo veía porque el ArtifactHash es
input-addressed y no se mueve por la hora del build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011L4H7RabF2NCxCvFPJi37r
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í.
18 recetas C verificadas de una, en unos nueve minutos, 15 REPRODUCEN y 3 eran DERIVA (ya al día).
Cero no-determinismos. Y `work/sources` se quedó en 4 KB toda la tanda gracias a la poda por
veredicto: antes una tanda así llevaba el disco del 95% al 98%.
Lo que hace sostenible a esta y no a la anterior es la SELECCIÓN. El filtro `MAX_SECONDS` sobre los
tiempos del worker no transfiere a Rust —`zola` se comió 40 minutos y no dio un solo veredicto— pero
en C sí: sin caché de cargo de por medio, un build de 40 s allá son 40 s acá. Lista explícita de
clase `c`, no muestreo ciego.
Tres que había clasificado como decisión legal y no lo eran. Las dos causas son distintas y las dos
me las estaba perdiendo por mirar sólo el árbol:
**ffmpeg → LGPL-2.1-or-later.** Su raíz trae cuatro COPYING.* y por eso el detector se planta, con
razón: el árbol solo no puede decidir. Pero el `LICENSE.md` del propio ffmpeg explica la regla —«In
combination the LGPL v2.1+ applies to FFmpeg» y «None of these parts are used by default, you have to
explicitly pass `--enable-gpl`… In this case, FFmpeg's license changes to GPL v2+»— y la respuesta la
da NUESTRA receta: no pasa `--enable-gpl` ni `--enable-version3`, y encima va con
`--disable-autodetect`. O sea que la licencia sale de cruzar el árbol con los flags, no de elegir uno
de los cuatro ficheros.
**llvm18 y clang18 → Apache-2.0 WITH LLVM-exception.** Acá el detector veía «tres licencias
distintas» en un solo `LICENSE.TXT` y se plantaba, pero el fichero ABRE diciendo «The LLVM Project is
under the Apache License v2.0 with LLVM Exceptions»: lo demás es la sección legada. La regla de
«varias licencias ⇒ decide un humano» es correcta como default y aquí el humano sólo tuvo que leer la
primera línea.
`licenses/` pasa de 48 a 49 textos: `LLVM-exception` hacía falta para poder declararla, que es el
lazo de siempre —no se declara lo que no se puede entregar—. El único sin texto canónico sigue siendo
`LicenseRef-qorpa-ajena-no-enumerable`, que es identificador nuestro y SPDX no publica.
97% (1135/1165); la cola baja de 27 a 24. Los 3 ArtifactHash, idénticos.