23 Commits
Author SHA1 Message Date
Sergio e0e390c282 repo list/verify: hablan HTTP — un repo remoto con 173 paquetes se leía como vacío
`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.
2026-09-21 21:28:49 +00:00
Sergio 7be17b5a2a pack: el control del target_bin AVISA, no aborta — abortar dejaba 65 paquetes fuera del catálogo
El control que agregué en el commit anterior era el natural y estaba mal: publicar el perfil
`servidor` entero con él dejó 65 de 175 paquetes fuera, porque son LIBRERÍAS (zlib, ncurses,
openssl, musl-*, gmp…) que no traen ningún binario y existen para que otros las resuelvan como
dep — un campo que su consumidor ni mira habría tirado un tercio del repo. Lo destapó el barrido,
no el razonamiento: la versión que abortaba pasaba los tests igual.

Ahora: corrige el path si el binario quedó en otro bindir (que es el caso que importa), y si no
hay ninguno lo dice y publica igual.

Medido con el perfil `servidor`: 162 publicados y firmados, 8 con target_bin corregido (bash, sed,
tar, parted, dhcpcd, squid, wpa_supplicant, zsh — todos publicados hasta hoy prometiendo un path
que no existe), 64 sin binario, 12 sin sellar en este store, 1 fallo (os-release, ya conocido).
Contra el repo firmado por HTTP: parted (6 parches + binario en /usr/sbin) instala en 0,17 s y
responde 3.7; bash responde 5.3.0(1).

Queda vivo: `install <librería>` directo sigue fallando tras hidratar (el .swm exige que el
target_bin exista). Hacer el campo opcional toca el formato — su propia unidad de trabajo.
2026-09-21 18:34:17 +00:00
Sergio 9fe1e010e2 paquetería: zsh se puede instalar — los parches múltiples viajaban unidos y el target_bin era una adivinanza
Cerrar el lazo servir→instalar sobre `zsh` (pedido del usuario) destapó dos fallos de la familia
del `strip_debug` del SDD 28 §5.2: cosas que el `.swm` no transportaba fiel y que sólo se ven
cerrando el lazo, no leyendo el código.

1. `pack` CONCATENABA los N parches en el `patch` inline único, y `hash_inputs` mete una entrada
   por parche (con `of_inputs` length-prefijado) ⇒ el receptor sellaba en otra dirección y el
   `expected_hash` no coincidía NUNCA. Medido: zsh anclado b3:0be3630d…, reproducido b3:d6329a62…
   (idéntico al de una copia de la receta con sus 6 parches concatenados a mano). Afectaba a las
   16 recetas del corpus con ≥2 parches. Ahora viajan como lista `patches`, en orden, un fichero
   por parche del lado del receptor; el `patch` único se sigue leyendo para no invalidar lo
   publicado. Regresión con control negativo en swm_bridge.

2. `target_bin` se adivinaba `/usr/bin/{name}`; zsh instala en `/bin` (--bindir=/bin), así que el
   paquete prometía un path que el artefacto no tiene y el error salía en el cliente DESPUÉS de
   hidratar 1331 ficheros. Con `--build` el artefacto está delante: se comprueba, se corrige
   diciéndolo, y si no hay ningún binario con ese nombre se ABORTA al publicar.

Y el verbo que faltaba para «avisame cuándo actualizar»: `takana outdated` compara la DB de
instalados con el índice firmado (por hash, no por versión: una re-publicación con la misma
etiqueta es otro artefacto). No construye, no baja .swm y no actualiza — instalar es verificar.

Medido tras el arreglo: install zsh ⇒ cache-hit del hash anclado, 1331 ficheros en 0,14 s, zsh 5.9
corriendo.
2026-09-21 18:31:14 +00:00
SergioandClaude Opus 5 a1ccce2b65 squid mudado — y la jaula arrancaba DEGRADADA cuando la arranca un init
El proxy de salida sirve desde la caja: `squid 7.7` en 2.29.29.217:1137 con la config, el `passwd`
de los tres usuarios y las dos ACL de gioser tal cual, dentro de una instancia qorpa y supervisado
por arje. El de gioser sigue vivo e intacto (7.6). Es un servicio con gente encima: el access.log
del origen mostraba CONNECT a api.anthropic.com y a claude.ai en el minuto anterior a mudarlo.

Control en los dos sentidos: desde una IP no autorizada, 407 igual que el original; desde localhost,
que su propia config permite, TCP_TUNNEL/200 a claude.ai y api.anthropic.com.

EL MURO QUE VALE, y es un bug de qorpa: **el rango de subuid se buscaba por `$USER`**. Una shell
interactiva lo trae; un init NO. Bajo arje la tarjeta arranca con PATH y HOME, el nombre salia
vacio, no habia rango, y la jaula caia al userns de UN SOLO ID — con lo que squid moria en
`setgid(15): (22) Invalid argument` y el supervisor lo reintentaba 80 veces. El aviso de qorpa
estaba impreso y era correcto («no hay rango para "" en /etc/subuid»), pero lo que se ve en el bucle
es el error de squid: **la degradacion silenciosa la paga el de mas abajo**. Y el MISMO comando a
mano funcionaba, porque la shell si pone `$USER`: el sintoma dependia de quien lo arrancaba.

⇒ `nombre_de_usuario()` deriva el nombre del UID REAL leyendo /etc/passwd y el entorno queda como
ultimo recurso, con la mitad pura (`nombre_en_passwd`) probada, control negativo incluido: un uid
que no esta NO inventa un nombre — devolver algo ahi haria buscar un rango ajeno.

Los otros tres muros quedan documentados en §6.27: provisionar ANTES de montar la config (pacman
aborta si el paquete no puede escribir sus defaults), `/dev/shm` que bwrap crea 0755 y squid no
puede usar al bajar a `proxy`, y el pidfile que en una jaula con `--unshare-pid` SIEMPRE parece
fresco porque cada corrida tiene su propio PID 2.

Y la pregunta que corresponde —¿no va en una receta?— con su respuesta: si, y sigue pendiente. La
jaula fue la via urgente. La diferencia con php-fpm importa: PHP no queremos que entre al corpus,
squid es C y su sitio natural es una receta como la de gitea.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 18:59:13 +00:00
SergioandClaude Opus 5 d52824efeb mudados tawasuyu.net y gioser.net — y los padres que inventa bwrap eran 0700
Los dos sitios que quedaban con trafico real sirven desde la caja `takana`, con TLS publico y
verificados desde fuera. Los servicios viejos SIGUEN corriendo en gioser a proposito (decision del
usuario: mover el DNS y dejar el origen vivo, para poder volver en un minuto).

· `tawasuyu.net` + `www`: la raiz en gioser era EL MONOREPO ENTERO (145 G) servido por HTTP. El log
  de accesos dice que las unicas rutas con trafico son `/`, `/descargas` y `/web`, asi que se
  replicaron solo los dos subarboles que el sitio usa (1,2 M + 3,8 G) con la MISMA estructura, para
  que cada `rewrite` siga siendo el mismo.
· `gioser.net` + `www`: 428 M de estaticos + `/reencuentro`, que necesita PHP. `/hooks/*` NO se
  muda: lo atiende `webhook-deploy.py`, que redespliega aura/sigma/summa/brahman — los cuatro
  FOSILES del §6.19 — y tiene cero peticiones. Muere con la caja vieja.
· `sergio` dejo de ser `CNAME -> www` y tiene su A propio (apuntando a gioser, sin cambio visible).
  Sin eso, mover `www` se lo llevaba puesto: en esa zona casi todo cuelga de `www`.

PHP no entra al corpus y no hace falta: `php-fpm 8.5.10` corre en la instancia `gioser-php` (ADR
0015), supervisado por arje. Control contra el original en los dos sentidos: POST con la trampa
anti-robots da `{"ok":true}` 200 y GET da 405, igual que en gioser.

EL MURO, que es general y no de PHP: para montar en `/work/www/gioser-web`, bwrap crea `/work` y
`/work/www` —que la imagen no trae— y los crea **0700 root**. Adentro somos root y a mano todo
funciona; pero un servicio que BAJA de privilegio (php-fpm a `http`, o cualquier instancia con
`run_as`) no puede ni atravesarlos, y el sintoma es un «File not found» sobre un fichero que ESTA.
Medido con el control que lo separa de un problema de permisos del anfitrion: como `http`, leer un
fichero de la IMAGEN funciona y leer el directorio CONCEDIDO da Permission denied.

`grants_to_args` crea ahora los ancestros que faltan con `--perms 0755 --dir`, y SOLO los que la
imagen no trae: hacerlo sobre `/etc` o `/home` le cambiaria los modos a la imagen. Con test, y
probado rompiendolo a proposito (sale `["/etc","/etc/php"]` en vez de `["/etc/php"]`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:58:17 +00:00
SergioandClaude Opus 5 dbfb7d492b la jaula no viajaba en NINGUNA imagen: bwrap y harkaq-exec entran a perfil.base
Mudando `api.sergio.gioser.net` a la caja de produccion, `takana qorpa provision` aborto:

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

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

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

Tres piezas:

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 21:46:08 +00:00
SergioandClaude Opus 5 467a87cdb8 la imagen del servidor, armada y booteada — tres fallos que sólo aparecen ahí
Se armó la imagen del perfil `servidor` con gitea y se arrancó en QEMU. Los tres hallazgos, en el
orden en que aparecieron, son los que justifican probar la imagen en vez de dar por buena la receta.

1) ⚠⚠ ESCRIBIR EN EL ROOTFS HIDRATADO ES ESCRIBIR DENTRO DEL STORE

`takana users --merge` hacía `fs::write` sobre `<rootfs>/etc/passwd`. Un rootfs hidratado se arma con
HARDLINKS contra el store: medido, ese fichero y el del artefacto `product-rootfs` eran **el mismo
inode (1225824, 2 links, modo 444)**. Un `write` habría modificado el artefacto SELLADO, y todas las
imágenes futuras habrían salido con la cuenta metida dentro del producto. Acá se salvó porque el
store es de sólo lectura y salió `Permission denied` — confiar en eso es confiar en un permiso.

Ahora `escribir_rompiendo_hardlink()`: temporal + `rename`. Con su control, que afirma lo que
importa: tras escribir, el fichero del store **conserva su contenido**, baja a 1 link y el del
rootfs tiene otro inode. Verificado también sobre la imagen real.

2) LA IMAGEN TRAÍA EL BINARIO, LA CUENTA… Y NADIE LO ARRANCABA

Primera imagen: `gitea` instalado, `gitea:x:916` en `/etc/passwd`, y en el `genesis` de la seed sólo
`sshd`, `console-getty`, `hammerd`, `hammer-product`. `servidor-image.sh` no inyectaba las Cards —
eso sólo estaba en el camino de las imágenes de escritorio. Y ninguna métrica lo dice: `--services`
responde que el perfil lo habilita, y lo habilita; lo que faltaba era el paso que lleva esa
declaración a la imagen. Ahora llama a `service-cards` + `inyectar-cards.py` (idempotente por label).

3)  EL BINARIO MORÍA CON `trap invalid opcode` — Y EL BUG ESTABA EN EL BUILDER

Con la card en el genesis y la config puesta, gitea arrancaba y moría al instante:

    traps: gitea[91] trap invalid opcode ip:79ea992 ... in gitea[...]

El sandbox exporta `CC` apuntando a un wrapper que pone `-mcpu=baseline` (`sandbox.rs` ya avisaba:
«de paso cierra el SIGILL de AVX en qemu64»), y la fase Go del propio builder lo PISABA con
`CC="zig cc"` a secas — que por defecto es `-mcpu=native` y hornea la ISA del que compila. El binario
corría perfecto en el worker y moría en QEMU-TCG.

No falla al compilar ni al sellar: falla al EJECUTAR en otra CPU. Y el `ArtifactHash` no lo puede
cazar, porque la CPU del builder no entra en `hash_inputs` — dos workers distintos sellan bytes
distintos bajo la misma dirección. Arreglado en el builder y en la receta; re-hashea las CINCO
recetas `cgo = true` (gitea, usql, sq, gocryptfs, naabu), que es correcto: lo que había sellado no
es portable. gitea: `b3:391a613e…` → `b3:5edf9c16…`.

Lo verificado en la VM: PID 1 = arje-zero · la cuenta de `[[user]]` en `/etc/passwd` y `/etc/group`
de la imagen · la card de gitea en el `genesis` · y arje encarnándola, con la guarda saliendo 78 y
`/var/log/arje/ente-gitea.log` diciendo exactamente «falta /etc/gitea/app.ini — es config del SITIO».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 16:43:29 +00:00
SergioandClaude Opus 5 d611f8577d [[user]] en la receta: la otra mitad del SDD 30 — un demonio que no corre como root necesita cuenta
El SDD 30 sacó las Cards de una constante de Rust y las puso en la receta. Los USUARIOS se quedaron
donde estaban las Cards: `/etc/passwd` de la imagen es `takana_bootstrap::PRODUCT_PASSWD`, un literal
con `root` y `sshd`, y añadir un tercero era editar Rust y recompilar takana.

No es simetría por elegancia, es un servicio que no arranca: `gitea` se niega a correr como root, su
Card hace `setuidgid gitea`, y sin la cuenta arranca, muere y reintenta para siempre con un log que
dice `unknown user` — no «a la imagen le falta una cuenta».

    [[user]]
    name = "gitea"
    uid  = 916
    home = "/var/lib/gitea"

**El uid se declara, no se asigna**, por la misma razón que el ULID de la Card: un «primero libre a
partir de 1000» hace que dos imágenes del mismo perfil salgan con dueños distintos y el rootfs deje
de reproducir SIN QUE NADA FALLE — los ficheros se ven iguales y `ls -l` dice otro número. Fuera de
`hash_inputs`, medido: el hash de gitea no se movió (`b3:391a613e…` antes y después).

`takana users <recetas…> [--merge <rootfs>]` es el gemelo de `service-cards`, y fusiona **por clave,
no por línea entera** — componer dos veces no duplica, y `grep -q` de la línea completa no serviría
porque la misma cuenta con otro GECOS se leería como nueva. Tres decisiones con su control:

· Una cuenta ya presente con OTRA línea es CONFLICTO y no se pisa: sale ≠0. Pisarla es cambiarle el
  uid a ficheros que ya son de alguien, y eso se descubre dentro de la VM.
· Se planea todo y sólo entonces se escribe. Fichero a fichero, un conflicto en `passwd` dejaba el
  `group` ya escrito: media cuenta es peor que ninguna, porque parece que está. Comprobado con el
  caso exacto — `group` sin la cuenta, `passwd` con otro uid — y los DOS ficheros quedan intactos.
· Un `passwd` ausente es un error, no un fichero a crear: crearlo dejaría una imagen SIN `root`.

La validación rechaza lo que rompe tarde: `root`/`sshd`/`nobody` y sus uids, uid fuera de
100..=65533, `home` relativo y un `:` en cualquier campo — que partiría la línea y fallaría lejos.

Y `--user-paths` NO es `--service-paths` con otro nombre: aquél lista los servicios HABILITADOS,
éste los paquetes INSTALADOS que declaran cuentas. `postgres` instalado y sin levantar necesita su
usuario igual, porque los ficheros de la imagen ya son suyos.

`scripts/servidor-image.sh` lo aplica entre hidratar el perfil y sellar la imagen, y aborta si hay
conflicto: un rootfs con el uid equivocado produce ficheros de un dueño que no existe.

Verde: 6 tests del módulo, core 234, cli 88, bootstrap 42, `targets.py --selftest` 7/7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
2026-09-14 15:39:52 +00:00
Sergio 3d79cdf98b SDD 30 4c: GNOME con los demonios supervisados por arje — y el control arregló colord
Los demonios de sistema pasan de lanzarse con `&` desde un script de 500 líneas
a ser Cards del `genesis`. El bloqueo que este frente daba por corpus se levantó
solo: otro agente selló evolution-data-server y gnome-shell mientras esto se
escribía, y escritorio-gnome quedó 309/309.

SE CORRIÓ UN CONTROL PRIMERO, y es lo único que hace interpretable el resultado:

  colord              control: `MURIÓ al arrancar`   cards: `ya vive (pid 175)`
  ColorManager        control: `NO apareció en 40s`  cards: `OK`
  login1/Accounts/UPower  OK en los dos
  compositor          wayland-0 y shell vivo en los dos

El fallo del control DESAPARECIÓ, y no lo buscaba: colord moría arrancado por el
script y vive arrancado por arje, con el bus ya listo porque la espera está
dentro de su argv. Sin el control, «ColorManager OK» sería un dato suelto en vez
de una diferencia. Los PIDs lo confirman: polkit=119, colord=175, upowerd=178,
accounts=180 — de antes de que el lanzador de sesión existiera.

QUÉ NO SE COMPROBÓ: no hay screendump; QEMU salió por timeout y el control
tampoco lo tuvo. La comparación es serial contra serial y lo que se afirma es
sobre los DEMONIOS, no sobre el pintado.

Las piezas donde corresponde: `takana service-cards` (UNA sola implementación de
receta→Card; el formato es contrato con card_core::Card), `targets.py
--service-paths` (une qué-es con si-arranca), y un inyector en FICHERO APARTE
porque anidar dos heredocs de python falló en vivo — el terminador del interno
cerró el externo y media cosa corrió como shell.

La espera del bus va DENTRO del argv de las 5 recetas de sistema: sin ella un
daemon arranca antes de que dbus escuche y queda en modo idle sin registrar su
nombre — un fallo que no se ve, porque el proceso vive y el bus no lo tiene. Los
5 hashes intactos. Y el guardia de gnome-start es por «¿está corriendo?», no por
una perilla: así es correcto venga de donde venga el proceso y la misma copia
sirve donde no se inyectaron cards.
2026-09-14 01:51:19 +00:00
SergioandClaude Opus 5 0a9622c7b0 kernel kexec: el reboot suave por kexec_file_load, sin kexec-tools
ADR 0017 §1. Se usa kexec_file_load, no el kexec_load viejo, y la
diferencia es de orden de magnitud: kexec_load recibe los segmentos YA
armados y el purgatory —el código que corre entre los dos kernels—, o
sea que usarlo obliga a reimplementar kexec-tools entero.
kexec_file_load recibe los DESCRIPTORES del kernel y del initrd y hace
el trabajo adentro. Son ~50 líneas.

El syscall va con asm! en vez de agregar la crate libc al workspace: es
UN syscall, su número y su ABI son contrato estable de Linux, y la
alternativa era arrastrar una dependencia a un workspace que comparten
dos frentes.

El cmdline viaja CON su NUL: el kernel cuenta cmdline_len incluyendo el
terminador, y sin él lee un byte de más. Es el error clásico de esta
llamada.

recipes/linux-metal-kexec.toml — variante cuya ÚNICA diferencia es
CONFIG_KEXEC_FILE=y. Variante y no flag en la canónica porque el .config
ES la identidad del artefacto (SDD 22 §1): tocar linux-metal re-hashea
el kernel que arranca las imágenes y el que reproduce bit a bit, y el
ADR deja esa decisión al usuario. Comprobado que la canónica no se
mueve: sigue en b3:2ed8f54a…, el que ya está sellado.

Y un diagnóstico que corregí a los dos minutos de escribirlo, porque
mandaba al lugar OPUESTO: decía "falta CONFIG_KEXEC_FILE" para EPERM.
Medido en este hub — el kernel de Artix trae CONFIG_KEXEC_FILE=y y aun
así devolvió EPERM, por no ser root. EPERM es falta de CAP_SYS_BOOT (o
lockdown si ya sos root); ENOSYS es el flag que falta. Confundirlos
manda a recompilar un kernel que estaba bien.

La ayuda del comando dice con todas las letras que esto NO es
"actualizar sin rebootear": el userspace muere igual. Ahorra los 20-30 s
de POST/UEFI, que es la mitad del downtime en un servidor remoto y nada
en un portátil.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-12 01:04:55 +00:00
SergioandClaude Opus 5 6642b93858 kernel A/B: BootNext hace la vuelta atrás, y la hace el firmware (ADR 0017 §2)
`takana kernel {stage,boot-status,confirm,rollback}`.

La idea que lo vuelve barato: hay DOS modos de fallo y sólo uno necesita
código nuestro.

  1. El candidato NO ARRANCA (pánico temprano, EFI-stub rechazado). Lo
     cubre BootNext y sale GRATIS: es una variable de UN SOLO USO que el
     firmware CONSUME al leerla. Si el kernel muere, el siguiente
     arranque ya no la encuentra, cae en BootOrder y vuelve al estable
     sin que corra una línea nuestra. No hay contador que mantener ni
     estado que se pueda corromper: el mecanismo es el firmware.
  2. Arranca pero el sistema no queda sano. Eso el firmware no lo sabe ⇒
     confirmación explícita, y cada arranque sin confirmar gasta un
     intento.

El candidato va a una ranura PROPIA de la ESP, nunca encima del estable:
un fallo a media copia dejaría sin kernel al que volver. La copia es tmp
+ rename.

Y `stage` REPONE el BootOrder después de crear la entrada: reconcile
deja al candidato primero, y un candidato no debe volverse el default —
tiene que arrancar UNA vez. Para eso está BootNext.

Verificado con DOS KERNELES REALES del store, tres arranques en OVMF:
  1º  6.16.12 estable, sin candidato → stage del 7.1.2
  2º  BdsDxe: starting Boot0004 "takana (candidato)" → uname = 7.1.2,
      " EN PRUEBA", confirm → promovido (y la ruta fallback también)
  3º  arranque normal → uname = 7.1.2: el estable cambió

Un bug que cazó un test y no una revisión: `en_esp` normalizaba los
backslashes DESPUÉS de quitar la barra inicial, así que una ruta en
formato EFI salía como "/EFI/..." —absoluta— y Path::join con absoluta
DESCARTA la base: habría escrito en el /EFI de la raíz del sistema en
vez de dentro de la ESP.

Y un hallazgo del banco de pruebas que vale para el diseño: con el
estado A/B en un tmpfs, stage + reboot lo perdía y el sistema decía "sin
candidato". Falló SEGURO (no promovió nada), pero dejaba la entrada
NVRAM huérfana sin que nadie lo supiera. boot-status ahora lo detecta
preguntándole al firmware, que es la fuente de verdad, y lo dice.

88 tests del CLI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-12 00:49:50 +00:00
SergioandClaude Opus 5 2b87ecb50f respaldo del arranque: lista blanca, porque BootCurrent rompía la idempotencia
Dos arranques reales seguidos, sin tocar nada, daban dos respaldos con
nombre distinto: boot-27b952c9….json y boot-f999e1e8….json. La promesa
de "un arranque que no cambió produce el mismo fichero" era falsa.

La causa: el filtro era «empieza por Boot», y eso deja entrar
BootCurrent, que dice por dónde arrancó ESTA vez y cambia en cada
arranque. Y encima es de SÓLO LECTURA, así que restore habría intentado
escribirla.

Ahora es lista blanca —BootOrder y Boot#### y nada más—, que también
deja fuera BootNext (de un solo uso: restaurarla dispararía un arranque
que nadie pidió) y BootOptionSupport (informativa). Ninguna de las tres
describe cómo debe arrancar la máquina.

Comprobado: cambiando BootCurrent a mano, el respaldo da el mismo
fichero y lo dice — "idéntico a uno que ya estaba: el arranque no
cambió".

Esto sólo se veía ARRANCANDO DOS VECES. Un test del respaldo contra un
efivarfs fabricado habría pasado en verde: la variable que rompía la
propiedad la pone el firmware, no el código.

80/80 del CLI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-12 00:37:13 +00:00
SergioandClaude Opus 5 bd1fa8bf8a boot entry backup/restore: el arranque como estado reproducible (ADR 0018 §5)
Guarda las variables de NVRAM CRUDAS —se reescriben tal cual;
decodificarlas para volver a codificarlas al restaurar sería una
oportunidad de perder algo que no entendemos— y de la ESP un manifiesto
con hashes, no los bytes: el kernel ya vive en el store y copiarlo otra
vez sería churn. El manifiesto es evidencia de qué había; para lo que no
esté en el store dice qué falta, en vez de prometer reponerlo.

El ADR se equivocaba en DÓNDE: decía "volcar al store". No va al store,
porque lo barre store-gc.sh, que clasifica por nombre de receta — un
respaldo ahí sería huérfano y se borraría en el primer --huerfanos. Vive
en /var/lib/hammer/boot/, que en las imágenes es su propia partición. El
nombre sale del CONTENIDO, así que un arranque que no cambió produce el
mismo fichero: corre en cada arranque sin llenar la partición de copias.

Lo que encontró la prueba de punta a punta y no estaba en el diseño:
restaurar es volver al pasado, y lo que llegó DESPUÉS no estaba en ese
pasado. Al reponer el BootOrder del respaldo, el Windows instalado más
tarde quedaba FUERA del orden — correcto, y justo lo que el usuario no
espera de algo llamado "restaurar el arranque". Ahora se avisa antes,
con nombre y apellido, y restore NO escribe por defecto: la NVRAM es lo
único de la máquina que no se rehace desde el store.

El lector FAT ganó lectura de ficheros, con su trampa propia: hay que
truncar al tamaño DECLARADO en el directorio, no al final del último
cluster. Un fichero de 1,5 MB en clusters de 1 KiB termina con relleno y
hashear el relleno daría un hash distinto al del mismo fichero en disco
— el síntoma sería "dos respaldos del mismo arranque difieren".
Verificado contra mtools como oráculo (1 500 000 y 900 000 bytes
exactos, mismo sha256 que los originales) y con un test determinista que
fabrica una FAT16 a mano, sin depender de que mtools esté.

79/79 del CLI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-12 00:34:21 +00:00
SergioandClaude Opus 5 2201fac08e boot entry survey: el informe del terreno, leyendo la ESP SIN montarla (ADR 0018 §4)
Reporta firmware, discos, ESPs, con quién se comparten y si la
instalación entra. No escribe nada — y para poder prometer eso hubo que
leer la ESP sin montarla, que es lo que obligó al lector FAT de sólo
lectura (fat_ro.rs). Montar para averiguarlo pedía privilegios, dejaba
un efecto secundario justo cuando prometimos no tocar nada, y falla si
el vecino dejó la FAT sucia por su hibernación.

Sobre una ESP de fábrica (100 MiB) con Windows dentro:

  FAT32 «ESP» — 100.0 MiB totales, 77.1 MiB usados, 22.1 MiB libres
  vecinos en \EFI: Microsoft, BOOT
    ⚠ «Microsoft» ⇒ hay Windows en esta ESP. Es el que reordena
      BootOrder al actualizarse.
  ✗ NO ENTRA: hacen falta 31.0 MiB y hay 22.1 MiB libres

Contrastado contra mtools como oráculo, que es lo que hace creíble el
número: mdir dice "23 221 248 bytes free" y el lector propio dice
22.1 MiB — el mismo. Los clusters libres se cuentan recorriendo la FAT y
NO se lee el FSInfo de FAT32 a propósito: ese campo lo deja
desactualizado un SO que desmontó mal, y un número optimista de más
haría fallar la instalación a mitad — justo lo que el §4 evita.

En el instalador el §4 resultó ser algo más que "cuánto espacio hay": la
rama UEFI se lleva el disco ENTERO, así que lo que hay que decir en voz
alta es con qué se lo va a llevar puesto. El survey corre antes de
particionar y nombra el \EFI\Microsoft si está. El usuario se entera
ANTES, y no después de que su Windows dejó de arrancar.

Dos cosas que habrían pasado inadvertidas sin test: el nombre largo
tiene que ganarle al 8.3 (sin juntar los LFN, "Microsoft" se lee
"MICROS~1" y la advertencia no dispara nunca), y el tipo de FAT sale del
número de clusters y no del texto del BPB, que es informativo y hay
formateadores que mienten. El primer test del tipo lo escribí mal —1999
clusters ES FAT12— y el síntoma es idéntico a un bug del lector.

78/78 del CLI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-11 22:55:46 +00:00
SergioandClaude Opus 5 66eaf8738f el reconciliador ARRANCA solo: montar sysfs, y un diagnóstico que no mentía a medias
Cierra el §2 del ADR 0018, verificado con dos arranques de la imagen
completa en OVMF:

  1º (por la fallback) → "ESP detectada en /dev/vda p1",
     "⚠ EL ARRANQUE ESTABA CAMBIADO — takana lo repuso",
     "faltaba la entrada «takana» ⇒ escrita en Boot0004",
     "BootOrder: 0000,0001,0002,0003 → 0004,0000,0001,0002,0003"
  2º → BdsDxe: starting Boot0004 "takana" from HD(1,GPT,923A070F-…)
       /\EFI\takana\takanax64.efi
       "✓ arranque en orden: Boot0004 «takana» ya es la primera"

Un sistema recién instalado se da de alta en el firmware en su PRIMER
arranque y desde el segundo arranca por su propia entrada, sin que nadie
corra un comando. Y el segundo no escribe nada.

Lo caro no fue el reconciliador sino un diagnóstico FALSO: el primer
arranque con el hook dijo "sin firmware EFI — esta máquina no arrancó
por UEFI" en una VM que SÍ arrancó por UEFI. La causa no tenía nada que
ver con UEFI: busybox switch_root no arrastra /sys —igual que no
arrastra /dev, cosa que el script ya contemplaba— así que el directorio
de efivars no existía. El mensaje mandaba a investigar el firmware, que
estaba perfecto.

Dos arreglos:
- el wrapper monta sysfs y después efivarfs. CONFIG_EFIVAR_FS=y ya
  estaba en el .config SELLADO (verificado, no supuesto) ⇒ no se
  re-hashea ningún kernel.
- el mensaje distingue TRES estados que se parecen: no existe (BIOS o
  /sys sin montar), existe y está VACÍO (falta el mount, y lo dice con
  el comando exacto), o tiene variables. Decir "no hay UEFI" cuando
  falta un mount manda a diagnosticar al lugar equivocado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-11 22:25:12 +00:00
SergioandClaude Opus 5 236abe48f3 boot entry reconcile: converger con la NVRAM en CADA arranque (ADR 0018 §2)
La NVRAM es estado compartido y el vecino la reescribe sin coordinarse.
Contra eso no sirve confiar: sirve converger. `reconcile` descubre su
propia ESP, repone la entrada y el BootOrder, y es idempotente.

Descubre la ESP por TIPO de partición (GUID de ESP en GPT, 0xEF en MBR),
que es el único dato fiable sin montar nada — un reconciliador que monta
sistemas de ficheros en el arranque es un efecto secundario que no
queremos. Si hay VARIAS ESP no adivina: las lista y pide --disk. Elegir
mal significa escribir una entrada que apunta a un disco que puede no
estar, y eso es peor que no escribir nada.

No rompe el arranque por nada: sin firmware EFI (máquina por BIOS) lo
dice y sale 0.

Y cuando actúa, GRITA — va a /dev/tty0 además del serial, a diferencia
del menú de arranque. Un reconciliador que repara en silencio deja al
usuario conviviendo con una rareza intermitente que no entiende; el
mensaje dice explícitamente que si se repite en cada arranque es que
otro sistema operativo le está reescribiendo la NVRAM.

Enganchado al wrapper de PID1 de las dos imágenes, con el mismo timeout
y el mismo || true que el menú: corre ANTES del exec de arje-zero y
colgarse ahí es un arranque muerto e indistinguible de un kernel colgado.

El test que vale es el escenario completo: takana se instala, llega el
vecino y se pone primero, y el siguiente arranque lo repone — SIN borrar
la entrada del vecino (takana se pone primera, no lo echa) — y el
arranque siguiente ya no escribe nada. Más el GUID de ESP, que va en
orden DE DISCO y no en el legible: escribirlo "como se lee" es el error
clásico y no casaría con ninguna ESP real.

15 tests en el módulo, 74/74 del CLI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-11 22:18:16 +00:00
SergioandClaude Opus 5 ba5754551f takana boot entry: la entrada NVRAM, sin efibootmgr — con el muro medido
ADR 0018 §1. El ADR dejaba dos vías y se intentó primero la ajena:
efivar 38 con musl/zig-cc choca con secure_getenv (no existe en musl),
sys/cdefs.h (tampoco) y -Wl,--add-needed (lld lo rechaza) — tres muros
sólo para compilar su generador de tablas, y arrastraría popt al sistema
instalado. Es el patrón "pantano de parches de distro".

Lo que efibootmgr hace en el fondo es escribir DOS ficheros, Boot#### y
BootOrder, con una estructura de la spec UEFI estable desde 2.0. Hacerlo
acá es menos código que mantener los parches, no agrega dependencias al
sistema instalado y el binario takana ya está en el disco.

crates/takana-cli/src/efi_boot.rs — EFI_LOAD_OPTION + device path
(HARD_DRIVE/FILE_PATH/END), lectura y escritura de efivarfs, y un lector
de tabla de particiones que detecta GPT o MBR solo. Los dos casos hacen
falta: install-image-efi.sh fabrica GPT y takana-live-install.sh instala
sobre MBR.

`takana boot entry {list,add}` (CLI en inglés, regla 4). `add` es
IDEMPOTENTE a propósito: está pensado para correr en CADA arranque como
reconciliador (§2), y la NVRAM tiene un número finito de escrituras, así
que si ya dice exactamente eso no se reescribe.

parse_load_option existe para poder COMPROBAR al constructor: un device
path mal formado no da error, el firmware ignora la entrada en silencio
y el usuario ve "no arranca" sin una sola pista.

10 tests, con los negativos que son los que valen: un disco sin 0x55AA
falla en vez de devolver ceros, una entrada GPT vacía no se convierte en
partición de tamaño 0, y un FilePathListLength que miente es error y no
un truncado silencioso. 69/69 del CLI en verde.

Verificado además contra una tabla GPT REAL de sfdisk: inicio LBA 2048 y
131072 sectores, los mismos números que reporta sfdisk -l.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-11 21:26:46 +00:00
SergioandClaude Opus 5 9bc518899e swm: strip_debug no viajaba en el paquete — y es ENTRADA DE HASH, así que 40 recetas no instalaban
Encontrado instalando `takana` desde su propio repo (SDD 28 §5), que es la primera vez que se hace
el viaje completo receta → `.tkn` → `install --require-signed` sobre un paquete con `strip_debug`.

    Error: expected_hash no coincide:
      declarado = b3:941d1857e6e93ebbfbad8240a1c30405263e5ff8fc881a923aee312160563eea
      obtenido  = b3:2727050ebdbd7817b53e79ffe9796569e9364454ed35474b51e881fcc6048766

`why-differs` sobre los dos artefactos lo nombró: las recetas selladas diferían en `version`,
`license` y **`strip_debug`**. Los dos binarios pesaban EXACTAMENTE lo mismo (3.374.768 bytes) y sólo
divergían en `.shstrtab` — la firma de un `strip` que corrió una vez y la otra no.

`swm_bridge` re-inyecta con cuidado `flags`, `phases`, `zig_version`, `strip_components`, `patches`,
`deps`, `evidence` y `slots` («fidelidad de reconstrucción», dice su comentario) y **se olvidaba de
`strip_debug`**, que ni siquiera existía en `SwmBuild` ⇒ no viajaba en el `.swm` en absoluto. Como
ENTRA EN `hash_inputs` (`recipe.rs:572`, con el comentario «cambia el CONTENIDO del artefacto, así
que TIENE que entrar»), el receptor reconstruía con `None` y sellaba en otra dirección.

**Alcance medido: 40 recetas del corpus lo usan, 18 de las 88 del perfil `servidor`.** Para todas,
el `expected_hash` anclado por `pack --build` no coincidía NUNCA y `install --require-signed`
abortaba. O sea: una quinta parte del repo no se podía instalar, y nadie lo sabía porque nunca se
había cerrado el lazo.

Por qué no lo cazó nadie antes: `tree` —sin `strip_debug`— da cache-hit y funciona perfecto. El bug
sólo aparece en las recetas que lo declaran, y el dogfood previo no las tocaba.

Test de regresión con su control: una receta CON `strip_debug` lo lleva en el `.swm`, y una SIN él no
gana un `false` inventado. 221 tests de takana-core en verde.

Verificado end-to-end tras el arreglo: `install takana --repo http://<caja> --require-signed` da
cache-hit en `b3:941d1857…` —el mismo hash anclado— e hidrata los 3 ficheros.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-10 21:33:45 +00:00
Sergio e20a5f44d8 takana: --takana-bin, con --hammer-bin como alias
Era la ultima superficie de CLI que quedaba con el nombre viejo. Mismo patron
que forja: el canonico es el nuevo, el viejo sigue aceptandose como alias.
El default pasa a .../release/takana.

El DESTINO no se toca: el binario se sigue instalando en /usr/bin/hammer dentro
del rootfs del producto, porque ese rootfs se hashea. Moverlo cambia el hash del
producto y obliga a rehacer el baseline del selfhost — es etapa 6 y decision
aparte, no efecto colateral de renombrar un flag.

Verificado en el subcomando correcto (bootstrap builder, no product): las dos
formas se aceptan. 605 tests en verde.
2026-09-09 19:43:05 +00:00
Sergio b818f5249f takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja
205 lineas en 73 ficheros de crates, mas la prosa de CLAUDE.md y del skill,
que se me habian quedado afuera de los barridos anteriores (no eran ni recetas
ni docs/ ni scripts/).

EL BARRIDO ANCHO ESTUVO A UN COMMIT DE ROMPER EL CORPUS ENTERO.

El primer intento reescribia los .rs completos, no solo los comentarios. Entre
las lineas de codigo que tocaba estaban SIETE etiquetas de separacion de
dominio, que son ENTRADA DE HASH:

  b"hammer-tree-v1"        <- el prefijo de ArtifactHash::of_tree (hash.rs:60)
  b"hammer-seed-v1"           la funcion que hashea TODOS los artefactos:
  b"hammer-stage1-rootfs-v2"  cambiarla mueve los 4750 hashes del store
  b"hammer-product-rootfs-v3"
  b"hammer-product-attested-v2"
  b"hammer-builder-rootfs-v1"
  b"hammer-attest-dev-rootkey-0001!!"  <- clave raiz de atestacion, [u8;32]

Revertido y rehecho solo sobre comentarios, esquivando ademas las cadenas
crudas de Rust (r#"..."#) porque el SYSTEM_PROMPT del traductor tiene lineas
que empiezan como comentario.

Controles: las 7 etiquetas siguen ahi, el diff toca CERO lineas de codigo,
605 tests en verde y el hash de zlib sigue en b3:dc363f26.

La leccion es la misma de toda esta etapa: un literal que parece prosa puede
ser entrada de hash, y la unica forma de saberlo es mirar donde se usa.
2026-09-09 19:38:33 +00:00
Sergio 1c3e185167 takana: las variables de entorno leen las dos formas, gana la nueva
TAKANA_X con caída a HAMMER_X (ADR 0016). No es un sed: estas variables son
contrato de usuario —knobs del instalador, el entorno del worker, mirror-env.sh—
y viven en perfiles de shell y units FUERA del repo, así que un renombre duro no
falla ruidosamente: la variable no aparece, se toma el default y el build se
comporta distinto sin que nada lo diga.

Rust: takana_core::env::{var,var_os} toma el nombre canónico y deriva el viejo
cambiando el prefijo — se le pasa TAKANA_* para que un grep del nombre nuevo
encuentre todas las lecturas. 5 tests, con DOS controles negativos: sin ninguna
de las dos no hay valor, y un nombre sin prefijo no inventa una caída.
Migrados los 17 sitios directos y los indirectos que el grep no mostraba
(bases_de_mirror, las constantes de kernel_cmd, ROOT_ENV de qorpa, env_path de
recover). takana-recover lleva la caída inline: es un mini-binario que se copia
a /usr/sbin y no vale arrastrarle una dep entera por dos líneas.

Scripts: 30 lecturas pasan a default, manteniendo el
nombre INTERNO de la variable para no tocar sus 190 usos.

Y donde el script EXPORTA en vez de leer, se ponen LAS DOS (mirror-env.sh y el
fragmento in-VM de bootstrap): ahí el lector puede ser un binario viejo —worker
sin recompilar, el /usr/bin/hammer pinado del baseline— que sólo conoce HAMMER_*.
La caída sirve para lectores nuevos; los viejos necesitan que la vieja siga puesta.

Verificado de punta a punta con el binario, no sólo con unit tests: HAMMER_LAB
sigue surtiendo efecto, TAKANA_LAB hace lo mismo, y con las dos gana TAKANA_LAB.
605 tests en verde y el hash de zlib sigue en b3:dc363f26… , intacto.

Cambio de comportamiento que va aparte y hay que decir: el hostname por defecto
de una instalación nueva pasa de 'hammer' a 'takana' (sólo si no se fija ninguna
de las dos variables).

NO se tocan: las rutas /var/lib/hammer de sistemas instalados, el volid
HAMMER_LIVE del ISO, ni el namespace HARKAQ_*, que es de otro subsistema.
2026-09-09 19:14:49 +00:00
Sergio 24bcf1783c takana etapa 4: los 10 crates de librería y el CLI pasan a takana-*
hammer-{core,build,bootstrap,overlay,journal,mirror,upgrade,agent,recover,cli}
→ takana-*, con sus deps de workspace, sus identificadores en el fuente y las
referencias -p de los scripts.

VERIFICADO que no mueve nada del corpus: `takana hash recipes/zlib.toml`
devuelve b3:dc363f26… , idéntico a antes del renombre. Los nombres de crate no
entran en hash_inputs, pero eso se comprueba, no se supone. 600 tests en verde.

DOS BINARIOS SE CONGELAN, y no por prolijidad:

- `hammerd` — paquete Y binario. Es componente de Stage 1 de la distro (musl,
  busybox, hammerd, arje-zero), lo supervisa arje-zero en el sistema arrancado,
  `PRESEED=hammerd` lo nombra en selfhost-verify y sus bytes anclan el baseline
  of_tree. El nombre del crate va en los símbolos ⇒ renombrarlo mueve los bytes.

- `hammer-recover` — el PAQUETE se renombra a takana-recover, el BINARIO no.
  hammer-live-install.sh lo copia a /usr/sbin/hammer-recover en sistemas ya
  instalados y hornea un hook de arranque que lo invoca por ese nombre:
  renombrarlo rompe máquinas instaladas, no el repo.

Consecuencia que hay que anotar igual: al renombrar hammer-core, los bytes de
hammerd cambian de todos modos porque linkea contra un crate con otro nombre.
El baseline of_tree del selfhost hay que rehacerlo — es efecto de la etapa 4,
no de un cambio de hammerd.

Las referencias en comentarios de recetas y docs (rutas hammer-core/src/…)
quedan para la etapa 5: son texto, no mueven hash.
2026-09-09 18:46:41 +00:00