# atuq — el envoltorio Gecko de la distro `atuq` (quechua: *zorro*) es el navegador de la distro: **un artefacto DERIVADO de `firefox`**, no un fork de su fuente. El diseño completo está en [`docs/26-atuq-envoltorio-gecko.md`](../../docs/26-atuq-envoltorio-gecko.md). Este directorio ES la fuente de atuq — `recipes/atuq.toml` lo declara con `source.dir` y takana lo hashea por CONTENIDO. Editar un fichero de acá mueve el `ArtifactHash`; no hay commit que pinear. ⚠ **Y ESTE README también entra en el hash.** O sea que escribir acá el resultado de una medición invalida el artefacto sobre el que se midió: el hash que un párrafo cita es siempre el PREDECESOR del vigente. Es esperable y no es deuda —el rebuild son 6 s mientras el `firefox` vigente esté sellado—, pero hay que saberlo antes de leer un hash de acá y pensar que el fichero quedó viejo. Los guardianes de `scripts/test-atuq-*.py` se NIEGAN a medir un artefacto que no sea el vigente (por eso el ciclo es: editar todo → resellar una vez → medir), y cada reselle deja 340 M más en el store: conviene juntar las ediciones en una. ## Qué hay acá y dónde cae | Acá | En el artefacto | Qué hace | |---|---|---| | `prefs/autoconfig.js` | `defaults/pref/autoconfig.js` | Le dice a Gecko que lea `atuq.cfg`. Es el único gancho posible antes de que arranque el perfil. | | `atuq.cfg` | `/atuq.cfg` | Prefs de fábrica + carga la hoja de estilo del chrome. | | `chrome/atuq.css` | `/chrome/atuq.css` | El aspecto. Se registra como **USER_SHEET** desde `atuq.cfg` — el mismo nivel de cascada que `userChrome.css`, para que el usuario pueda pisarlo. (Decía AGENT_SHEET; el fichero nunca lo usó, y AGENT_SHEET además alcanzaría al contenido de las páginas.) | | `distribution/policies.json` | `/distribution/policies.json` | Política de fábrica: telemetría, updates, el MODO y la CONFIG de las extensiones (no su instalación — ver más abajo), los CONTENEDORES y la config de fábrica del proxy. | | `extensions/inicio/` | `/distribution/extensions/inicio@atuq.tawasuyu.xpi` | Página de inicio y pestaña nueva. | | `extensions/proxy/` | `/distribution/extensions/proxy@atuq.tawasuyu.xpi` | **Proxy por contenedor** (SDD 26 §6.8). | | `extensions/sct/` | `/distribution/extensions/sct@atuq.tawasuyu.xpi` | **Transparencia de scripts** (SDD 26 §6.1). | | `extensions/descargas/` | `/distribution/extensions/descargas@atuq.tawasuyu.xpi` | **Descargas con identidad** (SDD 26 §6.2). | | `extensions/archivo/` | `/distribution/extensions/archivo@atuq.tawasuyu.xpi` | **Archivo personal** (SDD 26 §6.3). | | `extensions/medios/` | `/distribution/extensions/medios@atuq.tawasuyu.xpi` | **Medios fuera del navegador** (SDD 26 §6.6). | | `extensions/torrent/` | `/distribution/extensions/torrent@atuq.tawasuyu.xpi` | **Torrent con identidad** (SDD 26 §6.9). | | `extensions/foco/` | `/distribution/extensions/foco@atuq.tawasuyu.xpi` | **Modo foco: lo muestra, no lo apaga** (SDD 26 §6.5). | | `extensions/ia/` | `/distribution/extensions/ia@atuq.tawasuyu.xpi` | **IA local en la barra lateral** (SDD 26 §6.7). | | `native-messaging/*.json` | `/usr/lib/mozilla/native-messaging-hosts/` | El manifiesto del host nativo. **Fuera del appdir** — ver abajo. | | `bin/puriy-costura-host` | `/puriy-costura-host` | El lanzador del host: le pone el `--state` que el manifiesto no puede. | ## Quién instala las extensiones — y este README decía lo contrario **Las instala el ESCANEO de `distribution/extensions/`.** `ExtensionSettings` de la política las FIJA y las CONFIGURA; instalarlas, no. ⚠ Esta sección decía justo lo contrario —que el *sideloading* desde la carpeta «ya no funciona» y que instalaba la política— y era **falso**. Los dos mecanismos apuntan a los MISMOS ficheros, así que ninguna observación del artefacto los distingue y la política *parece* ser la que instala. Para saberlo hay que romper uno por vez, y eso es lo que hace `scripts/test-atuq-instalacion.py` (tres escenarios, medidos el 2026-09-09 sobre `atuq b3:fab2fbfb` y repetidos sobre `b3:9e16ac35`): | escenario | qué se rompe | resultado | |---|---|---| | `as-is` | nada | las dos extensiones puestas; la home es la nuestra | | `no-policy` | `policies.json` **sin** `ExtensionSettings` | **siguen puestas** y la home sigue siendo la nuestra ⇒ instala la carpeta | | `policy-only` | los XPI mudados fuera de la carpeta, con la política apuntando a la ruta nueva | **no aparecen** y la home vuelve a `about:home` ⇒ `install_url` con `file://` no instala nada | En las tres corridas hay un CONTROL por construcción: una extensión sonda que vive en la carpeta y que **ninguna política nombra**. Si la sonda habla, el escaneo está vivo en esa corrida; una corrida muda se declara rota en vez de leerse como «no instaló». Consecuencias prácticas para quien agregue una extensión: 1. el XPI **tiene que quedar en `distribution/extensions/`** — es lo único que la instala; 2. la entrada en `ExtensionSettings` no es opcional por eso: es la que la deja `normal_installed` (el usuario la puede quitar) y la que le entrega su `storage.managed` por `3rdparty.Extensions`; 3. `extensions.autoDisableScopes = 0` en `atuq.cfg` es lo que evita que el escaneo las instale **desactivadas**; 4. el `location` que `extensions.json` le pone al complemento **no dice de qué mecanismo salió**: medido, los instalados por la carpeta también salen `app-profile`. ## Una extensión nueva no cuesta nada, y su id tiene un solo dueño `extensions//` con un `manifest.json` adentro y ya: `rebrand.py` recorre el directorio, saca el id de `browser_specific_settings.gecko.id` —**no de una constante suya**— y escribe `.xpi`. El icono lo inyecta desde `branding/icons/atuq128.png`, así que tampoco hay que copiarlo en cada extensión: la v0.4 tenía ese PNG duplicado byte a byte y con dos extensiones habrían sido tres copias que se desalinean en cuanto alguien actualice una sola. Y después **cruza los ids contra `policies.json`**, en los dos sentidos: una extensión empaquetada que la política no declara, o una política que apunta a un XPI que no existe, matan el build. Lo mismo con la ruta: el `install_url` tiene que ser exactamente donde el XPI quedó escrito. Antes eso era una constante contra otra constante; ahora una de las dos se DERIVA del fichero real. `scripts/test-atuq-politica.py` le pone delante las cinco formas conocidas de romper ese cruce y exige que las cinco maten el build (más un control con la política intacta, que tiene que pasar). ## El proxy por contenedor (§6.8) Tres piezas, y ninguna alcanza sola: 1. `atuq.cfg` prende los contenedores (`privacy.userContext.enabled`), que Firefox trae apagados; 2. `policies.json` los CREA con `Containers.Default` —el único mecanismo que los pone en un perfil NUEVO— y deja la config de fábrica del proxy en `3rdparty.Extensions`, que es de dónde la extensión lee su `storage.managed`; 3. `extensions/proxy/` los enruta con `proxy.onRequest`, que es lo único que ve el `cookieStoreId` de cada petición. La config se escribe 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. Dos capas: `storage.managed` es la de fábrica (la distro) y `storage.local` la del usuario, que pisa por contenedor. ⚠ **Es separación de tráfico, NO anonimato**, y eso está escrito arriba de todo en la propia página de opciones, no en un pie de página. No toca la huella del navegador. **Fail closed.** Un contenedor que TIENE proxy configurado y no se pudo honrar no sale directo: va a un destino cerrado (127.0.0.1:9) y el navegador muestra el error. Salir directo sería una fuga silenciosa, que es la peor forma de este fallo. Y `proxyDNS` viene prendido: sin él la consulta DNS sale por la línea que se quería evitar. ## `sct` — transparencia de scripts (§6.1) La joya del §6, y la primera pieza de atuq que habla con la suite. Qué hace: por cada carga de página, la extensión intercepta el cuerpo de cada `