Es lo que el plan dejó sin medir tres veces («no se midió qué applets usa el
producto de verdad; eso abarataría el paso 1»). El modo lee del TSV los applets
con proveedor_medido = SOLO-BUSYBOX y los busca en el repo con las reglas
anti-ruido que el vigía ya tenía.
12 invocados: sh (28 sitios, lo cierra simi), ifconfig/route/udhcpc/netstat
(todos con ip/ss como primera opción y el applet como respaldo), setuidgid (3
cards: gitea, pacha-secretos, tejido), getty (la card de la consola), adduser,
nc, nslookup, poweroff. Y uno que es falso positivo del grep y conviene tener
escrito: los `insmod` de iso-image.sh/install-image.sh son de GRUB —`insmod
part_gpt` dentro de un grub.cfg—, no del busybox.
Los otros 52 no los llama nadie EN EL REPO, y la salida lo dice sin dejar que se
lea como una lista de borrado: un grep ve a quien invoca por nombre, y hay dos
consumidores que no aparecen nunca — el KERNEL (llama a /sbin/modprobe por
call_usermodehelper y al gestor de /dev por hotplug, así que modprobe/lsmod/
depmod/rmmod/modinfo/mdev/uevent/makedevs salen «libres» y no lo son) y el
USUARIO de la máquina, que espera clear/reset/vi/traceroute al entrar por ssh.
La primera versión del modo decía 10 y estaba mal en dos casos, los dos del lado
peligroso — dan permiso para retirar algo que se usa:
· setuidgid se invoca como `exec /usr/bin/setuidgid pacha`, con RUTA ABSOLUTA,
y el patrón de comando pide el nombre pegado al exec.
· getty vive como `"argv": ["getty", …]`, elemento de un array JSON, y la regla
que filtra cadenas por «parece una línea de comandos» —la que bajó el ruido
de 91 hits a 0— descarta un nombre suelto.
Las reglas que quitan ruido son las que crean ceguera; lo que decide es hacia
dónde se equivoca cada una. Ahora busca tres formas: nombre desnudo, ruta
absoluta y primer elemento de argv. El vigía sigue verde en su modo original
(123 applets retirados, nadie los invoca) y se salta su propio fichero, que
nombra applets en la documentación y se denunciaba a sí mismo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cuatro artefactos sellados y el paso 2 del plan cerrado. Los diez applets que
dejan de depender de busybox salieron de arreglar recetas, no de escribir código:
xz unxz xzcat lzma unlzma lzcat → recipes/xz-tools.toml (nueva, b3:194c7d69…)
vi → un symlink en recipes/vim.toml
cpio → un symlink en recipes/libarchive.toml
arch hostname → los trajo el feature de uutils (b3:614ff653…)
xz-tools es el TERCER caso de la familia de zstd-cli, y eso ya estaba escrito en
targets.toml: «la receta canónica construye sólo lib/ y su artefacto no tiene
usr/bin; declararla no habría arreglado nada». Igual que musl-shared, zlib-shared
y libffi-shared. Cuatro veces la misma forma — la receta publica la lib, la
imagen declara el paquete, y el binario no está. Va como variante porque ampliar
la canónica re-hashearía a sus 14 consumidores.
⚠ xz-tools selló DINÁMICO en la primera corrida pese al link = "static", y
funcionaba: comprimía y descomprimía sin una queja. Es el relink de libtool, que
libarchive.toml y bluez.toml ya documentan — hace falta LDFLAGS=-all-static en
compile Y en install. Lo delató el `file` del binario, no una prueba que fallara.
Paso 2 del plan: uutils, findutils, findutils-xargs, diffutils, gzip, grep y
xz-tools declarados en el perfil `base` — seis recetas que llevaban meses
`sealed` con `perfiles: []`. Hasta hoy, `find`, `xargs`, `diff`, `cmp`, `gzip`,
`zcat` y `grep` en una imagen de takana eran el applet de busybox, no porque
faltara escribirlos sino porque nadie los declaró. `grep` entra como PUENTE
declarado: ripgrep ya viaja pero publica `rg` y no es grep POSIX.
El censo gana modo --guardian, y vigila PÉRDIDAS, no un umbral: un umbral hay
que subirlo cada vez que se retira un applet y a la tercera nadie lo sube con
criterio; que un applet con proveedor medido deje de tenerlo es siempre una
regresión. Probado en los dos sentidos — rc=0 contra /store, rc=1 contra un store
mutilado, nombrando applet y proveedor perdido.
Los cuatro sellaron con el mismo hash en el store tirable y en /store: dos
work_root distintos, bytes idénticos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El DNS de `takana-os.net` y `www` quedó apuntado, así que Caddy sacó el
certificado de Let's Encrypt (vence el 2026-12-20) y el sitio responde 200. Un
detalle que costó dos minutos y no estaba escrito: **el vhost se había puesto
ANTES del DNS**, y Caddy no reintenta al instante — se queda en su backoff y el
nombre sigue mudo aunque el registro ya esté. `arjectl restart caddy` fuerza el
reintento; anotado en el README del sitio.
Dos nombres más, y los dos SÓLO redirigen:
takana.gioser.net → 301 a takana-os.net (la página vieja, la de hermanas.py)
git.takana-os.net → 301 a git.gioser.net (⏳ le falta el registro A)
`git.takana-os.net` no sirve gitea y no es por pereza: gitea tiene UN `ROOT_URL`
con el que arma cada enlace absoluto y cada URL de clon que muestra la UI.
Colgarlo de un segundo nombre daría un sitio que se contradice —entrás por uno y
te ofrece clonar del otro— y moverlo de verdad rompe los clones y el CI que ya
apuntan al viejo. `git clone` sigue el 301 sin chistar.
⚠ `publicar-webs.sh` comparaba `takana.gioser.net` contra el fichero en disco, y
eso ya no aplica: ahora es un 301. Se separó el control en dos —`comprobar` por
sha contra el disco, `redirige` por destino— y `takana-os.net` entra en el
primero. Lo que NO entra es el `pull`: su raíz es el árbol de trabajo que los
agentes editan, y tirar de ahí mueve HEAD bajo los pies de otro (CLAUDE.md §2 ter).
⚠ Y la primera versión de `redirige()` preguntaba por `getent hosts` quién
resuelve: **en esta caja `getent` NO EXISTE** —musl/busybox no lo traen— así que
daba 127 para todo y declaraba «no resuelve todavía» hasta de los nombres que
estaban andando. Un control que nunca mira nada se ve igual que uno en verde; es
la lección del reaper otra vez. Ahora se lo pregunta a curl, por su código 6.
Corrido entero: 4 verdes por contenido, 1 por destino, 1 avisando del A que falta.
La página suma los dos verbos que ya se pueden correr contra el repo remoto sin
construir nada (`repo list` y `repo verify`), que es el commit de al lado.
El plan de botar busybox decía de sí mismo que el reparto applet→dueño era
«juicio, no medida». Medido contra los 1407 artefactos del store: de los 277
applets vivos, 167 los provee el dueño que el TSV nombra, 31 los provee OTRO
paquete, y 79 no los provee NADIE salvo busybox — 56 más de los 23 previstos.
El hallazgo transversal es que una receta de LIBRERÍA no es un paquete de
HERRAMIENTAS, y build-state no distingue: `xz` figura en SEIS perfiles con el
`usr/bin` VACÍO (su receta pasa --disable-xz --disable-xzdec --disable-scripts,
o sea liblzma y nada más), y `ncurses` instala sólo install.libs/install.includes
⇒ ni clear, ni reset, ni tput. Las dos viajan, las dos dicen `sealed`, y el único
binario que hay adentro de la imagen es el applet de busybox.
Y al revés: `switch_root` y `sulogin` los trae util-linux, `partprobe` lo trae
parted, y `getty` tiene sustituto — `agetty` de util-linux, en los 7 perfiles ⇒
deja de ser ESCRIBIR y pasa a ser migrar el exec de la card. Seis de los 18
huecos de iproute2 (ipaddr iplink ipneigh iproute iprule iptunnel) no son
comandos de nadie: son nombres internos de busybox y se borran del defconfig.
uutils traía 78 applets, no los 90 que el plan le atribuía: el multicall
contestaba `coreutils: unknown program 'chmod'`. Construía con las default
features (feat_common_core) y el resto vive detrás de feat_os_unix_musl, que
cubre exactamente los 25 que faltaban. Construido y verificado contra un store
tirable: 79 → 106 applets, 27 nuevos y CERO perdidos, chmod/uname/id ejecutan, y
`stdbuf` correctamente ausente (por eso la variante _musl y no feat_os_unix:
upstream lo excluye en musl porque necesita un cdylib).
Importaba más de lo que parece: USERLAND_COMPONENTS hidrata uutils DESPUÉS de
busybox para ensombrecerlo, y lo que no existe no ensombrece nada — chmod, chown,
id, uname y stat los seguía dando busybox con el userland Rust instalado encima.
⚠ Dicho en la receta y en el documento: who/users/uptime/pinky compilan contra
los stubs de utmpx de musl y contestan vacío. No es regresión, tampoco es
sustitución.
El censo queda como script (scripts/busybox-censo-proveedores.py) y como dos
columnas del TSV, no como una tabla escrita a mano que vuelva a envejecer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
«simi» es boca/lengua en quechua. Es el sh que el frente C necesitaba: busybox
está en la raíz de confianza del arranque (STAGE1_COMPONENTS, ATTEST_PATHS) y en
el exec de cada card, y el candidato ajeno que se midió —brush 0.4.0— pesa 6,9 MB,
arranca 4x más lento que el ash y trae dos divergencias medidas (#1394, #1396).
Alcance MEDIDO, no supuesto: las 23 cards con shell (8 builtins, 13
construcciones), los 21 /init empotrados de las imágenes (+ if, funciones, case,
aquí-documentos) y el POSIX de takana-live-install.sh.
Resultado del banco diferencial, 159 casos contra el ash de busybox 1.36.1
(10 fixtures de regresión, 107 de tortura de comillas, 23 fragmentos de cards,
19 /init empotrados):
simi DIVERGE=0 bash DIVERGE=8 brush DIVERGE=35
Incluye el bucle REAL de config.status que genera cualquier configure de
autotools — el que tumbó a brush. 499 KB release, 1,4x la latencia del ash
(brush: 4,1x). 47 pruebas propias en verde.
Seis bugs que el banco destapó, y ninguno se veía sin un control al lado:
1. El lexer fundía lo entrecomillado con lo desnudo, así que x="a b" se ejecutaba
como ORDEN y el error decía «no encontrado» — mandando a buscar al lugar
equivocado. Ahora el comillado es por carácter.
2. Usaba la barra invertida como marca interna y la quitaba al final, así que una
barra PRODUCIDA por una sustitución se perdía. La quita de comillas es sobre
las comillas de la palabra, nunca sobre el resultado de una expansión: hace
falta una máscara de protección, no un escape dentro del texto.
3. set -e no se suspendía dentro del cuerpo de una función llamada desde una
condición. Ahora es un contador dinámico, que entra solo donde corresponde.
4. Los delimitadores de la sustitución de orden y de ${ } no respetaban la barra
invertida: un paréntesis escapado cerraba la sustitución.
5. Las clases entre corchetes no entendían la barra invertida — y la del
config.status de libtool es literalmente una clase con cuatro escapes.
6. Dentro de comillas dobles el cuerpo de un backtick sigue entre comillas, así
que la barra también se elimina ante la comilla doble.
Y uno que el banco NO vio y sí vio la prueba de regresión: el trap EXIT HEREDADO
se disparaba en el subshell. POSIX §2.12 manda resetearlo — es la mitad
complementaria del #1396 de brush, y al revés. Los dos oráculos hacen falta; el
caso quedó agregado al fixture 08.
Cron horario de root → /usr/local/bin/avisar-actualizaciones, un envoltorio que sólo fija las rutas
de la caja y llama al guion del repo. La lógica no se copia: comprobado en vivo que arreglar el
guion en el clon cambia la salida del cron sin volver a desplegar nada.
⚠ La primera corrida real destapó un defecto del aviso: con la DB de instalados vacía decía «al
día». Es verdad y no dice nada — peor, se lee como «comprobado y correcto» cuando lo cierto es que
no había NADA que comprobar. Un aviso que suena igual cuando vigila que cuando no vigila nada es
cómo se deja de mirar un aviso. Ahora distingue el caso y lo nombra.
Control en la caja con una DB de juguete (un zsh a otro hash): grita «1 con novedad · hash
distinto» y en la segunda corrida calla.
Y dos piezas que faltaban allá: /var/lib/hammer/trust/release.ed25519.pub (el almacén de confianza
por defecto de la CLI, que no existía ⇒ ahora install/outdated verifican sin pasar --trust) y
/usr/local/bin/takana con el arreglo del §5.5. Ojo: el PATH de la caja no incluye /usr/local/bin,
así que un `takana` a secas sigue siendo el del store; el cron usa rutas absolutas.
Pregunta distinta a la de las cards, y el doc lo dice: scripts/ corre en el hub
bajo el bash del anfitrión, así que su superficie sólo obliga el día del
auto-alojamiento. Lo que importa hoy es si algún guion se declara POSIX y no lo
es, porque ése se rompe en la imagen, donde /bin/sh es el ash de busybox.
- 81 POSIX (10.167 líneas), 76 bash (10.269), 21 EMPOTRADOS por heredoc (los
/init que viajan a la imagen, y que hoy nadie parsea), 6 que se hacen source
- los 81 POSIX y los 21 empotrados los parsea el ash: CERO roto
- 48 de los 76 bash parsean tal cual bajo ash; de los 28 que no, 26 lo son por
arrays y nada más. Ése es el presupuesto entero de un sh propio para correr la
herramienta del proyecto
- 27 builtins, 257 órdenes por nombre desnudo y 35 por ruta absoluta; 0 applets
retirados, o sea que este inventario y busybox-vigia.py ahora coinciden
El escáner se extrajo a scripts/lib/sh_analisis.py para no tener dos copias que
divergan (ADR 0019 un piso más abajo). Comprobado que la extracción no cambió la
salida de sh-superficie-cards.py, byte a byte, y que el orden de los empates es
determinista para que sea diffable.
Van anotados los SIETE bugs del instrumento, que es la parte que enseña: los
comentarios sin quitar (89 backticks de prosa en cosecha-cron.sh), shlex
desincronizándose sobre el fichero entero, los cuerpos de heredoc (uno era
Python y aportaba la orden 'p' — sacarlos bajó las externas de 680 a 257), los
patrones de case dando 'init', los cuerpos de $(( )) dando 'i+1', los
descriptores dando '2', y la ruta absoluta confundida con applet. Ninguno se
veía en la salida: los siete daban números plausibles.
`takana install zsh --repo https://repo.gioser.net --trust ./trust --require-signed` ⇒
`release: trusted (by release)`, 1331 ficheros en 0,286 s, zsh 5.9 corriendo. 173 paquetes
anclados, 0 sin ancla, certificado CN=repo.gioser.net de Let's Encrypt.
Tres cosas que sólo se supieron al poder entrar como root, y que el guion ahora sabe:
· `caddy reload` NO funciona acá: el Caddyfile lleva `admin off` y el reload habla por ese admin
(de ahí que el :2019 estuviera siempre cerrado). La recarga real es `arjectl restart caddy` —
caddy es un Ente de arje con Restart{1000,30000}, no un proceso suelto.
· Un restart levanta los 19 dominios o ninguno, así que el guion saca un TESTIGO de la config
previa (acá gitea.gioser.net), espera a que responda, y si no vuelve restaura y revierte.
Comprobado después: los 8 dominios vivos siguen en pie.
· El certificado tarda más que la comprobación: el guion dio «todavía no responde» y la misma URL
daba 200 segundos después. El mensaje ahora manda reintentar antes de ir a leer logs.
Y dos fallos míos, medidos: backticks dentro de comillas y de un heredoc sin entrecomillar se
EJECUTAN (salió un `journalctl: not found` en mitad de un mensaje de ayuda), y `[ -n "$X" ] && echo`
como última orden mata el guion entero bajo `set -e`.
El espejo provisional sin firmar de takana.gioser.net/repo se borró: con el firmado en pie, un
origen sin firma no es un origen.
Tercer bug de la misma card, de la misma familia y el de peor diente: con un
directorio llamado «juan perez» en /home, el $u desnudo se partía en dos
campos, id -u juan perez fallaba, y el || continue saltaba a ese usuario EN
SILENCIO — se quedaba sin su /run/user/<uid>.
- u=$(basename "$h"), chown "$u", "/run/user/$uid"
- id -u izado a uid=$(id -u "$u" 2>/dev/null) || continue: se llamaba cuatro
veces por usuario (una para el test y tres para las órdenes), ahora una. No
era el bug, era la forma limpia de entrecomillarlo — y se ve en la medición,
que pasó de id×4 a id×1
- los dos echo de error, ahora entre comillas. Inocuo hoy (ningún glob en el
texto) y se comprobó que el mensaje y el rc=78 salen idénticos
Medido con el mismo banco de stubs sobre un /home de prueba con «juan perez»,
root y nadie: antes recibía chown sólo root, ahora root y «juan perez». «nadie»
no recibe en ninguno, que es lo correcto — no está en passwd. Parsea en
busybox, bash y brush.
Los dos los encontró scripts/sh-superficie-cards.py al inventariar las órdenes
externas de las cards. La card nació así en su único commit (3b305642): no fue
un accidente de edición posterior.
- awk NR==2{print } → awk 'NR==2{print $4}'
Sin comillas, awk daba error de sintaxis SIEMPRE y el echo final imprimía
«montar-trabajo: /vvv libres», sin el número, en cada arranque.
- grep -q /vvv /proc/mounts → grep -q " /vvv " /proc/mounts (y lo mismo
para /work/sergio)
El patrón desnudo matchea cualquier línea que CONTENGA la subcadena, así que
la comprobación «¿ya está montado?» podía dar verdadero por un /vvv/algo y
saltarse el mount en silencio. Latente: hace falta otra línea con esa
subcadena para que muerda.
Probado corriendo el fragmento entero con /proc/mounts sustituido por un
fichero de prueba y mkdir/chmod/chown/mount/blkid stubbeados — nada mutó la
máquina. Antes: en el escenario del falso positivo NO montaba /vvv. Después:
monta, y el echo trae el número. Parsea en busybox, bash y brush.
Contesta qué tendría que saber hacer un /bin/sh propio para que el producto
arranque, contado sobre el árbol y no sobre POSIX.
- 33 cards en cuatro poblaciones (recetas, bootstrap, servidor, mudanza); 23
ejecutan shell, 10 son exec directo
- el idioma es uno y se repite 18 veces: test … || { echo >&2; exit 78; }; exec …
- NO aparece nunca: if, funciones, ${…}, posicionales, $?, here-docs, subshells,
segundo plano — ni comillas invertidas, que es lo que tumbó a brush en el sandbox
- 8 builtins y 14 órdenes externas; cruzadas con busybox-applets.tsv, CERO
retiradas por el recorte 401 → 277: es el guardián que el plan pedía, y ahora
corre en un segundo
- las 23 parsean idéntico en busybox, bash y brush (banco diferencial, --files)
Hallazgo que hacía falta para medir bien: en una Card el argv va SIN argv[0] —
arje lo pone desde exec (lo dice la card de hammerd). Sin eso, el idioma de la
mudanza (exec=/bin/sh, argv=[-c, …]) desaparecía de la cuenta.
La detección respeta comillas con un escáner de estados: la primera versión
contaba 11 backticks que eran prosa dentro de mensajes de error.
`work_root` sale de `dirname(store)/work`, o sea `/work` con el store en `/store`, y ahí `sources`
y `out` son ENLACES a `/work/sergio/work/…`, que es del usuario. Publicar como root dejaría
directorios de root en su árbol y su siguiente build moriría con un permiso denegado que no
menciona la causa. No es hipotético: root ya congeló el clon de tawasuyu del dueño dejándole 24
entradas suyas dentro del `.git`, y del lado de root no falló nada (ver publicar-webs.sh).
Si corre como root y nadie fijó TAKANA_WORK, lo manda a /var/tmp/takana-publicar-work y lo dice.
Comprobado que el override no mueve el hash: zsh sella en b3:0be3630d… con y sin él.
Y el mensaje final deja de decir «falta el vhost» cuando lo acaba de poner: si se instaló y aun así
no responde, lo que hay que mirar es el certificado, el DNS y el repo, en ese orden.
brush#1396 abierto. Antes de escribirlo apareció que reubeno/brush ya marcaba
el fallo como known_failure en trap.yaml:226 (y :269, :323 para las otras dos
variantes), sin issue que lo siguiera.
Queda anotado porque es método, no anécdota: un known_failure en la suite del
candidato es evidencia de primera y cuesta un gh api mirarla. Buscar en las
PRUEBAS del candidato, no sólo en sus issues, antes de decir «hallazgo nuevo».
Lo que el reporte sí aporta sobre esos TODOs: que las tres variantes son
probablemente un mismo bug (on_exit() sólo lo llaman los puntos de entrada de
nivel superior; el subshell de interp.rs:662-683 clona el shell y devuelve el
exit code sin pasar por ahí), que también pega en pipeline y en segundo plano,
y que falla en silencio con rc intacto.
Dos cosas que el ensayo del guion destapó, las dos medidas:
1. `repo-perfil.sh` NO acotaba el build. `pack --build` es instantáneo con cache-hit y una
compilación entera si el artefacto no está sellado, así que «publicá el repo» se convertía en
silencio en «ponete a compilar el perfil»: la corrida se puso a moler `libnftnl` en una caja de
4 cores que ya estaba a load 46, y después venía `rust`. `build-repo.sh` llevaba `timeout` desde
siempre; el que se corre en producción, no. Ahora BUILD_TIMEOUT (120 s) y SKIP_UNSEALED=1, con
el vencimiento tratado como «no estaba sellado» y no como error. Resultado del perfil entero:
173 anclados, 0 sin ancla, 2 saltados (os-release, rust), sin compilar nada.
2. `--install-vhost` pone el bloque de Caddy sin que haya que editar a mano, pero el orden es lo
único que lo vuelve seguro: copia → añade → `caddy validate` → SÓLO ENTONCES `reload`. Si
validate o reload fallan, restaura la copia y recarga con ella. Un reload con la config rota no
tira el sitio nuevo: tira los 19 de la caja.
Probado en los dos caminos contra un Caddy de juguete (el de la caja no tiene el admin abierto):
con reload bueno, el admin muestra el server nuevo sirviendo el repo y el sitio previo intacto; con
reload fallido, el Caddyfile vuelve byte a byte y no queda rastro del bloque.
Y `repo-perfil.sh` pasa a respetar TAKANA del entorno: la guarda de publicar-repo.sh comprobaba un
binario y se publicaba con otro, que es peor que no tener guarda.
El banco POSIX de 57 casos lo pasa brush 56/57 y aun así no construye una
receta autotools: mide lo que a uno se le ocurrió preguntar. Este pregunta si
el candidato hace LO MISMO que el control sobre el material que hay.
- compara (rc, stdout, árbol de ficheros producido); stderr es aviso, no
divergencia — el texto de error diverge legítimamente
- el árbol es la columna que faltaba: el caso 10 diverge SÓLO ahí, que es la
clase de bug del libtool mal escapado (rc=0, stdout idéntico)
- toda divergencia se confirma re-corriendo control y candidato: separa caso
no determinista de shell inestable de divergencia real
- fuentes: fixtures de regresión, generador de tortura de comillas, las 1130
fases del corpus y configure/libtool reales en sh -n
Medido, 1273 casos en 5,5 s: brush 35 duras, bash 8, y CERO en los 1156 casos
de parseo. El generador saca el #1394 entero sin conocerlo. Hallazgo nuevo:
brush no ejecuta nunca el trap EXIT de un subshell — y eso golpea el frente
del PRODUCTO, donde el veredicto del paso 3 decía que no había evidencia en
contra.
El nombre está dado de alta en la zona de Hetzner con la convención de la zona (A, ttl 60, la misma
IP que takana/git/gitea/www). Resuelve — comprobado contra Cloudflare; el resolutor de Google aún
servía el NXDOMAIN cacheado, que es negativo de 1 h por el `minimum` del SOA y no un fallo del alta.
⚠ La API vieja de DNS de Hetzner (dns.hetzner.com/api/v1) ya no es la buena y devuelve HTML de la
consola web, que es lo peor que puede devolver: no falla, contesta. Las zonas están hoy en la API de
cloud y las lee el MISMO token de ~/.config/hcloud/cli.toml (gioser.net = zona 986350).
Hoy el nombre resuelve y no sirve nada: falta el vhost, que es root. `publicar-repo.sh` hace la
parte publicable —repo firmado en /srv/repo + verificación contra trust/— y para imprimiendo el
bloque de Caddy en vez de editarlo: esta caja sirve 19 dominios y su Caddyfile ya lleva 20 `.bak`.
Y aborta antes de publicar si el takana de la caja es anterior al arreglo del SDD 28 §5.5 (se
detecta porque no conoce `outdated`): con uno viejo el repo sale con las 16 recetas multi-parche
irreproducibles y 8 paquetes prometiendo un binario que no existe.
Corre `takana outdated --json` contra el índice firmado y deja el resultado en un fichero de
estado. No actualiza nada: instalar es reproducir y verificar, y eso no se cuelga de un cron — el
aviso dice qué comando correr.
Tres cosas deliberadas:
· Sólo grita cuando la LISTA de novedades difiere de la del ciclo anterior. Un guardián que repite
lo mismo cada 30 min se deja de leer, y entonces no avisa de nada.
· La huella es nombre+versión publicada, no el JSON entero: éste trae contadores que se mueven
solos y harían «cambiar» un estado idéntico.
· Un repo caído NO se resume en «al día»: lo dice, sale ≠ 0 y conserva el último estado bueno.
Probado en los tres caminos contra un repo servido por HTTP en la caja.
Paso 1 de docs/plan-botar-busybox.md. 124 applets fuera del defconfig: los 105 que
no se reemplazan (dpkg, rpm, httpd, telnetd, ubi*, nand*, i2c*) y 19 cuya función ya
hace otro componente (init/runit -> arje-zero, syslogd/klogd/logread -> hammerd).
Binario de 1.230.976 a 977.096 bytes.
CORRIGE UN ERROR DEL PLAN: arje-zero y hammerd reemplazan la FUNCIÓN, no el COMANDO
-- son un binario cada uno. Por eso 5 indultos: setuidgid (la card de gitea lo
ejecuta), switch_root (el /init del instalador, antes de que arje exista) y
poweroff/halt/reboot (nada más en el corpus apaga la máquina).
scripts/busybox-vigia.py falla si alguien invoca un applet retirado; lee la lista de
la propia receta para que no haya dos que se desincronicen. Su primera versión daba
91 hits y ninguno real: las cuatro reglas que lo bajaron a 0 están documentadas.
El mapa applet->símbolo sale de las líneas //applet: del fuente, no de poner el
nombre en mayúsculas: eso falla en 6 casos medidos, dos de ellos en la lista.
Verificado: 277 applets exactos, los 5 indultados presentes, y zlib-ng, jq y
logrotate reconstruidas y selladas con el busybox recortado -- el sandbox sigue en
pie. Son 3 de las 24 dependientes, no las 24, y el boot de la imagen no se probó.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Medido en el worker, con el único cambio de /bin/sh en una copia por hardlinks del
lab (sed/grep/awk siguen siendo busybox) y stores tirables. Control busybox: 3/3
recetas selladas. brush: 0/3.
Lo que pasa: 56/57 del banco POSIX (igual que busybox), 0 rechazos sobre las 1130
fases de shell del corpus, parsea el configure de ffmpeg, funciona como argv[0]=sh
y con el patrón de las cards de arje.
Lo que falla: el configure corre entero pero el libtool que GENERA sale con un
nivel de escapado de menos en 25 líneas, y ese fichero ya no lo parsea ningún
shell. El parser de brush está bien — parsea sin quejarse el libtool del control.
Mecanismo exacto no determinado: echo, sustitución de comando y el propio
func_quote_for_eval de libtool dan idéntico en ambos shells aislados.
Y un dato independiente del bug: 813us de arranque contra 91us de busybox (8,9x),
sobre decenas de miles de shells por configure. El binario son 7,25 MB contra los
1,23 MB del busybox con sus 401 applets.
De paso: brush selló con el mismo hash que traía build-state del otro hub.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El §7.undecies dejó la bóveda declarada y a NADIE capaz de abrirla: ninguna
imagen traía un binario que sembrara `pacha_llavero::SEED_IDENTIDAD`. Esta es
esa unidad.
De las dos formas posibles entra `agora-cli`, y el motivo no es que sea mejor:
el wizard `churay-welcome-llimphi` SÍ tiene binario (medido: `src/main.rs` sin
`[[bin]]`, o sea que cargo lo descubre), pero decide además backend de IA,
dotfiles, fondo de pantalla y chasqui — la experiencia de primer arranque
entera, que no se decide dentro de una unidad del navegador.
⚠ Y antes de poder declararlo apareció lo que lo volvía imposible: sin
`AGORA_PASSPHRASE`, `Sesion::abrir()` caía en la frase de desarrollo
"agora-dev" con un aviso por stderr y un ✓ en pantalla. La cadena que eso toca:
frase → Argon2id → ChaCha20-Poly1305 que cifra la seed → la clave con la que
`boveda` descifra su base. O sea, en una imagen de escritorio, la bóveda de
todo el mundo cerrada con una palabra escrita en el fuente, y nada que falle.
Arreglado en tawasuyu (`fd08dc03a`): variable > terminal (se pregunta, sin eco,
y DOS veces en la génesis, donde un error de tipeo no se nota hasta que la seed
ya no se recupera) > desarrollo sólo si no hay a quién preguntarle. La decisión
vive en una función pura con cuatro tests, probada AL REVÉS: con el brazo
`Preguntar` borrado falla con `left: Desarrollo / right: Preguntar`.
Pin `9967b02c` → `da5fb8968` ⇒ `b3:46529e14`, 1,9 M, sellado en el worker con
la guarda PEGADA al build. Mirado por dentro (regla 3) y probado como
artefacto, con control negativo: `identity new` + `unlock` deja
`user pacha:id:default: 32` en `/proc/keys`, y con la frase equivocada contesta
«autenticación fallida» y NO re-siembra.
El muro del `Cargo.lock` por cuarta vez, con la causa cambiada: esta vez no la
puso quien tocó el lock sino otro agente que metió `shuma-taller` en un
`Cargo.toml`. Cerrado en el worker, donde el registro está completo: +1 línea.
Y el lock del árbol compartido traía otra vez el malo (índice y árbol con dos
versiones distintas, las dos rotas), así que el commit se armó con
`commit-tree` sin pasar por el índice.
Corrección al §7.undecies: el verbo es `agora-cli unlock`, no
`agora-cli identity unlock`.
El guardián de coherencia pasa de SEIS lugares a SIETE, con su cuarto control
negativo; los cuatro, en verde.
Queda: la herencia del llavero de SESIÓN entre procesos hermanos (sin medir —
y `/proc/keys` como root no la mide), y `pacha`/`pacha-secretos` en
`perfil.servidor` con el mismo hueco.
`jaula-herramientas.sh` deja el compilador, y eso alcanza hasta que algo toca una
biblioteca de C. `mirada-compositor` moría en su build script con «The
PKG_CONFIG_PATH environment variable is not set», y el mensaje engaña: las
dieciocho bibliotecas están en el store. Lo que no había era dónde verlas juntas
— los `.pc` del corpus declaran `prefix=/usr` porque se miran desde el sandbox de
`takana build`, que monta la clausura ahí.
La lista sale de `recipes/mirada-compositor.toml`, no de una copia que se pudra.
Se le suman dos cosas que ninguna receta nombra y que un sysroot a mano sí
necesita: `musl` (adentro del sandbox la libc la pone el sandbox; sin sus
cabeceras bindgen lee las de glibc y muere en 'stddef.h' file not found) y las
gemelas `<dep>-shared` cuando existen (la receta pide zlib/expat/libffi
estáticas, pero el producto es dinámico y al CORRER pide las .so).
Tres cosas que sólo aparecieron haciéndolo:
· La granja espeja el ÁRBOL ENTERO, no `/usr`. El primer intento perdía
`linux-pam` completo, que instala en /lib64 — y no falló compilando: falló con
el binario ya enlazado tomando el libpam de Arch y muriendo en __memcpy_chk.
· Los `-L` van explícitos y derivados de la granja. pkg-config sólo emite el del
módulo que le preguntan, y `pam-sys` no usa pkg-config: emite `link-lib=pam` y
nada más. Con el sysroot a medias eso daba verde porque lld caía al /usr/lib de
Arch; con el sysroot completo dice la verdad y no enlaza.
· El control corre `xkb_keysym_from_name` y no `xkb_context_new`: el contexto
necesita los datos de xkeyboard-config, que no están en la clausura, y el
control moría por algo que no estaba midiendo.
Medido adentro de la jaula, con el ambiente limpio (todo sale de la config de
cargo): `cargo build -p mirada-compositor` en 5m13s, y el binario arranca, elige
el backend DRM y llega hasta donde la jaula lo deja. Sin regresión: `format`
compila y los 14 tests de `llimphi-cpu` pasan.
El §7.novies dio la función por cerrada: las seis etapas del guardián de metal en verde, con el
navegador de verdad y el diálogo a la vista. Lo que seguía abierto era la decisión 1 del §7.sexies
—«en qué imágenes se declaran»—, escrita como NO mientras ninguna app llimphi pudiera pintar. Ese
motivo se cayó el 2026-09-18, así que antes de tomarla se volvió a medir en vez de darla por sabida:
atuq sealed perfiles=[cosmic, gnome, kde, sway]
puriy-costura sealed perfiles=[cosmic, gnome, kde, sway]
boveda sealed perfiles=[]
shuma-pregunta sealed perfiles=[]
`sealed` con `perfiles: []` es sellado ≠ instalado: la lección de `foot`, que targets.toml repetía
QUINCE veces antes de hoy y que igual volvió a morder. Las dos entran a los cuatro perfiles de
escritorio, las dos o ninguna —sin el dueño `vault.match` no ofrece nada; sin el diálogo,
`Command::new` falla y TODO `vault.fill` se deniega—: media bóveda es una que niega todo en
silencio. ~43 M por imagen (22 M + 21 M medidos), contra los ~1,25 GiB que ya lleva el §6.7.
Y al declararlas apareció el hueco de una capa más arriba: la receta instalaba `/usr/bin/boveda` y
nada más, y los lanzadores de los cuatro escritorios leen `/usr/share/applications`. La app viajaría
en la imagen sin existir para quien la usa — la misma forma de fallo que esto viene persiguiendo.
Entra `boveda.desktop`, con tres cosas medidas antes de escribirlo:
· el icono existe: `dialog-password` está en breeze-icons (6), adwaita (1) y cosmic-icons (2). El
cuarto perfil lleva sólo hicolor, que no trae iconos: ahí cae al genérico, que es degradarse;
· lo acepta el `desktop-file-validate` del store, con `atuq.desktop` de control. Deja un hint sobre
`Security`, y las dos formas de callarlo lo cambian por uno PEOR (dos categorías principales ⇒ la
app aparece dos veces en el menú). Se queda como está;
· ⚠ y lo que NO puede hacer: emparejar la ventana con el lanzador. `llimphi_ui::run` no llama nunca
a `with_name` ⇒ winit no manda `set_app_id` y la ventana sale SIN app_id y con el título
"llimphi". Por eso no hay `StartupWMClass`. Vale para toda app llimphi; se arregla en llimphi.
La receta se reconstruyó en el worker con la guarda del §7.quinquies puesta (`### receta verificada
3f1072cc` antes de compilar nada, porque el latido revierte la receta cada media hora y un acierto
de caché sobre la vieja imprime SELLADA en cero segundos): `b3:b0c6adc4` ⇒ `b3:3f1072cc`, 22 M, con
el árbol mirado por dentro y la entrada dentro del artefacto.
Y el guardián de coherencia pasa de CINCO lugares a SEIS: el sexto es `targets.toml` —quién DECLARA
al dueño en la imagen—, con control positivo (`atuq` tiene que estar, o el chequeo está leyendo el
campo equivocado) y su propio control negativo, el tercero. Probado en los dos sentidos: cuatro
perfiles en verde, y `--negative-control-perfil` en rojo.
Abierto, y dicho como lo que es: quién levanta la app con la sesión (atado a la decisión 2 del
§7.sexies, la raíz de las claves), y que el único proveedor de GL de las cuatro imágenes es iris
—mesa-llvmpipe en ningún perfil—, que la bóveda hereda y no agrega.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cargo exporta `CARGO=<ruta del binario REAL>` —el del store, no el envoltorio— y
hay macros que lo ejecutan: `proc-macro-crate` corre `$CARGO locate-project` para
ubicar la raíz del workspace. Adentro de la jaula ese exec moría con «No such
file or directory», que acá miente: el fichero existe y es ejecutable, el que
falta es el INTÉRPRETE. `crate_name()` devolvía Err y el derive `Type` de
zvariant caía a su última rama, `::zvariant` en vez de `::zbus::zvariant`:
error[E0433]: cannot find `zvariant` in the crate root (atspi-proxies 0.13.0)
Eso se lee como un defecto del lock del repo ajeno y NO lo es. Con el cargador
puesto en /lib, `llimphi-ui` de tawasuyu —winit + accesskit + wgpu— compila
entero en 2m17s. La clase es más grande que el caso: cualquier herramienta que
re-ejecute `$CARGO` o `$RUSTC` por ruta absoluta se rompía igual, en un crate
ajeno y a diez capas del origen.
El envoltorio ya prefería el cargador de la caja (`[ -x /lib/ld-musl… ]`); lo que
faltaba era ponerlo cuando no está. En la caja, que es musl, el guion se aparta.
Y va con su control, porque los trece de antes NO lo veían: todos entran por el
envoltorio. Brazo de control corrido con un cargador falso — los trece en verde y
el nuevo en rojo, que es exactamente el hueco.
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>
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>
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>
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ó.
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.
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.
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.
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.
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.
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>
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>
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».
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.
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>
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>
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>
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>
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>
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>
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>
`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>
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>
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>
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>
El guion dejaba las RUSTFLAGS en /etc/profile.d/rust-enlazador.sh y ninguna shell de login las
recogía, porque en esta caja /etc/profile NO EXISTE. O sea que el fichero habría sido exactamente la
avería que este repo persigue: presente, con contenido, y sin ningún efecto. Lo descubrió el control
nuevo, que pregunta por la VARIABLE en una shell de login en vez de por el fichero.
Se crea /etc/profile con el bucle estándar sobre profile.d —de paso revive el bash_completion.sh que
ya estaba ahí, igual de mudo— y no se toca el PATH, que lo fija el init.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`command -v cc gcc clang zig` en la caja no devolvía nada. Eso no es sólo incómodo: es lo que hacía
que rustc, con el compilador propio ya sellado, no pudiera enlazar una sola dylib —y ahí entran todas
las proc-macros—. El `-L /usr/lib` obvio empeora las cosas: rompe los estáticos, porque rustc enlaza
musl static-PIE con su copia self-contained y ese camino le hace preferir la libc.a del sistema, que
no es PIC. El camino de librerías no puede ser global; quien sabe cuál es cuál es un driver de C.
NO introduce nada nuevo en el corpus: envuelve el `seed-zig` que ya está en el store —producto del
bootstrap, como stage1-rootfs—. Volver a zig el compilador de C OFICIAL de la distro sería otra cosa,
y es decisión de ADR. La caché de zig va a /work y no a la raíz de 5,9 G, que ya nos costó una caída.
Controles que corre el propio guion, sobre el HECHO y no sobre la intención: compila y ejecuta un
hola mundo en C; enlaza una proc-macro, compila el programa que la usa y lo ejecuta; y —el que evita
el remedio peor que la enfermedad— comprueba que el estático SIN banderas siga enlazando.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>