La prueba que de verdad decide, la misma con que se midió brush el 21/09: copia
del lab, qillqa como /usr/bin/awk, y tres recetas autotools reales. Resultado:
pkgconf b3:cfb6a37e… sellada · IDÉNTICA al hash vigente
flex b3:6b25e351… sellada · IDÉNTICA
oniguruma b3:751a5270… sellada · IDÉNTICA
Los tres hashes son los que ya estaban en build-state.json, construidos con el
awk de busybox. O sea que qillqa no sólo deja construir: produce los MISMOS
BYTES. brush pasaba 56/57 casos POSIX y no construía ninguna de las tres.
Latencia 1,04× la de busybox (brush: 4,1×) y binario de 0,64 MB, más chico que
gawk (0,83) y que el busybox entero con sus 277 applets (0,98).
⚠ Son tres recetas, no las 943. Lo que prueban es que el camino que tumbó a brush
—el config.status que genera el libtool, donde awk hace el trabajo pesado— aquí
pasa. No prueban que no haya un cuarto caso.
Declarado en `base`, con lo medido escrito al lado de la declaración. El censo
baja a 62 con clear/reset, que ahora los da ncurses-tools.
Y una corrección de método que me costó una reparación: la primera copia del lab
la hice con `cp -al` (hardlinks) y después un `chown -R` sobre ella — que cambió
el dueño del INODO COMPARTIDO y se llevó por delante 471 entradas del
/work/dev-fs/alpine REAL, el lab que usan los demás agentes. Reparado
(chown -h 65534 sobre las 471; el control de /bin confirmó el dueño esperado) y
rehecho con copia real en el disco grande. Un hardlink no es una copia: lo que se
comparte no son sólo los datos, son los METADATOS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
«awk soberano» (usuario, 2026-09-22), que era la tercera opción del ADR pendiente
y la única que no adoptaba la semántica de otro: gawk es C con extensiones GNU,
goawk es Go y rompe la premisa de cadena de la Etapa C, frawk es otro dialecto.
Banco diferencial contra DOS oráculos a la vez —el awk de busybox 1.36.1 (el
applet que hoy viaja) y gawk 5.3.2 --posix— con 109 casos: campos y separadores,
el modelo de valores, aritmética, expresiones regulares, las funciones de cadena,
printf, control de flujo, arrays, funciones de usuario, E/S y los rincones del
parser. Resultado: 104 iguales a los dos, 0 divergencias, y 5 casos donde los
ORÁCULOS discrepan entre sí — que es justo para lo que sirve tener dos.
De esos 5, cuatro los tenemos del lado correcto por POSIX: `2^3^2` es 512 (la
potencia asocia a la derecha; busybox da 64), `index(s,"")` es 1, el `%*d` de
printf existe, y `length(array)` funciona. El quinto era nuestro y se arregló:
una coincidencia VACÍA pegada al final de la anterior no sustituye, así que
`gsub(/a*/,"-")` sobre "xax" da 3 y `-x-x-`, no 4 y `-x--x-`.
El motor de expresiones regulares es propio y esa es la razón de fondo para no
adoptar un crate: POSIX es leftmost-LONGEST y la familia Perl —el crate `regex`
en su modo por defecto— es leftmost-first. Para /a|ab/ sobre "ab", POSIX devuelve
`ab` y Perl devuelve `a`. En un awk eso decide qué borra un sub(), dónde parte un
split() y cuánto valen RSTART y RLENGTH. Construcción de Thompson con conjunto de
hilos: sin retroceso, así que `(a*)*b` contra sesenta `a` no explota.
Las cinco cosas que el banco destapó, todas dando resultados plausibles y ninguna
reventando —quedan como pruebas de regresión en tests/divergencias_medidas.rs:
· leer NF no forzaba el partido en campos ⇒ `{print NF}` daba 0 hasta que
alguien tocara un campo. El partido es perezoso a propósito (es lo que hace
barato asignar a $0) y el precio es acordarse de forzarlo.
· `length` sin paréntesis se leía como una VARIABLE llamada length, que vale
vacío. Es el único builtin al que POSIX permite omitirlos.
· los arrays se pasan por referencia, y eso se decide mirando si la función usa
el parámetro como array — `a["k"]=1` tiene el nombre en el LUGAR de la
asignación, y sólo se miraba el valor.
· substr con inicio ≤ 0 recorta a 1 SIN ajustar el largo, y los no enteros se
truncan. La lectura estricta de POSIX daría otra cosa; ni busybox ni gawk la
hacen, y lo que hay que ejecutar son los guiones escritos contra ellos.
· la regla del vacío en gsub, arriba.
Verificado además contra este repo: de los 61 programas awk únicos que usan sus
guiones y recetas, qillqa acepta los 59 reales (los 2 restantes son f-strings de
Python que mi extractor tomó por awk). 29 pruebas propias en verde.
El nombre es quechua («escritura») y sigue el precedente de simi: lo registra el
comentario de cabecera y lo DECIDE un ADR. El comando instalado será `awk` — eso
es contrato y lo llaman los configure de medio corpus.
⚠ Lo medido es el build NATIVO del hub. Falta la receta, el estático musl con zig
cc, y la prueba que de verdad decide: un `configure` de autotools con qillqa como
/usr/bin/awk. Es la lección de brush — pasó 56/57 casos POSIX y no construía una
sola receta.
Va también recipes/ncurses-tools.toml (clear, reset, tput, tset, infocmp, tic),
declarada en `base`: la canónica instala sólo lib y cabeceras, así que clear y
reset en una imagen eran el applet de busybox. Instala los binarios y NO la base
terminfo: resuelven con los --with-fallbacks compilados en la librería, que
cubren la consola del metal, QEMU y cualquier ssh moderno.
⚠ Y ncurses-tools selló DINÁMICO dos veces antes de salir bien: ncurses usa
`-static`/`-dynamic` como MARCADORES de tramo alrededor de las librerías —su
forma portable de -Wl,-Bstatic … -Wl,-Bdynamic—, no como decisión global. El
`-dynamic` llega al final de la línea de enlace y gana por orden. Se quita
sobreescribiendo LIBS_TIC y LIBS_TINFO en la línea de make. Tercer sabor del
agujero que documenta scripts/static-audit.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los dos guiones de WiFi que corren DENTRO de la imagen (metal-iso.sh y
cosmic/metal-desktop-image.sh) usaban cuatro applets que salen del defconfig:
udhcpc, ifconfig, route y el guión de hooks que udhcpc necesita. Los usos eran
básicos —subir la interfaz, pedir IP, poner ruta y resolv.conf— así que van a las
herramientas ya declaradas en `base`: `ip` de iproute2 y `dhcpcd`.
Y el cambio arregla algo de paso, no sólo sustituye: dhcpcd queda de daemon
RENOVANDO el lease. Una de las dos causas medidas el 2026-08-06 de que el enlace
se cayera solo a mitad de una depuración remota era precisamente que `udhcpc -q`
pedía la dirección una vez y nadie la renovaba. El bloque wifi-keep que lo
mitigaba sigue (la otra causa, el AP que olvida clientes ociosos, no se toca).
`adduser` de alta-usuario.sh NO se migra, y esa es la decisión, no una omisión:
`useradd` es de shadow y su modelo nativo es escribir /etc/shadow — el fichero
cuya creación dejó a la caja sin aceptar ni la clave de root, con los ocho
dominios caídos ~20 min (SDD 28 §6.51). El script tiene un guardián explícito
para cazar justo eso. Cambiar el applet por la herramienta «buena» reintroduce el
fallo que el guardián existe para atrapar ⇒ `adduser` queda indultado en el
defconfig junto a setuidgid, switch_root, halt, poweroff y reboot. El día que la
caja adopte shadow de verdad se revisa entero.
⚠ Sin probar en metal: no tengo la laptop acá. El cambio es de una línea por
guión y las dos herramientas están en los 7 perfiles, pero quien lo arranque en
hardware que mire `ip -4 addr show` antes de dar la red por buena.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Faltaba lo que la página no decía y es lo que distingue a la distro: no QUÉ es,
sino QUÉ SE DECIDIÓ y contra qué. Diez fichas, cada una con tres lecturas de lo
mismo — frente a lo habitual, técnicamente, y en simple— para que sirva igual al
que quiere el detalle y al que quiere entender de qué se trata.
01 fuentes clavadas por commit/sha256 06 sin systemd: arje y arranque por grafo
02 reproducible bit a bit, y why-differs 07 romper a mano y volver (overlay+diario)
03 el paquete es receta, no binario 08 Wayland-only y PipeWire
04 se reconstruye a sí misma (mrustc→rustc) 09 lo ajeno enjaulado y FUERA del store
05 musl estático con zig cc 10 hecha para que una IA la opere
La de los dos pisos no entra a la lista: es la sección de arriba y de ella cuelgan
las diez. Repetirla habría sido rellenar.
Ninguna ficha es opinión: todas salen de un ADR o de una decisión ya tomada en
`docs/` — 0006 fuentes pineadas, 0003 zig cc, 0007 arje, 0010 arranque por grafo,
0014 multi-origen con índice firmado, 0015 imágenes ajenas. Y las afirmaciones
técnicas se comprobaron en el código antes de publicarlas, no de memoria:
`--unshare-all` (que incluye la red) y `SOURCE_DATE_EPOCH=1` están en
`takana-build/src/sandbox.rs:415,453`.
⚠ El precio, dicho en el README del sitio: si una decisión cambia, esta página
miente hasta que alguien la toque. Se gana claridad en la landing y se paga en
mantenimiento.
Las fichas se armaron con un guion de un solo uso, que no entra al repo: la página
sigue siendo la única fuente y no hay generador que mantener. Renumerados los
sellos (el código pasa a 03, estado 04, paquetes 05, frontera 06) y la barra suma
su enlace. 142 pares es/en emparejados, etiquetas balanceadas, cero anclas rotas,
y lo servido coincide con el disco.
El criterio de terminado del paso 2 decía «y la imagen los lleva», y eso no se
comprueba declarando. Hidratación real con el orden del ensamblado (busybox
primero, el userland encima, como USERLAND_COMPONENTS): ningún binario de los
declarados lo sigue ganando busybox — grep, sed, find, xargs, diff, cmp, gzip,
zcat, xz, bc, dc, vi, cpio, killall, pstree, fuser y los de coreutils salen todos
del paquete que corresponde.
Y la prueba que faltaba: hidratar la xz canónica ENCIMA de xz-tools —el escenario
que motivó quitarle lib/ e include/ a la variante— deja liblzma.a de la canónica,
usr/bin/xz de la variante, y xz --version sigue contestando. Conviven, que es lo
que la receta afirmaba y ahora está medido en vez de razonado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fui a escribir el kmod en Rust que el plan pide —la pieza más grande que
quedaba— y lo primero fue buscar contra qué módulos probarlo. No hay ninguno:
· 0 ficheros .ko en los artefactos sellados de los cuatro kernels (sólo
boot/bzImage y el .config)
· 0 recetas del corpus hacen modules_install
· 0 drivers `=m` en los .config sellados (contra 1809 `=y` en linux-metal)
· `# CONFIG_MODULES is not set` en los tres kernels que están en el store
· nadie en el repo enciende CONFIG_MODULES
Sin CONFIG_MODULES el kernel ni siquiera implementa init_module/delete_module: un
insmod escrito en Rust recibiría ENOSYS. Los 6 applets de kmod no son escritura,
son 6 applets a retirar. Y con ellos otros 3: DEVTMPFS_MOUNT=y hace que el kernel
cree y monte /dev, no hay UEVENT_HELPER, y libudev-zero ya viaja en los 8
perfiles ⇒ mdev, makedevs y uevent tampoco tienen consumidor.
De los 23 applets que el plan llamaba «el trabajo real» queda: un ADR (awk), una
migración de card (getty → agetty, que ya está en los 7 perfiles), y decidir
nslookup (ningún candidato publica ese nombre; su único llamador es
scripts/drive-netup.py y migrarlo sale más barato que traer BIND).
Y la corrección que me toca: hace dos horas escribí en la salida del vigía, como
advertencia, que «el kernel llama a /sbin/modprobe por call_usermodehelper ⇒ esos
applets salen libres y no lo son». Es plausible, es la forma correcta de razonar,
y es falsa para este producto. La escribí sin medirla, en el mismo commit donde
presumía de haber arreglado dos falsos negativos. Corregida en el script.
NO se recorta ahora: editar RETIRADOS re-hashea busybox y las 24 recetas que lo
declaran build-dep, y 15 de esas 24 siguen en deuda por el recorte del 21/09. El
siguiente recorte junta lo ya decidido y se hace una vez: 6 de kmod + 3 de /dev +
6 alias internos de iproute2 (ipaddr iplink ipneigh iproute iprule iptunnel, que
no son comandos de nadie) = 15 applets, 277 → 262, sin escribir una línea.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
psmisc 23.7 (killall pstree fuser prtstat pslog) y gavinhoward/bc 7.0.3 (bc Y dc
del mismo árbol). Ninguna estaba en el corpus, y lo de psmisc es lo que el plan
daba por cubierto sin estarlo: procps-ng NO trae killall ni pstree ni fuser —
procps-ng es ps/top/free/kill/pgrep/pkill/pidof/uptime/vmstat/w/watch.
bc es el de gavinhoward y no el de GNU por la cadena: C99 sin dependencias, bc y
dc en un árbol, y es el bc de sistema de Alpine y FreeBSD. El de GNU arrastra ed
y flex, y su dc va en otro paquete.
⚠ killall5 NO lo trae psmisc: es de sysvinit. Acá lo cubre arje-zero por FUNCIÓN
(es PID 1 y apaga él) ⇒ se retira con los indultos, no se reemplaza. Son 3 de los
4 que la tabla prometía, dicho así para que no vuelva a prometer lo que no hay.
Dos pisones de psmisc, los dos escritos en la receta para no repetirlos:
1. `-lncurses` no existe acá (la nuestra es widec ⇒ libncursesw.a). No sirve
LIBS=-lncursesw —el AC_CHECK_LIB compila un programa de prueba CON -lncurses,
no pregunta si el símbolo resuelve— ni la variable de caché con
`make TERMCAP_LIB=…`, porque src_pstree_LDADD = @TERMCAP_LIB@ se sustituye en
tiempo de configure y el Makefile queda con -lncurses LITERAL. Lo que funciona
es un enlace libncurses.a → libncursesw.a en el scratch y un -L.
2. Y ese -L se comió el -static. El harness inyecta LDFLAGS=-static para las
recetas link="static" (takana-build/src/lib.rs:285); reemplazar LDFLAGS lo
pierde. Los seis binarios sellaron DINÁMICOS sin que nada fallara: killall
respondía bien y sólo pstree moría en ejecución con `Error relocating
/lib/libncursesw.so.6`, tomando la librería del anfitrión. Un `file` lo dice en
un segundo; ninguna prueba del build lo iba a decir.
Los dos van a `base` en targets.toml, y el censo baja de 69 a 64.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contesta el segundo pendiente del §7.duodecies: `pacha`/`pacha-secretos` en
`perfil.servidor`. La respuesta a la pregunta de origen es **NO**, y es la
CONTRARIA a la del escritorio.
Premisa medida, no supuesta: la Card de `pacha-secretos` es `scope=system` y
entra al `genesis`, o sea que arje arranca el daemon EN EL ARRANQUE. De ahí se
sigue que un sembrador en la imagen no llega a tiempo por construcción — en el
arranque no hay nadie logueado; `abrir_almacen()` decide una vez en `main()` y
no vuelve a mirar; y `agora-cli unlock` de una sesión ssh siembra en el llavero
de ESA sesión. Declararlo dejaría la función PARECIENDO cerrada, que es peor
que el hueco. Queda escrito al lado de las dos raíces, en `targets.toml`, para
que nadie copie la decisión del escritorio.
Y al ir a medirlo, el control POSITIVO salió rojo — con la seed en `/proc/keys`
el daemon decía que no había identidad — y eso destapó el defecto de verdad:
los CINCO lectores de la seed hacían `.ok().flatten()` (o un `_ =>`), que
convierte «el llavero no se pudo consultar» en «no hay identidad desbloqueada».
La causa del rojo la mide la sonda de syscalls: en el LXC `add_key` funciona y
`keyctl(KEYCTL_SEARCH)` da `ENOSYS` ⇒ se puede sembrar y no cosechar.
El peor de los cinco no era un mensaje feo: `pacha-cli` guardaba los dotfiles
SIN CIFRAR, en silencio, con la identidad del usuario desbloqueada.
Arreglado en tawasuyu (`4f2b5eac7`) con `pacha_llavero::SeedDeSesion` —tres
estados, tres textos— y `Reason` en `net.tawasuyu.Secretos1`. Pin de las dos
recetas `23a292863` → `cd9acd0d9` ⇒ `b3:e8038352` (4,6 M) y `b3:a9fb8c17`
(8,5 M), mirados por dentro y probados como artefacto: el daemon sellado ahora
dice «motivo=el llavero de sesión NO se pudo consultar (… os error 38) — esto
no es «no hay identidad»».
Tres cosas más que quedaron medidas por el camino:
- la guarda pegada al build **se disparó sola por primera vez**: el latido
revirtió `pacha.toml` entre las dos construcciones de la misma tanda;
- el muro del `Cargo.lock`, quinta vez, y la arista que faltaba era la mía de
esa mañana: se la llevó un «merge de git en «main» (import)». Bisecado con
`git log -S`;
- el índice Y el árbol compartidos de tawasuyu tenían una versión de
`pacha-boveda-llimphi` ANTERIOR al arreglo de `7917fbb96`. Lo delató el test
de regresión, no el diff.
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>
`repo list --repo <url>` tomaba un `PathBuf`, así que trataba la URL como una ruta
relativa que no existe y respondía **«repo vacío» con salida 0**. No es que no
soportara el caso: es que afirmaba lo contrario de lo que pasaba, y con rc=0, que
es la forma de fallo que un script no puede detectar. Salió al probar el repo del
dominio nuevo.
`install --repo` ya sabía hablar HTTP con `RepoSource` (ADR 0014: lista de
orígenes separada por comas, probados en orden). `list` y `verify` ahora usan el
MISMO resolvedor, así que la misma cadena de espejos vale en los tres verbos.
`sign` se queda local a propósito: firmar es escribir.
Y `verify` remoto no es un extra — es el caso de uso del ADR 0014. Verificar el
release de un espejo ANTES de instalarle nada era imposible sin construir.
De paso, tres estados que se daban por iguales y ahora se distinguen (CLAUDE.md
§3: un ausente falla ruidosamente, un vacío llega hasta el final diciendo que todo
fue bien):
dir que NO EXISTE → error, rc=1, y sugiere que quizá era una URL
dir sin index.json → «repo por crear» (estado válido: lo crea `pack --repo`)
índice con 0 paquetes → «índice publicado y SIN paquetes»
origen que no sirve → error del fetch, con los orígenes probados
Y el índice se atribuye al origen que REALMENTE lo sirvió, no a la lista entera:
con `<espejo-caído>,<bueno>` la salida dice el bueno. Es la misma regla que el
propio `fetch_desde_algun_origen` aplica a los `.swm` —«decir bajado de la lista
entera cuando sólo uno respondió es una media verdad»— que `read_index` tiraba.
Medido contra el repo vivo: `list` 173 paquetes [release firmado por release],
`verify` trusted. 93+93+6+1+3+4 tests del crate en verde.
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.
`cargo vendor --locked` sobre `4f2b5eac7` muere con «cannot update the lock
file». Esta vez la arista que faltaba era la MÍA de esta mañana: `rpassword`
en `agora-cli`, que `338d4a9b9 "merge de git en «main» (import)"` se llevó al
resolver el lock con el lado viejo. Bisecado con `git log -S`, no adivinado.
Quinta vez que este monorepo pone el mismo muro, y ya está claro que no lo
levanta quien toca el lock sino cualquiera que toque un `Cargo.toml` — o un
merge que lo resuelva mal.
Cerrado en el worker, donde el registro está completo: +1 línea, y el blob es
byte a byte el que ya se había publicado.
hash: pacha-secretos b3:7599de72 → b3:e8038352 · pacha b3:916788ae → b3:a9fb8c17
Los dos daemons de `perfil.servidor` leen la seed de identidad del llavero de
sesión, y los dos tragaban el error: `.ok().flatten()` (o un `_ =>`) convierte
«no se pudo preguntar» en «no hay identidad desbloqueada». La primera es un
error de sistema; la segunda se le echa a quien está delante.
Medido en el LXC del worker, con el binario sellado que va en la imagen:
`add_key` funciona y `keyctl(KEYCTL_SEARCH)` da `ENOSYS` (seccomp de
contenedor) ⇒ ahí se puede SEMBRAR y no COSECHAR, y con la seed presente en
`/proc/keys` `pacha-secretos` igual arrancaba diciendo «SIN identidad agora
desbloqueada». Es el mismo `Err` de cualquier sistema sin llavero de kernel.
Y en `pacha-cli` era peor que un mensaje equivocado: con el llavero roto los
dotfiles se guardaban SIN CIFRAR y sin decir una palabra.
Arreglado en tawasuyu (`4f2b5eac7`) con `pacha_llavero::SeedDeSesion` —los tres
estados, con `motivo()` distinto para cada uno— y test en los dos sentidos.
`pacha-secretos` gana además `reason` en `net.tawasuyu.Secretos1`, al lado de
`persistent`, que existía justamente porque «desde afuera es indistinguible».
hash: pacha-secretos b3:1d2946d0 → b3:7599de72 · pacha b3:ea0c664f → b3:916788ae
(las dos suben juntas al mismo commit para pagar UN solo vendoreo de 2,4 G)
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>
Vive junto a la página porque es su documentación, pero quedaba servido en
/README.md: rutas de la caja y el procedimiento del vhost, que no son para el
visitante. El bloque del README se sincroniza con el que quedó en la caja para
que no derive.
El dominio se registró hoy. La página entra al repo takana y NO al de gioser-web —
la que hay en `takana.gioser.net` la genera `hermanas.py` desde otro repo y todavía
apunta a `git.gioser.net/sergio/hammer`, un nombre que dejó de existir hace doce
días: una página sobre el proyecto que vive fuera del proyecto envejece sin que
nadie se entere.
Lo que la página publica, que es lo que se pidió: **dos direcciones**, no una.
código https://git.gioser.net/sergio/takana (clone por HTTPS)
paquetes https://takana-os.net/repo/ (el MISMO /srv/repo firmado)
El `/repo/` cuelga por `handle_path` del mismo árbol que sirve `repo.gioser.net`,
así que es la dirección en el dominio propio sin DNS ni certificado nuevos.
Comprobado de punta a punta antes de escribirlo en la página, no supuesto:
takana install age --repo https://repo.gioser.net --trust ./trust
⇒ release: trusted (by release) · apply OK
⚠ De paso se vio que `takana repo list --repo <url>` NO habla HTTP —sólo mira
directorios locales— y ante una URL responde «repo vacío» con salida 0 en vez de
decir que no sabe. Queda anotado en el README del sitio; el arreglo es otra unidad.
Las cifras de la tabla salen de `docs/state/build-state.json` y del store de la
caja, contadas hoy: 941 recetas, 926 selladas, 15 en deuda, 1407 artefactos, y las
clausuras de los cuatro escritorios (KDE 418/420, GNOME 318/319, COSMIC 282/283,
sway 269/270). No se estiman.
Sin terceros: las tipografías de la marca viajan en `fuentes/` (154 KB, subconjuntos
latin y latin-ext, las dos SIL OFL con su licencia al lado). Un sitio sobre no
depender de binarios ajenos que pidiera las fuentes a Google en cada visita sería
una contradicción barata.
Contraste medido, no a ojo: el brasa `#DF6B2A` de la marca sobre alpaca da 2,9:1 y
no se lee, así que el acento del tema claro es un brasa oscurecido `#9E3F16`.
Ninguna combinación de la página baja de 4,5:1.
El vhost ya está puesto en la caja (respaldo del Caddyfile + `caddy validate` +
`arjectl restart caddy` + testigo `git.gioser.net` respondiendo antes de darlo por
bueno). Falta lo único que no puedo hacer yo: los dos registros A a 2.29.29.217.
Mientras se escribían el /etc/shells y el `chsh -s /bin/zsh sergio` había un overlay del FHS
abierto —de un `takana install` sin --prefix sin commitear— y las DOS escrituras cayeron dentro,
aunque no tenían nada que ver con ese paquete ni pasaron por takana (un `cat >` y el chsh de
shadow). Medido con `mount --bind /` no recursivo: en el /etc REAL no había shells y sergio seguía
en /bin/bash, mientras la pantalla mostraba las dos cosas hechas. Un discard o un reinicio lo
borraba y nadie se habría enterado hasta el siguiente login.
Resuelto con commit (5 ficheros promocionados: passwd, passwd-, shells, zsh, zsh-5.9). El
desmontaje perezoso del §5.10 se ganó el sueldo ahí mismo: saltó en /bin y en /usr/bin.
La lección, que es más grande que el caso: `install` sin --prefix NO es un experimento acotado a
ese paquete — cambia la semántica de escritura de la máquina entera hasta que alguien commitea o
descarta, y el overlay no avisa por ningún lado. Como mínimo `takana status` debería salir al
entrar, y el `overlay listo` del install no debería leerse como «terminé». Anotado en SDD 28 §5.11.
Pedido tras instalar zsh: «no me funciona chsh». Dos capas, y sólo la primera es un arreglo.
1. /etc/shells NO EXISTÍA en la caja, y shadow rechaza cualquier shell que no esté listado (para un
usuario normal es rechazo duro, no aviso). Escrito con lo que existe, comprobado -x uno por uno,
en vez de copiar la lista de otra distro: un shell listado que no está deja a quien lo elija sin
poder entrar, y eso no se ve hasta el siguiente login. Con eso `chsh -s /bin/zsh sergio` va, y
`su - sergio` entra con zsh 5.9.
2. ⚠ Pero sergio sigue sin poder cambiárselo él mismo: `su sergio -c "chsh …"` ⇒ «Cannot change ID
to root». Los cinco binarios de shadow que necesitan setuid (chsh, passwd, su, chfn, newgrp)
salen del store r-xr-xr-x, porque la receta lleva --disable-account-tools-setuid heredado del
APKBUILD de Alpine (allá los hace setuid el empaquetado, no el configure). Y debajo hay otra
capa: of_tree sólo hashea el bit de EJECUCIÓN (mode & 0o111), así que el modelo de artefactos
hoy no puede ni expresar ni garantizar un setuid — aunque la receta lo pusiera, el hash no lo
cubriría y nadie notaría si se pierde o aparece.
Quitar la bandera no alcanza y no es un arreglo de receta: es una decisión de seguridad. Anotada
en la propia receta y en SDD 28 §5.11, sin decidir.
El comentario va en la cabecera de la receta y NO mueve su hash: comprobado antes y después,
b3:843e9098… las dos veces (los comentarios fuera de [build.phases] no entran en hash_inputs).
scripts/verificar-repro.sh simi en el worker: «✓ simi REPRODUCE», 0
no-determinismos. Queda anotado en docs/state/repro-verificado.tsv con su hash,
que es la clave del libro: si la receta o alguna dep cambia, la entrada deja de
casar y hay que volver a verificar.
Las dos trampas, anotadas en el SDD porque las dos fallan en silencio:
1. --store <tirable> NO sirve para verificar reproducibilidad. Un store vacío da
«Error: io: No such file or directory» a secas —sin decir qué falta— porque las
deps del build no están ahí. El método correcto ya estaba escrito: apartar el
artefacto dentro del MISMO store y reconstruir. Dos corridas perdidas por
inventar un instrumento en vez de leer el que existía.
2. La cosecha BORRA una receta sembrada a mano: siembra hub→worker con
rsync --delete, y el hub del que siembra es gioser, no el clon donde se
trabaja. El worker perdió recipes/simi.toml entre el build y la verificación
(el artefacto sellado sobrevivió, la receta no) y el verificador informó
«REPRODUCEN: 0 · sin artefacto: 0» — o sea nada, porque su primer [ -f ] falla
y salta en silencio.
Comprobado de paso: editar los comentarios de la receta NO cambia el hash
(hash_inputs es una lista explícita de campos). Y el artefacto vive sólo en el
store del worker: sella con PROMOTE=0 y la cosecha baja el manifiesto, no el
store. Promoverlo es otro paso.
b3:46ff6fcf67bd23dece4d130c92855908520200f88ca85981317f81fcc56bb834
Construida con flock -o en dev.gioser.net (el LXC prestado, coste 0). El
artefacto trae dos ficheros: usr/bin/simi y su .hammer/recipe.toml.
Medido sobre el ARTEFACTO, no sobre el build del hub:
- ELF x86-64 estático, stripped, 498.736 bytes
- scripts/static-audit.sh simi: «estáticos de verdad: 1 · MIENTEN: 0», o sea que
el link=static no es una declaración sino un hecho (ese guardián existe porque
libtool lo ignora en silencio en otras recetas)
- banco diferencial, 159 casos contra el ash de busybox: 0 divergencias
(bash: 8, brush: 35)
- latencia 468 us contra 449 us del ash: 1,04x. brush: 1569 us, 3,5x
Y la lección que vale más que el número: el build NATIVO del hub (glibc,
dinámico) daba 865 us, 1,4x el ash. El estático musl baja a 1,04x. Medir el
binario que VIAJA, no el que se compila a mano — la diferencia fue del 35 % y
habría quedado escrita como si fuera del shell.
Sigue sin estar en ningún perfil de targets.toml a propósito: declararla es
meterla en ATTEST_PATHS y eso es frente C, su propia unidad de trabajo.
recipes/simi.toml sigue el patrón de hammerd/netup: crate del workspace a un
commit fijado, zig cc, musl estático, strip_debug. SIN [build.phases]: el
BuildSys::Cargo instala el binario, y el symlink `sh` NO lo pone la receta a
propósito — quién gana como /bin/sh lo decide USERLAND_COMPONENTS en
takana-bootstrap, que es frente C y su propia unidad de trabajo.
El SDD queda con el veredicto (simi 0 divergencias, bash 8, brush 35), el alcance
por consumidor, los siete bugs del camino y las seis cosas que faltan. Con dos
avisos explícitos: lo medido es el build NATIVO del hub (glibc, dinámico) y el
estático musl no se construyó, así que simi no entra en ningún perfil de
targets.toml; y la shell interactiva de getty no está cubierta.
«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.
La primera instalación real de un paquete en la caja (`takana install zsh`, sin --prefix) dejó el
sistema A MEDIAS: `commit` desmontó 5 de 7 targets y murió con `target is busy` en /bin, con dos
overlays montados y el estado sin promocionar. La causa no es un fd abierto: los procesos tienen su
EJECUTABLE mapeado desde /bin y /usr/bin —empezando por PID 1, /usr/bin/arje-zero— y un ejecutable
mapeado pin-ea el montaje. En un FHS vivo eso no se puede evitar esperando.
El comentario de `do_umount_lenient` prometía el desmontaje perezoso «desde la Fase 2» y el código
NUNCA pasaba `-l`. Ahora, ante `busy`, reintenta perezoso: desengancha el montaje ya mismo (las
rutas vuelven a resolver al directorio real, que es lo que el promote necesita) y libera cuando el
último proceso que lo miraba se muere; los vivos ven el mismo contenido porque el commit copia el
upper al lower justo después.
Probado en la caja con un paquete nuevo: install tree ⇒ overlay; commit ⇒ WARN del perezoso sobre
/usr/bin y «commit OK — 1 archivo(s) promocionados»; 0 overlays, /usr/bin/tree real, tree v2.3.2.
El SDD 28 §5.10 anota las otras cuatro cosas que ese `sudo takana install zsh` destapó: el repo por
defecto que no existía, el lab que no se encontraba desde /root, el binario del sistema incapaz de
leer los .tkn nuevos, y —la peor— que la instalación queda PARTIDA porque el overlay cubre siete
directorios y /usr/share no es uno: 1296 de los 1331 ficheros fueron directos al FHS real mientras
2 binarios esperaban el commit, y el mensaje decía `apply OK` igual.
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.