4d77d232d398f26e0b06ae14183f495ab8fed104
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d77d232d3 |
la mudanza sin terceros: las fuentes de tawasuyu por HTTPS público, y el orden del cutover
Tres cosas, y las tres quitan vueltas. 1. NO HACE FALTA NINGUNA CREDENCIAL EN EL WORKER, Y TAMPOCO UN USUARIO NUEVO `tawasuyu/tawasuyu` y `sergio/takana` son repos PÚBLICOS en el gitea. Las 23 recetas que apuntaban a `gitea@git.tawasuyu.net` (SSH, que exige la clave que es el SSH de todo) pasan a `https://git.tawasuyu.net/…`. La URL es locator y NO entra en `hash_inputs` (ADR 0013): hecho con control antes/después, **ningún ArtifactHash se movió**. Medido en el worker: `git clone --mirror --filter=blob:none` + `git archive` extrae el árbol (155 M) **sin una sola credencial**. Un usuario propio en gitea sólo haría falta para un repo PRIVADO; hoy ninguna receta usa uno. Si mañana hace falta, es una cuenta de máquina con acceso al repo que toque — nunca la clave personal. De paso, tres recetas decían «HUB-ONLY: el worker secretless recibe Connection refused». Ya no es cierto y el comentario decía lo contrario de lo que pasa: corregido en las tres. 2. LA COPIA FUERA LA DA LA MUDANZA, NO GITHUB (decisión del usuario) El «paso 1: espejar los 26 repos» deja de ser el paso 1. En cuanto el gitea vive en la caja nueva, esos repos dejan de existir sólo en la máquina que se borra — que es lo que el paso pedía. El objetivo es dejar de pagar dos máquinas, no sumar una dependencia. `espejar-repos.sh` queda como herramienta disponible. 3. EL §9 REESCRITO: qué está hecho, qué falta y en qué orden Hecho y cerrado: la caja arranca takana puro · store/grafos/granja/respaldo · repo firmado · la imagen del perfil servidor con gitea sirviendo 200 supervisado por arje · `arjectl` · y el ensayo con los datos REALES de gioser corriendo sobre takana. Falta, en orden: actualizar la caja a la imagen nueva (`upgrade`, no `dd`) → mudar los datos del gitea (`.backup` + rsync, con el origen parado en el corte) → caddy con los 6 dominios vivos → DNS → verificar desde fuera con un clone real → el resto de servicios del censo → borrar gioser con las 8 puertas en verde. Bloqueantes con su tamaño: `git` sellado sin `remote-http` (afecta a clonar DESDE una caja takana, no al gitea que sirve; re-sella una raíz de `base`) y la puerta 5, que no bloquea la mudanza. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn |
||
|
|
ec1d43efbf |
ia: el modelo de embeddings entra como DESCARGA OPCIONAL, y el navegador dice cómo conseguirlo
Decisión del usuario. El motor (199 M) y el modelo de chat (1,04 GiB) van en las cuatro imágenes porque la barra lateral está en las cuatro; el de embeddings (610 MiB) no, porque es para una función —preguntarle al archivo por significado— que no todo el mundo usa. Y «opcional» no significa «lo armás vos»: el modelo ya es receta del corpus, así que la maquinaria de la Etapa F lo vuelve paquete sin trabajo extra. Publicado en `dist/repo` con su `expected_hash` anclado, junto con las deps que `install` necesita para reproducirlo — `llama-cpp` faltaba en el catálogo, y sin ella el paquete no se puede instalar aunque exista. De paso entraron nftables, libmnl y libnftnl, que tampoco estaban. ⚠ Tres de esos paquetes se habían publicado con el ancla de sanidad por default (`/usr/bin/<nombre>`), que en una librería o en un modelo NO EXISTE: `install` habría fallado al verificar. Re-empaquetadas con su ancla de verdad (`/usr/bin/llama-server`, `/usr/sbin/nft`, `/usr/lib/libmnl.so.0`…). Lo que hace que esto sea opcional de verdad es una línea: la receta NO está declarada en ningún perfil. Está escrito en el SDD para que nadie lo «arregle» agregándola. Y la mitad que separa «opcional» de «invisible»: con el modelo ausente el host ya no dice sólo «no está» sino cómo conseguirlo — «es una descarga opcional — instalalo con `takana install ia-modelo-embeddings`». Medido en el control negativo del guardián. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
60703e3975 |
puriy-costura: la receta del host de native messaging, y las dos trampas del monorepo
El binario de «la costura» (SDD 26 §7) entra al corpus: `b3:3b633431`, ELF estático musl de 952 K
pineado a tawasuyu `ebe41e96`. Verificado como artefacto y no sólo como build — un marco de 4 bytes
por stdin y contesta `{"id":1,"ok":true,"verb":"ping","version":"0.1.0"}`; y la promesa del §6.1
entera contra el binario SELLADO: cuatro visitas aprendiendo, a la cuarta `stable`, y en la quinta un
script cambiado ⇒ un evento que nombra el recurso y el delta de tamaño (+6 bytes). Deja
`registro.postcard` y `bitacora.postcard` en el `--state`.
Las dos cosas que costaron, las dos con el mismo patrón (el error no nombra la causa):
1. `cargo vendor --locked` murió con «cannot update the lock file», que NO dice qué crate falta. El
lock de tawasuyu estaba desalineado con su propio `main`, y —esto es lo reutilizable— **un
`Cargo.lock` regenerado en ese clon compartido NO describe su `main`**: ese árbol tiene cientos de
ficheros en vuelo de otras sesiones. El nombre del crate culpable salió de diffear el lock
publicado contra el que resuelve un árbol LIMPIO del commit (`git archive <sha> | tar -x`, que
además no toca el `.git` compartido). Arreglado allá en dos commits; acá se pinea el que cierra.
2. Después murió por el `vendor/` propio del monorepo: tawasuyu parchea `smithay` a mano y lo enchufa
con `[patch.crates-io] path = "vendor/smithay"`. Un patch por RUTA no necesita
`.cargo-checksum.json` —y upstream no lo commitea—, pero `cargo vendor` convierte su directorio de
salida en un directorio de REEMPLAZO DE FUENTE, donde cada crate sí tiene que traerlo. El síntoma
fue 1 de 1699 crates sin checksum y un error que hablaba de `taffy`. La solución ya estaba en el
corpus: `cargo_vendor_dir`, que las otras cinco recetas de este monorepo ya llevan.
⚠ Y la explicación FÁCIL era falsa: «cargo vendor borró el checksum» — `git ls-tree` dice que ese
fichero nunca existió. Queda escrito en la receta para que nadie lo vuelva a deducir.
El precio, medido y anotado: el vendoreo es del workspace ENTERO (2,4 G, 1699 crates, `axum` y
`aws-lc-sys` incluidos) para un host que usa quince deps. Mismo precio que ya pagan las otras cinco.
NO se declara en ningún perfil de `targets.toml` todavía, y NO se agrega el manifiesto de native
messaging a atuq: sin la extensión que lo llame (unidad 6) sería un binario que nadie invoca y un
manifiesto apuntando a una extensión inexistente, que es un fallo silencioso — el navegador arranca
igual y la función no está.
|