16493462b66b1bd3b0f04977b7bd804868d2b65c
572
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
589efee4f8 |
atuq: en la imagen el navegador SÍ pinta — la segunda pantalla no era
La sospecha salía del propio serial: el arranque del §6.10.ter veía DOS dispositivos DRM
(`drm: card0 card1 renderD128`) y uno nuevo ve uno solo. Sin `-vga none`, QEMU agrega una VGA
estándar además del virtio-gpu, OVMF pinta su GOP ahí y el kernel levanta simpledrm encima: un
compositor con dos tarjetas puede componer en la que el `screendump` no muestra, y eso se ve igual
que «la ventana no aparece». Por eso el guion lleva `VGA=1`, para pedir ese caso por su nombre.
Medido con las dos configuraciones y el mismo lanzamiento:
-vga none → card0 → 703766 px magenta
VGA=1 → card0 card1 → 703766 px magenta
El mismo número al píxel. La ventana aparece —panel, dock y el navegador con su barra lateral—,
`nsWindow::Create() Toplevel` está en el MOZ_LOG, la superficie se mapea y `mIsFullyOccluded 0`, con
`WaylandBufferSHM` de 1280x696, el mismo camino de buffers que bajo sway.
⇒ la segunda pantalla no es la causa. Queda una sola diferencia con aquella corrida: cómo se lanzó
el navegador. Acá va con perfil nuevo y MOZ_ENABLE_WAYLAND puesto; allá fue un `atuq` pelado con su
perfil por defecto tecleado en el serial. Para eso está `--como-usuario`, que es la corrida que sigue.
⚠ Dos trampas del arnés, medidas y escritas en el guion: el terminal hace ECO de lo que se le
escribe, así que una marca de fin literal se lee en el eco y las órdenes se pisan; y `cmd & ; echo`
es error de sintaxis en ash ⇒ el navegador no se lanzó y la corrida siguió dando capturas de un
escritorio vacío, que se leen igual que el fallo que se investiga.
|
||
|
|
d79299d351 |
granja: cuatro sondas quedaron ciegas tras el renombre — el informe decía «idle» compilando
Todas buscaban el proceso `release/hammer`, y desde el ADR 0016 el worker invoca
`./target/release/takana`. Ninguna daba error: simplemente no encontraban nada.
Medido hoy, no deducido: `estado-granja.sh` imprimía «moliendo: (nada — idle o entre
colas)» mientras en el worker corría
`./target/release/takana --store ./store build recipes/centrifugo.toml`. Leyendo ese
informe se concluye que la granja está seca. Tras el arreglo, el mismo informe dice
«moliendo: cilium-cli.toml» y `pgrep` allá confirma exactamente ese proceso.
Las cuatro:
· estado-granja.sh «moliendo» — mentía sobre si hay trabajo
· farm-worker-loop.sh×2 detección de build en vuelo
· deadman.sh `hay_trabajo` — y acá el precio es un worker BORRADO a mitad
de un build. En el LXC no llegó a morder (sólo borra cajas
hcloud y su timer no está activo allá), pero la próxima caja
de pago sí lo habría pagado.
Se aceptan LOS DOS nombres: cargo sigue compilando `hammer` como alias, y una sonda que
sólo mira el nombre nuevo se rompería con un worker que arrastre binario viejo — que es
justo lo que pasa acá, porque la siembra excluye `/target`.
Es el mismo renombre que dejó colgado el enlace de `go` (commit anterior). Un renombre
no rompe sólo lo que compila: rompe las cadenas que alguien escribió a mano.
|
||
|
|
756ed9c6a6 |
atuq: el compositor NO era la causa — con cosmic-comp nesteado el navegador pinta
El §6.10.ter dejó dos variables cambiando a la vez (compositor y arranque real) y ninguna medición que las separe. `scripts/cosmic/atuq-en-cosmic.sh` saca la primera de encima en ~2 min por vuelta, sin QEMU y sin imagen: sway headless de andamio, cosmic-comp nesteado encima por winit, atuq adentro, y `grim` capturando del lado de sway. El `--control` corre EL MISMO binario de atuq —el del cierre hidratado para la imagen— directo sobre sway. Las dos capturas coinciden al píxel en la región de la página (586331 px magenta, mismo bbox), y no es que el navegador se escape a sway: antes de lanzarlo, la ventana de cosmic-comp ocupa 1276x637 en ese mismo rectángulo, y el MOZ_LOG del widget ve `mode output size 1276 x 637` con cosmic contra `1280 x 720` en el control. Las dos corridas enteras difieren en 1130 píxeles, todos en las barras de título. ⇒ cosmic-comp no es el que impide la ventana. Queda como variable el arranque de verdad: kms/DRM sobre virtio-gpu en vez de winit, el PID1 de arje, el seat. De paso queda anotado el camino de buffers cuando SÍ funciona —`WaylandBufferSHM`, memoria compartida y no dmabuf—, que es la línea de base contra la que comparar la traza de la imagen. Y `scripts/cosmic/atuq-en-imagen.py` para medir allá: arranca la imagen, maneja el serial, lanza el navegador con el mismo MOZ_LOG y pide capturas por QMP. La corrida del §6.10.ter fue a mano y no dejó un solo log que se pueda releer. |
||
|
|
d570a8e03f |
granja: el worker llevaba 5 días sin poder construir NINGUNA receta Go, en silencio
El enlace `~/.cargo/bin/go` del worker apuntaba a `/opt/hammer/.dev-fs/tools/go/bin/go` — la ruta de ANTES del renombre a takana (ADR 0016, 2026-09-09). `/opt/hammer` ya no existe, así que el enlace estaba COLGADO desde entonces. Nada falló. `go` lo invoca takana DEL LADO DEL HOST (`go mod vendor` durante el fetch, con red), fuera del sandbox, así que el rootfs no lo cubre — es el mismo modo de fallo que `patch` en agosto, y `vps-setup.sh` ya lo tiene escrito como regla: «toda herramienta que takana invoque HOST-SIDE va en esta lista». `go` no estaba. Cómo se descubrió: por casualidad, verificando reproducibilidad. 16 de 25 recetas Go murieron en SEGUNDOS. Un fallo sistémico se lee como 16 recetas rotas si nadie abre el error — el verificador manda la salida del build a /dev/null. El error era `spawn go mod vendor: No such file or directory (¿está `go` en el PATH del host?)`, que lo decía todo. Arreglado en el worker (enlaces repuestos a /opt/takana) y COMPROBADO con un build real: `gron` no construía, ahora construye y reproduce bit a bit (why-differs: 5 entradas idénticas, 0 divergen). Acá va la parte que evita la repetición: el script daba «go enlazado ✓» sin comprobar nada — `ln -sfn` crea un enlace colgado tan campante. Ahora repone un enlace colgado preexistente (avisando) y comprueba que RESUELVE ejecutando `go version` A TRAVÉS DE ÉL; si no ejecuta, sale 1 con el síntoma escrito. Un enlace que existe no es un `go` que corre. Probado en los tres sentidos, con control que TIENE que pasar: · enlace sano ⇒ ok, rc=0 · enlace colgado a /opt/hammer ⇒ avisa, lo repone, rc=0 · destino inexistente ⇒ falla ruidoso, rc=1 |
||
|
|
3d79cdf98b |
SDD 30 4c: GNOME con los demonios supervisados por arje — y el control arregló colord
Los demonios de sistema pasan de lanzarse con `&` desde un script de 500 líneas a ser Cards del `genesis`. El bloqueo que este frente daba por corpus se levantó solo: otro agente selló evolution-data-server y gnome-shell mientras esto se escribía, y escritorio-gnome quedó 309/309. SE CORRIÓ UN CONTROL PRIMERO, y es lo único que hace interpretable el resultado: colord control: `MURIÓ al arrancar` cards: `ya vive (pid 175)` ColorManager control: `NO apareció en 40s` cards: `OK` login1/Accounts/UPower OK en los dos compositor wayland-0 y shell vivo en los dos El fallo del control DESAPARECIÓ, y no lo buscaba: colord moría arrancado por el script y vive arrancado por arje, con el bus ya listo porque la espera está dentro de su argv. Sin el control, «ColorManager OK» sería un dato suelto en vez de una diferencia. Los PIDs lo confirman: polkit=119, colord=175, upowerd=178, accounts=180 — de antes de que el lanzador de sesión existiera. QUÉ NO SE COMPROBÓ: no hay screendump; QEMU salió por timeout y el control tampoco lo tuvo. La comparación es serial contra serial y lo que se afirma es sobre los DEMONIOS, no sobre el pintado. Las piezas donde corresponde: `takana service-cards` (UNA sola implementación de receta→Card; el formato es contrato con card_core::Card), `targets.py --service-paths` (une qué-es con si-arranca), y un inyector en FICHERO APARTE porque anidar dos heredocs de python falló en vivo — el terminador del interno cerró el externo y media cosa corrió como shell. La espera del bus va DENTRO del argv de las 5 recetas de sistema: sin ella un daemon arranca antes de que dbus escuche y queda en modo idle sin registrar su nombre — un fallo que no se ve, porque el proceso vive y el bus no lo tiene. Los 5 hashes intactos. Y el guardia de gnome-start es por «¿está corriendo?», no por una perilla: así es correcto venga de donde venga el proceso y la misma copia sirve donde no se inyectaron cards. |
||
|
|
60202bf526 |
imagen: el navegador arranca en la imagen booteada y NO PINTA VENTANA — medido, con lo descartado
Primera prueba de `atuq` dentro de una imagen de disco arrancada, y el resultado no es el de la jaula. La cadena previa funcionó, y eso también es medición: el perfil `escritorio-cosmic` hidratado con el cierre de hoy (275 nodos, 8,4 G) trae `/usr/bin/atuq`, `/usr/bin/llama-server` y el modelo — declarar en el perfil SÍ pone las cosas en la imagen—, la imagen EFI de 12 G arranca en QEMU y COSMIC pinta panel y dock en ~2 min. Y `atuq` arranca —sus extensiones inician, la del foco sondea cada minuto, WebRender inicializa— sin que la ventana aparezca nunca. Descartado, para que nadie lo repita: no es el sandbox de Gecko (relanzado con los cinco MOZ_DISABLE_* puestos, idéntico), no es que el proceso muera (sigue ejecutando el JS de las extensiones), y no es la IA (pasa con about:blank). La pista: bajo sway headless el MISMO artefacto pinta perfecto y hay capturas del panel contestando. La diferencia son el compositor (cosmic-comp+llvmpipe contra sway+pixman) y el arranque real contra bwrap. Es su propia unidad de trabajo, no un parche apurado. ⚠ La lección del método: quince guardianes en verde, `vigia-imagen.py` en ✓, y el navegador igual no se puede usar en la imagen. «Sella», «hidrata» y «los tests pasan» son tres cosas distintas de «arranca y se ve». De paso, `metal-iso.sh` acepta ahora STORE y AUG por entorno, porque en esta máquina no funcionaba: arma el rootfs con `cp -al` desde el store y el store es un BIND-MOUNT del volumen — `linkat` rechaza cruzar mounts aunque sea el mismo disco, así que hay que nombrar los dos lados dentro del mismo mount. Es la cuarta vez que aparece el mismo EXDEV hoy. |
||
|
|
5e056b6177 |
atuq: una CAPTURA de la barra lateral — y la barra ya no se abre sola
`scripts/atuq-captura-ia.py`: el arnés de sway headless de los guardianes + `grim` + un PNG. No es un guardián (los veredictos siguen saliendo del `dump`); es la evidencia que ningún log puede dar: que el panel se pinta y que la respuesta está ahí. Encontró tres cosas, ninguna visible en un log: 1. **la barra lateral se abría SOLA** en el primer arranque, ocupando un tercio de la ventana. Gecko lo hace al instalar una extensión con `sidebar_action`, y para un navegador que la trae de fábrica eso es imponerle un panel a todo el mundo. `"open_at_install": false`, con captura del después; 2. el diálogo «Close Firefox» en la segunda corrida — el arnés mataba el navegador sin despedirse y quedaba el `.parentlock`. Arreglado en el arnés; 3. dos barras de notificación VACÍAS en el arranque. El atajo era reportarlas como fuga de marca, y era falso: instrumentando una copia del artefacto para volcar el DOM salieron `sandbox-content-disabled` (lo apaga el propio arnés) y `startup-restore-session-suggestion` (por el cierre abrupto anterior). El texto está en el DOM y no en los píxeles — comprobado además quitando nuestro CSS, con el mismo resultado ⇒ es el render por software de la jaula. ⇒ Una captura muestra síntomas; el DOM dice de quién son. Sin ese segundo paso, dos de los tres habrían entrado al SDD como bugs nuestros. El log del navegador viaja junto a la foto, siempre: una captura muestra que algo se ve raro y no por qué. Y `test-atuq-ia.py` + `test-atuq-archivo-semantico.py` siguen verdes tras el cambio. |
||
|
|
5e5369f775 |
libffi-shared en base: python3 volvía a ser INERTE en servidor, cli y base — y el vigía no miraba ahí
`vigia-sonames.py` corría por defecto SÓLO sobre los cinco perfiles de escritorio
(`p.startswith("escritorio")`). O sea que `base`, `cli` y `servidor` —el objetivo de la mudanza—
**nunca se miraron**. Apuntándolo a ellos a mano:
== servidor 147 nodos · 183 sonames provistos · 1 sin proveedor
FALTA libffi.so.8 ← lo piden: python3
`python3` no arrancaba en las tres imágenes, por el mecanismo EXACTO que `musl-shared` y
`zlib-shared` arreglaron dos días antes: la receta canónica de libffi es `--disable-shared`, ningún
artefacto del cierre publica ese soname, y dentro del lab el agujero lo tapa el rootfs de Alpine —
así que el build pasa y la imagen sale rota. `libffi-shared` ya existía en el corpus y ya estaba
declarada en otros dos perfiles; sólo faltaba en `base`.
servidor 148 nodos · 186 sonames · **0 sin proveedor** (base y cli, igual)
## Y el vigía pasa a mirar TODOS los perfiles
⚠ **Es la tercera vez en dos días que un guardián de este repo mide menos de lo que su resumen
afirma**: `static-audit.sh` sólo globeaba `recipes/` y no las 5 colas (3 mentiras ocultas); el bloque
de raíces sucias sólo corría al hidratar un perfil y no veía las 649 recetas fuera de toda imagen
(qdrant con 69 M de basura); y éste dejaba fuera medio sistema. **Un «0 huecos» sobre la mitad del
sistema se imprime igual que uno sobre todo él.**
⇒ Al leer un guardián, leer su SCOPE antes que su veredicto: el glob, el bucle, de dónde saca la
lista. Los ocho perfiles dan 0 sin proveedor, así que ampliar el alcance no mete ruido — sólo deja de
esconder.
|
||
|
|
1fc9de6a01 |
espejar-repos: mostrar el error de gh y cortar ante el límite de la API
Corrida real contra gioser: los 21 repos fallaron al crearse y el guión dijo «no se pudo crear» A SECAS, porque mandaba el stderr de `gh` a /dev/null. La causa era el LÍMITE DE LA API DE GITHUB agotado —que se resuelve esperando— y sin el mensaje eso parecía un problema de permisos, de nombre o del propio guión: hubo que reproducirlo a mano para enterarse de algo que la herramienta ya sabía. Dos arreglos: · El error de `gh` se muestra, recortado a su primera línea. · Ante «rate limit» se CORTA en el primero: los 20 restantes van a fallar igual y cada intento gasta cuota. Dice a qué hora se repone (leído de `gh api rate_limit`) y recuerda que reintentar es seguro, porque el guión es idempotente. No quedó estado parcial de la corrida fallida: el `pushurl` se agrega DESPUÉS de crear el repo, así que los 21 clones siguen con su remoto original intacto. Verificado en tres de ellos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x |
||
|
|
df1230d2a5 |
vigía de subcomandos: separar el hueco CONOCIDO del NUEVO — «44 inertes» todos los días no se lee
El vigía escribe `docs/state/subcomandos.txt` en cada ciclo del latido desde ayer, y decía
`TOTAL: 44 herramientas selladas que no se pueden invocar` a secas. Un número grande, constante y sin
explicación al lado entrena a saltearse el informe — y entonces el hueco NUEVO, que es el único que
pide acción, pasa desapercibido entre el ruido del viejo. Es la misma trampa que ya tienen resuelta
`static-audit.sh` (deuda decidida) y `--auditar-raices`.
Los 44 son todos del mismo hueco, y ahora el informe dice cuál y por qué:
• falta el binario `cargo` (el toolchain de Rust)
PENDIENTE DE DECISIÓN, no olvido. `cargo`/`rustc` viven hoy en el LAB y en la cadena de
selfhost (mrustc→1.91.1), no como receta del catálogo. Empaquetarlos es una decisión de
TOOLCHAIN —qué Rust publica la distro, y desde qué cadena— no trabajo de granja.
TOTAL: 44 … (44 de hueco CONOCIDO, 0 NUEVAS) ⇒ exit 0
⚠ **«PENDIENTE DE DECISIÓN» y no «deuda decidida»**, que es distinto: en el audit estático las tres
mentiras tienen un arreglo evaluado y descartado por su coste; acá nadie decidió nada todavía. La
etiqueta tiene que decir cuál de las dos cosas es, o dentro de tres meses se lee como cerrada.
El código de salida ahora refleja sólo los huecos NUEVOS. Probado con los dos controles: sacando
`cargo` de la tabla salen `44 NUEVAS` y exit 1; restaurándola, `0 NUEVAS` y exit 0.
⚠ Y el primer intento de ese control estaba MAL y daba verde: escribí `CONOCIDOS = {} or {…}` para
vaciar la tabla, y en Python `{}` es falsy ⇒ la expresión devuelve el segundo dict y la tabla seguía
llena. El control «pasaba» sin probar nada. Rehecho renombrando la clave.
|
||
|
|
713128f26d |
mudanza: guión que le da copia FUERA a los 25 repos que sólo existen en gioser
Hace por un árbol entero lo que `scripts/espejo-setup.sh` hace por takana: `pushurl` doble sobre `origin`, de modo que cualquier `git push origin` que ya exista empiece a espejar sin tocar ningún script. Crea el repo en GitHub como PRIVADO. Cuatro decisiones: · **Por defecto NO hace nada**: imprime qué haría; hay que pedirlo con `--apply`. Crear repos y empujar código a un servicio externo no es algo que deba pasar por correr un script sin leerlo. · **Varios clones del MISMO origen van a UN repo remoto.** En gioser hay cinco de `tawasuyu/tawasuyu`; cinco repos distintos multiplican la confusión y empujarlos todos al mismo `main` los haría chocar. El primario empuja normal; un secundario sólo se empuja —bajo `refs/heads/clon/<nombre>/*`— SI aporta historia propia, y eso se comprueba preguntándole al primario si ya tiene esos objetos (`cat-file -e`). Medido: los cinco tienen CERO commits que no estén en su remoto, así que los cuatro secundarios se saltan y se dice por qué. · **Sólo agrega; nunca toca el `fetch` de `origin`**, que sigue siendo el canónico. · **Lo sucio no viaja, y se dice.** Un espejo guarda historia commiteada, no el árbol de trabajo, y `tawasuyu` tiene 980 ficheros sin commitear. Creer que está a salvo cuando la mitad del trabajo no está commiteado es justo el fallo que este guión existe para evitar. ⚠ Y un criterio que escribí mal y corregí midiendo: el primario era «el clon con más commits alcanzables», y contra los cinco de tawasuyu dio un EMPATE EXACTO en 9343 — `rev-list --all` cuenta las refs de `origin`, que los cinco comparten, así que la métrica estaba dominada por lo común y no medía nada. El desempate quedaba en el orden del `find` y elegía `tw-deploy` (HEAD del 6/9) sobre `tawasuyu` (del 12/9), que es el que se está trabajando. Ahora manda la fecha del HEAD en ISO, que distingue y ordena como texto. Ensayo sobre gioser: 25 repos sin copia fuera ⇒ 21 a espejar, 4 saltados por redundantes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x |
||
|
|
01e721db84 |
verificar-repro: decir que además es el CENSO de la deuda invisible, con el número medido
`no construyó` no es ruido del verificador: es EL hallazgo. Un `sealed` en el grafo dice que alguien construyó eso alguna vez con algún lab, no que se construya hoy — y el lab rueda desde Alpine edge. Este script es lo único que convierte esa sospecha en un número SIN RIESGO, porque aparta en vez de borrar y restaura si el build falla. Barriendo las 15 recetas CMake baratas del corpus: REPRODUCEN 9 · DERIVA 1 · no construyeron 5. Cinco selladas y rotas a la vez, todas por el mismo crash del lld de zig con `--dependency-file`. Y la nota de método que hace legible el censo: barrer por FAMILIA de sistema de build. Si el fallo es del toolchain se concentra en una familia y el patrón salta; barrer al azar lo diluye. |
||
|
|
eb85c5976e |
latido: el vigía de servicios entra al cron — 18 demonios se embarcan y nadie los arranca
Contesta la pregunta que ni el grafo ni `vigia-sonames` contestan: no «¿está sellado?» ni «¿arranca el binario?» sino **«¿hay alguien que lo LEVANTE?»**. Un demonio puede estar sellado, con contenido, con todos sus sonames resueltos, y que ninguna imagen lo arranque nunca. Va al latido por la lección que `vigia-sonames` ya dejó escrita doce líneas más arriba —un vigía que hay que acordarse de invocar no se distingue de no tenerlo— y con más motivo: su entrada son DOS ficheros que cambian por separado (las recetas y `targets.toml`), así que la divergencia entre «declarado» y «habilitado» aparece sola, sin que nadie toque el vigía. Corre PRIMERO su propio `--selftest`: si el guardián está roto, su silencio no es una respuesta. Primera lectura, ya en el repo: 0 errores y 18 avisos. El más ruidoso es `dbus-system`, que viaja en SEIS perfiles y sólo GNOME lo arranca. |
||
|
|
1b56b16402 |
SDD 30 §4a+§4c: los 9 demonios de GNOME declarados — y aparecieron dos que no estaban en NINGÚN perfil
Lo que el script de sesión lanza con `&` ahora está declarado en las recetas y habilitado en el perfil. Ninguna receta movió su hash: 9/9 idénticos a los que los grafos ya registraban. EL HALLAZGO, y no lo buscaba: la comprobación inversa del resolutor rechazó `arje-logind-compat` y `arje-polkit-compat` porque están en CERO perfiles — y sin embargo qemu-desktop-image.sh los copia al rootfs a mano y el de COSMIC hace `exit 1` si falta logind-compat. Dos binarios imprescindibles, presentes en la imagen y ausentes del destino declarado: la misma forma del agujero de `foot`, encontrada por una comprobación en vez de por una imagen inusable. Son raíces de escritorio-gnome (los dos) y de escritorio-cosmic (sólo logind, verificado que sus scripts no nombran polkit). DOS COSAS QUE NO SON TRANSCRIPCIÓN: - `dbus-daemon --fork` no se traduce tal cual: arje supervisa al HIJO DIRECTO y Type=forking no existe, así que un daemon que forkea y sale deja a arje viendo morir al padre con éxito y reencarnándolo para siempre. La card usa --nofork. - `scope = system|session` decide DÓNDE va la card. Las de sesión necesitan XDG_RUNTIME_DIR y usuario logueado; en el genesis arrancarían antes de que exista ninguno. Y fuera de mirada NADIE entrega cards de sesión todavía, así que salen con AVISO: el hueco queda contado, no omitido. Correcciones propias: la unicidad del label es DENTRO del perfil, no del corpus (upower vive legítimamente en dos colas); la membresía se lee de los CINCO grafos, no sólo el del corpus; una RAÍZ manda sobre el grafo, que es derivado y lo regenera el cron; y la flag nace en inglés (`--services`) como manda la regla 4, aunque `--lista` sea deuda vieja del mismo fichero. `--selftest`: 7 casos, el primero es el CONTROL que tiene que pasar en verde. |
||
|
|
0b5dae7812 |
censar: 26 repositorios existen SÓLO en la máquina que se va a borrar — el paso 1 del plan
Buscando cómo escribir la receta de `tawasuyu` apareció que su ÚNICO remoto es el gitea de gioser (`ssh://gitea@git.tawasuyu.net:2345/…`, y ese nombre resuelve a 204.168.193.248 = gioser). Al mirar el resto: **28 repositorios con remoto en esta máquina, 26 SIN NINGUNA copia fuera**. Sólo `takana` y `llimphi-standalone` tienen espejo externo. Apagar el origen no borra unos servicios: borra EL CÓDIGO CON EL QUE SE VOLVERÍAN A CONSTRUIR, y los clones de trabajo están en el mismo disco que también muere. Entre los 26 está `tawasuyu`, que produce 10 de los 13 binarios que nadie más provee — la dependencia circular completa. No se ve desde ninguna otra parte del censo: un repo no es un proceso, ni un puerto, ni un dominio. Se descubre cuando ya no hay de dónde sacarlo. · `censar.py` lo mira ahora (`[[repo]]`), y reporta NO «tiene remoto» sino si alguno de sus remotos NO es esta máquina: un remoto que apunta afuera es la prueba de que el código sobrevive. La lista de «esta máquina» sale del censo mismo (sus IPs + los dominios que sirve), no de nombres cableados. · `planear.py` lo emite como PASO 1, por delante del rescate de binarios: aquello pierde un servicio, esto pierde la posibilidad de reconstruirlo. El arreglo ya está escrito en el repo —takana usa `pushurl` doble por `scripts/espejo-setup.sh`. Y una corrección de algo que dije antes: la perilla por receta que vi en `recipe.rs` es `strip_debug`, no una versión de rust. NO existe `rust_version`; sólo `zig_version`. Pinear rustc por receta para honrar el `rust-toolchain.toml` de tawasuyu (1.96.0, contra 1.97.0 del lab) sería código nuevo en takana-core, no una opción ya disponible. De paso, medido el subárbol de los diez binarios por separado: cuatro (`willay-daemon`, `sandokan-seguridad-core`, `pacha-secretos`, `tupu-cli`, entre 84 y 153 deps) NO arrastran criptografía en C; cuatro traen `ring` y dos `aws-lc-sys`. O sea que hay un escalón por donde empezar sin pelear con cmake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x |
||
|
|
556fcad5fa |
SDD 30 §4a: el perfil ya sabe HABILITAR — y avisa del demonio que nadie arranca
`paquetes` decía qué se INSTALA; no había dónde decir qué se LEVANTA. El sshd del producto arrancaba porque su Card estaba escrita a mano en una constante de Rust, no porque nadie lo hubiera declarado. `servicios = [...]` por perfil (se hereda como `paquetes`, mismo orden y dedup) + `targets.py --servicios <perfil>`, que cruza label → receta → exec contra los `[[service]]` del corpus y la membresía de perfil de build-state.json. Lo que vale más es la comprobación INVERSA: avisa de los paquetes que están en la imagen, TRAEN un demonio y el perfil no arranca. Es la versión servicios de la lección de `foot` —la métrica mide la clausura de lo DECLARADO y no ve lo que falta en la declaración— y no es hipotética: antes de escribir `servicios = ["sshd"]` el resolutor ya avisaba «openssh está en la imagen y TRAE este servicio, pero el perfil no lo arranca». Probado con roturas A PROPÓSITO, 5/5, y con un control que TIENE que pasar: habilitado sin declarar (ERROR) · dos recetas con el mismo label (ERROR) · lo declara una receta que no está en el perfil (ERROR: el card apuntaría a un binario ausente y arje lo encarnaría con ENOENT en cada backoff) · la inversa (AVISO, no rompe el cron) · el caso bueno (rc=0). Los 8 perfiles siguen expandiendo igual y los llamadores de shell no cambian. |
||
|
|
29831e6296 |
descargas: el guardián estaba ROJO hace dos días y nadie lo corría
La barrida de regresión del frente (14 guardianes tras rehacer host y navegador) encontró uno en rojo: `test-atuq-descargas.py` buscaba el CAS en `<estado>/descargas-cas` y el host lo escribe en `<estado>/cas` desde el commit del archivo personal (357791a85, 2026-09-10) — el §6.3 UNIFICÓ los dos CAS, que es justo lo que hace que una página archivada y un fichero bajado con el mismo contenido sean un solo objeto. Lo renombré yo y no actualicé este guardián. Lo que importa no es el renombre: es que el guardián estuvo rojo dos días sin que nadie se enterara, porque **un guardián que no se ejecuta no protege de nada** — la misma familia que el cache-hit que congela regresiones. Los otros 13 pasan. Arreglado el path, y el README dice ahora por qué el directorio se llama `cas` y no `descargas-cas`. |
||
|
|
6187c83c36 |
planear: metalog NO APLICA — el destino trae su propio journal
Un demonio de syslog no se muda a un sistema que ya tiene journal propio: la semilla de arje declara `provides: ["Spawn", "Journal"]` (crates/takana-bootstrap/src/lib.rs:185) y existe el crate `takana-journal`. Mismo razonamiento que dbus/udev/elogind, que ya estaban en NO_APLICA: no es gusto, es que el destino provee la función. Vale para metalog, syslogd, syslog-ng, rsyslogd, socklog y busybox-syslogd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x |
||
|
|
76fc142244 |
verificar-repro: activar el mirror — el veredicto dependía de la suerte del caché
Medido hoy: `ia-modelo-embeddings` dio REPRODUCE y `ia-modelo-chat` «no construyó», y la ÚNICA diferencia entre las dos era que el tar de la primera seguía en `work/tarballs` y el de la segunda lo había borrado yo liberando disco. Sin caché, el build sale a buscar la fuente a una URL `.invalid` —que es lo que el ADR 0013 pone a propósito cuando el objeto vive sólo en nuestro mirror— y muere con `Could not resolve host`. O sea que el gate informaba «no construyó» para `firefox-pgo-profile` y los dos modelos de IA según qué hubiera en el caché local. Un guardián cuyo veredicto depende de eso no es un guardián. Ahora hace `source` de `scripts/fuentes/mirror-env.sh` si existe. Aditivo, como manda el ADR 0013: si el fichero no está, todo sigue igual que antes. Con eso, ia-modelo-chat: REPRODUCE. |
||
|
|
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. |
||
|
|
f2bffd9e4b |
planear: cruzar la config contra las decisiones — y los puertos se adjudicaban por NOMBRE
La config del servidor declara de qué servicios depende: cada `reverse_proxy` y cada `php_fastcgi` apuntan a algo. Cruzarlo contra la decisión tomada sobre cada servicio caza un fallo que ninguna otra parte ve: **el dominio se MUDA y el servicio que lo sirve está marcado MUERE**. Las dos decisiones son razonables por separado y juntas dejan el sitio nuevo devolviendo 502 — no lo ve el DNS, no lo ven los procesos, y no lo ve quien decide de a una entrada por vez, que es como se decide. En gioser apareció el revés: dos sitios proxean a un puerto que NINGÚN servicio censado sirve, o sea que ya devuelven 502 hoy, en el origen — `mail.sigma.gioser.net` → :9000 y `api.gioser.net` → :8000. Confirmado aparte con `ss -lntp`: no hay nada escuchando en ninguno de los dos. **Y un falso positivo que hubo que matar primero.** La primera corrida acusaba a `sergio.gioser.net` de mudarse dejando atrás a `shuma`. Falso, y la causa estaba en el CENSO: los puertos se adjudicaban por prefijo de nombre (`proc.startswith(name[:15])`), así que un `shuma` DECLARADO-MUERTO se quedaba con el 7378 — que lo escucha `shuma-gateway` (pid 294, verificado con `ss -lntp`). Un declarado-muerto por definición no puede estar escuchando. Ahora se atan por PID, que es lo único sin ambigüedad, con caída al nombre sólo para servicios VIVOS cuando `ss` no da el pid. El puerto es la señal más fuerte de que algo sirve, así que colgárselo al servicio equivocado envenena todo lo que se derive de él — acá se derivó un guardián acusando al inocente, que es la forma más rápida de que un guardián se deje de leer. De paso, el lector de Caddy entiende `php_fastcgi` (antes caía en «directiva no reconocida») y registra a dónde proxea cada sitio, incluidos los bloques anidados. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x |
||
|
|
053e5e7edf |
planear: buscar la receta por el PAQUETE del origen — y el intérprete no es el programa
`origen_binario` buscaba la receta por el nombre del SERVICIO, y ése casi nunca es el nombre del paquete: `sshd` lo trae `openssh`, `crond` lo trae `cronie`. El censo ya le preguntó al gestor de paquetes quién posee cada binario, así que ese nombre también se prueba. El perfil pasó de 4 recetas a 7, y los 27 servicios quedan: receta-takana 9 · suelto 9 · paquete-ajeno 13 · interprete 4 · borrado 4. **La trampa, que es la que haría mentir al perfil.** `openclaw` lo posee el paquete `nodejs`; `fail2ban-server`, `glances` y `uvicorn` los posee `python`. Contarlos como cubiertos porque existe `recipes/nodejs.toml` haría salir el perfil N/N describiendo un servidor al que le faltan CUATRO programas. Tienen clase propia (`interprete`) y cuentan las DOS cosas a la vez, porque las dos son ciertas: su runtime entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo que pase— y el servicio sigue listado como NO cubierto. Meterlo sólo en las raíces miente; dejarlo sólo en los faltantes arma una imagen sin runtime y el programa, cuando llegue, no arranca. Y el paquete del origen no se llama igual que la receta ni para el mismo intérprete: Artix empaqueta `python` y el catálogo tiene `python3.toml`. Sin ese alias tres servicios decían «hace falta una receta takana» teniendo el runtime sellado — dos trabajos muy distintos. **Sellada y sin sellar tampoco son lo mismo**, y el perfil las listaba igual: una sellada se INSTALA del repo firmado, una sin sellar hay que CONSTRUIRLA. Salió con un caso real — `recipes/qdrant.toml` entró al catálogo desde otro frente mientras se escribía esto y no tiene artefacto. Ahora se marca en la línea y se resume al pie. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x |
||
|
|
fb3a326dfd |
latido: los dos vigías nuevos entran al cron — un guardián que hay que acordarse de invocar no existe
`vigia-subcomandos.py` y `hydrate-profile.py --auditar-raices` nacieron ayer encontrando cosas
reales: 46 binarios sellados que no se pueden invocar por falta de driver, y 4 comandos de `bzip2`
apuntando a `/out/usr/bin/…`. **Las dos las encontré a mano, y eso no se repite solo.**
El argumento ya estaba escrito tres líneas más abajo en este mismo fichero, al lado de
`vigia-sonames`, y costó caro: nadie lo corría, así que `libstdc++.so.6` —que rompía el navegador en
los CUATRO perfiles— estuvo en su salida meses sin que nadie la leyera.
Van FUERA de la puerta diaria porque son baratos: **4 s y 22 s** medidos. Dentro del `if` del sello
correrían una vez al día sin motivo.
⚠ **Y la primera versión de este commit los metió DENTRO de la puerta**, justo lo contrario de lo que
decía su propio comentario — el ciclo de prueba no imprimió ni una línea de ellos y así se vio. Por
eso se corre el ciclo de verdad antes de dar por bueno un cablazo al cron: un bloque mal colocado en
un script desatendido no avisa, simplemente no pasa nada.
Dejan fichero en `docs/state/` con la FECHA DE MEDICIÓN por delante, por la misma razón que el
static-audit: con el frente en verde el texto es constante, `git diff --cached --quiet` no vería
cambio, y dentro de tres meses el fichero sería indistinguible de uno rancio. Con la fecha, cada
ciclo deja huella en el `git log` — se ve que el vigía sigue VIVO, no sólo que el último veredicto
fue bueno.
Verificado corriendo el ciclo completo:
subcomandos.txt ✓ TOTAL: 44 herramientas selladas que no se pueden invocar.
raices.txt ✓ ✓ 0 ofensores NUEVOS sobre 1 artefactos.
==> estado commiteado+pusheado
|
||
|
|
edc95325d9 |
censar: cuatro binarios que YA NO EXISTEN en disco — y el paso de rescate, que caduca
`/proc/<pid>/exe` termina en « (deleted)» para cuatro servicios de gioser: el fichero ya no está en
disco, sólo vive el inodo que sostiene su proceso. Y en tres de los cuatro hay AHORA otro fichero en
la misma ruta, de distinto tamaño:
tejido corre 12750368 B · en su ruta hay 13815864 B
shuma-gateway corre 8431896 B · en su ruta hay 10363504 B
pacha-secretos corre 8634240 B · en su ruta hay 8647456 B
puerta-f6e393ff corre 197262440 B · en su ruta NO HAY NADA
Copiar la ruta NO FALLA: muda otra cosa, y el servicio nuevo no es el que estaba andando. Todo verde,
todo distinto — el modo de fallo más caro que hay en este frente.
Se recuperan leyendo `/proc/<pid>/exe`, y SÓLO mientras el proceso viva. En una mudanza que termina
BORRANDO el origen, un reinicio de gioser antes de este paso los pierde para siempre. Por eso:
· clase propia en `origen_binario` (`borrado`), no una coletilla dentro del texto de `suelto`: no es
«hay que llevarlo», es «se pierde en el próximo reinicio y el que está en su ruta no es el mismo».
El motivo trae el comando literal de rescate con su pid.
· paso `rescate` en el plan, ANTES del preflight, porque es el único paso que puede volverse
IMPOSIBLE mientras se piensa el resto.
· su verificación COMPARA TAMAÑOS contra el que corre. No es celo: un `cat` de un `/proc` que ya no
existe crea un fichero VACÍO y devuelve 0, así que sin comparar el rescate «pasa». Probado en los
dos sentidos — el paso real sale 0, y con un fichero vacío a propósito dice `FALTA tejido` y sale 1.
Los cuatro ya están rescatados en `work/mudanza/rescate/` (gitignored), byte a byte iguales a los que
corren, con su SHA256SUMS.
Y el empalme se hizo comprobando que el marcador fuera ÚNICO antes de cortar, que es la lección del
`def pasos` duplicado de ayer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
|
||
|
|
0f3b137578 |
censar: leer la config del servidor web — cinco sitios sirven un root que NO EXISTE
La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Eso lo dice el
FICHERO DE CONFIGURACIÓN, y leerlo no toca al origen: ni una petición, ni riesgo de fail2ban.
Para leerlo se agregó un LECTOR de Caddy al centro (`formatos/caddy.py` ya tenía el escritor), y el
censo lo usa en vez de tener su propio parser a medias — los de nginx y apache ya existían, y dos
parsers del mismo formato es cómo se separan sin que nadie lo note. Cuarto par de la familia web.
Sobre el Caddyfile real de gioser, cinco sitios apuntan a un `root` que no existe — y son justo los
que devolvían los 502 que en su día hicieron que el censo SE BANEARA A SÍ MISMO al sondearlos:
aura.gioser.net → /var/www/aura_frontend · sigma → /var/www/sigma/frontend
summa → /var/www/summa/frontend · kosmofono → … · dev.summa → …
Es evidencia MÁS FUERTE que el DNS: no hay nada que servir, devuelve 502 resuelva donde resuelva. Va
como recomendación `muere` con la ruta y el fichero donde está el bloque.
**Y sirve para lo contrario, que es donde el aviso hacía daño.** Un directorio que la config SÍ
referencia no es huérfano: el plan marcaba `/var/www/git-tawasuyu` como «nadie lo recuerda» estando
servido, y ese aviso aplicado tira `git.tawasuyu.net`.
Dos bugs propios, los dos encontrados contra el fichero real y no sobre un ejemplo mío:
· El `root` de ese sitio vive DENTRO de un `handle`, y yo saltaba los bloques anidados enteros por no
saber modelarlos. Que el pivote no sepa MODELAR algo no es razón para no VERLO: el bloque sigue
marcado SIN-TRADUCIR pero sus `root`/`reverse_proxy` se leen. Con eso aparecieron dos raíces
ausentes más, también anidadas.
· Detectar la cabecera de sitio con una lista negra de directivas dejaba pasar `log { output file … {`
y el snippet `(acceso) {`: DOS dominios inventados que el censo habría puesto a decidir. La regla
que aguanta es positiva — todos los tokens de la cabecera tienen que PARECER una dirección.
Sin regresión: `nginx → caddy` y `apache → caddy` siguen dando `Valid configuration` con el caddy del
corpus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
|
||
|
|
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. |
||
|
|
af20d90648 |
bzip2: cuatro comandos rotos desde siempre — /usr/bin/bzcmp -> /out/usr/bin/bzdiff
`bzcmp`, `bzegrep`, `bzfgrep` y `bzless` son symlinks con **la ruta del SANDBOX horneada dentro**. El Makefile de bzip2 los crea con `ln -s $(PREFIX)/bin/bzdiff bzcmp` —con `$(PREFIX)` DENTRO del destino— y como bzip2 **no soporta `DESTDIR`**, la receta pasa `PREFIX=/out/usr` (el mismo truco que valkey) ⇒ el destino que queda es `/out/usr/bin/bzdiff`. Al hidratar, esos cuatro apuntan a un directorio que en el sistema real no existe. ⚠ **Cinco indicadores en verde sobre cuatro comandos que no funcionan**: el artefacto tiene contenido, `bzip2`/`bzgrep`/`bzdiff` corren, `static-audit` pasa, el grafo lo cuenta y la receta REPRODUCE. Ninguna métrica existente mira a dónde apunta un enlace. Arreglo: rehacerlos RELATIVOS después del install, que es lo correcto para un artefacto direccionado por hash — un enlace relativo sigue valiendo esté el árbol montado donde esté. Verificado: los cuatro resuelven y `bzip2 -c | bzcat` sigue dando `hola`. ## El guardián, en el mismo sitio y por la misma pregunta `--auditar-raices` ya contestaba «¿esta receta dejó ficheros donde no van?». Ahora contesta también la otra mitad: «¿dejó ENLACES a una raíz que no es del FHS?». Son la misma familia —un `install` que se equivocó de destino— y comparten la definición de `FHS_RAIZ`, que es lo que evita dos listas que se desincronizan. La regla es independiente del perfil, y por eso vale sobre el store entero: un enlace a OTRO artefacto es normal (`kinfocenter -> /usr/bin/systemsettings`, los dos en `escritorio-kde`, resuelve en el rootfs fundido). Lo que nunca puede estar bien es un destino absoluto cuya primera componente no sea del FHS: `/out`, `/src`, `/tmp` son rutas del lab. Dos correcciones al propio guardián, las dos aprendidas midiendo: · **Sólo audita el artefacto VIGENTE de cada receta.** El store guarda todos los sellados históricos, así que la primera versión acusaba a bzip2 por el artefacto YA SUPERADO — el mismo sobre-reporte que `static-audit.sh` tuvo que quitarse de encima. El hash vigente sale de los grafos de estado, no de 1150 llamadas a `takana hash`. ⚠ Y hay que regenerar **los cinco** grafos: con sólo el principal regenerado, el de KDE seguía apuntando al bzip2 viejo y el filtro lo dejaba pasar. · **Un barrido que no miró NADA lo dice y sale 2.** Filtrar por hash vigente hace que un `--store` que no case con los grafos deje cero artefactos auditados, y sin la guarda eso se imprimía como «✓ 0 ofensores»: una respuesta falsa con forma de respuesta. Probado: `--store /tmp` → exit 2. El control del hallazgo es el propio arreglo, sobre datos reales y no sintéticos: antes del fix el barrido nombra los cuatro enlaces de bzip2; después, `✓ ninguno`. |
||
|
|
0f58df4795 |
planear: el preflight medía contra / — y los datos de gioser no entran ahí
La cadena `censar → planear → aplicar` corrió entera contra gioser por primera vez: 59 pasos, 20
ejecutables y 39 manuales, bien separados. El ensayo en seco destapó lo que ninguna prueba de juguete
iba a mostrar: **33,6 G de datos contra una raíz con 3,5 G libres** (el sitio está en `/work`, 66 G).
Dos cosas estaban mal a la vez:
· **El plan copiaba ruta → MISMA ruta**, sin forma de decir dónde cae cada árbol en el destino. Ahora
`[[datos]]` tiene `destino` (vacío = la misma ruta). El fallo que evita es caro: aparecía a mitad
de un rsync de 22 G.
· **El preflight sumaba todo y lo comparaba contra `/`.** Acierta POR CASUALIDAD mientras todo caiga
en `/`, y da un veredicto completamente falso en cuanto una ruta va a otro montaje — en las dos
direcciones. Ahora mide por sistema de ficheros, agrupa los destinos, nombra qué rutas caen en cada
uno, y el veredicto lo saca el propio comando en vez de un humano leyendo una columna.
Los dos controles, contra la caja de verdad:
✗ / necesita 34386 MiB · libres 3596 ⇐ /var/www /var/lib /srv /opt /home exit 1
✓ /work necesita 34386 MiB · libres 66192 ⇐ /work/var/www … /work/home exit 0
Además, el paso `muere` ahora cumple la regla 2 ENTERA («por su nombre Y CON SU TAMAÑO»). Un dominio
fósil no pesa nada por sí mismo: pesa lo que dejó en disco, y `terapeuta.ec` —que no resuelve en
ningún DNS— tiene 279 M en `/var/www/terapeuta`. Que eso se supiera dependía de que alguien se
acordara. Se lista un renglón POR DIRECTORIO (siete dominios `*.gioser.net` reclaman el mismo
`gioser.bak`; repetirlo siete veces convierte el aviso en ruido) y se dice «podría ser de», nunca
«es»: el directorio se llama `terapeuta` y el dominio `terapeuta.ec`, y emparejar «casi» acierta casi
siempre y el resto de las veces manda a borrar lo que no era. Los directorios que no reclama nadie se
listan aparte: son los que nadie recuerda.
⚠ Y un bug que me hice yo y que el control cazó: empalmar por `str.index('# ── 1. datos ───')` cuando
ese marcador existe en DOS funciones. Tomó el corte al revés (j < i) y DUPLICÓ `def pasos` entero;
Python se queda con la última definición, así que el fichero importaba bien y generaba planes con el
preflight viejo. Se vio porque el comando del plan no era el que yo acababa de escribir. Un marcador
de empalme que no es único no es un marcador.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
|
||
|
|
bd1fa8bf8a |
boot entry backup/restore: el arranque como estado reproducible (ADR 0018 §5)
Guarda las variables de NVRAM CRUDAS —se reescriben tal cual; decodificarlas para volver a codificarlas al restaurar sería una oportunidad de perder algo que no entendemos— y de la ESP un manifiesto con hashes, no los bytes: el kernel ya vive en el store y copiarlo otra vez sería churn. El manifiesto es evidencia de qué había; para lo que no esté en el store dice qué falta, en vez de prometer reponerlo. El ADR se equivocaba en DÓNDE: decía "volcar al store". No va al store, porque lo barre store-gc.sh, que clasifica por nombre de receta — un respaldo ahí sería huérfano y se borraría en el primer --huerfanos. Vive en /var/lib/hammer/boot/, que en las imágenes es su propia partición. El nombre sale del CONTENIDO, así que un arranque que no cambió produce el mismo fichero: corre en cada arranque sin llenar la partición de copias. Lo que encontró la prueba de punta a punta y no estaba en el diseño: restaurar es volver al pasado, y lo que llegó DESPUÉS no estaba en ese pasado. Al reponer el BootOrder del respaldo, el Windows instalado más tarde quedaba FUERA del orden — correcto, y justo lo que el usuario no espera de algo llamado "restaurar el arranque". Ahora se avisa antes, con nombre y apellido, y restore NO escribe por defecto: la NVRAM es lo único de la máquina que no se rehace desde el store. El lector FAT ganó lectura de ficheros, con su trampa propia: hay que truncar al tamaño DECLARADO en el directorio, no al final del último cluster. Un fichero de 1,5 MB en clusters de 1 KiB termina con relleno y hashear el relleno daría un hash distinto al del mismo fichero en disco — el síntoma sería "dos respaldos del mismo arranque difieren". Verificado contra mtools como oráculo (1 500 000 y 900 000 bytes exactos, mismo sha256 que los originales) y con un test determinista que fabrica una FAT16 a mano, sin depender de que mtools esté. 79/79 del CLI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj |
||
|
|
d0e3aadb12 |
declarar: /proc pasa a ser un lector del centro — y había DOS emisores de tarjetas, mal los dos
`declarar.py` tenía su propio molde de card de arje y `formatos/arje.py` el suyo: el N×M que el
pivote existe para evitar, adentro de mi propio código. Y no quedó en teoría — CADA COPIA TENÍA UN
CAMPO MAL DE LA RAÍZ, Y NINGUNO DE LOS DOS EL MISMO:
campo declarar.py arje.py semilla REAL del producto
provides ["Spawn","Journal"] ✓ [] ✗ ["Spawn","Journal"]
supervision Restart{…} ✗ "OneShot" ✓ "OneShot"
Las consecuencias son concretas: sin `Spawn`/`Journal` las hijas no tienen quién las lance ni dónde
escribir, y una card `Virtual` con `Restart` le pide a arje que respawnee algo que nunca corrió.
Ahora hay un lector `proc` (el censo es un formato de origen, igual que systemd u OpenRC) y el
escritor `arje` es el ÚNICO que emite tarjetas; `declarar.py` queda con lo suyo, el perfil. Tres
lectores en la familia: systemd y openrc leen LO DECLARADO —que es lo que miente, `rc-status` daba
`stopped` para cinco servicios vivos— y `proc` lee LO QUE CORRE, que es donde aparecen los 15
`no-declarado`.
Dos cosas más, las dos sobre no mentir:
· **Una semilla vacía parecería un éxito.** Si ningún servicio tiene `decision = "muda"`, el lector
lo DICE y sale ≠0 en vez de emitir cero tarjetas en silencio. Mismo modo de fallo que un artefacto
vacío en el store.
· **El acta se ahogaba en su propio ruido.** Anotaba el `envp` vacío una vez POR SERVICIO: 26 líneas
idénticas que tapaban los dos hallazgos reales (`shuma-daemon` corre como `sergio`; los que no
tienen cmdline). La limitación es del lector y vale para todos ⇒ una entrada nombrando a los 26.
Un acta donde casi todo es la misma línea se deja de leer, y entonces no queda ningún acta. Y al
revés: las entradas que SÍ son por servicio ahora lo nombran (`gitea: cwd=/var/lib/gitea`).
Controles: la raíz generada coincide campo por campo con la del producto; dos corridas dan el fichero
byte a byte idéntico; 26 tarjetas con 26 ids únicos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
|
||
|
|
43a7ef0bd1 |
static-audit: auditaba SÓLO recipes/ y remataba con «toda receta lo cumple» — las 5 colas nunca se miraron
El audit del enlace estático globeaba `recipes/*.toml` y nada más, y cerraba con
«✅ toda receta que declara link=static lo cumple»: una frase verdadera de una PARTE del corpus,
presentada como si fuera de todo él. Las cinco colas (`incoming{,-kde,-gnome,-cosmic,-wlr}`) son
~305 recetas que **nunca se auditaron**.
Lo destapé por accidente: al promover `libseccomp` de `incoming-gnome/` a `recipes/`, **el artefacto
no cambió ni un byte** y el audit pasó de MIENTEN:0 a MIENTEN:1. La mentira estaba ahí desde siempre
y lo único que la tapaba era el glob de una línea. Un guardián que mide menos de lo que su resumen
afirma es peor que no tenerlo: da por cubierto lo que no mira.
Ampliado el barrido a las colas, aparecen **3** que llevaban invisibles:
libseccomp scmp_sys_resolver 6 rebuilds (corpus 1 + incoming-gnome 5)
libgcrypt dumpsexp y 2 más 41 rebuilds (corpus 1 + incoming-kde 40)
libgpg-error gpg-error 45 rebuilds (corpus 4 + incoming-kde 41)
Radios medidos con `yupana radio`, que cruza colas — no con `grep recipes/*.toml`, que es justo el
error que este commit arregla.
En las tres, lo dinámico es un **binario auxiliar de diagnóstico**, no la librería: los consumidores
enlazan el `.a`, así que el impacto funcional hoy es NULO. Lo que está mal es la declaración, y
arreglarla cuesta ~92 rebuilds casi todos de KDE. Van con el próximo bump de cada una, cuando el
re-hash ya esté pagado — mismo criterio que la deuda de `perl`. Decisión con el número delante, no
olvido.
Por eso entran en `DEUDA_DECIDIDA` y se imprimen como `•` sin hacer fallar: si el audit fallara
siempre por tres viejas, un ofensor NUEVO se perdería entre el ruido. El resumen ahora distingue
«MIENTEN (nuevos)» de «deuda decidida».
Probado con los dos controles y no sólo con el que da verde: quitándole a propósito el
`LDFLAGS=-all-static` a `crun` y reconstruyéndola, el audit sale **1** y la nombra; restaurada, sale
**0**. O sea que la tabla de deuda no se traga a un ofensor nuevo. El artefacto de la prueba se
borró del store.
|
||
|
|
2fa89ce3fa |
censar: tres nombres equivocados que llegaban al plan, y los 7 «binario desconocido» eran permisos
El censo nombraba los servicios por `comm`, que viene del kernel. De ahí salieron tres errores de
CLASIFICACIÓN — y no son cosméticos: ese nombre es el que el plan mete en `--in /etc/init.d/<n>` y el
que va de `label` en la tarjeta de arje.
· `comm` está capado a 15 caracteres: `willay-crosscheck` llegaba como `willay-crossche` y con ese
nombre no casaba contra su declaración ⇒ figuraba como `no-declarado` TENIENDO su tarjeta en la
semilla de arje. La línea de comando trae el nombre entero.
· `supervise-daemon` no es un servicio, es un ENVOLTORIO. Los cinco de gioser se fundían en una
entrada `supervise-daemo`; y del otro lado `dbus`, `metalog`, `dhcpcd`, `squid` y `shuma-daemon`
salían como `declarado-muerto` ESTANDO VIVOS — el mismo agujero que este censo existe para tapar,
entrando por la otra puerta. El nombre real es su argv[1] y el comando real es el último token
antes del `--` suelto (comprobado contra los cinco).
· `head -15` con ppid==1 era un resto de tubería reparentado, contado como servicio. Va a
`descartados`, que se imprimen: un huérfano es un hallazgo, no basura.
**Y los 7 «no se pudo leer su binario» eran dos cosas distintas.** Muchos demonios reescriben su
`argv[0]` (`sshd: /usr/bin/sshd [listener]`, `php-fpm: master process (…)`), así que la línea de
comando no dice cuál es el binario — `/proc/<pid>/exe` sí, y necesita ser dueño o root. Medido:
uid 1001 : desconocido 7 · paquete-ajeno 13 · receta-takana 6 · suelto 13
root : desconocido 0 · paquete-ajeno 20 · receta-takana 5 · suelto 14
Ahora el censo DICE cuál de las dos pasó: «repetilo con sudo» y «averigualo a mano» son trabajos muy
distintos, y confundirlos manda a alguien a investigar un permiso.
Desenvolver al supervisor arregló además dos datos que salían falsos: el `exec` de `squid` era
`/usr/bin/supervise-daemon` (⇒ figuraba provisto por el paquete `openrc`), y el `cmdline` guardado
era el del SUPERVISOR — una tarjeta hecha con eso arrancaría `supervise-daemon` dentro de arje, que
ya supervisa. Y se rescata el `--user`: `shuma-daemon` corre como `sergio`, dato que la tarjeta de
arje no puede guardar (no tiene campo de usuario), así que ahora se avisa EN LA REVISIÓN — sin eso a
la vista, un servicio que acá corre sin privilegios termina de root en el destino.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
|
||
|
|
2201fac08e |
boot entry survey: el informe del terreno, leyendo la ESP SIN montarla (ADR 0018 §4)
Reporta firmware, discos, ESPs, con quién se comparten y si la
instalación entra. No escribe nada — y para poder prometer eso hubo que
leer la ESP sin montarla, que es lo que obligó al lector FAT de sólo
lectura (fat_ro.rs). Montar para averiguarlo pedía privilegios, dejaba
un efecto secundario justo cuando prometimos no tocar nada, y falla si
el vecino dejó la FAT sucia por su hibernación.
Sobre una ESP de fábrica (100 MiB) con Windows dentro:
FAT32 «ESP» — 100.0 MiB totales, 77.1 MiB usados, 22.1 MiB libres
vecinos en \EFI: Microsoft, BOOT
⚠ «Microsoft» ⇒ hay Windows en esta ESP. Es el que reordena
BootOrder al actualizarse.
✗ NO ENTRA: hacen falta 31.0 MiB y hay 22.1 MiB libres
Contrastado contra mtools como oráculo, que es lo que hace creíble el
número: mdir dice "23 221 248 bytes free" y el lector propio dice
22.1 MiB — el mismo. Los clusters libres se cuentan recorriendo la FAT y
NO se lee el FSInfo de FAT32 a propósito: ese campo lo deja
desactualizado un SO que desmontó mal, y un número optimista de más
haría fallar la instalación a mitad — justo lo que el §4 evita.
En el instalador el §4 resultó ser algo más que "cuánto espacio hay": la
rama UEFI se lleva el disco ENTERO, así que lo que hay que decir en voz
alta es con qué se lo va a llevar puesto. El survey corre antes de
particionar y nombra el \EFI\Microsoft si está. El usuario se entera
ANTES, y no después de que su Windows dejó de arrancar.
Dos cosas que habrían pasado inadvertidas sin test: el nombre largo
tiene que ganarle al 8.3 (sin juntar los LFN, "Microsoft" se lee
"MICROS~1" y la advertencia no dispara nunca), y el tipo de FAT sale del
número de clusters y no del texto del BPB, que es informativo y hay
formateadores que mienten. El primer test del tipo lo escribí mal —1999
clusters ES FAT12— y el síntoma es idéntico a un bug del lector.
78/78 del CLI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
|
||
|
|
beb1c2a597 |
auditar-raices: el guardián de la raíz sucia sólo veía los perfiles — y hay 649 recetas fuera
`hydrate-profile.py` ya tenía el bloque «quién ensucia la RAÍZ del rootfs», y es bueno: mira por ARTEFACTO y no sobre el árbol fundido, porque en el fundido el nombre del culpable ya se perdió. Pero sólo corre al hidratar un perfil ⇒ **sólo ve lo que alguna imagen declara**. Hoy hay **649 recetas que no alcanza ninguna imagen**, y una fase `install` que se equivoca de destino en una de ésas es invisible hasta el día que alguien la declare. Lo destapó `qdrant`: selló con **69 M** de cabeceras de protobuf bajo `/src`, y no está en ningún perfil ⇒ ningún guardián lo habría visto. Lo encontré mirando el árbol del artefacto a mano antes de promoverlo, que es justo lo que no se puede dejar a que alguien se acuerde. `--auditar-raices` hace la misma pregunta sobre el STORE ENTERO sin hidratar nada, reutilizando el mismo `FHS_RAIZ` (una sola definición de «qué puede ir en la raíz», no dos que se desincronizan). Y dos tablas, porque un barrido que canta tres cosas de las cuales dos son correctas se deja de leer: · `RAIZ_POR_CONTRATO` — `seed-zig` (el toolchain ES el artefacto) y los rootfs (`store`/`ente` son suyos por diseño). Se imprimen como ⊘ con el motivo y no cuentan. · `RAIZ_DEUDA_DECIDIDA` — `perl` y sus 945 páginas nroff en `/`. Es suciedad REAL pero su arreglo está decidido EN CONTRA por ahora (una línea, 305 rebuilds, va con el próximo bump). Se imprime como contexto y **no hace fallar**: si fallara siempre, un ofensor NUEVO se perdería entre el ruido del viejo — que es exactamente cómo se muere un guardián. Probado con los dos controles, no sólo con el que da verde: sobre un store de juguete con una suciedad inventada sale **1** y la nombra; quitándola sale **0**. Sobre el store real: 0 ofensores nuevos sobre 3 artefactos conocidos. |
||
|
|
54bf3e810e |
ia: el modelo de chat, pineado — Qwen2.5-1.5B-Instruct Q4_K_M (Apache-2.0)
Elegido por tres cosas, y las tres medidas antes de pinearlo: licencia Apache-2.0 (lo que una distro puede shipear sin letra chica, al revés que Llama-3.2 o Gemma), habla español —se le preguntó qué es una distribución de GNU/Linux y contestó dos frases correctas— y corre en CPU: 20,1 tokens/s en el hub sin GPU, 1,04 GiB. El objeto pineado es un tar que envuelve el .gguf, publicado en el mirror y servido por sha256: takana extrae toda fuente con `tar` y un GGUF pelado no lo es. Es el camino de firefox-pgo-profile. La receta anota el sha256 del GGUF DE UPSTREAM (el lfs.oid de HuggingFace, verificado al bajarlo) y no sólo el del tar nuestro, para que nadie tenga que confiar en nuestro tar. Round-trip verificado: apartados el caché y el artefacto, el build lo bajó del mirror y selló el mismo ArtifactHash. Guardián en la receta, porque el fallo es callado: un fichero truncado o un HTML de error renombrado a .gguf se instala igual y sella en verde. Se comprueban el mágico GGUF y el tamaño. ⚠ Y el modelo de verdad destapó una CARRERA en el guardián del §6.7: el censo de motores contaba en el instante del cierre — con el de juguete daba 0 y con el de la imagen daba 1, que se lee como fuga cuando en realidad matar un proceso con un giga mapeado tarda ~1 s. Ahora espera hasta 15 s y anota cuánto tardó. La rotura a propósito sigue fallando, ahora con el tiempo a la vista. Falta decidir en qué imágenes se declara (con el motor son ~1,25 GiB por perfil) y pinear el de embeddings (multilingual-e5-small) para la mitad semántica del §6.3. |
||
|
|
66eaf8738f |
el reconciliador ARRANCA solo: montar sysfs, y un diagnóstico que no mentía a medias
Cierra el §2 del ADR 0018, verificado con dos arranques de la imagen
completa en OVMF:
1º (por la fallback) → "ESP detectada en /dev/vda p1",
"⚠ EL ARRANQUE ESTABA CAMBIADO — takana lo repuso",
"faltaba la entrada «takana» ⇒ escrita en Boot0004",
"BootOrder: 0000,0001,0002,0003 → 0004,0000,0001,0002,0003"
2º → BdsDxe: starting Boot0004 "takana" from HD(1,GPT,923A070F-…)
/\EFI\takana\takanax64.efi
"✓ arranque en orden: Boot0004 «takana» ya es la primera"
Un sistema recién instalado se da de alta en el firmware en su PRIMER
arranque y desde el segundo arranca por su propia entrada, sin que nadie
corra un comando. Y el segundo no escribe nada.
Lo caro no fue el reconciliador sino un diagnóstico FALSO: el primer
arranque con el hook dijo "sin firmware EFI — esta máquina no arrancó
por UEFI" en una VM que SÍ arrancó por UEFI. La causa no tenía nada que
ver con UEFI: busybox switch_root no arrastra /sys —igual que no
arrastra /dev, cosa que el script ya contemplaba— así que el directorio
de efivars no existía. El mensaje mandaba a investigar el firmware, que
estaba perfecto.
Dos arreglos:
- el wrapper monta sysfs y después efivarfs. CONFIG_EFIVAR_FS=y ya
estaba en el .config SELLADO (verificado, no supuesto) ⇒ no se
re-hashea ningún kernel.
- el mensaje distingue TRES estados que se parecen: no existe (BIOS o
/sys sin montar), existe y está VACÍO (falta el mount, y lo dice con
el comando exacto), o tiene variables. Decir "no hay UEFI" cuando
falta un mount manda a diagnosticar al lugar equivocado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
|
||
|
|
236abe48f3 |
boot entry reconcile: converger con la NVRAM en CADA arranque (ADR 0018 §2)
La NVRAM es estado compartido y el vecino la reescribe sin coordinarse. Contra eso no sirve confiar: sirve converger. `reconcile` descubre su propia ESP, repone la entrada y el BootOrder, y es idempotente. Descubre la ESP por TIPO de partición (GUID de ESP en GPT, 0xEF en MBR), que es el único dato fiable sin montar nada — un reconciliador que monta sistemas de ficheros en el arranque es un efecto secundario que no queremos. Si hay VARIAS ESP no adivina: las lista y pide --disk. Elegir mal significa escribir una entrada que apunta a un disco que puede no estar, y eso es peor que no escribir nada. No rompe el arranque por nada: sin firmware EFI (máquina por BIOS) lo dice y sale 0. Y cuando actúa, GRITA — va a /dev/tty0 además del serial, a diferencia del menú de arranque. Un reconciliador que repara en silencio deja al usuario conviviendo con una rareza intermitente que no entiende; el mensaje dice explícitamente que si se repite en cada arranque es que otro sistema operativo le está reescribiendo la NVRAM. Enganchado al wrapper de PID1 de las dos imágenes, con el mismo timeout y el mismo || true que el menú: corre ANTES del exec de arje-zero y colgarse ahí es un arranque muerto e indistinguible de un kernel colgado. El test que vale es el escenario completo: takana se instala, llega el vecino y se pone primero, y el siguiente arranque lo repone — SIN borrar la entrada del vecino (takana se pone primera, no lo echa) — y el arranque siguiente ya no escribe nada. Más el GUID de ESP, que va en orden DE DISCO y no en el legible: escribirlo "como se lee" es el error clásico y no casaría con ninguna ESP real. 15 tests en el módulo, 74/74 del CLI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj |