Files
hammer/docs/12-init-real.md
T
SergioandClaude Opus 4.8 56af1b5748 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>
2026-06-11 02:00:29 +00:00

10 KiB
Raw Blame History

SDD 12 — arje como init real del Stage 1

Estado: diseño (2026-06-11). Contraparte en hammer del plan de tawasuyu 03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER.md §B. Cierra el último ítem ◑ del SDD 11: reemplazar el init provisional de busybox por arje-zero como PID 1 del rootfs de Stage 1, lo que entrega el CRASHED real que la Fase 5 dejó diferido (SDD 10, ADR 0007).

Este documento es un contrato, no código: especifica qué debe proveer el rootfs y cómo se mapea el init provisional a arje, fundamentado en el código real de arje-zero (citas abajo).

1. Punto de partida

Stage 1 ya ensambla un rootfs musl + busybox + hammerd y, como PID 1 provisional, el init de busybox vía /etc/inittab (monta los pseudo-FS, respawnea hammerd y una shell). Ese init es ciego: un respawn no distingue "salió limpio" de "crasheó", no hace backoff y no es observable. arje-zero sí: supervisión real con RestartTracker + Backoff, y on_death con el ExitStatus.

La receta puente recipes/arje-zero.toml ya construye arje-zero con el lab (commit de la migración A0 arje-cas→BLAKE3). Falta el contrato de runtime para que sea PID 1.

2. Qué hace arje-zero al boot (contrato observado)

Fuente: tawasuyu/03_ukupacha/arje/init/arje-zero/. Secuencia (PID 1):

  1. Guarda de PID 1 (src/main.rs): si PID≠1, modo dev; si PID 1, nunca retorna (un retorno ⇒ rescue shell en /dev/console, no panic del kernel).
  2. Superficie del kernel (arje-kernel::bootstrap_kernel_surface, init/arje-kernel/src/surface.rs): remonta / RW; monta /proc, /sys, /dev (devtmpfs), /sys/fs/cgroup (cgroup2), y tmpfs en /run, /tmp, /dev/pts, /dev/shm; crea /run/lock. Best-effort e idempotente (un EBUSY por ya-montado se ignora). prctl(PR_SET_CHILD_SUBREAPER).
  3. Carga la seed (src/seed.rs): lee /ente/seed.card.json (o /ente/seed.card); valida con .validate(); si falla, no bootea. (--restore <snapshot> y un synth de dev son los otros caminos.) La seed es un Card con payload: Virtual y genesis: Vec<Card>.
  4. Runtime tokio current-thread + primordial_loop: levanta el bus (arje-bus) en $ENTE_BUS_SOCK (default $XDG_RUNTIME_DIR/ente-bus-$USER.sock, fallback /tmp/...), crea el EnteGraph, e instancia los genesis cards (graph/lifecycle.rs::authorize_and_spawn).
  5. Bucle de estado: SIGCHLDreapon_death(id, status) → política de Supervision; requests del bus; uevents. No hay getty propia: el acceso humano es un genesis card.

Tipos relevantes (shared/card/card-core/src/lib.rs)

  • Payload::Native { exec, argv, envp } — corre un ELF nativo como proceso supervisado (incarnado por arje-incarnate/src/plain.rs con Command::new(exec)); obtiene PID, se reapea por SIGCHLD. Es como corre hammerd.
  • Payload::Wasm { module_sha256, entry } — módulo WASM resuelto del CAS (arje-cas, hoy BLAKE3).
  • Supervision::Restart { initial, max } | OneShot | Delegate. Restart usa sandokan-lifecycle::Backoff (exponencial; resetea si la unidad vivió ≥ max). La muerte de una card Restart = el CRASHED real.
  • La seed se serializa JSON (Card::to_json_pretty / from_json); ejemplo real: 03_ukupacha/arje/seeds/arje-qemu.card.json (un agetty Native con Restart).

3. Qué debe proveer el rootfs de Stage 1

Derivado del contrato §2. El ensamblado (hammer-bootstrap::assemble_rootfs) debe garantizar:

Necesidad Detalle Quién lo pone
PID 1 /usr/bin/arje-zero (estático) y /sbin/init → symlink a él receta arje-zero + assemble
Mount points vacíos /proc /sys /dev /run /tmp /sys/fs/cgroup /dev/pts /dev/shm (dirs vacíos) assemble (esqueleto FHS)
Consola de rescate /dev/console, /dev/kmsg accesibles kernel/devtmpfs (arje monta /dev)
Seed card /ente/seed.card.json válido (§4) assemble (genera la seed)
hammerd /usr/bin/hammerd (ya es componente de Stage 1) receta hammerd
Shell /bin/sh (busybox) para rescate y para la getty receta busybox
CAS (opcional) /var/lib/ente/cas + ENTE_CAS_ROOTsólo si hay payloads Wasm assemble (sólo si aplica)
Cards de disco (opcional) /etc/arje/cards.d/*.json para SpawnCardFromDisk assemble (opcional)

arje monta los pseudo-FS él mismo; el rootfs no los monta (a diferencia del inittab provisional). Sólo deben existir como directorios. Esto simplifica el ensamblado.

4. La seed card de Stage 1

Un Card Virtual (la semilla, sin proceso) cuyos genesis son el userland supervisado. Mínimo viable para Stage 1:

{
  "schema_version": 1,
  "id": "<ULID estable de la seed de Stage 1>",
  "label": "hammer-stage1",
  "provides": ["Spawn", "Journal"],
  "payload": "Virtual",
  "supervision": "OneShot",
  "genesis": [
    {
      "label": "hammerd",
      "payload": { "Native": {
        "exec": "/usr/bin/hammerd",
        "argv": ["--store", "/store", "--journal", "/var/lib/hammer/journal"],
        "envp": [["RUST_LOG", "info"], ["ENTE_BUS_SOCK", "/run/ente-bus.sock"]]
      }},
      "supervision": { "Restart": { "initial": 200, "max": 10000 } }
    },
    {
      "label": "console-getty",
      "payload": { "Native": {
        "exec": "/bin/busybox",
        "argv": ["getty", "-n", "-l", "/bin/sh", "115200", "console"],
        "envp": []
      }},
      "supervision": { "Restart": { "initial": 200, "max": 10000 } }
    }
  ]
}
  • hammerd como card Restart ⇒ su muerte dispara on_death con el ExitStatus: ese es el CRASHED real, manejado por el init (reinicio con backoff), no por un respawn ciego.
  • console-getty reemplaza el ::respawn:/bin/sh provisional con una getty supervisada.
  • ENTE_BUS_SOCK=/run/ente-bus.sock fija un path estable (en boot no hay XDG_RUNTIME_DIR; el default caería en /tmp/ente-bus-ente.sock). /run es tmpfs montado por arje.

Cómo se genera la seed (decisión abierta, con recomendación)

Opción Cómo Trade-off
A. Template JSON (recomendada v1) assemble_rootfs escribe el JSON de arriba desde un template versionado, fijado al mismo commit de arje-zero hammer queda autocontenido (no depende de card-core como librería); riesgo de drift de schema, mitigado por pin + smoke test de boot en VM
B. Dep card-core hammer-bootstrap añade git-dep a card-core y construye el Card con tipos + to_json_pretty type-safe, sin drift; acopla el build de hammer al de tawasuyu (contra el espíritu de C.3 del plan: tawasuyu es fuente de recetas, no librería de hammer)
C. arje-packager (end-state) un paso del bootstrap construye y corre arje-packager (receta Cargo) para emitir —y en A1, firmar— la seed canónico y necesario para la atestación A1; el más pesado

Recomendación: A ahora (autocontenido, pin + validación en VM), migrar a C cuando A1 (atestación) entre, porque la seed firmada necesita arje-packager de todos modos. B se descarta: acoplar los grafos de build de los dos repos es justo lo que C.3 evita.

5. Del init provisional a arje (mapeo)

Init provisional (busybox/inittab) Init real (arje-zero)
::sysinit:/bin/mount -t proc … (×4) arje-kernel::bootstrap_kernel_surface (arje monta solo)
::respawn:/usr/bin/hammerd (ciego) genesis card hammerd Native RestartCRASHED real + backoff
::respawn:/bin/sh genesis card console-getty Native Restart
/sbin/init = busybox /sbin/init/usr/bin/arje-zero
sin observabilidad on_death(ExitStatus), telemetría por arje-bus, brain

6. Fases de implementación (riesgo creciente)

  • I1 — arje-zero construible. (recipes/arje-zero.toml, receta puente).
  • I2 — arje es PID 1 de Stage 1. STAGE1_COMPONENTS += arje-zero; assemble_rootfs genera /ente/seed.card.json (§4, opción A), provee los mount points (incl. /ente, /var/lib/hammer, /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. 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) — ver el runbook de boot en QEMU; 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 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.
  • I4 — overlay + atestación. arje monta el overlay del store (lowerdir RO /store / upperdir, plan B.1); A1/A2: la seed gana attest: Vec<ConcesionCapacidad> y arje-zero verifica los binarios (BLAKE3, ya alineado por A0) antes de incarnar — el nexo del modelo de confianza en capas (el expected_hash de un .swm de hammer es el BLAKE3 que arje atesta).

7. Decisiones (fijadas en este SDD)

  • Generación de la seed: template JSON autocontenido (opción A) en v1; arje-packager (C) al llegar A1. ☐ a implementar.
  • getty: busybox getty (cero recetas nuevas) sobre console, no agetty de util-linux. decisión.
  • PID 1 enganche: symlink /sbin/init/usr/bin/arje-zero (funciona con cualquier cmdline; no exige init= en el kernel). decisión.
  • CAS: ENTE_CAS_ROOT=/var/lib/ente/cas sólo cuando haya payloads Wasm; con hammerd Native v1 no se monta. decisión.
  • Bus: ENTE_BUS_SOCK=/run/ente-bus.sock (path estable en boot). decisión.
  • CRASHED a la IA: diferido a I3/B.2 (requiere que hammerd hable arje-bus). En I2 el CRASHED ya lo maneja el init (reinicio con backoff); exponerlo aguas arriba es I3. ☐.

8. Hecho cuando

hammer bootstrap stage1 produce un rootfs cuyo PID 1 es arje-zero, que monta los pseudo-FS, carga la seed, levanta hammerd y la getty bajo supervisión real, y al matar hammerd lo reinicia con backoff registrando el on_death — el CRASHED real que la Fase 5 difirió, ahora en el init propio.