# Runbook — validar Stage 1 booteando en QEMU > Operativo, no diseño. Valida end-to-end que el rootfs que produce > `hammer bootstrap stage1` ([SDD 11](../11-bootstrap.md)) **bootea con arje-zero como PID 1** > ([SDD 12](../12-init-real.md)), levanta `hammerd` + getty bajo supervisión, y que el **`CRASHED` > real** funciona. > > **Esto es un procedimiento, no un transcript verificado.** La lógica de ensamblado/hash/seed está > cubierta por unit tests (y la seed se validó contra `card_core::Card`), pero el cross-compile y el > boot reales **no** se han corrido todavía — ese es justo el punto de este runbook. La primera > corrida puede destapar ajustes (link, `/dev/console`, getty/tty); §7 los anticipa. ## 0. Lo que valida / lo que no - **Valida:** que el árbol sellado por `stage1` arranca como sistema: arje-zero monta los pseudo-FS, carga la seed, incarna `hammerd` y la getty, y al matar `hammerd` lo reinicia con backoff (el `CRASHED` que la Fase 5 difirió). - **No valida (aún):** bus único (B.2), overlay del store (I4), atestación (A1/A2), kernel from-source. Aquí el kernel es prestado del host; el overlay no se monta. ## 1. Prerrequisitos - **Lab de dev** montado: `./scripts/bootstrap-devfs.sh` (deja `.dev-fs/alpine` + `.dev-fs/tools/zig`). - **Red**: el build de `hammerd` y `arje-zero` clona repos git (hammer, tawasuyu) y corre `cargo vendor` en el fetch. El build hermético del sandbox es `--offline`, pero el fetch necesita red. - **Herramientas host**: `qemu-system-x86_64`, un kernel x86_64 (`/boot/vmlinuz-*`), `cpio`, `gzip`. - **Espacio/tiempo**: construir `arje-zero` vendorea las deps del **monorepo tawasuyu entero** — es pesado y lento la primera vez (se cachea después). - **El binario `hammer`**: `cargo build --release -p hammer-cli` ⇒ `./target/release/hammer`. > El store debe vivir en la raíz del repo (`./store`) para que `BuildConfig` resuelva > `.dev-fs/alpine` como hermano (`store/..` = repo root). El toolchain de Stage 1 **no** es el de > `.dev-fs`: es la semilla de Stage 0 (paso 2), que `stage1` enchufa como `zig_dir` del sandbox. ## 2. Stage 0 — ingerir la semilla (zig) ```sh export HAMMER=./target/release/hammer export STORE=./store "$HAMMER" --store "$STORE" bootstrap stage0 \ --url https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz \ --sha256 70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 \ --version 0.16.0 # → imprime el seed_hash (b3:...). Guardalo: SEED="b3:..." # pegá el hash impreso ``` Idempotente: re-correr no re-descarga. La semilla queda sellada como `seed-zig` en el store y anota su línea en `bootstrap.json`. ## 3. Stage 1 — construir y sellar el rootfs ```sh "$HAMMER" --store "$STORE" bootstrap stage1 --seed-hash "$SEED" --recipes recipes # construye musl + busybox + hammerd + arje-zero, ensambla el rootfs FHS y lo sella. # → imprime el rootfs_hash (b3:...). # El árbol sellado (read-only) del rootfs: ROOTFS_DIR=$(ls -d "$STORE"/*-stage1-rootfs) echo "$ROOTFS_DIR" ls "$ROOTFS_DIR"/ente/seed.card.json "$ROOTFS_DIR"/sbin/init "$ROOTFS_DIR"/usr/bin/{hammerd,arje-zero} ``` Necesita red (git clone + `cargo vendor`). Si un build Cargo falla en el link (`-lgcc_s`) o por static-PIE, ver §7 — el `zig cc` del lab debería resolver `-lgcc_s` (bitácora M2 del plan arje↔hammer). ## 4. Empaquetar el rootfs como initramfs El árbol del store ya trae `/sbin/init`→`/usr/bin/arje-zero` y `/ente/seed.card.json`. Se empaqueta como cpio `newc` + gzip (formato initramfs): ```sh ( cd "$ROOTFS_DIR" && find . -print0 | cpio --null -o --format=newc ) | gzip -9 > /tmp/stage1.cpio.gz ls -lh /tmp/stage1.cpio.gz ``` `cpio newc` preserva symlinks y hardlinks. El symlink `/sbin/init`→`/usr/bin/arje-zero` es absoluto: en un initramfs resuelve dentro de su propia raíz. (Si el kernel no auto-monta `devtmpfs`, ver §7 por el nodo `/dev/console`.) ## 5. Bootear en QEMU ```sh qemu-system-x86_64 -m 512 -nographic \ -kernel /boot/vmlinuz-linux \ -initrd /tmp/stage1.cpio.gz \ -append "console=ttyS0 rdinit=/sbin/init" ``` - `rdinit=/sbin/init` — el default de un initramfs es `/init`; lo sobreescribimos a `/sbin/init` (→arje-zero). Alternativa directa: `rdinit=/usr/bin/arje-zero`. - `console=ttyS0` + `-nographic` — consola serie, donde arje escribe y la getty se engancha. - Salir de QEMU: `Ctrl-A` luego `X`. ## 6. Criterios de éxito (qué observar) 1. **PID 1**: en kmsg/consola, arje-zero loguea *"despierta como PID 1"*. 2. **Pseudo-FS**: monta `/proc /sys /dev` sin error fatal (best-effort; un `EBUSY` se ignora). 3. **Seed**: carga `/ente/seed.card.json` sin *"card inválida"* (ya validada contra `card_core::Card`). 4. **hammerd**: arranca como Ente — su tracing aparece y/o crea `/run/agent.sock`. 5. **getty**: aparece un prompt de shell (`/bin/sh`) en `ttyS0`. 6. **CRASHED real** — la prueba clave. En el shell: ```sh kill "$(pidof hammerd)" ``` arje registra el `on_death(ExitStatus)` y **reinicia hammerd tras el backoff** (200 ms → ×2…). Eso es el `CRASHED` que la Fase 5 dejó diferido, ahora manejado por el init propio. ## 7. Troubleshooting (síntoma → causa → fix) | Síntoma | Causa probable | Fix | |---|---|---| | `Kernel panic … no init found` | `/sbin/init` no ejecuta | probar `rdinit=/usr/bin/arje-zero`; verificar que el symlink quedó en el cpio (`cpio -tv`) | | arje cae a rescue shell *"card inválida"* | la seed no validó | confirmar que `/ente/seed.card.json` se empaquetó; comparar con el schema de `seeds/arje-qemu.card.json` | | hammerd/arje-zero: `exec format error` | binario no estático / falta loader | confirmar `link = "static"`; un static-PIE pide `/lib/ld-musl-x86_64.so.1` — para init-clásico desactivar PIE (`-C relocation-model=static`, bitácora M2) | | Build Cargo falla en `-lgcc_s` | hueco del toolchain musl dinámico | el lab usa `zig cc` (empaqueta su `compiler-rt`) → no debería aparecer; verificar `compiler = "zig-cc"` en la receta | | getty sin prompt | device `console` no engancha tty | probar `argv` con `ttyS0` en vez de `console`; revisar que `/dev/console` existe | | `No working init` / sin `/dev/console` | kernel sin auto-mount de devtmpfs | crear el nodo en el cpio (`mknod -m 600 dev/console c 5 1` antes de empaquetar) o usar un kernel con `CONFIG_DEVTMPFS_MOUNT=y` | | `cargo vendor` / git clone falla | el fetch necesita red | correr Stage 1 con red; el sandbox de build sigue siendo `--offline` | ## 8. Bitácora — primera corrida real (2026-06-11) Corrida en el host de desarrollo (Artix). **Resultado: el userland C builda end-to-end; el camino Rust está bloqueado por una decisión de toolchain.** Hallazgos, en orden: | # | Etapa | Resultado | |---|---|---| | 1 | Lab (`bootstrap-devfs.sh`) | ✅ Alpine + zig 0.16.0 + 43 build-tools | | 2 | Stage 0 (ingerir zig) | ✅ `seed_hash` sellado, idempotente | | 3 | **musl 1.2.5** | ✅ cross-compila con zig cc y se sella — **el cross-compile C hermético funciona** | | 4 | **busybox 1.36.1** | ✅ tras tres fixes (abajo); compila y **linkea** con zig cc, se sella | | 5 | **hammerd** (Cargo) | ❌ bloqueado: triple de toolchain (abajo) | | 6 | arje-zero (Cargo) | ⏸ no alcanzado (mismo bloqueo; además vendorea el monorepo entero) | **Fixes aplicados a `recipes/busybox.toml`** (incompatibilidades GNU-toolchain ↔ zig, esperadas): 1. **`gcc` hardcodeado.** El Kbuild de busybox fija `CC = gcc` / `HOSTCC = gcc` con `=` (ignora el env `CC` del sandbox). Fix: pasar `CC='zig cc' HOSTCC='zig cc'` en **línea de comando** de make. 2. **Flags GNU-ld que zig cc rechaza.** El link pasa `-Wl,--warn-common`, `-Wl,-Map,…`, `-Wl,--verbose`; `zig cc` valida los `-Wl,` y aborta. Fix: quitarlos de `scripts/trylink` con sed. 3. (`gcc` para herramientas host como `fixdep` y `gcc-version.sh` lo cubre el mismo `HOSTCC`.) **Bloqueo del camino Rust — decisión de toolchain (plan C.2):** - El vendoring (Lote 6) corre OK en el fetch (host, con red). El `cargo build` corre en el sandbox. - Se añadió `rust cargo` al rootfs (apk) — pero el rust de Alpine tiene host triple **`x86_64-alpine-linux-musl`**, mientras `BuildSys::Cargo` pide **`--target x86_64-unknown-linux-musl`**. Alpine no trae ese std y no hay rustup → `error[E0463]: can't find crate for core`. - **Opciones (a decidir):** (A) construir nativo sin `--target` (el sandbox ya es x86_64 musl) — cambio en `BuildSys::Cargo`; (B) recetas Cargo con `target = "x86_64-alpine-linux-musl"` — leakage de Alpine; (C) rustup en el sandbox + `rustup target add` — pierde hermeticidad/pin. Es la misma tensión de "pin de toolchain entra al hash" que el plan C.2 dejó abierta. ### Stage 1 completo + intento de boot Tras los fixes, **los 4 componentes buildan y el rootfs se sella** (musl+busybox+hammerd+arje-zero, 29 MB; `/sbin/init`→arje-zero, `/ente/seed.card.json`, loader musl). Se empaquetó como initramfs (`cpio newc`+gzip, 11 MB) y se booteó en QEMU. La cadena de hallazgos del boot: 1. **AVX.** El CPU default de QEMU (`qemu64`) no tiene AVX, pero los binarios (zig/rust nativos) emiten `vxorps` → `SIGILL` (`invalid_op`) en el loader → init muere. Fix: `-cpu Broadwell` (AVX2 bajo TCG; sin `/dev/kvm` no hay `-cpu host`). Para un distro real: compilar a un baseline x86-64 genérico, no al CPU del builder. 2. **musl dinámico.** El rust de Alpine produce binarios **dinámicos** (`DT_NEEDED libc.musl-x86_64.so.1`), no crt-static. El loader corre pero no resuelve `memcpy/memset/log2/…`: nuestro `libc.so` (musl 1.2.5 shared con zig cc) tiene 1586 dyn-syms pero **no exporta** esos símbolos. → `Error relocating /sbin/init: memcpy: symbol not found` → init muere. **Conclusión.** El bootstrap **C** (musl+busybox+zig cc) y el **build Rust nativo** (hammerd y arje-zero compilan y se sellan) están **validados** end-to-end; el kernel arranca el initramfs y ejecuta arje-zero como PID 1. El último bloqueo del boot es el **linking dinámico de musl**: el fix correcto y golden-path es **crt-static** — compilar los componentes Rust con `-C target-feature=+crt-static -C relocation-model=static` para binarios **autocontenidos** (sin `libc.so`, sin loader, sin soname, como busybox). Requiere rebuild de hammerd/arje-zero (el de arje-zero vendorea el monorepo: caro en tiempo y disco). Es la realización plena de la "vía de oro" estática de hammer y cierra de paso el problema del soname de Alpine. **Actualización (Opción A implementada).** `BuildSys::Cargo` ahora construye **nativo** sin `--target` cuando el target es el del sandbox (`SANDBOX_NATIVE_TARGET`), con un wrapper `.hammer-zig-cc` que (a) resuelve el `zig cc` de dos palabras para cargo/cc-rs y (b) **sanea el triple** que cc-rs pasa (`--target=x86_64-alpine-linux-musl` → `x86_64-linux-musl`, que zig sí entiende). Resultado: **hammerd compila nativo y se sella ✓** — el camino Rust funciona. **arje-zero queda bloqueado** por otra razón: **tawasuyu no commitea `Cargo.lock`** (`git ls-files Cargo.lock` vacío), así que el `git archive` no lo trae y `cargo vendor --locked` no puede crearlo. Decisión pendiente: commitear el lock en tawasuyu (reproducible, plan C.2 #5) vs relajar `--locked` en el vendoring de hammer. **Cierres posteriores.** (1) Vendoring `--locked` **condicional** al lock committeado → arje-zero vendorea sin `--locked` (genera el lock) y **se sella**: los 4 componentes buildan. (2) Boot: AVX → `-cpu Broadwell`; y el linking **dinámico** de musl (Alpine rust es dinámico por defecto → `DT_NEEDED libc.musl-x86_64.so.1`; nuestro `libc.so` no exporta `memcpy/…`) → fix **crt-static**. (3) Iteración del crt-static (cada paso destapó el siguiente; hammerd valida barato): `+crt-static [-C relocation-model=static]` en **RUSTFLAGS** rompe los proc-macros (dylibs PIC) en build nativo → **solución:** `cargo rustc --release … -- `, que aplica los flags sólo a la crate top-level. musl vuelve a `--disable-shared` (no se necesita loader). ### ✅ BOOT LOGRADO (2026-06-11) Con `cargo rustc -- +crt-static -C relocation-model=static`, `arje-zero` y `hammerd` salen **`statically linked` (ET_EXEC, sin `DT_NEEDED`, sin interpreter)**, el rootfs se sella, y el boot en QEMU (`-cpu Broadwell`) **levanta el sistema**: ``` arje_zero: ente-zero despierta como PID 1 arje_zero::seed: Tarjeta Semilla cargada y validada path=/ente/seed.card.json arje_zero: Ente #0 entra al bucle primordial label=hammer-stage1 arje_zero::graph::lifecycle: instanciando genesis count=2 Ente encarnado label=hammerd pid=Some(Pid(59)) Ente encarnado label=console-getty pid=Some(Pid(60)) ~ # (shell en consola) hammerd: arranque agent_sock=/run/agent.sock … hammerd: watcher fanotify activo hammerd::bus: escuchando sock=/run/agent.sock ``` **arje-zero es PID 1**, carga+valida la seed y **supervisa hammerd + getty**; hammerd corre con su watcher fanotify y su `agent.sock`. **Stage 1 bootea end-to-end.** Comando reproducible: `qemu-system-x86_64 -m 512 -no-reboot -nographic -cpu Broadwell -kernel -initrd stage1.cpio.gz -append "console=ttyS0 rdinit=/sbin/init"`. ### ✅ `CRASHED` real demostrado (lo que la Fase 5 difirió) Desde la shell de la consola, `kill $(pidof hammerd)` y arje-zero lo supervisa de verdad: ``` kill $(pidof hammerd) arje_zero::graph::lifecycle: Ente disuelto label=hammerd status=Killed(SIGTERM) arje_zero::graph::lifecycle: Restart programado label=hammerd delay_ms=200 arje_zero::graph::lifecycle: Ente encarnado label=hammerd pid=Some(Pid(64)) hammerd: arranque agent_sock=/run/agent.sock … ``` Detecta la muerte **con su `ExitStatus`** (`Killed(SIGTERM)`), programa el restart con **backoff** (200 ms) y **re-encarna** hammerd (pid 59→64). Es la supervisión real del init propio — el `CRASHED` que [ADR 0007](../adr/0007-arje-como-init-propio.md) prometía y la Fase 5 había diferido. Exponerlo a la capa de IA por `agent.sock` es B.2 (bus único). ## 8b. Stage 2 — verificación de reproducibilidad (pre-rebuild-in-rootfs) El `stage2` de hammer ancla el **content-hash** (`ArtifactHash::of_tree`, los bytes reales) del rootfs y lo compara con un rebuild. El rebuild **dentro** del rootfs usando *sólo* las herramientas de Stage 1 (auto-alojamiento pleno) necesita un **builder rootfs** — el Stage 1 mínimo es un *runtime* (no trae zig/make/cargo), así que ese paso es un hito aparte. Lo que **sí** se verificó acá: la **bit-reproducibilidad del build** (dos builds independientes del mismo componente → `diff -r`): | Componente | Reproducible | Notas | |---|---|---| | **musl** 1.2.5 | ✅ byte-idéntico | el build C con zig cc es determinista de fábrica | | **busybox** 1.36.1 | ✅ tras dos fixes | (ver abajo) | | **hammerd** (Rust) | ✅ byte-idéntico | crt-static + cargo `--locked` + `SOURCE_DATE_EPOCH` + paths fijos `/src` ⇒ rust reproducible | | **arje-zero** (Rust) | ✅ (indirecto) | mismo camino que hammerd; su único riesgo era el lock generado sin `--locked` — verificado: dos `cargo generate-lockfile` del commit pinned dan un `Cargo.lock` **byte-idéntico** (resolución determinista). Committear el lock lo haría explícito (plan C.2 #5) | **La verificación cazó dos no-determinismos reales en busybox** (justo lo que [SDD 09 §2](../09-trust-model.md) pide): 1. **Config interactiva.** Tras editar `.config`, el compile corre `silentoldconfig`, que ante opciones NEW (WERROR, etc.) **prompea leyendo stdin** → el set de applets salía distinto por corrida (build no-determinista *y* flaky). Fix: `yes '' | make oldconfig` (acepta defaults, determinista; busybox 1.36.1 no tiene `olddefconfig`). 2. **Headers del kernel.** Con el config determinista, los applets de `console-tools` exigen `linux/kd.h` (UAPI) que zig **no** bundlea → `linux-headers` en el lab (`bootstrap-devfs.sh`); zig cc nativo los encuentra en `/usr/include`. Con ambos, `musl` y `busybox` reconstruyen **bit-idéntico**. Sumados `hammerd` (directo) y `arje-zero` (indirecto), **los 4 componentes están verificados reproducibles**. Lo único que le queda a Stage 2 es el rebuild *dentro* de un **builder rootfs** (un Stage 1 que incluya el toolchain) — el auto-alojamiento pleno, un hito propio (el Stage 1 mínimo es runtime, sin compilador). ## 8c. Builder rootfs — el rebuild in-rootfs (auto-alojamiento, SDD 11 §7) El **ensamblado** del builder ya está implementado (`hammer bootstrap builder`, [SDD 11 §7.4](../11-bootstrap.md); variante a, toolchain desde Alpine). Produce, sobre el Stage 1 rootfs, una imagen con el toolchain adentro lista para reconstruirse a sí misma. Receta operacional: **1. Binario `hammer` estático (musl).** El builder lo bootea como PID-algo en la VM, así que debe ser autocontenido (como hammerd/arje-zero, crt-static — §7): ```sh cargo build --release --target x86_64-unknown-linux-musl -p hammer-cli # (o cargo rustc -- -C target-feature=+crt-static, según el camino de oro del boot) ``` **2. Ensamblar el builder** anclando la referencia `of_tree(stage1)` (la que imprimió `stage2`): ```sh hammer --store store bootstrap builder \ --stage1 b3: --seed-hash b3: \ --hammer-bin target/x86_64-unknown-linux-musl/release/hammer \ --toolchain .dev-fs/alpine --toolchain-tag alpine-3.23.4 \ --ref-content b3: \ --work-cache work \ --out work/builder-rootfs ``` `--work-cache work` embebe sólo `work/repos` (mirrors git) y `work/tarballs` (descargas verificadas) en `/work` ⇒ rebuild **offline y determinista** en la VM (de ahí `fetch` rematerializa los árboles fuente). **No** copia `work/sources` (los árboles ya materializados): es regenerable y pesa varios GB. Sin `--work-cache`, el rebuild fetchea por red (la seed card trae `networking: full`). **3. Empaquetar como initramfs y bootear** (igual que §4/§7, `-cpu Broadwell` por AVX). El script `scripts/boot-builder-vm.sh` encapsula esto (kernel, MEM, KVM configurables): ```sh # --owner=root:root es OBLIGATORIO: si los ficheros viajan con el uid del que armó el builder # (p. ej. 1000), ese uid no está mapeado en el userns de bwrap (mapea 0→0) y el copy-up de overlay # del sandbox falla con EACCES (`Can't mkdir /opt/zig: Permission denied`). ( cd work/builder-rootfs && find . -print0 | cpio --null -o -H newc --owner=root:root | gzip -1 ) > work/builder.cpio.gz ./scripts/boot-builder-vm.sh # o el qemu crudo: qemu-system-x86_64 -m 6144 -no-reboot -nographic -cpu Broadwell \ -kernel /boot/vmlinuz-linux -initrd work/builder.cpio.gz -append "console=ttyS0 rdinit=/sbin/init" ``` > **RAM.** El initramfs se desempaqueta a un tmpfs (~1.2 GB) y el rebuild Rust necesita varios GB > más, TODO en RAM. Con host ~7 GB y sin KVM va justo y lento (TCG). Sube `MEM` o monta un disco > para `/work` si el rebuild muere por OOM. **✅ Boot del builder confirmado (2026-06-11).** El initramfs (410 MB gz, 26 776 entradas) **bootea end-to-end**: arje-zero despierta como **PID 1**, carga+valida la seed card, instancia los 2 genesis (**hammerd** pid 60 + **console-getty** pid 61), hammerd levanta su watcher fanotify y el `agent.sock`, y la getty deja la shell `~ #` en consola. Idéntico al boot de Stage 1 (§7) pero con el toolchain adentro — listo para el rebuild. **4. Dentro de la VM**, desde la shell de la consola, correr el driver embebido: ```sh rebuild-stage1 # >> rebuild stage1 (toolchain in-rootfs, semilla b3:3ce721ec…) # >> stage1' = b3:… # ✓ REPRODUCIBLE: stage1' == stage1 (auto-alojado bit a bit) ← of_tree(stage1')==REF_CONTENT ``` `rebuild-stage1` apunta `HAMMER_ROOTFS=/toolchain`, corre `hammer bootstrap stage1` con la semilla y las recetas de adentro, y compara con `stage2 --verify $REF_CONTENT`. **✓ REPRODUCIBLE** cierra el auto-alojamiento end-to-end de la variante (a); **✗ DIVERGENTE** caza un no-determinismo nuevo ([SDD 09 §2](../09-trust-model.md)). La variante (b) — el toolchain construido por hammer desde fuente — reemplaza `/toolchain` Alpine pieza a pieza, con este mismo lazo verificando cada paso. ### ✅ Rebuild in-rootfs ejecutado (2026-06-11): 2/4 componentes sellados en la VM Corrida real (QEMU TCG, `-m 6144`, sin KVM). El driver atravesó, en orden, una cadena de muros reales — cada uno destapado **sólo** por correr el rebuild *dentro* de la VM (justo lo que Stage 2 existe para cazar) y arreglado: 1. **Toolchain incompleto.** Traía compiladores (cargo/make/zig) pero no la orquestación del lab: `bwrap` (anida el sandbox), `git` (`git archive` del mirror), `curl` (tarballs). En el host las da el sistema; en la VM el toolchain es el único userland ⇒ `apk add bubblewrap git curl`. 2. **Binarios dinámicos vs userland estático.** El toolchain es Alpine dinámico; el Stage 1 es musl estático (sin loader en `/lib`) ⇒ no corrían. Fix: **shim del loader** (`cp` del `ld-musl` a `/lib` + `LD_LIBRARY_PATH`/`PATH`) en `rebuild-stage1`. 3. **overlay es módulo.** `CONFIG_OVERLAY_FS=m` y el initramfs arranca sin módulos ⇒ `bwrap --tmp-overlay` daba `ENODEV`. Fix: `insmod /toolchain/lib/overlay.ko` (+ `kmod`; `overlay.ko` del kernel destino inyectado en el builder). 4. **Ownership del initramfs.** El copy-up de overlay daba `EACCES` (`Can't mkdir /opt/zig`): los ficheros viajaban con uid 1000, sin mapear en el userns de bwrap (mapea 0→0). Fix: empaquetar con `cpio --owner=root:root`. Con eso, **musl** y **busybox** se reconstruyeron y **sellaron dentro de la VM** con el toolchain de adentro (configure→compile→install→seal; ~35–38 min/componente bajo TCG). El mecanismo de auto-alojamiento queda **probado sobre builds reales**, no sólo en diseño. **Muro pendiente — deps Rust offline.** `hammerd` abortó en `cargo vendor` (`failed to sync`): el `/work` embebido lleva los **mirrors git de las fuentes** (`repos/`) pero no las **dependencias crates.io**, y la VM no tiene red. Los componentes C sellan (tarballs autocontenidos); los Rust (`hammerd`, `arje-zero`) necesitan, para offline, **o** red en la VM (qemu `-netdev user` + NIC + `/etc/resolv.conf`, los repos privados ya están mirroreados) **o** las crates vendoreadas en `/work`. Sumado al coste de los builds Rust bajo TCG (horas, RAM justa ⇒ riesgo de OOM en `arje-zero`), el veredicto pleno **4/4** se completa en un host con **KVM + más RAM** (y, idealmente, un builder booteado desde **disco** en vez de initramfs puro, que además evita la fricción ramfs/ownership). ### Rust-offline cruzado + un no-determinismo cazado: el CPU del builder (2026-06-11) **El muro Rust-offline cayó.** Se dio red a la VM (NIC e1000 qemu user-mode + `insmod e1000.ko` + config estática de `eth0` 10.0.2.15 + DNS de slirp + CA bundle de Alpine copiado a la ruta estándar, commits `09034a5`/`fd20e38`). Con eso `cargo vendor` resolvió **crates.io entero** (DNS+TLS), y **`hammerd` se compiló, instaló y selló dentro de la VM** (74 m 35 s bajo TCG). Para no pagar el vendoring del monorepo de arje-zero bajo TCG, esta corrida **preseed-eó** musl/busybox/arje-zero en el store del builder y dejó **sólo hammerd** para reconstruir in-VM (driver `work/drive-rebuild.py`). **Veredicto: `✗ DIVERGENTE`** — y es justo lo que Stage 2 existe para cazar. `of_tree(stage1)=b3:94a4f1…` (host) vs `of_tree(stage1')=b3:ab615b…` (VM). El **seal hash** del rootfs sí coincidió (`b3:f11ca6e5…`), pero el seal es `of_inputs` (recipe + `(name,hash)` de componentes): **mismo key ≠ mismos bytes**. Lo único reconstruido in-VM era `hammerd`, así que la divergencia está ahí. **Diagnóstico (experimento de control en el host, sin VM):** 1. **Rebuild de `hammerd` en el host → byte-idéntico** al cacheado (`cmp` limpio, dos veces). La receta es determinista host-a-host; el cache **no** está stale. 2. ⇒ la divergencia es **del entorno de build de la VM**. La única entrada de codegen no fijada es el **CPU del builder**: `zig cc` (que compila el C/asm de los build-scripts: blake3, curve25519-dalek) **default a `-mcpu=native`** y hornea la ISA de quien compila. 3. **Evidencia ISA:** el `hammerd` del host trae `sha256rnds2` (SHA-NI) ×32 y 3530 instrucciones AVX2 (`ymm`). El host tiene `sha_ni`+`avx2`; **`-cpu Broadwell` (el de la VM bajo TCG) NO tiene SHA-NI** ⇒ codegen distinto ⇒ bytes distintos ⇒ `of_tree` distinto. Es la misma raíz que el SIGILL de AVX del §7 (binarios al CPU del builder, no a un baseline). **Fix — `-mcpu=baseline` en TODOS los sitios `zig cc`** (codegen al baseline x86-64 genérico, independiente del CPU): el `CC`/`CXX` del sandbox (`sandbox.rs`), los dos wrappers Cargo (`.hammer-zig-cc`, `lib.rs`) y los `CC`/`HOSTCC` de la receta de busybox. Las rutas SIMD que el runtime sí usa (blake3/sha2) son asm con dispatch en runtime: siguen presentes y funcionando. Bonus: cierra de paso el AVX/SIGILL del §7 (el binario corre en qemu64 sin `-cpu Broadwell`). **Validado en el host:** con baseline, `hammerd` cambia de bytes (1 672 448 vs 1 677 584 — confirma que el default NO era baseline) y **los 3 componentes rebuildan limpio** (`stage1 rc=0`). Nueva referencia **CPU-independiente**: `of_tree(stage1-baseline)=b3:4408e44e2ec51c3769deffcd64dd1f6c2010d414418937c3fa7930a0ded3f845` (con musl `57b66a2e`+busybox `56664d70`+arje `bd0f8475`+hammerd `d08fd273`, todos baseline salvo arje que quedó preseed nativo). Nota de fondo (plan C.2): el flag de CPU **no** entra hoy en `of_inputs`, así que un cambio de toolchain/flags da cache-hit con bytes viejos — la misma tensión "pin de toolchain al hash" que conviene cerrar para que Stage 2 no dependa de borrar el store a mano (por eso el refresh de abajo borró los artefactos viejos a mano antes de reconstruir). **Store refrescado a baseline 4/4 + builder listo (2026-06-11).** Se reconstruyeron los **4** componentes con el toolchain baseline (incluido `arje-zero`, que re-vendorea el monorepo) en un store limpio, y se promovió a `./store` (el nativo quedó en `store-native-bak`). `arje-zero` baseline **difiere** del nativo (char 25) ⇒ baseline aplicado. Referencia **CPU-independiente full-4/4**: `of_tree(stage1)=b3:198f209f5e2c9d3a37411823b2ea42a09074d4e482f044278958b1b86f232dbb` (rootfs key `b3:2bd7c91b…`; nótese que el key `of_inputs` es **idéntico** al del preseed-nativo — el cambio de bytes de arje no movió su key: la misma escotilla C.2). El builder se re-ensambló con ese toolchain + el `hammer` baseline + `--ref-content 198f209f…` (embebida en `/etc/hammer/rebuild.env`) y se repackó **`work/builder.cpio.gz`** (415 MB, 27 027 entradas, `--owner=root:root`). **Listo para bootear:** `scripts/boot-builder-vm.sh` (o `scripts/drive-rebuild.py` no-interactivo) → adentro `rebuild-stage1` debe reproducir **`198f209f…`** ⇒ **✓ REPRODUCIBLE** cerraría el auto-alojamiento **4/4** bit a bit. Bajo TCG es lento y la RAM va justa; el veredicto pleno gana con **KVM + más RAM** (y boot desde disco). **Tarea reproducible end-to-end: `scripts/selfhost-verify.sh`.** Un solo comando hace TODO el pipeline (stage0 → stage1 baseline → stage2 ref → inyectar `overlay.ko`/`e1000.ko` del kernel local → ensamblar builder → empaquetar → bootear + driver no-interactivo → veredicto), pensado para correr en una máquina con **KVM + RAM holgada** (laptop): ```sh ./scripts/bootstrap-devfs.sh # una vez: el lab (.dev-fs/alpine + zig) KVM=1 MEM=10240 ./scripts/selfhost-verify.sh # PRESEED=hammerd → preseed C+arje y reconstruye sólo hammerd in-VM (barato si la RAM va justa) ``` Cross-check de reproducibilidad **entre máquinas**: el script compara su `of_tree(stage1)` contra `EXPECT_REF` (la referencia conocida-buena `198f209f…` del host de dev). Si difieren, o cambió un pin (toolchain/recipes/seed) o hay un no-determinismo nuevo. En el host de dev (Artix, 7.6 GB, **sin KVM**) el rebuild **4/4** in-VM colgó por presión de RAM bajo TCG a los ~17 min del `make` de musl — el muro de hardware que esta tarea traslada a un host capaz. ## 9. Cross-check opcional — `arje-packager` arje trae su propio empaquetador (`03_ukupacha/arje/init/arje-packager`): `arje-packager --seed --out [--bin LABEL=PATH]…`. Útil para comparar el initramfs **canónico de arje** contra el que sale del rootfs de hammer (§4). No reemplaza esta validación: aquí probamos el rootfs que **hammer** sella, no el que arje arma.