Es el único de la lista del SDD 26 §6 que no pasa por el host de native
messaging del §7: es API de Firefox y nada más. Cada contenedor —Personal,
Trabajo, Banco, Compras— puede salir por su propio proxy.
Tres piezas y ninguna alcanza sola: `atuq.cfg` prende los contenedores (vienen
apagados), `policies.json` los CREA con `Containers.Default` —el único
mecanismo que los pone en un perfil NUEVO— y deja la config de fábrica en
`3rdparty.Extensions`, y `extensions/proxy/` los enruta con `proxy.onRequest`,
que es lo único que ve el `cookieStoreId` de la petición.
Las cuatro se comprobaron DENTRO del artefacto de firefox antes de escribir una
línea, no en la documentación de Mozilla: `Containers` y `3rdparty` en el
`policies-schema.json` de browser/omni.ja, `cookieStoreId` en el
`schemas/proxy.json` de omni.ja, y `storage.managed` leyendo
`getExtensionPolicy(id)` en ext-storage.js. Es la regla del §2.sexies: la
pregunta no es si Mozilla lo tiene, es si NUESTRO build lo tiene.
Tres decisiones que valen más que el código:
1. La config se indexa por NOMBRE de contenedor y no por `cookieStoreId`: el id
depende del orden en que se crearon, así que la misma configuración aplicada
a otro perfil apuntaría a otro contenedor.
2. FAIL CLOSED. Un contenedor con proxy configurado que no se pudo honrar no
sale directo: va a un destino cerrado y el navegador muestra el error. Salir
directo sería una fuga silenciosa — la misma familia que el artefacto vacío
de la regla 3, el fallo que llega hasta el final diciendo que todo fue bien.
3. `proxyDNS` PRENDIDO por defecto: sin él la consulta DNS sale por la línea que
se quería evitar. Es la fuga clásica de esta configuración.
Y lo que no promete está arriba de todo en la página de opciones, no en un pie:
separación de tráfico, NO anonimato; para anonimato, Tor Browser. El §4 cumplido
donde el usuario lo lee.
De paso, una verdad que estaba escrita en dos sitios pasa a tener un dueño: el
id de cada extensión sale ahora del `manifest.json` y no de una constante de
rebrand.py, el nombre del XPI se deriva de él, y el `install_url` de la política
se cruza contra la ruta donde el fichero quedó escrito de verdad. El icono se
inyecta desde branding/icons/ en vez de estar copiado byte a byte dentro de cada
extensión. Agregar una tercera extensión es ahora un directorio.
PROBADO, corriendo el árbol en la misma jaula que atuq-nested.sh:
· captura de about:preferences#containers con los cuatro contenedores y sus
iconos, más el containers.json del perfil;
· extensions.json del perfil nombra las dos extensiones;
· `console.info: "atuq/proxy: 4 contenedor(es) enrutado(s)"` — leyó la config
de fábrica por storage.managed Y la casó con los contenedores de la política;
· scripts/test-atuq-politica.py: cinco formas de desincronizar política y XPI,
las cinco matan el build, y el control con la política intacta pasa.
NO probado y dicho por su nombre: que una petición hecha en «Banco» salga por el
proxy de «Banco». Pide automatizar la UI y queda pendiente.
Dos obstáculos del método, que valen para la próxima. La consola de una
extensión es CONTENIDO: `devtools.console.stdout.chrome` (que viene en true) no
la incluye, hace falta `...stdout.content`. Y un `moz-extension://` NO se abre
desde la línea de comandos —muere con `NS_NOINTERFACE [nsIFileURL.file]` y abre
la home en su lugar—, además de que `--screenshot` dispara al `load`, que puede
ocurrir antes de que arranquen las extensiones. Eso último destapó un fallo real
y arreglado: la página de opciones confundía «no hay contenedores» con «la API
no está» y mostraba un mensaje FALSO.
Verificado contra firefox b3:352d7880; se reconstruye contra el firefox con
RLBox cuando selle. La otra mitad de esta unidad —el rename de
recipes/atuq/extension/ a extensions/inicio/— entró sin querer en ff0b556, que
es de otro frente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
551 lines
36 KiB
Markdown
551 lines
36 KiB
Markdown
# 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.
|
||
|
||
**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: `<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.
|
||
|
||
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.
|
||
|
||
## 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 |
|
||
| 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 |
|
||
|
||
**Lo que NO está probado, dicho por su nombre:** que una petición hecha en el contenedor «Banco»
|
||
salga por el proxy de «Banco». Eso pide automatizar la UI —una pestaña de contenedor no se abre
|
||
desde la línea de comandos— y queda pendiente en vez de darse por hecho.
|
||
|
||
**Dos obstáculos del método, que valen para la próxima:**
|
||
|
||
- **La consola de una extensión es CONTENIDO, no chrome.** `devtools.console.stdout.chrome` viene
|
||
en `true` de fábrica y NO la incluye; hace falta `devtools.console.stdout.content`. Sin esa pref
|
||
los mensajes de la extensión sólo viven en la consola del navegador, que en headless no mira
|
||
nadie.
|
||
- **Un `moz-extension://` NO se puede abrir desde la línea de comandos.** El manejador intenta
|
||
resolverlo como fichero y muere con `NS_NOINTERFACE [nsIFileURL.file]`, abriendo la home en su
|
||
lugar — o sea que la captura sale de otra página y parece que la tuya no funciona. Y `--screenshot`
|
||
dispara al `load`, que puede ocurrir ANTES de que termine el arranque de las extensiones: ahí el
|
||
global `browser` todavía no está inyectado. Eso destapó un fallo real y se arregló: la página de
|
||
opciones distinguía mal «no hay contenedores» de «la API no está» y mostraba un mensaje FALSO.
|
||
|
||
### 2.ter Dónde vive la fuente de `atuq` — hizo falta un modo de `[source]` nuevo
|
||
|
||
Escribir la receta destapó un muro que este documento no había visto: `[source]` era
|
||
**obligatoriamente** git o tarball, así que una receta cuyo contenido no viene de upstream sino de
|
||
nosotros no se podía ni expresar. Los rodeos eran todos peores — fetchear una fuente que después se
|
||
ignora **miente** sobre la identidad del artefacto, y colgar el overlay de un repo aparte obliga al
|
||
worker a tener acceso a un repo privado.
|
||
|
||
Se agregó **`source.dir`** (commit `54fa4a9`): un árbol dentro del propio repo, hasheado por
|
||
**contenido** con `ArtifactHash::of_tree` —que ya existía para verificar bit-reproducibilidad en
|
||
Stage 2— y no por ruta. Es la misma disciplina que ya tenían los `patches`, que entran al hash por
|
||
bytes. Editar un CSS del overlay mueve el `ArtifactHash`; renombrar el directorio, no.
|
||
|
||
Un `.swm` **no** puede llevar una receta derivada, y los cuatro sitios que lo tocan fallan
|
||
diciéndolo: el manifiesto es compartible y necesita un puntero resoluble desde fuera.
|
||
|
||
**Hecho el 2026-09-05** (commits `aeebb19` y `6f6c32b`): `recipes/atuq.toml` + `recipes/atuq/`.
|
||
La **v0.2** (`b3:7eff4ba4`) ya trae el branding: `application.ini` (Vendor/Name/RemotingName/
|
||
CodeName, con el `ID` intacto para no salirse del ecosistema de complementos), `brand.ftl` dentro de
|
||
`browser/omni.ja`, los binarios renombrados, iconos propios y `.desktop`. El re-empaque preserva
|
||
orden, `STORED` y fechas del original, y se verificó contra el artefacto: **5306 entradas, mismo
|
||
orden, y una sola con contenido distinto**. El `BuildID` se deriva del contenido del overlay —no de
|
||
la fecha— porque es la clave con la que Gecko invalida su startup cache: sin eso, un cambio de
|
||
chrome arranca con la interfaz vieja y *parece* que el overlay no agarró.
|
||
|
||
La capa de configuración de la v0.1 son cuatro ficheros —autoconfig, `atuq.cfg`, `chrome/atuq.css`,
|
||
`policies.json`— y el CSS se registra como `USER_SHEET` desde autoconfig porque `userChrome.css`
|
||
vive en el perfil y exigiría que el usuario prenda una pref: atuq tiene que verse como atuq en el
|
||
primer arranque. Falta construirlo: su dep `firefox` está en vuelo.
|
||
|
||
### 2.quater Abrirlo en una pantalla: tres fallos que la clausura no podía ver
|
||
|
||
El 2026-09-05, con `atuq` sellado y verificado, se hidrató y se abrió en el compositor de la sesión
|
||
(waypipe). **No arrancó**, y los tres motivos son la diferencia entre «sellado» y «usable»:
|
||
|
||
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 <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:
|
||
|
||
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 | valida la tesis del reúso | build en curso |
|
||
| 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 construyendo | 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.
|