docs: runbook para validar Stage 1 booteando en QEMU
Procedimiento operativo end-to-end para de-riesgar lo construido: stage0 (ingerir zig) → stage1 (build + ensamblar el rootfs) → empaquetar initramfs (cpio newc + gzip) → bootear en QEMU con el kernel del host → criterios de éxito. Fundamentado en los comandos reales del repo (scripts/bootstrap-devfs.sh, la URL y sha256 de zig 0.16.0, hammer bootstrap stage0/stage1). La prueba clave: matar hammerd y ver a arje-zero reiniciarlo con backoff — el CRASHED real. Honesto sobre su estado: es el procedimiento, no un transcript verificado; el cross-compile y el boot reales aún no se corrieron (ese es el punto). §7 anticipa los ajustes probables (init, /dev/console, getty/tty, -lgcc_s). Enlazado desde SDD 12 §I2 y el índice de docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
14e10d9292
commit
56af1b5748
@@ -139,8 +139,9 @@ descarta: acoplar los grafos de build de los dos repos es justo lo que C.3 evita
|
|||||||
`/sys/fs/cgroup`, `/dev/pts`, `/dev/shm`) + `/sbin/init`→`/usr/bin/arje-zero`, y ya **no** escribe
|
`/sys/fs/cgroup`, `/dev/pts`, `/dev/shm`) + `/sbin/init`→`/usr/bin/arje-zero`, y ya **no** escribe
|
||||||
el `/etc/inittab` provisional. arje supervisa hammerd ⇒ **CRASHED real manejado por el init**.
|
el `/etc/inittab` provisional. arje supervisa hammerd ⇒ **CRASHED real manejado por el init**.
|
||||||
La seed embebida se **verificó contra el tipo real** `card_core::Card` (`from_json` + `validate()`
|
La seed embebida se **verificó contra el tipo real** `card_core::Card` (`from_json` + `validate()`
|
||||||
pasan), no sólo como JSON válido. El boot completo se valida en VM (no testeable sin kernel); los
|
pasan), no sólo como JSON válido. El boot completo se valida en VM (no testeable sin kernel) — ver
|
||||||
unit tests cubren la seed y el ensamblado, como el resto de Stage 1.
|
el [runbook de boot en QEMU](runbooks/stage1-vm-boot.md); los unit tests cubren la seed y el
|
||||||
|
ensamblado, como el resto de Stage 1.
|
||||||
- **I3 — bus único (plan B.2).** hammerd `announce` en `arje-bus` (cambio en hammerd) y **expone el
|
- **I3 — bus único (plan B.2).** hammerd `announce` en `arje-bus` (cambio en hammerd) y **expone el
|
||||||
`CRASHED` a la capa de IA** de hammer (`agent.sock` emite `{"t":"crashed",…}`). El `agent.sock`
|
`CRASHED` a la capa de IA** de hammer (`agent.sock` emite `{"t":"crashed",…}`). El `agent.sock`
|
||||||
(JSON, API de IA) queda **encima**; `arje-bus` (postcard) es el plano de control del init.
|
(JSON, API de IA) queda **encima**; `arje-bus` (postcard) es el plano de control del init.
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ discrepancia, o se corrige el código o se actualiza el SDD con un commit que ex
|
|||||||
| 11 | [Bootstrap from-scratch](11-bootstrap.md) | Track posterior: Stage 0/1/2, auto-alojamiento, semilla pinned |
|
| 11 | [Bootstrap from-scratch](11-bootstrap.md) | Track posterior: Stage 0/1/2, auto-alojamiento, semilla pinned |
|
||||||
| 12 | [`arje` como init real del Stage 1](12-init-real.md) | Contrato de runtime: seed card, hammerd supervisado, `CRASHED` real |
|
| 12 | [`arje` como init real del Stage 1](12-init-real.md) | Contrato de runtime: seed card, hammerd supervisado, `CRASHED` real |
|
||||||
|
|
||||||
|
### Runbooks (operativos)
|
||||||
|
|
||||||
|
| Runbook | Para qué |
|
||||||
|
|---|---|
|
||||||
|
| [Validar Stage 1 booteando en QEMU](runbooks/stage1-vm-boot.md) | `stage0`→`stage1`→initramfs→QEMU; criterios de éxito y troubleshooting |
|
||||||
|
|
||||||
## Architecture Decision Records (ADR)
|
## Architecture Decision Records (ADR)
|
||||||
|
|
||||||
Decisiones tomadas, con su contexto y consecuencias. Ver [`adr/`](adr/).
|
Decisiones tomadas, con su contexto y consecuencias. Ver [`adr/`](adr/).
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 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. 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.
|
||||||
Reference in New Issue
Block a user