26c153c149c81663140768bfda7dafb19c010e4f
22
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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.
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|