Files
takana/docs/12-init-real.md
T
Sergio f9ed89cd17 takana: espejo de GitHub renombrado, y las dos colas que quedaban
GitHub: sergiovelasquezzeballos/hammer -> takana (sigue privado). El segundo
pushurl del origin y el default de espejo-setup.sh actualizados; push real
verificado contra los DOS destinos.

Las dos colas las encontro hammer-4a y las verifique antes de aplicarlas:

1) CINCO scripts hardcodeaban /home/sergio/hammer y hoy funcionaban SOLO por el
   symlink que puse al mover. El peor era respaldo-storagebox.sh, cuya RAIZ por
   defecto salia de ahi: si alguien limpia el symlink dando el renombre por
   cerrado, el RESPALDO apunta a una ruta inexistente. Un respaldo que no
   encuentra su raiz no falla ruidosamente — se descubre el dia que lo
   necesitas. Ahora apuntan a /mnt/vvv/takana, la ruta real, sin depender de
   ningun enlace.

2) Cuatro referencias a gitea.gioser.net/sergio/hammer. El diagnostico del peer
   era el correcto y lo comprobe: ese HOST no resuelve, y no por el renombre —
   ya estaba mal antes, el remoto real es git.gioser.net. Cambiar solo
   hammer->takana las habria dejado igual de rotas. Van a
   https://git.gioser.net/sergio/takana, que responde 200.
2026-09-09 20:33:03 +00:00

171 lines
10 KiB
Markdown
Raw 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 takana del plan de tawasuyu
> [`03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER.md`](https://git.gioser.net/sergio/takana) §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 (`takana-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": "takana-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 | takana 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`** | takana-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 takana al de tawasuyu** (contra el espíritu de C.3 del plan: tawasuyu es *fuente de recetas*, no librería de takana) |
| **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 takana (`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 takana **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
`takana 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.