# 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:** 1. **Iteración en segundos, no en horas.** El chrome se toca sin volver a compilar C++. 2. **No mueve el `ArtifactHash` de `firefox`.** El corpus no se invalida; `waterfox` y cualquier otro fork siguen compartiendo el mismo artefacto base. 3. **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:** 1. **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. 2. **`--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.** 3. **Las librerías del sistema son diferencia a propósito, no deuda.** Alpine usa las suyas; nuestro corpus *es* la fuente y bundlear es lo que mantiene la clausura cerrada. **Corolario que ordena el plan: todo esto entra en UN SOLO rebuild de `firefox`.** Cada rebuild son cuatro horas y re-sella la cola Gecko entera (waterfox incluido), así que LTO, PGO, RELR, lld, RLBox y el scope de addons se juntan en una sola pasada, no en seis. ### 3.ter Los dos muros del PGO que las distros no tienen **El perfil se junta corriendo el navegador, y las dos distros lo hacen bajo `xvfb-run`** (Alpine: `xvfb-run -a -s "-screen 0 1920x1080x24" ./mach python build/pgo/profileserver.py`; Arch, idéntico). **Nosotros no tenemos X11 en el corpus** —es la decisión Wayland-only de toda la distro— así que ese camino no existe acá. La salida hammer-nativa es correrlo bajo un **sway headless** (`WLR_BACKENDS=headless`), que ya tenemos del frente wlr/sway y arranca en segundos. **✅ EL MURO DEL DISPLAY ESTÁ DESPEJADO (2026-09-06).** No es una previsión: se corrió. `sway` headless con `WLR_BACKENDS=headless` y `WLR_RENDERER=pixman` arranca en **gioser**, que es un LXC **sin `/dev/dri`** —o sea sin GPU ninguna, todo software— y dibuja píxeles de verdad: `docs/evidencia/sway-headless-gioser-sin-gpu-2026-09-06.png` (sway 1.10 / wlroots 0.18.2 / foot 1.27.0, los tres binarios nuestros, salida `HEADLESS-1` a 1280x720). El ciclo entero es de ~2 min con `scripts/wlr/sway-headless.sh`. Dos cosas que hubo que resolver y conviene tener escritas antes de repetirlo: · **Hidratar bajo el montaje del store.** `hydrate-profile.py` ENLAZA, y un hardlink no cruza montajes: con `--into work/...` sale `EXDEV` porque el store vive en otro disco. Con `--into store/.rootfs/sway` son 193/193 nodos, 41.677 ficheros y **4,9 G que no ocupan** (son hardlinks al store). · **Inyectar el loader de musl.** `sway` salió DINÁMICO y pide `/lib/ld-musl-x86_64.so.1`, que el cierre no trae; el error es `exec: /usr/bin/sway: not found`, que se lee como «falta el binario» y lo que falta es su intérprete. Se resuelve con `--ro-bind /usr/lib/musl/lib/libc.so /lib/ld-musl-x86_64.so.1` (en musl el `libc.so` ES el loader). Es la misma familia que el resto de la noche: el mensaje nombra lo que buscó, no lo que falta. **Y el perfil no es determinista.** Los contadores dependen del timing de la corrida, así que dos generaciones del profdata no dan los mismos bytes — a las distros no les importa porque no persiguen bit-repro; a nosotros nos rompe el invariante. La salida: **generar el perfil UNA vez y sellarlo como artefacto propio, consumido por hash** desde `firefox`. El build optimizado vuelve a ser determinista aunque su insumo no lo sea, y el perfil se regenera a propósito, no por accidente. ### 3.quater La cadena wasm, y las tres cosas que costó (2026-09-06) `--without-wasm-sandboxed-libraries` se cambió por `--with-wasi-sysroot=/usr/share/wasi-sysroot`, que es literalmente el flag de Alpine. Detrás hay cinco recetas nuestras, en este orden obligatorio: ``` wasi-libc-headers → wasi-compiler-rt → wasi-libc → wasi-libcxx → wasi-sdk → firefox ``` `wasi-libc-headers` existe **sólo** para romper el ciclo del bootstrap y pinea un commit VIEJO a propósito (triple `wasm32-wasi`), mientras `wasi-libc` pinea el nuevo (`wasm32-wasip1`, el que clang 22.1 usa de verdad). Es lo mismo que hace Alpine y no es un descuido. **Lo que costó, que es la parte reusable:** 1. **`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. 2. **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: `` 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. 3. **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:** 1. **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. 2. **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. 3. **`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: 1. **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. 2. **Marionette está en el artefacto y responde** —se le habló con un cliente propio de 60 líneas, el protocolo es `:`—, 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. 3. **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 tienen quién las provea**. Triadas: | Qué se pierde | Estado | |---|---| | **Códecs del sistema (H.264/AAC)** | hueco — no hay receta de `ffmpeg`. Lo bundleado cubre VP9/AV1/Opus, no H.264 | | **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 — sin receta | | **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. Los otros cuatro piden receta nueva, y el más caro para el usuario es `ffmpeg`: sin él, un sitio que sirva H.264 no reproduce. **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 — ✗ MEDIA: nadie atiende ⚠ Y una trampa para quien vaya a cerrar la de sway: **el nombre `mako` YA ESTÁ OCUPADO en el corpus** por el motor de plantillas de Python que usa Mesa para su codegen. No es el daemon de notificaciones de wlroots. Escribir `recipes/mako.toml` para el daemon pisaría una receta viva de la cadena de mesa — es exactamente la colisión de homónimos que la memoria del proyecto ya tiene anotada. El daemon habrá que nombrarlo de otro modo o meterlo en `incoming-wlr`. **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. ### 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»: 1. **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`. 2. **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. 3. **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 | 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: 1. 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. 2. **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: perfil bajo sway headless, **sellado como artefacto propio** y consumido por hash | la otra mitad de la ganancia | 3 | | 3.b | **RLBox ENCENDIDO** ✅ 2026-09-06 — cadena wasm de CINCO recetas selladas + guardián que la exige en el binario; firefox sellado **y REPRODUCE bit a bit** (`verificar-repro.sh`: 0 no-determinismos) | cierra el hueco de seguridad del §3.bis | 2 ✅ | | 4 | **`atuq`: receta derivada + overlay de chrome** ✅ v0.1 escrita | el andamio de todo lo demás | 3 | | 4.b | Branding + re-empaque de `omni.ja` ✅ v0.2 | el chrome de verdad | — | | 4.d | **Abrirlo en pantalla** — destapó EXDEV, el lanzador, el `atk` envenenado y la cadena GTK3 estática | «usable», no sólo «sellado» | — | | 4.e | **v0.3: página de inicio y pestaña nueva** por extensión de sistema; exigió `MOZ_REQUIRE_SIGNING` vacío | y con eso queda destrabada la unidad 6 (`sct`) | rebuild de firefox | | 4.c | `MOZ_BUILD_DATE` determinista ✅ — `BuildID=19700101000001`, con guardián en install y **`why-differs` 56/56 sobre dos builds reales** | el invariante de reproducibilidad | — | | 5 | Host de native messaging (tawasuyu) | los verbos del §6 | 4 | | 6 | `sct` v1 (extensión + testigo) | el diferenciador que nadie tiene | 5 | | 7 | Descargas al CAS | 6.2, y alimenta 6.9 | 5 | | 8 | Archivo + RAG | 6.3 | 5, 7 | | 9 | Torrent, medios, foco | 6.5–6.7, 6.9 | 5 | | 9.a | **Proxy por contenedor ✅ v0.5** — contenedores por política + extensión con `proxy.onRequest` | 6.8, y NO dependía de 5: es API de Firefox | — | Las unidades 2 y 4 son **paralelizables**: la toolchain no toca el chrome y el chrome no toca la toolchain. Si hay dos frentes, van juntas. ## 9. Por qué esto no abandona a `puriy` `puriy` es el motor DOM/CSS propio con JS real (QuickJS-NG sobre `wasmi`) cuyo límite declarado no es el lenguaje sino **el wiring nativo**: DOM bindings completos, red, render. Eso no se acelera escribiendo otro navegador; se acelera con tiempo. `atuq` no le compite porque no comparte una sola línea con él, y **le sirve** porque todo lo del §6 vive del lado Rust: el host del §7, el testigo de `sct`, las descargas al CAS, el archivo con RAG. Son **agnósticos del motor**. Cuando `puriy` madure, se enchufan del mismo lado sin reescribirse. Dicho al revés, que es como conviene recordarlo: **`atuq` es el navegador que se puede usar mientras `puriy` crece, y el andamio de las features que `puriy` va a heredar.** Si dentro de dos años `atuq` se apaga, lo que se tira es una receta derivada y una hoja de CSS; todo lo demás sobrevive.