Commit Graph
949 Commits
Author SHA1 Message Date
SergioandClaude Opus 5 65dab8d262 rust: rpath = false, la pareja obligatoria de poner lld como enlazador directo
El build con `ld.lld` en el spec murió compilando `std`:

    ld.lld: error: unknown argument '-Wl,-z,origin'
    ld.lld: error: unknown argument '-Wl,-rpath,$ORIGIN/../lib'

Con el enlazador invocado DIRECTO (sin driver de C), los `-Wl,…` que el bootstrap añade para el
rpath de sus propios binarios llegan tal cual a lld, que no los entiende — esa sintaxis existe para
que un `cc` los traduzca.

El rpath sobra igual: con `prefix = "/usr"` las librerías quedan en `/usr/lib`, que ya está en el
camino del cargador. Es lo que hacen las distros que empaquetan rust.

Costó 6 minutos de build descubrirlo, que es lo que vale tener la cadena en la granja: cada intento
falla temprano y con el mensaje exacto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:21:12 +00:00
SergioandClaude Opus 5 e903f486d9 el enlazador va DENTRO del compilador — /etc/cargo/config.toml es una ruta que cargo no lee
La prueba de la caja recién instalada —`cargo build` en un proyecto nuevo, sin flags ni rutas a
mano— falló con `linker `cc` not found`, y destapó que la pieza que había empaquetado era falsa:
**cargo NO LEE `/etc/cargo/config.toml`**. Sólo lee `$CARGO_HOME/config.toml`, los
`.cargo/config.toml` del proyecto y sus padres, y lo que se le pase con `--config` — que es lo que yo
venía pasando a mano en cada prueba, y por eso «funcionaba».

Y el arreglo no era mover el fichero: **el que enlaza es `rustc`, y rustc no lee la config de
cargo**. Las flags van DENTRO del compilador, en el spec del triple:

· `linker = "ld.lld"` + `linker_flavor = Gnu(Cc::No, Lld::Yes)` — sin esto rustc busca `cc`, que una
  caja takana no tiene.
· `crt_static_default = true` — sin esto enlaza dinámico y pide `-lgcc`, que no existe (la distro se
  construye con zig, que trae compiler-rt).
· `crt_static_allows_dylibs = true` — su pareja obligatoria: sin ella `crt-static` apaga los dylibs
  y con ellos los PROC-MACROS (serde_derive, clap_derive, las 44 recetas `cargo-*` con `derive`).
  El compilador en sí sigue dinámico: el `bootstrap.toml` fija `crt-static = false` para el build, y
  eso gana sobre el default del spec.

⇒ `cargo-config` (receta, árbol y copia en scripts/) se van enteros: un paquete que instala un
fichero que nadie abre es peor que no tenerlo.

Y de paso, `lld21` instala los cuatro alias como SYMLINKS y no como copias: cmake los duplicaba, 5 ×
96 M = 478 M de artefacto para 97 M de enlazador. Ahora 108 M, y `ld.lld` sigue despachando por
`argv[0]`, que es como upstream lo diseñó.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:07:56 +00:00
SergioandClaude Opus 5 0a894cfac1 perfil.servidor: fuera el prebuilt ajeno, entra el toolchain propio
Sale `rust-toolchain-bin` —799 M de bytes AJENOS sellados tal cual (`foreign`)—, que era el escalón 1
del SDD 31 y cumplió su función: con él se construyó el escalón 2, y el escalón 2 ya compila,
enlaza y corre.

Entran TRES, que son el mismo hecho («esta caja compila Rust») partido en piezas:
· `rust` (353 M) — el compilador y cargo, de la granja, desde fuente.
· `lld21` — el enlazador. NO es opcional: una caja takana no tiene `cc`, y sin enlazador `rustc`
  compila objetos y no produce un ejecutable.
· `cargo-config` — receta nueva que publica `/etc/cargo/config.toml` con las tres flags que unen a
  los dos. Sin ellas `cargo build` muere con «linker `cc` not found»: el toolchain estaría completo
  y MUDO, que es [[subcomando-sin-driver]] otra vez. Es config de SISTEMA y no de usuario a
  propósito — un `~/.cargo/config.toml` funcionaría para quien lo escribió y no para el siguiente.

⚠ Y `rust` se re-hashea otra vez (702094a8 → fe277b32) por una línea que descubrió el control de
PATH: **x.py instala en `/usr/local` por defecto, y el PATH de una caja takana es
`/bin:/usr/bin:/sbin:/usr/sbin`** — medido en la caja. O sea que el perfil habría proyectado un
`rustc` perfecto que nadie puede invocar: «existe ≠ se encuentra». Ahora `[install] prefix = "/usr"`,
como el resto del corpus.

Control previo que sí pasó: `rustc` y `cargo` corren SIN `LD_LIBRARY_PATH` — su `RUNPATH` es
`$ORIGIN/../lib`, así que la proyección del artefacto basta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:38:15 +00:00
SergioandClaude Opus 5 bad2b1c63a llvm21: dynamic y con zlib — las 79 herramientas dejan de crashear, y rust se rehace detrás
Dos cambios, los dos medidos, y los dos re-sellan la cadena entera (`link` y las deps son entrada de
hash): `llvm21` 95956a16… → bd4a4094…, `rust` 015a07fb… → 702094a8…, `lld21` → 95a4794b…

1. `link = "dynamic"`. Con `static`, las 79 herramientas que publica el artefacto CRASHEAN —`llc
   --version` incluido, y también en el worker, o sea que es el build y no el entorno—: salen
   `static-pie` y el enlace estático descarta los constructores globales de los que dependen los
   registros de LLVM (`cl::opt`, `TargetRegistry`). Funciona lo que no los necesita (`llvm-ar`,
   `llvm-config`) y revienta lo que sí. **No se notó en meses porque el único consumidor, `rust`, usa
   `llvm-config` y las `.a` — nunca una herramienta.** Lo destapó `lld21`, que enlaza contra estas
   mismas librerías: estático crasheaba con cualquier entrada, dinámico da errores limpios.
2. `-DLLVM_ENABLE_ZLIB=ON` + dep `zlib`. Sin eso, cualquier `lld` construido contra este LLVM hereda
   `LLVM_ENABLE_ZLIB 0` de `LLVMConfig.cmake` y no puede leer las secciones de depuración COMPRIMIDAS
   que trae la libc del lab. Encenderlo acá es la única vía: el build standalone de lld no puede
   contradecir a su LLVM.

Las `.a` que consume `rust` no cambian de contenido —el flag sólo quita el `-static` del enlace de
los EJECUTABLES— pero el hash sí, y eso es correcto: es lo que hace que la granja rehaga rust con un
LLVM cuyas herramientas funcionan.

Los tres hashes coinciden hub↔worker antes de sembrar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 10:36:20 +00:00
SergioandClaude Opus 5 201973b8f6 lld21: dinámico y con zlib — dos fallos que sólo se ven USÁNDOLO
El primer `lld21` selló, arrancó y se moría en cuanto hacía algo: `--version` y `--help` contestaban
bien, y CUALQUIER enlace —incluso uno que sólo debía dar un error, como un fichero de entrada
inexistente— terminaba en SIGSEGV con el banner de LLVM y un «Stack dump:» vacío.

🧨 Y no era culpa de esta receta: **las 79 herramientas que publica `llvm21` tienen el mismo defecto**
—`llc --version` también crashea, y también en el worker— porque salen `static-pie` y el enlace
estático descarta los constructores globales de los que dependen los registros de LLVM
(`cl::opt`, `TargetRegistry`). Lo que funciona es lo que no los necesita (`llvm-ar`, `llvm-config`),
y por eso nadie lo había visto: `rust` usa `llvm-config` y las `.a`, nunca una herramienta.
⇒ `link = "dynamic"` en esta receta (como `openssl-threads`, y por lo mismo: el flag global decide
cosas que la receta no dice). Control en los dos sentidos: con `static`, SIGSEGV; con `dynamic`,
`ld.lld: error: cannot open /no/existe.o: No such file or directory` — un error limpio.

Y el segundo, ya enlazando de verdad con nuestro rustc:

    lld: error: …/self-contained/crtn.o:(.debug_line) is compressed with ELFCOMPRESS_ZLIB,
         but lld is not built with zlib support

Los objetos `crt*` del sysroot de rust llevan las secciones de depuración COMPRIMIDAS. Para `llvm21`
apagar zlib era gratis; para el ENLAZADOR no lo es. ⇒ `-DLLVM_ENABLE_ZLIB` encendido y `zlib` a las
deps — copiar los flags del hermano sin preguntarse qué hace cada uno es lo que lo causó.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 04:36:38 +00:00
SergioandClaude Opus 5 a9911785e1 lld21: el enlazador de la distro — porque el bootstrap de rust se NIEGA a construirlo
La vía (a) está CERRADA, y lo dice el propio bootstrap en una línea:

    Cannot enable LLD with `rust.lld = true` when using external llvm-config.

Había leído el paso `Lld` —que compila desde el `src/llvm-project/lld` del tarball y reusa el
`llvm-config` externo— y me faltó mirar la VALIDACIÓN de config, que rechaza la combinación antes de
llegar a ese paso. La guarda tiene sentido: LLD y LLVM comparten ABI de C++, y con un llvm-config
externo el bootstrap no puede garantizar que casen. O sea: mientras usemos el `llvm21` del corpus
—que es lo que queremos— el toolchain NO se va a construir su propio `rust-lld`.

⇒ `recipes/lld21.toml` construye el MISMO `lld`, del mismo `llvm-project` (mismo tarball y mismo
sha256 que `llvm21`, byte por byte), pero SÓLO el subproyecto: `cmake -S lld` contra el LLVM ya
instalado por la dep. Minutos en vez de horas, y sin re-hashear `llvm21` —que encender
`LLVM_ENABLE_PROJECTS=lld` ahí dentro habría arrastrado a `rust` con él.

⚠ Y al revertir el flag aprendí algo que vale para todas las recetas con heredoc: el texto del
`bootstrap.toml` que la fase escribe **es parte de la fase, o sea ENTRADA DE HASH**. Mi comentario
explicando el fallo, puesto ahí adentro, cambiaba el hash del compilador entero: hora y media de
granja por un párrafo. Va fuera, como comentario de la receta, y el hash vuelve a `015a07fb…` — el
artefacto que ya está sellado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 01:02:49 +00:00
SergioandClaude Opus 5 b7f70f37a8 rust: lld = true — el compilador que no podía enlazar ahora trae su enlazador
Muro 8, cerrado por la vía (a): el toolchain se construye su propio `rust-lld` en vez de depender de
un `cc` que en una caja takana no existe.

Lo que lo destraba es una línea del log que parece informativa: `skipping llvm-tools (…): external
LLVM`. Al usar un LLVM externo —que es lo que queremos, porque `llvm21` es del corpus— x.py se salta
las herramientas de LLVM, y `rust-lld` es una de ellas. El prebuilt oficial sí la trae, y por eso el
escalón 1 enlazaba y el 2 no.

Comprobado ANTES de encolar dos horas de build, leyendo el bootstrap y midiendo los artefactos:
· el paso `Lld` compila desde `src/llvm-project/lld`, que YA VIENE en el tarball (1,1 G de fuente);
· usa el `llvm-config`/cmake del LLVM EXTERNO y NO dispara un build de LLVM entero;
· `llvm21` publica `lib/cmake/llvm/LLVMConfig.cmake`, que es justo lo que ese paso necesita;
· `dist.rs` copia `rust-lld` al sysroot SÓLO `if builder.config.lld_enabled`;
· y `llvm21` no servía de repuesto: sobre sus 1,6 G no publica NINGÚN `lld`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 00:58:29 +00:00
SergioandClaude Opus 5 ac856501b2 rust: el séptimo muro es el openssl de cargo — y va la variante CON THREADS
El parche del triple FUNCIONÓ: el build pasó el stage 2 —donde moría— y llegó hasta las herramientas.
Ahí cortó otra cosa:

    error: failed to run custom build command for `openssl-sys v0.9.114`
    The system library `openssl` required by crate `openssl-sys` was not found.

`tools = ["cargo"]` arrastra `openssl-sys`, y eso pasa DESPUÉS de compilar las ~400 crates del
compilador, o sea a la hora y media de build.

La dep que entra es `openssl-threads` y NO la canónica, aunque las dos publiquen sólo `.a`: el
`Configure` de openssl APAGA LOS THREADS en cuanto ve `-static` en LDFLAGS, y la canónica se
construye así. Un cargo —masivamente multihilo— enlazado contra un openssl sin soporte de hilos es
la clase de fallo que no se ve al construir ni al arrancar. La variante ya existía por exactamente
lo mismo para `python3`, y el worker la tiene sellada con el mismo hash.

Van también `OPENSSL_STATIC=1` y `OPENSSL_DIR=/usr`: el lab publica `.a` y ningún `.so`, y sin
decírselo `openssl-sys` busca la dinámica y falla con un mensaje que habla de pkg-config.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 23:59:22 +00:00
SergioandClaude Opus 5 d3dcd739cc sacar el kernel de la cola de la granja: se construyó en la caja
`recipes/incoming/linux-generic.toml` entró a la cola y salió el mismo día. El motivo es concreto:
la cola del worker se muele EN SERIE y `rust` está horas adentro, así que el kernel —que era lo
urgente, porque sin él no hay cortafuegos— habría esperado a que rust terminara. Se construyó en la
propia caja (4 cores, 4,6 G libres, CPU ociosa) en 21 minutos.

Dejarlo encolado no era gratis: el worker no tiene ese artefacto en su store —la cosecha va
worker→hub y nunca al revés— así que lo habría reconstruido entero para llegar al mismo b3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 22:00:10 +00:00
SergioandClaude Opus 5 de16c4ba24 rust: el triple de Alpine, DENTRO del compilador — el sexto muro, atacado
`recipes/rust-alpine-target.patch` registra `x86_64-alpine-linux-musl` en `rustc_target`: un spec
copiado del canónico y una línea en `supported_targets!`. Es lo que hace Alpine en su APKBUILD, y es
la única forma que vale para el triple del HOST (un `.json` por `RUST_TARGET_PATH` no alcanza).

La única diferencia real con el spec canónico es `crt_static_default = false`, que es lo que habilita
dylibs y con ellos los PROC-MACROS: un rustc con crt-static por default no compila `serde_derive` ni
`clap_derive` — ni las 44 recetas `cargo-*` del corpus que usan `derive`.

⚠ PROBADO EN SECO ANTES DE ENCOLAR UNA HORA DE BUILD (`patch -p1 --dry-run` contra el árbol real del
worker), y no de primera: la cabecera `@@ -0,0 +1,38 @@` del fichero nuevo tenía 37 líneas y `patch`
murió con «malformed patch», y el hunk de `mod.rs` lo escribí con números a ojo y falló. El segundo
lo generé con `diff -u` de verdad contra una copia. Un parche se COMPRUEBA, no se redacta.

⚠ Y EL `.patch` HAY QUE ENCOLARLO TAMBIÉN: `source.patches` resuelve contra el directorio de la
receta, así que con la receta enlazada en `recipes/incoming/` el parche se busca AHÍ. Es exactamente
el corolario que `farm-worker-loop.sh` documenta («los `.patch` NO viajan con la receta»), visto
desde el otro lado. Con los dos enlaces, el hash de la cola y el canónico coinciden: `b3:53c7f991…`,
y el worker calcula el mismo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:59:55 +00:00
SergioandClaude Opus 5 24a6dd3b2f el cortafuegos puesto en la caja — y el primer baneado fui yo
`table inet tawasuyu_input` con policy drop, 15 reglas de puerto, y el set `ban4` llenandose solo:
a los pocos minutos, 27 IPs baneadas y paquetes tirados por el contador. El reemplazo de fail2ban
funcionando sobre trafico real, dentro del kernel y sin daemon.

🧨 EL PRIMER BANEADO FUI YO, EN SEGUNDOS. Aplicada la primera version: ssh y https bien, y `http`,
proxy y git-ssh muertos; al minuto la caja entera dejo de contestarme. La causa no es una regla mal
escrita: **el set @ban4 es UNO SOLO para todos los servicios** y `ip saddr @ban4 drop` esta antes
que todo, asi que pasarse de tasa en UN puerto te tira en TODOS — y el `nuevas_por_min: 5` del
puerto de administracion es exactamente lo que me pasa a mi, porque cada comando remoto es una
conexion nueva. Lo devolvio el INTERRUPTOR DE HOMBRE MUERTO (un `nft flush ruleset` programado a
180 s que sólo se desarma si la verificacion externa sale bien). Sin consola serie, esa es la
diferencia entre un susto y un rescue.

⚠ Y el tipo ya lo avisaba: `PoliticaEntrada` tiene un campo `confiables` cuyo comentario describe
palabra por palabra lo que me paso. Copie el .ron de EJEMPLO en vez de leer el TIPO. La cura fue una
linea con el hub y el worker, que se aceptan ANTES de la logica de tasa.

⚠ `nft -c` rechazo 5 reglas antes de aplicar nada: `ct count over N` es CONFIG_NFT_CONNLIMIT y este
kernel no lo trae. Quedan COMENTADAS y no borradas en el fichero que se aplica — lo que la politica
declara y el kernel no puede tiene que verse. El simbolo ya entro a la receta del kernel.

Y para que sobreviva al reinicio, `nftables` declara su [[service]] con `lifecycle = "oneshot"`:
cargar un reglaset no es un daemon, es un acto. Va PRIMERO en el genesis — entre que la red esta y
que las reglas cargan, la caja esta abierta. Probado con `nft flush ruleset` + `arjectl start`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:56:51 +00:00
SergioandClaude Opus 5 f57c18ab7f kernel con nf_tables: construido y verificado — y NFT_COUNTER ya no existe
`b3:63fdd57c…` sellado EN LA CAJA (gioser tiene 2 G disponibles y SWAP 0; el worker está horas con
rust) en 21 minutos. Los dos controles, sobre el `.config` SELLADO y no sobre la receta:

· lo que se quería encender: NETFILTER_ADVANCED=y, NF_TABLES=y, NF_TABLES_INET=y, NFT_CT=y,
  NFT_LIMIT=y, NFT_LOG=y, NFT_REJECT{,_INET,_IPV4,_IPV6}=y, NFT_SYNPROXY=y.
· lo que NO podía romperse (el muro 1 del §6.4, que dejó una caja sin ver su disco): SCSI_VIRTIO=y,
  VIRTIO_BLK=y, VIRTIO_NET=y, VIRTIO_PCI=y, SCSI=y, EXT4_FS=y. Los seis siguen.

⚠ `NFT_COUNTER` salió AUSENTE — y está bien: en 7.1 ese símbolo **ya no existe** (los contadores son
parte del core de nf_tables), así que no aparece en el `.config` ni como `# … is not set`. La línea
`-e NFT_COUNTER` es inerte y se deja escrita CON LA NOTA, porque sin ella el próximo que compare la
receta con el `.config` va a leer esa ausencia como un fallo. El comentario no re-hashea: el hash
sigue en `63fdd57c…`, comprobado.

El kernel nuevo queda EN `/boot/bzImage.nuevo`, al lado y no en su sitio, con el vigente copiado a
`/boot/bzImage.sin-nftables`. Hasta que alguien decida, la caja arranca lo de siempre — y ese
camino de vuelta es el único que hay: **hcloud no tiene consola serie**, así que un kernel que no
arranca se arregla con rescue + restaurar el bzImage, no apretando una tecla en el menú de grub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:36:37 +00:00
SergioandClaude Opus 5 743d83d5e9 el kernel genérico enciende nf_tables — y rust entra a la cola de la granja
Dos cosas a moler, las dos en el worker (que tiene 6 cores, 16 G y 94 G libres), encoladas por
ENLACE a la receta canónica y no por copia: `takana hash` da el MISMO b3 por el enlace que por el
fichero real, así que la cola no puede divergir de lo canónico — que es justo el defecto que el
propio `farm-worker-loop.sh` documenta de las colas viejas («su copia aparcada era la versión
ANTERIOR y seguía leyéndose como deuda abierta»).

· `linux-generic` (b3:63fdd57c…, antes 23f1cc43…): `NETFILTER_ADVANCED`, `NF_TABLES`,
  `NF_TABLES_INET`, `NFT_CT`, `NFT_LIMIT`, `NFT_COUNTER`, `NFT_LOG`, `NFT_REJECT{,_INET}` y
  `NETFILTER_SYNPROXY`/`NFT_SYNPROXY`. Es lo que pide el cortafuegos de tawasuyu (SDD-ENTRADA): la
  tabla `inet`, el estado de conexión y el `limit rate` POR ELEMENTO de set, que es el ban por IP
  dentro del kernel y el reemplazo de fail2ban. SYNPROXY va ahora porque encenderlo después cuesta
  otro kernel y otro reinicio de producción.
· `rust` (b3:aa5d81e8…): el escalón 2 del SDD 31, que estaba en `never`. El worker ya tiene `llvm21`
  sellado con el MISMO hash que el hub (95956a16…), así que arranca desde ahí y no lo reconstruye.

Los dos hashes coinciden hub↔worker, comprobado antes de sembrar: el worker va a sellar exactamente
lo que sellaría acá.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:12:12 +00:00
SergioandClaude Opus 5 0a7afebe9c la caja tiene HORA, LATIDO y ROTACIÓN — y el respaldo del gitea dejó de ser a mano
`planear.py --revisar` sobre el censo ordena lo que queda, y lo primero no eran los servicios del
usuario sino TRES CAPACIDADES que ninguna metrica reclama: el disparador periodico, la hora y la
rotacion de logs. Los tres paquetes ya estaban sellados y declarados en `perfil.servidor` desde que
ese perfil nacio — lo que faltaba era el eslabon que los ARRANCA, igual que con squid.

· `chronyd`: la caja iba **15 s atrasada** y nadie la corregia. Ahora stratum 3 contra los NTP de
  Hetzner, 0,000005 s de NTP. Sin hora no hay TLS ni firmas, y el sintoma no se parece a la causa.
· `crond`: no existia el latido. Verificado con una entrada `* * * * *` en el crontab REAL.
· `logrotate`: `/var/log/squid` sin rotar. Diario, con `squid -k rotate` (squid mantiene los
  ficheros abiertos: un rename a secas lo deja escribiendo en un inode que nadie puede leer).
· `scripts/respaldo-gitea.sh` + `17 3 * * *`: cierra el hueco del §6.15 — los 44 repos vivian en UNA
  copia y el respaldo se hacia a mano. Sube 23+21 repos (1,5 G) y un `gitea.db` de 332 M tomado con
  `.backup`, consistente con el servidor vivo. El control no es que el script salga 0: es CONTAR los
  repos de los dos lados.

`chrony` y `cronie` NO declaraban `[[service]]` — el paquete llegaba a la imagen y no lo arrancaba
nadie. Ahora lo declaran (fuera de `hash_inputs`: los hashes no se movieron) y el perfil los habilita.

TRES MEDICIONES que valen mas que el resultado:
1. **La deriva era de gioser, no de la caja**: despues de sincronizar, el HUB quedo 3 s adelantado.
   Medir «contra el otro» sin un tercero no dice quien esta mal.
2. **El spool de cronie es `/var/spool/cron/<usuario>`**, no `…/crontabs/<usuario>`: `crontab -l`
   mostraba las entradas y `crontabs/` estaba vacio. Un crontab restaurado en el sitio equivocado es
   un fichero perfecto que nadie lee.
3. 🧨 **`sqlite3` seguia INERTE en la caja** (`compressBound: symbol not found`): el ultimo de los 23
   del §6.4. Le faltaba `zlib-shared`, sellado y declarado en `perfil.base` desde el 11-09 — pero la
   caja se instalo antes. Sin el no habia snapshot consistente. Un paquete declarado no es un
   paquete instalado: en una caja viva vale lo que el `upgrade` proyecto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:17:20 +00:00
SergioandClaude Opus 5 b927eeffcb squid corre desde el corpus: la jaula duró unas horas y se tiró
`b3:932ba096…` proyectado en la caja y arrancado por arje SIN bwrap: `pid 11548 · uid=968 ·
exe=/usr/sbin/squid`, con la cuenta `proxy` que declara la receta. La instancia `qorpa/squid` se
borró — el manifiesto es descartable por diseño (ADR 0015 D3) y esto es para lo que existía.

El arreglo de esta tanda: **`--disable-log-daemon-helpers` compilaba perfecto y no arrancaba**. El
default de `access_log` es `daemon:`, asi que sin `log_file_daemon` squid muere con `FATAL:
logfile_daemon /usr/lib/squid/log_file_daemon: (2) No such file or directory`. Sellaba, hasheaba y
pasaba `-k parse` — se ve SOLO levantando el servicio con la config real. Familia
[[subcomando-sin-driver]].

El control que lo caza, y que corre en el hub antes de tocar produccion: con la config REAL del
origen y en un puerto aparte, arranque completo + CONNECT a claude.ai y api.anthropic.com con
TCP_TUNNEL/200 + el access.log escrito por el log daemon + `basic_ncsa_auth` leyendo el `passwd` de
verdad (control negativo: clave mala ⇒ `ERR Wrong password`). Los DOS primeros intentos de esta
receta habrian llegado a produccion sin ese control: el binario estaba sellado y el hash era estable
las dos veces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:04:54 +00:00
SergioandClaude Opus 5 a394dda26a receta: squid 7.7 entra al corpus — y el --export-dynamic que desactivaba el estático
`b3:54070b26…`, 8,8 M, **estático de verdad** (binario y los tres helpers), y probado con la CONFIG
REAL de produccion: parsea sin FATAL, tunela `CONNECT claude.ai` y `CONNECT api.anthropic.com` con
TCP_TUNNEL/200, y su `basic_ncsa_auth` lee el `passwd` de los tres usuarios (control negativo: con
una clave mala contesta `ERR Wrong password`).

Es la respuesta definitiva a lo de ayer: el proxy se levanto primero en una jaula qorpa porque
estaba EN USO y eso lo destrabo en minutos, pero squid es C++ y el lab lo construye. La diferencia
con php-fpm importa: PHP no queremos que entre al corpus, squid si.

⚠ LA FUENTE NO SALE DE squid-cache.org: esa URL devuelve **200 con una pagina HTML de 8985 bytes**,
no el tarball. Pinear su sha256 habria anclado la receta a una pagina de error. Va el release de
GitHub, que ademas trae `configure` ya generado.

🧨 EL MURO, y es del lab: el primer sellado salio con `usr/sbin/squid` **dinamico y `NEEDED
libc.so`** mientras sus helpers salian estaticos — o sea inerte en cualquier imagen sin cargador
([[needed-colgante-libstdcxx]]) pese a `link = "static"`. La cadena: `squid_LDFLAGS` trae
`-export-dynamic` (para modulos eCAP, que estan apagados) ⇒ **el wrapper del lab quita `-static` de
cualquier enlace que mencione `--export-dynamic`**, a proposito, para poder enlazar las `.so`
dlopen-ables de Python y companhia. Y no alcanza con sacarlo de la variable: **libtool lo vuelve a
anhadir solo** en cuanto hay un `-dlopen`. Se vacia `export_dynamic_flag_spec` en el `libtool`
GENERADO (local al build, sin tocar la fuente) y con guarda: si el campo no aparece, aborta — un
`sed` que no acierta deja pasar el problema y el binario sale dinamico sin que nada falle.

⚠⚠ Y `-dlopen force` NO sobra, aunque lo parezca: vaciar `squid_LDFLAGS` entero rompe el enlace con
`undefined symbol: lt__PROGRAM__LTX_preloaded_symbols`, que emite el propio libtool **porque hay un
`-dlopen`**. De los dos flags, el que estorba es uno solo. Distinguirlos costo un build; adivinar
cuesta lo mismo y no ensenha nada.

Declarado en `perfil.servidor` como paquete Y en `servicios`, con su `[[user]] proxy` y su
`[[service]]` (SDD 30) — que comprueba la config del sitio y la cuenta, y sale 78 nombrando cual
falta en vez de dejar un bucle de reinicios. El hash NO cambio al declararlos: `[[user]]` y
`[[service]]` estan fuera de `hash_inputs`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:49:53 +00:00
Sergio 1e0e249d44 Merge remote-tracking branch 'origin/main' 2026-09-15 17:18:04 +00:00
SergioandClaude Opus 5 dbfb7d492b la jaula no viajaba en NINGUNA imagen: bwrap y harkaq-exec entran a perfil.base
Mudando `api.sergio.gioser.net` a la caja de produccion, `takana qorpa provision` aborto:

    Error: no encuentro harkaq-exec en /root/.cache/harkaq/harkaq-exec
      construilo:  gcc -O1 -Wall -static -o ... scripts/harkaq/harkaq-exec.c

El mensaje es bueno y la receta que propone es IMPOSIBLE de seguir: en una caja instalada no hay
gcc, ni `scripts/`, ni arbol de desarrollo. El binario solo existia como un `gcc` a mano en la
cache de `$HOME` del hub, o sea que la jaula del ADR 0015 funcionaba unicamente en la maquina
donde alguien la habia compilado.

Y su companero estaba igual, medido en `build-state.json`: `bwrap` sellado desde hace meses con
`"perfiles": []` — CERO perfiles. Lo invocan por PATH tanto `qorpa` como el sandbox de
`takana build` (`takana-build/src/sandbox.rs`), asi que **ninguna imagen de takana podia enjaular
nada, ni construir**. Es [[subcomando-sin-driver]] un piso mas abajo: el CLI que los llama viaja
en todas las imagenes y sus herramientas en ninguna.

Tres piezas:

· `recipes/harkaq-exec.toml` — nueva. Estatico musl, `b3:cd34954f...`, 269 K. El pin va al commit
  que toco la FUENTE (`1d9ddcee`, 2026-09-03) y no a HEAD: el `.c` no se mueve desde entonces y el
  repo commitea cada media hora por el cron de la cosecha — pinear HEAD re-hashearia la receta cada
  media hora sin que su fuente cambiara. La fuente sigue en `scripts/harkaq/` y no en un arbol
  propio porque tres scripts la compilan desde ahi y dos copias divergen en silencio.
· `qorpa.rs` — busca el binario tambien en `/usr/bin`, que es de donde sale en cualquier maquina
  que no sea el hub. El orden es HARKAQ_BIN (lo que el operador declara) → arbol de desarrollo →
  paquete, para que un cambio en la jaula se pruebe sin instalar nada. Y el error ya nombra las
  dos salidas, no solo la del hub.
· `targets.toml` — los dos en `perfil.base`.

Medido en la caja de produccion tras aplicarlos: `bwrap 0.11.0` + harkaq-exec responden, y
`takana qorpa provision sergioh-api` instala python 3.14.7 con pacman DENTRO de la jaula
(`[harkaq] jaula puesta: ABI 9`). El 3.14 no es casualidad: el venv de gioser trae extensiones
`cpython-314-...-gnu.so`, asi que la imagen de Arch pineada da la misma serie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:17:42 +00:00
sergioandsergio 6f0ba974c7 feat(atuq): la boveda de la suite guarda las contrasenas, y el gestor de Gecko se aparta
La decima extension de atuq. Ofrece la credencial que corresponde al sitio
abierto y la pone en el formulario, pero NO puede sacar una contrasena de la
boveda por su cuenta: pregunta cuales corresponden —y eso contesta titulo y
usuario, nada mas— y la contrasena sale por otro verbo que pregunta en el
escritorio antes de contestar. Si la extension queda comprometida, o si una
pagina consigue hablarle, lo que obtiene es la lista de titulos de los sitios
que coinciden con su propia direccion.

La direccion siempre la pone el chrome y nunca la pagina. Lo unico que el
trozo que corre dentro del sitio aporta es que HAY un formulario y lo que el
usuario tipeo; si pudiera decir de quien es la pagina, podria decir que es el
banco.

Y la extension no decide nada: quien sabe que credencial va en que sitio es
la funcion del otro lado del cable. Ponerlo en JS seria reimplementar el
original y, peor, poner en la pagina la decision de a quien se le entrega una
contrasena.

EL GESTOR DE GECKO SE APAGA, pero como valor de arranque y no como politica.
Dos gestores peleando por el mismo campo es la peor experiencia que puede
tener alguien que solo quiere entrar a su correo: el sitio se rellena dos
veces, o ninguna, y no hay forma de saber cual tiene la buena. De fabrica
manda la boveda; el que prefiera el de Gecko lo prende y listo. Prohibirselo
seria decidir por el, que es lo que este fichero dice en su cabecera que atuq
no quiere ser.

Y un guardian nuevo, que corre en un segundo y sin construir nada. Una
extension esta enchufada en CUATRO lugares distintos y ninguno da error si
falta: el identificador que ella declara, la politica que la instala, el
permiso para hablarle al host, y las preferencias que la acompanan. Si falta
la politica no se instala. Si falta el permiso se instala y se conecta a
nada, en silencio, y la insignia no aparece nunca. Si faltan las preferencias
los dos gestores se pelean. Ninguno de los tres se ve como un error: se ven
como "no anda". El guardian los mira todos y trae su control negativo, que le
saca el permiso del host y exige que lo detecte.

No reemplaza al guardian de metal —servidor, navegador real, login real, el
dialogo a la vista—: lo precede, y ahorra descubrir construyendo que faltaba
un renglon en un JSON.

Los cinco verbos del otro lado ya estan en tawasuyu (bc6903f9e), con trece
tests que leen el cable.
2026-09-15 10:08:20 -04:00
SergioandClaude Opus 5 3df94f3936 SDD 31: el compilador de Rust — el escalón 2 construyendo, y los cinco muros del camino
rustc desde fuente está compilando el stage1 en el worker. El documento recoge el frente entero: el
hueco medido (12 crates y 42 555 líneas de Rust, 44 recetas `cargo-*`, CERO de rustc, y el `rustc`
que construye todo eso saliendo del LAB — que ENTRA en el ArtifactHash), los tres escalones, y lo
que costó cada uno.

Los cinco muros del escalón 2, todos con un mensaje que no nombra la causa:

1. `llvm18` no sirve: `bad LLVM version, need >=21`. Mínimos medidos en el propio bootstrap
   (1.87→18 · 1.90→19 · 1.93/1.95→20 · 1.97→21). Bajar de rustc no es salida porque el stage0 de N
   es N-1 o N. De ahí `llvm21` — y `llvm18` se queda, que lo usa mesa.
2. **El toolchain musl oficial no puede producir proc-macros** (crt-static=true ⇒ sin dylibs):
   muere compilando el propio bootstrap de rustc, que usa `clap` con derive. Sirve para compilar
   programas, no para arrancar el build de rustc. El stage0 es el del lab.
3. x.py DESCARGA el stage0 si no le nombras el local: en un sandbox sin red, `RuntimeError: failed
   verification` en `download_toolchain()`, que no menciona ni la red ni el stage0.
4. El triple de Alpine no es el canónico: sin fijarlo, la sección `[target.…-unknown-…]` no aplica a
   nada y x.py se pone a construir LLVM solo; forzando el canónico, el stage0 no tiene su std.
5. cmake + zig = «compiler broken» en la prueba de ABI. CC=gcc, igual que llvm21 y cmake.

Y del escalón 1, lo que no estaba en el grafo: el prebuilt no ejecuta sin `musl-shared` (cargador) y
sin `gcc-libs` (`libgcc_s.so.1`) — dos paquetes sellados y en el perfil equivocado. Más una
propiedad que vale: no hace falta un `cc`, el toolchain trae su propio lld.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-15 00:29:33 +00:00
SergioandClaude Opus 5 5c83e906e6 rust: la guarda del stage0 gritaba de más — \\. en un string TOML de comillas simples es literal
El configure abortó con «stage0 NO es 1.97.0: rustc 1.97.0» — o sea, la guarda rechazando
exactamente la versión que exige. La causa: en un string multilínea con comillas SIMPLES, TOML no
procesa escapes, así que `grep -q '1\\.97\\.0'` busca una barra invertida literal y nunca casa.
Va `'1[.]97[.]0'`, que no depende de cómo el formato trate las barras.

Un guardián que falla sobre lo que debería aceptar es tan inútil como uno que nunca falla — pero al
menos éste falló RUIDOSAMENTE y en el primer minuto, no tres horas después.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-15 00:21:13 +00:00
SergioandClaude Opus 5 026eb44677 rust 1.97.0 desde fuente: paso 2 — con llvm21 externo y el prebuilt como stage0
El segundo escalón de los tres: rustc construido acá, con el `llvm21` del corpus y con
`rust-toolchain-bin` (el tarball oficial sellado ayer) como **stage0**. Un prebuilt ajeno sirve
exactamente para esto: ser el escalón que permite dejar de necesitarlo.

Tres cosas que no son detalles y que van escritas en la receta:

· **`local-rebuild = true`**: el bootstrap espera que el stage0 sea el compilador ANTERIOR (o el
  beta de esta versión), y acá es 1.97.0 construyendo 1.97.0. Sin esa opción x.py aborta por
  «stage0 compiler version mismatch». Es el modo con el que las distros reconstruyen la misma
  versión con la misma versión.
· **LLVM externo ≥21**, medido en el propio bootstrap (`bad LLVM version … need >=21`). Por eso
  existe `llvm21`: `llvm18` está sellado pero lo usa mesa, que no soporta 21.
· **Sin red**: el tarball `-src` trae `vendor/` con todas las deps de cargo. Es la única forma de
  construir dentro del sandbox, que no tiene red después del fetch.

Y una guarda en el `configure`: la versión del stage0 se COMPRUEBA (`rustc --version | grep 1.97.0`)
en vez de asumirse. `local-rebuild` sólo es correcto si el compilador de arranque es exactamente
esta versión; si alguien mueve el pin de `rust-toolchain-bin`, esto tiene que fallar ahí y no tres
horas después.

`llvm21` ya selló: 1,6 G, `llvm-config --version` → 21.1.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-15 00:19:37 +00:00
SergioandClaude Opus 5 229ecff90a llvm21: el LLVM que rustc necesita — porque el 18 NO sirve, y está medido
El plan era «rustc desde fuente con el llvm18 del corpus». No se puede, y el propio bootstrap de
rustc lo dice en una línea:

    panic!("\n\nbad LLVM version: {version}, need >=21\n\n")

Mínimos medidos, versión por versión, leyendo `src/bootstrap/src/core/build_steps/llvm.rs`:

    rustc 1.87 → LLVM ≥18   ·   1.90 → ≥19   ·   1.93/1.95 → ≥20   ·   1.97 → ≥21

El lab —y con él todo lo que construimos— está en **rust 1.97.0**. Con `llvm18` sólo se podría
construir `rustc 1.87`, que además no se puede bootstrapear con lo que tenemos (el stage0 de rustc N
es N-1 o N, y nuestro binario es 1.97). La salida no es bajar de rustc: es subir de LLVM.

`llvm18` se queda donde está: lo usa `mesa-llvmpipe`, y mesa 24.0.9 no soporta LLVM 21. Dos
consumidores con rangos incompatibles ⇒ dos artefactos. No es deuda, es la realidad de los rangos.

Dos diferencias de build frente a `llvm18`, las dos a propósito: **sin DYLIB** (rustc quiere las
estáticas y su `llvm-config`; el link del dylib es lo que pide «worker GORDO» según la propia receta
de llvm18 — sin él el pico de RAM entra en los 16 G del worker) y **RTTI ON**, que rustc exige.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 22:46:55 +00:00
SergioandClaude Opus 5 41ab76c8a0 RUST COMPILA EN LA CAJA — paso 1 cerrado, y tres paquetes que estaban en el perfil equivocado
`rustc 1.97.0` y `cargo 1.97.0` responden en `2.29.29.217`, y un `fn main(){println!("ok");}`
compila y **corre**. El toolchain oficial de rust-lang, verificado por su sha256 y sellado tal cual
con `foreign = true` (son bytes ajenos, no un build nuestro: clase `ajeno`, fuera del recuento del
corpus).

Lo que costó hacerlo correr, y ninguna de las dos cosas se ve en el grafo porque las dos estaban
`sealed` y en el perfil equivocado:

1. **El cargador musl.** `rustc` es PIE dinámico (`NEEDED librustc_driver-*.so`, `NEEDED libc.so`).
   En el worker ni arranca —`cannot execute: required file not found`, que es como se ve la falta
   del intérprete—; en la caja sí, porque ahí está `musl-shared`.
2. **`libgcc_s.so.1`**: `Error loading shared library … _Unwind_Resume: symbol not found`. Lo
   publica `gcc-libs`, sellado y declarado **sólo en los cuatro perfiles de escritorio**. Una línea
   en `base`. Es la tercera vez HOY que aparece la misma figura —`tar`, `os-release`, `gcc-libs`—:
   el paquete existe, está sellado, y no viaja en la imagen que lo necesita.

Y una propiedad que vale anotar: **no hace falta un `cc`**. El primer `rustc` falla con
`linker \`cc\` not found` y no hay que traer un compilador de C — el toolchain **trae su propio
lld** (`rustlib/<target>/bin/rust-lld`), y con `-C linker-flavor=ld.lld -C link-self-contained=yes`
enlaza y el binario corre. Una distro sin compilador de C puede compilar Rust igual.

Va en `perfil.servidor` y no en `base`: son 799 M y una imagen de escritorio no compila nada.

Pendiente, que es config del SITIO y no de la receta: que esas flags sean el default
(`/etc/cargo/config.toml`), para que `cargo build` funcione sin recordarlas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 22:07:50 +00:00
SergioandClaude Opus 5 cd78bb98c9 el logo OFICIAL de takana, extraído del PNG — y rustc entra al corpus como foreign
EL LOGO. El usuario pasó el PNG oficial. El ASCII no se inventa: se MIDE. Extraído celda por celda
del original — bbox 348×348 en un lienzo de 512, cuadrícula **7×7**, celda de 49,7 px — con cuatro
colores exactos y ninguno más:

    #a63a25 ladrillo · #df6b2a naranja · #e3a63d dorado · #fff6de crema

La primera versión que puse era un martillo dibujado a ojo: era *un* martillo, no *el* logo. Cada
celda son DOS caracteres de bloque porque un carácter de terminal es alto y angosto, y `██` es lo que
conserva la geometría cuadrada del original; reconstruirlo a ojo la pierde y deja de ser el logo.

Van los dos ficheros: `logo.png` (el oficial, tal cual) y `logo.txt` (el mismo en ANSI de color
verdadero), más la config global de fastfetch con `file-raw` — con `file` los escapes se imprimirían
como texto. Y `os-release` se mueve de `cli` a **`base`**: toda imagen de takana debe saber decir qué
es, incluida la más pelada. «No sólo para esta máquina, para siempre en takana».

De paso, dos cosas que sólo se ven construyendo: `cp -a` aborta en el sandbox («failed to preserve
ownership»), va `cp -r`; y un config de fastfetch que sólo define `logo` dibuja el logo y NI UN DATO
— `modules` hay que listarlo aunque parezca redundante.

RUSTC, PASO 1. `rust-toolchain-bin` 1.97.0, el tarball oficial de rust-lang verificado por su sha256
y sellado tal cual, con `foreign = true` porque son bytes ajenos y no un build nuestro (clase
`ajeno`: no infla el recuento del corpus, ADR 0015). Target **musl**, no gnu: un toolchain gnu
traería el cargador de glibc y sería needed-colgante otra vez.

Por qué importa, y no es gusto: TODO el instrumental de takana es Rust —12 crates, 42 555 líneas— y
el `rustc` que los construye sale del LAB, un rootfs ajeno que ENTRA en el ArtifactHash. La distro
depende de bytes de otro para reconstruirse a sí misma. Y el corpus tenía **44 recetas `cargo-*` y
CERO de rustc**: subcomando-sin-driver a escala de toolchain.

Es el paso 1 de tres, y los otros dos no son opcionales: (2) rustc desde fuente con el `llvm18` que
YA está sellado (379 M), y (3) la cadena mrustc→1.91.1 que ya existe documentada en el selfhost pero
vive en el bootstrap, no en el corpus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 22:03:30 +00:00
SergioandClaude Opus 5 5d6eadd304 fastfetch: el logo sin datos — modules hace falta aunque parezca redundante
Con un config que sólo define `logo`, fastfetch dibuja el logo **y ni un solo dato**: no cae a la
lista por defecto. Medido en la caja: salía el martillo y debajo, nada. Con `modules` explícito
queda la tarjeta completa — OS takana, kernel, CPU, memoria y los cuatro discos del layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 21:48:44 +00:00
SergioandClaude Opus 5 0d0ab79092 el tar de busybox no sirve para un rootfs ajeno — y takana ya no dibuja un pingüino
`qorpa pull` de un `.tar.zst` moría imprimiendo **la ayuda de busybox** y un exit 1: ni una palabra
sobre `--zstd`, que es la opción que no entiende. El código ya elegía el descompresor por MAGIC y
pasaba las flags correctas; lo que faltaba era un `tar` de verdad.

Dos arreglos, y el segundo es de una línea:

· `untar` comprueba `tar --version` y, si no es GNU, ABORTA nombrando la causa y el arreglo. Diez
  minutos de diagnóstico se vuelven una línea.
· **`tar` (GNU) entra en `perfil.base`.** Estaba sellado desde hace meses y declarado en UN solo
  perfil (`escritorio-kde`). Es la lección de `foot` otra vez: el paquete existe y no viaja en la
  imagen que lo necesita. Ya había costado dos veces — la caja tampoco podía desempacar su propio
  LAB por lo mismo.

Y el logo: fastfetch dibujaba **el pingüino genérico de Linux** porque elige por el `ID` de
os-release y no conoce `takana`. Ahora la receta `os-release` publica también
`/usr/share/takana/logo.txt` (un martillo, que es lo que significa el nombre en quechua) y
`/etc/xdg/fastfetch/config.jsonc` — la config GLOBAL por XDG, no la de un usuario. Van en la receta
de la IDENTIDAD y no en la de fastfetch a propósito: mañana el que dibuje puede ser otro programa y
el logo seguirá siendo el mismo fichero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 21:46:08 +00:00
SergioandClaude Opus 5 93362fc21b el TLS arreglado en la caja, la distro con nombre, y el ENOSPC que no era de la raíz
**curl ya valida**: `ca-certificates` re-sellado con **119 hash-links en el capath**, generados en el
build, y la caja baja por HTTPS sin apaño (200). Tardó tres intentos y los dos primeros los cazó la
guarda de la propia receta: `c_rehash` no estaba en el PATH (lo instala esa misma receta en
`/out/usr/bin`) y los certificados no están sueltos en `usr/share/ca-certificates/` sino en
`mozilla/`, así que sólo veía el bundle y lo saltaba —correctamente— con «does not contain exactly
one certificate». Sin la guarda habría sellado un capath vacío las tres veces.

**fastfetch 2.68.1 corre en la caja** (pedido del usuario) y al correrlo destapó que
**`/etc/os-release` no existía**: la distro era anónima para cualquier programa que no fuera suyo.
Ahora `OS: takana x86_64`. Va como receta propia (`source.dir`) y no como constante de
`takana-bootstrap`: meterlo ahí re-sellaría el product-rootfs —el baseline del selfhost— por cinco
líneas. Sin VERSION_ID ni fecha: un sello con la fecha del build cambiaría el hash cada día sin
motivo, y con él la clausura de toda imagen que lo lleve.

🧨 Y el hallazgo estructural: `upgrade apply` falló dos veces con **No space left on device teniendo
3 G libres en la raíz**. No era `/` ni los inodos (9%): el estado de generaciones vive en
**`/var/lib/hammer`, que es sda3 y mide 487 MB** — la partición «estado» del layout. Cada generación
guarda una copia del árbol aplicado (272 MB el nuestro), así que dos no caben. Movido a `/work` por
symlink, igual que qorpa, y la generación 6 entró. El layout de la imagen necesita revisión: 512 MB
no alcanzan para un mecanismo que guarda un árbol por generación, y el síntoma apunta al sitio
equivocado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 21:40:45 +00:00
SergioandClaude Opus 5 64d48e743e minga abierto a P2P (la ventana oficial), fastfetch al corpus, y el rehash por su ruta
**minga con su daemon escuchando.** Decisión del usuario: minga es la ventana oficial al mundo y git
queda como espejo de compatibilidad, así que el daemon no es opcional — es el punto de entrada. El
binario ya está sellado (`b3:121cf4a8…`, 22 M, static-pie) y ahora trae su `[[user]]` (uid 970, home
`/work/minga`: en la raíz no cabe y `/work` sobrevive a un re-`dd`) y su `[[service]]`:
`listen /ip4/0.0.0.0/tcp/4001` — la convención libp2p, libre en la caja (ocupados: 22022 admin,
2345 git-SSH, 3002 gitea en loopback, 80/443 caddy). Declarar ambos **no movió el ArtifactHash**.

⚠⚠ **La identidad no se sella ni se inventa.** El keypair lo genera el usuario con `minga init`;
tawasuyu avisó de que el suyo era provisorio. Si la card hiciera un `init` automático, la caja se
inventaría su identidad soberana en el primer arranque y todo lo que se firme después colgaría de
ella. La guarda comprueba que el repo exista y sale 78: un peer sin identidad decidida es peor que
un peer ausente.

**fastfetch** (2.68.1) entra al corpus y a `perfil.cli` — lo heredan servidor y los escritorios.
Receta CMake con `-DCMAKE_LINK_DEPENDS_USE_LINKER=OFF` (sin eso el lld de zig segfaultea y el error
no nombra a zig) y con la detección opcional APAGADA explícitamente: con `link = "static"` no hay
`dlopen`, y una detección que entra «porque la librería estaba en el lab ese día» produce artefactos
distintos bajo el mismo nombre.

Y `ca-certificates`: el rehash se llamaba por nombre y **`c_rehash` lo instala esta misma receta en
`/out/usr/bin`**, no está en el PATH del sandbox. Se invoca por su ruta. La guarda que añadí ayer
hizo su trabajo: el build falló ruidosamente en vez de sellar otra vez un capath sin índice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 21:34:42 +00:00
SergioandClaude Opus 5 88decb0498 🧨 el curl de la distro no puede validar TLS: el capath no tiene hash-links
Al traer la primera imagen ajena, la caja falló con «curl failed to verify the legitimacy of the
server». El certificado del servidor estaba bien; lo que falta es de este lado:

    * CApath: /etc/ssl/certs
    * SSL certificate ... unable to get local issuer certificate (20)
    $ curl --cacert /etc/ssl/certs/ca-certificates.crt ...   → 200

El curl del corpus se compila con `--with-ca-path=/etc/ssl/certs` y SIN CAfile, y ese directorio
tiene un solo fichero: el bundle. Un capath necesita hash-links (`c_rehash`) — el ÍNDICE — y no los
hay. Los certificados están; el índice no.

Por qué faltan: la receta deja el hook `/etc/ca-certificates/update.d/certhash`, que en Alpine
ejecuta `apk` al instalar. **En takana no lo ejecuta nadie**: la distro proyecta artefactos, no corre
post-install. Todo presente, y el paso que lo conecta no existe.

Alcance: no es sólo `qorpa pull`. Es `install --repo https://…`, el mirror y cualquier curl/wget de
una caja instalada. No falla al construir ni al instalar: falla la primera vez que la máquina intenta
bajar algo. No se había visto porque el hub usa el curl de Artix.

Arreglado en la receta: los hash-links se generan EN EL BUILD (donde c_rehash y perl existen) y
viajan en el artefacto, con una guarda que falla ruidosamente si el rehash no produce ninguno.
Re-sella `ca-certificates`, raíz de `perfil.base`. Apaño mientras tanto: `CURL_CA_BUNDLE=…` (200).

Y el estado de qorpa en la caja: preflight de «BLOQUEA 1 · LIMITA 4» a «LIMITA 1» — qorpa movido a
/work (la raíz tenía 3 G), subuid provisionado y `setcap` aplicado a newuidmap/newgidmap SIN tocar el
artefacto del store (inodes distintos: en una caja instalada esos binarios son copias). El pull baja
y verifica el sha256, y muere al desempacar: el rootfs de Arch es `.tar.zst` y el `tar` de la imagen
es el de busybox. `qorpa pull` necesita `zstd -dc | tar -x` para `.zst`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 21:27:38 +00:00
SergioandClaude Opus 5 146ace3ce5 minga: repineado a ba29e20df — sin el fix de musl el build MUERE, y son ramas distintas
Aviso de tawasuyu, confirmado acá: la cadena `minga-cli → sandokan-local → arje-incarnate` es dura,
y `arje-incarnate` leía `libc::ST_RELATIME`, que **musl no exporta** (publica los otros siete `ST_*`
de statvfs, ése no). Con un commit anterior, el worker muere con:

    error[E0425]: cannot find value `ST_RELATIME` in crate `libc`

Arreglado río arriba en `ba29e20df` con el valor del ABI de Linux; allá verificaron que el cruce da
un ELF static-pie de 32 MB sin dependencias dinámicas.

⚠ Y `ba29e20df` NO contiene a `98db584f` (el de arje-zero/arjectl): son RAMAS DISTINTAS, comprobado
con `merge-base --is-ancestor`. No hay contrato roto —`arje-incarnate` es una librería de ejecución
local, no el bus de arje, así que este binario no negocia protocolo con el `arje-zero` de la caja—
pero invalida el argumento de «un solo árbol» que traía la receta: acá conviven dos ramas de
tawasuyu a propósito, y el comentario lo dice en vez de seguir mintiendo.

Segundo arreglo, medido en el worker: `-p minga-cli` a secas falla con «extra arguments to `rustc`
can only be passed to one target» — el paquete tiene lib y bin. Va `--bin minga`, el mismo muro que
ya tuvo `recipes/takana.toml`.

Hash: `b3:121cf4a8…`. De paso, `ba29e20df` es el HEAD del remoto **servido por la caja nueva**: el
push de tawasuyu ya fue al gitea mudado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 21:22:32 +00:00
Sergio 981709d241 syntax-highlighting: arreglado el no-determinismo — era el generador de Jinja
Antes: dos reconstrucciones daban `libKF6SyntaxHighlighting.so` distintos —
8.800.310 bytes diferentes desde el offset 41, y hasta el tamaño cambiaba
(10.345.888 contra 10.345.840). Ahora: REPRODUCE.

Hash: 4c1638a0… → 3db73c19… (arrastra 4 dependientes directos, 5 transitivos).

── Dónde NO estaba, que también cuenta ────────────────────────────────────────────────
No era el indexer: `katehighlightingindexer` arma el índice con `QVariantMap`, que es
QMap y va ordenado. No eran los generadores de Perl: ninguno de los cuatro itera un
hash. Mirarlo antes ahorró parchear lo que no era.

── Dónde estaba: `data/generators/generate_jinja.py`, con tres dependencias del azar ──
1. `to_do.pop()` sobre un `set` saca un elemento ARBITRARIO. En `--dry-run` el orden de
   los `print(out_file)` es lo que CMake recoge en `out_xmls`, y eso acaba siendo **el
   orden de las entradas del `.qrc`** ⇒ el recurso compilado cambiaba de disposición
   entera. Se toma el menor: mismo conjunto, orden fijo.
2. `version = str(round(time.time()))` hornea la HORA DEL BUILD en el XML generado. Se
   honra `SOURCE_DATE_EPOCH`, que es la convención de reproducible-builds y que el
   sandbox de takana ya fija (=1).
3. `os.listdir()` sin ordenar. Sólo importa si dos ficheros declaran el mismo lenguaje,
   pero quitar la dependencia del readdir no cuesta nada.

── Medido en los dos sentidos ANTES de tocar la receta ────────────────────────────────
Corriendo el generador a mano, sin builds de por medio:

  · antes  — tres PYTHONHASHSEED distintos ⇒ TRES md5 distintos, y el orden salta a la
             vista: `jinja-json, jinja-yaml, jinja-toml…` contra
             `jinja-qml, jinja-dockerfile, jinja-typescript…`
  · después — las mismas tres semillas ⇒ el MISMO md5
  · generando de verdad con semillas Y momentos distintos ⇒ los 35 XML IDÉNTICOS,
    con `version="1"`
  · control negativo — sin `SOURCE_DATE_EPOCH` sigue poniendo la hora actual
    (comprobado: coincidía con `date +%s` al segundo) ⇒ fuera del sandbox no cambia nada

Primer aviso de esta medición: mi primera comparación dio «idéntico con las tres
semillas» y era MENTIRA — el script salía con «Destination folder does not exist» y yo
comparaba md5 de un mensaje de error. Comparar salidas sin mirar que la herramienta
hiciera algo es inventarse un control.
2026-09-14 21:15:00 +00:00
SergioandClaude Opus 5 69526768b7 CUTOVER: el gitea sirve desde la caja nueva — TLS válido, HTTPS y SSH, supervisado por arje
`git.gioser.net` y `git.tawasuyu.net` responden **200 con TLS válido desde 2.29.29.217**, `git clone`
funciona por HTTPS **y** por SSH:2345, y `gitea` + `caddy` corren supervisados por `arje-zero`. El
gitea de gioser está parado. Es el primer servicio real que deja la máquina que se va a borrar.

Mudanza INCREMENTAL: los DNS de gioser son casi todos `CNAME → www`, así que mover `www` habría
mudado quince dominios cuyos backends siguen allá. Se convirtió sólo `git` (las dos zonas) de CNAME
a A propio con TTL 60 — reversible en un minuto.

Cinco cosas que sólo se aprenden haciéndolo:

· **Parar el origen no es `rc-service gitea stop`**: dice «already stopped» con el proceso vivo, y
  matarlo no alcanza — lo revive arje-zero, que lo tiene como card `openrc-gitea` con Restart y
  **9001 reinicios** en el contador. Se paró con `arjectl stop openrc-gitea` (el arjectl que
  construimos hoy, hablando con el arje de gioser), y para eso hubo que extraer su card del genesis
  y escribirla en `cards.d` — que es justo el hueco del §6.12.
· **El token de `hcloud` también gestiona el DNS** (Cloud API unificada, `/v1/zones`); la API vieja
  `dns.hetzner.com/api/v1` redirige a la consola web. Y un `PUT` sobre el rrset no puede cambiar el
  TIPO: hay que DELETE del CNAME y POST del A.
· **ACME falló primero contra gioser** (502) porque el challenge salió antes de que propagara el
  DNS. Con el DNS al día, `arjectl restart caddy` → certificate obtained successfully.
· **La identidad SSH se muda con el servicio**: `REMOTE HOST IDENTIFICATION HAS CHANGED` hasta que
  se copiaron las claves de host de gioser a la caja. Así los clones existentes no notan nada; el
  precio es limpiar el known_hosts propio del `:22` de administración.
· El `sshd` del producto escucha sólo en `:22` ⇒ el git por SSH necesitó `Port 2345` y que el
  usuario `gitea` tenga shell real (su authorized_keys fuerza `command="gitea serv …"`).

`caddy` gana su `[[service]]` (con guarda del Caddyfile, HOME propio para los certificados de ACME
—o cada reinicio pediría certificados nuevos y se comería el límite de emisión— y la nota de por qué
corre como root) y el perfil lo arranca.

Consecuencia para la granja: las 23 recetas que clonan por `https://git.tawasuyu.net/…` **ya apuntan
a la caja**. Comprobado: el worker resuelve 2.29.29.217 y `git ls-remote` responde.

Revertir, si hiciera falta: `arjectl start openrc-gitea` en gioser y los dos `git` de vuelta a CNAME.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 19:53:09 +00:00
SergioandClaude Opus 5 0f0f9bb8bf receta: minga entra al corpus — el VCS soberano que va a manejar el árbol y la cola
Decisión del usuario (2026-09-14, opción A): se muda primero con git, y minga arranca EN PARALELO.
Para que minga pueda manejar el árbol del código y la cola de compilación de cara a los agentes
—con git como espejo (`import-git`/`export-git`, que ya existen río arriba)— tiene que ser algo
que la distro construye e instala. Hasta hoy el corpus tenía **0 recetas y 0 nodos** suyos.

Los dos enganches ya están escritos en `minga/PLAN-VCS.md` §F12 y no son teóricos:

· La CI firmada habla nuestro idioma: `minga attest` declara (commit, verde/rojo, BLAKE3 del
  binario) con quórum M-de-N, y `artefacto_consensuado` sólo certifica si las máquinas coinciden
  en UN solo BLAKE3 — reproducibilidad verificada ENTRE máquinas, que acá se comprueba a mano.
· El mismo hash: arje migró su CAS a BLAKE3 para hablar con takana y minga, y el `expected_hash`
  de un `.tkn` es el mismo que `minga grant-boot` firma en la concesión que arje verifica al boot.

Del monorepo y al MISMO commit que arje-zero/arjectl: reusa el árbol ya fetcheado y evita que el
cliente y el daemon salgan de árboles distintos.

Lo que esta receta NO decide, y queda como ADR: cómo se pinea una fuente por hash de minga (hoy
`[source]` sólo sabe de commit git, sha256 de tarball y directorio) y qué es una «tarea» en la cola.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 19:28:04 +00:00
SergioandClaude Opus 5 4d77d232d3 la mudanza sin terceros: las fuentes de tawasuyu por HTTPS público, y el orden del cutover
Tres cosas, y las tres quitan vueltas.

1. NO HACE FALTA NINGUNA CREDENCIAL EN EL WORKER, Y TAMPOCO UN USUARIO NUEVO

`tawasuyu/tawasuyu` y `sergio/takana` son repos PÚBLICOS en el gitea. Las 23 recetas que apuntaban a
`gitea@git.tawasuyu.net` (SSH, que exige la clave que es el SSH de todo) pasan a
`https://git.tawasuyu.net/…`. La URL es locator y NO entra en `hash_inputs` (ADR 0013): hecho con
control antes/después, **ningún ArtifactHash se movió**. Medido en el worker: `git clone --mirror
--filter=blob:none` + `git archive` extrae el árbol (155 M) **sin una sola credencial**.

Un usuario propio en gitea sólo haría falta para un repo PRIVADO; hoy ninguna receta usa uno. Si
mañana hace falta, es una cuenta de máquina con acceso al repo que toque — nunca la clave personal.

De paso, tres recetas decían «HUB-ONLY: el worker secretless recibe Connection refused». Ya no es
cierto y el comentario decía lo contrario de lo que pasa: corregido en las tres.

2. LA COPIA FUERA LA DA LA MUDANZA, NO GITHUB (decisión del usuario)

El «paso 1: espejar los 26 repos» deja de ser el paso 1. En cuanto el gitea vive en la caja nueva,
esos repos dejan de existir sólo en la máquina que se borra — que es lo que el paso pedía. El
objetivo es dejar de pagar dos máquinas, no sumar una dependencia. `espejar-repos.sh` queda como
herramienta disponible.

3. EL §9 REESCRITO: qué está hecho, qué falta y en qué orden

Hecho y cerrado: la caja arranca takana puro · store/grafos/granja/respaldo · repo firmado · la
imagen del perfil servidor con gitea sirviendo 200 supervisado por arje · `arjectl` · y el ensayo con
los datos REALES de gioser corriendo sobre takana.

Falta, en orden: actualizar la caja a la imagen nueva (`upgrade`, no `dd`) → mudar los datos del
gitea (`.backup` + rsync, con el origen parado en el corte) → caddy con los 6 dominios vivos → DNS →
verificar desde fuera con un clone real → el resto de servicios del censo → borrar gioser con las 8
puertas en verde.

Bloqueantes con su tamaño: `git` sellado sin `remote-http` (afecta a clonar DESDE una caja takana,
no al gitea que sirve; re-sella una raíz de `base`) y la puerta 5, que no bloquea la mudanza.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 18:59:31 +00:00
SergioandClaude Opus 5 390fdf2dcb ensayo con los DATOS REALES de gioser: su gitea corriendo en takana — y el git de la distro no clona por HTTP
Sin tocar gioser (todo lectura) y en la VM desechable. Dentro de la VM: `<title>GioSer Gitea: Git
with a cup of tea</title>`, 200, `/explore/repos` listando los repos de verdad (sergio/takana,
tawasuyu/agora, card, chasqui, cosmos, khipu, llimphi…) y el clon de uno de ellos con 478 commits y
HEAD correcto.

El snapshot de la DB se saca EN CALIENTE y sale consistente: `sqlite3 gitea.db ".backup …"` con el
servidor vivo, 1,5 s para 314 M, `integrity_check` ok, 44 filas en `repository`. Copiar el fichero a
pelo mientras el servidor escribe es justo lo que no hay que hacer.

Tres detalles que sólo aparecen con los datos puestos:

· **El uid del origen no es el del destino.** Los ficheros llegan con su uid NUMÉRICO y el gitea de
  gioser es 969, no el 916 que declaraba la receta. O se chownean 2 G —lento, y hay que acordarse—
  o gitea no puede leer sus datos, y eso no falla al copiar: falla al arrancar. La receta pasa a
  969, alineada con el origen. El ArtifactHash no se mueve (`[[user]]` está fuera de hash_inputs).
· El `app.ini` de gioser escucha en `127.0.0.1:3002` porque allá caddy hace de proxy: la caja sirve,
  pero sólo desde dentro. `HTTP_ADDR`/`ROOT_URL` son adaptación, no copiado.
· 🧨 **El `git` de la distro no puede clonar por HTTP**: `git: 'remote-http' is not a git command`.
  Medido sobre el artefacto sellado: `git-core/` trae `git-remote-ext`, `git-remote-fd` y
  `git-http-backend` (el lado SERVIDOR) y no `git-remote-http`; `strings` da 0 referencias a libcurl
  pese a que `curl` está en `[deps] build`. No se había notado porque en el hub se usa el git de
  Artix, no el sellado. Un hub nuevo no podría clonar el repo por HTTPS. Re-sellar `git` es raíz de
  `perfil.base` ⇒ unidad propia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 18:26:54 +00:00
SergioandClaude Opus 5 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
2026-09-14 17:59:11 +00:00
SergioandClaude Opus 5 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
2026-09-14 16:43:29 +00:00
Sergio 44b3624de6 kirigami: arreglado el no-determinismo — eran DOS causas, y ahora reproduce bit a bit
Antes: dos reconstrucciones con las mismas entradas y el mismo lab daban artefactos
distintos. Ahora: `why-differs` da 464 entradas idénticas · 0 divergen.

Hash: 41eea38e… → a72b1626… (arrastra 37 dependientes directos, 39 transitivos).

── CAUSA 1: el AOT de QML compilaba un conjunto DISTINTO de funciones cada vez ─────────
`libKirigamiTemplates.so` cambiaba de tamaño (5.737.344 vs 5.795.072) y difería en 177
símbolos locales, todos `QmlCacheGeneratedCode…__invoke` y concentrados en CUATRO ficheros:
InlineMessage, LinkButton, NavigationTabBar y Badge.

Y esos cuatro son justo los que importan módulos QML HERMANOS del propio proyecto
(`org.kde.kirigami.platform`, `.primitives`, `.controls`). `qmlcachegen` sólo compila a AOT
lo que puede resolver de tipos, y los tipos del hermano salen de su `.qmltypes`, que produce
otro objetivo del mismo build. `src/templates/CMakeLists.txt` no declara `DEPENDENCIES`
ninguna ⇒ con ninja en paralelo es una CARRERA.

Control que sostiene el diagnóstico: `Chip` y `Heading`, del mismo directorio, no divergen —
y `kirigami-addons` y `kquickcharts`, que también llevan QML, reproducen. No es «QML es
no-determinista».

Arreglo: `-j1` en el compile. Orden topológico fijo ⇒ el cachegen ve siempre lo mismo.
NO es throttling (eso va por nice/taskset justamente para no re-hashear): es corrección, y
el re-hash es el precio que se paga a propósito.

Descartadas, con motivo: `-DQT_QML_NO_CACHEGEN=ON` apaga también el bytecode precompilado
(coste de arranque en el escritorio); `--only-bytecode` sería lo quirúrgico pero
`QT_QMLCACHEGEN_ARGUMENTS` es propiedad de objetivo y NO es INHERITED, así que no hay forma
de ponerla desde la línea de órdenes.

── CAUSA 2: el .tar.bz2 de plantillas se armaba sin orden ──────────────────────────────
`kde_package_app_templates` de ECM tiene camino reproducible —`--sort=name --mtime=@
SOURCE_DATE_EPOCH --numeric-owner --owner=0 --group=0`— pero detrás de `if(GNU_TAR_FOUND)`,
que exige que `tar --version` diga «GNU tar». El rootfs del lab es Alpine: `tar` es BUSYBOX
⇒ caía al respaldo `cmake -E tar cvfj`, que ni ordena ni normaliza. El orden lo ponía readdir.

Arreglo: `tar` (GNU tar 1.35, ya en el corpus) entra en `[deps] build`.

Comprobado mirando el artefacto, no deducido:

    drwxr-xr-x 0/0  1970-01-01 00:00 ./
    -rw-r--r-- 0/0  1970-01-01 00:00 ./CMakeLists.txt
    -rw-r--r-- 0/0  1970-01-01 00:00 ./LICENSES/BSD-3-Clause.txt

owner 0/0, mtime al epoch (SOURCE_DATE_EPOCH=1 lo fija el sandbox) y la lista ORDENADA.
2026-09-14 16:40:35 +00:00
SergioandClaude Opus 5 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
2026-09-14 15:39:52 +00:00
SergioandClaude Opus 5 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
2026-09-14 15:16:21 +00:00
SergioandClaude Opus 5 74151a2b0c gitea 1.27.0 con sqlite y bindata: el binario sellado no podía abrir la base de datos
La mudanza de gioser pasa por levantar su gitea del otro lado — 28 repos, 26 sin copia fuera. El
corpus ya tenía `gitea` sellado (1.26.4, `b3:35bb4f04…`) y NO sirve, medido contra el artefacto:

    $ gitea --config app.ini migrate
    Error: sqlite3 requires: -tags sqlite,sqlite_unlock_notify
    this Gitea binary was not built with SQLite3 support

Construía, reproducía y no podía abrir la sqlite en la que gioser guarda TODO (`DB_TYPE = sqlite3`).
Y `gitea --version` decía `version development`. Es subcomando-sin-driver un piso más abajo.

Tres cambios, y el tercero es el que no se ve venir:

· Pin 1.27.0 (gioser corre 1.27.0). No es cosmético: gitea migra el esquema hacia adelante y se
  planta si la DB trae migraciones más nuevas que el binario, así que el pin va por delante del
  origen, nunca por detrás.
· Tags `sqlite,sqlite_unlock_notify` (⇒ `cgo = true`, el driver de mattn es C) y `bindata`, que
  embebe `public/` y `templates/`: sin él la receta publica sólo el ejecutable y el servidor
  arrancaría sin UI.
· La fuente pasa de `repo`+`commit` al TARBALL DE RELEASE. El clon no alcanza: la UI se compila
  con Node y las deps Go no están vendoreadas en git. El `gitea-src-1.27.0.tar.gz` trae las dos
  cosas hechas — medido: `public/assets/` 764, `vendor/` 11246, `templates/` 663 — así que
  construye con el lab tal cual, sin Node y sin red. La URL es locator (ADR 0013); ancla el sha256.

Nuevo hash: `b3:391a613e…`. Declarado en `perfil.servidor` como paquete y NO en `servicios`:
arrancarlo necesita el [[service]] del SDD 30 con su app.ini, y declarar un arranque que no existe
sería la mentira que ese campo existe para no tener.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 14:17:52 +00:00
Sergio 5cb6e568d6 catálogo: wl-clipboard sube al corpus — cerraba el único orphan_dep de cuatro grafos
Estaba sólo en `recipes/incoming-wlr/`, o sea inalcanzable desde las otras
cuatro colas. `cliphist` vive en el CORPUS y la pide en `[deps] run`, así que
corpus, gnome, kde y cosmic la reportaban como `orphan_deps: ["wl-clipboard"]`.

No era cosmético: `escritorio-cosmic` declara `cliphist` de raíz, y la imagen
se habría armado con el demonio de historial y SIN `wl-copy`/`wl-paste` — el
modo de fallo silencioso que la propia receta de cliphist advierte (el binario
corre y no hace nada). La clausura no lo podía ver: decía `falta=0` en los
cinco perfiles, porque una dep que no resuelve no se cuenta, desaparece.

Solución = la que el catálogo ya usó con `mpv` y `atuq`: subirla al corpus, de
donde las colas la alcanzan (sibling-first y después el catálogo padre).

Medido antes de mover: ninguna de sus 9 deps tenía variante hermana en
`incoming-wlr`, así que el ArtifactHash no podía cambiar — y no cambió
(`b3:fc2ab7b7…` a los dos lados, comprobado con `takana hash`). Cero re-sellos.

Tras regenerar los cinco grafos: `orphan_deps: []` en los cinco, y la clausura
de `escritorio-cosmic` pasa de 273 a 274, sellada.
2026-09-14 13:54:28 +00:00
Sergio 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.
2026-09-14 01:51:19 +00:00
Sergio 73b288171c libical y evolution-data-server: dos rotas más del censo — y una cae enlazando una .so, no un binario
Segunda tanda del censo con `verificar-repro.sh`, esta vez sobre `poppler-glib` y las dos recetas
CMake de la cola de GNOME: **2 de 3 no construyen**. Con esto el censo va en **7 rotas de 18
barridas**, todas por el mismo crash del `lld` de zig con el `--dependency-file` de CMake ≥3.27.

⚠ **`libical` cae enlazando `libical.so` — una librería COMPARTIDA, no un ejecutable.** Eso termina
de enterrar el predictor barato que intenté ayer («falla la que instala binarios», 12 aciertos de
14): el crash puede estar en CUALQUIER link del build. Ni ejecutables instalados, ni sólo
ejecutables: cualquier link.

`evolution-data-server` muere en `camel-lock-helper` y `camel-gpg-photo-saver`, y necesita su propia
perilla además de la de su dep — arreglar `libical` no la arregla, porque el fallo está en SU link.

Las dos estaban selladas y rotas a la vez. El artefacto tapaba que la cola de GNOME ya no se puede
reconstruir entera.
2026-09-14 01:32:19 +00:00
Sergio 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.
2026-09-13 21:29:29 +00:00
Sergio bdecd1c479 mesa-llvmpipe: la campaña de glvnd, probada donde no le cuesta a nadie — y SALE QUE SÍ
mesa-llvmpipe  b3:7662f5ec25c42cd679da1679c8c6ed7c53cd49d57fa781493ab7ff433d51b9b0

En vez de lanzar 153 rebuilds a ciegas, la campaña se prueba en la única de las tres mesa que tiene
**radio 0 y cero imágenes** (`yupana radio mesa-llvmpipe`: 0 dependientes, «(ninguna declarada)»).
Cambiarla no tira un solo sellado ajeno, así que es el laboratorio exacto.

LA PREGUNTA, que hasta hoy nadie tenía contestada: con `-Dglvnd=true`, ¿mesa produce de verdad el
VENDOR que al despacho le falta? RESPUESTA MEDIDA SOBRE EL ARTEFACTO:

  usr/lib/libEGL_mesa.so.0                     ← el vendor (hoy no existe en ningún lado)
  usr/share/glvnd/egl_vendor.d/50_mesa.json    ← su registro, que es lo que glvnd busca
  usr/lib/libgbm.so.1 · libglapi.so.0 · dri/{swrast,kms_swrast}_dri.so

…y **deja de instalar libEGL.so.1 y libGLESv2.so.2** (los dos `ls` fallan).

⇒ La colisión de sonames del rootfs de KDE —dos libEGL.so.1 distintos en la misma ruta— DESAPARECE
POR CONSTRUCCIÓN con la campaña: mesa ya no reclama esas rutas y el dueño pasa a ser glvnd, que es
exactamente lo que su partición quiere. La campaña deja de ser una apuesta: se sabe qué produce.
Lo que queda por decidir es CUÁNDO pagar, no SI funciona.

EL PRECIO, medido antes de empezar y sin cambiar: `yupana radio mesa` = 148 directos, 154
transitivos, **153 sellados que caen a deuda**, las CINCO imágenes, 123 de ellos en incoming-kde —
la cola que otro agente está moliendo ahora mismo. Por eso esto es una prueba, no la campaña.

⚠ Y cubre SÓLO la mitad de mesa. La otra es que `libglvnd` vuelva a instalar su libEGL.so.1 de
despacho (hoy va `-Degl=false` justamente para no colisionar). Las dos mitades van en la MISMA
campaña o el resultado es el de hoy con otro disfraz. Queda escrito en las dos recetas.
2026-09-13 21:20:05 +00:00
Sergio 0e35652fb1 un solo NetworkManager (y KDE recupera WiFi) + upower al corpus sin arrastrar polkit
── 1. Se RETIRA incoming-kde/networkmanager.toml ───────────────────────────────────────────────
  networkmanager-qt  b3:78bbdcda00d758fb3cc3a0be7da6a67395dfbe0e4d55c5d8e5cf8afaf728ff72
  plasma-nm          b3:496a9ff7794ae5d89ac2842bdfbca38c25c91df37465a049db7cf75ee0041b82

Ayer se promovió NetworkManager al corpus CON WiFi porque la variante de KDE iba con -Dwifi=false,
y quedaron dos. Esto cierra el arco: retirada la de la cola, `networkmanager-qt` resuelve al PADRE
y la distro tiene UNO solo. Consecuencia concreta: **KDE pasa a tener WiFi**, porque su applet
hablaba con un demonio construido sin soporte inalámbrico.

Medido con `yupana radio` antes de borrar: 2 rebuilds (networkmanager-qt, plasma-nm), los dos en
la cola KDE. Y comprobado sobre el ARTEFACTO del corpus que publica lo que nm-qt pide —`libnm.pc`
y `usr/include/libnm`— antes de quitarle el suelo. Los dos sellan.

── 2. upower al corpus: COSMIC dibujaba una batería sin nadie que se la contara ────────────────
  libgudev  b3:87ca5e73…  ·  udev-pc  b3:fa880b5c…   (hash IDÉNTICO desde las dos partes ⇒ cache-hit)
  upower    b3:c82e655ff7011150f21f9413433c518f12c827c52ecf6f9fc3484f75d85c155c

Ayer escribí que esta cadena eran «3 promociones, una de ellas polkit, decisión de arquitectura».
Fui a mirar y polkit SALE de la ecuación con una perilla: la variante de KDE va `-Dpolkit=enabled`
«porque polkit YA está sellada» —cierto EN ESA COLA y falso desde el corpus—, y apagarlo cuesta
poco medido en FUNCIÓN: polkit en upower sólo gobierna las acciones privilegiadas (suspender e
hibernar por org.freedesktop.UPower, además deprecadas: hoy eso lo hace logind). Reportar batería,
carga, tiempo restante y línea de corriente NO pasa por polkit, que es justo lo que COSMIC quiere.

Se corrigió además la frase del comentario heredado que decía `-Dpolkit=enabled` al lado de un flag
que ahora dice `disabled`: una nota que desmiente al código de al lado es peor que no tener nota.
Y se le añadió su [[service]] (el corpus no lo traía; el label/id son los mismos que en GNOME a
propósito: el ULID identifica al SERVICIO, no al artefacto). No mueve el hash.

Comprobadas las NEEDED de upowerd con provee.py: libupower-glib, libffi.so.8, libz.so.1,
libudev.so.1 — las cuatro las publica el corpus y ya estaban en [deps]. Sin NEEDED colgante.

── 3. targets.toml ─────────────────────────────────────────────────────────────────────────────
  escritorio-kde     servicios += NetworkManager  (llegaba por clausura de plasma-nm y NADIE lo
                                                   arrancaba: el applet sobre un demonio apagado)
  escritorio-cosmic  paquetes  += upower · servicios += upowerd

⚠ Y se CORRIGE la nota de ayer que decía «NO declarar networkmanager en escritorio-kde porque
plasma-nm ya arrastra la variante de la cola». Ya no hay variante de cola.
2026-09-13 21:15:14 +00:00
Sergio 4b38af8f3f networkmanager al corpus CON WiFi y con nmcli — y los perfiles que no tenían gestor de red
networkmanager  b3:c36414ece23e322ef007d0acc7ce6f65b8b822ae55c38b38c9afa4b3eb340adb  (52 MB)

Arreglo del segundo hueco del barrido: `networkmanager` figuraba SÓLO en escritorio-kde, y desde
GNOME, COSMIC o sway no se alcanza (cola hermana). Pero promover la receta de KDE tal cual habría
sido peor que no hacerlo, y sólo se vio mirando el ARTEFACTO:

  · `-Dwifi=false`  ⇒ un gestor de red que no maneja interfaces inalámbricas. Allá es correcto
    (esa receta existe para dar libnm.so a plasma-nm y nada más); acá habría puesto en tres
    imágenes un demonio que arranca, dibuja el applet y no lista una sola red.
  · sin `nmcli`     ⇒ ni una herramienta con la que manejarlo. En sway eso es la diferencia entre
    tener red y no tenerla.

La variante del corpus cambia tres cosas, y la tercera hizo falta MEDIRLA:
  1. `-Dwifi=true`. Habla nl80211 por libnl (ya era dep) y delega el handshake WPA2 en
     `wpa_supplicant`, que desde hoy está en perfil.base ⇒ va también en [deps] runtime.
  2. `readline` en [deps] build.
  3. `-Dnmcli=true -Dreadline=libreadline`. Con sólo la dep, nmcli NO se construyó: `-Dnmcli` vale
     true de fábrica pero `-Dreadline` es un COMBO que por defecto va en `auto`, y sin decirle
     dónde mirar resolvió a «none» ⇒ meson apagó el CLI EN SILENCIO. Comprobado sobre el store.

Y `readline-shared` en runtime, cazado con provee.py sobre las NEEDED del binario: nmcli sale con
`NEEDED libreadline.so.8` y la readline canónica del corpus es sólo .a. La dep de BUILD y la de
RUNTIME son artefactos distintos — tercer caso del día tras cmus y bluez. No mueve el hash.

⚠ Se queda `-Dcrypto=null`, y es una limitación real escrita en la receta: sin cripto no valida
802.1X / WPA-Enterprise (el WiFi de una oficina). WPA2-PSK funciona porque ése lo hace el
supplicant. Encender nss o gnutls arrastra dos cadenas enormes: unidad de trabajo aparte.

── targets.toml ────────────────────────────────────────────────────────────────────────────────
  escritorio-gnome, escritorio-cosmic, escritorio-sway  += networkmanager  (+ servicio)
  escritorio-kde, escritorio-sway                       += wireplumber     (+ servicio)

⚠ networkmanager NO se declara en escritorio-kde: allá plasma-nm ya arrastra la variante de la
cola, y dos NetworkManager distintos en la misma ruta es la enfermedad de las dos glib. Que la de
KDE vaya sin WiFi es deuda del frente KDE, y queda escrita donde toca.
2026-09-13 01:38:50 +00:00
Sergio 72b9f54c19 recetas: libtool y libndp al corpus — promoción GRATIS, hash idéntico desde las dos partes
libtool  b3:5c09bff565b24c0a8edb306e0d4ca4e5e8e2a8c8e4a2b8ac047c8816b6883dcb
  libndp   b3:51be8f4336084b181e94a7f4a4178b87602cd39e975c2ddbd3125e7b782b9189

Entran como escalón para promover `networkmanager` al corpus (el barrido de perfiles midió que
GNOME, COSMIC y sway no alcanzaban ningún gestor de red: la receta vivía sólo en incoming-kde, que
desde ellos es una cola hermana).

La comprobación que decide una promoción NO es `diff` de las recetas sino `takana hash`, porque la
resolución de deps es hermano→padre y dos ficheros idénticos en colas distintas PUEDEN sellar
distinto. Acá sellan IGUAL desde las dos partes ⇒ misma dirección del store ⇒ las dos entraron por
CACHE-HIT, cero builds y cero rebuilds. (networkmanager, en cambio, sí difiere: resuelve varias
deps contra el corpus en vez de contra la cola de KDE.)

Las copias de `incoming-kde` se DEJAN donde están, a propósito: sellan en la misma dirección, así
que no hay dos artefactos ni colisión de nombre, y retirar ficheros de la cola que otro agente está
moliendo ahora mismo no es algo que este barrido tenga que hacer. Jubilarlas es trabajo del frente
KDE, el día que le convenga.
2026-09-13 01:37:26 +00:00