El README de la receta afirmaba (a) que las extensiones las instala la POLÍTICA porque el sideloading desde `distribution/extensions/` «ya no funciona», y (b) que el chrome de verdad —split view— exige entrar a `omni.ja` y escribirlo nosotros. Las dos son falsas, y ninguna observación del artefacto las distinguía: hubo que romper un mecanismo por vez. scripts/test-atuq-instalacion.py — tres escenarios: as-is nada roto ⇒ las dos extensiones puestas no-policy `policies.json` sin ExtensionSettings ⇒ SIGUEN puestas policy-only los XPI fuera de la carpeta ⇒ NINGUNA, y la home vuelve a about:home ⇒ instala el ESCANEO de la carpeta; `install_url` con `file://` no instala nada. El §7.bis del SDD 26 tenía razón. El control es por construcción: la sonda vive en la carpeta y ninguna política la nombra, así que una corrida muda se declara ROTA en vez de leerse como «no instaló». Y la segunda señal que probé NO discrimina, queda dicho: el `location` de `extensions.json` sale `app-profile` también cuando instala la carpeta. scripts/test-atuq-chrome.py — el chrome se programa desde `atuq.cfg`, sin abrir el zip: observando `browser-delayed-startup-finished` se toca el `gBrowser` de cada ventana. Y la vista dividida ya la trae el motor (fx 154) PRENDIDA de fábrica, ejercitada de verdad —`addTabSplitView` ⇒ `activeSplitView` + 2 navegadores—, no «el fichero está». Dos pendientes que eran de upstream. Control negativo del propio motor: con las pestañas fijadas devuelve null (WRAPPER null, ACTIVA no). Corolario para lo que venga: cada función del chrome que NO entre a `omni.ja` es una que no pelea con el orden del `jarlog` del PGO — que es justo lo que tiene al jarlog aparcado. Queda escrito lo que NO está: ninguno de los `test-atuq-*` corre en el latido (sólo lo hace `vigia-sonames.py`), y meterlos cuesta ~3 min por ciclo más el rootfs hidratado en el hub. Medido sobre atuq b3:fab2fbfb → b3:8f6d09c2 (el README entra en el hash: documentar la medición re-hashea el artefacto medido, y los guardianes se niegan a medir uno que no sea el vigente).
10 KiB
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.
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 |
<appdir>/atuq.cfg |
Prefs de fábrica + carga la hoja de estilo del chrome. |
chrome/atuq.css |
<appdir>/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 |
<appdir>/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/ |
<appdir>/distribution/extensions/inicio@atuq.tawasuyu.xpi |
Página de inicio y pestaña nueva. |
extensions/proxy/ |
<appdir>/distribution/extensions/proxy@atuq.tawasuyu.xpi |
Proxy por contenedor (SDD 26 §6.8). |
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:
- el XPI tiene que quedar en
distribution/extensions/— es lo único que la instala; - la entrada en
ExtensionSettingsno es opcional por eso: es la que la dejanormal_installed(el usuario la puede quitar) y la que le entrega sustorage.managedpor3rdparty.Extensions; extensions.autoDisableScopes = 0enatuq.cfges lo que evita que el escaneo las instale desactivadas;- el
locationqueextensions.jsonle pone al complemento no dice de qué mecanismo salió: medido, los instalados por la carpeta también salenapp-profile.
Una extensión nueva no cuesta nada, y su id tiene un solo dueño
extensions/<lo-que-sea>/ 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
<id>.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:
atuq.cfgprende los contenedores (privacy.userContext.enabled), que Firefox trae apagados;policies.jsonlos CREA conContainers.Default—el único mecanismo que los pone en un perfil NUEVO— y deja la config de fábrica del proxy en3rdparty.Extensions, que es de dónde la extensión lee sustorage.managed;extensions/proxy/los enruta conproxy.onRequest, que es lo único que ve elcookieStoreIdde 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.
Por qué el CSS no es userChrome.css
userChrome.css vive en el PERFIL del usuario y exige que el usuario prenda
toolkit.legacyUserProfileCustomizations.stylesheets. Una distro no puede depender de eso: el
navegador tiene que verse como atuq la primera vez que arranca, con un perfil recién creado. La vía
que sí funciona a nivel de aplicación es el nsIStyleSheetService desde autoconfig, que es lo que
hace atuq.cfg.
La marca
El arte viene de un tablero de identidad del operador (2026-09-05): un zorro en medallón con cenefa andina, sobre disco teal y anillo noche.
branding/icons/atuq{16,32,48,64,128,256}.png— el medallón recortado del tablero, con máscara circular y alfa, reducido con LANCZOS desde un master de 1024 px. Se generan una vez y se copian; no se dibujan en cada build.- La paleta del chrome se muestreó del propio medallón, tomando el color más frecuente de cada
zona, en vez de estimarla a ojo:
#c15728el zorro,#1b6470el disco,#04161del anillo,#f5ecdelos reflejos. Un token escrito «parecido» es cómo una identidad se desalinea de su logo en tres ediciones. - Del tablero se toman la forma y el color; los eslóganes NO — el operador los descartó
explícitamente.
brand.ftllleva el nombre y nada más.
⚠ A 16 px el medallón pierde el detalle y queda como una mancha cálida redonda. Es intrínseco a un logo con cenefa, no un defecto del escalado: la salida sería un dibujo simplificado para ese tamaño, y eso lo decide quien dibujó el zorro.
El chrome se programa desde atuq.cfg, no abriendo omni.ja
Esto era un pendiente («el chrome de verdad dentro de omni.ja») y resultó ser dos cosas falsas.
Medido con scripts/test-atuq-chrome.py sobre atuq b3:9e16ac35:
-
No hace falta entrar al zip para programar el chrome.
atuq.cfgya corre con privilegios —de ahí salensIStyleSheetService—, así que desde ahí se observabrowser-delayed-startup-finishedy se toca elgBrowserde CADA ventana. La sonda abre dos pestañas y las parte en dos, desdeatuq.cfg, sin tocaromni.ja.Y es la vía que hay que usar, no un atajo: re-empacar
omni.japelea con el orden deljarlogdel PGO —por eso eljarlogestá aparcado—, así que cada función nueva del chrome que NO entre al zip es una que no compite con la optimización de arranque. -
La vista dividida ya la trae el motor, y prendida. Firefox 154 lleva
tabbrowser/tabsplitview.js(492 líneas),opentabs-splitview.mjsysplit-view-footer.jsdentro debrowser/omni.ja, ydefaults/preferences/firefox.jsdicepref("browser.tabs.splitView.enabled", true). La sonda lo ejercita de verdad —gBrowser.addTabSplitView([t1, t2])⇒activeSplitViewpuesta ysplitViewBrowsers.length == 2— así que no es «existe el fichero»: funciona en nuestro build. No hay nada que escribir acá.
⚠ Lo que esto NO dice: atuq todavía no lleva JS de chrome propio. Lo medido es el GANCHO, con la
sonda inyectada por una capa de overlay que no viaja en el artefacto. Cuando haya una función que lo
justifique, el fichero va en chrome/atuq.js y lo carga atuq.cfg igual que la hoja de estilo.
El control negativo de la sonda sale del propio motor: addTabSplitView documenta que si TODAS las
pestañas están fijadas borra el wrapper y devuelve null. --negative-control las fija y exige eso.
Lo que TODAVÍA no está
- La extensión de
scty el host de native messaging (SDD 26 §6.1 y §7). (El §6.8 ya no tiene pendientes: que una petición hecha en «Banco» sale por el proxy de «Banco» está probado porscripts/test-atuq-ruteo.py, que abre las dos pestañas por sessionstore y mira a qué puerto llama el navegador. Trae control negativo.)