4a2801331696245af7df3cccbbac5da68360bf98
59
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4a28013316 |
atuq §6.3: la búsqueda semántica, VERDE en la jaula — y la barra lateral tiene las dos preguntas
Los cuatro guardianes pasan sobre los artefactos sellados (atuq bfc14c92, puriy-costura 3f31233e, ia-modelo-embeddings 2c0c4258): semántico 0.6252 gato / 0.2471 red / 0.0973 pan → y la otra pregunta gana la otra página control «no hay modelo de embeddings en …» y NINGÚN orden inventado ia el modelo contesta y el motor se va con el navegador (0 vivos) foco los tres estados, y el estado intacto tras la sesión La barra lateral ahora tiene dos botones: «Al modelo» (genera texto) y «A mis páginas» (ordena lo que ya leíste). No se mezclan a propósito — una inventa y la otra recuerda, y juntas sería imposible saber cuál contestó. Los resultados van EN ORDEN y sin porcentaje: el puntaje es un coseno y leerlo como «85 % de acierto» sería inventarle un significado. ⚠ Y el guardián nació midiendo NADA: metía las tres páginas en `<iframe>` y archivó cero, porque el §6.3 ignora lo que no es marco principal. Encadenadas como navegación de verdad entran las tres; y las páginas de tránsito van sin texto visible para que el archivo las descarte y la evidencia no liste coincidencias sin título. Dos cosas más del camino, las dos medidas: el `cp` del install buscaba el nombre de upstream y el tar —nuestro— lleva el fichero con nombre corto; y el build murió dos veces por DISCO LLENO (0 bytes en /mnt/vvv), no por el lock. `scripts/poda-fuentes.sh --horas 6` liberó 4,6 G, que es exactamente para lo que existe. |
||
|
|
8cdaceb3d6 | SDD 26 §6.4: los tres bloqueos de qullqa, medidos — y el primero es que el kernel no trae FUSE | ||
|
|
f123bc99cc | SDD 26 §6.3.ter: lo medido fuera de la jaula (3/3) y lo que todavía espera el lock | ||
|
|
c78c5ff91f |
ia: el modelo de embeddings elegido NO servía — Qwen3-Embedding-0.6B en su lugar, y un guardián que lo habría cazado
multilingual-e5-small sellaba, cargaba, contestaba 384 dimensiones y 45 tests en verde. Y con el modelo de verdad, de punta a punta, el ranking devolvía SIEMPRE la misma página. Seis pasos descartando hipótesis —batching, posición, nuestro código, la cuantización, la conversión— hasta que `/tokenize` lo dijo en una línea: «cortafuegos», «minino», «duerme» y «tejado» van todos al id 100 = `<unk>`. Un vocabulario XLM-RoBERTa por esta ruta deja casi todo en desconocido, y un texto que es todo `<unk>` embebe igual que cualquier otro. La pista estaba a la vista desde el principio: `gato~perro` y `gato~cortafuegos` daban el mismo número a CUATRO DECIMALES. En su lugar, Qwen3-Embedding-0.6B Q8_0, GGUF oficial de Qwen (Apache-2.0): tokeniza español de verdad (`cort|af|uegos`), acierta 3/3 con márgenes anchos (0,649 contra 0,237), y es de la misma familia que el modelo de chat. Cuesta 610 MiB en vez de 126: es el precio de que funcione, y sube la cuenta de la imagen a ~1,85 GiB si se declara. ⚠ Y LA PARTE QUE IMPORTA PARA LA PRÓXIMA VEZ: el guardián del `install` ya no mira sólo el mágico y el tamaño —«existe» no es «sirve»—. Ahora tokeniza dos frases en español sin palabras en común y exige que no compartan tokens (con el e5 roto compartían la mitad, todos `<unk>`), y después levanta el servidor de verdad y exige que un gato se parezca más a un perro que a un cortafuegos. Con esos dos chequeos el e5 no habría sellado nunca. El tar del modelo roto se borró del mirror (476 MB) y del disco. ⚠ El build está en la cola del flock detrás de otro agente (pixi, 22 min y contando), así que el artefacto todavía no está sellado y los guardianes del navegador no corrieron. Lo medido hasta acá: el camino completo host→motor→índice con el modelo nuevo, 3/3 (`archive.ask` de punta a punta, fuera de la jaula), más 45 tests en tawasuyu. |
||
|
|
0ca88fdcd5 |
atuq §6.3.ter: preguntarle al archivo por lo que DECÍA, no por las palabras exactas
El muro no era un daemon ni un LLM — y el propio comentario de la extensión lo decía mal, copiando lo que hace willay-rag. Medido: para ordenar por parecido no hace falta ningún LLM (eso es un coseno) y el «daemon de embeddings» resultó ser el mismo llama-server que ya levanta el chat, con otro modelo. La corrección quedó escrita donde estaba la afirmación. Del lado de la suite (tawasuyu 22527f7d1 y b4ecffea8): el protocolo de llama-server salió a `shared/foreign-llama` (regla 4 — vivía dentro de puriy-costura y ya tenía dos consumidores), el `Provider` es `rimay-verbo-llama` (regla 10 — la familia verbo YA es la abstracción de embeddings, y éste es su primer backend sin nube ni descargas), y el índice es `rimay-verbo-index::VectorIndex`. Acá: la receta del modelo (multilingual-e5-small, MIT), que se pinea en fp32 y la receta CUANTIZA a Q8_0 con nuestro llama-quantize — así la procedencia es de quien declara la licencia, la imagen se lleva 126 MB en vez de 476, y la transformación es nuestra y verificable. Más el guardián y la extensión, que ahora expone `archive.ask` al lado del `archive.search` literal: son dos preguntas distintas y conviven. ⚠ El guardián está escrito pero NO corrió todavía: `ia-modelo-embeddings` quedó en la cola del flock detrás del build de otro agente. Lo medido hasta acá es el modelo a mano (384 dimensiones, el pasaje correcto gana con y sin los prefijos de e5) y 45 tests en tawasuyu. La línea del SDD que lo dice se borra cuando dé verde. |
||
|
|
54bf3e810e |
ia: el modelo de chat, pineado — Qwen2.5-1.5B-Instruct Q4_K_M (Apache-2.0)
Elegido por tres cosas, y las tres medidas antes de pinearlo: licencia Apache-2.0 (lo que una distro puede shipear sin letra chica, al revés que Llama-3.2 o Gemma), habla español —se le preguntó qué es una distribución de GNU/Linux y contestó dos frases correctas— y corre en CPU: 20,1 tokens/s en el hub sin GPU, 1,04 GiB. El objeto pineado es un tar que envuelve el .gguf, publicado en el mirror y servido por sha256: takana extrae toda fuente con `tar` y un GGUF pelado no lo es. Es el camino de firefox-pgo-profile. La receta anota el sha256 del GGUF DE UPSTREAM (el lfs.oid de HuggingFace, verificado al bajarlo) y no sólo el del tar nuestro, para que nadie tenga que confiar en nuestro tar. Round-trip verificado: apartados el caché y el artefacto, el build lo bajó del mirror y selló el mismo ArtifactHash. Guardián en la receta, porque el fallo es callado: un fichero truncado o un HTML de error renombrado a .gguf se instala igual y sella en verde. Se comprueban el mágico GGUF y el tamaño. ⚠ Y el modelo de verdad destapó una CARRERA en el guardián del §6.7: el censo de motores contaba en el instante del cierre — con el de juguete daba 0 y con el de la imagen daba 1, que se lee como fuga cuando en realidad matar un proceso con un giga mapeado tarda ~1 s. Ahora espera hasta 15 s y anota cuánto tardó. La rotura a propósito sigue fallando, ahora con el tiempo a la vista. Falta decidir en qué imágenes se declara (con el motor son ~1,25 GiB por perfil) y pinear el de embeddings (multilingual-e5-small) para la mitad semántica del §6.3. |
||
|
|
a14b62436c |
atuq §6.7.bis: la barra lateral pregunta, y contesta un modelo de ESTA máquina
Inferencia REAL de punta a punta, en una jaula sin red: panel → fondo → host → `llama-server` (el llama-cpp del corpus) → respuesta. El servidor lo levanta el host en la primera pregunta —no hay servicio de IA en la imagen— y **se muere con el navegador**, al revés que el daemon del torrent (§6.9): un torrent tiene trabajo que sobrevive; medio giga de modelo en RAM, no. El puerto nativo vive en el FONDO y no en la página: si viviera en la página, cerrar la barra lateral se llevaría el host y el modelo cargado con él. `scripts/test-atuq-ia.py` mide tres cosas: que la respuesta salga del modelo de la imagen, que no venga VACÍA, y que al cerrarse el navegador queden cero `llama-server`. Control negativo: sin modelo aparece la causa y NO hay respuesta inventada. ⚠ Ese censo nació roto y del género que este repo colecciona: `pgrep -c` no existe en busybox y el `|| echo 0` convertía el error en un cero — o sea en un verde. Con un motor vivo a propósito el guardián pasaba igual. Ahora cuenta leyendo /proc y la rotura falla como debe. Y el PRIMER intento de romperlo tampoco rompía nada: el motor de mentira moría al hacer bind porque el socket estaba en /salida, compartido entre las dos corridas. Queda UNA decisión, no trabajo: qué modelo se pinea (tamaño de imagen y licencia). Hasta entonces la imagen no trae modelo y el panel lo dice con todas las letras. |
||
|
|
bcd1cdefaf |
SDD 26 §6.7: el motor de inferencia local, y la corrección al propio documento
La tabla del §6 le ponía costo «bajo» a la IA local porque `rimay`/`iniy` parecían alcanzar. Se miró antes de empezar y no alcanzan: los backends de `pluma-llm` son todos de nube, y `rimay-verbo-fastembed` descarga onnxruntime (glibc) y el modelo de HuggingFace en el primer arranque. Queda escrito, porque es lo que evita que alguien vuelva a estimarlo en «bajo». Y de paso junta dos filas que el plan llevaba separadas: el §6.7 y la mitad semántica del §6.3 no eran dos problemas — era que el corpus no tenía con qué correr un modelo. Un solo muro. Queda documentado el hallazgo caro, con las dos líneas de cmake enfrentadas: SOURCE_DATE_EPOCH —que exportamos para que los builds REPRODUZCAN— apagaba INS_ENB y con él las seis perillas de ISA de ggml, sellando un motor de inferencia con CERO instrucciones vectoriales que corría igual. Tabla 0/0/0 vs 42.356/1.298/86, medida sobre artefactos sellados. Y las dos trampas del arnés que van a volver cuando se pinee el modelo de producción: `/health` contesta 503 mientras carga, y un GGUF sin tipo de pooling hace que el endpoint compatible con OpenAI conteste 400 — un error que parece del cliente. ⚠ Escrito también lo que esto NO es: el motor no es la función. Falta el modelo (fuente pineada, como el perfil de PGO), los verbos del host y quién levanta el servidor. Y `llama-cpp` no se declara en ningún perfil todavía: una imagen no crece 199 M por una función que aún no existe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqBhowvFe3aiieGCKgvxwa |
||
|
|
7c4617aeb2 | SDD 26: fila 10 del plan — el foco (6.5) con su mitad medida y lo que falta nombrado | ||
|
|
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. |
||
|
|
212b304b60 |
atuq §6.5: el foco por cgroup MEDIDO — y corregido el propio documento
La fila de la tabla se leía como «bloquear sitios». El cortafuegos NO sabe de sitios: su política de egress tiene UNA dimensión, qué cgroup sale, y ninguna de destino (leído en UnidadRed, no supuesto). El foco que estas piezas dan es «el navegador no sale», con todo lo local andando — promesa más honesta, además: una lista de dominios se esquiva con un espejo; un cgroup sin egress no. Medido con nuestro propio nft, en el LXC donde somos root: linea-base exit=0 · control exit=0 · foco exit=1 Tres medidas y no una: la línea base porque un harness roto se lee igual que un foco que funciona, y el control porque un reglaset que niega de más tampoco se distingue. `--broken-rules` abre egress al cgroup en foco y el guardián falla nombrándolo (sale 1). Verificado en los dos sentidos. Restricciones reales que salieron de medir: crear un cgroup pide root (EPERM incluso en un userns con CAP_ALL), y `nft -c` no es sintaxis — resuelve el path del cgroup contra la máquina viva. ⚠ Y lo que rompí en el camino, documentado: `/sys/fs/cgroup` está montado SHARED, así que desmontar la copia de un --rbind se propaga al montaje real. Dejé al worker sin cgroup2 dos veces, en silencio. Remontado y jerarquía intacta. Ahora todo va en un mount namespace propio (--propagation private), sin `umount -l` antes de un rm -rf, y con /proc/mounts consultado antes de borrar. Tres falsos positivos más que cazó el harness, todos con cara de éxito: un gmp viejo tomado por `ls store/*-gmp` (sin .so ⇒ nada conectaba, y el lab del hub lo tapaba), /sbin fuera del PATH del chroot (sin `ip`, loopback caído), y un COMENTARIO que se ejecutó — backticks dentro de un heredoc sin comillas corrieron `ip`/`ifconfig` en el host y pegaron su salida en el guión generado. |
||
|
|
b98c46b530 |
atuq §6.9: el guardián del torrent dejaba un daemon suelto y decía ✓
Encontrado con `ps` sobre la máquina, no por un test: al cerrar el sandbox quedaban vivos el daemon y su `bwrap` interno, sosteniendo overlays ya borrados. Es correcto que el daemon se quede —su torrent de prueba no tiene enjambre, así que nunca termina y nunca está «sin nada»—, lo que faltaba era despedirlo y MEDIR que se fue. - el guión de adentro le pide `puriy-costura-torrent stop` (el verbo del producto); - la huella del socket se anota adentro y ANTES del stop: el daemon lo borra al irse; - el guardián censa daemons antes/después con `ps -C` (nunca `pkill -f`) y falla nombrando el PID; el `finally` barre lo propio para que un test rojo no deje basura; - tope de 180 s al sandbox, como seguro contra un cuelgue. Comprobado en los dos sentidos con una copia rota fuera del repo: sin el `stop` el guardián daba ✓ igual, y con la aserción nueva sale ✗ nombrando el PID. La hipótesis de que se COLGARÍA era falsa: el `bwrap` externo vuelve; el que espera es el interno. Verde: positivo, control negativo y rotura a propósito. |
||
|
|
78f1c6a771 |
atuq: re-pin del daemon con el arreglo del inmortal, y la lección en el SDD
El §6.9 apunta ahora a tawasuyu `3bd1440d` (`b3:27c7b73a`), que arregla un daemon que
quedaba VIVO PARA SIEMPRE cuando su sesión de torrent no arrancaba: el bucle hacía
`let Ok(g) = gestor() else { continue }` y el `continue` saltaba la evaluación de
«¿sobro?».
Y lo que queda escrito en el §6.9 es CÓMO se encontró, porque es lo reutilizable: no lo
encontró ningún test sino mirar `ps` después de las pruebas —un daemon con 23 minutos y
`idle_seconds = 300`—. Los tests cubrían la decisión como función PURA, y esa función
estaba bien: el que no llegaba a llamarla era el bucle. **Una decisión correcta que
nadie toma se ve igual que una que no existe.**
Los dos guardianes vuelven a pasar sobre los artefactos vigentes (`atuq b3:d1a444ad`,
`puriy-costura b3:cad5c855`, `puriy-costura-torrent b3:27c7b73a`): el positivo con
`TOMADO prueba-atuq.bin` y el socket que nadie arrancó, y el control negativo nombrando
lo que falta.
|
||
|
|
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).
|
||
|
|
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`.
|
||
|
|
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`.
|
||
|
|
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. |
||
|
|
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`.
|
||
|
|
9f476ddd70 |
SDD 26: la unidad 5 está hecha — el host, y por qué la extensión no puede hablarle al testigo
Cierra la fila 5 del plan con lo que hay: `puriy-costura` + `shared/foreign-webext` en tawasuyu (`ebe41e96`, 14 tests) y `recipes/puriy-costura.toml` acá (`b3:3b633431`, verificado como artefacto: contesta un marco de 4 bytes y hace la promesa del §6.1 completa). El §7.quater deja tres cosas que no estaban escritas: · POR QUÉ HACÍA FALTA el host y no se podía atajar: el §6.1 decía que la extensión «consulta al testigo», y el cable del testigo es un POST con postcard, no JSON — un JS no puede ser su cliente. Reimplementar el TOFU en la extensión sería el sustituto paralelo que la regla 10 de tawasuyu prohíbe, además de duplicar la única pieza ya certificada. · CÓMO SE ELIGIÓ EL NOMBRE, con el grep de la regla 10 hecho ANTES: ningún término del dominio significaba esto, y los que suenan a mensajero están ocupados por dominios ajenos y grandes (chasqui = type broker, chaka = puente a COBOL, paloma = correo, tampu = común de objetos). El nombre salió del título de este propio §7: la costura. · LOS DOS LÍMITES, que van en el código y en los LEEME y no en una nota de sesión: se hashea el texto ya decodificado (vale para detectar, no es comparable con el hash de un tercero ni con el de la v2), y la semilla del log sale del directorio de estado ⇒ a prueba de reescritura, no de suplantación. Y lo medido para la unidad 6 preguntándole al artefacto: `filterResponseData` y el permiso `webRequestFilterResponse` SÍ están en nuestro omni.ja ⇒ la extensión podrá leer el cuerpo de un script. Los inline NO se ven por esa vía, y para v1 está bien: el ataque que sct nombra es la sustitución en el CDN, o sea el caso `<script src>`. |
||
|
|
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). |
||
|
|
17f8f4fb80 |
takana: la ruta del repo es /mnt/vvv/takana
El directorio se movio de verdad, asi que las referencias absolutas dentro del repo ya eran incorrectas. Tambien los ejemplos que usaban ~/hammer. NO se tocan docs/evidencia/: son REGISTRO de lo que se corrio ese dia, y reescribir una ruta ahi adentro falsifica la evidencia. Que nombren una ruta que ya no existe es correcto: existia cuando se midio. |
||
|
|
97ceb72411 |
takana etapa 5b: los 59 docs de diseño, runbooks y ADR
645 líneas. Los ADR entran porque en este repo SON documentos vivos, no registros inmutables: el 0013 tiene 5 commits, el 0009 dos. Eso se comprobó antes de decidir, no se asumió por convención general. EXCLUIDOS por ser REGISTRO o generado: docs/evidencia/ (6), el HANDOFF de la noche de KDE (1) y docs/state/ (24, se regenera solo). Reescribir un comando dentro de una evidencia la falsifica. Y el ADR 0016 se excluye de todo barrido, con un aviso adentro para el próximo que barra: habla SOBRE el renombre, así que necesita seguir diciendo 'hammer'. El barrido se lo llevó puesto y lo dejó titulado 'Renombre del sistema: takana → takana'; revertido. Congelados, verificados uno por uno con controles: /opt/hammer, /var/lib/hammer, /usr/bin/hammer, /mnt/vvv/hammer, la URL de gitea, hammer-farm.service, hammer-live-install.sh, BRIEFING-hammer.md, hammerd, hammer-recover y HAMMER_LIVE. |
||
|
|
8ff6ae7b3a |
pgo: el control que faltaba — el arranque eran 16 s y diluía todos los porcentajes
El banco medía el ciclo COMPLETO del proceso (arrancar → renderizar → capturar → salir) y nunca se midió cuánto de eso era arranque. Con una página vacía como control, y las 4 cargas × 2 variantes en UNA sola sesión intercalada (restar entre corridas de días distintos no vale: la misma variante deriva ~1%): maquetación trabajo 7281 → 4744 ms -34,8% (publicado: -10,9%) carga ajena trabajo 4550 → 4634 ms +1,8% = cero, y es buena señal SunSpider trabajo -34 → -6 ms bajo el ruido Dos correcciones al documento: 1. El -10,9% es correcto para el ciclo completo pero se leía como 'firefox 11% más rápido'. Sobre el trabajo de la página el PGO rinde -34,8%: tres veces más. El error subestimaba el propio resultado. 2. La explicación que di del SunSpider era aire. Su trabajo mide -34 ms, NEGATIVO: la página con el benchmark tardó menos que la vacía. No había nada que medir, y yo le colgué encima una teoría sobre el JIT. La teoría puede ser cierta; esta medición nunca la probó. Las tablas viejas se dejan sin retocar, con un aviso arriba: el error de método enseña más que el número corregido. Todo el rigor estadístico estaba puesto sobre una cantidad que no era la que yo creía medir, y ninguna repetición lo habría revelado — sólo un control, que costó siete corridas de una página vacía. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 |
||
|
|
c3413d293e |
jarlog APARCADO: rompe atuq, y el coste está medido mientras el beneficio no
Al reconstruir atuq sobre el firefox con jarlog, murió:
zipfile.BadZipFile: Bad magic number for central directory (rebrand.py)
El jarlog convierte el omni.ja al formato «jar optimizado» de Mozilla, que mueve
el directorio central al principio:
sin jarlog: PK\003\004 ZIP estándar — zipfile lo abre, 5306 entradas
con jarlog: \376\204#\0 zipfile lo rechaza
Y atuq reempaqueta el omni.ja con zipfile para su branding. O sea que el jarlog
rompe el navegador propio de la distro.
La cuenta es asimétrica: el COSTE está medido y el BENEFICIO no —el banco corre
con caché caliente, donde reordenar el omni.ja no ahorra ninguna lectura, y dio
-0,1%, por debajo del suelo de ruido del propio banco—. Cambiar algo que
funciona por una ganancia que no se pudo medir, rompiendo algo que sí
funcionaba, es mal negocio.
Revertido a los hashes YA construidos y medidos (perfil 3647c6be, firefox
3d199174, atuq fab2fbfb): cero reconstrucciones. La documentación va fuera de
los campos hasheados, verificado antes y después.
Se retoma cuando (1) se mida el arranque EN FRÍO, que es su terreno, y (2)
rebrand.py sepa leer el jar optimizado o des-optimizarlo antes. El blob
92497cdd del mirror ya trae el jarlog: retomarlo es cambiar una línea.
Y LA LECCIÓN, que es la que más vale: el jarlog parecía GRATIS. «Es lo que hace
upstream y no cuesta nada» lo escribí yo en este mismo documento hace unas
horas. El coste no apareció midiendo el jarlog sino CONSTRUYENDO LO QUE DEPENDÍA
DE ÉL. Una función que se declara gratuita sin haber reconstruido a sus
consumidores no es gratuita: es no medida.
|
||
|
|
92aacc6a5f |
SDD 26: el jarlog puesto — probado en el artefacto, no medible en el banco
sin PGO v1 v2 v3 (= v2 + jarlog) DOM/maquetación 23.008 20.851 20.527 20.502 (-10,9%) SunSpider 16.006 15.859 15.869 15.838 (-1,0%) jarlog solo: -0,1% y -0,2% Indistinguible de cero, y el argumento no es que los rangos se solapen —que se solapan— sino que la MISMA variante varía ~1% entre corridas de días distintos (v1 dio 20.851 hoy y 21.050 ayer). El suelo de ruido del banco es diez veces el efecto buscado. No es un fallo del jarlog: el banco corre con la caché de página CALIENTE, y ahí reordenar omni.ja no ahorra ninguna lectura. Su terreno es el arranque en frío, que este instrumento no reproduce. Lo que SÍ está probado es que la función existe, y del lado del artefacto: libxul IDÉNTICO byte a byte entre v2 y v3, omni.ja distinto en 26 bytes. El jarlog tocó exactamente lo que debía y nada más. Se conserva porque es lo que hace upstream y no cuesta nada; lo que no se afirma es una ganancia no medida. Y no hizo falta la extensión Quitter, que este mismo documento daba por bloqueante: profileserver.py sólo traduce JARLOG_FILE a MOZ_JAR_LOG_FILE. Anotado además: los dos atípicos del banco son 120.034 y 120.030 ms, o sea exactamente el timeout del arnés. No son ruido de carga sino corridas COLGADAS al arrancar en headless, 2 de 56 (~3,5%). |
||
|
|
6b469a81d2 |
SDD 26: el corpus ampliado rinde — de -9,6% a -11,4% en maquetación
sin PGO v1 (36 pág) v2 (46 pág) DOM/maquetación 23.285 ms 21.050 (-9,6%) 20.640 (-11,4%) SunSpider 3d-raytrace 16.003 ms 15.846 (-1,0%) 15.857 (-0,9%) La mejora en maquetación NO se afirma por las medianas sino por la separación: SEIS de las siete muestras de v1 son más lentas que TODAS las de v2; sólo una cae dentro del rango de v2. En SunSpider v1 y v2 son INDISTINGUIBLES —los valores se entrelazan por completo—, que es exactamente lo esperable: ese camino lo ejecuta el JIT y el PGO no lo toca. Un corpus mejor no puede mejorar lo que el PGO no alcanza. El -0,9% frente al -1,0% no dice que v2 sea peor ahí: dice que no se distingue. Una de las siete corridas de v2 dio 120.029 ms contra ~20.600 de las otras seis. La mediana lo ignora por diseño; una media lo habría convertido en «v2 es catastróficamente peor». Fue la razón de elegir mediana antes de ver un número. Y lo que el banco no puede afirmar, escrito: las 10 páginas nuevas ejercitan los mismos subsistemas que la página de medición, así que el parentesco con lo que ahora se entrena es mayor que antes. El -11,4% es real para ESTA carga; generalizarlo a «cualquier página» sería el mismo error que cometía el corpus de Mozilla, en la otra dirección. |
||
|
|
b8865560c0 |
SDD 26 §8: la unidad 4.e deja de ser una afirmación y pasa a estar medida
`scripts/test-atuq-inicio.py` le pregunta al motor por sus overrides en vez de mirar el XPI en el disco: moz-extension://…/inicio.html en las dos, contra about:home / about:newtab cuando se le saca el XPI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
1bebeab818 |
SDD 26 §7.bis: el camino de native messaging existe, con su sonda y su control
Antes de escribir el crate del host en tawasuyu había que saber si el mecanismo funciona en nuestro build. Funciona, y el cuadro deja las cuatro cosas que hacen falta para escribirlo: dónde va el manifiesto (/usr/lib/mozilla, no el appdir), qué instala de verdad la extensión (el escaneo de distribution/extensions, NO el install_url de la política), que el host tiene que ser un proceso largo, y que el único error que nombra la causa es el del puerto de connectNative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
e31a36d5a4 |
SDD 26 §6.10: la cadena de notificaciones, probada desde la página web
Con `--via-atuq` el emisor es `new Notification(...)` dentro del navegador, o sea las cinco piezas: libxul dlopeando libnotify.so.4 (invisible para cualquier auditor de ELF), D-Bus, la activación y dunst dibujando. Veredicto por el `onshow` del motor: MOSTRADA #33 con el .service, «ERROR al mostrar» sin él. Y deja probado de paso que en el proceso de atuq la GLib NO se duplica — usa la cadena `-shared` que arrastra su GTK3, la misma contra la que enlaza libnotify.so.4. El que la duplicaba era dunstify. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
e98362925a |
SDD 26 §6.10: la mitad de sway queda cerrada, con la evidencia al lado
`dunst` 1.12.2 atiende `org.freedesktop.Notifications` en el cuarto escritorio. La prueba no es que la receta selle: el bus ACTIVA al daemon y el daemon DIBUJA —14.832 píxeles cambiados con el .service puesto, 0 sin él—. Y queda anotada la media función que apareció de paso: dunstify segfaultea por las dos GLib (estática del corpus + compartida que arrastra libnotify.so.4), o sea que en este corpus quién enlaza qué GLib es parte del contrato. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
acccbc0a2c |
SDD 26: la ganancia del PGO, medida — y sale al revés de lo esperable
DOM+layout+strings, FUERA del corpus: 23.540 -> 21.327 ms -9,4% SunSpider 3d-raytrace, DENTRO: 16.034 -> 15.878 ms -1,0% Mismo rootfs, mismo script, corridas intercaladas A/B/A/B, mediana de 7, calentamiento descartado; lo único que cambia entre variantes es un --ro-bind de /usr/lib/firefox. Los rangos no se solapan en ninguna de las dos, así que ambas diferencias son reales y no ruido. Yo esperaba lo contrario: que medir sobre el conjunto de ENTRENAMIENTO inflara la ganancia. Da el número MÁS BAJO, y la razón es mejor que la predicción: 3d-raytrace es aritmética pura en un bucle caliente, y ese bucle no lo ejecuta el C++ de SpiderMonkey sino código máquina que el JIT genera en runtime. El PGO optimiza el intérprete, el GC y el propio JIT — no lo que el JIT emite. Un benchmark JIT-bound es casi ciego al PGO por construcción. Donde se ve es en DOM/layout/arranque, que es C++ de punta a punta, y que además es lo que el usuario percibe: la medición cubre el ciclo completo del proceso (arrancar, renderizar, capturar, salir), no el régimen de una página cargada. Corolario anotado: el corpus de entrenamiento está sesgado hacia JS justo donde el PGO menos rinde. Un corpus con más maquetación probablemente daría más. Es su propia unidad de trabajo. |
||
|
|
361a47cfc4 |
SDD 26: cae la última frase del §6.10 que el §6.11 desmintió, y la unidad 4.f al plan
«El más caro para el usuario es ffmpeg: sin él, un sitio que sirva H.264 no reproduce» — el hueco más caro del mapa no existía. De los cuatro «sin receta» quedan tres y ninguno es un códec. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
6a1343d2d2 |
SDD 26 §6.11: la pregunta era «¿reproduce?», y la respuesta destapó dos errores y un bug
El §6.10 preguntó «¿qué NO puede hacer?» y contestó leyendo cadenas de sonames. Media pregunta. Al hacer la otra mitad —¿se ve el vídeo?— salieron tres cosas: DOS ERRORES DEL MAPA, los dos por leer una etiqueta en vez de medir: - «no hay receta de ffmpeg» era falso: la hay, sellada, y ya viajaba en la clausura de los cuatro escritorios por `mpv`. Lo que faltaba era la lista de raíces del runner. - «VA-API sin receta» también: `libva` está y está en las cuatro imágenes. El hueco es real pero es el DRIVER — las tres mesa van `-Dgallium-va=disabled`. Y queda anotado que ahora el mapa ya no lo puede ver, porque la librería presente silencia el aviso sin encender la función. UN BUG REAL: AV1 no reproducía. Gecko se come el primer elemento de `LD_LIBRARY_PATH` al lanzar el RDD ⇒ sin appdir ⇒ ffvpx no carga ⇒ se cae al ffmpeg del sistema, que no trae AV1 por software. Sin error, sin NEEDED faltante, sin cadena ausente: `readyState=1` para siempre. Y lo que quedó MEDIDO, por el lanzador de verdad y headless: H.264+AAC, VP9+Opus, AV1, MP3 y FLAC reproducen, con el tiempo avanzando y no con «se creó el decodificador» — que en AV1 se creaba igual y no entregaba un cuadro. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
fe95998675 |
SDD 26: el firefox con PGO reproduce bit a bit — unidad 3.a cerrada
verificar-repro.sh: REPRODUCEN 1, DERIVA 0, NO-DETERMINISMO 0. Era el riesgo real de esta unidad, no el rendimiento. El perfil es no determinista por naturaleza; congelarlo como fuente pineada era el ARGUMENTO de por qué el build seguiría siendo reproducible, y esto es la MEDICIÓN. Si -fprofile-use hubiera metido cualquier decisión dependiente del orden o del timing, habríamos cambiado velocidad por el invariante del proyecto. |
||
|
|
024ebc52d2 |
SDD 26 §3.quinquies: el PGO encendido y los tres muros que el plan no tenía
El §3.ter daba dos muros —el display y el no-determinismo del perfil— y los dos
eran ciertos. Aparecieron tres más, ninguno visible sin construir:
1. libclang_rt.profile.a no existía: el lab trae clang pero NINGUNA runtime de
compiler-rt. El instrumentado murió en el minuto 38:56.
2. Los sandboxes de Firefox matan la corrida de perfilado con signal 11: el
proceso que renderiza no nace, 0 GET, y el .profraw que queda es el del
padre arrancando. Un perfil de nada, con todo en verde.
3. El worker no puede bajar del mirror: tiene mirror-env.sh pero no la clave.
Y queda escrito cómo se verificó que el PGO llegó, que NO pudo ser como RLBox.
Ahí el artefacto delata la jaula (símbolos w2c_*, cero -> 634). Acá el
discriminante análogo —las secciones .text.hot que clang emite al particionar
por temperatura— NO SIRVE: con lld y ThinLTO el enlazador las fusiona y dan cero
con PGO y sin él. La evidencia válida está un nivel bajo el configure: 189
invocaciones del compilador con -fprofile-use, llvm-profdata encontrado, cero
avisos de perfil que no cuadra, y libxul creciendo 1,7 MB.
Es evidencia de BUILD y no de ARTEFACTO, y conviene decirlo así en vez de
presentarla como si fuera lo mismo.
|
||
|
|
570aa1ae5b |
SDD 26 §6.10: me equivoqué — las tres «baratas» no lo son, y libnotify está a medias en sway
Ayer escribí que tres de los siete huecos eran «promoción y no autoría» porque
libsecret, libcanberra y vulkan-loader ya tienen receta sellada en colas
incoming. Fui a declararlas en los perfiles y lo comprobé antes: en los TRES
casos falta la otra mitad.
vulkan-loader las TRES mesa construyen con -Dvulkan-drivers= VACÍO
-> un cargador sin un solo ICD no es WebGPU
libsecret no hay receta de gnome-keyring ni de ningún servicio de
secretos -> firefox hablaría a org.freedesktop.secrets y no
habría nadie
libcanberra no hay receta de sound-theme-freedesktop -> carga y no suena
Declararlas habría subido el número de paquetes de la imagen sin encender una
sola función, que es la peor clase de verde. Una librería es MEDIA función; la
otra mitad es quien la atiende.
La misma vara sobre libnotify, que sí declaré ayer, medida perfil por perfil:
plasma-workspace en kde, gnome-shell en gnome, cosmic-notifications en cosmic —
completa en tres— y NADIE en sway. Ahí queda a medias y ahora está escrito cuál
es.
Y una trampa anotada para quien vaya a cerrarla: el nombre `mako` YA ESTÁ OCUPADO
en el corpus por el motor de plantillas de Python que usa Mesa para su codegen.
No es el daemon de wlroots. Escribir recipes/mako.toml para el daemon pisaría una
receta viva de la cadena de mesa — la colisión de homónimos que la memoria del
proyecto ya tiene anotada. Me lo comí yo: fui a ver si `mako` estaba sellado, dijo
que sí, y por poco lo cuento como «el daemon ya está».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
6886b1541c |
libnotify: el primer hueco que el mapa del §6.10 encontró Y cerró
`libxul.so` lleva la cadena `libnotify.so.4` adentro y la abre por dlopen cuando
una página pide permiso para notificar. Nadie en el corpus la proveía, así que la
función quedaba apagada SIN UN SOLO MENSAJE: el navegador arranca, la web pide
notificaciones, y no pasa nada. No lo veía ninguna herramienta porque no hay
NEEDED en ningún ELF — es el tercer escalón, y sobrevivió a vigia-sonames con los
cinco perfiles en CERO.
Receta nueva, 0.8.8, SÓLO en variante compartida y eso no es un olvido: a un
`dlopen("libnotify.so.4")` una `.a` no le sirve de nada, así que una receta
estática sería un artefacto que nadie puede consumir.
Dos cosas que la receta se comió y quedan escritas:
1. `libpng-shared` hace falta porque las deps de hammer NO son transitivas: el
`.pc` de gdk-pixbuf-2.0 declara `Requires: libpng` y el meson muere con un
mensaje que nombra a gdk-pixbuf —que sí está— en vez de a lo que falta.
2. El guardián informaba «18 bytes» y parecía una librería vacía: `stat -c%s` no
sigue el symlink, y `libnotify.so.4` apunta a `libnotify.so.4.0.0`. El
artefacto estaba bien y el MENSAJE mentía. Con `-L` son 162.928 bytes. Se
arregla el mensaje porque es lo que alguien va a leer a las tres de la mañana,
y de paso el guardián exige el fichero real, no sólo el nombre.
⚠ Y lo que NO arregla, escrito en la receta para que nadie lea de más: libnotify
no trae daemon, manda org.freedesktop.Notifications por D-Bus. Tenerla resuelve
la mitad —que firefox la encuentre—; la otra mitad es que en la imagen haya
alguien escuchando ese nombre.
Verificado con el propio mapa: `--dlopen` pasa de 25 huecos a 24 y libnotify.so.4
desaparece; el soname viejo `.so.1` se reclasifica de «hueco» a «ruido», que es lo
que ahora es. El guardián normal sigue en CERO con un soname más pedido (66).
Queda pendiente la membresía de perfil en `docs/state/targets.toml`, que es
catálogo compartido y no lo toco sin decidirlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
7a81b6480c |
atuq §6.10: el tercer escalón — qué NO puede hacer el navegador, y por qué
hammer-9f nombró el punto ciego que ni su vigía ni mi guardián podían cubrir:
una librería que sólo aparece como CADENA LITERAL dentro de un dlopen(). No hay
NEEDED en ningún ELF, así que ningún auditor de readelf la encuentra. La
jerarquía queda:
NEEDED del ejecutable -> lo vemos los dos
NEEDED de un .so dlopeado -> lo vemos los dos
dlopen("libfoo.so.1") literal -> NO LO VE NINGUNO
Y un navegador vive de eso. Firefox sondea ffmpeg, VA-API, vulkan y libnotify
por nombre, y cuando no están NO FALLA: apaga la función y sigue. No hay línea
roja; hay una función que nadie ofrece y nadie reclama. Es la forma que ya costó
caro con OBS y su dlopen("libGL.so.1").
`--dlopen` busca esas cadenas y las cruza contra el rootfs. Es un HEURÍSTICO y se
declara como tal —una cadena no prueba un dlopen y su ausencia no prueba que no
lo haya—, así que no falla nunca: imprime un mapa triado. Lo afirmable es lo
contrario, que es lo útil: si la cadena está y el fichero no, esa función no
existe en esta imagen.
De 85 cadenas, 47 sin proveedor. Siete son huecos de verdad:
códecs del sistema (H.264/AAC) sin receta de ffmpeg — el más caro
notificaciones web sin receta
llavero (libsecret) RECETA YA EXISTE en incoming-gnome
sonidos (libcanberra) receta en incoming-kde
WebGPU (vulkan-loader) receta en incoming-kde
vídeo por hardware (VA-API) sin receta
lectura en voz alta sin receta
Tres de los siete son promoción, no autoría. Ocho son decisiones ya tomadas
(libGL por Wayland-only sin GLX; libcurl porque sólo lo usa el pingsender de
telemetría) y siete son ruido de musl o sonames viejos. El triaje va en una tabla
del propio script, no escondido en un `if`, porque es criterio y no medición: ahí
se puede discutir.
Sin triar: 0. Si aparece una cadena nueva, el informe la marca «SIN TRIAR» en vez
de tragársela.
Lo que esto cambia de fondo: hasta hoy la pregunta era «¿arranca?» y la respuesta
era sí. La que faltaba era «¿y qué NO puede hacer?», que ninguna métrica del repo
respondía porque todas miran presencia y ésta mira ausencia declarada por el
propio binario.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
26a0dd31c7 |
SDD 26: restauradas §2.ter–§2.sexies, que mi commit anterior borró sin querer
Al reescribir el §6.8 corté el texto entre «lo que NO está probado» y `## 7`, y entre medio no había sólo eso: vivían ahí §2.ter (el modo `[source] dir`), §2.quater (los tres fallos que destapó abrirlo en pantalla), §2.quinquies (los tres mecanismos de la v0.3) y §2.sexies (lo que quedó probado y con qué). Ciento veinte líneas de método pagado con horas. Recuperadas de HEAD~1 y comprobadas byte a byte con `diff` contra el original, no a ojo. La causa, que es la de siempre en este repo: usé un ANCLA («hasta el próximo `## 7`») dando por hecho que el documento estaba en el orden en que yo lo imaginaba. El §6.8 lo había insertado yo mismo antes del §2.ter hace unas horas, así que el ancla saltaba por encima de cuatro secciones. Un ancla es una etiqueta, no una propiedad del documento — la misma familia que la cabecera de hunk que nombra la función anterior. Lo que lo hizo visible en el mismo minuto fue mirar el `git show --stat`: 210 líneas cambiadas en un fichero donde yo había escrito 45. La regla 2 del CLAUDE.md manda mirarlo para el ALCANCE del commit, pero sirve igual para el tamaño del cambio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ |
||
|
|
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
|
||
|
|
e88e4ab3d7 |
SDD 26 §3.ter: el muro del display del PGO está despejado, con evidencia
El PGO necesita correr el navegador para juntar el perfil, y las distros lo hacen bajo xvfb-run. Nosotros no tenemos X11 (Wayland-only), así que el plan decía «la salida es un sway headless». Ya no es plan: se corrió. sway con WLR_BACKENDS=headless y WLR_RENDERER=pixman arranca en gioser —un LXC SIN /dev/dri, o sea sin GPU ninguna— y dibuja píxeles reales. La captura va como evidencia: sway 1.10 / wlroots 0.18.2 / foot 1.27.0, los tres binarios nuestros, salida HEADLESS-1 a 1280x720. Ciclo completo ~2 min. Dos cosas que costó y quedan escritas para no repetirlas: · Hidratar BAJO el montaje del store. hydrate-profile.py enlaza, y un hardlink no cruza montajes: con --into work/... sale EXDEV porque el store está en otro disco. Con --into store/.rootfs/sway son 193/193 nodos y 4,9 G que no ocupan. · Inyectar el loader de musl. sway salió DINÁMICO y pide /lib/ld-musl-x86_64.so.1, que el cierre no trae. El error es «exec: /usr/bin/sway: not found», que se lee como «falta el binario» cuando lo que falta es su INTÉRPRETE. En musl el libc.so ES el loader, así que se resuelve con un --ro-bind. Misma familia que el resto de la noche: el mensaje nombra lo que buscó, no lo que falta. Queda el segundo muro del PGO, que es de diseño y no de infra: el profdata no es determinista, así que hay que generarlo UNA vez y sellarlo como artefacto propio consumido por hash. |
||
|
|
1d2f7b7686 |
SDD 26: waterfox también reproduce bit a bit
`scripts/verificar-repro.sh recipes/waterfox.toml`: REPRODUCEN 1, DERIVA 0, NO-DETERMINISMO 0. Con esto el guardián del BuildID queda probado en las DOS direcciones que exige la doctrina del repo, y no por diseño sino porque los hechos cayeron así: cazó una rotura real —el artefacto anterior selló en verde con BuildID=20260906062042, la hora del build— y pasa un control positivo, que es el arreglado reproduciendo de verdad. Un guardián con sólo la primera mitad no se distingue de uno que mata siempre; con sólo la segunda, de uno que no mira nada. Los dos Gecko del corpus (firefox con RLBox y waterfox) reproducen. |
||
|
|
349da65086 |
SDD 26: el firefox con RLBox REPRODUCE bit a bit
`scripts/verificar-repro.sh recipes/firefox.toml`: reconstruido y comparado contra el sellado da REPRODUCEN 1, DERIVA 0, NO-DETERMINISMO 0. No es un trámite. Encender RLBox mete una cadena de compilación ENTERA nueva —wasi-libc, libc++ a wasm32, los builtins, wasm2c traduciendo a C— dentro del build de firefox, y la duda razonable era si algo de eso metía una fecha, un orden de tabla hash o una ruta absoluta. No lo hace. Es además la distinción que esta misma noche costó cara en la receta de al lado: waterfox SELLÓ EN VERDE sin reproducir, porque el ArtifactHash es input-addressed y no se mueve por un BuildID que sea la hora del build. «Selló» y «está bien» son dos preguntas distintas, y la segunda cuesta un comando. |
||
|
|
25934f52f3 |
SDD 26: waterfox sella — unidad 1 cerrada, y el corpus con cero deuda
b3:88b5a762, 377 M, 43 ficheros, BuildID=19700101000001 (determinista, con
guardián). Con esto el grafo queda 850/852 sellados y los dos que faltan son
`ajeno` (steam-runtime-sniper, xwayland), que son frontera y no deuda.
Los cuatro muros que costó, y ninguno era de la receta:
1. Moría en 4 s por un submódulo git — era el binario de hammer del worker,
13 h más viejo que el arreglo que los materializa.
2. El linker moría sin mensaje — era el watchdog del worker-loop barriendo el
objdir a los 2 minutos, antes de que nadie pudiera mirarlo.
3. `multiple definition` de cairo — la receta traía la lista de deps anterior
a la migración de firefox a las variantes -shared.
4. Iconos faltantes al empaquetar — inconsistencia interna de Waterfox: el
manifiesto pide ocho tamaños y branding-common.mozbuild instala cinco.
Rotura sólo de Linux.
Y una quinta que el sello NO habría delatado: selló primero con
BuildID=20260906062042, o sea sin reproducir, y en VERDE.
|
||
|
|
ee1b6eb0df |
SDD 26: el conteo de símbolos w2c_ estaba inflado — 634, no 1547
`llvm-nm --defined-only | grep -c w2c_` cuenta también los nombres C++
mangleados que llevan `w2c_` en el MEDIO —`_ZN5rlbox...PK16w2c_mem_capacity...`
son 913 de los 1547—, no sólo los símbolos de la jaula wasm. Anclado
(`grep -c " w2c_"`, o `awk '{print $3}' | grep -c "^w2c_"`) da 634, que es lo
que mide el guardián de recipes/firefox.toml y lo que el documento quería decir.
La conclusión no se mueve: la base sin RLBox da 0 con cualquiera de los dos
patrones, y el derivado hereda los mismos 634 que la base. Lo que se corrige es
la afirmación, que decía «símbolos w2c_*» y contaba otra cosa: un número sin su
patrón no es una medición.
Lo cazó hammer-03 al reproducir el número desde su lado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
69425b0600 |
atuq v0.5 rehecha sobre el firefox con RLBox, y la dep medida en vez de creída
`firefox` selló con RLBox (b3:8116bdec) y `atuq` se reconstruyó encima en 2 segundos, que es exactamente lo que el §2 prometía del camino derivado: el overlay no cambió una línea por un rebuild del motor. Comprobado que la base nueva es la que quedó DENTRO del derivado, no sólo declarada: el `libxul.so` de atuq trae 1547 símbolos `w2c_*` donde la base anterior (352d7880) traía 0. Y la capa de v0.5 sigue entera sobre ella — los cuatro contenedores, las dos extensiones y el mapa del proxy en un arranque real. De paso, una promesa del documento que convenía no creer sin medir: que si sube firefox, atuq se reconstruye solo. Se midió en un catálogo de sonda (una copia de atuq.toml + su árbol + firefox.toml, resuelto por el fallback al catálogo padre): con el firefox idéntico al del corpus, atuq da el mismo hash; cambiando una bandera de ese firefox copiado, atuq pasa a b3:60ade76a. La dep entra en hash_inputs, así que un atuq viejo no puede quedarse tapando un motor nuevo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ |
||
|
|
9a2a55a4c8 |
SDD 26: la cadena wasm reproduce byte a byte entre el hub y el worker
Las cinco recetas se construyeron por separado en las dos máquinas —gioser (4c) y el LXC dev.gioser.net (6c)— y los cinco artefactos salen idénticos comparando el árbol completo, no sólo el ArtifactHash. No es un trámite. El lab NO entra en hash_inputs, así que dos labs distintos pueden sellar bytes distintos en la MISMA dirección del store y nada lo detecta: la igualdad de hashes no probaba nada por sí sola. Esto dice que para esta cadena los dos labs coinciden de verdad. Sale gratis de haber construido en las dos máquinas por otro motivo (el hub no tenía wasi-libcxx y sin él no podía correr scripts/test-guardianes-wasm.sh). |
||
|
|
98884c785a |
SDD 26 §6.8: el intento de probar el paquete, y los tres muros que encontró
La v0.5 dejó dicho que faltaba probar que una petición hecha en «Banco» sale
por el proxy de «Banco». Se intentó y no se llegó; queda escrito el intento
porque el muro es reusable y el que venga después no tiene por qué volver a
descubrirlo.
El diseño de la prueba sirve y no necesita red ni servidor: la MISMA url a un
puerto vacío desde dos contenedores, y comparar los errores. `connectionFailure`
contra `proxyConnectFailure`. Que sean DISTINTOS es la prueba, porque el segundo
sólo aparece si Gecko habló con un proxy y el único que se lo pudo indicar es
nuestro proxy.onRequest mirando el cookieStoreId.
Lo que falta es abrir una pestaña EN un contenedor, y las tres puertas están
cerradas:
1. Desde la línea de comandos no existe el flag.
2. Marionette SÍ está en el artefacto y responde —se le habló con un cliente
propio de 60 líneas, el protocolo es `<longitud>:<json>`, abrió sesión y
aceptó comandos—, pero el contexto chrome exige `-remote-allow-system-access`
y en la jaula no lo tomó ni por bandera, ni con --remote-debugging-port, ni
por MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1, que es lo que RemoteAgent.sys.mjs lee
en su constructor. Comprobado que la variable llega al proceso.
3. Desde la extensión, `tabs.create({cookieStoreId})` exige el permiso
`cookies` (ext-tabs-base.js:getUserContextIdForCookieStoreId). Darle a la
extensión del proxy acceso a TODAS las cookies del usuario para poder
probarse a sí misma sería pagar con la superficie de ataque del producto una
comodidad del test. No se hace, y queda dicho por qué.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
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
|
||
|
|
df4111c57d |
SDD 26: §3.quater — la cadena wasm y las tres cosas que costó
Deja escrito en el documento que se lee para reanudar: el orden obligatorio de las cinco recetas, por qué wasi-libc-headers pinea un commit viejo a propósito, y las tres trampas ya pagadas (sesgo de clang en check-symbols, el wasi-libcxx que faltaba, y el directorio vacío que es una sonda del compilador). Y el criterio de guardián que sale de todo esto, que es lo más reusable: «existe» no es «se encuentra». |