La barrida de regresión del frente (14 guardianes tras rehacer host y navegador) encontró uno en rojo: `test-atuq-descargas.py` buscaba el CAS en `<estado>/descargas-cas` y el host lo escribe en `<estado>/cas` desde el commit del archivo personal (357791a85, 2026-09-10) — el §6.3 UNIFICÓ los dos CAS, que es justo lo que hace que una página archivada y un fichero bajado con el mismo contenido sean un solo objeto. Lo renombré yo y no actualicé este guardián. Lo que importa no es el renombre: es que el guardián estuvo rojo dos días sin que nadie se enterara, porque **un guardián que no se ejecuta no protege de nada** — la misma familia que el cache-hit que congela regresiones. Los otros 13 pasan. Arreglado el path, y el README dice ahora por qué el directorio se llama `cas` y no `descargas-cas`.
396 lines
27 KiB
Markdown
396 lines
27 KiB
Markdown
# 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` | `<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: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.
|
|
|
|
## `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 ya está elegido y pineado: **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).
|
|
Lo que sigue sin decidirse es **en qué imágenes se declara**: con el motor son ~1,25 GiB por perfil.
|
|
Mientras no se declare, la imagen no lo trae y el panel lo dice.
|
|
|
|
## `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.)*
|