Files
sergioandsergio 6f0ba974c7 feat(atuq): la boveda de la suite guarda las contrasenas, y el gestor de Gecko se aparta
La decima extension de atuq. Ofrece la credencial que corresponde al sitio
abierto y la pone en el formulario, pero NO puede sacar una contrasena de la
boveda por su cuenta: pregunta cuales corresponden —y eso contesta titulo y
usuario, nada mas— y la contrasena sale por otro verbo que pregunta en el
escritorio antes de contestar. Si la extension queda comprometida, o si una
pagina consigue hablarle, lo que obtiene es la lista de titulos de los sitios
que coinciden con su propia direccion.

La direccion siempre la pone el chrome y nunca la pagina. Lo unico que el
trozo que corre dentro del sitio aporta es que HAY un formulario y lo que el
usuario tipeo; si pudiera decir de quien es la pagina, podria decir que es el
banco.

Y la extension no decide nada: quien sabe que credencial va en que sitio es
la funcion del otro lado del cable. Ponerlo en JS seria reimplementar el
original y, peor, poner en la pagina la decision de a quien se le entrega una
contrasena.

EL GESTOR DE GECKO SE APAGA, pero como valor de arranque y no como politica.
Dos gestores peleando por el mismo campo es la peor experiencia que puede
tener alguien que solo quiere entrar a su correo: el sitio se rellena dos
veces, o ninguna, y no hay forma de saber cual tiene la buena. De fabrica
manda la boveda; el que prefiera el de Gecko lo prende y listo. Prohibirselo
seria decidir por el, que es lo que este fichero dice en su cabecera que atuq
no quiere ser.

Y un guardian nuevo, que corre en un segundo y sin construir nada. Una
extension esta enchufada en CUATRO lugares distintos y ninguno da error si
falta: el identificador que ella declara, la politica que la instala, el
permiso para hablarle al host, y las preferencias que la acompanan. Si falta
la politica no se instala. Si falta el permiso se instala y se conecta a
nada, en silencio, y la insignia no aparece nunca. Si faltan las preferencias
los dos gestores se pelean. Ninguno de los tres se ve como un error: se ven
como "no anda". El guardian los mira todos y trae su control negativo, que le
saca el permiso del host y exige que lo detecte.

No reemplaza al guardian de metal —servidor, navegador real, login real, el
dialogo a la vista—: lo precede, y ahorra descubrir construyendo que faltaba
un renglon en un JSON.

Los cinco verbos del otro lado ya estan en tawasuyu (bc6903f9e), con trece
tests que leen el cable.
2026-09-15 10:08:20 -04:00
..

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).
extensions/sct/ <appdir>/distribution/extensions/sct@atuq.tawasuyu.xpi Transparencia de scripts (SDD 26 §6.1).
extensions/descargas/ <appdir>/distribution/extensions/descargas@atuq.tawasuyu.xpi Descargas con identidad (SDD 26 §6.2).
extensions/archivo/ <appdir>/distribution/extensions/archivo@atuq.tawasuyu.xpi Archivo personal (SDD 26 §6.3).
extensions/medios/ <appdir>/distribution/extensions/medios@atuq.tawasuyu.xpi Medios fuera del navegador (SDD 26 §6.6).
extensions/torrent/ <appdir>/distribution/extensions/torrent@atuq.tawasuyu.xpi Torrent con identidad (SDD 26 §6.9).
extensions/foco/ <appdir>/distribution/extensions/foco@atuq.tawasuyu.xpi Modo foco: lo muestra, no lo apaga (SDD 26 §6.5).
extensions/ia/ <appdir>/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 <appdir>/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:homeinstall_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/<lo-que-sea>/ con un manifest.json adentro y ya: rebrand.py recorre el directorio, saca el id de browser_specific_settings.gecko.idno 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:

  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 <script src> con filterResponseData y se lo pasa al host nativo; el host —puriy-costura, que por dentro es puriy-sct— lo hashea con BLAKE3, aprende el conjunto de código estable de cada origen (TOFU) y avisa cuando un sitio ya estable ejecuta código que nadie vio nunca. Eso es el síntoma de un CDN envenenado, una cadena de suministro comprometida o un script dirigido a un usuario, y no lo tiene ningún navegador.

La extensión no hashea ni guarda nada. Ve bytes y hace preguntas; el registro es del núcleo del otro lado del cable. Duplicarlo en JS habría sido un segundo registro que se desalinea del primero, y el primero es el que está certificado sin red.

servidor HTTP → filterResponseData → connectNative → /usr/lib/mozilla/native-messaging-hosts/
              → puriy-costura --state → puriy-sct (TOFU + bitácora fork-proof)

Cinco cosas de esta cadena que no se pueden deducir, y por eso se midieron

  1. El manifiesto va en /usr/lib/mozilla/native-messaging-hosts/, NO en el appdir. Gecko saca esa ruta de XRESysNativeManifests, que en Linux es un /usr/lib/mozilla compilado: el rebranding a atuq no lo mueve. Y nadie más en el corpus escribe ahí (comprobado sobre el store), así que la ruta es de atuq.
  2. El manifiesto NO puede llevar argumentos. En omni.ja, modules/NativeMessaging.sys.mjs hace let command = hostInfo.manifest.path y le pasa sólo [ruta-del-manifiesto, id-de-la-extensión]. Como puriy-costura sin --state corre todo en memoria —y entonces cada arranque del navegador volvería a «aprendiendo» y nada alertaría nunca—, hace falta el lanzador bin/puriy-costura-host. Es la peor forma de fallo: la función existe, no falla, y no protege de nada.
  3. filterResponseData y el permiso webRequestFilterResponse existen en NUESTRO build (están en omni.ja, chrome/toolkit/content/extensions/schemas/web_request.json). Se preguntó al artefacto antes de escribir la extensión, no a la documentación de Mozilla.
  4. Los scripts inline no se ven por esta vía, y no es un agujero disimulado: filterResponseData entrega el cuerpo de una PETICIÓN, y un inline viaja dentro del HTML. El ataque que sct nombra es la sustitución en el CDN, o sea exactamente el caso <script src>. Los inline los ve la v2, con el gancho en el script loader de Gecko.
  5. Una carga de página puede producir MÁS DE UNA petición del mismo documento, y una llega con tabId = -1. La primera versión agrupaba por (tabId, documento) y contaba esa carga como DOS visitas. No es cosmético: inflar las visitas estabiliza el origen antes de tiempo, y un origen estabilizado antes de conocer su código real alerta por churn legítimo — el falso positivo que la spec de puriy-sct pide evitar por encima de todo. Ahora se agrupa por documento, y scripts/test-atuq-sct.py vigila el invariante: una carga, una visita.

v1 observa y avisa; no bloquea — y eso es del diseño

El §6.1 escribió «consulta al testigo antes de dejarla pasar». La forma fuerte es la v2. Acá se observa y se avisa por dos razones: puriy-sct es explícito en que su v1 alerta y no bloquea (bloquear necesita antes la acción del usuario, que es su M1), y poner un viaje ida-y-vuelta entre procesos en el camino crítico de cada script de cada página no es «más seguro»: es un navegador que nadie usa.

El aviso es pasivo: una insignia en la barra con el número de scripts nuevos y, en el tooltip, sus nombres. No un modal — un modal por cada despliegue entrena a la gente a cerrarlo sin leer, y entonces el que importa también se cierra.

Y el límite que hay que decir primero: se hashea el TEXTO ya decodificado que la extensión entrega, no los bytes que sirvió el servidor. La detección vale —el TOFU compara lo mismo contra lo mismo—, pero ese hash no es comparable con el que publique un tercero sobre los bytes servidos.

scripts/test-atuq-sct.py mide la cadena entera con un servidor HTTP real y seis cargas de página, y trae dos controles: la quinta carga repite el MISMO script ya estable y no debe alertar, y --negative-control borra el manifiesto y exige que no haya veredicto — o sea que el veredicto de la corrida positiva viene del host y no de la extensión inventándolo.

descargas — una descarga con identidad (§6.2)

Cuando una descarga TERMINA, la extensión le pasa al host la ruta, el nombre y de dónde salió; el host la ingiere a un CAS BLAKE3 y contesta el hash, el tamaño y cuántas veces se bajó ese mismo contenido. El mismo contenido con otro nombre es un objeto y dos nombres, y el navegador lo dice. 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.

No es un almacén nuevo: es arje-cas, el mismo formato y el mismo hash que usan arje, takana y tejido.

Tres cosas que hay que saber, y las tres están medidas:

  1. La raíz del CAS del navegador NO es la del sistema. arje_cas::gc borra todo blob que no esté en el set reachable de su llamador, y el único que existe hoy (arje-brain, GcCas) lo arma con la cadena de audit y las raíces vivas del grafo. Una descarga no está en ninguno: el primer GcCas se la llevaría, en silencio. Por eso van a <estado>/cas. El precio, dicho: tejido sirve el CAS por defecto, así que compartir una descarga hoy exige apuntarlo a esa raíz. ⚠ Se llamaba descargas-cas hasta que entró el §6.3: ahora el archivo personal y las descargas comparten un CAS, que es lo que hace que una página archivada y un fichero bajado con el mismo contenido sean un solo objeto. El guardián de descargas se quedó mirando el nombre viejo y estuvo rojo dos días sin que nadie lo corriera.
  2. El fichero que bajaste no se toca. Se copia al CAS y se deja donde estaba. El guardián lo comprueba byte a byte.
  3. La extensión observa, no intercepta. Se entera cuando la descarga terminó. Meterse en el medio obligaría a decidir qué pasa si el CAS falla, y la respuesta correcta —que la descarga siga igual— es exactamente lo que se consigue no metiéndose.

scripts/test-atuq-descargas.py baja de verdad (Content-Disposition: attachment) dos veces y exige dedup=true la segunda; con --negative-control la segunda descarga es otro contenido y exige lo contrario.

archivo — el archivo personal (§6.3)

Cada página que leés se congela: el HTML ya ejecutado —lo que viste, no lo que mandó el servidor— va al CAS con su BLAKE3, y la url, el título y el texto visible al índice. Después se busca. Volver a la misma página no la duplica; la misma URL con otro contenido es otra página, así que el archivo tiene versiones. Un historial de direcciones guarda punteros, y los punteros se pudren.

Lo que NO se archiva es la parte que hay que mirar:

  1. nada de una ventana privada — se comprueba en el fondo y no en el guión de contenido, porque tabs.get() es lo único que sabe si la pestaña lo es; y si no se puede saber, no se archiva. ⚠ Medido: hoy el que impide archivar en privado es el navegador (las extensiones no corren ahí salvo que se las habilite), así que nuestro incognito no se ejecuta nunca — y es justo lo que haría seguro habilitar private_browsing el día que haga falta;
  2. nada fuera del marco principal: un <iframe> de publicidad no es una página que alguien leyó;
  3. nada sin texto visible: el HTML de un visor de PDF llenaría el archivo de cosas que no se encuentran.

archive.search es el registro LITERAL —todas las palabras de la consulta tienen que aparecer en la url, el título o el texto indexado—. No es preguntarle al historial en lenguaje natural: eso es el registro semántico, un motor rag-motor::RagMotor como willay-rag, que necesita un daemon de embeddings y un LLM de verdad y devuelve None cuando no están. Cuando exista se enchufa del mismo lado y esto no cambia.

scripts/test-atuq-archivo.py visita, revisita y busca; su control negativo abre la página en una ventana privada, exige que no se archive nada y dice quién lo impidió — nuestro código o el navegador—, porque las dos protegen pero sólo una es nuestra.

medios — el vídeo no se ve en una pestaña (§6.6)

Cuando una navegación de primer nivel resulta ser un medio —el servidor contesta video/* o audio/*—, la navegación se cancela y la URL se la lleva mpv, que ya viaja en las cuatro imágenes de escritorio. El navegador vuelve a ser un navegador en vez de un reproductor a medias metido en una pestaña.

Sólo el primer nivel: un <video> embebido es parte de la página, y sacarlo de ahí rompería el sitio que lo puso. Las reglas anchas en el camino de cada petición son las que terminan rompiendo la web de alguien.

Si no hay reproductor, la navegación VUELVE al navegador. Cancelar es síncrono y la respuesta del host llega después, así que no se puede saber en el momento si arrancó; se cancela sólo con el puerto vivo y, ante un fallo, se reabre la URL con una marca para no entrar en bucle. Quedarse sin vídeo y sin pestaña es el único resultado inaceptable, y el control negativo del guardián lo prueba.

⚠ El binario no viene del mensaje: es una constante del host. Lo único que viaja es la URL, sólo http(s), y detrás de un --.

ia — la barra lateral le pregunta a un modelo de ESTA máquina (§6.7)

Sin nube, sin clave de API y sin un byte saliendo: el host levanta un llama-server (el llama-cpp del corpus) contra el modelo pineado en /usr/share/takana/ia/modelo.gguf y habla con él por un socket UNIX. No hay servicio de IA en la imagen: el primer ai.ask lo levanta, y se muere con el navegador — al revés que el daemon del torrent (§6.9), porque un torrent tiene trabajo que sobrevive y medio giga de modelo en RAM no.

El puerto nativo vive en el fondo, no en la página del panel: si viviera en la página, cerrar la barra lateral se llevaría el proceso del host y el modelo cargado con ella. panel.html?q=… abre el panel con la pregunta ya hecha, y ?p=… la manda a tus páginas en vez de al modelo.

Dos preguntas, dos botones, y no se mezclan a propósito. «Al modelo» genera texto; «A mis páginas» ordena por parecido lo que ya leíste (§6.3, archive.ask) y muestra los títulos en orden, sin porcentaje: el puntaje es un coseno y leerlo como «85 % de acierto» sería inventarle un significado. Presentar las dos juntas haría imposible saber cuál contestó — una inventa y la otra recuerda.

Cuando algo falta, el panel muestra la causa («no hay modelo en …», «no hay motor de inferencia en …»). Una IA que ante un fallo contesta algo genérico es peor que una que no contesta: el usuario no puede distinguir «no hay modelo» de «el modelo dijo eso».

scripts/test-atuq-ia.py mide la cadena entera con inferencia REAL, en una jaula --unshare-net, y comprueba además que al cerrarse el navegador queden cero llama-server. Control negativo: sin modelo, la causa aparece y no hay respuesta inventada.

El modelo de chat es Qwen2.5-1.5B-Instruct Q4_K_M (recipes/ia-modelo-chat.toml, Apache-2.0, 1,04 GiB, 20,1 tok/s en CPU, habla español — las tres cosas medidas antes de pinearlo), y va en las cuatro imágenes de escritorio junto con el motor: ~1,25 GiB por perfil.

El de embeddings (ia-modelo-embeddings, Qwen3-Embedding-0.6B, 610 MiB) es descarga opcional: no está declarado en ningún perfil —decisión, no olvido— y se instala con takana install ia-modelo-embeddings. Hasta que se instale, «A mis páginas» contesta con esa misma línea en vez de con un «no está» sin salida.

foco — el navegador MUESTRA el foco, y no tiene con qué apagarlo (§6.5)

El foco no es un bloqueador de sitios: es una regla nft de egress por cgroup que aplicó root, y mientras está puesta el navegador no sale a la red — con todo lo local andando (el archivo del §6.3 se lee, el CAS del §6.2 está). Medido en una máquina con root: el proceso del cgroup en foco no abre la conexión y el del cgroup de al lado sí (scripts/test-foco-egress.sh).

Esta extensión es la mitad del navegador, y es asimétrica a propósito: sólo lee. Pregunta focus.state al host cada minuto —el estado lo cambia root por fuera, así que no hay evento al que suscribirse— y pinta tres cosas y ninguna más: foco si está puesto, nada si no, y ? si nadie escribió el estado. Ese tercer caso es el que importa: si «no sé» se redondeara a «apagado», la insignia afirmaría que no hay foco sin haberlo mirado.

No hay verbo para apagarlo, y eso es la propiedad y no un pendiente. Si desde el navegador se pudiera levantar, el modo foco valdría lo que vale un bloqueador de extensión: dos clics. En tawasuyu hay un test que lo fija (focus.stop → «verbo desconocido»), y acá el guardián mide el efecto: después de una sesión entera del navegador, el fichero de estado quedó igual.

scripts/test-atuq-foco.py corre los tres estados y comprueba, por corrida, qué dijo la extensión, qué pintó y que el estado no cambió. Verificado rompiéndolo: con una extensión que pinta «foco» pase lo que pase, el guardián falla nombrando la insignia.

Lo que todavía NO existe —y no se inventó de paso— es quién pone a atuq en un cgroup y quién aplica la política: lo primero pide delegación de un subárbol a la sesión (decisión de arje, SDD 10) y lo segundo pide root. Ver el §6.5 del SDD 26.

torrent — el magnet lo toma la suite (§6.9)

La extensión registra un handler para magnet: (protocol_handlers, que existe en nuestro build), así que un clic abre su propia página con el enlace, y esa página se lo pasa al host. El host se lo entrega a puriy-costura-torrent, que es otro proceso.

Por qué otro proceso y no un verbo más: una descarga tiene que sobrevivir al navegador, y el host no lo hace —Gecko lo mata al cerrarse el puerto—. Y librqbit son 226 crates: dentro del host, ese binario pasaría de 952 K a ~20 MB para las cinco extensiones que lo comparten.

Perezoso: no hay servicio en la imagen. El binario está y no corre hasta que hay un torrent; lo levanta su cliente (con setsid, o se moriría con el navegador) y se va solo cuando no queda nada activo. Sembrar cuenta como actividad.

La página no valida la forma del origen: la valida el host, que es quien decide qué le pasa al daemon. Tener la regla escrita dos veces es tenerla mintiendo el día que una cambie.

scripts/test-atuq-torrent.py sirve un .torrent hecho a mano (un magnet: bloquearía esperando metadata que sin peers no llega) y comprueba que el daemon aparezca sin que nadie lo arranque. Su control negativo saca el cliente de la imagen y exige que el host lo diga en vez de fingirlo.

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: #c15728 el zorro, #1b6470 el disco, #04161d el anillo, #f5ecde los 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.ftl lleva 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:

  1. No hace falta entrar al zip para programar el chrome. atuq.cfg ya corre con privilegios —de ahí sale nsIStyleSheetService—, así que desde ahí se observa browser-delayed-startup-finished y se toca el gBrowser de CADA ventana. La sonda abre dos pestañas y las parte en dos, desde atuq.cfg, sin tocar omni.ja.

    Y es la vía que hay que usar, no un atajo: re-empacar omni.ja pelea con el orden del jarlog del PGO —por eso el jarlog está 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.

  2. La vista dividida ya la trae el motor, y prendida. Firefox 154 lleva tabbrowser/tabsplitview.js (492 líneas), opentabs-splitview.mjs y split-view-footer.js dentro de browser/omni.ja, y defaults/preferences/firefox.js dice pref("browser.tabs.splitView.enabled", true). La sonda lo ejercita de verdad — gBrowser.addTabSplitView([t1, t2])activeSplitView puesta y splitViewBrowsers.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 v2 de sct: el gancho en el script loader de Gecko, que es lo único que ve los scripts inline y lo único que puede BLOQUEAR. Eso sí es parche del árbol (§2.bis).
  • La mitad SEMÁNTICA del archivo (6.3): un motor rag-motor::RagMotor sobre el archivo, que necesita el daemon de embeddings y un backend LLM en la imagen. willay-rag es el molde.
  • La IA local en la barra lateral (6.7), que está bloqueada por algo medido: el corpus no tiene ninguna receta de LLM ni de embeddings. Es también la que traería el motor que le falta al archivo.
  • Que una descarga se pueda COMPARTIR con tejido: hoy el objeto está en el CAS de descargas y tejido sirve el del sistema. Falta que el índice de descargas sea una raíz que el GC respete, y ahí las dos raíces pueden volver a ser una. (El §6.8 ya no tiene pendientes: que una petición hecha en «Banco» sale por el proxy de «Banco» está probado por scripts/test-atuq-ruteo.py, que abre las dos pestañas por sessionstore y mira a qué puerto llama el navegador. Trae control negativo.)