3280 Commits
Author SHA1 Message Date
Sergio 96703f4870 estado: cosecha granja 2026-09-18T21:01:44Z — avance del árbol KDE 2026-09-18 21:01:44 +00:00
SergioandClaude Opus 5 5df1f86a0a la sesión extra moría por un BYTE 0x01 donde debía ir la retroreferencia del sed
Abrir una segunda sesión fallaba con «Error: ninguna imagen empieza por "\u{1}" — traela con hammer
qorpa pull». El `\u{1}` no era un misterio: la línea que extrae el sha de la imagen tenía un `sed`
con `\1` de reemplazo, y el fichero terminó guardando el BYTE 0x01 literal en su lugar. O sea que
`qorpa create --base` recibía el carácter de control.

Se reemplaza por un `awk` que parte por comillas: no tiene retroreferencias ni nada que otra
herramienta pueda reinterpretar al escribir el fichero. Y si la extracción sale vacía, ahora corta
con un mensaje que nombra el fichero, en vez de pasarle basura a qorpa.

Comprobado: la segunda sesión se crea y arranca (claude-sergio-3, Claude 2.1.240) con la primera
todavía abierta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 20:54:42 +00:00
Sergio 9b222a6aa3 estado: cosecha granja 2026-09-18T20:31:55Z — avance del árbol KDE 2026-09-18 20:31:55 +00:00
Sergio b9e5423064 granja: el git del corpus no tiene remote-https — el espejo de GitHub no se empuja desde la jaula 2026-09-18 20:21:23 +00:00
Sergio fd065720f5 granja: el -i ~/.ssh/github5 de todos los scripts es un no-op — entra el fallback a id_ed25519 2026-09-18 20:20:37 +00:00
Sergio 317ce48c16 estado: cosecha granja 2026-09-18T20:01:41Z — avance del árbol KDE 2026-09-18 20:01:41 +00:00
SergioandClaude Opus 5 3b3056428f shuma-tui no encontraba a su agente: nadie exportaba XDG_RUNTIME_DIR
El síntoma era «no hay agente atendiendo» y después «arranqué shuma-daemon y en 10 s no atendió
ningún socket», con un daemon de sergio VIVO al lado. La causa: esta caja no tiene logind, así que
nadie crea /run/user/<uid> ni exporta XDG_RUNTIME_DIR, y entonces cada programa elige su propio
repliegue y dejan de encontrarse — el tui buscaba en `$XDG_RUNTIME_DIR/shuma.sock`, o sea en la nada,
mientras el daemon abría `/tmp/shuma-<uid>.sock`. Dos repliegues distintos para el mismo acuerdo.

Ahora /etc/profile.d exporta XDG_RUNTIME_DIR y la card de arranque crea /run/user/<uid> para cada
cuenta con home. Comprobado en una sesión de login de verdad: XDG=/run/user/1001, el tui levanta su
agente ahí y lista los conjuntos guardados (shuma 4 puestos, telefono-864, los diag…), que estaban en
disco y no se perdieron al reiniciarlo.

Entra también la card del volumen de trabajo —esta caja no tiene fstab y el init no monta discos
nuevos— y quedan versionados /etc/profile y los dos profile.d, que hasta ayer no los leía nadie
porque /etc/profile no existía.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 19:45:46 +00:00
SergioandClaude Opus 5 2f9253886f tres arreglos que pidió el agente de adentro, y uno era mío
1) `publicar-webs.sh` corría como root y hacía el `git pull` igual: git dejó 24 entradas de root
dentro del .git de /work/sergio/tawasuyu —refs, logs, config, directorios de objetos— y el dueño se
quedó sin poder ni hacer `fetch` («unable to append to .git/logs/refs/remotes/origin/main»). El clon
quedó congelado 21 commits atrás sin que nada fallara del lado de root. Ahora el pull va COMO EL
DUEÑO, y los cuatro clones quedaron con sus permisos.

2) `cc-por-zig.sh` elegía el rust con `ls -d …/*-rust | tail -1`, que es el último ALFABÉTICO:
certificaba `fe277b32…` mientras la jaula usaba el vigente `6441302d…`. Dos artefactos distintos, uno
certificado y otro en uso. Ahora pregunta por el hash vigente de la receta y sólo cae al más reciente
—con el nombre exacto— si no puede. Lo detectó el agente comparando los dos guiones.

3) `rustdoc` entra en `tools`: el artefacto salía con cargo y rustc y SIN rustdoc, o sea que en una
caja takana `cargo doc` no existe. `docs = false` se queda —la documentación de la std es otra
decisión, cientos de MB—. Radio medido: 0 dependientes de build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 19:34:08 +00:00
Sergio 67f601305b estado: cosecha granja 2026-09-18T19:31:36Z — avance del árbol KDE 2026-09-18 19:31:36 +00:00
Sergio a6cefd9b5d .gitignore: /store sin barra — igual que .dev-fs, hoy es un SYMLINK
Misma causa y mismo síntoma que el `/.dev-fs` de esta tarde: `/store/` sólo ignora un directorio, y
un clon que quiera construir necesita `store -> /store` (así lo tiene /opt/takana). El enlace
aparecía como `?? store` en cada `git status`, que es ruido permanente en un repo donde varios
agentes leen ese status para separar lo suyo de lo ajeno.
2026-09-18 19:23:41 +00:00
Sergio 1a97da9839 exp-ec: tres fallos del propio arnés — sellaba basura y el veredicto no miraba el estado de salida
Los encontró la corrida en el worker, y los tres estaban disfrazados de resultado.

1. El `exit 77` se insertaba antes de la ÚLTIMA `'''` del fichero, que no siempre cierra una fase:
   una receta puede traer bloques `'''` después (un `[[service]]`, una nota larga). En `dwarves`,
   `elfutils` y `elfutils-libdw` cayó fuera de toda fase, el build corrió entero y SELLÓ — con el
   hash movido por el `set -e`, o sea tres artefactos basura. Ahora se rastrea la anidación.

2. El veredicto se leía con `grep "exit 77"` sobre el log, y takana IMPRIME el script de la fase:
   esa cadena aparece siempre porque la inyectamos nosotros. Daba SOBREVIVE hasta a un build que
   selló. Ahora se ata a `build phase falló (exit 77)`, que es el estado de salida de verdad.
   (Comprobado a posteriori que los 14 SOBREVIVE de la corrida buena sí habían salido 77.)

3. Sin `--store`, takana usa su default `/store`, que en el worker es un store de SCRATCH casi
   vacío — reconstruía el mundo en vez de reusar el de la granja (`./store`, 908 artefactos). Ahora
   el store va por `STORE`.

Y un guardián para lo que no debería volver a pasar: si un build SELLA, se clasifica
`SELLO-INDEBIDO` y se grita el hash, en vez de contarse como un resultado más. Los tres artefactos
basura ya se borraron del `/store` de scratch del worker; el store de la granja nunca se tocó.
2026-09-18 19:19:28 +00:00
Sergio 7b09e26eca jaula: los builds SÍ corren adentro — el comentario mandaba a ssh por nada
Medido desde la jaula, con el hash forzado para que no hubiera caché: `anew`
(Go, estática) sella en 6,8 s y `lz4` (C, con `make install` escribiendo /out)
en 4,4 s. Con `root = false` el bwrap anidado se levanta y /proc monta.

Lo del uid 0 sigue siendo cierto para una receta cuyo `install` fije DUEÑOS, y
así queda escrito — pero no es «los builds no corren», que es lo que se leía y
lo que mandaba a pedirle cada receta al anfitrión por ssh.
2026-09-18 19:13:35 +00:00
Sergio 785518b908 jaula: el toolchain del corpus no arrancaba adentro — faltaba el cargador musl
Los diez envoltorios daban verde porque el control se corrió EN LA CAJA, que es
musl. Adentro de la imagen de Arch, `rustc`, `cargo` y `python3` —las tres musl
DINÁMICAS— mueren con `cannot execute: required file not found`: su PT_INTERP
pide `/lib/ld-musl-x86_64.so.1` y ahí no está. `LD_LIBRARY_PATH` no puede
taparlo, actúa después de que el cargador cargue. Ahora se invoca el cargador
del lab explícitamente, y sólo si la caja no tiene el suyo: el mismo guion sirve
en los dos sitios sin ramificar.

Segundo defecto, del mismo viaje: `zig cc` a secas no sirve de `cc` para cargo.
cc-rs le pasa su propio triple y zig no lo entiende —`unable to parse target
query 'x86_64-unknown-linux-musl'`—, lo que tumba `blake3` y con él medio árbol
de tawasuyu. Los dos drivers (el de la caja y el de la jaula) lo descartan.

Las banderas de enlace van dentro del envoltorio de `rustc` y no en
/etc/profile.d: la jaula no corre shells de login, así que allá el fichero
quedaría presente, con contenido y sin efecto.

Y los controles ahora COMPILAN en vez de preguntar `--version`, que no tocaba ni
el driver de C ni el enlazador y por eso no vio ninguno de los dos defectos.

Medido con esto puesto y sin una sola variable a mano:
`cargo check -p tejido` sobre tawasuyu, verde en 1m13s.
2026-09-18 19:03:37 +00:00
Sergio 990e3ecc6b estado: cosecha granja 2026-09-18T19:02:00Z — avance del árbol KDE 2026-09-18 19:02:00 +00:00
Sergio 0f46c0976b atuq: un .pyc invisible movía el hash — el hub sellaba algo que un clon limpio no reproduce
Buscando si los cambios de atuq habían llegado a la caja apareció esto: el hub y el worker
calculaban `b3:e556024b` y un clon limpio `b3:5156cb8f`, con los 52 ficheros versionados de
`recipes/atuq/` byte a byte IGUALES en las tres máquinas y la receta con el mismo md5.

La diferencia era un fichero de más: `tools/__pycache__/rebrand.cpython-314.pyc`, bytecode que dejó
alguien al correr `rebrand.py` dentro del repo. Comprobado en los dos sentidos — metiendo y sacando
ese .pyc en un clon limpio, el hash salta de `5156cb8f` a `e556024b` y vuelve.

Lo traicionero es que `__pycache__/` está en `.gitignore`: el árbol se ve limpio en `git status`
mientras el hash ya es otro. Y el artefacto sellado en el store era el de `e556024b` — o sea uno que
un clon limpio no puede reproducir jamás. Es la regla 3 al revés: no un vacío que pasa por lleno,
sino basura invisible que pasa por fuente.

`ArtifactHash::of_tree` no sabe de `.gitignore`, y son dos cosas legítimamente distintas; pero hoy
divergen por descuido y no por decisión. Queda escrito en la receta, con la comprobación barata al
lado, para las cuatro que usan `source.dir`. El `.pyc` se barrió del hub y del worker: las tres
máquinas ya convergen en `5156cb8f`.
2026-09-18 18:52:31 +00:00
Sergio 3609b50220 exp-ec: REC apuntable a una copia — en el worker el rsync --delete borra las copias a mitad
La siembra de código hub→worker de `cosecha-cron` corre con `--delete` cada 30 minutos. Las copias
del experimento viven en `recipes/` por obligación (takana resuelve las deps relativas al directorio
de la receta), así que una tanda larga en el worker las vería desaparecer a mitad. Con `REC` se
apunta a una copia entera de `recipes/` fuera del árbol sembrado y el problema no existe.
2026-09-18 18:46:37 +00:00
Sergio 065fc4e481 exp-ec: portable al worker — el lock relativo, takana por ruta y el techo por entorno
Tres cosas mías que sólo se vieron al correrlo allá. El lock estaba cableado a `/work/.farm-build.lock`,
que es la ruta de la CAJA (donde `work` es symlink a /work); en el worker `work` es un directorio de
verdad y flock moría con `cannot open lock file`. `takana` no está en el PATH del worker, que lo
invoca por `./target/release/takana`. Y el techo de 420 s alcanza para las ligeras pero no para llvm,
firefox ni los kernels, así que ahora es `TLIMITE`.

Todas por entorno y con el valor de antes por defecto, así que la corrida de la caja no cambia.
2026-09-18 18:46:22 +00:00
Sergio 76a0f8c399 estado: cosecha granja 2026-09-18T18:31:50Z — avance del árbol KDE 2026-09-18 18:31:50 +00:00
Sergio b3500a83eb .gitignore: /.dev-fs sin barra — hoy es un SYMLINK y la barra no lo tapaba
`/.dev-fs/` sólo ignora un directorio. Desde que el lab vive fuera del árbol y se engancha con un
enlace (`.dev-fs -> /work/dev-fs`, que es como está en /opt/takana y como lo necesita cualquier clon
que quiera construir), el enlace aparecía como `?? .dev-fs` en cada `git status`. Ruido permanente
en un repo donde varios agentes leen ese status para ver qué es suyo y qué es de otro.
2026-09-18 18:17:57 +00:00
Sergio 27ec9caea0 ADR 0020: el experimento CORRIDO — 10 de 12 sobreviven a -ec, y el que cae tiene el artefacto BIEN
El diseño es la parte que vale: NO SELLA NADA. Modificar una receta le cambia el ArtifactHash, así
que medirlo a lo bruto habría dejado una docena de duplicados basura en un store que se respalda. En
vez de eso se inyecta `set -e` en cada fase Y un `exit 77` al final de la última: el build siempre
falla, nunca sella, y el veredicto se lee en el código de salida — 77 significa que todas las fases
corrieron enteras bajo set -e. Queda en scripts/farm/exp-ec.sh.

Muestra de 12 recetas ligeras (los pesos pesados quedan fuera por tiempo, y el sesgo va declarado):
10 sobreviven sin tocar nada, 1 se rompe, 1 inconcluso por timeout.

El que se rompe es `e2fsprogs`, y mirarlo de cerca cambia lo que significa: su ./configure aborta con
`external uuid library not found` pese a que la receta pasa --disable-libuuid, y hoy eso se traga
—comprobado: la misma receta sin set -e y con exit 77 LLEGA al 77—. Pero el artefacto que sella está
bien: 149 ficheros con e2fsck, mke2fs, los cuatro fsck.ext* y los mkfs.ext*. O sea que ahí `-ec` no
atrapa un bug: rompe una receta que funciona. Ése es el coste, y es el número que no se podía
adivinar leyendo: ~8%, unas 8 recetas sobre las 96 expuestas.

También se corrige el recuento: 96, no 89. El 89 salía de restar 142−53 dando por hecho que las 53
con `set -e` propio estaban todas dentro de las 142, y no lo estaban.

jaula-preparar suma lo que hizo falta para poder correr todo esto desde adentro: rsync/python3/jq/
cargo enlazados del store, y el cargador de musl, sin el cual python3 y cargo no arrancan en una
imagen glibc.
2026-09-18 18:17:42 +00:00
Sergio 6ce34e97d1 estado: cosecha granja 2026-09-18T18:01:52Z — avance del árbol KDE 2026-09-18 18:01:52 +00:00
SergioandClaude Opus 5 afdc4ffb9b el toolchain de la jaula sale del CORPUS, no de pacman
El agente de adentro reportó que le faltan cargo, rustc, make, pkg-config y python3. La salida obvia
—sumarlos a `packages` y `qorpa provision`— hoy NO funciona: con un mapa de un solo id, pacman no
puede chownear su directorio de descarga y muere en «failed to chown temporary download directory»,
que es exactamente la consecuencia que el aviso de qorpa viene anunciando.

Y no hace falta: las diez herramientas YA están selladas en el store, son musl estáticas y corren
dentro de la imagen glibc. Usarlas es además lo coherente con la distro —el corpus es el que provee,
no Arch—. Quedan como envoltorios en /work/<usuario>/bin, que el lanzador pone en el PATH junto con
el go y el zig del lab.

Dos errores míos que el propio control destapó y quedan escritos: el glob `/store/*-make` también
casa con `…-cargo-make` (eligió ése y `make` moría en un `exec` que no nombraba la causa) — ahora el
nombre se compara entero quitándole el hash; y el control daba ✓ a todo porque `cmd | head` devuelve
el estado de head, o sea siempre 0.

Probadas las diez: make 4.4.1, pkgconf 2.5.1, python3 3.12.10, jq 1.8.1, rsync 3.5.0, git 2.54.0,
curl 8.20.0, takana 0.0.1, rustc y cargo 1.97.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 17:58:58 +00:00
SergioandClaude Opus 5 b54b97f8cc el PATH del lab entra en la jaula: go y zig, que ningún doc nombraba
Lo reportó el agente de adentro y muerde en el primer build: `takana build` invoca `go` y `zig` POR
NOMBRE, y en la imagen de la jaula no están. Muere con «spawn go mod vendor: No such file or
directory», un mensaje que no menciona el PATH y manda a buscar el problema donde no está. Los dos
viven en /work/dev-fs/tools, que ya estaba concedido — sólo faltaba nombrarlos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 17:56:08 +00:00
Sergio 944eb1befe estado-granja: comprobar el LATIDO por sus commits, no el crontab por su presencia
El bloque decía «⚠ NO instalado en el crontab» y era cierto y ruidoso a la vez: desde que el latido
lo sostiene el cron de root del ANFITRIÓN, un agente enjaulado no ve ese crontab ni lo verá nunca,
así que el aviso saltaba en TODAS las corridas desde la jaula. Un aviso que salta siempre es un
aviso que nadie lee, y tapaba el caso real —que el latido se pare— con ruido permanente.

Ahora se comprueba el efecto: el último commit de cosecha y su edad. Dispara en :00 y :30, así que
más de 40 minutos sin commitear es un latido perdido y se avisa; si no, dice «✓ al día» con la edad
exacta. El crontab se sigue mirando, pero sólo para SUMAR información cuando está, nunca para
avisar cuando falta.

Medido desde la jaula: «último ciclo: hace 18m 54s — estado: cosecha granja 2026-09-18T17:31:27Z».
2026-09-18 17:50:29 +00:00
Sergio 88f3977e2c jaula-preparar: enlazar takana/patch/gh/hcloud del store al PATH de adentro
Cuarto defecto de la misma familia que los tres que el script ya repone, encontrado construyendo
DESDE la jaula: los binarios que el agente necesita no están en la imagen de Arch, pero sí sellados
en `/store`, que la instancia ya tiene concedido. Son musl estáticos y corren dentro de una imagen
glibc, así que no hay que instalar nada — sólo enlazarlos.

El que costó encontrarlo es `patch`: takana aplica los parches FUERA del sandbox, o sea que lo busca
en el PATH de la jaula, no en el del lab. `wtype` (sin parches) construía y `zsh` (con seis) moría en
`spawn patch: No such file or directory`. El `patch` del lab no sirve de nada acá: es musl DINÁMICO
y no arranca en una imagen glibc.

Los wrappers ya viven en /work/<usuario>/bin, que es bind del anfitrión y sobrevive al recreate; lo
único que se pierde es el enlace en el PATH.
2026-09-18 17:45:11 +00:00
Sergio 08b475ec17 ADR 0020: el corpus ya se defiende solo en 53 de las 142 — el radio de explosión son 89, no 142
Medido al construir `wtype` en la caja: su fase de install empieza con `set -e` puesto a mano por el
autor. No es la única — 53 de las 142 recetas con fase multi-comando hacen lo mismo.

Son dos datos. Uno: nadie escribe `set -e` en 53 recetas por gusto, se escribe después de que algo
selle mal, así que el default ya estaba pagándose de a una. Dos: esas 53 ya corren bajo `set -e` y
no romperían con `-ec`, o sea que la migración es sobre las 89 restantes y la muestra del
experimento sale de ahí, no de las 142.
2026-09-18 17:40:05 +00:00
Sergio cd8469e09e estado: cosecha granja 2026-09-18T17:31:27Z — avance del árbol KDE 2026-09-18 17:31:27 +00:00
SergioandClaude Opus 5 95093ac827 varias sesiones a la vez: si la instancia está ocupada, se abre la siguiente
El kernel niega dos overlays sobre el mismo upper y eso no se discute. Pero «un solo Claude» era
empaquetado nuestro: instancias distintas tienen upper distintos y conviven. Ahora el lanzador busca
una libre y, si hace falta, la crea.

La instancia nueva NO se aprovisiona: se CLONA la capa ya hecha. Con un mapa de un solo id —lo que
hay hoy— pacman no puede chownear su descarga y el provision muere en «failed to chown temporary
download directory», que es exactamente la consecuencia que el aviso de qorpa predice. Copiar el
upper cuesta ~1 s y 169 M y no depende de eso; cuando el mapeo por rango funcione, `provision` vuelve
a ser el camino.

De paso quedó medido por qué el provision fallaba antes incluso con root = true: la IMAGEN es de
root y el mapa tiene un solo id, así que adentro sus ficheros aparecen sin mapear y no se pueden
escribir. Con la imagen pasada a la persona, el mkdir de /run/user/0 pasa y se llega hasta pacman.

Probado con la primera sesión viva (1h39m): la segunda abre y contesta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 17:22:26 +00:00
SergioandClaude Opus 5 2bbfd80d93 «una sola sesión» es del kernel; «un solo Claude» era empaquetado nuestro
El usuario chocó con Resource busy al abrir un segundo agente y lo llamó por su nombre: «no quiero
restricciones arbitrarias». Conviene separar las tres cosas, porque desde afuera se sienten igual.

Del kernel: dos overlays sobre el mismo upper los niega overlayfs, no nosotros. Eso no se toca.
Nuestro: una instancia por persona, de ahí «un solo Claude» — y instancias distintas conviven; crear
la segunda cuesta 0 s y 16 K porque la imagen se comparte de sólo lectura. Accidentes, ya
corregidos: el directorio de instancias era root-only (una persona no podía crear la suya), ~/.ssh
iba ro, /store iba ro (el agente no podía sellar), claude abría /opt/takana siempre y /usr/bin/claude
era una copia congelada.

Y queda un defecto real que hoy bloquea la segunda instancia: aprovisionarla muere con «Can't mkdir
parents for /run/user/0». Medido: esta caja no tiene /run/user —no hay logind— y qorpa arma esa ruta
sin alternativa; XDG_RUNTIME_DIR no alcanza porque lo honra otra rama. Es arreglo en el crate.

La distinción que importa: la jaula restringe al agente, no a la persona —sergio tiene sudo y manda
en la caja—, y dentro de la jaula el alcance es declarativo. Lo indefendible no es que haya límites:
es que un límite accidental parezca de diseño.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 17:11:36 +00:00
Sergio d90dda284f estado: cosecha granja 2026-09-18T17:01:33Z — avance del árbol KDE 2026-09-18 17:01:33 +00:00
Sergio 71f5783e2a estado: cosecha granja 2026-09-18T16:31:38Z — avance del árbol KDE 2026-09-18 16:31:38 +00:00
Sergio 48a9aff129 ADR 0020: las fases corren con sh -c sin -e, y un fallo no final sella igual
Sale de construir `zsh`. El agujero es estrictamente el de los comandos NO FINALES de una fase: el
estado de salida de `sh -c` es el del último comando, así que `make … install install.info` podía
morir con `makeinfo: not found` y el artefacto sellaba con `/usr/share/info` VACÍO adentro.

Medido, no supuesto: 144 logs del worker, 88 sellados, 4 con fallo de alta señal. Clasificados uno
por uno da UN hueco real confirmado (zsh, ya reparado en 3b36e9df), uno que NO lo es (python3: ahí
takana SÍ abortó, porque el `make` era el último comando de su fase) y uno inconcluso (strace, en
logs multi-receta donde el target que falla no se puede atribuir).

Ese caso de python corrige mi propio encuadre inicial, que decía «las fases sellan con huecos
silenciosos» a secas y era demasiado amplio. Queda escrito en el ADR porque la versión amplia manda
a buscar el bug donde no está.

Exposición medida: de 943 recetas, 142 tienen fase multi-comando escrita a mano. Las ~800 restantes
NO están expuestas — usan las fases de `resolve_phases`, que encadena con `&&` y sí propaga.

La decisión (`-ec` o no) queda ABIERTA a propósito: falta el único número que importa, que es
cuántas de esas 142 dejan de sellar bajo `-ec`, y eso no se saca leyendo sino reconstruyendo.
Cambiar el default a ciegas sobre 1115 sellados es el reflejo que este repo evita.
2026-09-18 16:31:33 +00:00
Sergio 3b36e9df0a zsh: dejar de pedir install.info — fallaba siempre y sellaba un /usr/share/info VACÍO
`makeinfo` no está en el lab, así que `make … install install.info` moría con
`/bin/sh: makeinfo: not found` ⇒ `Error 2` en TODOS los builds. Y el artefacto sellaba igual: las
fases corren con `/bin/sh -c` sin `-e` (`takana-build/src/sandbox.rs:332,491`), o sea que un fallo
a mitad de fase no aborta — sólo cuenta el estado del ÚLTIMO comando, que acá era el bucle de
guardianes y daba 0.

Lo que llegaba al store era un zsh por lo demás correcto (estático, con sus módulos, con `=~`) pero
con un `/usr/share/info` vacío adentro. Es la regla 3 de CLAUDE.md a escala de fase en vez de
artefacto: un ausente falla ruidosamente, un vacío llega hasta el final diciendo que todo fue bien.

Lo que salvó al artefacto no fue el estado de salida sino los dos guardianes que la receta ya traía
escritos a mano. Una receta sin guardianes propios no tiene esa red — eso es el hallazgo general, y
va aparte.
2026-09-18 16:27:01 +00:00
SergioandClaude Opus 5 c7c2337559 el informe del agente de adentro: los cinco defectos y las tres trampas
Primer informe hecho DESDE la jaula, y vale más que cualquier inspección desde el anfitrión porque
son las cosas con las que se choca al usarla. Queda registrado qué se arregló y —lo importante—
DÓNDE vive cada arreglo: 1 y 2 en el upper, que es caché descartable, y por eso hay un guion que los
repone tras cada recreate; 3 y 5 en el manifiesto; 4 en el worker.

Del 4: se autorizó la clave de la caja, no la de root — credencial distinta y revocable. Del 5:
takana NO se compila adentro; el binario es el musl del store, y lo que fallaba es que el
target/release del anfitrión enlaza a /usr/bin/takana, que adentro es el de Arch.

Y las tres trampas de orientación que el agente reportó: el store real es /store y los runbooks dicen
./store porque están escritos para el hub; hay dos checkouts y no son intercambiables; y el .fleet
ausente no es defecto, lo repone la cosecha.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:06:58 +00:00
SergioandClaude Opus 5 e5e723b670 lo que el agente arregló DENTRO de la jaula, escrito afuera para que sobreviva al recreate
El agente que trabaja enjaulado encontró y arregló tres defectos a mano, pero viven en el `upper`,
que por el D3 del ADR 0015 es caché descartable: al primer `recreate` se pierden. Esto es esa mano,
escrita.

1. No hay entrada en /etc/passwd para el uid de adentro: ssh y git mueren con «No user exists for uid
   1001» y ni intentan conectar. Pasa porque la instancia de persona va con root = false —si no, el
   CLI se ve uid 0 y rechaza --dangerously-skip-permissions— y ese uid no existe en la imagen Arch.
2. /etc/ssh/ssh_config.d es de la capa inferior y su dueño cae fuera del mapa (nobody), así que ssh
   ABORTA con «Bad owner or permissions»; no degrada, aborta. El directorio no se puede mover (EXDEV
   en overlay), así que se redirige el Include a una copia propia.
3. known_hosts se siembra desde el anfitrión, que es lo que evita el StrictHostKeyChecking contra el
   worker la primera vez —y el ~/.ssh de adentro ya pasó a rw en el manifiesto, para que ssh pueda
   escribirlo cuando aparezca un host nuevo—.

Escribe en el upper como la PERSONA, no como root: si no, le dejaría ficheros que ella no puede tocar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:06:21 +00:00
Sergio c4d8b68692 estado: cosecha granja 2026-09-18T16:01:37Z — avance del árbol KDE 2026-09-18 16:01:37 +00:00
SergioandClaude Opus 5 4be4680310 atuq: la bóveda ENTREGA una contraseña, y sólo cuando alguien dice que sí — las seis etapas en verde
A ✓ sin la app dueña, el host contesta locked:true — y con ok:true, que es la trampa
    B ✓ con la app, vault.status dice ABIERTA: el socket sube en 1-2 s
    C ✓ el diálogo ABRE en el compositor y se lo CONTESTA: «1» ⇒ yes, «2» ⇒ no
    D ✓ vault.save con consentimiento real guarda, y vault.match la encuentra SIN contraseña
    E ✓ el navegador real, sobre una página servida por HTTP: la contraseña llega al CAMPO
    F ✓ contestando que NO, al campo no llega NADA

Dos corridas seguidas en verde, 6 min cada una. La unidad 12 del SDD queda CERRADA.

La E es la promesa entera del §6 y sola no probaría nada —una bóveda que entrega siempre se ve
idéntica a una que entrega con permiso—; por eso la F corre lo mismo diciendo que no. Y lo que se
mide no es lo que el host contesta sino lo que hay EN EL CAMPO: la página delata al servidor lo que
le pongan. Es la lección del §7.quater aplicada al otro extremo.

Tres fallos del ARNÉS, los tres disfrazados de fallo del producto:
· `wait` a secas esperaba también a la app de la bóveda ⇒ la jaula se comía su timeout y a la app la
  mataba un KILL, y con ella lo recién guardado (sled no alcanzaba a volcar). La etapa siguiente no
  encontraba la credencial: «la bóveda no guarda», el diagnóstico contrario al verdadero;
· el diálogo NO responde al teclado mientras la app de la bóveda inicializa su GPU — dos llimphi
  arrancando a la vez sobre render por software se pisan: doce teclas sin efecto y `denied` por
  timeout con la ventana abierta. Ahora se espera a que la app PINTE (12-13 s), no a su socket;
· una sola tecla no alcanza y el borde se mueve: el contestador insiste hasta que la ventana se va,
  que es la única señal que no depende de adivinar cuánto tarda.

El guardián entra ENTERO a la suite (era `--hasta A`): 20 en verde, ~38 min.

Lo que costó llegar: el guardián se escribió para medir una función y destapó tres piezas rotas,
NINGUNA en atuq — el dueño y el diálogo no estaban en el corpus, ninguna ventana llimphi podía pintar
sin Vulkan, y el dueño atendía de a un cliente. Las tres se veían igual desde el navegador: una
bóveda que no ofrece nada, sin un solo error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:57:20 +00:00
SergioandClaude Opus 5 057332363a /usr/bin/claude era una COPIA del guion, por eso el arreglo no llegaba
El usuario reportó dos veces que claude seguía abriendo en /opt/takana con el arreglo ya pusheado y
ya tirado en la caja. La causa: `claude` en el PATH no era este fichero sino una COPIA suya en
/usr/bin, hecha en la instalación y congelada con el destino viejo cableado. Se vio en su propio
error anterior —«/usr/bin/claude: line 23»—, que apuntaba a un fichero distinto del que yo editaba.

Ahora /usr/bin/claude es un ENLACE a este guion, así que no puede volver a derivar, y queda dicho en
la cabecera. Comprobado por traza: desde /work/sergio/takana pasa /work/sergio/takana y desde
/home/sergio pasa /home/sergio. Barrido: no hay otras copias instaladas de scripts/servidor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:41:37 +00:00
SergioandClaude Opus 5 5999aeda45 borré /usr/bin/id probando el setuid — qué pasó y por qué no se prueba así
Para saber si el bit setuid elevaba copié busybox sobre /usr/bin/id (busybox elige el applet por
argv[0] y necesitaba un nombre válido) y después borré «mis» copias de prueba. Una era el `id` del
sistema, y no quedaba otro: /bin/id tampoco existía. El usuario se lo encontró de frente: «claude:
line 23: id: not found» y «-bash: /usr/bin/id: No such file or directory».

Restaurado como sus hermanos de identidad —whoami y groups apuntan a ../../bin/busybox— y comprobado
con id de root, de sergio y con el `id -un` que usa el lanzador. Barrido: 0 enlaces colgados en
/bin, /usr/bin, /sbin y /usr/sbin.

La lección es de método: para probar una propiedad del sistema no se usa un nombre que el sistema ya
ocupa. La sonda correcta fue la que terminé escribiendo —cinco líneas de C a un nombre inventado—.
Y es la misma familia que el /etc/shadow del §6.51: la caja no avisa cuando le falta algo esencial,
se entera el que lo usa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:39:14 +00:00
SergioandClaude Opus 5 9db1845f22 claude abre DONDE ESTÁS PARADO, no en /opt/takana
Reportado tal cual: «entro al repo y ejecuto claude, pero claude sigue estando en /opt/takana y no me
agarra el repo». Era literal: el lanzador tenía el destino cableado a /opt/takana, así que desde
/work/sergio/takana abría el árbol del root — y encima con la memoria de ESE proyecto, porque la
memoria del agente se indexa por ruta de trabajo.

Ahora el defecto es $PWD; PROY sigue ganando cuando se lo pasa a propósito, y si la ruta no existe
cae a /opt/takana. Probado con un doble del binario en los tres casos.

Y el `cd` de adentro ahora falla ruidosamente: la jaula sólo monta lo concedido, así que desde una
ruta no concedida no hay nada que abrir. Antes eso se lo tragaba un `&&` y el agente arrancaba en `/`
sin decir por qué.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:33:54 +00:00
Sergio e04089b222 estado: cosecha granja 2026-09-18T15:31:57Z — avance del árbol KDE 2026-09-18 15:31:58 +00:00
SergioandClaude Opus 5 f32f2f3543 el mapeo por rango: se intentó cerrar y NO alcanzó — queda dicho dónde para
Se hicieron las dos cosas obvias: rango para sergio en /etc/subuid y /etc/subgid, y los ayudantes
newuidmap/newgidmap en setuid root y además con capacidades (cap_setuid=ep). El error SE MOVIÓ —ya no
dice «no hay rango para sergio»— pero sigue sin mapear: «newuidmap: open of uid_map failed:
Permission denied».

Lo descartado, medido: el setuid SÍ eleva en esta caja (sonda en C compilada con el cc nuevo:
uid=1001 euid=0), / no está nosuid, y el uid_map del proceso es suyo (-rw-r--r-- sergio:sergio). O
sea que no es ni el rango que faltaba ni el bit que no tomaba. La causa de fondo queda SIN
DETERMINAR, y se escribe así en vez de inventarla.

De paso, un detalle que cuesta un rato si no se sabe: `chown` BORRA el bit setuid, así que hay que
chownear primero y ponerlo después. La primera sonda salió sin bit por ese orden y parecía que el
setuid no funcionaba en la caja.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:25:54 +00:00
SergioandClaude Opus 5 ff871f4a07 dar de alta una persona: casi es adduser, y lo que falta es lo que tumbó la caja
Respuesta medida a «¿agregar un usuario es más complicado que un simple adduser?». `adduser` está —es
el de busybox— y crea la cuenta. Lo que no alcanza es DÓNDE queda la contraseña: esta caja NO USA
/etc/shadow, el hash vive en el campo 2 de /etc/passwd, y el shadow que hubo está aparcado como
/etc/shadow.el-que-bloqueo-root. El nombre no es broma: crear un /etc/shadow con las cuentas
bloqueadas dejó a sshd sin aceptar ni la clave de root, y la caja se quedó sin administración con los
ocho dominios caídos ~20 minutos. Un adduser a secas puede crear ese fichero sin avisar.

Así que el guion crea la cuenta, deja el hash donde el login lo lee, y APARTA cualquier /etc/shadow
que el alta haya creado donde no había. El control es el que importa: fotografía quién podía entrar
ANTES y falla si alguien perdió su contraseña.

Y hace lo que hace falta para tener poder de verdad: sudoers.d (el sudo de acá sí es setuid; doas no
funciona), rango en /etc/sub{u,g}id con los ayudantes newuidmap/newgidmap en setuid —sin eso un
userns anidado no mapea uid 0 y takana build no corre dentro de la jaula—, ssh, y la instancia
claude-<u> con `root = false`, porque si no el CLI se ve uid 0 y rechaza --dangerously-skip-permissions.

Probado de punta a punta con un usuario desechable y borrado después sin dejar residuo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:20:55 +00:00
SergioandClaude Opus 5 3dc4424597 la jaula que te mapea a root rompe al propio agente
`claude --dangerously-skip-permissions` moría en la caja con «cannot be used with root/sudo
privileges»: la instancia de sergio traía `root = true`, que mapea el uid del invocante a 0 ADENTRO
(qorpa.rs:1533), así que el CLI se veía a sí mismo como root. Con `root = false` queda uid 1001 y la
bandera se acepta — comprobado: `claude --dangerously-skip-permissions --version` contesta.

De paso, a esa instancia le faltaba `/store` en `rw` —la de root ya lo tenía—: sin eso el agente lee
el store y no puede sellar nada. Ahora escribe (probado y limpiado).

Y se corrige el aviso sobre los builds, que ahora falla por otro motivo: con root=false el bwrap
anidado SÍ se levanta y /proc monta, pero no puede mapear el uid 0 que takana build pide, porque
sergio no tiene rango en /etc/subuid y newuidmap/newgidmap no son setuid en esta caja. Cerrarlo es
una decisión con precio (un setuid más, y el /usr/bin de acá está hidratado del store), no un
trámite; mientras tanto el build se le pide al anfitrión por ssh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:03:17 +00:00
Sergio f485f0a187 estado: cosecha granja 2026-09-18T15:02:07Z — avance del árbol KDE 2026-09-18 15:02:07 +00:00
SergioandClaude Opus 5 984ebdf58f la subpágina de humanoid se publica con rsync, y queda dicho por qué
El resto de los sitios se publican con `git pull` porque Caddy sirve de los clones. Con humanoid no
se puede: su repo es el proyecto Android y lo que se sirve son APK ya construidos más su índice —
salida de release que no vive en ningún repo, y que para regenerarse pide Android SDK + Java, que la
caja no tiene. Hasta que la caja construya Android, esto es un puente declarado, no el modelo.

`--checksum` a propósito: los APK se reconstruyen con el mismo tamaño y otra fecha, y un rsync por
fecha subiría 91 M cada vez; medido hoy, lo que de verdad cambió fueron tres ficheros (index.html,
en/index.html y rizoma-launcher.apk) y los otros nueve sólo tenían la marca de tiempo distinta. Y no
lleva --delete: borrar a ciegas en lo que se está sirviendo no es publicar, es perder.

El control pregunta al SITIO, no al rsync —que "salió bien" y una página vieja conviven sin ruido— y
comprueba además un APK, porque el índice puede estar bien y la descarga rota: es lo que la gente se
lleva.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 14:57:48 +00:00
SergioandClaude Opus 5 bd8f9fbbb2 atuq: la bóveda ANDA hasta la etapa D — y con el navegador abierto quedaba muda por un tercer fallo
Con el arreglo de llimghi sellado, el guardián de metal pasó de la A a la D:

    A ✓ sin la app dueña, el host contesta locked:true (y con ok:true, que es la trampa)
    B ✓ con la app, vault.status dice ABIERTA — el socket sube en 1 s
    C ✓ el diálogo ABRE en el compositor y se lo CONTESTA: «1» ⇒ yes, «2» ⇒ no (los dos sentidos)
    D ✓ vault.save con consentimiento real guarda, y vault.match la encuentra SIN contraseña
    E ✗ el navegador pide la contraseña y no llega nunca

La E destapó un fallo que no es de atuq ni de llimphi: el dueño de la bóveda atendía de a UN cliente
—`atender_cliente` no vuelve hasta que el cliente se va, y se lo llamaba en el hilo del accept—, así
que la primera conexión se quedaba con él mientras viviera. Y el caso normal es ése: Gecko lanza un
`puriy-costura` por PUERTO, ocho en atuq, y todos conectan al arrancar. La extensión de la bóveda
mandaba `vault.match` y no volvía nunca: sin insignia, sin log y sin error, indistinguible de «este
sitio no tiene contraseñas».

Control sin navegador, en los dos sentidos: un solo cliente contesta en milisegundos; con otro host
conectado y quieto, la misma pregunta queda colgada y la mata el timeout a los 30 s.

Arreglado en tawasuyu (`cf3540460`, un hilo por conexión; los diálogos los serializa ahora el Mutex
de la bóveda) con su test de regresión, que además cazó que el arreglo obvio —clonar el Dueno— borra
el socket en el Drop del primer hilo que termina. Los siete tests que ya había no podían ver el fallo:
abrían un cliente por vez, que es justo lo que producción nunca hace.

⚠ Y subir el pin volvió a chocar con el `Cargo.lock` abierto de tawasuyu. Lo que importa para la
próxima es CUÁL operación lo cierra: `cargo metadata` sin `--locked` da **1 línea** de diff y cero
checksums movidos; `cargo generate-lockfile` da 6.305 líneas y 750 checksums, e invalidaría el
vendoreo de todos. Publicado en `b80f7567c` con índice temporal (estaba MM).

Del lado del guardián, tres cosas que salieron de fallar:
· el contestador INSISTE hasta que la ventana se va — con el mismo `wtype rc=0`, una corrida
  contestaba y otra volvía `denied`: la ventana entra al árbol del compositor antes de que llimphi
  tenga el teclado enganchado, y un sleep más largo sólo mueve el borde;
· el contestador busca la ventana POR PATRÓN: con el navegador abierto, teclear «la enfocada» le
  contestaría a la página, y eso es un sí que nadie dio;
· la sonda del chrome mira `isShownForTab` y la insignia ANTES de apretar — `triggerClickOrPopup`
  sale por la puerta de atrás si la acción no está mostrada, y un click que no llega se ve igual que
  una bóveda que no contesta. (Y `WebExtensionPolicy` no es global en el scope de una ventana: sale
  del módulo. Eso costó una corrida.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 14:52:50 +00:00
SergioandClaude Opus 5 4b52acae06 la batuta: los tres repos al lado y los sitios publicándose solos
terapeuta y los .ec quedan decididos —respaldar, no mudar, y los dominios vencieron hace tiempo—, lo
que explica el NXDOMAIN del §10 y lo saca de los bloqueos.

El gitea de la caja YA tenía sergio/takana.git al lado de sergio/tawasuyu.git; lo que faltaba era el
árbol de trabajo. Los tres clones quedan en /work/sergio de sergio:sergio y los tres EMPUJAN: se le
generó clave en la caja y se registró en su cuenta de gitea por la API con un token de `gitea admin`.

Y los sitios pasan a servirse de los clones, así que publicar es `git pull`. El contenido estaba al
día —se comparó lo servido con lo que generan hoy los repos y coincidía byte a byte—; lo roto era el
modelo: cuando el repo cambiara, la web no se iba a enterar, y eso no se nota porque un sitio viejo
responde 200 igual que uno nuevo.

Lo de humanoid es distinto: su repo es el proyecto Android y lo que se sirve son APK ya construidos,
salida de release que no vive en ningún repo. Refrescar esa página no es publicar, es compilar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 14:52:12 +00:00
SergioandClaude Opus 5 69a93a9d0d publicar los sitios pasa a ser git pull: Caddy sirve de los clones, no de copias
La mudanza dejó los sitios como copias a mano en /work/www —4,2 G del monorepo sólo para
tawasuyu.net— sin ninguna forma de refrescarlas. El contenido estaba bien y el modelo era frágil: el
día que cambia el repo la web no se entera, y eso no se nota, porque un sitio viejo responde 200
igual que uno nuevo.

Ahora Caddy sirve directo de los clones: tawasuyu.net del monorepo y gioser.net/takana/hifas del
clon de gioser-web. Los medios que NO están en git —los APK de humanoid, los 265 M de muestras,
vivo, ende— siguen en /work/www con su propio `root`, que es la separación correcta. Liberados 4,2 G.

El guion hace `pull --ff-only` (no pisa cambios locales de un árbol que está sirviendo), regenera las
dos páginas hermanas —hermanas.py trae el destino cableado a una ruta de gioser, así que se le pasa
el del clon sin tocar el fichero de ese repo— y después COMPRUEBA: que lo servido sea byte a byte lo
que hay en disco, que los medios sigan en 200, y que /.git/config dé 404, porque servir desde un clon
no puede exponer el repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 14:49:04 +00:00
Sergio 4be104c8fa estado: cosecha granja 2026-09-18T14:32:20Z — avance del árbol KDE 2026-09-18 14:32:20 +00:00