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>
10 KiB
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 elCRASHEDreal 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):
- 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). - 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 (unEBUSYpor ya-montado se ignora).prctl(PR_SET_CHILD_SUBREAPER). - 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 unCardconpayload: Virtualygenesis: Vec<Card>. - 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 elEnteGraph, e instancia los genesis cards (graph/lifecycle.rs::authorize_and_spawn). - Bucle de estado:
SIGCHLD→reap→on_death(id, status)→ política deSupervision; 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 porarje-incarnate/src/plain.rsconCommand::new(exec)); obtiene PID, se reapea porSIGCHLD. 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.Restartusasandokan-lifecycle::Backoff(exponencial; resetea si la unidad vivió ≥max). La muerte de una cardRestart= elCRASHEDreal.- La seed se serializa JSON (
Card::to_json_pretty/from_json); ejemplo real:03_ukupacha/arje/seeds/arje-qemu.card.json(unagettyNative conRestart).
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_ROOT — só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 } }
}
]
}
hammerdcomo cardRestart⇒ su muerte disparaon_deathcon elExitStatus: ese es elCRASHEDreal, manejado por el init (reinicio con backoff), no por unrespawnciego.console-gettyreemplaza el::respawn:/bin/shprovisional con una getty supervisada.ENTE_BUS_SOCK=/run/ente-bus.sockfija un path estable (en boot no hayXDG_RUNTIME_DIR; el default caería en/tmp/ente-bus-ente.sock)./runes 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 Restart ⇒ CRASHED 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_rootfsgenera/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/inittabprovisional. arje supervisa hammerd ⇒ CRASHED real manejado por el init. La seed embebida se verificó contra el tipo realcard_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
announceenarje-bus(cambio en hammerd) y expone elCRASHEDa la capa de IA de hammer (agent.sockemite{"t":"crashed",…}). Elagent.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 ganaattest: Vec<ConcesionCapacidad>y arje-zero verifica los binarios (BLAKE3, ya alineado por A0) antes de incarnar — el nexo del modelo de confianza en capas (elexpected_hashde un.swmde 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) sobreconsole, noagettyde util-linux. ✅ decisión. - PID 1 enganche: symlink
/sbin/init→/usr/bin/arje-zero(funciona con cualquier cmdline; no exigeinit=en el kernel). ✅ decisión. - CAS:
ENTE_CAS_ROOT=/var/lib/ente/cassó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.