d1032c03366f6009cb65a356a7d37cbc645cb706
609
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5d16b55e14 |
el visor del grafo reventaba con un estado que no conocía
`build-state-view.py` traía los cuatro estados del corpus cableados (sealed/debt/never/unhashable) y contaba TODOS los nodos contra ese diccionario. `build-state.py` emite dos más —`wanted` (hueco declarado en targets.toml, sin receta) y `ajeno` (imagen de qorpa, que no se construye desde fuente)—, así que el visor moría con `KeyError: 'ajeno'` y el tablero HTML llevaba sin poder generarse. O sea: agregar un estado nuevo rompía el tablero entero, en silencio para quien no lo corriera. Ahora los de fuera del corpus se cuentan aparte y se NOMBRAN en la leyenda —no se cuelan a la barra, que es sobre `totals["recipes"]` y pasaría del 100 %—, y cualquier estado que aparezca mañana cae en esa misma rama en vez de tumbar el visor. De paso, el grafo regenerado: 930 selladas, 2 en deuda, 5 nunca, 3 ajenas; git ya figura sellado en su hash nuevo (b3:215742cb…) y no arrastró a nadie, como decía el radio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d4896c2999 |
pagada la deuda: el git del corpus vuelve a clonar por HTTPS
El §6.50 quinquies dejó la caja trayendo fuentes por SSH porque su git no tenía remote-https. Eso era el síntoma; la causa estaba un paso antes de donde mirábamos —incluida la nota que yo mismo había escrito, que culpaba al Makefile—: el `configure` de git prueba libcurl con AC_CHECK_LIB, que enlaza `conftest.c -lcurl` y nada más, y con libcurl estática faltan sus privadas (-lssl -lcrypto -lz). El test falla, config.mak.autogen se lleva NO_CURL=YesPlease, el Makefile excluye los tres helpers y conserva git-http-backend —esa asimetría en el artefacto era la firma, y estuvo a la vista desde el 14 de septiembre—. El build termina en verde. Medido sobre el artefacto: b3:215742cb… trae git-remote-http, -https, git-http-fetch y los ftp, y CLONA por HTTPS (clone, no ls-remote). En la caja, hidratado y con la reescritura apagada, funcionan tanto `git clone` como `git clone --mirror`, que es el comando exacto del fetch de takana. Y el radio, medido en vez de temido: la nota vieja decía «raíz de perfil.base ⇒ radio grande»; `yupana radio git` dice cero dependientes directos y cero transitivos. El miedo escrito a mano había sobrestimado el costo de arreglarlo, y eso lo mantuvo roto tres días. Las reescrituras url.insteadOf quedan retiradas de la caja; el guion se conserva como salida de emergencia y así lo dice su cabecera. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0bb40af496 |
la caja trae fuentes git por SSH: el arreglo, en un guion idempotente
Lo que hasta ahora era estado a mano en la caja queda reproducible. Hace las cinco piezas medidas: reescritura de URLs con url.insteadOf, la clave del host reexpresada SIN puerto (cargo la busca así), la huella de github.com PINEADA contra la publicada —si no coincide, sale con error en vez de aceptarla—, la IdentityFile por host, y `net.git-fetch-with-cli = true` para que cargo delegue en el git de verdad. Además mueve CARGO_HOME fuera de la raíz de 5,9 G, que es lo que tiró la caja. No mueve ningún hash: la URL es un localizador y no entra en hash_inputs (ADR 0013). Termina con un control que CLONA de los dos orígenes; si alguno no llega, sale distinto de cero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e7b77ff657 |
la cuenta de sergio en la caja, su jaula propia, y shuma corriendo como él
Pedido: «crea mi cuenta sergio con la clave tawasuyu, desde ahí abrir los claudes como lo estoy
haciendo acá con shuma sobre los proyectos».
· cuenta `sergio` uid 1001 (el mismo que en gioser), home /home/sergio, clave verificada
· su propia jaula `claude-sergio`, que concede SU home —su `.claude`: credenciales y memoria— y
NO el de root. Con la instancia de root, sergio moría en «bwrap: Can't mkdir /root/.claude»
· `claude` elige instancia por usuario; `PROY=/otra/ruta claude` abre el agente sobre otro proyecto
· shuma-daemon y shuma-gateway pasan a correr como `sergio`, no como la cuenta de servicio `shuma`:
shuma es su CONSOLA, y cada pestaña que abre tiene que ser una sesión suya — si no, `claude`
buscaría una jaula `claude-shuma` que no existe
⚠ NO se eleva con doas/sudo: en esta caja el setuid NO TOMA («doas: Operation not permitted», con
NoNewPrivs 0 y `/` sin nosuid — queda como deuda entender por qué). No hace falta: `qorpa run`
levanta la jaula con user namespaces y anda sin privilegio, siempre que el directorio de la
instancia sea del usuario.
⚠ Y un detalle que costó una vuelta: sin `shift 2`, el HOME y el proyecto que el lanzador pasa como
posicionales se cuelan COMO ARGUMENTOS del agente — la primera sesión se abrió con el prompt
«/home/sergio», y el agente contestó, con razón, que eso es una ruta y no un comando.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d6ba65e23c |
el lanzador arranca EN el repo: sin ese cd, el agente empieza sin memoria
La memoria se indexa por ruta de trabajo. `qorpa run` deja el shell en `/`, así que las primeras pruebas usaron el proyecto `-` y el agente arrancaba en blanco aunque sus 163 ficheros estuvieran en disco. Con `cd /opt/takana` usa `-opt-takana`, que es donde se renombró la memoria traída de gioser (allá el repo era /mnt/vvv/takana). Comprobado en la caja, con dos preguntas que sólo puede contestar leyendo: ¿el logo? → «martillo 7×7, cuatro colores exactos» (está en su memoria) ¿la regla 1? → «todo takana build va envuelto en flock, y con -o», con el matiz del lock heredado O sea: el agente de la caja arranca sabiendo lo que sabe éste. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eb96c9219e |
el agente corre en la caja y puede construir — la condición que el usuario puso para borrar gioser
$ claude -p "Decí exactamente: CLAUDE CORRIENDO EN LA CAJA" CLAUDE CORRIENDO EN LA CAJA Turno real —API, credenciales, respuesta— desde 2.29.29.217. El CLI es un ELF glibc de 360 M y la caja es musl, así que va enjaulado (ADR 0015, el caso de sergioh-api). La instancia declara red, nesting y SEIS directorios: /opt/takana, /work/dev-fs, /store, /root/.claude, /root/.ssh (ro) y el CLI (ro). Es ANCHA y está dicho: un agente que construye, commitea y empuja necesita lo mismo que un humano; lo que la jaula compra es que el alcance esté escrito y acotado. ⚠ El `takana` que construye NO se instala adentro: sale del store, que ya está concedido — es estático musl y corre igual dentro de una imagen glibc. LA CAJA CONSTRUYE: sellados hoy `intel-ucode` (156 signatures) y `sof-firmware` (2 firmware + 8 topologías). Faltaba una pieza que el lab no traía: `.dev-fs/tools` (zig 0.13/0.16 y go, 1 G) — la imagen del lab empaqueta `alpine/` y nada más, y el primer intento murió con «zig no encontrado». 🧱 Y lo que NO anda, con su rodeo: `takana build` dentro de la jaula muere con «bwrap: Can't mount proc on /proc» aunque `nesting = true`. No es el anidamiento —un `bwrap --proc /proc` a mano SÍ funciona adentro— sino el userns anidado del sandbox del build; probado también con `root = false`, que es la sospecha que el propio código documenta, y falla igual. El rodeo que sí anda, escrito en el lanzador: pedirle el build al ANFITRIÓN por ssh a 127.0.0.1:22022. Así se selló sof-firmware desde dentro de la jaula. Manifiesto y lanzador versionados en scripts/servidor/: una instancia que sólo existe en la caja se pierde con la caja. ⚠ Pendiente menor: la memoria del agente está en `-mnt-vvv-takana` y allá el repo vive en /opt/takana ⇒ el índice POR RUTA no coincide y arranca sin memoria del proyecto aunque los 733 ficheros estén en disco. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
597406ad70 |
git-sincronizar: gritaba «el commit desapareció» SIEMPRE — era pipefail + SIGPIPE, no el rebase
El guión avisaba en cada corrida de que el commit se había perdido, iba a recuperarlo, y el cherry-pick le contestaba que el contenido ya estaba. Un guardián que grita siempre se ignora, así que valía la pena encontrar por qué. No era git: era la comprobación. El guión corre con `set -o pipefail` y comprobaba con `git log … | grep -qF "$MSG"`. **`grep -q` sale en cuanto encuentra**, eso le manda SIGPIPE a `git log`, que termina ≠0, y con `pipefail` la tubería entera se lee como FALLO **aunque el grep haya encontrado**. Demostrado en una línea: $ bash -c 'set -o pipefail; seq 1 100000 | grep -q "^5$" && echo OK || echo FALLO' FALLO Arreglado capturando la salida antes de comparar: sin tubería, sin SIGPIPE, sin falso positivo. ⇒ Y de paso desmiente lo que el propio guión daba por hecho: de las cinco «pérdidas» de commit que motivaron escribirlo, al menos las últimas dos fueron ESTE falso positivo, no el rebase. Las primeras sí están en el reflog con su `(start)`/`(finish)` sin `pick`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b985570788 |
respaldo: un parcial de SÓLO LECTURA envenena esa ruta para siempre — y el bucle lo llamaba «red»
La primera corrida completa desde la caja (puerta 7) se cayó 40 veces seguidas con el MISMO fichero: open ".../fonts/dejavu/.rsync-partial/DejaVuSans-Oblique.ttf" failed: No such file… (2) .. store: corte de red (rsync 23). Intento 39; reanudo en 60s. !! store: 40 intentos y sigue cayéndose. Dejo lo subido y paro. No era red. Una transferencia interrumpida el 11-sep dejó ese PARCIAL en el destino con modo -r--r--r-- (el del origen: el store es de sólo lectura por diseño), y ningún intento posterior puede reescribirlo. El fallo es ETERNO, y el bucle reintentaba cada 60 s contra esa pared — exactamente lo que la cabecera de la función de reintento dice que no hay que hacer. Dos arreglos: 1. `--chmod=Fu+w`: los ficheros del respaldo quedan escribibles por su dueño, así que un corte a mitad se RETOMA en vez de trabarse. El precio es no conservar el bit de sólo-lectura en la copia; es bajo: el contenido es lo que se respalda y los permisos los reconstruye el store. 2. Al rsync 23 se le dan TRES intentos, no cuarenta, y al tercero se NOMBRA la causa probable (un parcial de sólo lectura en el destino) en vez de llamarlo corte de red. El 23 es «some files were not transferred», que tanto puede ser un cable como una pared. Comprobado: borrado el parcial envenenado, ese fichero sube a la primera. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
615cc2f839 |
git-sincronizar: un cherry-pick VACÍO no es un fallo, es que el contenido ya estaba
Estrenando el guión con su propio commit saltó el caso: la comprobación por mensaje dio negativo, el cherry-pick de recuperación salió con «the previous cherry-pick is now empty» y el guión abortó con error… teniendo el contenido ya en el árbol. Ahora distingue los dos casos: vacío ⇒ falso positivo de la comprobación, se aborta el cherry-pick y se sigue; cualquier otro error ⇒ se aborta, se imprime el reflog y se sale ≠0 para que lo mire un humano. Lo que importa es que el CONTENIDO esté, no que el sha coincida. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
17b6ea1a4f |
git-sincronizar.sh: la regla 2 ter automatizada — porque hoy hizo falta cinco veces
En dos días, `git pull --rebase` se llevó por delante CINCO commits propios en este árbol compartido, siempre con el mismo reflog: `commit:` mío, `(start): checkout <otro>`, `(finish): returning to main`, y ni un `pick` en el medio. El comando no es el culpable —en un repo de juguete reaplica bien— sino que alguien mueve `refs/heads/main` mientras el rebase está en vuelo. La receta manual (CLAUDE.md regla 2 ter) funciona: mirar el log y recuperar con reflog + cherry-pick. Esto es esa receta automatizada, porque **una regla que hay que acordarse de aplicar en cada push es una regla que un día no se aplica**. Hace, en orden: anota su HEAD · rebasa · COMPRUEBA que el commit sigue en la rama —por su mensaje, no por el sha, que el rebase reescribe— · si no está lo recupera del reflog · empuja · verifica que gitea quedó donde tiene que quedar · y si el espejo de GitHub quedó divergente lo realinea con `--force-with-lease` (nunca `--force` a secas) y sólo tras comprobar que su contenido ya está en nuestra historia. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
80e04872d7 |
poda-fuentes: no corría en takana — y la causa era que la caja NO TIENE /dev/fd
El latido, corrido desde la caja, moría así: poda-fuentes.sh: line 72: /dev/fd/63: No such file or directory poda-fuentes.sh: line 73: viejos: unbound variable ⚠ poda de fuentes falló (rc=1) — sigo El segundo error es consecuencia del primero (el `mapfile` nunca corrió), y el primero no nombra la causa: **la sustitución de procesos de bash necesita `/dev/fd`, y una caja takana no lo tiene**. gioser sí: `/dev/fd -> /proc/self/fd`. O sea que NINGÚN `<(...)` de NINGÚN script del hub funcionaba allá — esto era sólo el primero en toparse. Dos arreglos, y los dos hacían falta: 1. En el script: `<(...)` fuera (fichero temporal, que anda en cualquier shell y con cualquier /dev) y `df -B1 --output=avail`, que es GNU, por `df -k` + awk. Es la poda de `work/sources`, justo lo que un hub necesita para no llenarse. 2. En la caja: `/dev/fd -> /proc/self/fd`, con una Card OneShot en el genesis para que sobreviva al reinicio (como la de `hostname`). Comprobado después: `bash -c "cat <(echo funciona)"` responde. Lo correcto a futuro es que lo haga el init o el product-rootfs, no una Card — queda anotado. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1ca210a311 |
store-gc: xargs -d tampoco existe en busybox — el arreglo anterior imprimía «huérfanos ?»
El primer intento cambió `du --files0-from=-` (GNU) por `xargs -d '\n' du -sk`… y `-d` también es extensión GNU. En la caja quedó «espacio: superados 0 · huérfanos ?». Va con `tr '\n' '\0' | xargs -0`, que busybox sí tiene, y se probó la función suelta en las dos máquinas. Lo que SÍ funcionó del intento anterior es el `?`: cuando el total no se puede calcular, la función lo DICE en vez de imprimir 0. Un cero inventado habría dicho «no hay nada que ganar» justo cuando había 150 huérfanos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4fe6046200 |
store-gc: decía «espacio: superados ·» y «libres: Available → Available» — dos bugs que salen en takana
Corrido por primera vez en la caja (una máquina busybox, no GNU), el recolector funcionó —337 artefactos borrados y verificados, 26,8 G liberados— pero sus dos NÚMEROS salieron vacíos: ==> espacio: superados · huérfanos ==> 337 artefactos borrados y VERIFICADOS (0 sobrevivientes) · libres: Available → Available 1. `du -sch --files0-from=-` es de GNU coreutils y busybox NO lo tiene ⇒ `espacio()` devolvía vacío. Un número que falta se lee como un número chico: sin él nadie puede decidir si vale la pena correrlo, que es justo para lo que está el dry-run. Ahora `xargs du -sk` + awk, que anda en los dos mundos. 2. `df -h /home` estaba CABLEADO, y el store casi nunca vive ahí: en gioser es un bind-mount del volumen y en una caja takana es `/store`, su propia partición. En la caja no hay `/home`, así que `tail -1` se quedó con la CABECERA y el resultado fue «Available → Available». Se mide `$STORE`. Medido de verdad con `df` a mano: /store pasó de 84,7 G usados (91 %) a 57,9 G (62 %) — 26,8 G liberados, 1657 → 1320 artefactos. Y el control que importa después de un gc: los 11 enlaces de `/usr/bin` que apuntan DENTRO del store siguen resolviendo y los 21 entes siguen corriendo. ⚠ Vale anotar la advertencia que el propio gc imprime y que en esa caja no se puede satisfacer: «sin .config legible ⇒ NO se protege ningún kernel por esta vía». El default (sólo superados) no toca lo vigente, pero `--huerfanos` en una máquina que arranca de su store hay que pensarlo dos veces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2271cc3c10 |
el latido se pone al día antes de commitear — si no, late y no publica, para siempre
`cosecha-cron.sh` commitea el estado y lo empuja, pero nunca hacía `pull`. En el hub no se notaba: el árbol lo mantiene al día el agente que trabaja ahí. Al mover el latido a una caja donde NO hay nadie trabajando —que es justo lo que pide la puerta 4— el repo se queda atrás en el primer push ajeno y a partir de ahí TODOS los ciclos fallan el push, cada uno anotando «reintenta próximo ciclo». Un latido que late y no publica. `git pull --ff-only`, y nunca `--rebase`: este script corre en un árbol COMPARTIDO con otros agentes y ahí un `pull --rebase` ya se llevó un commit por delante (regla 2 ter). Fast-forward no reescribe nada: o adelanta limpio, o falla sin tocar el árbol y se anota en el log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1c237f9da2 |
mudanza: decidir SIN teclas unitarias, /mnt/vvv entra al censo, y 25 rutas de secretos más
Tres cosas que salieron de usar la herramienta de verdad.
1. `--decidir <fichero>`: decidir EN LOTE desde `<ruta-o-nombre> <decision>` por línea.
El usuario: «no sé cómo activar o desactivar cosas aquí desde shuma llimphi remoto, que las
opciones son con teclas unitarias». El modo interactivo lee una tecla por entrada, y hay
terminales donde eso no se puede usar — una herramienta cuya única forma de decidir exige un
tipo de terminal no es una herramienta, es una herramienta PARA ESA TERMINAL. El fichero anda en
cualquiera, se revisa antes de aplicarlo, se versiona y se vuelve a correr.
Una clave que no empareja con nada, o que empareja con varias, es ERROR RUIDOSO y no se escribe
NADA: un lote a medias deja decidido lo que nadie revisó.
2. `/mnt/vvv` entra a las raíces del censo. No estaba, y ahí vive el trabajo: los repos de la
persona, el monorepo, el store, work/sources. El agujero se vio preguntando por `humanoid`: el
censo sólo conocía `/home/sergio/humanoid` —200 M de `build/` de junio— mientras el proyecto de
verdad, con su `.git`, estaba en `/mnt/vvv/humanoid`, fuera de toda raíz censada. `repos_git` SÍ
lo miraba: una parte del censo conocía el árbol y la otra no.
3. 25 rutas más en `rutas-fuera-de-git.txt`, del barrido que hizo el frente tawasuyu sobre su home:
`~/.wawa/seeds` (sin él ninguna app nueva del génesis nace), `~/.tejido` (la identidad de esta
máquina en la flota), `~/.local/share/agora`, `~/.config/{wawa,minga,thasnuna,shuma,mirada,hcloud}`,
`~/keys` y `~/fdroid` (⚠ firma de apps Android), `~/.gnupg`, `~/.pgpass`, `/etc/wireguard`,
`/etc/{shuma,sandokan,tawasuyu/agente,local.d}`. Son identidades y semillas: no se «vuelven a
generar», porque generar otras significa ser OTRA máquina para el resto de la flota.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5ec4e2b213 |
mudanza: el tercer camino — mudar, RESPALDAR o abandonar, cosa por cosa y con su descripción
Pedido del usuario: «una lista de cada cosa con una corta descripción y la opción de yo elegir mudar, mover a un directorio para respaldar ese directorio, o abandonarlo». Eran tres huecos distintos. 1. «CADA COSA». El censo medía RAÍCES: `/home` era UNA entrada de 25 G y UNA decisión, con repos, SDKs, 4,3 G de caché y trabajo irrepetible adentro. Ahora emite un renglón por cosa —169 en gioser contra 5—, un nivel hacia adentro de cada raíz y DOS en `/home`, porque su primer nivel son usuarios y la decisión no es por usuario. `--solo-totales` conserva la vista vieja, que es la que dimensiona la mudanza; las dos viajan en el censo (`datos` y `datos_raiz`). ⚠ El primer intento mandaba las ~110 sondas en UNA llamada, se pasaba del timeout y devolvía lista VACÍA: «no hay datos» en vez de «no pude medirlos». Va en tandas, y una tanda que falla se nombra. 2. «UNA CORTA DESCRIPCIÓN», y son hechos: lo sirve el servidor web · lo usa un proceso vivo · es repo git y a dónde apunta · tiene cambios sin commitear · adentro hay node_modules/target/venv · lo más nuevo que hay dentro. «Sin señales» también es un hecho, y es el que dice dónde mirar. 3. «ELEGIR ENTRE TRES»: DECISIONES pasa de (muda, muere) a (muda, RESPALDA, muere). Faltaba el camino que se usa de verdad —no lo quiero corriendo allá, tampoco lo quiero perder—; con dos opciones, todo lo dudoso se marcaba `muda` y la mudanza engordaba. La regla de fondo no cambió (qué datos VALEN no lo dice la máquina), pero cuatro hechos sí los sabe: servido/usado ⇒ muda · caché ⇒ muere · repo limpio con remoto ⇒ muere · repo sin remoto o sucio ⇒ respalda. Y el cruce que evita el peor error: un remoto que apunta a ESTA MISMA máquina no es un respaldo, es el mismo disco (medido: /mnt/vvv/humanoid → ssh://git@127.0.0.1:2345). 4. LAS DECISIONES SE GUARDAN. `--decide` las usaba sólo para el plan de esa corrida: cerrabas la terminal y se perdían las 169. Ahora reescribe el censo (temporal+fsync+rename, `.bak`, y releído antes de pisar nada; aborta si perdió alguna). Para eso hubo que hacer el emisor IDEMPOTENTE: escribía `decision = ""` fijo, así que re-emitir un censo decidido duplicaba la clave y el TOML dejaba de parsear — la herramienta no podía reescribir su propio fichero. 5. EL PASO `respaldo`: corral por defecto leído de respaldo-storagebox.sh (no una segunda dirección a mano), nombre aplanado, y NO se instala en el destino. Sin corral, aborta: un rsync a ninguna parte devuelve 0 y se lee como un respaldo. Probarlo con rotura a propósito Y con el control que tiene que pasar destapó dos bugs: `--mkpath` es obligatorio (el primer respaldo siempre estrena un nivel) y un FICHERO no lleva barra final — `rsync fichero/ dst/` es un error, y el paso de `datos` tenía el mismo defecto latente. La verificación es rsync en seco contra el destino real, no `du` ni `find`: el Storage Box no tiene find, no acepta tuberías, y su `du` comprime. 6. De paso: `aplicar.py --dry-run` decía «✅ todos los pasos ejecutados y verificados» sin haber ejecutado nada. Ahora dice EN SECO: N mostrados, NINGUNO ejecutado y NINGUNO verificado. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2098e86ba4 |
atuq: la boveda ANDA — 19 guardianes en verde, y el bloqueo de tres semanas era un fichero sin empujar
El pin de `puriy-costura` subio a `23a292863` y con eso se apagaron los dos rojos del cable. La suite entera sobre `b3:e556024b`: **19 verdes, 0 rojos, 32,1 min**. No se toco ningun guardian — que la suite existiera ANTES del arreglo es lo que permite afirmarlo: el cuadro de ayer y el de hoy son el mismo instrumento. La medicion que cierra es la que abrio el §7.quinquies, leida al reves: `strings` sobre el binario sellado da `vault` **10** veces donde daba 0, con `cas`/`sct` de control; y `vigia-atuq-verbos` pasa de 3 verbos sin dueno a cero, con su control positivo intacto. Lo destrabo publicar el `Cargo.lock` de tawasuyu (`23a292863` alla), regenerado en un arbol limpio: 215 lineas, todas de contabilidad de deps por ruta, cero `checksum` y cero `source` movidos. Y el remate, que es la leccion: **ese lock era byte a byte el que la otra sesion ya tenia en su arbol sin commitear**. Tres semanas de bloqueo y el fichero correcto estaba escrito, sin empujar, a un directorio de distancia. Antes de decir «bloqueado por otro repo», mirar si lo que falta no esta ya hecho y sin publicar. Commiteado alla con indice temporal (`GIT_INDEX_FILE` + `commit-tree`), que es la unica forma de publicar una ruta en `MM` sin llevarse por delante lo del otro agente: su arbol quedo byte a byte igual, comprobado con `cmp` antes y despues. ⚠ Y la cabecera del runner se desmintio SOLA dos veces: anuncio «un rojo» cuando eran dos, y «dos» cuando ya no quedaba ninguno. Un numero escrito como prediccion envejece callado. Con todo en verde el numero ya no distingue un producto sano de un runner que no corre nada, asi que lo que sostiene la corrida queda escrito: la puerta del sello (verificada contra un store vacio) y el control interno de cada guardian. |
||
|
|
e2bb4f9b89 |
censar: «no existe» y «no puedo mirar» se confundían — 572 M invisibles, y el censo de hoy
El censo ya tenía tres estados a propósito (`ausente` / `sin_permiso` / `ok`), pero el tercero sólo se detectaba sobre la PROPIA ruta: cuando el que no deja pasar es un ANCESTRO, `[ -e "$p" ]` contesta que no existe y la ruta caía en `ausente`, que el censo descarta sin decir nada. Medido en gioser: `/root/.local` es 0700 ⇒ un censo corrido como `sergio` daba `/root/.local/bin` por AUSENTE. Como root son 572 M de binarios puestos a mano que ningún paquete provee — justo la clase que la mudanza no puede reconstruir. Arreglado: si un ancestro existe y no es atravesable, el estado es `sin_permiso`. Probado en los dos sentidos, que es lo que separa un guardián de un adorno: ancestro 0700 de otro dueño → sin_permiso (sale en el ⚠ del resumen) ruta que NO existe de verdad → ausente (no se reporta) ← el control que tiene que pasar Con el arreglo, el censo sin privilegio pasa de 2 rutas ciegas a 13, entre ellas el home entero de `artix`, que antes no figuraba de ninguna manera. Y el §6.34 con el estado MEDIDO de la mudanza hoy: de los 29 dominios queda UNO sirviéndose desde gioser (`sergio.gioser.net`); los otros seis vivos ya contestan 200 contra 2.29.29.217. El rescate del §6.20 sigue coincidiendo bit a bit con cuatro de los seis procesos que corren desde un inodo borrado, y los otros dos tienen en disco una versión más nueva. Lo que queda abierto son los pasos 6 y 7: el montón de daemons propios (shuma, willay, pacha, tejido, tupu, matilda…) y los datos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fb7dba530d |
atuq: el que estaba «nombrado como hueco» entra a la suite — 17 verdes y 2 rojos en 32 min
La primera version dejaba afuera al guardian de notificaciones (`dunst-headless.sh --via-atuq`) con
la excusa de que vive en el arnes de sway, y lo escribia en la cabecera «para que su ausencia no se
lea como cobertura». Eso dura tres dias: al cuarto la lista ES la cobertura y el parrafo no lo lee
nadie. Corrido aparte: verde en 180 s. Entra.
Es ademas el que mide lo que ningun auditor de ELF puede ver: una pagina llama `new
Notification(...)`, `libxul` hace `dlopen("libnotify.so.4")` —que no es NEEDED de nada—, el bus
activa dunst y dunst DIBUJA. Cinco piezas y la unica forma de saber que estan las cinco es verlo.
Para que entrara, la lista pasa a llevar el COMANDO de cada guardian en vez de su nombre de fichero:
lo que lo tenia afuera era el despacho por extension, no una decision.
Corrida entera de los diecinueve sobre `b3:e556024b`: **17 verdes, 2 rojos, 32,2 min**, que es
exactamente lo que la cabecera predice — y la cabecera es el control, no un adorno.
Los caros: sct 242 s, archivo semantico 238 s, ia 182 s, instalacion 180 s, notificaciones 180 s.
Los cuatro primeros salen en 5 s y no tocan el navegador.
|
||
|
|
eaa7d02725 |
el corte, con hora: 93 minutos — y en un servicio que ya autoriza no va límite por tasa
Del `access.log`, sin ambigüedad. La IP del usuario (`45.234.61.160`, autenticada como `sergio`, 2709 peticiones) tuvo tres silencios: **93,2 min (11:59:37 → 13:32:49 UTC)**, 50,1 min (10:44:58 → 11:35:06) y 10,0 min (11:45:06 → 11:55:05). Los tres son el `timeout` de 10 minutos del ban disparándose una y otra vez: cada vez que volvía, su primera ráfaga lo baneaba de nuevo. El corte grande terminó exactamente cuando vacié los sets. 🧨 Y LA EXENCIÓN QUE TENÍA NO SERVÍA: la ACL histórica de squid exime `45.234.60.0/24` y él entra desde `45.234.61.160` — **una red de al lado**. Parecía cubierto y no lo estaba. La pertenencia se MIDE (`ip in red`), no se deduce del parecido del prefijo. DECISIÓN DE FONDO: en un servicio que YA AUTORIZA no va límite por tasa. El uso de esta caja es INTENSIVO —varias sesiones de agente en paralelo, cada una abriendo túneles a la vez— y un límite calibrado para «una persona normal navegando» no protege de nada: el atacante ajusta su ritmo, el usuario no puede trabajar más despacio. Squid ya pide usuario y contraseña y tiene su ACL. El ban del puerto queda apagado (60000/min, ráfaga 1000) y contra un flood queda el tope GLOBAL `syn_nuevas_por_seg`, que no distingue usuarios ni banea a nadie por ser rápido. Control tras aplicar: su túnel pasando —`45.234.61.160 … CONNECT api.anthropic.com:443 sergio`— y `45.234.61.160 ∈ 45.234.60.0/23` comprobado con aritmética de redes, no a ojo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
618dcfd851 |
🧨 el cortafuegos baneaba a los usuarios del proxy — y squid nunca se cayó
Preguntaron si el proxy estaba caído. NO lo estaba: el ente llevaba horas corriendo con ↻ 0 y contestando 407. Lo caído era el ACCESO — el cortafuegos de ayer baneaba a los autorizados. La prueba en una línea: **154.197.1.2, una IP que está en la ACL de squid (gente con contraseña), apareció en `@ban4`**, con el contador del ban en 38.246 paquetes tirados. LA CAUSA NO ES LA TASA, ES LA RÁFAGA: `limit rate over 300/minute burst 5 packets` tolera cinco conexiones por encima del promedio, y un navegador o un agente detrás de un proxy abre DECENAS de CONNECT en el mismo instante aunque su promedio sea bajísimo. Y como @ban4 es UN SOLO set consultado antes que todo, caer por el puerto del proxy te tira también el web y el git. ⚠ El tipo ya traía el campo `rafaga` (default 5) con un comentario que dice «para un servicio web va alto (100-200)». Otra vez leí el .ron de EJEMPLO y no el TIPO. Hecho, en orden de urgencia: flush de los sets (servicio restaurado en el acto) · los 21 IPs y 5 redes de la ACL de squid a `confiables` · `rafaga` por servicio según su clientela (ssh 5, git 20, web 100, proxy 200) · proxy a 1200/min con ban de 60 s, que es red de contención contra un flood y no control de uso. Control después: CERO autorizados en el set, 24 baneados (escáneres) y tráfico real pasando —`154.194.14.37 … CONNECT api.deepseek.com:443 sigma`, 298 K tunelados—. ⚠⚠ Y un fallo de método que casi lo tapa: **`nft -f` SUMA a la tabla existente**. Cargué el reglaset corregido encima del viejo y quedaron 30 reglas con las VIEJAS ADELANTE, así que los `confiables` nuevos no se evaluaban nunca. Por eso `cortafuegos apply-input` borra la tabla antes de cargar. La lección: un cortafuegos correcto y uno usable no son lo mismo, y la diferencia NO se ve al aplicarlo. El control que lo habría cazado es el que no corrí: una ráfaga de conexiones desde una IP que no esté en `confiables`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e903f486d9 |
el enlazador va DENTRO del compilador — /etc/cargo/config.toml es una ruta que cargo no lee
La prueba de la caja recién instalada —`cargo build` en un proyecto nuevo, sin flags ni rutas a mano— falló con `linker `cc` not found`, y destapó que la pieza que había empaquetado era falsa: **cargo NO LEE `/etc/cargo/config.toml`**. Sólo lee `$CARGO_HOME/config.toml`, los `.cargo/config.toml` del proyecto y sus padres, y lo que se le pase con `--config` — que es lo que yo venía pasando a mano en cada prueba, y por eso «funcionaba». Y el arreglo no era mover el fichero: **el que enlaza es `rustc`, y rustc no lee la config de cargo**. Las flags van DENTRO del compilador, en el spec del triple: · `linker = "ld.lld"` + `linker_flavor = Gnu(Cc::No, Lld::Yes)` — sin esto rustc busca `cc`, que una caja takana no tiene. · `crt_static_default = true` — sin esto enlaza dinámico y pide `-lgcc`, que no existe (la distro se construye con zig, que trae compiler-rt). · `crt_static_allows_dylibs = true` — su pareja obligatoria: sin ella `crt-static` apaga los dylibs y con ellos los PROC-MACROS (serde_derive, clap_derive, las 44 recetas `cargo-*` con `derive`). El compilador en sí sigue dinámico: el `bootstrap.toml` fija `crt-static = false` para el build, y eso gana sobre el default del spec. ⇒ `cargo-config` (receta, árbol y copia en scripts/) se van enteros: un paquete que instala un fichero que nadie abre es peor que no tenerlo. Y de paso, `lld21` instala los cuatro alias como SYMLINKS y no como copias: cmake los duplicaba, 5 × 96 M = 478 M de artefacto para 97 M de enlazador. Ahora 108 M, y `ld.lld` sigue despachando por `argv[0]`, que es como upstream lo diseñó. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
72ba86c290 |
la cadena rehecha: llvm21 arreglado, lld enlaza, rust reconstruido — los seis controles pasan
Un cambio de dos líneas en `llvm21` re-selló tres artefactos y los molió la granja sola, en orden,
por dependencia: llvm21 95956a16→bd4a4094, lld21 27be53c1→95a4794b, rust 015a07fb→702094a8.
llc --version Stack dump: + segfault → LLVM 21.1.2, Optimized build
opt --version ídem → LLVM 21.1.2
ld.lld «not built with zlib» → ENLAZA
rustc --version → 1.97.0 (built from a source tarball)
--print target-list → x86_64-alpine-linux-musl
cargo build + correr → 0,16 s y el binario habla
La config del sitio pasa a usar `lld` (más rápido); el `ld` de binutils queda documentado como
alternativa que también funciona.
LA REGLA QUE SE GANÓ SU LUGAR: **`link = "static"` en el encabezado decide cosas que la receta no
dice**. Van tres, todas medidas: apagó los threads de openssl, le mintió a libtool, y acá rompió los
registros estáticos de LLVM (`cl::opt`, `TargetRegistry`) porque el `static-pie` descarta sus
constructores globales. Con C++ grande la postura por defecto pasa a ser `dynamic` y medirlo — y el
control no es que selle: es CORRER UNA HERRAMIENTA del artefacto.
Y el corolario de por qué nadie lo vio en meses: el único consumidor de llvm21 era rust, que usa
`llvm-config` y las `.a`, nunca una herramienta. **Un artefacto puede estar roto en todo lo que nadie
usa** — como los 44 `cargo-*` sin cargo y los 23 binarios sin cargador.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ef1f9252a1 |
✅ EL TOOLCHAIN COMPILA, ENLAZA Y CORRE — en una caja sin compilador de C
$ cargo build --offline
Compiling prueba v0.1.0 … Finished in 0.44s
$ ./target/debug/prueba
cargo, rustc y ld: los tres nuestros
`cargo` y `rustc` son los que construyó la granja; el enlazador es el `ld` de nuestro `binutils`.
EL MURO 8 SE CERRÓ SIN `lld`. La cadena, que es lo que vale: `-C linker-flavor=ld.lld` → «linker
`lld` not found» (el build con LLVM externo no genera rust-lld); `rust.lld = true` → el bootstrap lo
RECHAZA; `lld21` propio → crasheaba con cualquier entrada; `lld21` con zlib → **no se puede encender
desde ahí**, porque `LLVMConfig.cmake` dice `LLVM_ENABLE_ZLIB 0` y el build standalone lo hereda.
Lo que faltaba NO era un enlazador: era decirle a rustc que enlace en ESTÁTICO. Con `+crt-static`,
rustc usa su propio libunwind/compiler_builtins y deja de pedir `-lgcc` — que es lo que rompía el
intento con `ld`, porque la distro se construye con zig (compiler-rt) y libgcc no existe. Y estático
es el modo natural de musl.
⇒ `scripts/servidor/cargo-config.toml` (instalado en `/etc/cargo/config.toml`) cierra el pendiente
«config del sitio» del §5, con las tres flags y el porqué de cada una.
🧨 Y `lld21` destapó algo que nadie había visto: **las 79 herramientas de `llvm21` están ROTAS**.
`llc --version` crashea igual —también en el worker— porque salen `static-pie` y el enlace estático
descarta los constructores globales de los que dependen `cl::opt` y `TargetRegistry`. Funciona lo
que no los necesita (`llvm-ar`, `llvm-config`) y revienta lo que sí; nadie lo notó porque `rust` usa
`llvm-config` y las `.a`, nunca una herramienta. Queda como deuda DECLARADA: arreglarlo (o encender
zlib) re-hashea `llvm21` y con él `rust`. Hoy no bloquea nada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9f325a3c05 |
🐛 la granja molía UNA receta por cola y por ciclo — el xargs -I{} recibía la cola entera junta
Medido hoy al encolar `lld21` junto a `rust`: el ciclo decía «2 recetas en recipes/incoming» y
construía UNA. La causa está en el filtro de «ya sellado en el hub»: acumula en `filtrada` uniendo
con ESPACIOS, y más abajo la cola se le pasa a `xargs -I{}`, que trata **una línea = un elemento**.
Unidas por espacios, N recetas llegan como UN SOLO argumento:
printf '%s\n' " a.toml b.toml c.toml" | xargs -I{} sh -c 'echo [$1]' _ {}
recibe: [a.toml b.toml c.toml] ← uno solo
⇒ la fase paralela fallaba SIEMPRE con «No such file», y la fase 1b —el reintento serial— salvaba
exactamente UNA receta: la última, porque `basename` de esa ristra da el último nombre. El log
parecía normal, con su ✗ seguido de un ✓ (reintento serial), y ese patrón está en TODAS las colas
(yambar, spidermonkey, rust…). O sea que el reintento serial, que existe para conciliar colisiones
de concurrencia, venía tapando el bug desde que se escribió el filtro.
El arreglo son dos líneas: acumular con salto de línea y descartar la línea vacía con la que arranca
el acumulador (una línea vacía también es un elemento para `xargs -I{}`). Probado en chiquito, en
los dos sentidos: antes 1 elemento, después 3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
24a6dd3b2f |
el cortafuegos puesto en la caja — y el primer baneado fui yo
`table inet tawasuyu_input` con policy drop, 15 reglas de puerto, y el set `ban4` llenandose solo: a los pocos minutos, 27 IPs baneadas y paquetes tirados por el contador. El reemplazo de fail2ban funcionando sobre trafico real, dentro del kernel y sin daemon. 🧨 EL PRIMER BANEADO FUI YO, EN SEGUNDOS. Aplicada la primera version: ssh y https bien, y `http`, proxy y git-ssh muertos; al minuto la caja entera dejo de contestarme. La causa no es una regla mal escrita: **el set @ban4 es UNO SOLO para todos los servicios** y `ip saddr @ban4 drop` esta antes que todo, asi que pasarse de tasa en UN puerto te tira en TODOS — y el `nuevas_por_min: 5` del puerto de administracion es exactamente lo que me pasa a mi, porque cada comando remoto es una conexion nueva. Lo devolvio el INTERRUPTOR DE HOMBRE MUERTO (un `nft flush ruleset` programado a 180 s que sólo se desarma si la verificacion externa sale bien). Sin consola serie, esa es la diferencia entre un susto y un rescue. ⚠ Y el tipo ya lo avisaba: `PoliticaEntrada` tiene un campo `confiables` cuyo comentario describe palabra por palabra lo que me paso. Copie el .ron de EJEMPLO en vez de leer el TIPO. La cura fue una linea con el hub y el worker, que se aceptan ANTES de la logica de tasa. ⚠ `nft -c` rechazo 5 reglas antes de aplicar nada: `ct count over N` es CONFIG_NFT_CONNLIMIT y este kernel no lo trae. Quedan COMENTADAS y no borradas en el fichero que se aplica — lo que la politica declara y el kernel no puede tiene que verse. El simbolo ya entro a la receta del kernel. Y para que sobreviva al reinicio, `nftables` declara su [[service]] con `lifecycle = "oneshot"`: cargar un reglaset no es un daemon, es un acto. Va PRIMERO en el genesis — entre que la red esta y que las reglas cargan, la caja esta abierta. Probado con `nft flush ruleset` + `arjectl start`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ba202d7826 |
atuq: los dieciocho guardianes en UN comando — y el sello llevaba nueve horas atrás
`atuq` es `source.dir` entero, asi que el commit de la boveda de esta manana (`6f0ba974`) movio el ArtifactHash y dejo el artefacto vigente SIN SELLAR. Durante esas nueve horas ningun guardian podia medir: se niegan a correr contra uno que no sea el vigente, que es lo correcto. Pero un guardian que se niega solo grita CUANDO alguien lo corre, y nadie lo corria — el repo se veia sano y la unica senal era un `hash --check` que nadie tenia motivo para teclear. Resellado en 3,7 s (es derivado y el firefox vigente estaba en el store) y corridos los dieciocho sobre `b3:e556024b`: 16 en verde y 2 en ROJO, los dos por la MISMA causa —los tres verbos de la boveda del §7.quinquies—. O sea que la unidad 12 no rompio nada de al lado, que es lo que habia que saber antes de seguir: toco `atuq.cfg`, la politica y el empaquetado de extensiones. `scripts/test-atuq-suite.sh` deja eso en un comando, en serie (cuatro nucleos sin swap; dos navegadores a la vez es un OOM) y cada guardian a SU fichero, porque dos corridas sobre el mismo log se mezclan y el resultado parece un pase. Lo primero que mira es el sello y NO corre nada si falta: verificado contra un store vacio, sale por ahi y devuelve 1. Dos cosas quedan escritas como control y no como adorno: · el numero de rojos esperados va en la cabecera. Un cuadro entero en verde puede ser un producto sano o un runner que no corre nada, y se ven igual; · esa cabecera decia «un rojo» —prediccion escrita antes de correr— y la primera corrida dijo dos: el guardian de coherencia de la boveda lleva el mismo chequeo del cable adentro. Coste medido en gioser: ~29 min. Los cuatro primeros salen en 10 s y no tocan el navegador. |
||
|
|
0a7afebe9c |
la caja tiene HORA, LATIDO y ROTACIÓN — y el respaldo del gitea dejó de ser a mano
`planear.py --revisar` sobre el censo ordena lo que queda, y lo primero no eran los servicios del usuario sino TRES CAPACIDADES que ninguna metrica reclama: el disparador periodico, la hora y la rotacion de logs. Los tres paquetes ya estaban sellados y declarados en `perfil.servidor` desde que ese perfil nacio — lo que faltaba era el eslabon que los ARRANCA, igual que con squid. · `chronyd`: la caja iba **15 s atrasada** y nadie la corregia. Ahora stratum 3 contra los NTP de Hetzner, 0,000005 s de NTP. Sin hora no hay TLS ni firmas, y el sintoma no se parece a la causa. · `crond`: no existia el latido. Verificado con una entrada `* * * * *` en el crontab REAL. · `logrotate`: `/var/log/squid` sin rotar. Diario, con `squid -k rotate` (squid mantiene los ficheros abiertos: un rename a secas lo deja escribiendo en un inode que nadie puede leer). · `scripts/respaldo-gitea.sh` + `17 3 * * *`: cierra el hueco del §6.15 — los 44 repos vivian en UNA copia y el respaldo se hacia a mano. Sube 23+21 repos (1,5 G) y un `gitea.db` de 332 M tomado con `.backup`, consistente con el servidor vivo. El control no es que el script salga 0: es CONTAR los repos de los dos lados. `chrony` y `cronie` NO declaraban `[[service]]` — el paquete llegaba a la imagen y no lo arrancaba nadie. Ahora lo declaran (fuera de `hash_inputs`: los hashes no se movieron) y el perfil los habilita. TRES MEDICIONES que valen mas que el resultado: 1. **La deriva era de gioser, no de la caja**: despues de sincronizar, el HUB quedo 3 s adelantado. Medir «contra el otro» sin un tercero no dice quien esta mal. 2. **El spool de cronie es `/var/spool/cron/<usuario>`**, no `…/crontabs/<usuario>`: `crontab -l` mostraba las entradas y `crontabs/` estaba vacio. Un crontab restaurado en el sitio equivocado es un fichero perfecto que nadie lee. 3. 🧨 **`sqlite3` seguia INERTE en la caja** (`compressBound: symbol not found`): el ultimo de los 23 del §6.4. Le faltaba `zlib-shared`, sellado y declarado en `perfil.base` desde el 11-09 — pero la caja se instalo antes. Sin el no habia snapshot consistente. Un paquete declarado no es un paquete instalado: en una caja viva vale lo que el `upgrade` proyecto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
db24d206dc |
arnés de la imagen: el piso del sello se COMPRUEBA — un piso que falla callado no existe
Los 180 s de `PISO_SELLO` son de esta máquina (TCG sin KVM y con la carga que tenga el anfitrión, igual que los +97/+120/+137 de la pintura). Con el anfitrión cargado el mismo arranque puede no llegar al gancho que sella el éxito, y entonces la corrida suma al contador de caídas y envenena la siguiente EN SILENCIO — que es exactamente como empezó todo esto. Ahora que `toolkit.startup.last_success` se lee antes y después, eso se puede decir, así que se dice: ✓ el arranque SELLÓ el éxito (last_success X → Y): esta corrida no envenena la siguiente ⚠⚠ el sello NO se movió: este arranque no terminó ⇒ la corrida SUMA al contador. Subí PISO_SELLO Es la diferencia entre un piso confiado y un piso comprobado, y cuesta cuatro líneas porque el dato ya estaba medido y guardado. |
||
|
|
5b47cd596c |
atuq: el reloj del sello es el del LANZAMIENTO, no el de la pintura — y el piso va en el arnés
Leyendo el perfil en cada captura, el sello (`toolkit.startup.last_success`) y la limpieza del contador llegan al disco entre +100 s y +137 s DESDE EL LANZAMIENTO, no desde el primer cuadro. Y es UN gancho y no dos: el mismo arranque que limpia escribe el sello, con su propia hora de arranque. Las dos cotas salen de corridas independientes, que es lo que hace al intervalo creíble: · a los +100 s NO había sellado — la corrida S1 vivió 100 s desde el lanzamiento (primer cuadro a +97 s, fin del proceso tres segundos después, medido con los tiempos de sus propios ficheros); · a los +137 s YA había sellado — la corrida con `--watch-prefs` lo ve aparecer en esa captura. S1 parecía la excepción del modelo y resulta ser su cota inferior: si el reloj fuera el de la pintura tendría que haber sellado, y no selló. La consecuencia es contraintuitiva y es la que muerde: cuanto MÁS RÁPIDO pinta, más fácil es envenenar el perfil, porque `--until-paint` corta antes. La serie de ayer no se rompió por ser larga sino por ser rápida. Por eso la regla va en el arnés y no en un runbook: `PISO_SELLO = 180`, margen sobre la cota alta, y `--until-paint` no corta antes de ese piso. El comentario del piso lleva los dos números medidos al lado — un piso que se lee como elegido lo baja el próximo que lo vea. Las corridas `cierre-1`/`cuanto-1`, el mando `--watch-prefs` y el piso los midió y escribió la otra sesión del frente; acá van con la cota de S1 que los cierra por abajo. |
||
|
|
1de1fc31f6 |
atuq: lo que limpia el contador es SOBREVIVIR un rato después de pintar, no pintar
El par aísla la variable —mismo pin, mismo perfil, misma imagen, y la única diferencia es cuánto vive el navegador después del primer cuadro—: S1 +97 s, muerta en el acto (`--until-paint`) ⇒ contador después: 1 cierre-1 +120 s, ~360 s de vida después de pintar ⇒ contador después: AUSENTE Con eso queda contestada la pregunta que dos secciones atrás este documento daba por cerrada y no lo estaba, y A deja de ser una anomalía: también siguió viva minutos después del primer cuadro. Se dice además lo que la tabla NO prueba: si ese arranque además SELLÓ el éxito. El volcado del «después» sólo grepeaba el contador; ahora `toolkit.startup.last_success` va en los DOS volcados y se guarda en la medición (`last_success_antes` / `_despues`), que es lo que faltaba para no repetir la corrida. Si el sello se mueve, es un gancho que sella y limpia a la vez; si no, son dos. Y queda escrito que el 16 sigue sin explicación, ahora con una contradicción medida encima: desapareció en una corrida diálogo + ATUQ-EXIT=0, y las fases k2/k3 del umbral fueron diálogo + ATUQ-EXIT=0 y no limpiaron nada. La respuesta buena no tapa la sucia. La corrida `cierre-1` y el mando `--watch-prefs` son de la otra sesión del frente. |
||
|
|
c30905e2b9 |
atuq: vigía de las dos puntas del cable — y el extractor de verbos que no veía la mitad
El chequeo del commit anterior miraba una extensión. El cable tiene la misma forma para las diez, y
este vigía lo recorre entero: qué verbos manda cada extensión y si el commit que `recipes/
puriy-costura.toml` pinea los atiende. Corre en un segundo y no construye nada.
Medido hoy, 13 verbos sobre 8 extensiones: sólo los TRES de la bóveda están sin dueño. O sea que
esto no se podía deducir mirando cuándo se tocó la receta por última vez — las otras nueve
extensiones conviven bien con el mismo pin viejo.
⚠ Y lo que costó más que el vigía: **el extractor de verbos no veía la mitad de lo que hay**. Estaba
buscando `verb: "…"`, que es como lo escribe la bóveda. Pero la extensión de IA arma el mensaje con
`postMessage({ id, verb: verbo, … })` —el verbo llega por VARIABLE—, así que ese patrón encontraba
cero verbos en `ia` y la declaraba sana sin haber mirado nada: cero verbos encontrados y cero verbos
faltando son el mismo cero con dos causas. Ahora recoge toda cadena con forma `ns.verbo` y descarta
las que son nombres de fichero, que es lo único que se les parece. Con eso aparecen `ai.ask` y
`archive.ask`, que antes no estaban en ninguna cuenta.
De ahí sale el tercer control, que no es negativo ni positivo sino de COBERTURA: una extensión de la
que no se extrae ningún verbo se REPORTA. Y las dos que de verdad no le hablan al host —`inicio` y
`proxy`, que viven enteras dentro del navegador— están nombradas en el código, para que «no manda
verbos» no se pueda confundir con «el extractor se quedó mudo».
Los otros tres controles: `sct.observe` tiene que aparecer en el commit pineado (si no, lo roto es
el vigía y lo dice); `--negative-control` agrega un verbo inventado a cada extensión y exige que lo
vea; y si el clon de tawasuyu no está, o no conoce el commit pineado, esto FALLA — «no se pudo
comprobar» no es «está bien». Los cuatro probados, incluida la rama del commit desconocido.
Pregunta por `git grep '"<verbo>" =>' <commit> -- '*.rs'` leyendo del objeto: sin checkout, porque
el árbol de tawasuyu es compartido y siempre tiene ficheros en vuelo de otras sesiones.
|
||
|
|
e7369efe82 |
atuq: la bóveda le habla a un host que no sabe sus verbos — y eran CINCO lugares, no cuatro
Este guardián nació esta mañana diciendo que una extensión está enchufada en CUATRO sitios y que
ninguno da error si falta. Los cuatro estaban bien. El quinto no, y es el que hoy está roto.
El quinto lugar vive en otra receta y en otro repo: `recipes/puriy-costura.toml` pinea un commit de
tawasuyu, y es ESE commit el que decide si `vault.match` es un verbo o una cadena que nadie atiende.
Medido, con su control al lado, sobre el artefacto SELLADO y vigente (`hash --check` dice SELLADO
para `b3:1eb2b692`):
strings del binario sellado → «vault» 0 veces · «cas» 5 veces ⇐ EL CONTROL
El cero solo no probaba nada —un grep que devuelve cero puede ser una ausencia o un sitio
equivocado, y las dos se ven igual—; lo que lo convierte en evidencia es el «cas» que TENÍA que dar
positivo en el mismo binario. Y del lado de la fuente sale lo mismo: la receta pinea `e19bb0e5`, que
es ANCESTRO de `bc6903f9e` —el commit que agregó los verbos—, así que el host sellado es anterior a
la bóveda por construcción.
Cómo se ve ese fallo desde la silla del usuario, que es por qué esto merece guardián: el manifiesto
está, el permiso está, la política instala, `connectNative` CONECTA, la extensión manda
`vault.match` y el host contesta `verbo desconocido`. `fondo.js` lee `r.ok !== true`, borra la
insignia y se calla. Un navegador sin bóveda, y todos los ficheros en orden.
El chequeo pregunta `git grep '"<verbo>" =>' <commit> -- '*.rs'` sobre el clon, sin checkout: el
árbol de tawasuyu es compartido y siempre tiene ficheros en vuelo de otras sesiones. Los verbos los
saca de `fondo.js` y no de una lista escrita acá, que se desincronizaría justo en el único sitio
donde eso no se ve.
Tres controles, porque el chequeo es un grep que espera cero:
- POSITIVO: `sct.observe` tiene que aparecer en el commit pineado. Si no aparece, lo roto es el
guardián y lo dice así, en vez de acusar a la receta;
- `--negative-control-verbo`: le agrega un verbo inventado y exige que lo detecte;
- y si el clon no está, «no se pudo comprobar» NO es «está bien»: falla ruidosamente.
Lo que este commit NO hace: subir el pin. Eso es su propia unidad y tiene un muro medido delante —el
`Cargo.lock` de tawasuyu en `main` todavía describe a `puriy-costura` con sus once deps viejas, sin
`pacha-boveda` ni `pacha-cifrador`, y `pacha-boveda-daemon` no está en el lock en absoluto. Con eso,
`cargo vendor --locked` muere sin nombrar al culpable, que es exactamente la trampa que la propia
receta dejó escrita.
|
||
|
|
98c9e4daa6 |
atuq: la tasa con perfil sano es +97 y +98 s — y PINTAR NO ES TERMINAR el arranque
Cuatro arranques fríos en serie, el perfil partiendo del 5 que dejó la corrida del umbral: S1 pin 0 antes 5 después 1 el navegador ✓ +97 s S2 sin pin antes 1 después 2 el navegador ✓ +98 s S3 sin pin antes 2 después 3 (matada a los 60 s) S4 sin pin antes 3 después 4 EL DIÁLOGO ✗ en 420 s Tres cosas: · la tasa, por fin con perfil sano: dos corridas independientes y pegadas, +97 y +98 s, contra los +123/+195/+197/+204 de antes. Con el anfitrión descargado el mismo artefacto pinta en la mitad del tiempo ⇒ el número es de la máquina y la carga tanto como del producto, y se publica con sus condiciones; · el umbral se reprodujo solo y en otra secuencia: S4 arrancó con 3, sumó a 4 y abrió el diálogo — segunda medición independiente del mismo borde, esta vez identificando la ventana por `min_size` y no por el título (que está traducido, así que un grep por «Troubleshoot Mode» en una imagen en castellano no engancha y su ausencia se lee como la conclusión contraria); · ⚠ y una corrida que PINTA no limpia el contador: S2 pintó y dejó 2. Más todavía, `toolkit.startup.last_success` vale lo mismo en las CUATRO (1789494795, las 17:53) ⇒ el gancho de cierre no corrió ni una vez, ni en las que pintaron. Pintar no es terminar de arrancar. Entonces «quién limpia el contador» SIGUE SIN MEDIR, y el propio documento decía que esta serie lo cerraría: no lo cierra, y queda escrito así. Hace falta una corrida que deje al navegador terminar de arrancar y salir limpio. Lo más caro es para el instrumento: con `--until-paint` cada corrida suma uno, pinte o no, así que una serie se rompe sola a la cuarta — que es lo que pasó ayer sin que nadie lo viera. El arnés ahora AVISA cuando el contador está en 3 («esta corrida va a abrir el diálogo, no el navegador») y la receta para una serie queda escrita: pinnear `--crashes 0` en cada corrida, que resetea la base (S1 lo muestra partiendo de 5). También cae una candidata mía, con un cero que viene con control (medido por la otra sesión): el pref no tiene default en este build, así que fijarlo en 0 SÍ se persiste y el «ausente» no era el pin. |
||
|
|
7997464337 |
atuq: el umbral son 4 — y mostrar el diálogo NO limpia el contador, como este documento decía
Medido en los dos lados, partiendo del perfil en 2 y con tres fases más en un arranque: el que encuentra 2 guardado PINTA (y queda en 3); el que encuentra 3 lo sube a 4, compara 4 > 3 y abre el diálogo (y queda en 4); el siguiente queda en 5 y vuelve a abrir el diálogo. O sea que la comparación va DESPUÉS del incremento del propio arranque: contando desde la última corrida que terminó bien, son cuatro arranques interrumpidos seguidos y el quinto lanzamiento ya no abre el navegador. `toolkit.startup.last_success` vale lo mismo en las tres fases (1789494795, el arranque de la última corrida sana): el contador sube mientras esa marca no se mueve. Con las dos cifras juntas, un contador que no cambió deja de ser ambiguo entre «no subió» y «subió y se limpió». ⚠ Y eso refuta una atribución que yo había escrito ayer y que estaba en tres sitios: mostrar el diálogo NO limpia el contador (k2 lo dejó en 4, k3 en 5). Queda retirada del SDD y del comentario del arnés. La desaparición del 16 entre las 17:12 y las 17:20 pasa a ser un hecho SIN explicación medida, con dos candidatas escritas y ninguna dada por buena — y una de ellas es mía y fea: todas las corridas que pintaron llevaban `--crashes 0` pinneado, que es el valor por DEFECTO, y un pref igual al default no se persiste. El «ausente» puede ser eso y no una limpieza. Queda dicho cómo se cierra: dejar el contador en 2 SIN pinnearlo, correr hasta que pinte y leerlo después. Un discriminador que salió gratis: `ATUQ-EXIT=137` es «la matamos» (128+9) y `ATUQ-EXIT=0` es «se fue sola» por el diálogo. El código de salida contesta lo que una captura no podía. Medido por la otra sesión del frente (work/umbral-1.txt), con la predicción escrita antes de mirar. |
||
|
|
2f90d8eb40 |
arnés de la imagen: el contador de caídas viaja con la medición, y las envenenadas se apartan
`atuq-en-imagen.py` ahora lee `toolkit.startup.recent_crashes` del perfil ANTES y DESPUÉS de cada corrida y lo anota junto al tiempo de primera pintura (`recent_crashes_antes` / `_despues` / `_fijado`). Los dos volcados, no uno: Gecko BORRA el contador al mostrar el diálogo de Modo de resolución de problemas, así que un perfil envenenado se ve limpio si se lo mira después — y la corrida siguiente parece arreglarse sola mientras la anterior parece intermitente. Con eso, `vigia-imagen.py` saca de la tasa las corridas con el contador > 3 (ahí el sujeto no es el navegador sino un diálogo modal) y para las 21 anteriores al campo dice que NO SE PUEDEN CLASIFICAR en vez de contarlas como buenas. Un denominador se marca, no se borra. Mandos y guardas nuevas: · `--crashes N` fija el contador en el `user.js` del perfil: 0 limpia, >3 reproduce. Es el control en los dos sentidos, que es lo único que distingue causa de correlación con suerte; · `WidgetScreen:5` en el MOZ_LOG y una sonda `ATUQ-DIAG` en la propia página (screen/outer/inner por `dump()`), que es lo que refutó la carrera con `wl_output`; · la lista ordenada del protocolo ya no se ahoga en los ~60 modos que anuncia QEMU —el `head` cortaba antes del `set_window_geometry`— y trae `set_title`/`set_app_id`, que es lo que identificó la ventana; · si QEMU no arranca, se dice en el primer segundo leyendo `qemu.log`. El fallo real es «Failed to get write lock» con otra VM sobre la misma imagen, y sin esta guarda se veía como diez minutos esperando una marca del serial: el socket queda creado y el `connect()` funciona. Las fases (`--xulstore`), el `ATUQ-EXIT=$?`, la copia del log del arranque anterior y el `find` del perfil sobre la raíz entera los escribió la otra sesión que trabaja este frente; conviven acá porque el fichero es compartido. |
||
|
|
6f0ba974c7 |
feat(atuq): la boveda de la suite guarda las contrasenas, y el gestor de Gecko se aparta
La decima extension de atuq. Ofrece la credencial que corresponde al sitio abierto y la pone en el formulario, pero NO puede sacar una contrasena de la boveda por su cuenta: pregunta cuales corresponden —y eso contesta titulo y usuario, nada mas— y la contrasena sale por otro verbo que pregunta en el escritorio antes de contestar. Si la extension queda comprometida, o si una pagina consigue hablarle, lo que obtiene es la lista de titulos de los sitios que coinciden con su propia direccion. La direccion siempre la pone el chrome y nunca la pagina. Lo unico que el trozo que corre dentro del sitio aporta es que HAY un formulario y lo que el usuario tipeo; si pudiera decir de quien es la pagina, podria decir que es el banco. Y la extension no decide nada: quien sabe que credencial va en que sitio es la funcion del otro lado del cable. Ponerlo en JS seria reimplementar el original y, peor, poner en la pagina la decision de a quien se le entrega una contrasena. EL GESTOR DE GECKO SE APAGA, pero como valor de arranque y no como politica. Dos gestores peleando por el mismo campo es la peor experiencia que puede tener alguien que solo quiere entrar a su correo: el sitio se rellena dos veces, o ninguna, y no hay forma de saber cual tiene la buena. De fabrica manda la boveda; el que prefiera el de Gecko lo prende y listo. Prohibirselo seria decidir por el, que es lo que este fichero dice en su cabecera que atuq no quiere ser. Y un guardian nuevo, que corre en un segundo y sin construir nada. Una extension esta enchufada en CUATRO lugares distintos y ninguno da error si falta: el identificador que ella declara, la politica que la instala, el permiso para hablarle al host, y las preferencias que la acompanan. Si falta la politica no se instala. Si falta el permiso se instala y se conecta a nada, en silencio, y la insignia no aparece nunca. Si faltan las preferencias los dos gestores se pelean. Ninguno de los tres se ve como un error: se ven como "no anda". El guardian los mira todos y trae su control negativo, que le saca el permiso del host y exige que lo detecte. No reemplaza al guardian de metal —servidor, navegador real, login real, el dialogo a la vista—: lo precede, y ahorra descubrir construyendo que faltaba un renglon en un JSON. Los cinco verbos del otro lado ya estan en tawasuyu (bc6903f9e), con trece tests que leen el cable. |
||
|
|
798cfab7e8 |
atuq: el 117x70 lo pide el CLIENTE, no el compositor
Ordenando el protocolo numerado en los dos sentidos: cosmic-comp manda configure_bounds(0,0)+configure(0,0) («elegi vos»), el cliente contesta set_min_size(117,37), set_max_size(348,16332) y set_window_geometry(117,70), y RECIEN ahi el compositor devuelve configure(117,70) teniendo 1280x692 para dar. No hay nada que arreglar en COSMIC. El MOZ_LOG dice como: «Initial resize to 1 x 1» — Gecko crea la ventana de 1x1 y nunca la agranda; 117x70 es lo que queda cuando el chrome se mide a si mismo. El frente se mueve a Gecko. |
||
|
|
f4e1e99f19 |
atuq: la ventana SI esta — mide 117x70 px (lo dice el protocolo)
Como cosmic-comp no emite DEBUG (macros compiladas fuera), se le pregunto por el protocolo con WAYLAND_DEBUG=1. Manda configure_bounds(0,0) al mapear, despues bounds(1280,692) y configure(117,70): la ventana queda de 117x70. El pixel lo confirma — restando el cuadro de antes aparece un cluster de 286x86 con la decoracion de COSMIC en (496,115). Cae la hipotesis del §6.10.nonies: hay 43 wl_callback.done, o sea que SI hay frame callbacks. Y la tasa 2 de 10 no medía «pinto/no pinto» sino «ventana grande / ventana de 117x70». Arnes: --wayland-debug y --set-rust-log (perilla por /etc/cosmic-mode, que cosmic-start lee antes de RUST_LOG); margen de espera 420->780s porque con carga ~13 la VM no llegaba al shell. |
||
|
|
21381b598f |
atuq: la serie con UNA sola tarjeta — 2 de 10, y el hueco resulta real
Con -vga none la captura ya no tiene punto ciego, asi que un ✗ significa «la ventana no esta». Clasificado el ultimo cuadro de las 10: 2 con la pagina pintada, 1 con la ventana en blanco y 7 con SOLO el escritorio. Las dos salidas explicaban los ✗ inflados de las series VGA=1, no el fenomeno. El MOZ_LOG separa los dos regimenes: las que tienen ventana escriben 952-1505 lineas hasta el apagado; las 7 sin ventana se cortan a los ~40s y quedan 6 minutos de silencio, sin crash (WebRender arranca). Quedan como hipotesis NO medida los frame callbacks que el compositor no manda. Ademas: tasa-primera-pintura.sh nombra el log por VGA (la serie nueva habia sobrescrito los crudos de la anterior). |
||
|
|
77732edead |
hydrate-profile: encontrar el binario donde ESTÉ — no sólo en el árbol de desarrollo
`HAMMER` estaba cableado a `ROOT/target/release/takana`, así que el guión no corre en una caja
INSTALADA, donde el binario es `/usr/bin/takana` y `target/` ni existe:
FileNotFoundError: [Errno 2] … '/opt/takana/target/release/takana'
Es la misma deuda que este frente ya pagó dos veces —`respaldo-storagebox.sh` y el «unhashable 875»
con el lab perfecto—: el instrumental asume que el hub es un árbol de desarrollo. Ahora busca en
`$TAKANA`, `$HAMMER`, el árbol de desarrollo y el `PATH`, en ese orden.
Salió al ir a actualizar la caja de producción a la imagen nueva, que es exactamente el caso de uso
para el que no servía.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
|
||
|
|
5b98d1b2bf |
atuq: no era el perfil — son DOS SALIDAS, y el «3 de 13» medía la cámara
Dos cosas antes de la medición: el MOZ_LOG de las que pintan y las que no termina IDÉNTICO (las dos
commitean WaylandBufferSHM de 1280x696), y el cruce de las 15 corridas está confundido — las 13 de
«perfil del usuario» salieron todas con VGA=1.
`screendump` de QMP fotografía UN dispositivo. Con VGA=1 hay dos, y hasta ahora se miraba uno. Con
`id=` en cada uno y capturando los dos en cada toma, en la misma corrida y el mismo instante:
+312s ██ PRIMERA PINTURA — 704456 px EN vga0
gpu0 = 962675 px de fondo + panel y dock (el escritorio, SIN navegador)
vga0 = 703766 px magenta (la ventana, con la página)
⇒ cosmic-comp maneja las DOS salidas y la ventana cae en una u otra. «Pinta 3 de 13» era cuántas
veces cayó en la pantalla que yo fotografiaba: una tasa de mi cámara, no del producto. El §6.10.ter
tiene la misma explicación (aquel arranque también traía `drm: card0 card1`).
Queda en pie, ya sin confundido: el navegador arranca, mapea y commitea SIEMPRE; cuando la ventana
está en la pantalla que se mira, se ve entre +184 y +312 s; el perfil no era la causa y cosmic-comp
nesteado tampoco.
⚠ Tercera vez en el día con la misma forma: un ✗ de una captura de UNA pantalla no dice «la ventana
no está», dice «no está en ESA» — igual que `mapped 1` no probaba que se viera. El estado anota ahora
en qué pantalla apareció y cuántas se fotografiaron, y el vigía NO cuenta las ciegas: las nombra.
|
||
|
|
49b2842ddb |
atuq: la TASA — pinta 3 de 13 con el perfil del usuario, y cuando pinta siempre a ~200 s
Diez corridas idénticas con `scripts/cosmic/tasa-primera-pintura.sh` (--as-user, VGA=1, ventana 480 s,
captura cada 30 s), más las cinco anteriores, todas anotadas en docs/state/primera-pintura.json:
perfil del usuario 3 de 13 pintaron 184, 199, 204 s
perfil nuevo en tmpfs 2 de 2 197, 197 s
Dos cosas que la tasa dice y una anécdota no podía: cuando pinta, pinta SIEMPRE en la misma ventana
(184…204 s) y nunca a los 300, 400 ni 900 ⇒ no es una cola larga, son DOS REGÍMENES —o sale a los
~200 s o no sale—; y con el perfil del usuario falla ~3 de cada 4, mientras que con perfil nuevo en
tmpfs no falló (2 de 2, pocas corridas para afirmar que nunca, suficientes para saber dónde mirar:
qué hace el primer arranque del perfil que el perfil ya hecho no hace).
⚠ Condición de la medida, parte del número: las diez salieron con el anfitrión a load ~10 (tanda de
KDE de otro agente + una VM de otra sesión, en 4 cores). El número es un PISO, no una constante.
⚠ Y una trampa del arnés medida en las corridas 8–10: con esa carga el guest tarda más de 420 s en
llegar al shell y los `esperar` vencen. No las invalida —se verificó una por una que el navegador se
lanzó y dejó su MOZ_LOG de 1200–5000 líneas— pero una corrida abortada de verdad se ve casi igual:
por eso el guion cuenta las abortadas APARTE en vez de sumarlas a los fallos.
|
||
|
|
79a9ac5bda |
farm: sembrar-fuente.sh — el worker construye lo de tawasuyu SIN tener la clave
El worker no puede clonar el gitea de tawasuyu y **no debe poder**: esa clave es el SSH de todo y
él sólo necesita leer un repo. Lo que viaja es el ÁRBOL, no la credencial: el hub —que sí la
tiene— clona, y el guión manda el mirror.
⚠ Y copiar el mirror tal cual NO alcanza, que es lo que costó descubrir: el fetch de takana clona
con `--filter=blob:none`, así que el mirror copiado **no tiene los blobs** y los va a buscar a un
remoto que allá no responde. El síntoma no nombra la causa:
tar: This does not look like a tar archive
Error: git archive | tar -x falló (tar exit Some(2))
Por eso el guión HIDRATA (`fetch --refetch --no-filter`, 17 M → 210 M) y verifica con la prueba del
CONSUMIDOR —`git archive` de verdad, en el worker— y no con `cat-file -e`, que pasa igual sobre un
mirror parcial. Más el `chown -R root:root`, sin el cual git rechaza el repo por «dubious
ownership» cuando el builder corre como root: falla al construir, no al copiar.
Dos bugs propios, cazados corriéndolo:
· `grep -q .` sobre la salida de `git archive` decía «vacío» con un archive perfectamente bueno: es
un tar BINARIO y puede no traer un salto de línea en los primeros bytes. Va `wc -c`.
· `head -c` cierra el pipe, `git archive` muere con SIGPIPE y con `pipefail` eso hacía fallar el
pipeline entero: el guión se cortaba EN SILENCIO justo en la línea que dice que verifica. Un
verificador que aborta sin decir nada es peor que no verificar.
Probado con `recipes/arjectl.toml`: idempotente (segunda pasada no reclona) y el worker extrae el
árbol. Y el control del contrato: con una receta de tarball dice que no es una receta git y sale.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
|
||
|
|
fd09ca77b0 |
arjectl en la imagen: relanzar en caliente — y el genesis NO alcanzaba para eso
El servicio que falla y agota su backoff sólo se podía recuperar reiniciando la máquina entera.
Ya no. Y no hubo que escribir nada: **el cliente existía en tawasuyu y el crate se llama `arje-ctl`**
(el binario, `arjectl`). Buscarlo por `arjectl` no lo encontraba, y de ahí salió mi conclusión falsa
de que había que implementarlo — el protocolo ya traía ListEntes, SpawnCardFromDisk,
StopCardFromDisk, KillEnte y EnteStatus.
`recipes/arjectl.toml` lo construye del MISMO commit que `arje-zero` (98db584f), y no por comodidad:
el bus es un protocolo entre dos binarios y un cliente de otro árbol puede conectar sin entenderse
con el init. Publica sólo `arjectl`; el crate también produce un `systemctl` de camuflaje que acá no
se instala — en una distro sin systemd, ese nombre en el PATH invita a escribir runbooks con el
verbo ajeno.
⚠ EL HUECO QUE SÓLO SE VE USÁNDOLO: el genesis de la seed dice qué arranca AL BOOT, pero
`start`/`restart` usan `SpawnCardFromDisk`, que lee `/etc/arje/cards.d/<label>.json` — y el armado
no lo escribía:
$ arjectl start gitea
Error: arje-zero rechazó: card gitea: No such file or directory
(buscada en /etc/arje/cards.d/gitea.json)
Son dos preguntas distintas —qué arranca solo, y qué se puede encarnar a pedido— y arje las responde
desde sitios distintos. `inyectar-cards.py` escribe ahora los dos árboles, y en `cards.d` escribe
TODAS las cards, no sólo las nuevas: `sshd` viene del product-rootfs y tampoco era relanzable.
Medido con la VM arrancada UNA sola vez: la imagen trae `cards.d/{gitea,sshd}.json` · `list-units`
da PID/CPU/MEM/HILOS/reinicios · poner el `app.ini` + `arjectl start gitea` ⇒ **GET / 200 sin
reiniciar** (uptime 6 min) · `arjectl restart gitea` cambia el PID (118 → 192) y sigue en 200.
⚠ Y un aviso que costó un HTTP intermitente: **`arjectl start` sobre un Ente YA VIVO lo DUPLICA** —
`SpawnCardFromDisk` no deduplica por label y arje le da un ULID nuevo. En gitea el síntoma fue
`unable to lock level db … resource temporarily unavailable` y un `[F]`: dos servidores peleando por
el mismo estado. Para relanzar se usa `restart`, o se mira `list-units` antes. Un `start` idempotente
es trabajo de arje, no de esta imagen.
⚠ Deuda anotada, no barrida: `arjectl` va en `perfil.servidor` porque este frente es el que lo pagó.
TODA imagen de takana corre arje-zero como PID 1 y ninguna se puede operar sin él ⇒ el argumento
para subirlo a `base` es fuerte, y es una línea. Se deja como decisión.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
|
||
|
|
467a87cdb8 |
la imagen del servidor, armada y booteada — tres fallos que sólo aparecen ahí
Se armó la imagen del perfil `servidor` con gitea y se arrancó en QEMU. Los tres hallazgos, en el orden en que aparecieron, son los que justifican probar la imagen en vez de dar por buena la receta. 1) ⚠⚠ ESCRIBIR EN EL ROOTFS HIDRATADO ES ESCRIBIR DENTRO DEL STORE `takana users --merge` hacía `fs::write` sobre `<rootfs>/etc/passwd`. Un rootfs hidratado se arma con HARDLINKS contra el store: medido, ese fichero y el del artefacto `product-rootfs` eran **el mismo inode (1225824, 2 links, modo 444)**. Un `write` habría modificado el artefacto SELLADO, y todas las imágenes futuras habrían salido con la cuenta metida dentro del producto. Acá se salvó porque el store es de sólo lectura y salió `Permission denied` — confiar en eso es confiar en un permiso. Ahora `escribir_rompiendo_hardlink()`: temporal + `rename`. Con su control, que afirma lo que importa: tras escribir, el fichero del store **conserva su contenido**, baja a 1 link y el del rootfs tiene otro inode. Verificado también sobre la imagen real. 2) LA IMAGEN TRAÍA EL BINARIO, LA CUENTA… Y NADIE LO ARRANCABA Primera imagen: `gitea` instalado, `gitea:x:916` en `/etc/passwd`, y en el `genesis` de la seed sólo `sshd`, `console-getty`, `hammerd`, `hammer-product`. `servidor-image.sh` no inyectaba las Cards — eso sólo estaba en el camino de las imágenes de escritorio. Y ninguna métrica lo dice: `--services` responde que el perfil lo habilita, y lo habilita; lo que faltaba era el paso que lleva esa declaración a la imagen. Ahora llama a `service-cards` + `inyectar-cards.py` (idempotente por label). 3) ⚡ EL BINARIO MORÍA CON `trap invalid opcode` — Y EL BUG ESTABA EN EL BUILDER Con la card en el genesis y la config puesta, gitea arrancaba y moría al instante: traps: gitea[91] trap invalid opcode ip:79ea992 ... in gitea[...] El sandbox exporta `CC` apuntando a un wrapper que pone `-mcpu=baseline` (`sandbox.rs` ya avisaba: «de paso cierra el SIGILL de AVX en qemu64»), y la fase Go del propio builder lo PISABA con `CC="zig cc"` a secas — que por defecto es `-mcpu=native` y hornea la ISA del que compila. El binario corría perfecto en el worker y moría en QEMU-TCG. No falla al compilar ni al sellar: falla al EJECUTAR en otra CPU. Y el `ArtifactHash` no lo puede cazar, porque la CPU del builder no entra en `hash_inputs` — dos workers distintos sellan bytes distintos bajo la misma dirección. Arreglado en el builder y en la receta; re-hashea las CINCO recetas `cgo = true` (gitea, usql, sq, gocryptfs, naabu), que es correcto: lo que había sellado no es portable. gitea: `b3:391a613e…` → `b3:5edf9c16…`. Lo verificado en la VM: PID 1 = arje-zero · la cuenta de `[[user]]` en `/etc/passwd` y `/etc/group` de la imagen · la card de gitea en el `genesis` · y arje encarnándola, con la guarda saliendo 78 y `/var/log/arje/ente-gitea.log` diciendo exactamente «falta /etc/gitea/app.ini — es config del SITIO». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn |
||
|
|
c14db82263 |
atuq: el tiempo de primera pintura, en el vigía de la imagen — y es 3 de 5, no un número
`scripts/cosmic/atuq-en-imagen.py` ya no contesta «pinta / no pinta» sino CUÁNTO TARDA: mide cada
captura, imprime `+Ns ██ PRIMERA PINTURA`, acepta `--budget` (def. 420 s) y `--until-paint`, y sale
≠0 si no pintó o si se pasó del presupuesto. El magenta se cuenta CON TOLERANCIA porque `cosmic-idle`
atenúa la pantalla al 46 % a los ~+590 s y un contador por color exacto lo lee como «desapareció».
El número se publica en `docs/state/primera-pintura.json` y `scripts/vigia-imagen.py` lo informa como
sexto dato, marcado como «no lo mide este vigía» (arrancar la imagen son ~15 min sin KVM). Se ACUMULA
una entrada por corrida, no se pisa, y el vigía informa la tasa:
⚠ pintaron 3 de 5 corridas · primera pintura +197…+204s
⚠ Y eso corrige lo que publiqué hace una hora. Con cinco corridas sobre la MISMA imagen: pintó en
tres (+197, +197, +204 s) y NO pintó en dos —una con 900 s de observación—, siendo la 4 y la 5 el
mismo comando. El fallo es INTERMITENTE: ni «nunca pinta» ni «sólo tardaba».
El argumento que me llevó a «tarda» era el MOZ_LOG (`mapped 1` + «has buffer» + WaylandBufferSHM
commiteado). La corrida 5 lo refutó: commiteaba cuadros desde ≤+377 s con la pantalla vacía a los
900 s. Que el cliente se crea visible NO es evidencia de que se vea; la evidencia es el píxel. Queda
escrito en el vigía, al lado de la tasa, para que no se vuelva a usar como prueba.
De paso, las flags nuevas nacen en inglés (CLAUDE.md §4): --as-user, --wait, --budget, --until-paint,
--only-cosmic; los mensajes siguen en castellano.
|
||
|
|
d611f8577d |
[[user]] en la receta: la otra mitad del SDD 30 — un demonio que no corre como root necesita cuenta
El SDD 30 sacó las Cards de una constante de Rust y las puso en la receta. Los USUARIOS se quedaron
donde estaban las Cards: `/etc/passwd` de la imagen es `takana_bootstrap::PRODUCT_PASSWD`, un literal
con `root` y `sshd`, y añadir un tercero era editar Rust y recompilar takana.
No es simetría por elegancia, es un servicio que no arranca: `gitea` se niega a correr como root, su
Card hace `setuidgid gitea`, y sin la cuenta arranca, muere y reintenta para siempre con un log que
dice `unknown user` — no «a la imagen le falta una cuenta».
[[user]]
name = "gitea"
uid = 916
home = "/var/lib/gitea"
**El uid se declara, no se asigna**, por la misma razón que el ULID de la Card: un «primero libre a
partir de 1000» hace que dos imágenes del mismo perfil salgan con dueños distintos y el rootfs deje
de reproducir SIN QUE NADA FALLE — los ficheros se ven iguales y `ls -l` dice otro número. Fuera de
`hash_inputs`, medido: el hash de gitea no se movió (`b3:391a613e…` antes y después).
`takana users <recetas…> [--merge <rootfs>]` es el gemelo de `service-cards`, y fusiona **por clave,
no por línea entera** — componer dos veces no duplica, y `grep -q` de la línea completa no serviría
porque la misma cuenta con otro GECOS se leería como nueva. Tres decisiones con su control:
· Una cuenta ya presente con OTRA línea es CONFLICTO y no se pisa: sale ≠0. Pisarla es cambiarle el
uid a ficheros que ya son de alguien, y eso se descubre dentro de la VM.
· Se planea todo y sólo entonces se escribe. Fichero a fichero, un conflicto en `passwd` dejaba el
`group` ya escrito: media cuenta es peor que ninguna, porque parece que está. Comprobado con el
caso exacto — `group` sin la cuenta, `passwd` con otro uid — y los DOS ficheros quedan intactos.
· Un `passwd` ausente es un error, no un fichero a crear: crearlo dejaría una imagen SIN `root`.
La validación rechaza lo que rompe tarde: `root`/`sshd`/`nobody` y sus uids, uid fuera de
100..=65533, `home` relativo y un `:` en cualquier campo — que partiría la línea y fallaría lejos.
Y `--user-paths` NO es `--service-paths` con otro nombre: aquél lista los servicios HABILITADOS,
éste los paquetes INSTALADOS que declaran cuentas. `postgres` instalado y sin levantar necesita su
usuario igual, porque los ficheros de la imagen ya son suyos.
`scripts/servidor-image.sh` lo aplica entre hidratar el perfil y sellar la imagen, y aborta si hay
conflicto: un rootfs con el uid equivocado produce ficheros de un dueño que no existe.
Verde: 6 tests del módulo, core 234, cli 88, bootstrap 42, `targets.py --selftest` 7/7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
|
||
|
|
de7c3faa9c |
gitea arranca: su [[service]], probado con el entorno de verdad — y dos fallos que sólo salen ahí
La receta ya declara su servicio (SDD 30) y `perfil.servidor` lo habilita. El ArtifactHash NO se mueve (`b3:391a613e…` antes y después): `[[service]]` está fuera de `hash_inputs`. ⚠ Primero, una corrección del commit anterior: declaraba `gitea` en `perfil.servidor` con SEIS LÍNEAS DE COMENTARIO y sin la línea `"gitea",`. El perfil cargó igual, el grafo no dijo nada y el paquete simplemente no estaba. Lo cazó `targets.py --services` al resolver el label: «lo declara gitea, que NO pertenece al perfil». Un comentario que explica una entrada que no existe se lee como la entrada. La card no encarna el binario directo: **gitea se niega a correr como root** (`[F] Gitea is not supposed to be run as root`), así que va por `setuidgid`. Y comprueba dos cosas del SITIO antes de arrancar, saliendo 78 con un mensaje que las nombra: `/etc/gitea/app.ini` y el usuario `gitea`. Un gitea sin config no falla — arranca y ofrece el asistente de «crear administrador» a quien pase. Probado en el worker con el argv exacto y `env -i`, que es lo que arje hace de verdad. Tres fallos que con una shell normal no se ven NUNCA: · `exec setuidgid …` a secas ⇒ `sh: exec: line 0: setuidgid: not found`. El `sh` de busybox de la imagen no trae `FEATURE_SH_STANDALONE`: no despacha sus applets, los busca en `PATH`, y PID 1 no garantiza ninguno. Todo con ruta absoluta (`/bin/grep`, `/usr/bin/setuidgid`). · sin `PATH` en el `envp` ⇒ `git not found: executable file not found in $PATH`. **gitea lanza `git` como subproceso**, y el mensaje se lee como «falta git» con git instalado y raíz de `base`. Un envp vacío no es «limpio»: es sin PATH. · sin `cd` ⇒ `fatal: error reading '/root/.git'`. El cwd se hereda y gitea corre `git config` en él. `Service` no tiene campo `cwd`, así que va en el argv, a la vista. Con eso: escucha, `GET /` responde **200** y el proceso corre como `gitea`. Las dos guardas verificadas por separado (78 y su mensaje cada una). El worker quedó limpio y censado: sin usuario, sin /etc/gitea, sin /var/lib/gitea, sin symlinks y sin procesos. Y `/etc/gitea` entra en las rutas de la mudanza: el `app.ini` guarda los SECRETOS generados (gitea los escribe en el propio fichero, que por eso tiene que ser suyo, `gitea:gitea 0660`). ⚠ Lo que queda abierto y está anotado en la receta: **el usuario `gitea` no existe en el producto**. `/etc/passwd` de la imagen es la constante `takana_bootstrap::PRODUCT_PASSWD` y sólo trae `root` y `sshd`. El SDD 30 sacó las Cards de una constante de Rust y las puso en la receta; los USUARIOS siguen donde estaban las Cards. Hasta que se declaren, el usuario llega con los datos del sitio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn |
||
|
|
76535f8e42 |
censar: el home de la PERSONA no se miraba nunca — y ahí está la clave privada de release
`fuera_de_git()` era una lista cableada de 10 rutas con `~` adentro, y el censo corre como root: `~` se expandía a `/root`. La huella quedó en `work/mudanza/censo-gioser.toml`, donde `~/.ssh` y `/root/.ssh` salen IDÉNTICOS. Lo que nunca se censó por su nombre: /home/sergio/.config/takana ⚠ la clave PRIVADA de release (el .pub está EN el repo) /home/sergio/.config/gh el token con el que se crean los espejos de los 26 repos /home/sergio/.ssh/config el bloque `git.gioser.net` `Port 2345`, sin el cual no se clona /home/sergio/.gitconfig los `insteadOf`, que reescriben remotos y esconden que uno es local /home/sergio/.claude 6,8 G la memoria y los transcripts del proyecto Ninguna de esas falla el día que se borra la máquina: la clave falla la próxima vez que alguien firma, y para entonces no hay de dónde sacarla. Hoy sólo viajaban dentro del bloque `/home` (22 G, `destino = ""`), o sea sin que nadie las hubiera mirado. Cuatro cambios: · **La lista sale a `rutas-fuera-de-git.txt`**, un fichero de datos con el motivo de cada ruta. Una lista cableada se queda vieja sin que nada falle: la anterior preguntaba por `~/.config/hammer` —muerto desde el renombre, existe VACÍO— y no preguntaba por `~/.config/takana`. · **`homes()` lee `/etc/passwd`** y expande `~/…` por cada home real (root incluido, cuentas de servicio con `nologin` fuera). En gioser: 6 homes, 19 entradas contra las 9 de antes. · **Tres estados, no dos.** Un `ls` sin permiso se lee igual que un directorio ausente: `ausente` se descarta, `sin_permiso` se REPORTA («2 rutas EXISTEN y no pude leerlas») para que nadie decida sobre una lista incompleta creyéndola completa. Probado corriendo el censo como no-root. · **Una sola llamada** en vez de una por ruta: N rutas × M homes por SSH era el censo tardando más que el trabajo que describe. Y cada entrada lleva dueño, tamaño y por qué importa. Y el paso que faltaba en `planear.py`: `fuera_de_git` no lo consumía NADIE, así que el censo lo listaba y el plan no proponía copiarlo — se veía y no se actuaba. Ahora es el paso 0 bis, pegado al del código, con revisión humana para decidir y, por cada ruta `muda`, su `rsync -aHAX --numeric-ids` (permisos: una llave con el modo cambiado no falla al copiarse, falla al usarse) verificado POR CONTENIDO: el digest de los digests, que no revela nada y caza lo que contar ficheros no caza — una llave truncada cuenta como un fichero igual que la entera. Probado en los dos sentidos: idéntico ⇒ 0, un byte distinto ⇒ 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn |