gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

⎇ 97c35d67b3d4a693508353bdb724cf0557031a4e recipes/atuq/README.md Markdown · 159 siq'ikuna · 10.5 KB Ñawpa willay ch'uya

atuq: dos cosas que el README daba por ciertas eran falsas, y las dos se midieron 97c35d67b · Sergio ·

1# atuq — el envoltorio Gecko de la distro
2
3`atuq` (quechua: *zorro*) es el navegador de la distro: **un artefacto DERIVADO de `firefox`**, no un
4fork de su fuente. El diseño completo está en [`docs/26-atuq-envoltorio-gecko.md`](../../docs/26-atuq-envoltorio-gecko.md).
5
6Este directorio ES la fuente de atuq — `recipes/atuq.toml` lo declara con `source.dir` y takana lo
7hashea por CONTENIDO. Editar un fichero de acá mueve el `ArtifactHash`; no hay commit que pinear.
8
9⚠ **Y ESTE README también entra en el hash.** O sea que escribir acá el resultado de una medición
10invalida el artefacto sobre el que se midió: el hash que un párrafo cita es siempre el
11PREDECESOR del vigente. Es esperable y no es deuda —el rebuild son 6 s mientras el `firefox`
12vigente esté sellado—, pero hay que saberlo antes de leer un hash de acá y pensar que el fichero
13quedó viejo. Los guardianes de `scripts/test-atuq-*.py` se NIEGAN a medir un artefacto que no sea
14el vigente (por eso el ciclo es: editar todo → resellar una vez → medir), y cada reselle deja
15340 M más en el store: conviene juntar las ediciones en una.
16
17## Qué hay acá y dónde cae
18
19| Acá | En el artefacto | Qué hace |
20|---|---|---|
21| `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. |
22| `atuq.cfg` | `<appdir>/atuq.cfg` | Prefs de fábrica + carga la hoja de estilo del chrome. |
23| `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.) |
24| `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. |
25| `extensions/inicio/` | `<appdir>/distribution/extensions/inicio@atuq.tawasuyu.xpi` | Página de inicio y pestaña nueva. |
26| `extensions/proxy/` | `<appdir>/distribution/extensions/proxy@atuq.tawasuyu.xpi` | **Proxy por contenedor** (SDD 26 §6.8). |
27
28## Quién instala las extensiones — y este README decía lo contrario
29
30**Las instala el ESCANEO de `distribution/extensions/`.** `ExtensionSettings` de la política las
31FIJA y las CONFIGURA; instalarlas, no.
32
33⚠ Esta sección decía justo lo contrario —que el *sideloading* desde la carpeta «ya no funciona» y
34que instalaba la política— y era **falso**. Los dos mecanismos apuntan a los MISMOS ficheros, así
35que ninguna observación del artefacto los distingue y la política *parece* ser la que instala. Para
36saberlo hay que romper uno por vez, y eso es lo que hace `scripts/test-atuq-instalacion.py`
37(tres escenarios, medidos el 2026-09-09 sobre `atuq b3:fab2fbfb` y repetidos sobre `b3:9e16ac35`):
38
39| escenario | qué se rompe | resultado |
40|---|---|---|
41| `as-is` | nada | las dos extensiones puestas; la home es la nuestra |
42| `no-policy` | `policies.json` **sin** `ExtensionSettings` | **siguen puestas** y la home sigue siendo la nuestra ⇒ instala la carpeta |
43| `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 |
44
45En las tres corridas hay un CONTROL por construcción: una extensión sonda que vive en la carpeta y
46que **ninguna política nombra**. Si la sonda habla, el escaneo está vivo en esa corrida; una corrida
47muda se declara rota en vez de leerse como «no instaló».
48
49Consecuencias prácticas para quien agregue una extensión:
50
511. el XPI **tiene que quedar en `distribution/extensions/`** — es lo único que la instala;
522. la entrada en `ExtensionSettings` no es opcional por eso: es la que la deja `normal_installed`
53   (el usuario la puede quitar) y la que le entrega su `storage.managed` por `3rdparty.Extensions`;
543. `extensions.autoDisableScopes = 0` en `atuq.cfg` es lo que evita que el escaneo las instale
55   **desactivadas**;
564. el `location` que `extensions.json` le pone al complemento **no dice de qué mecanismo salió**:
57   medido, los instalados por la carpeta también salen `app-profile`.
58
59## Una extensión nueva no cuesta nada, y su id tiene un solo dueño
60
61`extensions/<lo-que-sea>/` con un `manifest.json` adentro y ya: `rebrand.py` recorre el directorio,
62saca el id de `browser_specific_settings.gecko.id` —**no de una constante suya**— y escribe
63`<id>.xpi`. El icono lo inyecta desde `branding/icons/atuq128.png`, así que tampoco hay que copiarlo
64en cada extensión: la v0.4 tenía ese PNG duplicado byte a byte y con dos extensiones habrían sido
65tres copias que se desalinean en cuanto alguien actualice una sola.
66
67Y después **cruza los ids contra `policies.json`**, en los dos sentidos: una extensión empaquetada
68que la política no declara, o una política que apunta a un XPI que no existe, matan el build. Lo
69mismo con la ruta: el `install_url` tiene que ser exactamente donde el XPI quedó escrito. Antes eso
70era una constante contra otra constante; ahora una de las dos se DERIVA del fichero real.
71
72`scripts/test-atuq-politica.py` le pone delante las cinco formas conocidas de romper ese cruce y
73exige que las cinco maten el build (más un control con la política intacta, que tiene que pasar).
74
75## El proxy por contenedor (§6.8)
76
77Tres piezas, y ninguna alcanza sola:
78
791. `atuq.cfg` prende los contenedores (`privacy.userContext.enabled`), que Firefox trae apagados;
802. `policies.json` los CREA con `Containers.Default` —el único mecanismo que los pone en un perfil
81   NUEVO— y deja la config de fábrica del proxy en `3rdparty.Extensions`, que es de dónde la
82   extensión lee su `storage.managed`;
833. `extensions/proxy/` los enruta con `proxy.onRequest`, que es lo único que ve el `cookieStoreId`
84   de cada petición.
85
86La config se escribe por **nombre de contenedor** y no por `cookieStoreId`: el id depende del orden
87en que se crearon, así que la misma configuración aplicada a otro perfil apuntaría a otro
88contenedor. Dos capas: `storage.managed` es la de fábrica (la distro) y `storage.local` la del
89usuario, que pisa por contenedor.
90
91⚠ **Es separación de tráfico, NO anonimato**, y eso está escrito arriba de todo en la propia página
92de opciones, no en un pie de página. No toca la huella del navegador.
93
94**Fail closed.** Un contenedor que TIENE proxy configurado y no se pudo honrar no sale directo: va a
95un destino cerrado (127.0.0.1:9) y el navegador muestra el error. Salir directo sería una fuga
96silenciosa, que es la peor forma de este fallo. Y `proxyDNS` viene prendido: sin él la consulta DNS
97sale por la línea que se quería evitar.
98
99## Por qué el CSS no es `userChrome.css`
100
101`userChrome.css` vive en el PERFIL del usuario y exige que el usuario prenda
102`toolkit.legacyUserProfileCustomizations.stylesheets`. Una distro no puede depender de eso: el
103navegador tiene que verse como atuq la primera vez que arranca, con un perfil recién creado. La vía
104que sí funciona a nivel de aplicación es el `nsIStyleSheetService` desde autoconfig, que es lo que
105hace `atuq.cfg`.
106
107## La marca
108
109El arte viene de un tablero de identidad del operador (2026-09-05): un **zorro en medallón** con
110cenefa andina, sobre disco teal y anillo noche.
111
112- `branding/icons/atuq{16,32,48,64,128,256}.png` — el medallón recortado del tablero, con máscara
113  circular y alfa, reducido con LANCZOS desde un master de 1024 px. Se generan una vez y se copian;
114  no se dibujan en cada build.
115- La **paleta del chrome se muestreó del propio medallón**, tomando el color más frecuente de cada
116  zona, en vez de estimarla a ojo: `#c15728` el zorro, `#1b6470` el disco, `#04161d` el anillo,
117  `#f5ecde` los reflejos. Un token escrito «parecido» es cómo una identidad se desalinea de su logo
118  en tres ediciones.
119- **Del tablero se toman la forma y el color; los eslóganes NO** — el operador los descartó
120  explícitamente. `brand.ftl` lleva el nombre y nada más.
121
122⚠ A 16 px el medallón pierde el detalle y queda como una mancha cálida redonda. Es intrínseco a un
123logo con cenefa, no un defecto del escalado: la salida sería un dibujo simplificado para ese tamaño,
124y eso lo decide quien dibujó el zorro.
125
126## El chrome se programa desde `atuq.cfg`, no abriendo `omni.ja`
127
128Esto era un pendiente («el chrome de verdad dentro de `omni.ja`») y resultó ser dos cosas falsas.
129Medido con `scripts/test-atuq-chrome.py` sobre `atuq b3:9e16ac35`:
130
1311. **No hace falta entrar al zip para programar el chrome.** `atuq.cfg` ya corre con privilegios —de
132   ahí sale `nsIStyleSheetService`—, así que desde ahí se observa `browser-delayed-startup-finished`
133   y se toca el `gBrowser` de CADA ventana. La sonda abre dos pestañas y las parte en dos, desde
134   `atuq.cfg`, sin tocar `omni.ja`.
135
136   Y es la vía que hay que usar, no un atajo: re-empacar `omni.ja` pelea con el orden del `jarlog`
137   del PGO —por eso el `jarlog` está aparcado—, así que **cada función nueva del chrome que NO entre
138   al zip es una que no compite con la optimización de arranque.**
139
1402. **La vista dividida ya la trae el motor, y prendida.** Firefox 154 lleva
141   `tabbrowser/tabsplitview.js` (492 líneas), `opentabs-splitview.mjs` y `split-view-footer.js`
142   dentro de `browser/omni.ja`, y `defaults/preferences/firefox.js` dice
143   `pref("browser.tabs.splitView.enabled", true)`. La sonda lo ejercita de verdad —
144   `gBrowser.addTabSplitView([t1, t2])` ⇒ `activeSplitView` puesta y `splitViewBrowsers.length == 2`
145   — así que no es «existe el fichero»: **funciona en nuestro build**. No hay nada que escribir acá.
146
147⚠ Lo que esto NO dice: atuq **todavía no lleva JS de chrome propio**. Lo medido es el GANCHO, con la
148sonda inyectada por una capa de overlay que no viaja en el artefacto. Cuando haya una función que lo
149justifique, el fichero va en `chrome/atuq.js` y lo carga `atuq.cfg` igual que la hoja de estilo.
150
151El control negativo de la sonda sale del propio motor: `addTabSplitView` documenta que si TODAS las
152pestañas están fijadas borra el wrapper y devuelve `null`. `--negative-control` las fija y exige eso.
153
154## Lo que TODAVÍA no está
155
156- **La extensión de `sct`** y el host de native messaging (SDD 26 §6.1 y §7).
157*(El §6.8 ya no tiene pendientes: que una petición hecha en «Banco» sale por el proxy de «Banco»
158está probado por `scripts/test-atuq-ruteo.py`, que abre las dos pestañas por sessionstore y mira a
159qué puerto llama el navegador. Trae control negativo.)*

README.md

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 artefactoQué hace
prefs/autoconfig.jsdefaults/pref/autoconfig.jsLe dice a Gecko que lea atuq.cfg. Es el único gancho posible antes de que arranque el perfil.
atuq.cfg<appdir>/atuq.cfgPrefs de fábrica + carga la hoja de estilo del chrome.
chrome/atuq.css<appdir>/chrome/atuq.cssEl 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.jsonPolí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.xpiPágina de inicio y pestaña nueva.
extensions/proxy/<appdir>/distribution/extensions/proxy@atuq.tawasuyu.xpiProxy 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):

escenarioqué se romperesultado
as-isnadalas dos extensiones puestas; la home es la nuestra
no-policypolicies.json sin ExtensionSettingssiguen puestas y la home sigue siendo la nuestra ⇒ instala la carpeta
policy-onlylos XPI mudados fuera de la carpeta, con la política apuntando a la ruta nuevano 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/<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:

  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.

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.

⚠ 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á