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.
42 KiB
SDD 26 — atuq: el envoltorio Gecko de la distro
Escrito 2026-09-05, a partir de la pregunta del usuario: «ya habiendo compilado firefox y waterfox,
qué tan complicado vs provechoso suena crear nuestro propio envoltorio, en vez de zen?», y de su
aclaración de por qué la pregunta existe: puriy quedó muy verde y es difícil sacarlo de ahí.
atuq es zorro en quechua. El nombre estaba libre —verificado con grep -ri atuq sobre
/mnt/vvv/hammer y /mnt/vvv/tawasuyu, cero coincidencias— y dice exactamente lo que la cosa es:
otro fox, con marca propia y sin usar la de Mozilla (ver el §branding de recipes/firefox.toml).
Este documento no reemplaza a puriy ni lo cancela. puriy sigue siendo el motor soberano; el
§9 explica por qué atuq es su andamio y no su desvío.
1. La corrección que ordena todo: no hay envoltorio fuera del chrome
La intuición natural —una carcasa Llimphi con nuestros widgets, y Gecko adentro pintando la página—
no es posible. Gecko no tiene API de embebido en escritorio desde que murió XULRunner; GeckoView
existe y es de Android. No hay forma soportada de meter el motor en una ventana de mirada.
⇒ un envoltorio de Gecko es, necesariamente, chrome-level: JS/CSS/XHTML dentro del propio Firefox, más extensiones, más procesos externos que hablen con él.
Y eso es exactamente lo que Zen es. Zen no toca el motor: su diferencia entera —workspaces, split
view, glance, compact mode— vive en el chrome, más una herramienta (surfer) que reaplica sus
parches sobre el árbol de Firefox y recompila. Su costo no es el motor: es la cinta de correr del
rebase cada cuatro semanas. Copiar su método sería heredar su costo sin heredar su equipo.
2. La decisión de arquitectura: artefacto DERIVADO, no fork de fuente
Si atuq parchea el árbol, cada iteración de UI cuesta un build de Gecko de cuatro horas. Eso
mata el proyecto en la primera semana de diseño, porque el diseño de chrome es iterativo por
naturaleza.
Pero nosotros no distribuimos un binario: sellamos artefactos. Entonces atuq es una receta cuyo
[deps] incluye firefox, y cuyas fases copian el árbol instalado e inyectan encima:
| Qué se inyecta | Mecanismo de Firefox | Sin recompilar |
|---|---|---|
| Marca, iconos, nombre | distribution/, application.ini |
✅ |
| Prefs de fábrica | defaults/pref/*.js + autoconfig (.cfg) |
✅ |
| Política de empresa | distribution/policies.json |
✅ |
| Chrome propio (dientes, layout) | userChrome.css + re-empaque de omni.ja |
✅ |
| Extensiones nuestras | distribution/extensions/ |
✅ |
Ventajas que esto tiene sobre el método de Zen, y que son estructurales:
- Iteración en segundos, no en horas. El chrome se toca sin volver a compilar C++.
- No mueve el
ArtifactHashdefirefox. El corpus no se invalida;waterfoxy cualquier otro fork siguen compartiendo el mismo artefacto base. - Las CVE se heredan gratis. Un fork de fuente te vuelve dueño del reloj de seguridad del
binario más atacado de la máquina — es lo que hundió a casi todos los forks de Firefox, y el
reproche histórico a Waterfox. Un derivado se reconstruye solo cuando sube
firefox.
Los dos gotchas reales del camino, que son trabajo pero chico:
omni.jaes un zip ⇒ hay que re-empacarlo determinista (mtimes fijos, orden estable) o el artefacto deja de reproducir bit a bit. Es la misma disciplina que ya exige la imagen del lab.- El
jarlogdel PGO ordena elomni.japara el arranque (§3.bis). Re-empacarlo a lo bruto tira esa optimización a la basura sin que nadie lo note. El re-empaque preserva el orden. - Hay que invalidar el startup cache tras tocarlo (fichero
.purgecachesjunto al binario, o bump del BuildID). Si no, los cambios no se ven y parece que el overlay no agarró: un falso negativo de los caros, de la misma familia que el cache-hit que congela regresiones.
2.bis Cuándo SÍ hay que parchear el árbol
Cuando algo necesite tocar C++ del motor. Hoy conocemos un caso: sct en su forma fuerte (§6.1).
La regla es: nada baja al árbol antes de que exista la toolchain del §3, porque hasta entonces
cada intento cuesta cuatro horas. Y cuando baje, el vigía de parches (scripts/vigia-parches.py,
commit aa200a4) es lo que convierte esas cuatro horas en un minuto para saber si el parche agarra.
3. La puerta de las optimizaciones: no es un flag, es una toolchain
El usuario decidió habilitar las optimizaciones. Lo que hay que saber antes de intentarlo:
recipes/firefox.toml va con compiler = "gcc" por el muro 3 (zig enlaza libc++ estática para
musl y el configure de Mozilla exige encontrar un NEEDED …libc++; es negativa de upstream, no una
perilla). El problema es que PGO, LTO y BOLT son cadena de clang en Gecko:
--enable-lto=crossquiere clang + lld. El LTO de GCC sobre Gecko no está soportado upstream.--enable-profile-useespera-fprofile-instr-use(clang), no-fprofile-use(gcc).- BOLT necesita
llvm-bolt.
Y el corpus no tiene clang usable como compilador. Verificado en las recetas, no supuesto:
recipes/clang18.tomlcompilaninja -C build libclang.so clang-resource-headersy su faseinstallcopia sólobuild/lib/libclang.so*y las cabecerasclang-c. Está ahí porquebindgencargalibclang.soen runtime. No hay driverclang, nilld, nilibc++.llvm18se selló con-DLLVM_ENABLE_PROJECTS=""— LLVM a secas.
⇒ la primera versión de este documento concluyó que hacía falta una receta llvm-toolchain
(clang+lld+libc++ desde fuente). Era caro de más, y se corrigió el mismo día mirando el lab en
vez de suponerlo:
.dev-fs/alpineya trae clang22 + llvm22 22.1.8 — la misma major que usa el APKBUILD de Alpine para este mismo Firefox (_llvmver=22).Compiler::Clangya existe en hammer, cableado de punta a punta (parse_compilerlo acepta;hammer-buildponeCC=clang,CXX=clang++,AR=llvm-ar). Ninguna receta lo usaba.- Faltaba sólo
ld.lld:apk add lld⇒ dos paquetes, cero upgrades.
Y los tres muros caen sin perder lo que gcc daba: el sondeo de linker se satisface con
--enable-linker=lld, el ar lo pone hammer solo, y el NEEDED de la stdlib de C++ existe porque
clang++ de Alpine usa la libstdc++ compartida — la prueba no es teórica: Alpine construye este
Firefox con clang22 y sin libcxx en sus makedepends.
La huella del lab no se movió, y se midió antes de tocar nada. lld no casa ningún prefijo de
TOOLCHAIN_PREFIXES (hammer-core/src/lab.rs), así que los 43 paquetes que entran en hash_inputs
salieron idénticos ⇒ los 837 artefactos sellados quedan intactos. Eso abarata el cambio hoy y a
la vez deja un agujero escrito: la versión de lld no es parte de la identidad del artefacto, y sólo
expone a las recetas compiler="clang" — hoy, una. Cerrarlo cuesta re-hashear el corpus entero.
Hecho el 2026-09-05 (commit cb3ecd5): firefox va con compiler = "clang",
--enable-linker=lld, --enable-lto=cross, --enable-packed-relative-relocs y
--with-unsigned-addon-scopes=app,system. Falta construirlo.
Riesgos de PGO, escritos antes de empezar:
- El PGO de Mozilla corre el navegador para juntar el perfil (
profileserver.py, headless + marionette). Dentro de un sandbox hermético, sin red y sin X11, eso hay que hacerlo andar. Headless no necesita display, pero sí necesita que el árbol de perfil se genere adentro. - El perfil resultante es un input del artefacto. O entra en
hash_inputs, oatuqdeja de ser determinista y no lo vamos a notar: sería otra forma del lab que no está enhash_inputs. - Ganancia esperable, para calibrar expectativas: PGO+LTO en Gecko son del orden de 10-25% en carga de página y JS. Hoy estamos ATRÁS de Zen en este eje, no adelante — Zen hereda la configuración de Mozilla tal cual. Esto no es una ventaja nuestra: es una deuda que se salda.
3.bis El diff contra Alpine — y no es sólo PGO
Medido el 2026-09-05 trayendo el APKBUILD y el mozconfig de community/firefox de aports y el
PKGBUILD de Arch, y comparándolos línea a línea con el mozconfig de recipes/firefox.toml.
Alpine es nuestro propio upstream —de ahí salen los once parches de musl— así que la comparación
no es contra una distro lejana: es contra la que ya construye este mismo código.
recipes/firefox.toml |
Alpine community/firefox |
|
|---|---|---|
| LTO | no | --enable-lto=cross |
| PGO | no | --enable-profile-use=cross + merged.profdata + jarlog |
| Linker | GNU ld (via gcc) | --enable-linker=lld (lld 22) |
| RELR | no | --enable-packed-relative-relocs |
| Sandbox RLBox | APAGADO (--without-wasm-sandboxed-libraries) |
--with-wasi-sysroot |
| Extensiones sin firmar | no declarado | --with-unsigned-addon-scopes=app,system |
| Allocator | --disable-jemalloc |
--disable-jemalloc — igual, no es desviación nuestra |
| Librerías | todo bundleado | --with-system-{icu,nss,nspr,av1,vpx,webp,png,jpeg,zlib,ffi,libevent,pixman,pipewire} |
Arch hace lo mismo en el eje de velocidad («Do 3-tier PGO», --enable-lto=cross,
--enable-profile-use=cross). Ni Alpine ni Arch usan BOLT (cero menciones en los dos ficheros)
⇒ BOLT no es la desventaja, es el extra. Lo que nos separa del resto del mundo es PGO + LTO, del
orden de 10-20% en carga de página y JS.
Tres lecturas que hay que sacar de esa tabla:
- RLBox apagado es una desventaja de SEGURIDAD, no de velocidad, y probablemente pesa más que el
PGO. Es la jaula wasm alrededor de los parsers de fuentes y medios —graphite, ogg, expat,
woff2—, o sea exactamente el código que come entrada no confiable. Encenderlo pide
wasi-sdk+wasi-compiler-rten el corpus: receta propia, no un flag. --with-unsigned-addon-scopes=app,systemnos falta yatuqlo NECESITA para shipear sus propias extensiones desdedistribution/extensions/(§6.1, §7). Es un flag deconfigure⇒ una capacidad deatuqque no se resuelve en el overlay: exige tocar la base.- Las librerías del sistema son diferencia a propósito, no deuda. Alpine usa las suyas; nuestro corpus es la fuente y bundlear es lo que mantiene la clausura cerrada.
Corolario que ordena el plan: todo esto entra en UN SOLO rebuild de firefox. Cada rebuild son
cuatro horas y re-sella la cola Gecko entera (waterfox incluido), así que LTO, PGO, RELR, lld, RLBox
y el scope de addons se juntan en una sola pasada, no en seis.
3.ter Los dos muros del PGO que las distros no tienen
El perfil se junta corriendo el navegador, y las dos distros lo hacen bajo xvfb-run (Alpine:
xvfb-run -a -s "-screen 0 1920x1080x24" ./mach python build/pgo/profileserver.py; Arch, idéntico).
Nosotros no tenemos X11 en el corpus —es la decisión Wayland-only de toda la distro— así que ese
camino no existe acá. La salida hammer-nativa es correrlo bajo un sway headless
(WLR_BACKENDS=headless), que ya tenemos del frente wlr/sway y arranca en segundos.
✅ EL MURO DEL DISPLAY ESTÁ DESPEJADO (2026-09-06). No es una previsión: se corrió. sway
headless con WLR_BACKENDS=headless y WLR_RENDERER=pixman arranca en gioser, que es un LXC
sin /dev/dri —o sea sin GPU ninguna, todo software— y dibuja píxeles de verdad:
docs/evidencia/sway-headless-gioser-sin-gpu-2026-09-06.png (sway 1.10 / wlroots 0.18.2 /
foot 1.27.0, los tres binarios nuestros, salida HEADLESS-1 a 1280x720). El ciclo entero es de
~2 min con scripts/wlr/sway-headless.sh.
Dos cosas que hubo que resolver y conviene tener escritas antes de repetirlo:
· Hidratar bajo el montaje del store. hydrate-profile.py ENLAZA, y un hardlink no cruza
montajes: con --into work/... sale EXDEV porque el store vive en otro disco. Con
--into store/.rootfs/sway son 193/193 nodos, 41.677 ficheros y 4,9 G que no ocupan (son
hardlinks al store).
· 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»
y lo que falta es su intérprete. Se resuelve con
--ro-bind /usr/lib/musl/lib/libc.so /lib/ld-musl-x86_64.so.1 (en musl el libc.so ES el
loader). Es la misma familia que el resto de la noche: el mensaje nombra lo que buscó, no lo que
falta.
Y el perfil no es determinista. Los contadores dependen del timing de la corrida, así que dos
generaciones del profdata no dan los mismos bytes — a las distros no les importa porque no persiguen
bit-repro; a nosotros nos rompe el invariante. La salida: generar el perfil UNA vez y sellarlo como
artefacto propio, consumido por hash desde firefox. El build optimizado vuelve a ser determinista
aunque su insumo no lo sea, y el perfil se regenera a propósito, no por accidente.
3.quater La cadena wasm, y las tres cosas que costó (2026-09-06)
--without-wasm-sandboxed-libraries se cambió por --with-wasi-sysroot=/usr/share/wasi-sysroot,
que es literalmente el flag de Alpine. Detrás hay cinco recetas nuestras, en este orden obligatorio:
wasi-libc-headers → wasi-compiler-rt → wasi-libc → wasi-libcxx → wasi-sdk → firefox
wasi-libc-headers existe sólo para romper el ciclo del bootstrap y pinea un commit VIEJO a
propósito (triple wasm32-wasi), mientras wasi-libc pinea el nuevo (wasm32-wasip1, el que
clang 22.1 usa de verdad). Es lo mismo que hace Alpine y no es un descuido.
Lo que costó, que es la parte reusable:
-
check-symbolsfalla por sesgo de clang, no por un artefacto malo. El lab trae clang 22.1.8 y el snapshot del árbol viene de uno anterior: sobra#define __wasip1__ 1y nada más. Se reconcilia el snapshot; no se salta conmake no-check-symbols, que se lleva por delante la comparación de símbolos —la mitad que sí vale y que pasaba byte a byte. -
Faltaba
wasi-libcxx, y el hueco era visible en el grafo sin construir nada. El configure murió a los 6 s con'cstring' file not found, culpando a los headers de WASI, que estaban perfectos:<cstring>es C++ y las librerías enjauladas no son todas C. Elwasi-sdkde Alpine declarawasi-libc wasi-libcxx wasi-compiler-rt—TRES— y la cadena tenía dos. Eldependsdel paquete ajeno equivalente es una comprobación de completitud gratis, antes de gastar CPU. -
Un directorio VACÍO que sí importa. Con
wasi-libcxxsellado el configure volvió a morir igual, ycstringestaba en el artefacto: clang no lo buscaba. Falta elmkdir -p include/c++/v1que Alpine tiene en supackage()y que parece ruido de empaquetado — es la sonda por la que el driver decide que el sysroot tiene layout de libc++. Medido dentro del lab: sin ese directorio,'cstring' file not found; con él, exit 0. Es el reverso exacto de la regla 3 delCLAUDE.md: allá un directorio vacío es un artefacto mentiroso, acá es una declaración dirigida al compilador.
De ahí sale el criterio de guardián que ahora usan estas recetas: «existe» no es «se encuentra».
wasi-libcxx corre hoy la misma prueba que hace el configure de firefox, contra un sysroot
fusionado con el de su dep, así que un fallo aparece en el segundo 20 de una receta chica —con la
lista de búsqueda de clang impresa— y no en el segundo 6 de un build de cuatro horas que además
culpa a otro. Y firefox exige RLBox en el binario: sobre b3:352d7880 (el último con RLBox
apagado) los patrones w2c_, rlbox y wasm2c dan cero los tres en libxul.so, así que
cualquiera apareciendo prueba que la jaula entró. Es la tercera repetición de la lección de
MOZ_REQUIRE_SIGNING y MOZ_BUILD_DATE: una bandera que el configure acepta y el artefacto ignora.
Evidencia de reproducibilidad cruzada (2026-09-06). Las cinco recetas se construyeron por
separado en las DOS máquinas —el hub (gioser, 4c) y el worker (dev.gioser.net, LXC 6c)— y los
cinco artefactos salen byte a byte idénticos, comparando el árbol completo de cada uno:
| receta | sha256 del árbol | |
|---|---|---|
wasi-libc-headers |
0d34f9cb27530606… |
✓ |
wasi-compiler-rt |
0c4fc6fdf08ef908… |
✓ |
wasi-libc |
c13acdf896f2b78c… |
✓ |
wasi-libcxx |
b1d9aeec08499063… |
✓ |
wasi-sdk |
b49f7a74a5caed3f… |
✓ |
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. Esto dice que para esta cadena
los dos labs coinciden de verdad, y no sólo que coinciden los ArtifactHash.
4. Lo que atuq NO promete
Esta sección existe antes que la lista de features a propósito, con el mismo criterio que el modelo
de adversario de qullqa: lo que no se promete se escribe primero, porque una promesa falsa de
privacidad es peor que no ofrecer nada.
atuq no es Tor Browser y no va a tener «modo Tor». Tor Browser no es «Firefox + SOCKS»: su
valor son las contramedidas de huella (RFP, letterboxing, normalización de fuentes/canvas/timers) y
sobre todo un conjunto de anonimato donde todos se ven idénticos. atuq es, por construcción, un
binario único en el mundo: musl, Wayland-only, branding propio, extensiones propias. Alguien saliendo
por Tor desde atuq es más identificable, no menos, y su conjunto de anonimato son diez
personas. Para anonimato: Tor Browser, y lo decimos nosotros primero.
Lo que sí se ofrece, y es honesto porque es otra cosa: proxy por contenedor (§6.8) — separación de tráfico, no anonimato.
atuq tampoco promete anti-fingerprinting propio (es un programa de investigación con equipo
full-time) ni bloqueo de anuncios propio (se shipea uBO y listo).
5. Contexto verificado al escribir (2026-09-05)
De docs/state/build-state.json, no de memoria:
| Dato | Valor |
|---|---|
| Recetas / nodos | 838 / 840 |
| Selladas | 837 |
firefox 154.0 |
sealed, cola corpus |
waterfox 6.7.1.1 |
never — primer build en curso |
| Plataforma compartida | gtk3, atk, nodejs, clang18, cbindgen: todas sealed |
atuq no arranca hasta que waterfox selle. Waterfox es la prueba de la tesis del reúso —«las
diferencias reales con firefox.toml son TRES: la fuente, el branding y la versión»—. Si esa tesis
falla, el precio de todo este documento cambia y hay que releerlo.
6. Los diferenciadores, priorizados
La columna «quién más lo tiene» es lo que evita que nos contemos un cuento.
| # | Qué | Quién más lo tiene | Pieza que ya existe | Costo |
|---|---|---|---|---|
| 6.1 | sct — transparencia de scripts |
nadie | puriy-sct, puriy-sct-testigo |
medio |
| 6.2 | Descargas direccionadas por contenido | nadie | store CAS de hammer, tejido |
bajo |
| 6.3 | Archivo personal + RAG local | Rewind/Recall (nube, Windows); SingleFile (guarda, no busca) | khipu, rag-motor, willay-rag |
medio |
| 6.4 | Historial y perfil sobre qullqa |
nadie | qullqa-core, qullqa-pozo |
medio |
| 6.5 | Foco por cortafuegos, no por extensión |
nadie (nadie es dueño del navegador y del sistema) | cortafuegos, pacha |
bajo |
| 6.6 | Medios por fuera del navegador | extensiones sueltas; de fábrica no | foreign-ytdlp, -platform, -dlna |
bajo |
| 6.7 | IA local en la barra lateral | Chrome/Edge son nube; Zen no tiene | rimay, iniy |
bajo |
| 6.8 | Proxy por contenedor ✅ v0.5 | nadie de fábrica | — (API de Firefox) | bajo |
| 6.9 | Torrent adentro | Vivaldi lo trae; Brave tuvo WebTorrent | shared/foreign-torrent (SDD propio, sobre librqbit) |
bajo |
6.1 sct es la joya
BLAKE3 a todo lo que se ejecuta, y negarse a correr código que ningún testigo vio nunca. Es lo más cerca que hay de «Certificate Transparency, pero para el JavaScript que te corre en la cara», y no lo tiene nadie: ni Zen, ni Brave, ni Tor Browser.
En tawasuyu ya está escrito y —dice su README— la lógica es agnóstica del transporte y está
certificada sin red; puriy-sct-testigo es sólo el cable HTTP.
- v1, sin tocar C++: extensión con
webRequestbloqueante que hashea cada respuesta de script y consulta al testigo antes de dejarla pasar. - v2, forma fuerte: gancho en el script loader de Gecko. Eso sí es parche de árbol ⇒ §2.bis ⇒ después de la toolchain del §3.
6.2 Una descarga con identidad
Lo bajado no es un archivo en ~/Downloads: es un objeto BLAKE3 en el store — deduplicado,
verificable, y re-compartible por tejido. El torrent (6.9) alimenta lo mismo. Ningún navegador
trata una descarga como algo con identidad; para todos es un blob con nombre, y por eso lo bajás
dos veces y no lo sabés.
Ésa, y no «tener torrent», es la razón por la que 6.9 vale: Vivaldi ya tiene torrent, pero termina en una carpeta.
6.3 El archivo personal es la razón por la que alguien usaría atuq
Cada página visitada se congela en el CAS y entra al DAG de khipu; después le preguntás a tu
propio historial en lenguaje natural, todo local. Es la feature que más justifica el proyecto
entero, y sale de piezas que ya están escritas.
6.4 qullqa con su advertencia puesta
Almacenamiento con negación plausible y modelo de adversario escrito — ningún navegador tiene
eso; el modo incógnito es teatro. ⚠ Con la advertencia que el propio SDD de qullqa ya anotó y que
hay que repetir acá, no esconder: en una máquina con swap sin cifrar, el documento no aplica.
Se promete lo que se puede probar.
6.8 Proxy por contenedor — HECHO (v0.5, 2026-09-06)
Es el primer diferenciador del §6 que se paga entero, y se pudo pagar ahora porque es el único de la lista que no pasa por el host del §7: es API de Firefox y nada más. Cada contenedor —Personal, Trabajo, Banco, Compras— puede salir por su propio proxy.
Tres piezas, en tres ficheros, y ninguna alcanza sola:
| Pieza | Dónde | Qué aporta |
|---|---|---|
privacy.userContext.enabled |
atuq.cfg |
prende los contenedores, que Firefox trae apagados |
Containers.Default |
distribution/policies.json |
los CREA — es el único mecanismo que los pone en un perfil NUEVO |
3rdparty.Extensions |
distribution/policies.json |
la config de fábrica, que la extensión lee como storage.managed |
proxy.onRequest |
extensions/proxy/fondo.js |
lo único que ve el cookieStoreId de cada petición |
Las cuatro se comprobaron DENTRO del artefacto antes de escribir una línea, que es la regla que
dejó el §2.sexies: Containers y 3rdparty están en el policies-schema.json de browser/omni.ja,
cookieStoreId en el schemas/proxy.json de omni.ja, y storage.managed lee
Services.policies.getExtensionPolicy(id) en ext-storage.js. Ninguna de las cuatro se leyó de la
documentación de Mozilla: se leyeron de nuestro build, que es el que las tiene que traer.
Tres decisiones de diseño que valen más que el código:
- La configuración se indexa por NOMBRE de contenedor, no por
cookieStoreId. El id (firefox-container-3) depende del ORDEN en que se crearon, así que la misma configuración aplicada a otro perfil apuntaría a otro contenedor. Un identificador que cambia de significado entre máquinas no sirve para configurar una distro. - Fail closed. Un contenedor que TIENE proxy configurado y no se pudo honrar —entrada inválida, API que falla, mapa a medio construir— no sale directo: va a un destino cerrado y el navegador muestra el error. Salir directo sería una fuga silenciosa, y es la misma familia que el artefacto vacío de la regla 3: el fallo que llega hasta el final diciendo que todo fue bien. Un usuario que pidió que «Banco» no salga por su línea tiene que ver un error, no navegar.
proxyDNSviene PRENDIDO. Sin él, Gecko resuelve el nombre por su cuenta antes de hablar con el proxy: la consulta DNS sale justo por la línea que se quería evitar. Es la fuga clásica de esta configuración, así que el default es el seguro y hay que apagarlo a mano.
Y lo que NO promete está en la propia página de opciones, arriba de todo y no en un pie: separación de tráfico, no anonimato; no toca la huella del navegador; para anonimato, Tor Browser. Es el §4 cumplido en el sitio donde el usuario lo va a leer.
Lo que quedó PROBADO, y con qué
Corriendo el árbol de atuq en la misma jaula que usa scripts/atuq-nested.sh:
| Afirmación | Prueba |
|---|---|
| La política CREA los cuatro contenedores | captura de about:preferences#containers con Personal/Trabajo/Banco/Compras y sus iconos, más el containers.json del perfil |
| Las dos extensiones se INSTALAN | extensions.json del perfil nombra inicio@atuq.tawasuyu y proxy@atuq.tawasuyu |
| El ruteo se arma contra los contenedores reales | console.info: "atuq/proxy: 4 contenedor(es) enrutado(s)" — o sea que leyó la config de fábrica por storage.managed Y la casó con los contenedores que creó la política |
| Los guardianes del cruce política↔XPI FALLAN cuando deben | scripts/test-atuq-politica.py: 5 formas de romperlo, las 5 matan el build, y el control con la política intacta pasa |
| La capa sobrevive al cambio de BASE | rehecho sobre el firefox con RLBox (b3:8116bdec, 2026-09-06): atuq sella en 2 s como b3:f2960991, su libxul.so trae 634 símbolos que se LLAMAN w2c_* donde la base anterior traía 0, y los cuatro contenedores y las dos extensiones siguen apareciendo en un arranque real |
⚠ Ese 634 hay que contarlo anclado, y la primera vez se contó mal. llvm-nm --defined-only \| grep -c w2c_ da 1547 sobre el mismo fichero, y los 913 de más son nombres C++ mangleados que contienen w2c_ en el MEDIO (_ZN5rlbox...PK16w2c_mem_capacity...), no símbolos de la jaula. El patrón que cuenta lo que dice contar es grep -c ' w2c_' —o el equivalente awk '{print $3}' \| grep -c '^w2c_'—, y da 634, que es lo que mide el guardián de recipes/firefox.toml. La conclusión no cambia (la base anterior daba 0 con cualquiera de los dos patrones); lo que cambia es que un número sin su patrón no es una medición. Muestra de lo que sí es un símbolo de la jaula: w2c_rlbox_0x5F_cxa_atexit_0.
Y de paso quedó comprobado que la dep SÍ se sigue, que era la promesa del §2 y no una que convenga
creer sin medir: en un catálogo de sonda, cambiar una bandera del firefox copiado mueve el hash de
atuq (f2960991 → 60ade76a), mientras que con el firefox idéntico al del corpus da el mismo
hash. O sea que un atuq viejo no puede quedarse tapando un motor nuevo — que es justo la forma
que tendría acá el cache-hit que congela regresiones.
Lo que NO está probado, dicho por su nombre: que una petición hecha en el contenedor «Banco» salga por el proxy de «Banco». Se intentó y se documenta el intento, porque el que venga después no tiene por qué volver a descubrir el muro.
El diseño de la prueba está bien y no necesita red ni servidor: se pide la MISMA url —un puerto
donde no hay nadie— desde dos contenedores y se comparan los dos errores. Sin proxy tiene que dar
about:neterror?e=connectionFailure; con el proxy de «Banco» apuntando a un puerto cerrado tiene
que dar e=proxyConnectFailure. Que los códigos sean DISTINTOS es la prueba: el segundo sólo
aparece si Gecko intentó hablar con un proxy, y el único que se lo pudo indicar es nuestro
proxy.onRequest mirando el cookieStoreId. El control sin proxy no es decorativo — sin él, dos
fallos podrían ser el mismo fallo.
Lo que falta es abrir una pestaña EN un contenedor, y las tres puertas están cerradas:
- Desde la línea de comandos, no existe. No hay flag para el
userContextIdde la pestaña inicial. - Marionette SÍ está en el artefacto (
chrome/remote/content/marionette/enomni.ja) 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 abrir una pestaña de contenedor pide contexto chrome, y eso exige-remote-allow-system-access; en esta jaula NO lo tomó ni por bandera, ni combinado con--remote-debugging-port, ni por la variableMOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1que el propioRemoteAgent.sys.mjslee en su constructor (comprobado que la variable llega al proceso). Queda como cabo suelto medido, no como suposición. - Desde la extensión tampoco, y por una razón de diseño que conviene NO saltarse:
tabs.create({cookieStoreId})exige el permisocookies— está enext-tabs-base.js:getUserContextIdForCookieStoreId, que lanzaNo permission for cookieStoreIdsin él. Agregarle a la extensión del proxy un permiso sobre 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.
Así que el camino que queda es el (2) con el allowSystemAccess resuelto, o una prueba desde el
lado del sistema (mirar el connect() con strace sobre el proceso). Ninguno de los dos entra en
esta unidad.
Dos obstáculos del método, que valen para la próxima:
- La consola de una extensión es CONTENIDO, no chrome.
devtools.console.stdout.chromeviene entruede fábrica y NO la incluye; hace faltadevtools.console.stdout.content. Sin esa pref los mensajes de la extensión sólo viven en la consola del navegador, que en headless no mira nadie. - Un
moz-extension://NO se puede abrir desde la línea de comandos. El manejador intenta resolverlo como fichero y muere conNS_NOINTERFACE [nsIFileURL.file], abriendo la home en su lugar — o sea que la captura sale de otra página y parece que la tuya no funciona. Y--screenshotdispara alload, que puede ocurrir ANTES de que termine el arranque de las extensiones: ahí el globalbrowsertodavía no está inyectado. Eso destapó un fallo real y se arregló: la página de opciones distinguía mal «no hay contenedores» de «la API no está» y mostraba un mensaje FALSO.
2.ter Dónde vive la fuente de atuq — hizo falta un modo de [source] nuevo
Escribir la receta destapó un muro que este documento no había visto: [source] era
obligatoriamente git o tarball, así que una receta cuyo contenido no viene de upstream sino de
nosotros no se podía ni expresar. Los rodeos eran todos peores — fetchear una fuente que después se
ignora miente sobre la identidad del artefacto, y colgar el overlay de un repo aparte obliga al
worker a tener acceso a un repo privado.
Se agregó source.dir (commit 54fa4a9): un árbol dentro del propio repo, hasheado por
contenido con ArtifactHash::of_tree —que ya existía para verificar bit-reproducibilidad en
Stage 2— y no por ruta. Es la misma disciplina que ya tenían los patches, que entran al hash por
bytes. Editar un CSS del overlay mueve el ArtifactHash; renombrar el directorio, no.
Un .swm no puede llevar una receta derivada, y los cuatro sitios que lo tocan fallan
diciéndolo: el manifiesto es compartible y necesita un puntero resoluble desde fuera.
Hecho el 2026-09-05 (commits aeebb19 y 6f6c32b): recipes/atuq.toml + recipes/atuq/.
La v0.2 (b3:7eff4ba4) ya trae el branding: application.ini (Vendor/Name/RemotingName/
CodeName, con el ID intacto para no salirse del ecosistema de complementos), brand.ftl dentro de
browser/omni.ja, los binarios renombrados, iconos propios y .desktop. El re-empaque preserva
orden, STORED y fechas del original, y se verificó contra el artefacto: 5306 entradas, mismo
orden, y una sola con contenido distinto. El BuildID se deriva del contenido del overlay —no de
la fecha— porque es la clave con la que Gecko invalida su startup cache: sin eso, un cambio de
chrome arranca con la interfaz vieja y parece que el overlay no agarró.
La capa de configuración de la v0.1 son cuatro ficheros —autoconfig, atuq.cfg, chrome/atuq.css,
policies.json— y el CSS se registra como USER_SHEET desde autoconfig porque userChrome.css
vive en el perfil y exigiría que el usuario prenda una pref: atuq tiene que verse como atuq en el
primer arranque. Falta construirlo: su dep firefox está en vuelo.
2.quater Abrirlo en una pantalla: tres fallos que la clausura no podía ver
El 2026-09-05, con atuq sellado y verificado, se hidrató y se abrió en el compositor de la sesión
(waypipe). No arrancó, y los tres motivos son la diferencia entre «sellado» y «usable»:
- EXDEV al hidratar.
hydrateproyecta con hardlinks ylinkat()rechaza cruzar un punto de montaje aunque los dos lados sean el mismo filesystem. Acá el store es/dev/sdbbind-monteado ywork/vive en/dev/sdc. El rootfs va bajo/mnt/cosecha, donde el volumen está montado entero. Se comprueba confindmnt -T, nunca constat -c %d. - El lanzador no puede ser un symlink. Ni el binario ni sus
.sotraenRPATH/RUNPATH(readelf -d), así que el motor no encontraba su propiolibnspr4.soy moría enXPCOMGlueLoad. Es un script conLD_LIBRARY_PATH+exec, como Debian y Fedora. La alternativa limpia —RPATH=$ORIGINconpatchelf, que es lo que hace Alpine— espera a quepatchelfexista como receta del corpus. - Un bug del corpus, no de atuq:
atkse había tragado GObject. Declaraba la variante ESTÁTICA de glib y produce un objeto compartido, así quelibatk-1.0.sollevaba una copia entera del sistema de tipos. Dos GObject en un proceso ⇒ 45GLib-GObject-CRITICALySegmentation fault. El síntoma no nombra a atk, y no se ve mirando el rootfs: había una solalibgobject. Se cazó preguntando quién DEFINE el símbolo:nm -D --defined-only <cada .so> | grep " T g_type_register_static"— deben salir uno, salieron dos.
Radio medido antes de tocar (yupana radio atk): 4 transitivos, 3 sellados a deuda; GNOME fuera
porque usa gtk4. La cadena atk → gtk3 → firefox → atuq volvió a sellar 4/4, y firefox tardó
~50 minutos, no las cuatro horas que repetía este documento — el número venía del folclore de la
receta, no de una medición.
La lección para el plan: ninguna unidad de este frente está cerrada hasta que algo se ABRE en una pantalla. La clausura decía 100% con un navegador que no llegaba a pintar un píxel.
2.quinquies La v0.3 y los tres mecanismos: dos muertos, uno vivo
La página de inicio y la pestaña nueva parecían un pref y resultaron ser un frente. Lo que se
midió, en orden:
| Mecanismo | Resultado |
|---|---|
distribution/extensions/ — el clásico de las distros |
No instala nada. Firefox retiró el sideloading. El extensions.json del perfil ni lo mencionaba y el log no dijo una palabra: fallo perfectamente silencioso |
policies.json → ExtensionSettings con install_url |
Se lee (browser.policies.applied=true en el perfil) y falla nombrándose: ERROR_SIGNEDSTATE_REQUIRED |
xpinstall.signatures.required = false |
No alcanza — la exigencia viene COMPILADA |
El instrumento que destrabó el diagnóstico fue un testigo. defaultPref no deja rastro en
prefs.js —sólo se guarda lo que difiere del default— así que un autoconfig que no se ejecuta
es indistinguible de uno que sí y no hace nada. atuq.cfg escribe ahora atuq.autoconfig.ok como
pref de usuario, legible desde fuera sin abrir el navegador. Salió true ⇒ el .cfg corría, y
por lo tanto la pref de firma se estaba ignorando.
De ahí MOZ_REQUIRE_SIGNING vacío en recipes/firefox.toml. Dos detalles que costaron cada uno su
vuelta:
- El default sale del milestone (
milestone.is_release_or_beta), no del canal de actualización. El nuestro esdefaulty la exigencia estaba activa igual. - Se desactiva con el valor VACÍO, no con cero.
=0muere con «takes 0 values»: es booleana y el cero es un valor que no acepta. Leído del parser de mozbuild, no deducido.
El precio, escrito: este Firefox deja de exigir la firma de Mozilla para cualquier complemento.
La alternativa es firmar en AMO —cuenta y revisión de Mozilla por versión—, que es la dependencia
externa que esta distro existe para no tener. Y no es un desvío: sin esto, el sct del §6.1
tampoco se podría shipear nunca, así que la unidad 6 dependía de ésta sin que el plan lo dijera.
Método que conviene repetir: la prueba del testigo se hizo sin reconstruir, montando el
.cfg modificado con --ro-bind por encima del artefacto. Editar el fichero en el rootfs habría
escrito sobre un hardlink del store y corrompido el artefacto sellado.
2.sexies Lo que quedó PROBADO, y con qué
Tres afirmaciones que este documento venía haciendo sin medir, y cómo se cerraron:
| Afirmación | Prueba |
|---|---|
El re-empaque del omni.ja es determinista |
why-differs entre dos builds de atuq: 85 entradas idénticas, 0 divergen |
| La capa de configuración se aplica | testigo atuq.autoconfig.ok escrito como pref de usuario en el perfil |
| El branding se ve | captura de la pantalla real con grim desde dentro de la jaula, y la página de inicio renderizada por el propio navegador con --headless --screenshot |
| La base también reproduce | why-differs entre dos builds de firefox en el worker: 56 entradas idénticas, 0 divergen |
Y el contraste que salió de la primera: el derivado reproducía y la base no. El BuildID de
firefox era la hora del build, así que dos construcciones con el mismo ArtifactHash daban bytes
distintos — y build-state.json no lo podía ver, porque el hash es input-addressed y no se mueve
por esto. Verde y mintiendo. Cerrado con MOZ_BUILD_DATE desde SOURCE_DATE_EPOCH, que es el
mecanismo de hermeticidad que el sandbox ya tenía — y comprobado como se comprueba esto:
apartando el artefacto como .ref, construyendo de nuevo y pasándole why-differs. Reproduce.
Dos guardianes nuevos en la fase install de firefox, hermanos entre sí, porque los dos fallos
son de la misma familia —una bandera que el configure acepta y el artefacto ignora, muda y a 50
minutos de distancia—: uno comprueba MOZ_REQUIRE_SIGNING: false dentro del omni.ja, el otro que
el BuildID sea el que SOURCE_DATE_EPOCH obliga.
La regla que sale de todo el día: cuando la duda es «¿llegó al artefacto?», la respuesta no está en el log del build ni en la receta — está dentro del artefacto, y hay que ir a buscarla ahí.
7. La costura: un host de native messaging en Rust
Todo lo del §6 que no es CSS pasa por un solo mecanismo: un proceso Rust que habla native
messaging con la extensión de atuq y, del otro lado, con la suite (agora para identidad Ed25519,
khipu, el store, foreign-*, cortafuegos).
Que sea uno y no siete es la decisión: cada feature del §6 pasa a ser un verbo de ese host, no un
proyecto nuevo. El binario vive en tawasuyu (es suite, no build system); atuq sólo lo declara como
dep y le deja el manifiesto de native messaging en su sitio. El nombre del crate se decide en
tawasuyu y con su regla 10 puesta — acá no se bautiza nada de allá.
8. Plan, por unidades de trabajo
Cada una cierra sola, se commitea y se pushea. El orden no es preferencia: cada una destraba a la siguiente.
| # | Unidad | Puerta que abre | Bloqueada por |
|---|---|---|---|
| 1 | waterfox SELLA ✅ 2026-09-06 — b3:88b5a762, 377 M, BuildID determinista y REPRODUCE bit a bit. Cuatro muros, ninguno de la receta: hammer viejo en el worker, el watchdog barriendo el post-mortem, deps estáticas vs. el cairo bundleado, y una rotura de upstream sólo-Linux |
valida la tesis del reúso | — |
| 2 | llvm-toolchainapk add lld + compiler="clang" ✅ 2026-09-05 |
LTO y el muro 3 | — |
| 3 | Construir el firefox con LTO (hash b3:6f2a3b2f) |
el eje de velocidad y las extensiones de atuq |
2 ✅ |
| 3.a | PGO: perfil bajo sway headless, sellado como artefacto propio y consumido por hash | la otra mitad de la ganancia | 3 |
| 3.b | RLBox ENCENDIDO ✅ 2026-09-06 — cadena wasm de CINCO recetas selladas + guardián que la exige en el binario; firefox sellado y REPRODUCE bit a bit (verificar-repro.sh: 0 no-determinismos) |
cierra el hueco de seguridad del §3.bis | 2 ✅ |
| 4 | atuq: receta derivada + overlay de chrome ✅ v0.1 escrita |
el andamio de todo lo demás | 3 |
| 4.b | Branding + re-empaque de omni.ja ✅ v0.2 |
el chrome de verdad | — |
| 4.d | Abrirlo en pantalla — destapó EXDEV, el lanzador, el atk envenenado y la cadena GTK3 estática |
«usable», no sólo «sellado» | — |
| 4.e | v0.3: página de inicio y pestaña nueva por extensión de sistema; exigió MOZ_REQUIRE_SIGNING vacío |
y con eso queda destrabada la unidad 6 (sct) |
rebuild de firefox |
| 4.c | MOZ_BUILD_DATE determinista ✅ — BuildID=19700101000001, con guardián en install y why-differs 56/56 sobre dos builds reales |
el invariante de reproducibilidad | — |
| 5 | Host de native messaging (tawasuyu) | los verbos del §6 | 4 |
| 6 | sct v1 (extensión + testigo) |
el diferenciador que nadie tiene | 5 |
| 7 | Descargas al CAS | 6.2, y alimenta 6.9 | 5 |
| 8 | Archivo + RAG | 6.3 | 5, 7 |
| 9 | Torrent, medios, foco | 6.5–6.7, 6.9 | 5 |
| 9.a | Proxy por contenedor ✅ v0.5 — contenedores por política + extensión con proxy.onRequest |
6.8, y NO dependía de 5: es API de Firefox | — |
Las unidades 2 y 4 son paralelizables: la toolchain no toca el chrome y el chrome no toca la toolchain. Si hay dos frentes, van juntas.
9. Por qué esto no abandona a puriy
puriy es el motor DOM/CSS propio con JS real (QuickJS-NG sobre wasmi) cuyo límite declarado no es
el lenguaje sino el wiring nativo: DOM bindings completos, red, render. Eso no se acelera
escribiendo otro navegador; se acelera con tiempo.
atuq no le compite porque no comparte una sola línea con él, y le sirve porque todo lo del §6
vive del lado Rust: el host del §7, el testigo de sct, las descargas al CAS, el archivo con RAG.
Son agnósticos del motor. Cuando puriy madure, se enchufan del mismo lado sin reescribirse.
Dicho al revés, que es como conviene recordarlo: atuq es el navegador que se puede usar mientras
puriy crece, y el andamio de las features que puriy va a heredar. Si dentro de dos años atuq
se apaga, lo que se tira es una receta derivada y una hoja de CSS; todo lo demás sobrevive.