1# SDD 26 — `atuq`: el envoltorio Gecko de la distro
2
3Escrito 2026-09-05, a partir de la pregunta del usuario: *«ya habiendo compilado firefox y waterfox,
4qué tan complicado vs provechoso suena crear nuestro propio envoltorio, en vez de zen?»*, y de su
5aclaración de por qué la pregunta existe: **`puriy` quedó muy verde y es difícil sacarlo de ahí**.
6
7`atuq` es *zorro* en quechua. El nombre estaba libre —verificado con `grep -ri atuq` sobre
8`/mnt/vvv/hammer` y `/mnt/vvv/tawasuyu`, cero coincidencias— y dice exactamente lo que la cosa es:
9otro fox, con marca propia y sin usar la de Mozilla (ver el §branding de `recipes/firefox.toml`).
10
11**Este documento no reemplaza a `puriy` ni lo cancela.** `puriy` sigue siendo el motor soberano; el
12§9 explica por qué `atuq` es su andamio y no su desvío.
13
14---
15
16## 1. La corrección que ordena todo: no hay envoltorio fuera del chrome
17
18La intuición natural —una carcasa Llimphi con nuestros widgets, y Gecko adentro pintando la página—
19**no es posible**. Gecko no tiene API de embebido en escritorio desde que murió XULRunner; GeckoView
20existe y es de Android. No hay forma soportada de meter el motor en una ventana de `mirada`.
21
22⇒ un envoltorio de Gecko es, necesariamente, **chrome-level**: JS/CSS/XHTML *dentro* del propio
23Firefox, más extensiones, más procesos externos que hablen con él.
24
25Y eso es exactamente lo que Zen es. Zen no toca el motor: su diferencia entera —workspaces, split
26view, glance, compact mode— vive en el chrome, más una herramienta (`surfer`) que reaplica sus
27parches sobre el árbol de Firefox y recompila. **Su costo no es el motor: es la cinta de correr del
28rebase cada cuatro semanas.** Copiar su método sería heredar su costo sin heredar su equipo.
29
30## 2. La decisión de arquitectura: artefacto DERIVADO, no fork de fuente
31
32Si `atuq` parchea el árbol, **cada iteración de UI cuesta un build de Gecko de cuatro horas**. Eso
33mata el proyecto en la primera semana de diseño, porque el diseño de chrome es iterativo por
34naturaleza.
35
36Pero nosotros no distribuimos un binario: sellamos artefactos. Entonces `atuq` es una receta cuyo
37`[deps]` incluye `firefox`, y cuyas fases copian el árbol instalado e inyectan encima:
38
39| Qué se inyecta | Mecanismo de Firefox | Sin recompilar |
40|---|---|---|
41| Marca, iconos, nombre | `distribution/`, `application.ini` | ✅ |
42| Prefs de fábrica | `defaults/pref/*.js` + autoconfig (`.cfg`) | ✅ |
43| Política de empresa | `distribution/policies.json` | ✅ |
44| Chrome propio (dientes, layout) | `userChrome.css` + re-empaque de `omni.ja` | ✅ |
45| Extensiones nuestras | `distribution/extensions/` | ✅ |
46
47**Ventajas que esto tiene sobre el método de Zen, y que son estructurales:**
48
491. **Iteración en segundos, no en horas.** El chrome se toca sin volver a compilar C++.
502. **No mueve el `ArtifactHash` de `firefox`.** El corpus no se invalida; `waterfox` y cualquier otro
51 fork siguen compartiendo el mismo artefacto base.
523. **Las CVE se heredan gratis.** Un fork de fuente te vuelve dueño del reloj de seguridad del
53 binario más atacado de la máquina — es lo que hundió a casi todos los forks de Firefox, y el
54 reproche histórico a Waterfox. Un derivado se reconstruye solo cuando sube `firefox`.
55
56**Los dos gotchas reales del camino, que son trabajo pero chico:**
57
58- **`omni.ja` es un zip ⇒ hay que re-empacarlo determinista** (mtimes fijos, orden estable) o el
59 artefacto deja de reproducir bit a bit. Es la misma disciplina que ya exige la imagen del lab.
60- **El `jarlog` del PGO ordena el `omni.ja` para el arranque** (§3.bis). Re-empacarlo a lo bruto
61 tira esa optimización a la basura sin que nadie lo note. El re-empaque preserva el orden.
62- **Hay que invalidar el startup cache** tras tocarlo (fichero `.purgecaches` junto al binario, o
63 bump del BuildID). Si no, los cambios no se ven y **parece que el overlay no agarró**: un falso
64 negativo de los caros, de la misma familia que el cache-hit que congela regresiones.
65
66### 2.bis Cuándo SÍ hay que parchear el árbol
67
68Cuando algo necesite tocar C++ del motor. Hoy conocemos un caso: `sct` en su forma fuerte (§6.1).
69La regla es: **nada baja al árbol antes de que exista la toolchain del §3**, porque hasta entonces
70cada intento cuesta cuatro horas. Y cuando baje, el vigía de parches (`scripts/vigia-parches.py`,
71commit `aa200a4`) es lo que convierte esas cuatro horas en un minuto para saber si el parche agarra.
72
73## 3. La puerta de las optimizaciones: no es un flag, es una toolchain
74
75El usuario decidió habilitar las optimizaciones. Lo que hay que saber antes de intentarlo:
76
77`recipes/firefox.toml` va con `compiler = "gcc"` por el **muro 3** (zig enlaza `libc++` estática para
78musl y el `configure` de Mozilla exige encontrar un `NEEDED …libc++`; es negativa de upstream, no una
79perilla). El problema es que **PGO, LTO y BOLT son cadena de clang en Gecko**:
80
81- `--enable-lto=cross` quiere clang + lld. El LTO de GCC sobre Gecko no está soportado upstream.
82- `--enable-profile-use` espera `-fprofile-instr-use` (clang), no `-fprofile-use` (gcc).
83- BOLT necesita `llvm-bolt`.
84
85Y **el corpus no tiene clang usable como compilador**. Verificado en las recetas, no supuesto:
86
87- `recipes/clang18.toml` compila `ninja -C build libclang.so clang-resource-headers` y su fase
88 `install` copia **sólo** `build/lib/libclang.so*` y las cabeceras `clang-c`. Está ahí porque
89 `bindgen` carga `libclang.so` en runtime. **No hay driver `clang`, ni `lld`, ni `libc++`.**
90- `llvm18` se selló con `-DLLVM_ENABLE_PROJECTS=""` — LLVM a secas.
91
92⇒ la primera versión de este documento concluyó que hacía falta **una receta `llvm-toolchain`
93(clang+lld+libc++ desde fuente)**. **Era caro de más, y se corrigió el mismo día mirando el lab en
94vez de suponerlo:**
95
96- `.dev-fs/alpine` **ya trae clang22 + llvm22 22.1.8** — la misma major que usa el APKBUILD de
97 Alpine para este mismo Firefox (`_llvmver=22`).
98- **`Compiler::Clang` ya existe en hammer**, cableado de punta a punta (`parse_compiler` lo acepta;
99 `hammer-build` pone `CC=clang`, `CXX=clang++`, `AR=llvm-ar`). Ninguna receta lo usaba.
100- Faltaba **sólo `ld.lld`**: `apk add lld` ⇒ dos paquetes, cero upgrades.
101
102Y los tres muros caen sin perder lo que gcc daba: el sondeo de linker se satisface con
103`--enable-linker=lld`, el `ar` lo pone hammer solo, y el `NEEDED` de la stdlib de C++ existe porque
104clang++ de Alpine usa la `libstdc++` **compartida** — la prueba no es teórica: Alpine construye este
105Firefox con clang22 y **sin** `libcxx` en sus makedepends.
106
107**La huella del lab no se movió, y se midió antes de tocar nada.** `lld` no casa ningún prefijo de
108`TOOLCHAIN_PREFIXES` (`hammer-core/src/lab.rs`), así que los 43 paquetes que entran en `hash_inputs`
109salieron idénticos ⇒ **los 837 artefactos sellados quedan intactos**. Eso abarata el cambio hoy y a
110la vez deja un agujero escrito: la versión de `lld` no es parte de la identidad del artefacto, y sólo
111expone a las recetas `compiler="clang"` — hoy, una. Cerrarlo cuesta re-hashear el corpus entero.
112
113**Hecho el 2026-09-05** (commit `cb3ecd5`): `firefox` va con `compiler = "clang"`,
114`--enable-linker=lld`, `--enable-lto=cross`, `--enable-packed-relative-relocs` y
115`--with-unsigned-addon-scopes=app,system`. Falta construirlo.
116
117**Riesgos de PGO, escritos antes de empezar:**
118
119- El PGO de Mozilla **corre el navegador** para juntar el perfil (`profileserver.py`, headless +
120 marionette). Dentro de un sandbox hermético, sin red y sin X11, eso hay que hacerlo andar.
121 Headless no necesita display, pero sí necesita que el árbol de perfil se genere adentro.
122- **El perfil resultante es un input del artefacto.** O entra en `hash_inputs`, o `atuq` deja de ser
123 determinista y no lo vamos a notar: sería otra forma del lab que no está en `hash_inputs`.
124- Ganancia esperable, para calibrar expectativas: PGO+LTO en Gecko son del orden de 10-25% en carga
125 de página y JS. **Hoy estamos ATRÁS de Zen en este eje, no adelante** — Zen hereda la
126 configuración de Mozilla tal cual. Esto no es una ventaja nuestra: es una deuda que se salda.
127
128### 3.bis El diff contra Alpine — y no es sólo PGO
129
130Medido el 2026-09-05 trayendo el `APKBUILD` y el `mozconfig` de `community/firefox` de aports y el
131`PKGBUILD` de Arch, y comparándolos línea a línea con el mozconfig de `recipes/firefox.toml`.
132**Alpine es nuestro propio upstream** —de ahí salen los once parches de musl— así que la comparación
133no es contra una distro lejana: es contra la que ya construye este mismo código.
134
135| | `recipes/firefox.toml` | Alpine `community/firefox` |
136|---|---|---|
137| LTO | **no** | `--enable-lto=cross` |
138| PGO | **no** | `--enable-profile-use=cross` + `merged.profdata` + `jarlog` |
139| Linker | GNU ld (via gcc) | `--enable-linker=lld` (lld 22) |
140| RELR | **no** | `--enable-packed-relative-relocs` |
141| Sandbox RLBox | **APAGADO** (`--without-wasm-sandboxed-libraries`) | `--with-wasi-sysroot` |
142| Extensiones sin firmar | **no declarado** | `--with-unsigned-addon-scopes=app,system` |
143| Allocator | `--disable-jemalloc` | `--disable-jemalloc` — **igual, no es desviación nuestra** |
144| Librerías | todo bundleado | `--with-system-{icu,nss,nspr,av1,vpx,webp,png,jpeg,zlib,ffi,libevent,pixman,pipewire}` |
145
146Arch hace lo mismo en el eje de velocidad («Do 3-tier PGO», `--enable-lto=cross`,
147`--enable-profile-use=cross`). **Ni Alpine ni Arch usan BOLT** (cero menciones en los dos ficheros)
148⇒ BOLT no es la desventaja, es el extra. Lo que nos separa del resto del mundo es **PGO + LTO**, del
149orden de 10-20% en carga de página y JS.
150
151**Tres lecturas que hay que sacar de esa tabla:**
152
1531. **RLBox apagado es una desventaja de SEGURIDAD, no de velocidad, y probablemente pesa más que el
154 PGO.** Es la jaula wasm alrededor de los parsers de fuentes y medios —graphite, ogg, expat,
155 woff2—, o sea exactamente el código que come entrada no confiable. Encenderlo pide `wasi-sdk` +
156 `wasi-compiler-rt` en el corpus: receta propia, no un flag.
1572. **`--with-unsigned-addon-scopes=app,system` nos falta y `atuq` lo NECESITA** para shipear sus
158 propias extensiones desde `distribution/extensions/` (§6.1, §7). Es un flag de `configure` ⇒
159 **una capacidad de `atuq` que no se resuelve en el overlay: exige tocar la base.**
1603. **Las librerías del sistema son diferencia a propósito, no deuda.** Alpine usa las suyas; nuestro
161 corpus *es* la fuente y bundlear es lo que mantiene la clausura cerrada.
162
163**Corolario que ordena el plan: todo esto entra en UN SOLO rebuild de `firefox`.** Cada rebuild son
164cuatro horas y re-sella la cola Gecko entera (waterfox incluido), así que LTO, PGO, RELR, lld, RLBox
165y el scope de addons se juntan en una sola pasada, no en seis.
166
167### 3.ter Los dos muros del PGO que las distros no tienen
168
169**El perfil se junta corriendo el navegador, y las dos distros lo hacen bajo `xvfb-run`** (Alpine:
170`xvfb-run -a -s "-screen 0 1920x1080x24" ./mach python build/pgo/profileserver.py`; Arch, idéntico).
171**Nosotros no tenemos X11 en el corpus** —es la decisión Wayland-only de toda la distro— así que ese
172camino no existe acá. La salida hammer-nativa es correrlo bajo un **sway headless**
173(`WLR_BACKENDS=headless`), que ya tenemos del frente wlr/sway y arranca en segundos.
174
175**✅ EL MURO DEL DISPLAY ESTÁ DESPEJADO (2026-09-06).** No es una previsión: se corrió. `sway`
176headless con `WLR_BACKENDS=headless` y `WLR_RENDERER=pixman` arranca en **gioser**, que es un LXC
177**sin `/dev/dri`** —o sea sin GPU ninguna, todo software— y dibuja píxeles de verdad:
178`docs/evidencia/sway-headless-gioser-sin-gpu-2026-09-06.png` (sway 1.10 / wlroots 0.18.2 /
179foot 1.27.0, los tres binarios nuestros, salida `HEADLESS-1` a 1280x720). El ciclo entero es de
180~2 min con `scripts/wlr/sway-headless.sh`.
181
182Dos cosas que hubo que resolver y conviene tener escritas antes de repetirlo:
183· **Hidratar bajo el montaje del store.** `hydrate-profile.py` ENLAZA, y un hardlink no cruza
184 montajes: con `--into work/...` sale `EXDEV` porque el store vive en otro disco. Con
185 `--into store/.rootfs/sway` son 193/193 nodos, 41.677 ficheros y **4,9 G que no ocupan** (son
186 hardlinks al store).
187· **Inyectar el loader de musl.** `sway` salió DINÁMICO y pide `/lib/ld-musl-x86_64.so.1`, que el
188 cierre no trae; el error es `exec: /usr/bin/sway: not found`, que se lee como «falta el binario»
189 y lo que falta es su intérprete. Se resuelve con
190 `--ro-bind /usr/lib/musl/lib/libc.so /lib/ld-musl-x86_64.so.1` (en musl el `libc.so` ES el
191 loader). Es la misma familia que el resto de la noche: el mensaje nombra lo que buscó, no lo que
192 falta.
193
194**Y el perfil no es determinista.** Los contadores dependen del timing de la corrida, así que dos
195generaciones del profdata no dan los mismos bytes — a las distros no les importa porque no persiguen
196bit-repro; a nosotros nos rompe el invariante. La salida: **generar el perfil UNA vez y sellarlo como
197artefacto propio, consumido por hash** desde `firefox`. El build optimizado vuelve a ser determinista
198aunque su insumo no lo sea, y el perfil se regenera a propósito, no por accidente.
199
200### 3.quinquies El PGO, y los tres muros que el plan no tenía escritos (2026-09-07)
201
202El §3.ter daba dos muros: el display (X11 que no tenemos) y el no-determinismo del perfil. Los dos
203eran ciertos. Aparecieron **tres más**, y ninguno se ve sin construir:
204
2051. **`libclang_rt.profile.a` no existía.** El lab trae clang 22.1.8 pero **ninguna runtime de
206 compiler-rt** — su `lib/` del resource dir ni existe, exactamente como ya documentaba
207 `wasi-compiler-rt` para el caso wasm. El build instrumentado murió en el minuto 38:56 con
208 `ld.lld: cannot open …/libclang_rt.profile.a`. De ahí sale `recipes/compiler-rt-profile.toml`.
209
2102. **Los sandboxes de Firefox matan la corrida de perfilado.** Con ellos puestos los procesos hijo
211 mueren con **signal 11**, el que renderiza no llega a nacer, el servidor local registra **0 GET**
212 y el único `.profraw` que queda es el del padre arrancando — un perfil de nada, con todo en
213 verde. Es lo mismo que hace el `profileserver.py` de Mozilla. Sólo aplica a la corrida de
214 entrenamiento: el firefox que se distribuye lleva sus sandboxes intactos.
215
2163. **El worker no puede bajar del mirror de fuentes.** Tiene `scripts/fuentes/mirror-env.sh` desde
217 el 2026-09-01 pero no la clave del Storage Box, así que una receta pineada al mirror —y el perfil
218 es la primera— no se construye allá. Se sembró el artefacto sellado, que es content-addressed:
219 consigue lo mismo sin mover una credencial de máquina.
220
221**Cómo se verificó que el PGO llegó, y por qué NO como RLBox.** Con RLBox el artefacto delata la
222jaula (símbolos `w2c_*`, cero → 634). Acá el discriminante análogo serían las secciones
223`.text.hot`/`.text.unlikely` que clang emite al particionar por temperatura — y **no sirve**: con
224lld y ThinLTO el enlazador las fusiona, así que dan cero en el binario con PGO y sin él. La
225evidencia que sí vale está un nivel por debajo del `configure`: **189 invocaciones del compilador
226llevan `-fprofile-use`**, el configure encontró `llvm-profdata`, y no hay ni un aviso de perfil que
227no cuadre. `libxul.so` pasa de 226.078.752 a 227.802.816 bytes, consistente con inlining de caminos
228calientes. Es evidencia de build, no de artefacto, y conviene decirlo así.
229
230**LA GANANCIA, MEDIDA (2026-09-07).** Mismo rootfs, mismo script, corridas INTERCALADAS A/B/A/B
231para que la deriva de carga afecte a las dos, mediana de 7, calentamiento descartado. Lo único que
232cambia entre variantes es un `--ro-bind` de `/usr/lib/firefox`.
233
234| página | sin PGO | con PGO | |
235|---|---|---|---|
236| DOM+layout+strings, **fuera** del corpus de entrenamiento | 23.540 ms | 21.327 ms | **−9,4 %** |
237| SunSpider `3d-raytrace`, **dentro** del corpus | 16.034 ms | 15.878 ms | −1,0 % |
238
239Los rangos no se solapan en ninguna de las dos (23.185–23.654 vs 21.030–21.650), así que las dos
240diferencias son reales y no ruido.
241
242⚠ **Y el resultado sale al revés de lo esperable, que es lo interesante.** La intuición dice que
243medir sobre el conjunto de ENTRENAMIENTO infla la ganancia; acá el corpus da el número MÁS BAJO. La
244razón es que `3d-raytrace` es aritmética pura en un bucle caliente, y ese bucle **no lo ejecuta el
245C++ de SpiderMonkey: lo ejecuta código máquina que el JIT genera en tiempo de ejecución**. El PGO
246optimiza el intérprete, el GC y el propio compilador JIT — no el código que el JIT emite. Un
247benchmark JIT-bound es casi ciego al PGO por construcción.
248
249Donde sí se ve es en el camino DOM/layout/arranque, que es C++ de principio a fin. Y eso es también
250lo que un usuario percibe: la medición cubre el ciclo COMPLETO del proceso (arrancar → renderizar →
251capturar → salir), no el rendimiento en régimen de una página ya cargada. De ahí que hasta la página
252de SunSpider tarde 16 s: casi todo es arranque.
253
254**Corolario para el corpus de entrenamiento:** está sesgado hacia JS (SunSpider domina en número de
255páginas) justo donde el PGO menos rinde. Un corpus con más maquetación y menos aritmética
256probablemente daría más ganancia. Es una unidad de trabajo propia y no se hace de paso.
257
258**Y la pregunta que un insumo no determinista obliga a hacer, respondida: `firefox` SIGUE
259REPRODUCIENDO bit a bit** (`verificar-repro.sh`: REPRODUCEN 1, DERIVA 0, NO-DETERMINISMO 0). Era el
260riesgo real de esta unidad, no el rendimiento: si el `-fprofile-use` hubiera metido cualquier
261decisión dependiente del orden o del timing, habríamos cambiado velocidad por el invariante del
262proyecto. Congelar el perfil como fuente pineada era el argumento; esto es la medición.
263
264### 3.quater La cadena wasm, y las tres cosas que costó (2026-09-06)
265
266`--without-wasm-sandboxed-libraries` se cambió por `--with-wasi-sysroot=/usr/share/wasi-sysroot`,
267que es literalmente el flag de Alpine. Detrás hay cinco recetas nuestras, en este orden obligatorio:
268
269```
270wasi-libc-headers → wasi-compiler-rt → wasi-libc → wasi-libcxx → wasi-sdk → firefox
271```
272
273`wasi-libc-headers` existe **sólo** para romper el ciclo del bootstrap y pinea un commit VIEJO a
274propósito (triple `wasm32-wasi`), mientras `wasi-libc` pinea el nuevo (`wasm32-wasip1`, el que
275clang 22.1 usa de verdad). Es lo mismo que hace Alpine y no es un descuido.
276
277**Lo que costó, que es la parte reusable:**
278
2791. **`check-symbols` falla por sesgo de clang, no por un artefacto malo.** El lab trae clang 22.1.8
280 y el snapshot del árbol viene de uno anterior: sobra `#define __wasip1__ 1` y nada más. Se
281 reconcilia el snapshot; **no** se salta con `make no-check-symbols`, que se lleva por delante la
282 comparación de símbolos —la mitad que sí vale y que pasaba byte a byte.
283
2842. **Faltaba `wasi-libcxx`, y el hueco era visible en el grafo sin construir nada.** El configure
285 murió a los 6 s con `'cstring' file not found`, culpando a los headers de WASI, que estaban
286 perfectos: `<cstring>` es C++ y las librerías enjauladas no son todas C. El `wasi-sdk` de Alpine
287 declara `wasi-libc wasi-libcxx wasi-compiler-rt` —TRES— y la cadena tenía dos. **El `depends`
288 del paquete ajeno equivalente es una comprobación de completitud gratis**, antes de gastar CPU.
289
2903. **Un directorio VACÍO que sí importa.** Con `wasi-libcxx` sellado el configure volvió a morir
291 igual, y `cstring` estaba en el artefacto: clang no lo buscaba. Falta el `mkdir -p
292 include/c++/v1` que Alpine tiene en su `package()` y que parece ruido de empaquetado — es la
293 sonda por la que el driver decide que el sysroot tiene layout de libc++. Medido dentro del lab:
294 sin ese directorio, `'cstring' file not found`; con él, exit 0. Es el **reverso exacto de la
295 regla 3 del `CLAUDE.md`**: allá un directorio vacío es un artefacto mentiroso, acá es una
296 declaración dirigida al compilador.
297
298De ahí sale el criterio de guardián que ahora usan estas recetas: **«existe» no es «se encuentra»**.
299`wasi-libcxx` corre hoy la misma prueba que hace el configure de firefox, contra un sysroot
300fusionado con el de su dep, así que un fallo aparece en el segundo 20 de una receta chica —con la
301lista de búsqueda de clang impresa— y no en el segundo 6 de un build de cuatro horas que además
302culpa a otro. Y `firefox` exige RLBox **en el binario**: sobre `b3:352d7880` (el último con RLBox
303apagado) los patrones `w2c_`, `rlbox` y `wasm2c` dan cero los tres en `libxul.so`, así que
304cualquiera apareciendo prueba que la jaula entró. Es la tercera repetición de la lección de
305`MOZ_REQUIRE_SIGNING` y `MOZ_BUILD_DATE`: una bandera que el configure acepta y el artefacto ignora.
306
307**Evidencia de reproducibilidad cruzada (2026-09-06).** Las cinco recetas se construyeron por
308separado en las DOS máquinas —el hub (gioser, 4c) y el worker (`dev.gioser.net`, LXC 6c)— y los
309cinco artefactos salen **byte a byte idénticos**, comparando el árbol completo de cada uno:
310
311| receta | sha256 del árbol | |
312|---|---|---|
313| `wasi-libc-headers` | `0d34f9cb27530606…` | ✓ |
314| `wasi-compiler-rt` | `0c4fc6fdf08ef908…` | ✓ |
315| `wasi-libc` | `c13acdf896f2b78c…` | ✓ |
316| `wasi-libcxx` | `b1d9aeec08499063…` | ✓ |
317| `wasi-sdk` | `b49f7a74a5caed3f…` | ✓ |
318
319No es un trámite: **el lab NO entra en `hash_inputs`**, así que dos labs distintos pueden sellar
320bytes distintos en la misma dirección del store y nada lo detecta. Esto dice que para esta cadena
321los dos labs coinciden de verdad, y no sólo que coinciden los `ArtifactHash`.
322
323## 4. Lo que `atuq` NO promete
324
325Esta sección existe antes que la lista de features a propósito, con el mismo criterio que el modelo
326de adversario de `qullqa`: **lo que no se promete se escribe primero, porque una promesa falsa de
327privacidad es peor que no ofrecer nada.**
328
329**`atuq` no es Tor Browser y no va a tener «modo Tor».** Tor Browser no es «Firefox + SOCKS»: su
330valor son las contramedidas de huella (RFP, letterboxing, normalización de fuentes/canvas/timers) y
331sobre todo **un conjunto de anonimato donde todos se ven idénticos**. `atuq` es, por construcción, un
332binario único en el mundo: musl, Wayland-only, branding propio, extensiones propias. Alguien saliendo
333por Tor desde `atuq` es **más** identificable, no menos, y su conjunto de anonimato son diez
334personas. Para anonimato: Tor Browser, y lo decimos nosotros primero.
335
336Lo que sí se ofrece, y es honesto porque es otra cosa: **proxy por contenedor** (§6.8) — separación
337de tráfico, no anonimato.
338
339**`atuq` tampoco promete** anti-fingerprinting propio (es un programa de investigación con equipo
340full-time) ni bloqueo de anuncios propio (se shipea uBO y listo).
341
342## 5. Contexto verificado al escribir (2026-09-05)
343
344De `docs/state/build-state.json`, no de memoria:
345
346| Dato | Valor |
347|---|---|
348| Recetas / nodos | 838 / 840 |
349| Selladas | 837 |
350| `firefox` 154.0 | **sealed**, cola `corpus` |
351| `waterfox` 6.7.1.1 | **never** — primer build en curso |
352| Plataforma compartida | `gtk3`, `atk`, `nodejs`, `clang18`, `cbindgen`: todas sealed |
353
354**`atuq` no arranca hasta que `waterfox` selle.** Waterfox es la prueba de la tesis del reúso —«las
355diferencias reales con `firefox.toml` son TRES: la fuente, el branding y la versión»—. Si esa tesis
356falla, el precio de todo este documento cambia y hay que releerlo.
357
358## 6. Los diferenciadores, priorizados
359
360La columna «quién más lo tiene» es lo que evita que nos contemos un cuento.
361
362| # | Qué | Quién más lo tiene | Pieza que ya existe | Costo |
363|---|---|---|---|---|
364| 6.1 | **`sct` — transparencia de scripts** | **nadie** | `puriy-sct`, `puriy-sct-testigo` | medio |
365| 6.2 | **Descargas direccionadas por contenido** | nadie | store CAS de hammer, `tejido` | bajo |
366| 6.3 | **Archivo personal + RAG local** | Rewind/Recall (nube, Windows); SingleFile (guarda, no busca) | `khipu`, `rag-motor`, `willay-rag` | medio |
367| 6.4 | **Historial y perfil sobre `qullqa`** | nadie | `qullqa-core`, `qullqa-pozo` | medio |
368| 6.5 | **Foco por `cortafuegos`, no por extensión** | nadie (nadie es dueño del navegador *y* del sistema) | `cortafuegos`, `pacha` | bajo |
369| 6.6 | **Medios por fuera del navegador** | extensiones sueltas; de fábrica no | `foreign-ytdlp`, `-platform`, `-dlna` | bajo |
370| 6.7 | **IA local en la barra lateral** | Chrome/Edge son nube; Zen no tiene | `rimay`, `iniy` | bajo |
371| 6.8 | **Proxy por contenedor** ✅ **v0.5** | nadie de fábrica | — (API de Firefox) | bajo |
372| 6.9 | **Torrent adentro** | **Vivaldi lo trae; Brave tuvo WebTorrent** | `shared/foreign-torrent` (SDD propio, sobre librqbit) | bajo |
373
374### 6.1 `sct` es la joya
375
376BLAKE3 a todo lo que se ejecuta, y **negarse a correr código que ningún testigo vio nunca**. Es lo
377más cerca que hay de «Certificate Transparency, pero para el JavaScript que te corre en la cara», y
378no lo tiene nadie: ni Zen, ni Brave, ni Tor Browser.
379
380En tawasuyu ya está escrito y —dice su README— la lógica es **agnóstica del transporte** y está
381certificada sin red; `puriy-sct-testigo` es sólo el cable HTTP.
382
383- **v1, sin tocar C++:** extensión con `webRequest` bloqueante que hashea cada respuesta de script y
384 consulta al testigo antes de dejarla pasar.
385- **v2, forma fuerte:** gancho en el script loader de Gecko. Eso sí es parche de árbol ⇒ §2.bis ⇒
386 después de la toolchain del §3.
387
388### 6.2 Una descarga con identidad
389
390Lo bajado no es un archivo en `~/Downloads`: es un objeto BLAKE3 en el store — deduplicado,
391verificable, y re-compartible por `tejido`. El torrent (6.9) alimenta lo mismo. **Ningún navegador
392trata una descarga como algo con identidad**; para todos es un blob con nombre, y por eso lo bajás
393dos veces y no lo sabés.
394
395Ésa, y no «tener torrent», es la razón por la que 6.9 vale: Vivaldi ya tiene torrent, pero termina en
396una carpeta.
397
398### 6.3 El archivo personal es la razón por la que alguien usaría `atuq`
399
400Cada página visitada se congela en el CAS y entra al DAG de `khipu`; después le preguntás a tu
401propio historial en lenguaje natural, **todo local**. Es la feature que más justifica el proyecto
402entero, y sale de piezas que ya están escritas.
403
404### 6.4 `qullqa` con su advertencia puesta
405
406Almacenamiento con negación plausible y modelo de adversario **escrito** — ningún navegador tiene
407eso; el modo incógnito es teatro. ⚠ Con la advertencia que el propio SDD de `qullqa` ya anotó y que
408**hay que repetir acá, no esconder**: en una máquina con swap sin cifrar, el documento no aplica.
409Se promete lo que se puede probar.
410
411### 6.8 Proxy por contenedor — HECHO (v0.5, 2026-09-06)
412
413**Es el primer diferenciador del §6 que se paga entero**, y se pudo pagar ahora porque es el único
414de la lista que no pasa por el host del §7: es API de Firefox y nada más. Cada contenedor
415—Personal, Trabajo, Banco, Compras— puede salir por su propio proxy.
416
417Tres piezas, en tres ficheros, y **ninguna alcanza sola**:
418
419| Pieza | Dónde | Qué aporta |
420|---|---|---|
421| `privacy.userContext.enabled` | `atuq.cfg` | prende los contenedores, que Firefox trae apagados |
422| `Containers.Default` | `distribution/policies.json` | los CREA — es el único mecanismo que los pone en un perfil NUEVO |
423| `3rdparty.Extensions` | `distribution/policies.json` | la config de fábrica, que la extensión lee como `storage.managed` |
424| `proxy.onRequest` | `extensions/proxy/fondo.js` | lo único que ve el `cookieStoreId` de cada petición |
425
426**Las cuatro se comprobaron DENTRO del artefacto antes de escribir una línea**, que es la regla que
427dejó el §2.sexies: `Containers` y `3rdparty` están en el `policies-schema.json` de `browser/omni.ja`,
428`cookieStoreId` en el `schemas/proxy.json` de `omni.ja`, y `storage.managed` lee
429`Services.policies.getExtensionPolicy(id)` en `ext-storage.js`. Ninguna de las cuatro se leyó de la
430documentación de Mozilla: se leyeron de **nuestro** build, que es el que las tiene que traer.
431
432**Tres decisiones de diseño que valen más que el código:**
433
4341. **La configuración se indexa por NOMBRE de contenedor, no por `cookieStoreId`.** El id
435 (`firefox-container-3`) depende del ORDEN en que se crearon, así que la misma configuración
436 aplicada a otro perfil apuntaría a otro contenedor. Un identificador que cambia de significado
437 entre máquinas no sirve para configurar una distro.
4382. **Fail closed.** Un contenedor que TIENE proxy configurado y no se pudo honrar —entrada
439 inválida, API que falla, mapa a medio construir— **no sale directo**: va a un destino cerrado y
440 el navegador muestra el error. Salir directo sería una fuga silenciosa, y es la misma familia
441 que el artefacto vacío de la regla 3: el fallo que llega hasta el final diciendo que todo fue
442 bien. Un usuario que pidió que «Banco» no salga por su línea tiene que ver un error, no navegar.
4433. **`proxyDNS` viene PRENDIDO.** Sin él, Gecko resuelve el nombre por su cuenta antes de hablar
444 con el proxy: la consulta DNS sale justo por la línea que se quería evitar. Es la fuga clásica de
445 esta configuración, así que el default es el seguro y hay que apagarlo a mano.
446
447**Y lo que NO promete está en la propia página de opciones, arriba de todo y no en un pie**:
448separación de tráfico, no anonimato; no toca la huella del navegador; para anonimato, Tor Browser.
449Es el §4 cumplido en el sitio donde el usuario lo va a leer.
450
451#### Lo que quedó PROBADO, y con qué
452
453Corriendo el árbol de atuq en la misma jaula que usa `scripts/atuq-nested.sh`:
454
455| Afirmación | Prueba |
456|---|---|
457| 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 |
458| Las dos extensiones se INSTALAN | `extensions.json` del perfil nombra `inicio@atuq.tawasuyu` y `proxy@atuq.tawasuyu` |
459| 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 |
460| **Una petición hecha en «Banco» SALE por el proxy de «Banco»** | `scripts/test-atuq-ruteo.py`: dos pestañas piden la misma url; al destino llega `GET /directo` y al puerto del proxy un saludo **SOCKS5 (`\x05\x01\x00`)**, y la de «Banco» no aparece nunca en el destino. Con `--negative-control` (sin proxy configurado) las dos van directas y al proxy no llama nadie |
461| 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 |
462| 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 |
463
464⚠ **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`.
465
466**Y de paso quedó comprobado que la dep SÍ se sigue**, que era la promesa del §2 y no una que convenga
467creer sin medir: en un catálogo de sonda, cambiar una bandera del `firefox` copiado mueve el hash de
468`atuq` (`f2960991` → `60ade76a`), mientras que con el `firefox` idéntico al del corpus da el mismo
469hash. O sea que un `atuq` viejo **no** puede quedarse tapando un motor nuevo — que es justo la forma
470que tendría acá el cache-hit que congela regresiones.
471
472**Cómo se cerró lo que faltaba (2026-09-06).** Durante unas horas esta sección decía que probar el
473paquete quedaba pendiente, con tres puertas cerradas. Las tres siguen cerradas y la prueba existe
474igual, porque el camino era otro.
475
476Las tres puertas, que conviene NO volver a golpear:
477
4781. **Desde la línea de comandos no hay flag** para el `userContextId` de la pestaña inicial. Y un
479 `moz-extension://` tampoco se abre así: el manejador intenta resolverlo como fichero y muere con
480 `NS_NOINTERFACE [nsIFileURL.file]`, abriendo la home en su lugar.
4812. **Marionette está en el artefacto y responde** —se le habló con un cliente propio de 60 líneas,
482 el protocolo es `<longitud>:<json>`—, pero abrir una pestaña de contenedor pide contexto chrome,
483 y eso exige `-remote-allow-system-access`; en la jaula no lo tomó ni por bandera, ni con
484 `--remote-debugging-port`, ni por `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1`, que es lo que
485 `RemoteAgent.sys.mjs` lee en su constructor. Cabo suelto medido, no suposición.
4863. **Desde la extensión, `tabs.create({cookieStoreId})` exige el permiso `cookies`**
487 (`ext-tabs-base.js:getUserContextIdForCookieStoreId`). Dárselo a la extensión del proxy para que
488 pueda probarse a sí misma sería pagar con la superficie de ataque del producto una comodidad del
489 test. Sigue sin dárselo.
490
491**La cuarta puerta estaba abierta: el fichero de sesión.** `sessionstore` guarda el `userContextId`
492de cada pestaña y Gecko lo restaura, así que se fabrica a mano. El formato es `mozLz40\0` + tamaño +
493un bloque LZ4, y **un bloque de sólo literales es LZ4 válido**: veinte líneas, sin depender de
494ninguna librería. Con eso se abren dos pestañas —misma url, distinto contenedor— sin pedirle nada al
495navegador.
496
497Y la medición no le pregunta nada tampoco: **se le ponen dos oídos en la red y se mira a cuál llama.**
498El destino contesta un HTTP 200; el «proxy» no habla SOCKS, sólo anota quién lo saludó. Que al
499puerto del proxy llegue un `\x05\x01\x00` es la prueba: ese saludo sólo aparece si Gecko decidió
500hablar con un proxy para esa petición, y el único que se lo pudo indicar es nuestro
501`proxy.onRequest` mirando el `cookieStoreId`.
502
503Tres detalles sin los cuales la prueba mide un silencio y se lee como un fallo:
504`browser.sessionstore.restore_on_demand=false` (si no, las pestañas restauradas no piden nada hasta
505que alguien las mira), `network.proxy.allow_hijacking_localhost=true` (Gecko saltea el proxy para
506localhost, y la petición de «Banco» iría directa haciendo parecer culpable a la extensión), y
507`triggeringPrincipal_base64: vQ==` en cada entrada de sesión (sin principal la pestaña se restaura
508pero no navega).
509
510**Y la prueba trae su propio control negativo**, porque es lo que este repo se exige desde hoy: con
511`--negative-control` no se configura el proxy y se exige el resultado CONTRARIO —las dos pestañas
512directas, nadie llamando al proxy—. La única diferencia entre las dos corridas es una línea de
513configuración y el observable se da vuelta entero. Sin ese modo, una prueba que se hubiera vuelto
514ciega se vería idéntica a una que funciona.
515
516### 6.10 Lo que `atuq` NO PUEDE hacer hoy, y por qué (2026-09-07)
517
518Sale de un punto ciego que nombramos entre los dos frentes y que **ningún auditor de `readelf`
519puede cubrir**. La jerarquía de lo que se ve:
520
521 NEEDED del ejecutable → lo ve `test-atuq-rootfs.py` y lo ve `vigia-sonames.py`
522 NEEDED de un .so dlopeado → lo ven los dos, si el fichero está en el artefacto
523 dlopen("libfoo.so.1") LITERAL → NO LO VE NINGUNO
524
525Y un navegador vive del tercer escalón: Firefox sondea ffmpeg, VA-API, vulkan, libnotify y una
526docena más **por nombre**, y cuando no están **no falla: apaga la función y sigue**. No hay línea
527roja; hay una función que nadie ofrece y nadie reclama. Es la misma forma que ya nos costó caro con
528OBS (`dlopen("libGL.so.1")` desde su glad).
529
530`scripts/test-atuq-rootfs.py --dlopen` busca esas cadenas y las cruza contra el rootfs. **Es un
531heurístico y se declara como tal** —una cadena no prueba un `dlopen`, y su ausencia no prueba que no
532lo haya—, así que no falla nunca: imprime un mapa triado. Lo que sí se puede afirmar es lo
533contrario, que es lo útil: **si la cadena está y el fichero no, esa función no existe en esta
534imagen.**
535
536De 85 cadenas con forma de soname, **47 no tenían quién las provea**. Triadas (y con dos filas
537corregidas el mismo día — ver §6.11, que las midió en vez de deducirlas; hoy el mapa da **7 huecos**):
538
539| Qué se pierde | Estado |
540|---|---|
541| **Códecs del sistema (H.264/AAC)** | ❌ **ERA FALSO** — `recipes/ffmpeg.toml` existe, está sellada y ya viajaba en los cuatro escritorios por `mpv`. Corregido en §6.11: **reproduce** |
542| **Notificaciones web** (`libnotify`) | ✅ **CERRADO el 2026-09-07**: `recipes/libnotify.toml`, 0.8.8, compartida. Es el primer hueco que este mapa encontró Y cerró |
543| **Llavero** (`libsecret`) | hueco — pero **la receta ya existe** en `incoming-gnome` |
544| **Sonidos del sistema** (`libcanberra`) | hueco — receta en `incoming-kde` |
545| **WebGPU / vulkan** (`libvulkan`) | hueco — receta `vulkan-loader` en `incoming-kde` |
546| **Vídeo por hardware** (VA-API) | hueco, pero **no por falta de receta**: `libva` está sellada y en las cuatro imágenes. Falta el driver — las tres mesa van con `-Dgallium-va=disabled` y `-Dvideo-codecs=` vacío ⇒ ni un `*_drv_video.so` |
547| **Lectura en voz alta** (`libspeechd`) | hueco — sin receta |
548| `libGL` | **decisión**: la distro es Wayland-only sin GLX y Firefox va por EGL |
549| `libcurl` | **decisión**: sólo lo usa `pingsender`, que manda telemetría — apagada |
550| Kerberos, menú global, ML | fuera de alcance |
551| `libdl/libpthread/librt/libudev.so.0/libfreebl3` | ruido: musl los funde, o el soname es viejo |
552
553**El primero ya está cerrado.** `libnotify` no tenía receta y ahora la tiene: es la prueba de que el
554mapa sirve para algo más que mirarlo. ⚠ Con una advertencia que va escrita en la propia receta:
555`libnotify` no trae un daemon, manda `org.freedesktop.Notifications` por D-Bus ⇒ tenerla resuelve la
556mitad —que firefox la encuentre— y la otra mitad es que en la imagen haya alguien escuchando ese
557nombre. Nadie debería leer «notificaciones arregladas» y esperar un globo en un QEMU pelado.
558
559**⚠ CORRECCIÓN, mismo día: escribí que tres de los siete eran «baratos, promoción y no autoría»
560—`libsecret`, `libcanberra`, `vulkan-loader`— y es FALSO.** Lo comprobé antes de declararlos en los
561perfiles, y en los tres casos falta la otra mitad:
562
563| «barata» | por qué NO alcanza con declararla |
564|---|---|
565| `vulkan-loader` | **las tres recetas de mesa construyen con `-Dvulkan-drivers=` VACÍO** ⇒ el loader no encontraría un solo dispositivo. Un cargador sin ICD no es WebGPU |
566| `libsecret` | no hay receta de `gnome-keyring` ni de ningún servicio de secretos: firefox hablaría a `org.freedesktop.secrets` y no habría nadie |
567| `libcanberra` | no hay receta de `sound-theme-freedesktop`: la librería carga y no suena nada |
568
569Declararlas habría subido el número de paquetes de la imagen sin encender una sola función. **Una
570librería es media función; la otra mitad es quien la atiende**, y eso vale para todas las de esta
571tabla. Quedan como huecos, no como promociones pendientes.
572
573⚠ **Y esta frase también se cayó el mismo día.** Decía que de los otros cuatro «el más caro para el
574usuario es `ffmpeg`: sin él, un sitio que sirva H.264 no reproduce» — con lo cual el hueco más caro
575del mapa **no existía**. Ver §6.11: la receta estaba, faltaba declararla en el runner, y H.264 (y
576AAC, VP9, Opus, AV1, MP3, FLAC) reproducen. De los cuatro «sin receta» quedan tres, y ninguno es un
577códec: lectura en voz alta (`libspeechd`), el driver de VA-API, y `libpci` —que sólo lo usa
578`glxtest`, o sea irrelevante sin GLX.
579
580**Y la misma vara aplicada a `libnotify`, que sí se declaró:** su otra mitad es un daemon que
581escuche `org.freedesktop.Notifications`. Medido perfil por perfil:
582
583 escritorio-kde plasma-workspace ✓ completa
584 escritorio-gnome gnome-shell ✓ completa
585 escritorio-cosmic cosmic-notifications ✓ completa
586 escritorio-sway dunst 1.12.2 ✓ completa — CERRADA el 2026-09-07
587
588La de sway se cerró el mismo día con `recipes/incoming-wlr/dunst.toml`, y la trampa del homónimo
589—**el nombre `mako` YA ESTÁ OCUPADO en el corpus** por el motor de plantillas de Python de Mesa— se
590esquivó eligiendo dunst, que además habla D-Bus por GDBus y no por `sd-bus`, o sea sin receta nueva
591de `basu`. La prueba no es que selle: `scripts/wlr/dunst-headless.sh` levanta sway headless y un bus
592de sesión, manda un `notify-send` y **deja que el bus active el daemon solo** —el mismo camino que
593recorre una página web en `atuq`—, y mide un diff de píxeles antes/después: 14.832 cambiados con el
594`.service` puesto, 0 sin él. Evidencia en `docs/evidencia/dunst-sway-notificacion-2026-09-07.png`.
595
596Y la cadena se cerró hasta el final, que es donde vive la promesa: con `--via-atuq` el emisor no es
597`notify-send` sino **una página web dentro del navegador**. Ahí se ejercitan las cinco piezas —
598`new Notification(...)` → `libxul` haciendo `dlopen("libnotify.so.4")` (que no es NEEDED de ningún
599ELF, o sea invisible para cualquier auditor) → D-Bus → activación → dunst dibujando—, y el veredicto
600es el `onshow` del motor, no el diff de píxeles: con el `.service` puesto llega a `MOSTRADA #33`, sin
601él el motor dice `ERROR al mostrar` y no se dibuja nada. Evidencia en
602`docs/evidencia/atuq-notificacion-web-sway-2026-09-07.png`.
603
604Vale la pena decir qué prueba eso de más: que en el proceso de `atuq` **la GLib no se duplica**. El
605navegador usa la cadena `-shared` (la que arrastra su GTK3) y `libnotify.so.4` enlaza esa misma, así
606que el `dlopen` no repite GObject. El que sí lo repetía era `dunstify`, y por eso no se shipea.
607
608⚠ De paso apareció otra media función, y ésta toca a `libnotify` directamente: **`dunstify`
609segfaultea hasta en `--help`** porque mezcla la GLib ESTÁTICA del corpus con la COMPARTIDA que
610arrastra `libnotify.so.4` — dos copias de GObject en un proceso. No se shipea; el emisor es
611`notify-send`, que viene dentro del artefacto de libnotify y enlaza toda la cadena compartida. Vale
612como recordatorio de que en este corpus **quién enlaza qué GLib es parte del contrato**, no un
613detalle del build.
614
615**Lo que esta sección cambia de fondo:** hasta hoy la pregunta era «¿arranca?», y la respuesta era
616sí. La pregunta que faltaba era «¿y qué NO puede hacer?», que ninguna métrica del repo respondía
617porque todas miran presencia y ésta mira ausencia declarada por el propio binario.
618
619### 6.11 Y ahora la pregunta que faltaba: ¿REPRODUCE? (2026-09-07)
620
621El §6.10 preguntó «¿qué NO puede hacer?» y contestó leyendo cadenas. Media pregunta. La otra mitad
622—**¿se ve el vídeo?**— no la contesta ningún auditor de ELF, y al hacerla aparecieron **dos errores
623del mapa y un bug real del navegador**.
624
625**Error 1: «no hay receta de ffmpeg».** La hay. `recipes/ffmpeg.toml`, 7.1, sellada, publica
626`libavcodec.so.61` —uno de los once sonames que `libxul.so` sondea por `dlopen` literal— y **ya
627viajaba en la clausura de los cuatro escritorios**, arrastrada por `mpv`. Comprobado con
628`yupana.membresia`, que es la vista honesta por par `(cola, nombre)`, no leyendo el TOML:
629
630 escritorio-kde 308 nodos · escritorio-gnome 192 · escritorio-sway 203 · escritorio-cosmic 163
631 ffmpeg ✓ en los cuatro · libva ✓ en los cuatro
632
633Lo que faltaba no era la receta: era **la lista de raíces de `scripts/atuq-nested.sh`**, escrita a
634mano, que hidrataba un rootfs más pobre que la imagen. Otra vez el instrumento y no el artefacto —
635la misma forma que `gcc-libs` el día anterior. Y la comprobación previa que sí se pudo hacer sin
636correr nada: de los 82 símbolos con forma `av*_` que `libxul` dlsimboliza, **los 9 que nuestro
637`libavcodec` no exporta son los de la API vieja** (`avcodec_decode_video2`, `av_lockmgr_register`,
638`avcodec_register_all`…), que Firefox sólo pide para las versiones 53-57.
639
640**Error 2: «VA-API, sin receta».** También la hay, y también está en las cuatro imágenes. El hueco
641es real pero por la OTRA mitad: las tres recetas de mesa construyen con `-Dgallium-va=disabled` y
642`-Dvideo-codecs=` vacío ⇒ **no existe un solo `*_drv_video.so`**. Un cargador sin ICD no acelera
643nada, exactamente como `vulkan-loader`. ⚠ Y con una trampa nueva: ahora que `libva` está en el
644rootfs, **el mapa de `--dlopen` ya no la reporta**, porque mide presencia de fichero. La librería
645presente SILENCIA el aviso sin encender la función. Queda escrito acá porque el guardián ya no
646puede decirlo.
647
648**El bug real: AV1 no reproducía.** Gecko **se come el primer elemento de `LD_LIBRARY_PATH`** al
649lanzar el proceso RDD, el que decodifica vídeo. El lanzador ponía `/usr/lib/atuq` una sola vez —o
650sea, primero—, así que el RDD arrancaba sin él, no encontraba el ffvpx bundleado (donde vive dav1d)
651y anotaba `FFVPX: Link result: NoProvidedLib`. Y ahí **no falla**: se cae al ffmpeg del sistema, que
652cubre H.264/AAC/VP8/VP9/MP3/FLAC/Opus… pero **no AV1 por software**, porque el decodificador `av1`
653de ffmpeg es sólo hwaccel y nuestra receta va sin dav1d. Resultado: un vídeo AV1 se quedaba en
654`readyState=1` para siempre, sin un error, sin un NEEDED faltante y sin una cadena ausente.
655
656Tres corridas del MISMO artefacto que sólo cambian esa variable:
657
658 LD=/usr/lib/atuq:/usr/lib:/lib → RDD NoProvidedLib AV1 ✗
659 LD=/usr/lib/atuq:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓
660 LD=/relleno:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓ (el relleno se lo come)
661
662El arreglo es repetir el appdir en `recipes/atuq/bin/atuq`. Feo y correcto mientras no haya
663`patchelf` en el corpus para grabar `RUNPATH=$ORIGIN`, que borraría la variable entera.
664
665**Lo que quedó medido**, con `scripts/test-atuq-codecs.sh`, headless, por el lanzador de verdad y
666sin `LD_LIBRARY_PATH` de afuera:
667
668| Muestra | Veredicto |
669|---|---|
670| H.264 + AAC (mp4) | ✅ `t=3.00`, 75 cuadros |
671| VP9 + Opus (webm) | ✅ `t=3.00` |
672| AV1 (mp4) | ✅ `t=3.00` — sólo desde el arreglo del lanzador |
673| MP3 | ✅ |
674| FLAC | ✅ |
675
676El veredicto **no es «se creó el decodificador»**: es que `currentTime` AVANCE. La distinción no es
677retórica — en el caso de AV1 el decodificador se creaba (`RemoteDecoderChild … codec: av1`) y no
678entregaba un solo cuadro. El guardián corre con `--negative-control`, que saltea el lanzador y
679**exige que AV1 falle**: probado en los dos sentidos, porque uno que no puede fallar y uno que nunca
680falló se ven igual desde afuera.
681
682**Dos bugs de andamiaje que salieron de paso**, los dos en `atuq-nested.sh` y los dos escondidos
683detrás de una caché: el bucle de hidratación salía 1 siempre (línea vacía final + `set -e`) y mataba
684el script sin imprimir nada, y la comparación del sello nunca daba igual (un `\n` que `$(cat …)`
685recorta de un lado y no del otro), así que el rootfs se rehidrataba entero en cada corrida. El
686primero sólo se veía cuando cambiaban los hashes; lo destapó agregar `ffmpeg` a las raíces.
687
688### 2.ter Dónde vive la fuente de `atuq` — hizo falta un modo de `[source]` nuevo
689
690Escribir la receta destapó un muro que este documento no había visto: `[source]` era
691**obligatoriamente** git o tarball, así que una receta cuyo contenido no viene de upstream sino de
692nosotros no se podía ni expresar. Los rodeos eran todos peores — fetchear una fuente que después se
693ignora **miente** sobre la identidad del artefacto, y colgar el overlay de un repo aparte obliga al
694worker a tener acceso a un repo privado.
695
696Se agregó **`source.dir`** (commit `54fa4a9`): un árbol dentro del propio repo, hasheado por
697**contenido** con `ArtifactHash::of_tree` —que ya existía para verificar bit-reproducibilidad en
698Stage 2— y no por ruta. Es la misma disciplina que ya tenían los `patches`, que entran al hash por
699bytes. Editar un CSS del overlay mueve el `ArtifactHash`; renombrar el directorio, no.
700
701Un `.swm` **no** puede llevar una receta derivada, y los cuatro sitios que lo tocan fallan
702diciéndolo: el manifiesto es compartible y necesita un puntero resoluble desde fuera.
703
704**Hecho el 2026-09-05** (commits `aeebb19` y `6f6c32b`): `recipes/atuq.toml` + `recipes/atuq/`.
705La **v0.2** (`b3:7eff4ba4`) ya trae el branding: `application.ini` (Vendor/Name/RemotingName/
706CodeName, con el `ID` intacto para no salirse del ecosistema de complementos), `brand.ftl` dentro de
707`browser/omni.ja`, los binarios renombrados, iconos propios y `.desktop`. El re-empaque preserva
708orden, `STORED` y fechas del original, y se verificó contra el artefacto: **5306 entradas, mismo
709orden, y una sola con contenido distinto**. El `BuildID` se deriva del contenido del overlay —no de
710la fecha— porque es la clave con la que Gecko invalida su startup cache: sin eso, un cambio de
711chrome arranca con la interfaz vieja y *parece* que el overlay no agarró.
712
713La capa de configuración de la v0.1 son cuatro ficheros —autoconfig, `atuq.cfg`, `chrome/atuq.css`,
714`policies.json`— y el CSS se registra como `USER_SHEET` desde autoconfig porque `userChrome.css`
715vive en el perfil y exigiría que el usuario prenda una pref: atuq tiene que verse como atuq en el
716primer arranque. Falta construirlo: su dep `firefox` está en vuelo.
717
718### 2.quater Abrirlo en una pantalla: tres fallos que la clausura no podía ver
719
720El 2026-09-05, con `atuq` sellado y verificado, se hidrató y se abrió en el compositor de la sesión
721(waypipe). **No arrancó**, y los tres motivos son la diferencia entre «sellado» y «usable»:
722
7231. **EXDEV al hidratar.** `hydrate` proyecta con hardlinks y `linkat()` rechaza cruzar un punto de
724 montaje aunque los dos lados sean el mismo filesystem. Acá el store es `/dev/sdb` bind-monteado
725 y `work/` vive en `/dev/sdc`. El rootfs va bajo `/mnt/cosecha`, donde el volumen está montado
726 entero. Se comprueba con `findmnt -T`, **nunca** con `stat -c %d`.
7272. **El lanzador no puede ser un symlink.** Ni el binario ni sus `.so` traen `RPATH`/`RUNPATH`
728 (`readelf -d`), así que el motor no encontraba su propio `libnspr4.so` y moría en
729 `XPCOMGlueLoad`. Es un script con `LD_LIBRARY_PATH` + `exec`, como Debian y Fedora. La
730 alternativa limpia —`RPATH=$ORIGIN` con `patchelf`, que es lo que hace Alpine— espera a que
731 `patchelf` exista como receta del corpus.
7323. **Un bug del corpus, no de atuq: `atk` se había tragado GObject.** Declaraba la variante
733 ESTÁTICA de glib y produce un objeto compartido, así que `libatk-1.0.so` llevaba una copia
734 entera del sistema de tipos. Dos GObject en un proceso ⇒ 45 `GLib-GObject-CRITICAL` y
735 `Segmentation fault`. **El síntoma no nombra a atk**, y no se ve mirando el rootfs: había una
736 sola `libgobject`. Se cazó preguntando quién DEFINE el símbolo:
737 `nm -D --defined-only <cada .so> | grep " T g_type_register_static"` — deben salir uno, salieron
738 dos.
739
740Radio medido antes de tocar (`yupana radio atk`): 4 transitivos, 3 sellados a deuda; GNOME fuera
741porque usa gtk4. La cadena `atk → gtk3 → firefox → atuq` volvió a sellar 4/4, y **firefox tardó
742~50 minutos, no las cuatro horas** que repetía este documento — el número venía del folclore de la
743receta, no de una medición.
744
745**La lección para el plan: ninguna unidad de este frente está cerrada hasta que algo se ABRE en una
746pantalla.** La clausura decía 100% con un navegador que no llegaba a pintar un píxel.
747
748### 2.quinquies La v0.3 y los tres mecanismos: dos muertos, uno vivo
749
750La página de inicio y la pestaña nueva parecían un `pref` y resultaron ser un frente. Lo que se
751midió, en orden:
752
753| Mecanismo | Resultado |
754|---|---|
755| `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 |
756| `policies.json` → `ExtensionSettings` con `install_url` | **Se lee** (`browser.policies.applied=true` en el perfil) y falla nombrándose: `ERROR_SIGNEDSTATE_REQUIRED` |
757| `xpinstall.signatures.required = false` | **No alcanza** — la exigencia viene COMPILADA |
758
759**El instrumento que destrabó el diagnóstico fue un testigo.** `defaultPref` no deja rastro en
760`prefs.js` —sólo se guarda lo que difiere del default— así que un autoconfig que **no se ejecuta**
761es indistinguible de uno que sí y no hace nada. `atuq.cfg` escribe ahora `atuq.autoconfig.ok` como
762pref de **usuario**, legible desde fuera sin abrir el navegador. Salió `true` ⇒ el `.cfg` corría, y
763por lo tanto la pref de firma se estaba ignorando.
764
765De ahí `MOZ_REQUIRE_SIGNING` vacío en `recipes/firefox.toml`. Dos detalles que costaron cada uno su
766vuelta:
767
7681. El default sale del **milestone** (`milestone.is_release_or_beta`), no del canal de actualización.
769 El nuestro es `default` y la exigencia estaba activa igual.
7702. **Se desactiva con el valor VACÍO, no con cero.** `=0` muere con «takes 0 values»: es booleana y
771 el cero es un valor que no acepta. Leído del parser de mozbuild, no deducido.
772
773**El precio, escrito:** este Firefox deja de exigir la firma de Mozilla para cualquier complemento.
774La alternativa es firmar en AMO —cuenta y revisión de Mozilla por versión—, que es la dependencia
775externa que esta distro existe para no tener. Y no es un desvío: **sin esto, el `sct` del §6.1
776tampoco se podría shipear nunca**, así que la unidad 6 dependía de ésta sin que el plan lo dijera.
777
778**Método que conviene repetir:** la prueba del testigo se hizo **sin reconstruir**, montando el
779`.cfg` modificado con `--ro-bind` por encima del artefacto. Editar el fichero en el rootfs habría
780escrito sobre un **hardlink del store** y corrompido el artefacto sellado.
781
782### 2.sexies Lo que quedó PROBADO, y con qué
783
784Tres afirmaciones que este documento venía haciendo sin medir, y cómo se cerraron:
785
786| Afirmación | Prueba |
787|---|---|
788| El re-empaque del `omni.ja` es determinista | `why-differs` entre dos builds de `atuq`: **85 entradas idénticas, 0 divergen** |
789| La capa de configuración se aplica | testigo `atuq.autoconfig.ok` escrito como pref de **usuario** en el perfil |
790| 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` |
791| La base también reproduce | `why-differs` entre dos builds de `firefox` en el worker: **56 entradas idénticas, 0 divergen** |
792
793Y el contraste que salió de la primera: **el derivado reproducía y la base no.** El `BuildID` de
794`firefox` era la hora del build, así que dos construcciones con el mismo `ArtifactHash` daban bytes
795distintos — y `build-state.json` no lo podía ver, porque el hash es input-addressed y no se mueve
796por esto. Verde y mintiendo. Cerrado con `MOZ_BUILD_DATE` desde `SOURCE_DATE_EPOCH`, que es el
797mecanismo de hermeticidad que el sandbox ya tenía — y **comprobado como se comprueba esto**:
798apartando el artefacto como `.ref`, construyendo de nuevo y pasándole `why-differs`. Reproduce.
799
800**Dos guardianes nuevos en la fase `install` de `firefox`**, hermanos entre sí, porque los dos fallos
801son de la misma familia —una bandera que el `configure` acepta y el artefacto ignora, muda y a 50
802minutos de distancia—: uno comprueba `MOZ_REQUIRE_SIGNING: false` dentro del `omni.ja`, el otro que
803el `BuildID` sea el que `SOURCE_DATE_EPOCH` obliga.
804
805**La regla que sale de todo el día:** cuando la duda es «¿llegó al artefacto?», la respuesta no está
806en el log del build ni en la receta — está **dentro del artefacto**, y hay que ir a buscarla ahí.
807
808## 7. La costura: un host de native messaging en Rust
809
810Todo lo del §6 que no es CSS pasa por **un solo mecanismo**: un proceso Rust que habla native
811messaging con la extensión de `atuq` y, del otro lado, con la suite (`agora` para identidad Ed25519,
812`khipu`, el store, `foreign-*`, `cortafuegos`).
813
814Que sea **uno** y no siete es la decisión: cada feature del §6 pasa a ser un verbo de ese host, no un
815proyecto nuevo. El binario vive en tawasuyu (es suite, no build system); `atuq` sólo lo declara como
816dep y le deja el manifiesto de native messaging en su sitio. **El nombre del crate se decide en
817tawasuyu y con su regla 10 puesta** — acá no se bautiza nada de allá.
818
819## 8. Plan, por unidades de trabajo
820
821Cada una cierra sola, se commitea y se pushea. El orden no es preferencia: cada una destraba a la
822siguiente.
823
824| # | Unidad | Puerta que abre | Bloqueada por |
825|---|---|---|---|
826| 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 | — |
827| 2 | ~~`llvm-toolchain`~~ → **`apk add lld` + `compiler="clang"`** ✅ 2026-09-05 | LTO **y** el muro 3 | — |
828| 3 | Construir el `firefox` con LTO (hash `b3:6f2a3b2f`) | el eje de velocidad **y** las extensiones de `atuq` | 2 ✅ |
829| 3.a | **PGO ENCENDIDO** ✅ 2026-09-07 — perfil de 501.521 funciones recogido bajo headless, publicado en el mirror y pineado por sha256; 189 compilaciones con `-fprofile-use`. Sin `jarlog` todavía | la otra mitad de la ganancia | 3 ✅ |
830| 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 ✅ |
831| 4 | **`atuq`: receta derivada + overlay de chrome** ✅ v0.1 escrita | el andamio de todo lo demás | 3 |
832| 4.b | Branding + re-empaque de `omni.ja` ✅ v0.2 | el chrome de verdad | — |
833| 4.d | **Abrirlo en pantalla** — destapó EXDEV, el lanzador, el `atk` envenenado y la cadena GTK3 estática | «usable», no sólo «sellado» | — |
834| 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 |
835| 4.f | **Los códecs, medidos** ✅ 2026-09-07 — H.264+AAC, VP9+Opus, AV1, MP3 y FLAC reproducen; arreglado el `LD_LIBRARY_PATH` que dejaba al RDD sin ffvpx (AV1 muerto en silencio) y nacido `scripts/test-atuq-codecs.sh` con control negativo | «se ve el vídeo», que ningún auditor de ELF podía responder | — |
836| 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 | — |
837| 5 | Host de native messaging (tawasuyu) | los verbos del §6 | 4 |
838| 6 | `sct` v1 (extensión + testigo) | el diferenciador que nadie tiene | 5 |
839| 7 | Descargas al CAS | 6.2, y alimenta 6.9 | 5 |
840| 8 | Archivo + RAG | 6.3 | 5, 7 |
841| 9 | Torrent, medios, foco | 6.5–6.7, 6.9 | 5 |
842| 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 | — |
843
844Las unidades 2 y 4 son **paralelizables**: la toolchain no toca el chrome y el chrome no toca la
845toolchain. Si hay dos frentes, van juntas.
846
847## 9. Por qué esto no abandona a `puriy`
848
849`puriy` es el motor DOM/CSS propio con JS real (QuickJS-NG sobre `wasmi`) cuyo límite declarado no es
850el lenguaje sino **el wiring nativo**: DOM bindings completos, red, render. Eso no se acelera
851escribiendo otro navegador; se acelera con tiempo.
852
853`atuq` no le compite porque no comparte una sola línea con él, y **le sirve** porque todo lo del §6
854vive del lado Rust: el host del §7, el testigo de `sct`, las descargas al CAS, el archivo con RAG.
855Son **agnósticos del motor**. Cuando `puriy` madure, se enchufan del mismo lado sin reescribirse.
856
857Dicho al revés, que es como conviene recordarlo: **`atuq` es el navegador que se puede usar mientras
858`puriy` crece, y el andamio de las features que `puriy` va a heredar.** Si dentro de dos años `atuq`
859se apaga, lo que se tira es una receta derivada y una hoja de CSS; todo lo demás sobrevive.
26-atuq-envoltorio-gecko.md
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
ArtifactHash de firefox. El corpus no se invalida; waterfox y 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.ja es 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
jarlog del PGO ordena el omni.ja para 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
.purgecaches junto 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=cross quiere clang + lld. El LTO de GCC sobre Gecko no está soportado upstream.
--enable-profile-use espera -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.toml compila ninja -C build libclang.so clang-resource-headers y su fase
install copia sólo build/lib/libclang.so* y las cabeceras clang-c. Está ahí porque
bindgen carga libclang.so en runtime. No hay driver clang, ni lld, ni libc++.
llvm18 se 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/alpine ya trae clang22 + llvm22 22.1.8 — la misma major que usa el APKBUILD de
Alpine para este mismo Firefox (_llvmver=22).
Compiler::Clang ya existe en hammer, cableado de punta a punta (parse_compiler lo acepta;
hammer-build pone CC=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, o atuq deja de ser
determinista y no lo vamos a notar: sería otra forma del lab que no está en hash_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-rt en el corpus: receta propia, no un flag.
--with-unsigned-addon-scopes=app,system nos falta y atuq lo NECESITA para shipear sus
propias extensiones desde distribution/extensions/ (§6.1, §7). Es un flag de configure ⇒
una capacidad de atuq que 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.quinquies El PGO, y los tres muros que el plan no tenía escritos (2026-09-07)
El §3.ter daba dos muros: el display (X11 que no tenemos) y el no-determinismo del perfil. Los dos
eran ciertos. Aparecieron tres más, y ninguno se ve sin construir:
-
libclang_rt.profile.a no existía. El lab trae clang 22.1.8 pero ninguna runtime de
compiler-rt — su lib/ del resource dir ni existe, exactamente como ya documentaba
wasi-compiler-rt para el caso wasm. El build instrumentado murió en el minuto 38:56 con
ld.lld: cannot open …/libclang_rt.profile.a. De ahí sale recipes/compiler-rt-profile.toml.
-
Los sandboxes de Firefox matan la corrida de perfilado. Con ellos puestos los procesos hijo
mueren con signal 11, el que renderiza no llega a nacer, el servidor local registra 0 GET
y el único .profraw que queda es el del padre arrancando — un perfil de nada, con todo en
verde. Es lo mismo que hace el profileserver.py de Mozilla. Sólo aplica a la corrida de
entrenamiento: el firefox que se distribuye lleva sus sandboxes intactos.
-
El worker no puede bajar del mirror de fuentes. Tiene scripts/fuentes/mirror-env.sh desde
el 2026-09-01 pero no la clave del Storage Box, así que una receta pineada al mirror —y el perfil
es la primera— no se construye allá. Se sembró el artefacto sellado, que es content-addressed:
consigue lo mismo sin mover una credencial de máquina.
Cómo se verificó que el PGO llegó, y por qué NO como RLBox. Con RLBox el artefacto delata la
jaula (símbolos w2c_*, cero → 634). Acá el discriminante análogo serían las secciones
.text.hot/.text.unlikely que clang emite al particionar por temperatura — y no sirve: con
lld y ThinLTO el enlazador las fusiona, así que dan cero en el binario con PGO y sin él. La
evidencia que sí vale está un nivel por debajo del configure: 189 invocaciones del compilador
llevan -fprofile-use, el configure encontró llvm-profdata, y no hay ni un aviso de perfil que
no cuadre. libxul.so pasa de 226.078.752 a 227.802.816 bytes, consistente con inlining de caminos
calientes. Es evidencia de build, no de artefacto, y conviene decirlo así.
LA GANANCIA, MEDIDA (2026-09-07). Mismo rootfs, mismo script, corridas INTERCALADAS A/B/A/B
para que la deriva de carga afecte a las dos, mediana de 7, calentamiento descartado. Lo único que
cambia entre variantes es un --ro-bind de /usr/lib/firefox.
| página | sin PGO | con PGO | |
| DOM+layout+strings, fuera del corpus de entrenamiento | 23.540 ms | 21.327 ms | −9,4 % |
SunSpider 3d-raytrace, dentro del corpus | 16.034 ms | 15.878 ms | −1,0 % |
Los rangos no se solapan en ninguna de las dos (23.185–23.654 vs 21.030–21.650), así que las dos
diferencias son reales y no ruido.
⚠ Y el resultado sale al revés de lo esperable, que es lo interesante. La intuición dice que
medir sobre el conjunto de ENTRENAMIENTO infla la ganancia; acá el corpus da el número MÁS BAJO. La
razón es que 3d-raytrace es aritmética pura en un bucle caliente, y ese bucle no lo ejecuta el
C++ de SpiderMonkey: lo ejecuta código máquina que el JIT genera en tiempo de ejecución. El PGO
optimiza el intérprete, el GC y el propio compilador JIT — no el código que el JIT emite. Un
benchmark JIT-bound es casi ciego al PGO por construcción.
Donde sí se ve es en el camino DOM/layout/arranque, que es C++ de principio a fin. Y eso es también
lo que un usuario percibe: la medición cubre el ciclo COMPLETO del proceso (arrancar → renderizar →
capturar → salir), no el rendimiento en régimen de una página ya cargada. De ahí que hasta la página
de SunSpider tarde 16 s: casi todo es arranque.
Corolario para el corpus de entrenamiento: está sesgado hacia JS (SunSpider domina en número de
páginas) justo donde el PGO menos rinde. Un corpus con más maquetación y menos aritmética
probablemente daría más ganancia. Es una unidad de trabajo propia y no se hace de paso.
Y la pregunta que un insumo no determinista obliga a hacer, respondida: firefox SIGUE
REPRODUCIENDO bit a bit (verificar-repro.sh: REPRODUCEN 1, DERIVA 0, NO-DETERMINISMO 0). Era el
riesgo real de esta unidad, no el rendimiento: si el -fprofile-use hubiera metido cualquier
decisión dependiente del orden o del timing, habríamos cambiado velocidad por el invariante del
proyecto. Congelar el perfil como fuente pineada era el argumento; esto es la medición.
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-symbols falla 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__ 1 y nada más. Se
reconcilia el snapshot; no se salta con make 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. El wasi-sdk de Alpine
declara wasi-libc wasi-libcxx wasi-compiler-rt —TRES— y la cadena tenía dos. El depends
del paquete ajeno equivalente es una comprobación de completitud gratis, antes de gastar CPU.
-
Un directorio VACÍO que sí importa. Con wasi-libcxx sellado el configure volvió a morir
igual, y cstring estaba en el artefacto: clang no lo buscaba. Falta el mkdir -p include/c++/v1 que Alpine tiene en su package() 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 del CLAUDE.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
webRequest bloqueante 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.
proxyDNS viene 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 |
| Una petición hecha en «Banco» SALE por el proxy de «Banco» | scripts/test-atuq-ruteo.py: dos pestañas piden la misma url; al destino llega GET /directo y al puerto del proxy un saludo SOCKS5 (\x05\x01\x00), y la de «Banco» no aparece nunca en el destino. Con --negative-control (sin proxy configurado) las dos van directas y al proxy no llama nadie |
| 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.
Cómo se cerró lo que faltaba (2026-09-06). Durante unas horas esta sección decía que probar el
paquete quedaba pendiente, con tres puertas cerradas. Las tres siguen cerradas y la prueba existe
igual, porque el camino era otro.
Las tres puertas, que conviene NO volver a golpear:
- Desde la línea de comandos no hay flag para el
userContextId de la pestaña inicial. Y un
moz-extension:// tampoco se abre así: el manejador intenta resolverlo como fichero y muere con
NS_NOINTERFACE [nsIFileURL.file], abriendo la home en su lugar.
- Marionette está en el artefacto y responde —se le habló con un cliente propio de 60 líneas,
el protocolo es
<longitud>:<json>—, pero abrir una pestaña de contenedor pide contexto chrome,
y eso exige -remote-allow-system-access; 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. Cabo suelto medido, no suposición.
- Desde la extensión,
tabs.create({cookieStoreId}) exige el permiso cookies
(ext-tabs-base.js:getUserContextIdForCookieStoreId). Dárselo a la extensión del proxy para que
pueda probarse a sí misma sería pagar con la superficie de ataque del producto una comodidad del
test. Sigue sin dárselo.
La cuarta puerta estaba abierta: el fichero de sesión. sessionstore guarda el userContextId
de cada pestaña y Gecko lo restaura, así que se fabrica a mano. El formato es mozLz40\0 + tamaño +
un bloque LZ4, y un bloque de sólo literales es LZ4 válido: veinte líneas, sin depender de
ninguna librería. Con eso se abren dos pestañas —misma url, distinto contenedor— sin pedirle nada al
navegador.
Y la medición no le pregunta nada tampoco: se le ponen dos oídos en la red y se mira a cuál llama.
El destino contesta un HTTP 200; el «proxy» no habla SOCKS, sólo anota quién lo saludó. Que al
puerto del proxy llegue un \x05\x01\x00 es la prueba: 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 nuestro
proxy.onRequest mirando el cookieStoreId.
Tres detalles sin los cuales la prueba mide un silencio y se lee como un fallo:
browser.sessionstore.restore_on_demand=false (si no, las pestañas restauradas no piden nada hasta
que alguien las mira), network.proxy.allow_hijacking_localhost=true (Gecko saltea el proxy para
localhost, y la petición de «Banco» iría directa haciendo parecer culpable a la extensión), y
triggeringPrincipal_base64: vQ== en cada entrada de sesión (sin principal la pestaña se restaura
pero no navega).
Y la prueba trae su propio control negativo, porque es lo que este repo se exige desde hoy: con
--negative-control no se configura el proxy y se exige el resultado CONTRARIO —las dos pestañas
directas, nadie llamando al proxy—. 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.
6.10 Lo que atuq NO PUEDE hacer hoy, y por qué (2026-09-07)
Sale de un punto ciego que nombramos entre los dos frentes y que ningún auditor de readelf
puede cubrir. La jerarquía de lo que se ve:
NEEDED del ejecutable → lo ve `test-atuq-rootfs.py` y lo ve `vigia-sonames.py`
NEEDED de un .so dlopeado → lo ven los dos, si el fichero está en el artefacto
dlopen("libfoo.so.1") LITERAL → NO LO VE NINGUNO
Y un navegador vive del tercer escalón: Firefox sondea ffmpeg, VA-API, vulkan, libnotify y una
docena más 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 misma forma que ya nos costó caro con
OBS (dlopen("libGL.so.1") desde su glad).
scripts/test-atuq-rootfs.py --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 que sí se puede afirmar 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 con forma de soname, 47 no tenían quién las provea. Triadas (y con dos filas
corregidas el mismo día — ver §6.11, que las midió en vez de deducirlas; hoy el mapa da 7 huecos):
| Qué se pierde | Estado |
| Códecs del sistema (H.264/AAC) | ❌ ERA FALSO — recipes/ffmpeg.toml existe, está sellada y ya viajaba en los cuatro escritorios por mpv. Corregido en §6.11: reproduce |
Notificaciones web (libnotify) | ✅ CERRADO el 2026-09-07: recipes/libnotify.toml, 0.8.8, compartida. Es el primer hueco que este mapa encontró Y cerró |
Llavero (libsecret) | hueco — pero la receta ya existe en incoming-gnome |
Sonidos del sistema (libcanberra) | hueco — receta en incoming-kde |
WebGPU / vulkan (libvulkan) | hueco — receta vulkan-loader en incoming-kde |
| Vídeo por hardware (VA-API) | hueco, pero no por falta de receta: libva está sellada y en las cuatro imágenes. Falta el driver — las tres mesa van con -Dgallium-va=disabled y -Dvideo-codecs= vacío ⇒ ni un *_drv_video.so |
Lectura en voz alta (libspeechd) | hueco — sin receta |
libGL | decisión: la distro es Wayland-only sin GLX y Firefox va por EGL |
libcurl | decisión: sólo lo usa pingsender, que manda telemetría — apagada |
| Kerberos, menú global, ML | fuera de alcance |
libdl/libpthread/librt/libudev.so.0/libfreebl3 | ruido: musl los funde, o el soname es viejo |
El primero ya está cerrado. libnotify no tenía receta y ahora la tiene: es la prueba de que el
mapa sirve para algo más que mirarlo. ⚠ Con una advertencia que va escrita en la propia receta:
libnotify no trae un daemon, manda org.freedesktop.Notifications por D-Bus ⇒ tenerla resuelve la
mitad —que firefox la encuentre— y la otra mitad es que en la imagen haya alguien escuchando ese
nombre. Nadie debería leer «notificaciones arregladas» y esperar un globo en un QEMU pelado.
⚠ CORRECCIÓN, mismo día: escribí que tres de los siete eran «baratos, promoción y no autoría»
—libsecret, libcanberra, vulkan-loader— y es FALSO. Lo comprobé antes de declararlos en los
perfiles, y en los tres casos falta la otra mitad:
| «barata» | por qué NO alcanza con declararla |
vulkan-loader | las tres recetas de mesa construyen con -Dvulkan-drivers= VACÍO ⇒ el loader no encontraría un solo dispositivo. Un cargador sin ICD no es WebGPU |
libsecret | no hay receta de gnome-keyring ni de ningún servicio de secretos: firefox hablaría a org.freedesktop.secrets y no habría nadie |
libcanberra | no hay receta de sound-theme-freedesktop: la librería carga y no suena nada |
Declararlas habría subido el número de paquetes de la imagen sin encender una sola función. Una
librería es media función; la otra mitad es quien la atiende, y eso vale para todas las de esta
tabla. Quedan como huecos, no como promociones pendientes.
⚠ Y esta frase también se cayó el mismo día. Decía que de los otros cuatro «el más caro para el
usuario es ffmpeg: sin él, un sitio que sirva H.264 no reproduce» — con lo cual el hueco más caro
del mapa no existía. Ver §6.11: la receta estaba, faltaba declararla en el runner, y H.264 (y
AAC, VP9, Opus, AV1, MP3, FLAC) reproducen. De los cuatro «sin receta» quedan tres, y ninguno es un
códec: lectura en voz alta (libspeechd), el driver de VA-API, y libpci —que sólo lo usa
glxtest, o sea irrelevante sin GLX.
Y la misma vara aplicada a libnotify, que sí se declaró: su otra mitad es un daemon que
escuche org.freedesktop.Notifications. Medido perfil por perfil:
escritorio-kde plasma-workspace ✓ completa
escritorio-gnome gnome-shell ✓ completa
escritorio-cosmic cosmic-notifications ✓ completa
escritorio-sway dunst 1.12.2 ✓ completa — CERRADA el 2026-09-07
La de sway se cerró el mismo día con recipes/incoming-wlr/dunst.toml, y la trampa del homónimo
—el nombre mako YA ESTÁ OCUPADO en el corpus por el motor de plantillas de Python de Mesa— se
esquivó eligiendo dunst, que además habla D-Bus por GDBus y no por sd-bus, o sea sin receta nueva
de basu. La prueba no es que selle: scripts/wlr/dunst-headless.sh levanta sway headless y un bus
de sesión, manda un notify-send y deja que el bus active el daemon solo —el mismo camino que
recorre una página web en atuq—, y mide un diff de píxeles antes/después: 14.832 cambiados con el
.service puesto, 0 sin él. Evidencia en docs/evidencia/dunst-sway-notificacion-2026-09-07.png.
Y la cadena se cerró hasta el final, que es donde vive la promesa: con --via-atuq el emisor no es
notify-send sino una página web dentro del navegador. Ahí se ejercitan las cinco piezas —
new Notification(...) → libxul haciendo dlopen("libnotify.so.4") (que no es NEEDED de ningún
ELF, o sea invisible para cualquier auditor) → D-Bus → activación → dunst dibujando—, y el veredicto
es el onshow del motor, no el diff de píxeles: con el .service puesto llega a MOSTRADA #33, sin
él el motor dice ERROR al mostrar y no se dibuja nada. Evidencia en
docs/evidencia/atuq-notificacion-web-sway-2026-09-07.png.
Vale la pena decir qué prueba eso de más: que en el proceso de atuq la GLib no se duplica. El
navegador usa la cadena -shared (la que arrastra su GTK3) y libnotify.so.4 enlaza esa misma, así
que el dlopen no repite GObject. El que sí lo repetía era dunstify, y por eso no se shipea.
⚠ De paso apareció otra media función, y ésta toca a libnotify directamente: dunstify
segfaultea hasta en --help porque mezcla la GLib ESTÁTICA del corpus con la COMPARTIDA que
arrastra libnotify.so.4 — dos copias de GObject en un proceso. No se shipea; el emisor es
notify-send, que viene dentro del artefacto de libnotify y enlaza toda la cadena compartida. Vale
como recordatorio de que en este corpus quién enlaza qué GLib es parte del contrato, no un
detalle del build.
Lo que esta sección cambia de fondo: hasta hoy la pregunta era «¿arranca?», y la respuesta era
sí. La pregunta 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.
6.11 Y ahora la pregunta que faltaba: ¿REPRODUCE? (2026-09-07)
El §6.10 preguntó «¿qué NO puede hacer?» y contestó leyendo cadenas. Media pregunta. La otra mitad
—¿se ve el vídeo?— no la contesta ningún auditor de ELF, y al hacerla aparecieron dos errores
del mapa y un bug real del navegador.
Error 1: «no hay receta de ffmpeg». La hay. recipes/ffmpeg.toml, 7.1, sellada, publica
libavcodec.so.61 —uno de los once sonames que libxul.so sondea por dlopen literal— y ya
viajaba en la clausura de los cuatro escritorios, arrastrada por mpv. Comprobado con
yupana.membresia, que es la vista honesta por par (cola, nombre), no leyendo el TOML:
escritorio-kde 308 nodos · escritorio-gnome 192 · escritorio-sway 203 · escritorio-cosmic 163
ffmpeg ✓ en los cuatro · libva ✓ en los cuatro
Lo que faltaba no era la receta: era la lista de raíces de scripts/atuq-nested.sh, escrita a
mano, que hidrataba un rootfs más pobre que la imagen. Otra vez el instrumento y no el artefacto —
la misma forma que gcc-libs el día anterior. Y la comprobación previa que sí se pudo hacer sin
correr nada: de los 82 símbolos con forma av*_ que libxul dlsimboliza, los 9 que nuestro
libavcodec no exporta son los de la API vieja (avcodec_decode_video2, av_lockmgr_register,
avcodec_register_all…), que Firefox sólo pide para las versiones 53-57.
Error 2: «VA-API, sin receta». También la hay, y también está en las cuatro imágenes. El hueco
es real pero por la OTRA mitad: las tres recetas de mesa construyen con -Dgallium-va=disabled y
-Dvideo-codecs= vacío ⇒ no existe un solo *_drv_video.so. Un cargador sin ICD no acelera
nada, exactamente como vulkan-loader. ⚠ Y con una trampa nueva: ahora que libva está en el
rootfs, el mapa de --dlopen ya no la reporta, porque mide presencia de fichero. La librería
presente SILENCIA el aviso sin encender la función. Queda escrito acá porque el guardián ya no
puede decirlo.
El bug real: AV1 no reproducía. 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 el ffvpx bundleado (donde vive dav1d)
y anotaba FFVPX: Link result: NoProvidedLib. Y ahí no falla: se cae al ffmpeg del sistema, que
cubre H.264/AAC/VP8/VP9/MP3/FLAC/Opus… pero no AV1 por software, porque el decodificador av1
de ffmpeg es sólo hwaccel y nuestra receta va sin dav1d. Resultado: un vídeo AV1 se quedaba en
readyState=1 para siempre, sin un error, sin un NEEDED faltante y sin una cadena ausente.
Tres corridas del MISMO artefacto que sólo cambian esa variable:
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 relleno se lo come)
El arreglo es repetir el appdir en recipes/atuq/bin/atuq. Feo y correcto mientras no haya
patchelf en el corpus para grabar RUNPATH=$ORIGIN, que borraría la variable entera.
Lo que quedó medido, con scripts/test-atuq-codecs.sh, headless, por el lanzador de verdad y
sin LD_LIBRARY_PATH de afuera:
| Muestra | Veredicto |
| H.264 + AAC (mp4) | ✅ t=3.00, 75 cuadros |
| VP9 + Opus (webm) | ✅ t=3.00 |
| AV1 (mp4) | ✅ t=3.00 — sólo desde el arreglo del lanzador |
| MP3 | ✅ |
| FLAC | ✅ |
El veredicto no es «se creó el decodificador»: es que currentTime AVANCE. La distinción no es
retórica — en el caso de AV1 el decodificador se creaba (RemoteDecoderChild … codec: av1) y no
entregaba un solo cuadro. El guardián corre con --negative-control, que saltea el lanzador y
exige que AV1 falle: probado en los dos sentidos, porque uno que no puede fallar y uno que nunca
falló se ven igual desde afuera.
Dos bugs de andamiaje que salieron de paso, los dos en atuq-nested.sh y los dos escondidos
detrás de una caché: el bucle de hidratación salía 1 siempre (línea vacía final + set -e) y mataba
el script sin imprimir nada, y la comparación del sello nunca daba igual (un \n que $(cat …)
recorta de un lado y no del otro), así que el rootfs se rehidrataba entero en cada corrida. El
primero sólo se veía cuando cambiaban los hashes; lo destapó agregar ffmpeg a las raíces.
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.
hydrate proyecta con hardlinks y linkat() rechaza cruzar un punto de
montaje aunque los dos lados sean el mismo filesystem. Acá el store es /dev/sdb bind-monteado
y work/ vive en /dev/sdc. El rootfs va bajo /mnt/cosecha, donde el volumen está montado
entero. Se comprueba con findmnt -T, nunca con stat -c %d.
- El lanzador no puede ser un symlink. Ni el binario ni sus
.so traen RPATH/RUNPATH
(readelf -d), así que el motor no encontraba su propio libnspr4.so y moría en
XPCOMGlueLoad. Es un script con LD_LIBRARY_PATH + exec, como Debian y Fedora. La
alternativa limpia —RPATH=$ORIGIN con patchelf, que es lo que hace Alpine— espera a que
patchelf exista como receta del corpus.
- Un bug del corpus, no de atuq:
atk se había tragado GObject. Declaraba la variante
ESTÁTICA de glib y produce un objeto compartido, así que libatk-1.0.so llevaba una copia
entera del sistema de tipos. Dos GObject en un proceso ⇒ 45 GLib-GObject-CRITICAL y
Segmentation fault. El síntoma no nombra a atk, y no se ve mirando el rootfs: había una
sola libgobject. 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 es default y la exigencia estaba activa igual.
- Se desactiva con el valor VACÍO, no con cero.
=0 muere 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-toolchain → apk 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 ENCENDIDO ✅ 2026-09-07 — perfil de 501.521 funciones recogido bajo headless, publicado en el mirror y pineado por sha256; 189 compilaciones con -fprofile-use. Sin jarlog todavía | 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.f | Los códecs, medidos ✅ 2026-09-07 — H.264+AAC, VP9+Opus, AV1, MP3 y FLAC reproducen; arreglado el LD_LIBRARY_PATH que dejaba al RDD sin ffvpx (AV1 muerto en silencio) y nacido scripts/test-atuq-codecs.sh con control negativo | «se ve el vídeo», que ningún auditor de ELF podía responder | — |
| 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.