1# Runbook — validar Stage 1 booteando en QEMU
2
3> Operativo, no diseño. Valida end-to-end que el rootfs que produce
4> `hammer bootstrap stage1` ([SDD 11](../11-bootstrap.md)) **bootea con arje-zero como PID 1**
5> ([SDD 12](../12-init-real.md)), levanta `hammerd` + getty bajo supervisión, y que el **`CRASHED`
6> real** funciona.
7>
8> **Esto es un procedimiento, no un transcript verificado.** La lógica de ensamblado/hash/seed está
9> cubierta por unit tests (y la seed se validó contra `card_core::Card`), pero el cross-compile y el
10> boot reales **no** se han corrido todavía — ese es justo el punto de este runbook. La primera
11> corrida puede destapar ajustes (link, `/dev/console`, getty/tty); §7 los anticipa.
12
13## 0. Lo que valida / lo que no
14
15- **Valida:** que el árbol sellado por `stage1` arranca como sistema: arje-zero monta los pseudo-FS,
16 carga la seed, incarna `hammerd` y la getty, y al matar `hammerd` lo reinicia con backoff (el
17 `CRASHED` que la Fase 5 difirió).
18- **No valida (aún):** bus único (B.2), overlay del store (I4), atestación (A1/A2), kernel
19 from-source. Aquí el kernel es prestado del host; el overlay no se monta.
20
21## 1. Prerrequisitos
22
23- **Lab de dev** montado: `./scripts/bootstrap-devfs.sh` (deja `.dev-fs/alpine` + `.dev-fs/tools/zig`).
24- **Red**: el build de `hammerd` y `arje-zero` clona repos git (hammer, tawasuyu) y corre
25 `cargo vendor` en el fetch. El build hermético del sandbox es `--offline`, pero el fetch necesita red.
26- **Herramientas host**: `qemu-system-x86_64`, un kernel x86_64 (`/boot/vmlinuz-*`), `cpio`, `gzip`.
27- **Espacio/tiempo**: construir `arje-zero` vendorea las deps del **monorepo tawasuyu entero** — es
28 pesado y lento la primera vez (se cachea después).
29- **El binario `hammer`**: `cargo build --release -p hammer-cli` ⇒ `./target/release/hammer`.
30
31> El store debe vivir en la raíz del repo (`./store`) para que `BuildConfig` resuelva
32> `.dev-fs/alpine` como hermano (`store/..` = repo root). El toolchain de Stage 1 **no** es el de
33> `.dev-fs`: es la semilla de Stage 0 (paso 2), que `stage1` enchufa como `zig_dir` del sandbox.
34
35## 2. Stage 0 — ingerir la semilla (zig)
36
37```sh
38export HAMMER=./target/release/hammer
39export STORE=./store
40
41"$HAMMER" --store "$STORE" bootstrap stage0 \
42 --url https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz \
43 --sha256 70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 \
44 --version 0.16.0
45# → imprime el seed_hash (b3:...). Guardalo:
46SEED="b3:..." # pegá el hash impreso
47```
48
49Idempotente: re-correr no re-descarga. La semilla queda sellada como `seed-zig` en el store y anota
50su línea en `bootstrap.json`.
51
52## 3. Stage 1 — construir y sellar el rootfs
53
54```sh
55"$HAMMER" --store "$STORE" bootstrap stage1 --seed-hash "$SEED" --recipes recipes
56# construye musl + busybox + hammerd + arje-zero, ensambla el rootfs FHS y lo sella.
57# → imprime el rootfs_hash (b3:...).
58
59# El árbol sellado (read-only) del rootfs:
60ROOTFS_DIR=$(ls -d "$STORE"/*-stage1-rootfs)
61echo "$ROOTFS_DIR"
62ls "$ROOTFS_DIR"/ente/seed.card.json "$ROOTFS_DIR"/sbin/init "$ROOTFS_DIR"/usr/bin/{hammerd,arje-zero}
63```
64
65Necesita red (git clone + `cargo vendor`). Si un build Cargo falla en el link (`-lgcc_s`) o por
66static-PIE, ver §7 — el `zig cc` del lab debería resolver `-lgcc_s` (bitácora M2 del plan arje↔hammer).
67
68## 4. Empaquetar el rootfs como initramfs
69
70El árbol del store ya trae `/sbin/init`→`/usr/bin/arje-zero` y `/ente/seed.card.json`. Se empaqueta
71como cpio `newc` + gzip (formato initramfs):
72
73```sh
74( cd "$ROOTFS_DIR" && find . -print0 | cpio --null -o --format=newc ) | gzip -9 > /tmp/stage1.cpio.gz
75ls -lh /tmp/stage1.cpio.gz
76```
77
78`cpio newc` preserva symlinks y hardlinks. El symlink `/sbin/init`→`/usr/bin/arje-zero` es absoluto:
79en un initramfs resuelve dentro de su propia raíz. (Si el kernel no auto-monta `devtmpfs`, ver §7 por
80el nodo `/dev/console`.)
81
82## 5. Bootear en QEMU
83
84```sh
85qemu-system-x86_64 -m 512 -nographic \
86 -kernel /boot/vmlinuz-linux \
87 -initrd /tmp/stage1.cpio.gz \
88 -append "console=ttyS0 rdinit=/sbin/init"
89```
90
91- `rdinit=/sbin/init` — el default de un initramfs es `/init`; lo sobreescribimos a `/sbin/init`
92 (→arje-zero). Alternativa directa: `rdinit=/usr/bin/arje-zero`.
93- `console=ttyS0` + `-nographic` — consola serie, donde arje escribe y la getty se engancha.
94- Salir de QEMU: `Ctrl-A` luego `X`.
95
96## 6. Criterios de éxito (qué observar)
97
981. **PID 1**: en kmsg/consola, arje-zero loguea *"despierta como PID 1"*.
992. **Pseudo-FS**: monta `/proc /sys /dev` sin error fatal (best-effort; un `EBUSY` se ignora).
1003. **Seed**: carga `/ente/seed.card.json` sin *"card inválida"* (ya validada contra `card_core::Card`).
1014. **hammerd**: arranca como Ente — su tracing aparece y/o crea `/run/agent.sock`.
1025. **getty**: aparece un prompt de shell (`/bin/sh`) en `ttyS0`.
1036. **CRASHED real** — la prueba clave. En el shell:
104 ```sh
105 kill "$(pidof hammerd)"
106 ```
107 arje registra el `on_death(ExitStatus)` y **reinicia hammerd tras el backoff** (200 ms → ×2…).
108 Eso es el `CRASHED` que la Fase 5 dejó diferido, ahora manejado por el init propio.
109
110## 7. Troubleshooting (síntoma → causa → fix)
111
112| Síntoma | Causa probable | Fix |
113|---|---|---|
114| `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`) |
115| 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` |
116| 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) |
117| 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 |
118| getty sin prompt | device `console` no engancha tty | probar `argv` con `ttyS0` en vez de `console`; revisar que `/dev/console` existe |
119| `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` |
120| `cargo vendor` / git clone falla | el fetch necesita red | correr Stage 1 con red; el sandbox de build sigue siendo `--offline` |
121
122## 8. Bitácora — primera corrida real (2026-06-11)
123
124Corrida en el host de desarrollo (Artix). **Resultado: el userland C builda end-to-end; el camino
125Rust está bloqueado por una decisión de toolchain.** Hallazgos, en orden:
126
127| # | Etapa | Resultado |
128|---|---|---|
129| 1 | Lab (`bootstrap-devfs.sh`) | ✅ Alpine + zig 0.16.0 + 43 build-tools |
130| 2 | Stage 0 (ingerir zig) | ✅ `seed_hash` sellado, idempotente |
131| 3 | **musl 1.2.5** | ✅ cross-compila con zig cc y se sella — **el cross-compile C hermético funciona** |
132| 4 | **busybox 1.36.1** | ✅ tras tres fixes (abajo); compila y **linkea** con zig cc, se sella |
133| 5 | **hammerd** (Cargo) | ❌ bloqueado: triple de toolchain (abajo) |
134| 6 | arje-zero (Cargo) | ⏸ no alcanzado (mismo bloqueo; además vendorea el monorepo entero) |
135
136**Fixes aplicados a `recipes/busybox.toml`** (incompatibilidades GNU-toolchain ↔ zig, esperadas):
137
1381. **`gcc` hardcodeado.** El Kbuild de busybox fija `CC = gcc` / `HOSTCC = gcc` con `=` (ignora el
139 env `CC` del sandbox). Fix: pasar `CC='zig cc' HOSTCC='zig cc'` en **línea de comando** de make.
1402. **Flags GNU-ld que zig cc rechaza.** El link pasa `-Wl,--warn-common`, `-Wl,-Map,…`,
141 `-Wl,--verbose`; `zig cc` valida los `-Wl,` y aborta. Fix: quitarlos de `scripts/trylink` con sed.
1423. (`gcc` para herramientas host como `fixdep` y `gcc-version.sh` lo cubre el mismo `HOSTCC`.)
143
144**Bloqueo del camino Rust — decisión de toolchain (plan C.2):**
145
146- El vendoring (Lote 6) corre OK en el fetch (host, con red). El `cargo build` corre en el sandbox.
147- Se añadió `rust cargo` al rootfs (apk) — pero el rust de Alpine tiene host triple
148 **`x86_64-alpine-linux-musl`**, mientras `BuildSys::Cargo` pide **`--target x86_64-unknown-linux-musl`**.
149 Alpine no trae ese std y no hay rustup → `error[E0463]: can't find crate for core`.
150- **Opciones (a decidir):** (A) construir nativo sin `--target` (el sandbox ya es x86_64 musl) —
151 cambio en `BuildSys::Cargo`; (B) recetas Cargo con `target = "x86_64-alpine-linux-musl"` — leakage
152 de Alpine; (C) rustup en el sandbox + `rustup target add` — pierde hermeticidad/pin. Es la misma
153 tensión de "pin de toolchain entra al hash" que el plan C.2 dejó abierta.
154
155### Stage 1 completo + intento de boot
156
157Tras los fixes, **los 4 componentes buildan y el rootfs se sella** (musl+busybox+hammerd+arje-zero,
15829 MB; `/sbin/init`→arje-zero, `/ente/seed.card.json`, loader musl). Se empaquetó como initramfs
159(`cpio newc`+gzip, 11 MB) y se booteó en QEMU. La cadena de hallazgos del boot:
160
1611. **AVX.** El CPU default de QEMU (`qemu64`) no tiene AVX, pero los binarios (zig/rust nativos)
162 emiten `vxorps` → `SIGILL` (`invalid_op`) en el loader → init muere. Fix: `-cpu Broadwell`
163 (AVX2 bajo TCG; sin `/dev/kvm` no hay `-cpu host`). Para un distro real: compilar a un baseline
164 x86-64 genérico, no al CPU del builder.
1652. **musl dinámico.** El rust de Alpine produce binarios **dinámicos** (`DT_NEEDED
166 libc.musl-x86_64.so.1`), no crt-static. El loader corre pero no resuelve `memcpy/memset/log2/…`:
167 nuestro `libc.so` (musl 1.2.5 shared con zig cc) tiene 1586 dyn-syms pero **no exporta** esos
168 símbolos. → `Error relocating /sbin/init: memcpy: symbol not found` → init muere.
169
170**Conclusión.** El bootstrap **C** (musl+busybox+zig cc) y el **build Rust nativo** (hammerd y
171arje-zero compilan y se sellan) están **validados** end-to-end; el kernel arranca el initramfs y
172ejecuta arje-zero como PID 1. El último bloqueo del boot es el **linking dinámico de musl**: el fix
173correcto y golden-path es **crt-static** — compilar los componentes Rust con
174`-C target-feature=+crt-static -C relocation-model=static` para binarios **autocontenidos** (sin
175`libc.so`, sin loader, sin soname, como busybox). Requiere rebuild de hammerd/arje-zero (el de
176arje-zero vendorea el monorepo: caro en tiempo y disco). Es la realización plena de la "vía de oro"
177estática de hammer y cierra de paso el problema del soname de Alpine.
178
179**Actualización (Opción A implementada).** `BuildSys::Cargo` ahora construye **nativo** sin `--target`
180cuando el target es el del sandbox (`SANDBOX_NATIVE_TARGET`), con un wrapper `.hammer-zig-cc` que
181(a) resuelve el `zig cc` de dos palabras para cargo/cc-rs y (b) **sanea el triple** que cc-rs pasa
182(`--target=x86_64-alpine-linux-musl` → `x86_64-linux-musl`, que zig sí entiende). Resultado:
183**hammerd compila nativo y se sella ✓** — el camino Rust funciona. **arje-zero queda bloqueado** por
184otra razón: **tawasuyu no commitea `Cargo.lock`** (`git ls-files Cargo.lock` vacío), así que el
185`git archive` no lo trae y `cargo vendor --locked` no puede crearlo. Decisión pendiente: commitear
186el lock en tawasuyu (reproducible, plan C.2 #5) vs relajar `--locked` en el vendoring de hammer.
187
188**Cierres posteriores.** (1) Vendoring `--locked` **condicional** al lock committeado → arje-zero
189vendorea sin `--locked` (genera el lock) y **se sella**: los 4 componentes buildan. (2) Boot:
190AVX → `-cpu Broadwell`; y el linking **dinámico** de musl (Alpine rust es dinámico por defecto →
191`DT_NEEDED libc.musl-x86_64.so.1`; nuestro `libc.so` no exporta `memcpy/…`) → fix **crt-static**.
192(3) Iteración del crt-static (cada paso destapó el siguiente; hammerd valida barato):
193`+crt-static [-C relocation-model=static]` en **RUSTFLAGS** rompe los proc-macros (dylibs PIC) en
194build nativo → **solución:** `cargo rustc --release … -- <flags>`, que aplica los flags sólo a la
195crate top-level. musl vuelve a `--disable-shared` (no se necesita loader).
196
197### ✅ BOOT LOGRADO (2026-06-11)
198
199Con `cargo rustc -- +crt-static -C relocation-model=static`, `arje-zero` y `hammerd` salen
200**`statically linked` (ET_EXEC, sin `DT_NEEDED`, sin interpreter)**, el rootfs se sella, y el boot en
201QEMU (`-cpu Broadwell`) **levanta el sistema**:
202
203```
204arje_zero: ente-zero despierta como PID 1
205arje_zero::seed: Tarjeta Semilla cargada y validada path=/ente/seed.card.json
206arje_zero: Ente #0 entra al bucle primordial label=hammer-stage1
207arje_zero::graph::lifecycle: instanciando genesis count=2
208 Ente encarnado label=hammerd pid=Some(Pid(59))
209 Ente encarnado label=console-getty pid=Some(Pid(60))
210~ # (shell en consola)
211hammerd: arranque agent_sock=/run/agent.sock …
212hammerd: watcher fanotify activo
213hammerd::bus: escuchando sock=/run/agent.sock
214```
215
216**arje-zero es PID 1**, carga+valida la seed y **supervisa hammerd + getty**; hammerd corre con su
217watcher fanotify y su `agent.sock`. **Stage 1 bootea end-to-end.** Comando reproducible:
218`qemu-system-x86_64 -m 512 -no-reboot -nographic -cpu Broadwell -kernel <vmlinuz> -initrd
219stage1.cpio.gz -append "console=ttyS0 rdinit=/sbin/init"`.
220
221### ✅ `CRASHED` real demostrado (lo que la Fase 5 difirió)
222
223Desde la shell de la consola, `kill $(pidof hammerd)` y arje-zero lo supervisa de verdad:
224
225```
226kill $(pidof hammerd)
227arje_zero::graph::lifecycle: Ente disuelto label=hammerd status=Killed(SIGTERM)
228arje_zero::graph::lifecycle: Restart programado label=hammerd delay_ms=200
229arje_zero::graph::lifecycle: Ente encarnado label=hammerd pid=Some(Pid(64))
230hammerd: arranque agent_sock=/run/agent.sock …
231```
232
233Detecta la muerte **con su `ExitStatus`** (`Killed(SIGTERM)`), programa el restart con **backoff**
234(200 ms) y **re-encarna** hammerd (pid 59→64). Es la supervisión real del init propio — el `CRASHED`
235que [ADR 0007](../adr/0007-arje-como-init-propio.md) prometía y la Fase 5 había diferido. Exponerlo
236a la capa de IA por `agent.sock` es B.2 (bus único).
237
238## 8b. Stage 2 — verificación de reproducibilidad (pre-rebuild-in-rootfs)
239
240El `stage2` de hammer ancla el **content-hash** (`ArtifactHash::of_tree`, los bytes reales) del
241rootfs y lo compara con un rebuild. El rebuild **dentro** del rootfs usando *sólo* las herramientas
242de Stage 1 (auto-alojamiento pleno) necesita un **builder rootfs** — el Stage 1 mínimo es un
243*runtime* (no trae zig/make/cargo), así que ese paso es un hito aparte. Lo que **sí** se verificó
244acá: la **bit-reproducibilidad del build** (dos builds independientes del mismo componente →
245`diff -r`):
246
247| Componente | Reproducible | Notas |
248|---|---|---|
249| **musl** 1.2.5 | ✅ byte-idéntico | el build C con zig cc es determinista de fábrica |
250| **busybox** 1.36.1 | ✅ tras dos fixes | (ver abajo) |
251| **hammerd** (Rust) | ✅ byte-idéntico | crt-static + cargo `--locked` + `SOURCE_DATE_EPOCH` + paths fijos `/src` ⇒ rust reproducible |
252| arje-zero (Rust) | ◑ probable | mismo camino que hammerd, pero vendorea **sin** `--locked` (tawasuyu no committea `Cargo.lock`) ⇒ el lock generado podría variar; committear el lock lo cierra (plan C.2 #5) |
253
254**La verificación cazó dos no-determinismos reales en busybox** (justo lo que [SDD 09 §2](../09-trust-model.md) pide):
255
2561. **Config interactiva.** Tras editar `.config`, el compile corre `silentoldconfig`, que ante
257 opciones NEW (WERROR, etc.) **prompea leyendo stdin** → el set de applets salía distinto por
258 corrida (build no-determinista *y* flaky). Fix: `yes '' | make oldconfig` (acepta defaults,
259 determinista; busybox 1.36.1 no tiene `olddefconfig`).
2602. **Headers del kernel.** Con el config determinista, los applets de `console-tools` exigen
261 `linux/kd.h` (UAPI) que zig **no** bundlea → `linux-headers` en el lab (`bootstrap-devfs.sh`);
262 zig cc nativo los encuentra en `/usr/include`.
263
264Con ambos, `musl` y `busybox` reconstruyen **bit-idéntico**. El próximo paso de Stage 2 es el
265rebuild *dentro* de un builder rootfs y, antes, verificar/forzar la reproducibilidad de los
266componentes Rust.
267
268## 9. Cross-check opcional — `arje-packager`
269
270arje trae su propio empaquetador (`03_ukupacha/arje/init/arje-packager`):
271`arje-packager --seed <CARD.json> --out <initramfs.cpio.gz> [--bin LABEL=PATH]…`. Útil para comparar
272el initramfs **canónico de arje** contra el que sale del rootfs de hammer (§4). No reemplaza esta
273validación: aquí probamos el rootfs que **hammer** sella, no el que arje arma.
stage1-vm-boot.md
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) bootea con arje-zero como PID 1
(SDD 12), 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)
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
"$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):
( 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
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)
- PID 1: en kmsg/consola, arje-zero loguea "despierta como PID 1".
- Pseudo-FS: monta
/proc /sys /dev sin error fatal (best-effort; un EBUSY se ignora).
- Seed: carga
/ente/seed.card.json sin "card inválida" (ya validada contra card_core::Card).
- hammerd: arranca como Ente — su tracing aparece y/o crea
/run/agent.sock.
- getty: aparece un prompt de shell (
/bin/sh) en ttyS0.
- CRASHED real — la prueba clave. En el shell:
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):
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.
- 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.
- (
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:
- 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.
- 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 … -- <flags>, 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 <vmlinuz> -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 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) | ◑ probable | mismo camino que hammerd, pero vendorea sin --locked (tawasuyu no committea Cargo.lock) ⇒ el lock generado podría variar; committear el lock lo cierra (plan C.2 #5) |
La verificación cazó dos no-determinismos reales en busybox (justo lo que SDD 09 §2 pide):
- 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).
- 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):
arje-packager --seed <CARD.json> --out <initramfs.cpio.gz> [--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.