bootstrap: busybox reproducible — fixes destapados por la verificación de Stage 2
La verificación de reproducibilidad de Stage 2 (dos builds + diff de bytes) probó que musl reconstruye bit-idéntico pero busybox NO, y cazó dos no-determinismos: 1. Config interactiva: tras editar .config, silentoldconfig prompea por opciones NEW leyendo stdin → set de applets variable (build no-determinista y flaky). Fix en busybox.toml: `yes '' | make oldconfig` (defaults, determinista; 1.36.1 no tiene olddefconfig). 2. Headers UAPI del kernel: el config determinista habilita console-tools, que exige linux/kd.h (zig no la bundlea). Fix: linux-headers en bootstrap-devfs.sh (zig cc nativo la halla en /usr/include). Con ambos, musl y busybox reconstruyen bit-idéntico. Runbook §8b documenta la verificación y deja pendiente: reproducibilidad de los componentes Rust y el rebuild dentro de un builder rootfs (auto-alojamiento pleno). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
3 archivos · +41 −2
diff --git a/docs/runbooks/stage1-vm-boot.md b/docs/runbooks/stage1-vm-boot.md
index 904cdb59..cc1dbf34 100644
--- a/docs/runbooks/stage1-vm-boot.md
+++ b/docs/runbooks/stage1-vm-boot.md
@@ -235,6 +235,35 @@ Detecta la muerte **con su `ExitStatus`** (`Killed(SIGTERM)`), programa el resta
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 / arje-zero (Rust) | ☐ pendiente | rust bit-reproducible es más duro; el rebuild de arje-zero es caro (monorepo) |
+
+**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**. El próximo paso de Stage 2 es el
+rebuild *dentro* de un builder rootfs y, antes, verificar/forzar la reproducibilidad de los
+componentes Rust.
+
## 9. Cross-check opcional — `arje-packager`
arje trae su propio empaquetador (`03_ukupacha/arje/init/arje-packager`):
diff --git a/recipes/busybox.toml b/recipes/busybox.toml
index 3a44b97a..368fcbcd 100644
--- a/recipes/busybox.toml
+++ b/recipes/busybox.toml
@@ -31,7 +31,13 @@ link = "static"
# `zig cc`, manteniendo la hermeticidad zig-only (ADR 0003) sin meter gcc al rootfs.
# El link final asume GNU ld y pasa flags que `zig cc` valida y rechaza (`--warn-common`, `-Map`,
# `--verbose`); los quitamos de scripts/trylink. Es la incompatibilidad GNU-ld↔zig/lld esperada.
-configure = "make CC='zig cc' HOSTCC='zig cc' defconfig && sed -i -e 's/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/' -e 's/^CONFIG_TC=y/# CONFIG_TC is not set/' -e 's/^CONFIG_FEATURE_TC_INGRESS=y/# CONFIG_FEATURE_TC_INGRESS is not set/' .config && sed -i -e 's/-Wl,--warn-common//g' -e 's/-Wl,-Map,[^ ]*//g' -e 's/-Wl,--verbose//g' scripts/trylink"
+#
+# `yes '' | make oldconfig` tras editar .config: sin esto, el compile corre `silentoldconfig`, que
+# al ver opciones NEW (p.ej. WERROR) **prompea interactivamente** leyendo stdin — build
+# no-determinista y flaky (lo destapó la verificación de reproducibilidad de Stage 2). `yes ''`
+# acepta el default de cada NEW ⇒ config resuelta y determinista (busybox 1.36.1 no tiene el target
+# `olddefconfig`, así que usamos `oldconfig` alimentado por `yes`).
+configure = "make CC='zig cc' HOSTCC='zig cc' defconfig && sed -i -e 's/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/' -e 's/^CONFIG_TC=y/# CONFIG_TC is not set/' -e 's/^CONFIG_FEATURE_TC_INGRESS=y/# CONFIG_FEATURE_TC_INGRESS is not set/' .config && yes '' | make CC='zig cc' HOSTCC='zig cc' oldconfig && sed -i -e 's/-Wl,--warn-common//g' -e 's/-Wl,-Map,[^ ]*//g' -e 's/-Wl,--verbose//g' scripts/trylink"
compile = "make -j\"$(nproc)\" CC='zig cc' HOSTCC='zig cc'"
# busybox instala en CONFIG_PREFIX (no DESTDIR): bin/busybox + symlinks de cada applet.
install = "make CC='zig cc' HOSTCC='zig cc' CONFIG_PREFIX=/out install"
diff --git a/scripts/bootstrap-devfs.sh b/scripts/bootstrap-devfs.sh
index b82b68c5..7a9e15f4 100755
--- a/scripts/bootstrap-devfs.sh
+++ b/scripts/bootstrap-devfs.sh
@@ -113,7 +113,11 @@ else
# y Alpine no trae ese std (no hay rustup). Falta resolver el toolchain Rust del lab (plan C.2)
# — ver docs/runbooks/stage1-vm-boot.md §8. Hasta entonces, los componentes C (musl, busybox)
# sí construyen end-to-end.
- NEEDED="make autoconf automake m4 patch coreutils libtool pkgconf bash binutils rust cargo"
+ # `linux-headers`: UAPI del kernel (linux/kd.h, etc.) que zig cc NO bundlea; busybox y otros
+ # paquetes la necesitan. zig cc nativo la encuentra en /usr/include. (Lo destapó la
+ # verificación de reproducibilidad de Stage 2: con el config determinista, los applets de
+ # console-tools la exigen.)
+ NEEDED="make autoconf automake m4 patch coreutils libtool pkgconf bash binutils rust cargo linux-headers"
missing=""
for pkg in $NEEDED; do
# apk info -e devuelve el paquete si está instalado, vacío si no.