Commit Graph
18 Commits
Author SHA1 Message Date
Sergio 10c499777e atuq §6.5.bis: la extensión foco — muestra el foco del sistema y no tiene con qué apagarlo
La mitad del navegador del §6.5, deliberadamente asimétrica: sólo lee. Pregunta `focus.state` (nuevo
en puriy-costura, commit 38815b5f3) y pinta tres estados — `foco`, nada, y `?` cuando nadie escribió
el estado. Ese tercero 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 es la propiedad y no un pendiente: si el navegador pudiera levantar el
foco, valdría lo que vale un bloqueador de extensión. En tawasuyu hay un test que lo fija; acá el
guardián mide el EFECTO — tras una sesión entera, el fichero de estado quedó igual.

`scripts/test-atuq-foco.py` corre los tres estados sobre el path de PRODUCCIÓN (`/etc/takana/focus`),
no la escotilla de pruebas: una escotilla mide el código, no el contrato con la imagen. Verde sobre
el artefacto vigente (atuq 5d1afc50, puriy-costura 0de6b4ca), y verificado rompiéndolo — con una
extensión parcheada que pinta «foco» siempre (en una COPIA del artefacto, vía ATUQ_DIR), falla
nombrando la insignia.
2026-09-10 23:17:32 +00:00
Sergio c214438f00 atuq: el torrent lo toma un daemon propio, perezoso, que sobrevive al navegador
El 6.9 del SDD 26, con la arquitectura que pidió el operador: daemon PROPIO,
CONFIGURABLE y PEREZOSO. Vivaldi ya trae torrent y termina en una carpeta; acá lo que
baja entra al CAS —un objeto BLAKE3 con la misma identidad que una descarga del
navegador (§6.2) o un `.swm`—, y ésa es la razón por la que esto vale.

    página de la extensión → fondo → connectNative → puriy-costura
      → /usr/bin/puriy-costura-torrent add   (que LEVANTA el daemon si no está)
      → daemon: librqbit, y al completarse, el CAS

POR QUÉ NO ES UN VERBO MÁS DEL HOST — dos razones, y la primera decide:
1. una descarga tiene que sobrevivir al navegador, y Gecko mata al host cuando se
   cierra el puerto (medido hoy);
2. librqbit + tokio + rustls son 226 crates: dentro del host, ese binario —952 K,
   compartido por CINCO extensiones— pasaría a ~20 MB y cada iteración de cualquier
   función del navegador a un cuarto de hora.

⚠ Que esa pila COMPILA para musl con zig-cc se midió ANTES de decidir, con una receta
desechable: 226 crates y sella. Era una incógnita real —ninguna receta del corpus
había construido tokio+rustls desde tawasuyu— así que la decisión no fue «no se
puede», fue «no ahí». El artefacto del daemon pesa 7,7 M.

PEREZOSO EN LOS DOS SENTIDOS: no hay servicio en la imagen (el binario está y no corre
hasta que hay un torrent; lo levanta su cliente con `setsid`, sin lo cual moriría con
el navegador) y se va solo tras `inactividad_seg` sin nada activo — sembrar cuenta como
actividad.

CONFIGURABLE: TOML opcional; sin fichero anda, y uno ROTO es error. El CAS por defecto
es el del host, con un test que falla si esas raíces se separan.

LO QUE EL GUARDIÁN NO HACE, Y POR QUÉ (está en su cabecera y en el §6.9):
· no usa un `magnet:` sino un `.torrent` servido por HTTP —dar de alta un magnet
  BLOQUEA esperando metadata que sin peers no llega: se mediría un timeout y no la
  cadena—. El `.torrent` se arma en el propio guardián, en bencode a mano, porque un
  binario de prueba en el repo es una dependencia que nadie revisa;
· no pasa por el despacho de protocolos de Gecko: un clic en `magnet:` abre el diálogo
  de «¿con qué lo abro?», que en headless no contesta nadie, y eso es una elección del
  usuario y no código nuestro. Se abre la página de la extensión —la misma que el
  handler abriría— descubriendo su URL base del `dump` de una primera corrida, porque
  el UUID lo asigna Gecko por perfil y adivinarlo sería inventar.

El control negativo saca el cliente del daemon de la imagen y exige que el host lo diga
NOMBRANDO lo que falta, en vez de fingir que lo tomó.

⚠ Y sigue sin haber test automático de un transfer REAL entre peers: haría falta un
sembrador y una espera que volverían la suite una que nadie corre. Dicho en el test del
daemon y en el §6.9, para que nadie lo lea como «probado de punta a punta».

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

Del §6 quedan el foco (6.5) y la IA local (6.7), ésta bloqueada por algo medido: el
corpus no tiene ninguna receta de LLM ni de embeddings.

MEDIDO sobre `atuq b3:d1a444ad`, `puriy-costura b3:cad5c855` y
`puriy-costura-torrent b3:76afb7a8` (7,8 M):

    MAGNET http://…/prueba.torrent
    TOMADO prueba-atuq.bin · nuevo · en /salida/hogar/Downloads · id=0
    socket del daemon: run/puriy-costura-torrent.sock      ← nadie lo arrancó

⚠ Y DOS COSAS QUE EL GUARDIÁN DESTAPÓ, las dos por comprobar el NOMBRE y no sólo que
la respuesta llegara:

1. **la extensión leía claves que el daemon no manda** (`name`/`files`/`bytes` contra
   `nombre`/`carpeta`), y el síntoma era «TOMADO — 0 fichero(s), 0 bytes»: parecía un
   torrent vacío y era un vocabulario inventado del lado del lector — la misma trampa
   que había evitado en el host y no acá;
2. mirándolo apareció que **el socket y la config del daemon estaban en castellano**, y
   la regla 7 de tawasuyu nombra explícitamente las claves de configuración entre lo
   que se tipea. Corregido allá antes de que llegara a ninguna imagen: un protocolo y
   un fichero de config son contrato, y renombrarlos después rompe lo que alguien ya
   escribió.

tawasuyu: 4f36f057 (el crate) · 3f69aab2 (lock) · 3b56db26 (el verbo del host) ·
066b3761 (el log del daemon, que faltaba y hacía invisible su único fallo de arranque)
· ca9947b9 (las claves en inglés).
2026-09-10 20:45:53 +00:00
Sergio 928d5eb119 atuq: un vídeo se abre en el reproductor de la distro, no en una pestaña
El 6.6 de la unidad 9 del SDD 26. Una navegación de PRIMER NIVEL a un medio
(`Content-Type: video/*` o `audio/*`) se cancela y la URL se la lleva `mpv`, que ya viaja en las
cuatro imágenes de escritorio: esto no agrega ni una receta, es cablear lo que ya estaba.

    MEDIO http://…/audio.wav
    CANCELADA la navegación http://…/audio.wav
    ABIERTO /usr/bin/mpv pid=266 http://…/audio.wav
    mpv: AO: [null] 8000Hz mono 1ch u8        ← su propio log: DECODIFICÓ, no sólo arrancó

LA REGLA ES ESTRECHA A PROPÓSITO: 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.

FAIL-OPEN, y es lo que prueba el control negativo: cancelar es SÍNCRONO y la respuesta del host llega
después, así que no se puede saber en el momento si el reproductor arrancó. Se cancela sólo con el
puerto vivo y, si el host contesta que no pudo, la URL VUELVE al navegador con una marca para no
entrar en bucle:

    SIN REPRODUCTOR no hay reproductor en /usr/bin/mpv: esta imagen no lo trae
    VUELVE AL NAVEGADOR http://…/audio.wav?atuq-medios=no

Quedarse sin vídeo Y sin pestaña es el único resultado inaceptable, y es justo el que se consigue si
uno confía en que salió bien.

⚠ EL BUG QUE ESTE GUARDIÁN DESTAPÓ VALE MÁS QUE LA FUNCIÓN, y estaba DESPUÉS del éxito aparente: la
primera corrida detectó el medio, canceló y recibió el pid… y después el puerto murió con «Native
application tried to send a message of 546281442 bytes». **Un hijo hereda los descriptores del padre,
y los del padre SON la tubería de native messaging**: `mpv` escribía su salida ahí y el navegador la
leía como un marco. Arreglado en tawasuyu (`2e99d216`, pineado acá) con tres redirecciones, cada una
contra un fallo distinto — `stdin` a null porque mpv LEE stdin y se comería los mensajes del
navegador; `stdout` al stderr del host y no a `/dev/null`, porque apagarlo arreglaría el bug y se
llevaría puesto el diagnóstico; `stderr` heredado por lo mismo.

Y dos cosas del ARNÉS, las dos ya vistas antes en esta misma sesión:
· la URL NO se pasa por la línea de comandos: una petición del arranque compite con la
  inicialización de la extensión (la misma carrera del guardián de `sct`). El guardián navega a una
  página que redirige a los 3 s, que además es lo que hace una persona: seguir un enlace;
· la evidencia de que el reproductor CORRIÓ se le pide a su propio `log-file`, porque su salida ya no
  va al stdout del host y Gecko no vuelca el stderr del host al del navegador. Sin eso sólo se sabría
  que hubo un `spawn`.

Del §6 quedan el foco (6.5), la IA local (6.7) —que además es la que traería el motor que le falta al
archivo del §6.3— y el torrent (6.9). Medido de paso: **el corpus no tiene ninguna receta de LLM ni de
embeddings**, así que 6.7 hoy no se puede pagar.

Medido sobre `atuq b3:a7080058` y `puriy-costura b3:060314e6`.
2026-09-10 15:45:08 +00:00
Sergio 7f6459c132 atuq: el archivo personal — lo que leés queda congelado, y se encuentra
La unidad 8 del SDD 26 (§6.3), en su mitad pagable. Cada página que se lee se congela: el HTML **ya
ejecutado** —lo que la persona vio, 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, sin el navegador.

    visita 1 (página A)  ⇒ archivada, visitas=1, total=1
    visita 2 (página A)  ⇒ dedup, visitas=2, total=1     ← volver NO duplica
    visita 3 (página B)  ⇒ total=2
    «masa» → 1 de 2 · «tobera» → 1 de 2 · «masa tobera» → 0 · «helicóptero» → 0

LA IDENTIDAD ES EL BLAKE3 DEL HTML, NO LA URL, y de ahí sale lo que hace que esto valga: la misma URL
con otro contenido ES otra página, así que el archivo tiene VERSIONES de lo que leíste. Un historial
de direcciones guarda punteros, y los punteros se pudren.

⚠ LA MITAD QUE NO SE PAGA HOY, dicha en el código, en el LEEME de allá y en el §6.3.bis: preguntarle
al historial en LENGUAJE NATURAL. Eso es el registro semántico y en la suite ya tiene forma —un motor
`rag-motor::RagMotor`, como `willay-rag`—, que necesita un daemon de embeddings y un backend LLM real
y devuelve `None` cuando no están. `archive.search` es el registro LITERAL: todas las palabras tienen
que aparecer. Enseñar lo uno diciendo que es lo otro sería el peor cambio posible.

LO QUE NO SE ARCHIVA ES LA PARTE QUE HAY QUE MIRAR: nada de una ventana privada, nada fuera del marco
principal (un `<iframe>` de publicidad no es una página que alguien leyó) y nada sin texto visible (el
HTML de un visor de PDF llenaría el archivo de cosas que no se encuentran). Y si no se puede saber si
la pestaña es privada, NO se archiva.

⚠ Y EL CONTROL NEGATIVO NO SÓLO COMPRUEBA QUE NO SE ARCHIVE: DICE QUIÉN LO IMPIDIÓ. La respuesta
medida no es la que uno supondría — hoy lo impide **el navegador**, porque las extensiones no corren
en ventanas privadas salvo que se las habilite, así que nuestro `if (incognito) return` no se ejecuta
nunca. No es código muerto: es lo que haría seguro habilitar `private_browsing` el día que haga
falta. Lo que sí habría sido un error es escribir «la extensión no archiva lo privado» sin saber cuál
de las dos cosas estaba pasando.

DOS TECHOS CON NOMBRE (en el host): 4 KiB de texto por página en el índice —es JSON para poder leerse
con `cat`, y uno sin techo deja de poder— y el corte por CARÁCTER y no por byte, porque `truncate`
sobre medio multibyte entra en pánico y en un archivo personal el texto con acentos es el caso normal.

Del lado de tawasuyu (`357791a8`, pineado acá): `archive.add`/`archive.search`, 6 tests nuevos (20 en
total) y el CAS renombrado de `descargas-cas` a `cas` — el mismo almacén guarda ahora lo bajado y lo
leído, y un nombre que describe la mitad de su contenido es el que se lee mal dentro de seis meses.

Medido sobre `atuq b3:5cfe3221` y `puriy-costura b3:c06f55aa`.
2026-09-10 14:58:50 +00:00
Sergio 897fad178a atuq: una descarga deja de ser un archivo con nombre y pasa a ser un objeto con identidad
La unidad 7 del SDD 26 (§6.2). Cuando una descarga TERMINA, la extensión
`descargas@atuq.tawasuyu` 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 — que es exactamente lo
que ningún navegador sabe hacer: para todos una descarga es un blob con nombre, y por eso la bajás
dos veces y no te enterás.

No es un almacén nuevo: es `arje-cas`, el mismo formato y el mismo hash que usan arje, takana y
tejido. El `<hex>` del CAS ES el `expected_hash` de un `.swm`.

⚠ TRES DECISIONES QUE SALIERON DE MEDIR, NO DE DISEÑAR:

1. **La raíz del CAS de descargas 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 (`arje-brain`, `GcCas`) lo arma con la
   cadena de audit y las raíces vivas del grafo. Una descarga del usuario no está en ninguno: el
   primer GC se la llevaría, en silencio. Van a `<estado>/descargas-cas` — mismo formato (mover un
   objeto al CAS del sistema es un `rename`), fuera del alcance del GC de otro. El precio queda
   escrito: tejido sirve el CAS por defecto, así que compartir una descarga hoy exige apuntarlo ahí.
2. **El fichero del usuario no se toca**: se copia y se deja donde estaba. El guardián lo comprueba
   byte a byte. Borrar o mover lo que alguien acaba de bajar no es decisión de un navegador.
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 lo que se consigue no metiéndose.

Y EL HALLAZGO QUE COSTÓ LA TARDE, que no es de esta unidad: **en `--headless` el navegador se cae con
SIGSEGV en cuanto una descarga termina.** Se atribuyó con dos controles antes de tocar nada:

    --headless, con la extensión      → baja el fichero, TERMINADA, Segmentation fault (139)
    --headless, SIN la extensión      → baja el fichero, Segmentation fault (139)   ← no es nuestro
    --headless, panel de descargas apagado → Segmentation fault (139)
    sway headless (compositor REAL)   → baja, INGIERE al CAS, y no se cae          ← no es del producto

Sin el primer control esto se leía como «la extensión de descargas rompe el navegador» (perseguir un
bug que no existe); sin el último, como «atuq no puede descargar» (reportar un bug de producto que
tampoco existe). Por eso `scripts/test-atuq-descargas.py` corre sobre sway y no sobre `--headless`, y
por eso lo dice en su cabecera: un arnés distinto al de los demás guardianes, sin explicación, es una
invitación a «simplificarlo» de vuelta al que se cae. Queda en el §6.10.bis del SDD, que es donde vive
lo que atuq NO puede hacer.

El guardián baja de verdad (`Content-Disposition: attachment`, no una llamada a la API) dos veces
dentro de un mismo compositor, y trae control negativo: con OTRO contenido la segunda vez exige
`dedup=false` y dos objetos. Sin él, una sonda que dijera «ya lo tenías» siempre se vería idéntica a
una que funciona.

Del lado de tawasuyu (`ffa939c6`, pineado acá): `cas.ingest`/`cas.list` en el host y
`arje-cas::almacenar_fichero_en` — ingesta en streaming, porque `store` toma `&[u8]` y una ISO de
4 GiB serían 4 GiB de `Vec`. 5 tests nuevos en arje-cas y 5 en puriy-costura.

Y EL BUG QUE EL GUARDIÁN DESTAPÓ, que vale más que la función: **hay un host por PUERTO, no uno por
perfil.** Con `sct` y `descargas` hablando hay dos procesos vivos a la vez, cada uno con su copia en
memoria del estado, y cada uno escribía el fichero ENTERO al guardar: el de descargas borraba el
registro TOFU de `sct` al salir, y el de `sct` revertía el índice de descargas. Los dos contestaban
bien — el daño estaba sólo en el disco. Se vio porque el guardián lee el índice EN DISCO en vez de
creerle a la respuesta: decía `veces=2` y el fichero decía 1. Arreglado en tawasuyu (`55b918e8`,
pineado acá): cada proceso escribe sólo la parte que tocó, y el test que lo fija se comprobó
ROMPIENDO el arreglo a propósito — porque la primera versión de ese test abría los hosts en secuencia
y pasaba con el bug puesto.
2026-09-10 14:16:32 +00:00
Sergio 02513b861f atuq: sct v1 — el navegador avisa cuando un sitio ya estable ejecuta código que nadie vio nunca
La unidad 6 del SDD 26, que es el diferenciador del §6 que no tiene ningún navegador. Cadena entera,
medida de punta a punta con un servidor HTTP real y seis cargas de página:

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

    carga 1-3 (mismo script)          fase=learning  eventos=0   insignia vacía
    carga 4   (mismo script)          fase=stable    eventos=0   insignia vacía
    carga 5   (mismo script, estable) fase=stable    eventos=0   insignia vacía   ← control
    carga 6   (script CAMBIADO)       fase=stable    eventos=1   insignia "1"
              EVENTO ext:…/app.js 0ff4771fb797→f7ccb9fedfbd +27B

La extensión NO hashea ni guarda nada: ve bytes y pregunta. El registro es `puriy-sct`, 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.

CINCO COSAS QUE SE MIDIERON EN VEZ DE SUPONERSE, y las cinco fallan calladas:

1. el manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/` y NO en el appdir: la ruta sale de
   `XRESysNativeManifests`, un `/usr/lib/mozilla` COMPILADO dentro de Gecko;
2. **el manifiesto no puede llevar argumentos** — `NativeMessaging.sys.mjs` hace
   `command = manifest.path` y los únicos argumentos son `[ruta-del-manifiesto, id]`. Y sin `--state`
   el host corre en MEMORIA: cada arranque volvería a «aprendiendo» y nada alertaría nunca. De ahí el
   lanzador `bin/puriy-costura-host`, que además decide la ruta del estado — dónde vive el estado de
   un usuario es layout del FHS, o sea asunto de la distro y no del crate;
3. `filterResponseData` y el permiso `webRequestFilterResponse` SÍ están en nuestro `omni.ja`
   (se le preguntó al artefacto, no a la documentación de Mozilla);
4. los scripts `inline` NO se ven por esta vía —`filterResponseData` entrega el cuerpo de una
   PETICIÓN— y para v1 alcanza: el ataque que sct nombra es la sustitución en el CDN;
5. ⚠ **una carga de página puede producir dos peticiones 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 conocer su código real,
   y entonces alerta por churn legítimo — el falso positivo que la spec de puriy-sct pide evitar por
   encima de todo. Ahora agrupa por documento (dos pestañas con la misma url cuentan UNA: es el error
   seguro, tarda más en proteger y no alerta de más) y el guardián VIGILA el invariante «una carga,
   una visita», así que si vuelve, falla ruidoso.

DOS CORRECCIONES DEL PROPIO §6.1, que decía «consulta al testigo antes de dejarla pasar»: el cable
del testigo es un POST con postcard, así que un JS no puede ser su cliente; y v1 OBSERVA Y AVISA, no
bloquea — es lo que puriy-sct dice de su propia v1, y poner un viaje 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 SE MIDE, NO SE SUPONE: la extensión relee la insignia con `getBadgeText` después de ponerla,
y el guardián exige vacía en las cinco cargas sin novedad y "1" en la del script cambiado. Es la única
parte de la cadena que el usuario ve; dejarla en «se llamó a la API» era dejar sin medir el final.

CONTROL NEGATIVO: `--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.

Y el aviso pasivo es decisión, no falta de tiempo: insignia y tooltip, no modal. Un modal por cada
despliegue de un sitio entrena a la gente a cerrarlo sin leer, y entonces el que importa también se
cierra.

Además: `rebrand.py` instala y CRUZA los manifiestos nativos (que el `path` exista y sea ejecutable
dentro del artefacto, y que sus `allowed_extensions` sean extensiones que de verdad empaquetamos), y
de paso se corrige el comentario del §4.ter que repetía la afirmación falsa sobre quién instala las
extensiones — lo mide `scripts/test-atuq-instalacion.py`: instala el escaneo de la carpeta.

`runtime = [..., "puriy-costura"]` en atuq.toml: sin eso la imagen llevaría manifiesto y extensión y
el host NO estaría, y la función se apagaría sola sin una línea de error. `yupana radio` confirma que
llega a las cuatro imágenes de escritorio.

⚠ Límite escrito en `fondo.js`, en el `lib.rs` del host y en los dos LEEME: se hashea el TEXTO ya
decodificado que entrega la extensión, no los bytes que sirvió el servidor. Vale para comparar dos
cargas nuestras; NO es comparable con el hash que publique un tercero sobre los bytes servidos, ni
con el de la v2, que engancha el script loader y ve los bytes reales.

Y una segunda cosa que el guardián encontró y que es del PRODUCTO, no del test: **la página que abre
el navegador al lanzarse puede no ser observada** — compite con la inicialización de la extensión, y
la carrera se gana o se pierde según la corrida. En uso real sólo afecta a esa primera página (después
la extensión ya está escuchando). Por eso las aserciones van sobre la SECUENCIA OBSERVADA y no sobre
un calendario: se exige que ninguna carga se observe dos veces, que la única que puede faltar sea la
del arranque, y que la secuencia aprender→estabilizar→no-alertar→alertar sea la correcta.

Sin regresiones: `test-atuq-politica.py` («guardianes: todos correctos», y sus cinco roturas siguen
matando el build con tres extensiones) y `test-atuq-inicio.py` (la home y la pestaña nueva siguen
siendo las nuestras) pasan sobre el artefacto final `b3:d3ced586`.
2026-09-10 01:57:07 +00:00
Sergio 97c35d67b3 atuq: dos cosas que el README daba por ciertas eran falsas, y las dos se midieron
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).
2026-09-09 23:15:09 +00:00
SergioandClaude Opus 5 a44fc4f06a atuq: AV1 no reproducía, y la causa era UN elemento de LD_LIBRARY_PATH
Gecko SE COME EL PRIMER ELEMENTO de `LD_LIBRARY_PATH` al lanzar el proceso RDD
—el que decodifica vídeo—. El lanzador ponía `/usr/lib/atuq` una sola vez, o sea
primero, así que el RDD arrancaba sin él, no encontraba `libmozavcodec.so` /
`libmozavutil.so` (el ffvpx bundleado, donde vive dav1d) y anotaba:

    PlatformDecoderModule  FFVPX: Link result: NoProvidedLib

El navegador NO falla ahí: se cae al ffmpeg del sistema, que cubre
H.264/AAC/VP8/VP9/MP3/FLAC/Opus. Pero AV1 por software NO lo cubre —el
decodificador `av1` de ffmpeg es sólo hwaccel y nuestra receta va sin dav1d—, así
que un vídeo AV1 se quedaba en `readyState=1` para siempre, sin un error, sin un
NEEDED faltante y sin una cadena ausente. Media función apagada en silencio, que
es la forma de fallo de esta casa.

Tres corridas que sólo cambian esa variable, con el mismo artefacto:

    LD=/usr/lib/atuq:/usr/lib:/lib            → RDD NoProvidedLib   AV1 ✗
    LD=/usr/lib/atuq:/usr/lib/atuq:/usr/lib   → RDD Success         AV1 ✓
    LD=/relleno:/usr/lib/atuq:/usr/lib        → RDD Success         AV1 ✓

El arreglo es repetir el appdir. Feo y correcto mientras no haya `patchelf` en el
corpus para grabar `RUNPATH=$ORIGIN`, que borraría la variable entera.

Y el guardián que sale de este punto ciego, `scripts/test-atuq-codecs.sh`: abre
cinco muestras versionadas en `scripts/fixtures/codecs/` y mira si `currentTime`
AVANZA — «se creó el decodificador» no es «decodifica». El veredicto sale por
`dump()` al stdout, así que no necesita ni red ni servidor, y corre headless para
que sirva en el worker. Con `--negative-control` se saltea el lanzador y EXIGE
que AV1 falle: probado en los dos sentidos, 5/5 y control ✓.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 15:38:02 +00:00
SergioandClaude Opus 5 2e093f9cab atuq §6.8: probado que el paquete sale por el proxy del contenedor — y con control negativo
Era lo único que la v0.5 dejó sin probar, y las tres puertas que había
encontrado siguen cerradas: no hay flag de línea de comandos para el
userContextId, Marionette no toma `-remote-allow-system-access` en la jaula, y
`tabs.create({cookieStoreId})` exige el permiso `cookies` — que no se le da a la
extensión del proxy sólo para que pueda probarse a sí misma.

La cuarta puerta estaba abierta: el fichero de SESIÓN guarda el userContextId de
cada pestaña y Gecko lo restaura. Se fabrica a mano — `mozLz40\0` + tamaño + un
bloque LZ4, y un bloque de sólo literales es LZ4 válido: veinte líneas, sin
librería.

Y la medición no le pregunta nada al navegador: le pone dos oídos en la red y
mira a cuál llama. Dos pestañas piden la MISMA url, una sin contenedor y otra en
«Banco»:

    al destino (8099)  GET /directo   <- la de sin contenedor, directa
    al proxy   (9099)  \x05\x01\x00   <- saludo SOCKS5 de la de «Banco»

Ese saludo sólo aparece si Gecko decidió hablar con un proxy para esa petición, y
el único que se lo pudo indicar es proxy.onRequest mirando el cookieStoreId. La
pestaña sin contenedor no es decorativa: sin ella, «todo fue por el proxy» y «el
ruteo anda» se verían iguales.

CONTROL NEGATIVO, que es lo que este repo se exige desde hoy: con
`--negative-control` no se configura el proxy y se exige lo contrario — las dos
pestañas directas y nadie llamando al proxy. Corrido: pasa. La única diferencia
entre las dos corridas es una línea de configuración y el observable se da vuelta
entero. Sin ese modo, una prueba que se hubiera vuelto ciega se vería idéntica a
una que funciona.

Tres detalles sin los cuales la prueba mide un silencio y se lee como fallo:
restore_on_demand=false, allow_hijacking_localhost=true, y el
triggeringPrincipal_base64 en cada entrada de sesión.

El artefacto se resuelve por `hammer hash` y no por glob, que es la lección de
hammer-03 de esta madrugada: con dos artefactos de la misma receta en el store,
`ls | head -1` es una ruleta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-06 23:47:08 +00:00
SergioandClaude Opus 5 c5ebbda933 atuq v0.5: proxy por contenedor — el primer diferenciador del §6 que se paga entero
Es el único de la lista del SDD 26 §6 que no pasa por el host de native
messaging del §7: es API de Firefox y nada más. Cada contenedor —Personal,
Trabajo, Banco, Compras— puede salir por su propio proxy.

Tres piezas y ninguna alcanza sola: `atuq.cfg` prende los contenedores (vienen
apagados), `policies.json` los CREA con `Containers.Default` —el único
mecanismo que los pone en un perfil NUEVO— y deja la config de fábrica en
`3rdparty.Extensions`, y `extensions/proxy/` los enruta con `proxy.onRequest`,
que es lo único que ve el `cookieStoreId` de la petición.

Las cuatro se comprobaron DENTRO del artefacto de firefox antes de escribir una
línea, no en la documentación de Mozilla: `Containers` y `3rdparty` en el
`policies-schema.json` de browser/omni.ja, `cookieStoreId` en el
`schemas/proxy.json` de omni.ja, y `storage.managed` leyendo
`getExtensionPolicy(id)` en ext-storage.js. Es la regla del §2.sexies: la
pregunta no es si Mozilla lo tiene, es si NUESTRO build lo tiene.

Tres decisiones que valen más que el código:

1. La config se indexa 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.
2. FAIL CLOSED. Un contenedor con proxy configurado que no se pudo honrar no
   sale directo: va a un destino cerrado y el navegador muestra el error. Salir
   directo sería una fuga silenciosa — la misma familia que el artefacto vacío
   de la regla 3, el fallo que llega hasta el final diciendo que todo fue bien.
3. `proxyDNS` PRENDIDO por defecto: sin él la consulta DNS sale por la línea que
   se quería evitar. Es la fuga clásica de esta configuración.

Y lo que no promete está arriba de todo en la página de opciones, no en un pie:
separación de tráfico, NO anonimato; para anonimato, Tor Browser. El §4 cumplido
donde el usuario lo lee.

De paso, una verdad que estaba escrita en dos sitios pasa a tener un dueño: el
id de cada extensión sale ahora del `manifest.json` y no de una constante de
rebrand.py, el nombre del XPI se deriva de él, y el `install_url` de la política
se cruza contra la ruta donde el fichero quedó escrito de verdad. El icono se
inyecta desde branding/icons/ en vez de estar copiado byte a byte dentro de cada
extensión. Agregar una tercera extensión es ahora un directorio.

PROBADO, corriendo el árbol en la misma jaula que atuq-nested.sh:
  · captura de about:preferences#containers con los cuatro contenedores y sus
    iconos, más el containers.json del perfil;
  · extensions.json del perfil nombra las dos extensiones;
  · `console.info: "atuq/proxy: 4 contenedor(es) enrutado(s)"` — leyó la config
    de fábrica por storage.managed Y la casó con los contenedores de la política;
  · scripts/test-atuq-politica.py: cinco formas de desincronizar política y XPI,
    las cinco matan el build, y el control con la política intacta pasa.

NO probado y dicho por su nombre: que una petición hecha en «Banco» salga por el
proxy de «Banco». Pide automatizar la UI y queda pendiente.

Dos obstáculos del método, que valen para la próxima. La consola de una
extensión es CONTENIDO: `devtools.console.stdout.chrome` (que viene en true) no
la incluye, hace falta `...stdout.content`. Y un `moz-extension://` NO se abre
desde la línea de comandos —muere con `NS_NOINTERFACE [nsIFileURL.file]` y abre
la home en su lugar—, además de que `--screenshot` dispara al `load`, que puede
ocurrir antes de que arranquen las extensiones. Eso último destapó un fallo real
y arreglado: la página de opciones confundía «no hay contenedores» con «la API
no está» y mostraba un mensaje FALSO.

Verificado contra firefox b3:352d7880; se reconstruye contra el firefox con
RLBox cuando selle. La otra mitad de esta unidad —el rename de
recipes/atuq/extension/ a extensions/inicio/— entró sin querer en ff0b556, que
es de otro frente.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-06 01:57:44 +00:00
Sergio ff0b5567d6 firefox: RLBox ENCENDIDO — --without-wasm-sandboxed-libraries -> --with-wasi-sysroot
Cierra la unidad 3.b del SDD 26. El flag que se va apagaba la jaula wasm que
envuelve a los parsers de fuentes y medios (graphite, ogg, expat, woff2), o sea
el código que come entrada NO confiable de la red. Estaba apagado por una razón
honesta —«el SDK de WASI no está en ninguna cola»— que dejó de ser cierta hace
un commit: la cadena wasi-libc-headers -> wasi-compiler-rt -> wasi-libc ->
wasi-sdk sella entera.

El flag nuevo es LITERALMENTE el de Alpine, verificado hoy trayendo su mozconfig
de community/firefox: `--with-wasi-sysroot=/usr/share/wasi-sysroot`, que es la
misma ruta donde instala nuestro wasi-libc.

Las deps van las TRES y no es redundancia: en hammer las deps NO son
transitivas —el bwrap apila como overlay sólo lo que está escrito en la lista—
así que declarar sólo `wasi-sdk` no arrastraría al `wasi-libc` que necesita, y
ése es el peor fallo posible de esta cadena: el sysroot no existiría y el error
saldría horas después, hablando de un header de wasm sin nombrar a ninguna
receta. Alpine declara wasi-compiler-rt + wasi-sdk porque su wasi-sdk YA trae el
sysroot dentro; el nuestro está partido en cuatro, así que la lista es distinta
a propósito.

Esto re-hashea firefox: b3:352d7880 -> b3:dba8fbf9. Es un rebuild largo y va a
la máquina que corresponde, no a gioser (4c/7,6 G sin swap).
2026-09-06 01:32:05 +00:00
Sergio 2660063f99 atuq v0.4: el branding que se VE estaba en las imágenes del zip, no en el texto
Capturé la pantalla con `grim` y por fin miré lo que el usuario miraba. El diagnóstico cambió
entero: la ventana SÍ estaba —el título decía «New Tab — atuq» y el icono del lanzador era el
zorro— pero la PÁGINA mostraba el logo y la palabra «Nightly».

O sea que la v0.2 rebrandeó lo que se lee y no lo que se ve. El branding de Gecko vive en TRES
sitios y sólo uno es texto FTL:

  · `brand.ftl`        — las cadenas nuevas (título de ventana, menús). Ya estaba.
  · `brand.properties` — las cadenas VIEJAS, formato heredado. Seguía diciendo Nightly, y nadie lo
                         habría notado leyendo la receta: son 71 bytes en otra ruta del mismo zip.
  · las imágenes de `chrome/browser/content/branding/` — TODO lo visible: el favicon de las páginas
                         internas (ese orbe azul en la pestaña), el logo de about:, y el «wordmark»,
                         que es una IMAGEN con la palabra dibujada, no un texto traducible.

Se reemplazan 10 imágenes + las dos tablas de cadenas + los dos wordmarks. Cada imagen va con LAS
MISMAS DIMENSIONES que la que sustituye (192, 384, 300x236, 16/32/48/64/128): son tamaños que el CSS
del chrome da por sentados, y meter otro tamaño descoloca la página en vez de rebrandearla. El
`about.png` (300x236, no cuadrado) lleva el medallón ENCAJADO y centrado, no estirado.

El wordmark conserva el `viewBox` exacto del original (0 0 372 99) y `fill="context-fill"`, que es
la convención de Gecko para que el color lo ponga quien lo usa — así sigue el tema claro/oscuro solo
en vez de imponer un color.

La variante «private» usa el mismo medallón a propósito: inventar una segunda marca para un modo del
navegador es ruido, y ese modo ya se distingue por su propio chrome.

MÉTODO, que es lo que más vale de esta vuelta: `grim` está en el corpus (incoming-wlr), se hidrata
en el mismo rootfs y captura la pantalla real desde dentro de la jaula. Dejé de decir «mirá tu
pantalla» y pasé a mirarla yo.
2026-09-05 16:45:58 +00:00
Sergio 1dc662c48d atuq v0.3: página de inicio y pestaña nueva — y la firma de complementos venía COMPILADA
La página de inicio va en una extensión de sistema (`inicio@atuq.tawasuyu`) y no en una pref, porque
la pestaña NUEVA no se puede redirigir con prefs desde hace años: el único mecanismo soportado es
`chrome_url_overrides.newtab`. La misma pieza resuelve la home con `chrome_settings_overrides`.

La página: el medallón, el nombre y una barra que decide sola. Si lo tecleado PARECE una dirección,
navega; si no, BUSCA CON EL MOTOR POR DEFECTO DEL USUARIO vía `browser.search.query`. No se cablea
ningún buscador: una distro que hornea su motor en la página de inicio está tomando por el usuario
la única decisión que esa página debería respetar. Y dice cuál es el motor, para que la búsqueda no
sea una caja negra.

DOS MECANISMOS MUERTOS Y UNO VIVO, TODOS MEDIDOS:
- `distribution/extensions/` (el clásico de las distros) NO instala nada: Firefox retiró el
  sideloading. El `extensions.json` del perfil ni lo mencionaba y el log no decía una palabra —
  fallo perfectamente silencioso.
- `policies.json` con `ExtensionSettings` SÍ se lee (`browser.policies.applied=true` en el perfil) y
  falla con un error que al menos se nombra: `ERROR_SIGNEDSTATE_REQUIRED`.
- Y `xpinstall.signatures.required=false` TAMPOCO alcanza.

PARA SABER SI EL AUTOCONFIG SIQUIERA CORRÍA, HIZO FALTA UN TESTIGO. `defaultPref` no deja rastro en
`prefs.js` (sólo se guarda lo que difiere del default), así que un `.cfg` que no se ejecuta es
INDISTINGUIBLE de uno que sí y no hace nada. `atuq.cfg` ahora escribe `atuq.autoconfig.ok` como pref
de USUARIO, legible desde fuera sin abrir el navegador. Salió `true` ⇒ el .cfg corre, y la pref de
firma se estaba ignorando: la exigencia viene COMPILADA. El default de `MOZ_REQUIRE_SIGNING` sale
del MILESTONE (154.0 es release), no del canal — el nuestro es `default` y aun así estaba activa.

De ahí `export MOZ_REQUIRE_SIGNING=0` en firefox.toml, con su precio escrito: este Firefox deja de
exigir la firma de Mozilla para CUALQUIER complemento. La alternativa es firmar en AMO —cuenta y
revisión de Mozilla por versión—, que es la dependencia externa que esta distro existe para no
tener. Y no es un desvío: sin esto, el `sct` del SDD 26 §6.1 tampoco se podría shipear nunca.

La prueba se hizo SIN reconstruir: se montó el `.cfg` con el testigo por encima del artefacto con
`--ro-bind`, en vez de editar un hardlink del store (que habría corrompido el artefacto sellado).
2026-09-05 15:11:37 +00:00
Sergio 082246da84 atuq: la marca de verdad — el zorro en medallón, y la paleta muestreada del propio logo
El operador pasó un tablero de identidad: zorro en medallón con cenefa andina, disco teal, anillo
noche. Se adapta la FORMA y el COLOR. Los eslóganes NO — los descartó explícitamente, y de todos
modos `brand.ftl` no es sitio para copy: lleva el nombre y nada más.

EL ICONO DEJA DE DIBUJARSE EN CÓDIGO. Hasta la v0.2 `rebrand.py` generaba una marca geométrica en
Python puro, con el argumento de que un PNG en el árbol no se puede revisar en un diff. Ese
argumento valía mientras el icono era un marcador de posición inventado por falta de arte. Ya hay
arte, así que el criterio se invierte: un asset de diseño ES la fuente, no un derivado. Lo que se
conserva es la disciplina — los seis tamaños salen del MISMO master (recorte del medallón con
máscara circular y alfa, LANCZOS desde 1024 px), se generan una vez y se copian, así que dos builds
ponen los mismos bytes.

LA PALETA SE MIDIÓ, NO SE ESTIMÓ. Los cuatro tokens salen del color más frecuente de cada zona del
medallón: #c15728 el zorro, #1b6470 el disco, #04161d el anillo, #f5ecde los reflejos. Escribirlos
«parecidos» a ojo es cómo una identidad se desalinea de su propio logo en tres ediciones. Con eso el
CSS suma `accent-color` —un solo cambio alcanza decenas de widgets nativos del chrome sin enumerar
ninguno— y el fondo de la barra de pestañas verticales, para que marco e icono cuenten lo mismo.

⚠ Anotado y no escondido: 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; la salida sería un dibujo simplificado para ese tamaño
y lo decide quien dibujó el zorro, no este script.

Construido y sellado EN LOCAL en 3 segundos (b3:97328d1c), hidratado y abierto: 8 procesos vivos,
anunciándose como `(atuq:9581)`.
2026-09-05 14:42:18 +00:00
Sergio 5d06a3b83d atuq ABRE: el app_id estaba compilado en el binario, y el árbol copiado llegaba de sólo lectura
Con la cadena GTK3 arreglada, atuq arrancó de verdad: parent vivo, CERO procesos de contenido
muertos, y pintando una página local. Quedaban dos cosas.

1. SE ANUNCIABA COMO `firefox-default`. El application.ini decía RemotingName=atuq y aun así el
   proceso se presentaba así — porque esa cadena es un MOZ_APP_REMOTINGNAME horneado en el ELF, y es
   la que Gecko pasa a g_set_prgname(), o sea el **app_id de Wayland**. Consecuencia visible: el
   escritorio no casaba la ventana con nuestro .desktop (StartupWMClass=atuq) ⇒ icono genérico y sin
   agrupar.

   Se parchea EN SITIO, no recompilando: cambiarlo en recipes/firefox.toml brandearía la BASE como
   atuq, y firefox tiene que seguir siendo firefox para los otros forks. Es seguro porque es una
   cadena C terminada en NUL en el pool de .rodata —`firefox-default\0` son 16 bytes exactos y se
   escriben 16—, y se EXIGE una única ocurrencia: si upstream la duplica o la renombra, falla en vez
   de parchear el sitio equivocado. Verificado después: 0 ocurrencias de la vieja, y el log dice
   `(atuq:26423)`.

2. UN BUG QUE SÓLO SE VE FUERA DE ROOT. `cp -a` preserva los modos y los ficheros del store están
   sellados sin permiso de escritura; todo lo que viene después los modifica. En el worker pasaba
   inadvertido porque corre como root, que ignora los bits. El primer build local murió con
   `PermissionError: /out/usr/lib/atuq/application.ini`. Se arregla con `chmod -R u+w` en la receta y
   no en el entorno: un artefacto no debe depender de con qué uid lo construiste.

Y el runner pasa a hidratar las variantes `-shared`: en runtime una `.a` no sirve de nada, y son las
que gtk3 declara NEEDED desde el arreglo del cuadro de las dos Pango.

Este atuq se construyó EN LOCAL en segundos, que era la promesa entera del diseño derivado del
SDD 26: iterar el envoltorio sin volver a pagar un build de Gecko.
2026-09-05 14:31:35 +00:00
Sergio 0f7d19232e atuq: el lanzador deja de ser un symlink, y un runner para abrirlo en la pantalla que ya tenés
Dos cosas que salieron de intentar ABRIRLO, que es lo único que distingue «sellado» de «usable».

1. EL LANZADOR. Con `/usr/bin/atuq` como symlink, el navegador moría antes de pintar:

     XPCOMGlueLoad error for file /usr/lib/atuq/libmozsandbox.so:
     Error loading shared library libnspr4.so: No such file or directory

   El motor carga sus propias librerías desde `/usr/lib/atuq` y ni el binario ni esas `.so` traen
   RPATH/RUNPATH — comprobado con `readelf -d`, no supuesto. Pasa a ser un script que exporta
   `LD_LIBRARY_PATH` (lo mismo que hacen Debian y Fedora) y hace `exec`, para que el proceso que
   queda sea el motor y `/proc/self/exe` siga resolviendo el appdir. El `LD_LIBRARY_PATH` además
   tiene que HEREDARSE: Firefox lanza un proceso por pestaña y todos cargan las mismas librerías.
   La alternativa limpia es grabar RPATH=$ORIGIN con `patchelf` —es lo que hace Alpine— pero
   `patchelf` todavía no existe como receta del corpus; cuando exista, esto vuelve a ser un symlink.

2. `scripts/atuq-nested.sh` — hidrata el cierre de runtime y abre atuq como ventana anidada en el
   compositor que ya está delante (waypipe, mirada, sway). Trae dos cosas aprendidas a golpes:

   · EL ROOTFS VA EN EL MISMO MOUNT QUE EL STORE. `hydrate` proyecta con hardlinks y `linkat()`
     rechaza cruzar un punto de montaje aunque sea el mismo filesystem. Acá el store es /dev/sdb
     bind-monteado y `work/` vive en /dev/sdc: hidratar a `work/…` muere con «Invalid cross-device
     link (os error 18)». El volumen entero está en /mnt/cosecha, así que el rootfs va ahí. Se
     comprueba con `findmnt -T`, nunca con `stat -c %d`.
   · `dejavu-fonts` va en las raíces por necesidad, no por completismo: un navegador sin una sola
     fuente arranca, pinta y muestra cuadraditos. Ya nos costó una tarde en GNOME.

   Y no saltea en silencio: si falta un artefacto de la lista, sale con error en vez de armar un
   rootfs al que le faltan tres paquetes y falla tres capas más abajo.
2026-09-05 09:53:33 +00:00
Sergio 6f6c32bb9d atuq 0.2.0: branding real — el zip, el .ini, los binarios y un icono que no es de otro
Hash b3:7eff4ba4. La v0.1 dejó el branding fuera porque había que MIRAR el árbol antes de adivinar.
Se miró, y lo que apareció decidió la forma de esta versión:

- `application.ini` del appdir arranca diciendo «This file is not used». Es herencia del árbol de
  desarrollo: en un build empaquetado el launcher SÍ lo lee. `rebrand.py` no le cree al comentario:
  comprueba que las claves estén y falla si no.
- Las cadenas que la gente VE están DENTRO de `browser/omni.ja` ⇒ sin re-empacar el zip no hay
  branding. Y el original no dice «Firefox», dice **Nightly**, porque construimos con
  `--with-branding=unofficial`. Sin tocarlo, atuq se presentaría como lo peor de los dos mundos.
- Los ICONOS, en cambio, viven FUERA del zip ⇒ se reemplazan sin abrirlo.

EL RE-EMPAQUE PRESERVA TRES PROPIEDADES DEL ORIGINAL, Y CADA UNA POR UNA RAZÓN MEDIDA:
orden de las 5306 entradas (es el que Gecko lee al arrancar, y es lo que el jarlog del PGO va a
refinar), `compress_type=0` (Mozilla lo deja SIN COMPRIMIR para poder mapearlo) y `date_time`
2010-01-01 (ya venía normalizado: Mozilla también persigue reproducibilidad).

VERIFICADO CONTRA EL ARTEFACTO, NO CONTRA EL LOG:
- diff entrada por entrada entre el omni.ja de firefox y el de atuq: **5306 → 5306, mismo orden,
  todo STORED, mismas fechas, y UNA SOLA entrada con contenido distinto**: brand.ftl.
- re-empacar dos veces el mismo zip da el mismo sha256 ⇒ el paso es determinista.
- application.ini: Vendor=tawasuyu, Name/RemotingName/CodeName=atuq. `ID` NO se toca: es el GUID
  con el que las extensiones declaran compatibilidad, y cambiarlo dejaría a atuq fuera del
  ecosistema de complementos.

EL BuildID SE DERIVA DEL CONTENIDO DEL OVERLAY, no de la fecha. Es la clave con la que Gecko
invalida su startup cache: si atuq cambia su chrome y el BuildID no se mueve, el navegador arranca
con la interfaz vieja cacheada y PARECE que el overlay no agarró. Derivarlo del contenido lo mueve
exactamente cuando hace falta y dos builds del mismo atuq dan el mismo número — con la fecha
pasaría lo contrario en los dos sentidos. Se vio funcionar: al sumar `CodeName` el BuildID cambió
solo, de 81921491609672 a 24128696592342.

El icono se DIBUJA en código (PNG en Python puro, zlib + struct) y no se commitea como binario:
un PNG en el árbol no se puede revisar en un diff, el código sí, y el resultado es idéntico en cada
build. ⚠ Es un marcador de posición declarado —una marca geométrica plana, 5 tamaños—; lo correcto
es que alguien dibuje el zorro. Su única función irrenunciable ya la cumple: que la ventana y el
lanzador NO muestren el icono de otro producto.

Binarios: firefox → atuq y firefox-bin → atuq-bin, con symlink `firefox` → `atuq` dentro del appdir
porque hay scripts de terceros que invocan por el nombre histórico y Gecko resuelve su directorio
por /proc/self/exe (entrar por el symlink resuelve al mismo sitio). Más `.desktop` con
StartupWMClass=atuq, que casa con el app_id que da RemotingName bajo Wayland.
2026-09-05 08:49:46 +00:00
Sergio aeebb194dc atuq 0.1.0: la receta del envoltorio, derivada de firefox
Primera receta del navegador de la distro (SDD 26). No compila nada: depende de `firefox`, copia su
árbol y le pone encima cuatro ficheros. Hash b3:2d6e4dcf.

LA CAPA SON CUATRO FICHEROS Y CADA UNO ESTÁ DONDE ESTÁ POR UNA RAZÓN:
- `defaults/pref/autoconfig.js` — el único gancho que corre ANTES de que exista un perfil.
- `atuq.cfg` — prefs de fábrica + carga del chrome. Con `defaultPref` y no `lockPref`: son valores
  de arranque, no una cárcel; lo que de verdad se bloquea va en policies.json.
- `chrome/atuq.css` — el aspecto.
- `distribution/policies.json` — telemetría, updates, primer arranque.

POR QUÉ EL CSS NO ES `userChrome.css`: ese fichero vive en el PERFIL y exige que el usuario prenda
`toolkit.legacyUserProfileCustomizations.stylesheets`. atuq tiene que verse como atuq en el primer
arranque, con un perfil recién creado, sin que nadie prenda nada. El único gancho a nivel de
APLICACIÓN es nsIStyleSheetService desde autoconfig — y llegar a él es la razón de
`sandbox_enabled = false`. Se registra como USER_SHEET, el mismo nivel de cascada que userChrome,
para que el usuario pueda seguir pisándolo.

DOS TRAMPAS DEL FORMATO, ESCRITAS EN EL PROPIO FICHERO:
- la PRIMERA LÍNEA de un autoconfig se ignora siempre, por diseño (herencia de Netscape). Código en
  la línea 1 no corre y no da error: el fallo más caro de ese fichero es el que no dice nada.
- el CSS va acotado con @-moz-document a browser.xhtml. Una hoja USER sin acotar alcanza CUALQUIER
  documento chrome —visor de PDF, inspector, diálogos— y un selector como `toolbar` termina pintando
  ventanas que nadie miró.

`extensions.autoDisableScopes = 0` es la otra mitad del `--with-unsigned-addon-scopes` que ya viaja
en el build de firefox: sin los dos, las extensiones de la distro se instalan DESACTIVADAS.

LAS ASERCIONES VAN PRIMERO Y FALLAN RUIDOSAS. Un derivado que no encuentra su base produciría un
directorio con cuatro ficheros de config, `Store::has` lo daría por presente y el fallo aparecería
el día que alguien abra el navegador (regla 3 del CLAUDE.md). Y al final imprime el inventario de la
capa con sus tamaños, que es la primera pregunta del diagnóstico cuando «atuq se ve como Firefox».

LO QUE LA v0.1 NO HACE, A PROPÓSITO: no renombra el binario ni toca application.ini, y no
re-empaqueta omni.ja. Las dos cosas necesitan mirar la forma real del árbol que selle `firefox`, y
adivinarla desde acá daría un artefacto que arranca en una máquina y en ninguna otra.

No se puede construir todavía: su dep `firefox` está en vuelo.
2026-09-05 04:12:55 +00:00