Files
hammer/docs/12-init-real.md
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

171 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](https://gitea.gioser.net/sergio/hammer) §B.
> Cierra el último ítem ◑ del [SDD 11](11-bootstrap.md): 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](10-roadmap.md), [ADR 0007](adr/0007-arje-como-init-propio.md)).
>
> 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**: `SIGCHLD``reap``on_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_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:
```jsonc
{
"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` `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_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](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
`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.