From 4a1d30db921eb47f6be0094d5ac6bdc7f87a0afd Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 11 Jun 2026 01:48:01 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20SDD=2012=20=E2=80=94=20arje=20como=20in?= =?UTF-8?q?it=20real=20del=20Stage=201=20(dise=C3=B1o=20del=20contrato=20d?= =?UTF-8?q?e=20runtime)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diseña el último paso ◑ del Stage 1: reemplazar el init provisional de busybox por arje-zero como PID 1, lo que entrega el CRASHED real (Fase 5 diferida). Fundamentado en el código real de arje-zero (citas a tawasuyu): - contrato de boot: arje monta los pseudo-FS él mismo (arje-kernel); lee /ente/seed.card.json (Card Virtual + genesis); supervisa con Restart/Backoff - qué debe proveer el rootfs (mount points, seed, /sbin/init→arje-zero, consola) - la seed card de Stage 1: hammerd como Payload::Native Restart ⇒ on_death = el CRASHED real; console-getty supervisada; ENTE_BUS_SOCK estable en /run - generación de la seed: template JSON autocontenido v1 (no acoplar build a tawasuyu), arje-packager al llegar la atestación A1 - fases I1(✅)→I2(PID 1)→I3(bus único B.2)→I4(overlay+atestación) - relación agent.sock (API IA, encima) vs arje-bus (control del init) Contraparte en hammer del plan tawasuyu PLAN-ATESTACION-Y-HAMMER §B. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/11-bootstrap.md | 6 +- docs/12-init-real.md | 167 +++++++++++++++++++++++++++++++++++++++++++ docs/README.md | 1 + 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 docs/12-init-real.md diff --git a/docs/11-bootstrap.md b/docs/11-bootstrap.md index 0b07844b..f9d87aff 100644 --- a/docs/11-bootstrap.md +++ b/docs/11-bootstrap.md @@ -132,9 +132,9 @@ de componentes + init), no del árbol en disco, así que es reproducible. El PID receta Cargo (repo pinned + deps vendoreadas en el fetch para build `--offline`). `arje` ([ADR 0007](adr/0007-arje-como-init-propio.md)) ya tiene su **receta puente** `recipes/arje-zero.toml` (Cargo, fuente = monorepo tawasuyu pinned al commit de la migración del CAS a BLAKE3, `-p arje-zero`): -prueba que el lab construye el init, pero todavía **no** es PID 1. Falta el paso "init real": -arje-zero como PID 1 del rootfs con su seed card, `arje-bus` y hammerd como Card de servicio, que -entrega el `CRASHED` real. +prueba que el lab construye el init, pero todavía **no** es PID 1. El paso "init real" —arje-zero +como PID 1 del rootfs con su seed card, `arje-bus` y hammerd como Card de servicio supervisada, que +entrega el `CRASHED` real— está diseñado en el [SDD 12](12-init-real.md). `SeedSpec { kind, version, url, sha256 }` es la identidad pinned de la semilla; `seed_hash()` deriva el `ArtifactHash` de `(kind, version, sha256)` — no del `url` ni del host, así que diff --git a/docs/12-init-real.md b/docs/12-init-real.md new file mode 100644 index 00000000..bbfa2ac7 --- /dev/null +++ b/docs/12-init-real.md @@ -0,0 +1,167 @@ +# 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 ` y un synth de dev son los otros + caminos.) La seed es un `Card` con `payload: Virtual` y `genesis: Vec`. +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": "", + "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 + `/sbin/init`→arje-zero, y retira + el `/etc/inittab` provisional. arje supervisa hammerd ⇒ **CRASHED real manejado por el init**. + Validación: boot en VM (no testeable sin display/kernel; los unit tests cubren la generación de 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` 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. diff --git a/docs/README.md b/docs/README.md index 3e08d19d..5dfc9387 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ discrepancia, o se corrige el código o se actualiza el SDD con un commit que ex | 09 | [Modelo de confianza](09-trust-model.md) | Reproducibilidad, verificar-no-confiar, log de transparencia | | 10 | [Roadmap](10-roadmap.md) | Fases, MVP, primer entregable | | 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 | ## Architecture Decision Records (ADR)