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.
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
`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.
De los 13 servicios cuyo binario NADIE provee —los que se pierden al apagar gioser— DIEZ salen del
mismo repositorio (`tawasuyu`, hoy `bad13117c`): matilda, pacha (paquete `pacha-cli`),
pacha-secretos, sandokan-watch (`sandokan-seguridad-core`), shuma-daemon, shuma-gateway, tejido, tupu
(`tupu-cli`), willay-crosscheck (`willay-cruce`) y willay-daemon. Los otros tres son ajenos
(act_runner, adb, y el `puerta-…` de target/debug). Una receta cubre diez servicios.
**El tamaño real es una quinta parte del aparente.** El Cargo.lock del workspace tiene 2823 crates,
pero eso incluye su stack gráfico, audio y Android; el subárbol que esos diez binarios necesitan son
524. Y sólo CINCO traen C: `aws-lc-sys` y `ring` (criptografía, compilan C/asm y piden cmake) más
`dirs-sys`, `inotify-sys` y `netlink-sys`, que son bindings puros sin librería externa.
⚠ **Lo que hay que decidir antes de escribirla.** `tawasuyu/rust-toolchain.toml` fija 1.96.0 y lo
argumenta en el propio fichero: una distro que promete builds deterministas y atestación firmada no
puede tener el compilador flotando. El lab de takana trae 1.97.0 y es RODANTE por diseño —
`lab-toolchain.lock` detecta la deriva, no la evita. Una receta takana compilaría con un rustc
distinto del que tawasuyu exige: el binario NO sería el mismo que corre, y rompería el contrato de
atestación del otro frente. Tres salidas, y es decisión, no trámite: pinear 1.96.0 para esta receta
(como las 84 que pinean zig 0.13.0), mover el pin de tawasuyu, o llevar los binarios tal cual
sabiendo que no reproducen.
Y no se construye en el origen: 4 núcleos y ~2 G disponibles. Eso va al worker dev.gioser.net.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
Ayer estas dos huyeron a `compiler = "gcc"` para esquivar un crash del linker en los tres
`protoc-gen-upb*`. **Esquivé el fallo sin conocer su causa, y salió caro:** al mover sólo protobuf, el
link murió con `undefined reference to std::__1::basic_string<…>` —el `__1` de libc++— porque abseil
seguía en zig, así que hubo que arrastrar las dos fuera del toolchain por defecto.
La causa apareció al día siguiente en `dwarves`, bisecando la línea de enlace real:
tal cual → exit 139 (SIGSEGV)
quitando `-static` → exit 139 ⇒ no es el enlace estático
quitando `--dependency-file` → **exit 0** ⇒ ES ESO
El `lld` de zig 0.16.0 segfaultea con el `--dependency-file` que CMake ≥3.27 mete en la línea de
enlace. Con `-DCMAKE_LINK_DEPENDS_USE_LINKER=OFF` **los cuatro binarios de protobuf enlazan con zig**,
incluidos los tres plugins que se caían.
Esto es, además, la validación de la causa raíz en un proyecto INDEPENDIENTE de dwarves: dos
proyectos sin relación, mismo síntoma, misma perilla, los dos arreglados.
Con el escape se va también el `-static-libstdc++`, que sólo hacía falta porque g++ enlaza contra la
libstdc++ de GNU. Comprobado sobre el artefacto: el único NEEDED de `protoc` es `libc.so`, que provee
`musl-shared`.
Verificado de punta a punta, no por el código de salida: `protoc --version` → `libprotoc 36.1`, y un
`.proto` compila a `m.pb.h`/`m.pb.cc` **y** a `p/m.pb.go` pasando por `protoc-gen-go` — o sea que la
cadena plugin↔driver que abrí anteayer sigue entera. Las dos REPRODUCEN bit a bit.
Se deja escrito el camino completo en las recetas, diagnóstico corto incluido, porque la lección no
es la perilla: es que **un escape que funciona sin explicar el fallo se paga después**, y acá se pagó
con dos recetas fuera del toolchain por defecto y un choque de runtimes de C++ que sólo apareció
porque el escape era parcial.
El diseño completo del hueco: qué declara el PAQUETE (`[[service]]`, hecho) y
qué decide el PERFIL (si arranca, pendiente), que es la misma partición que
systemd hace entre [Service] e [Install] y que `arje-absorb` respeta al absorber
sólo lo habilitado.
Deja escritas las dos cosas que cuestan caro si se descubren después:
1. El SDD 06 decía «el init (arje) lee ese árbol» de /etc/hammer/init.d/*.rule.
Es FALSO: el único uso de INIT_RULES_DIR en el repo es escribirlo. Había TRES
convenciones de dónde vive un servicio y ninguna se tocaba con las otras
(más una cuarta en `query service:`). Canónicos son los de arje —genesis de
la seed y cards.d/—; la mutación del .swm queda derogada o reapuntada, y la
afirmación falsa queda marcada en su propio doc.
2. La trampa para el emisor: el sidecar .hammer/recipe.toml NO entra al
ArtifactHash, así que el openssh ya sellado no lleva el bloque y, con el hash
sin mover, NUNCA se reconstruye solo. Derivar la seed del sidecar hoy daría
un producto SIN sshd, en silencio. Por eso product_seed_card() sigue usando
la constante a propósito, y re-sellar es una unidad aparte CON control de
reproducibilidad — openssh no está certificado como reproducible.
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.
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.
`dwarves` estaba SELLADA y llevaba tiempo sin construir. Se destapó al arreglar los symlinks de
`bzip2`: su hash se movió, dwarves cayó a deuda, y al reconstruirla el link murió con
`Error running link command: Segmentation fault` — crash del linker, no error de símbolos.
**No lo rompió el cambio de bzip2, y se puede probar**: el `libbz2.a` y el `bzlib.h` nuevos son
byte-idénticos a los viejos (`cmp -s`); lo único que cambió fueron cuatro destinos de symlink. El
artefacto sellado tapaba una rotura que ya existía — el lab rueda desde Alpine edge y zig subió. El
rehash no causó la rotura: la DESTAPÓ.
## La causa, bisecada sobre la línea de enlace real
CMake deja la línea literal en `build/CMakeFiles/<target>.dir/link.txt`, y el árbol de post-mortem la
conserva. Rehecha a mano FUERA de takana y del sandbox, sustituyendo el wrapper por el `zig` del lab
y las rutas `/usr/lib/*` por las del store:
tal cual → exit 139 (SIGSEGV)
quitando `-static` → exit 139 ⇒ no es el enlace estático
quitando `--dependency-file` → **exit 0** ⇒ ES ESO
`-Xlinker --dependency-file=…` lo emite CMake ≥3.27 para que el LINKER calcule las dependencias de
enlace, y el `lld` de zig 0.16.0 segfaultea procesándolo. `-DCMAKE_LINK_DEPENDS_USE_LINKER=OFF` es la
palanca de upstream (cmake del corpus: 3.31.6).
**Es mejor que `compiler = "gcc"`**, que es como esquivé ayer el MISMO crash en los tres
`protoc-gen-upb*` de protobuf sin conocer la causa: deja la receta en el toolchain por defecto del
proyecto en vez de escapar de él.
⚠ **Y el alcance no son dos recetas: 153 del corpus usan CMake con zig.** Todas selladas, así que hoy
nadie lo ve — pero el crash depende de los inputs (dentro de protobuf caían 3 de ~10 ejecutables), o
sea que no se sabe cuáles fallan hasta que su hash se mueva. Deuda latente pura.
No se arregla poniendo la perilla en el lab: la mayoría de esas 153 traen su `cmake …` EXPLÍCITO en
la receta, así que tocar la fase por defecto no las tocaría **y** re-hashearía a las que sí usan la
heurística. Incompleto y disruptivo a la vez.
Verificado: los 10 ejecutables estáticos con 0 NEEDED —incluidos `codiff` y `dtagnames`, los dos que
segfaulteaban—, `pahole --version` → v1.30, y REPRODUCE bit a bit.
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
`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
`/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
multilingual-e5-small sellaba, cargaba, contestaba 384 dimensiones y 45 tests en verde. Y con el
modelo de verdad, de punta a punta, el ranking devolvía SIEMPRE la misma página.
Seis pasos descartando hipótesis —batching, posición, nuestro código, la cuantización, la conversión—
hasta que `/tokenize` lo dijo en una línea: «cortafuegos», «minino», «duerme» y «tejado» van todos al
id 100 = `<unk>`. Un vocabulario XLM-RoBERTa por esta ruta deja casi todo en desconocido, y un texto
que es todo `<unk>` embebe igual que cualquier otro. La pista estaba a la vista desde el principio:
`gato~perro` y `gato~cortafuegos` daban el mismo número a CUATRO DECIMALES.
En su lugar, Qwen3-Embedding-0.6B Q8_0, GGUF oficial de Qwen (Apache-2.0): tokeniza español de verdad
(`cort|af|uegos`), acierta 3/3 con márgenes anchos (0,649 contra 0,237), y es de la misma familia que
el modelo de chat. Cuesta 610 MiB en vez de 126: es el precio de que funcione, y sube la cuenta de la
imagen a ~1,85 GiB si se declara.
⚠ Y LA PARTE QUE IMPORTA PARA LA PRÓXIMA VEZ: el guardián del `install` ya no mira sólo el mágico y
el tamaño —«existe» no es «sirve»—. Ahora tokeniza dos frases en español sin palabras en común y
exige que no compartan tokens (con el e5 roto compartían la mitad, todos `<unk>`), y después levanta
el servidor de verdad y exige que un gato se parezca más a un perro que a un cortafuegos. Con esos
dos chequeos el e5 no habría sellado nunca.
El tar del modelo roto se borró del mirror (476 MB) y del disco.
⚠ El build está en la cola del flock detrás de otro agente (pixi, 22 min y contando), así que el
artefacto todavía no está sellado y los guardianes del navegador no corrieron. Lo medido hasta acá:
el camino completo host→motor→índice con el modelo nuevo, 3/3 (`archive.ask` de punta a punta, fuera
de la jaula), más 45 tests en tawasuyu.
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
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.
La diferencia entre los dos syscalls decide el frente: kexec_load pide
los segmentos armados y el purgatory (= kexec-tools entero);
kexec_file_load recibe los descriptores y hace el trabajo adentro, ~50
líneas sin dependencias.
Con recipes/linux-metal-kexec.toml (única diferencia: CONFIG_KEXEC_FILE)
queda respondida media decisión abierta nº2: kexec y Secure Boot SÍ
pueden coexistir, porque lockdown prohíbe kexec_load y acepta
kexec_file_load con imagen firmada. Lo que queda es si se firma y quién
paga el artefacto extra — decisión de coste, no de viabilidad.
Y queda anotado el diagnóstico que mandaba al lugar opuesto: EPERM no es
"falta CONFIG_KEXEC_FILE" sino falta de CAP_SYS_BOOT. Medido en el hub,
cuyo kernel trae el flag y aun así dio EPERM por no ser root. Tercera
vez en dos días que el error caro no es el mecanismo sino el mensaje que
lo explica.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
Cuatro arranques en OVMF con dos kernels REALES del store:
1º 6.16.12 estable → stage del 7.1.2: "copiado (17142784 bytes)",
"BootNext = Boot0004"
2º starting Boot0004 "takana (candidato)" → uname = 7.1.2,
"⏳ EN PRUEBA" → confirm → "✓ promovido"
3º arranque normal → uname = 7.1.2: el estable cambió
4º stage de un candidato ROTO (4K de basura) y reinicio →
starting Boot0002 (la ruta normal) → uname = 7.1.2 →
"✗ el candidato Boot0004 NO arrancó: esta sesión vino del estable"
La cuarta es la que justifica todo: la máquina sobrevivió a que le
instalaran un kernel que no arranca, sin que nadie interviniera, y
además SABE que pasó.
Y lo hizo sin código nuestro: BootNext es de un solo uso y el firmware
la consume al leerla, así que si el kernel muere el siguiente arranque
ya no la encuentra y cae en BootOrder. No hay contador que mantener ni
estado que se pueda corromper — el mecanismo ES el firmware.
Se suma la detección de entradas huérfanas en boot-status, que salió de
ver al banco de pruebas perder el estado en un tmpfs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
Decisión del usuario (2026-09-12): `llama-cpp` + `ia-modelo-chat` de raíz en escritorio-gnome, -kde,
-cosmic y -sway. El motivo es que el panel de la barra lateral está en las cuatro —viene con `atuq`—
y una función del navegador que sólo existe en una imagen es una trampa: en las otras el usuario ve
el panel y lee «esta imagen no trae modelo».
Cuesta ~1,25 GiB por imagen (199 M el motor + 1,04 GiB el modelo) y está escrito en el comentario,
al lado de las líneas, para que bajarlo sea una decisión y no un descuido.
Verificado como manda la lección de `foot` que este fichero ya aprendió: las dos raíces expanden en
los cuatro perfiles (`scripts/targets.py`), las dos están SELLADAS al hash vigente, y el grafo las
ve con sus cuatro perfiles encima — una raíz que no resuelve quedaría `wanted` y ninguna métrica lo
diría. El grafo CIERRA y el topo-sort sigue OK.
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
Dos arranques reales seguidos, sin tocar nada, daban dos respaldos con
nombre distinto: boot-27b952c9….json y boot-f999e1e8….json. La promesa
de "un arranque que no cambió produce el mismo fichero" era falsa.
La causa: el filtro era «empieza por Boot», y eso deja entrar
BootCurrent, que dice por dónde arrancó ESTA vez y cambia en cada
arranque. Y encima es de SÓLO LECTURA, así que restore habría intentado
escribirla.
Ahora es lista blanca —BootOrder y Boot#### y nada más—, que también
deja fuera BootNext (de un solo uso: restaurarla dispararía un arranque
que nadie pidió) y BootOptionSupport (informativa). Ninguna de las tres
describe cómo debe arrancar la máquina.
Comprobado: cambiando BootCurrent a mano, el respaldo da el mismo
fichero y lo dice — "idéntico a uno que ya estaba: el arranque no
cambió".
Esto sólo se veía ARRANCANDO DOS VECES. Un test del respaldo contra un
efivarfs fabricado habría pasado en verde: la variable que rompía la
propiedad la pone el firmware, no el código.
80/80 del CLI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
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