1# SDD 12 — `arje` como init real del Stage 1
2
3> **Estado:** diseño (2026-06-11). Contraparte en hammer del plan de tawasuyu
4> [`03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER.md`](https://gitea.gioser.net/sergio/hammer) §B.
5> Cierra el último ítem ◑ del [SDD 11](11-bootstrap.md): reemplazar el init provisional de
6> busybox por **arje-zero como PID 1** del rootfs de Stage 1, lo que entrega el **`CRASHED`
7> real** que la Fase 5 dejó diferido ([SDD 10](10-roadmap.md), [ADR 0007](adr/0007-arje-como-init-propio.md)).
8>
9> Este documento es un **contrato**, no código: especifica qué debe proveer el rootfs y cómo se
10> mapea el init provisional a arje, fundamentado en el código real de arje-zero (citas abajo).
11
12## 1. Punto de partida
13
14Stage 1 ya ensambla un rootfs `musl + busybox + hammerd` y, como PID 1 **provisional**, el `init`
15de busybox vía `/etc/inittab` (monta los pseudo-FS, respawnea `hammerd` y una shell). Ese init es
16ciego: un `respawn` no distingue "salió limpio" de "crasheó", no hace backoff y no es observable.
17arje-zero sí: supervisión real con `RestartTracker` + `Backoff`, y `on_death` con el `ExitStatus`.
18
19La receta puente `recipes/arje-zero.toml` ya construye arje-zero con el lab (commit de la
20migración A0 `arje-cas`→BLAKE3). Falta el **contrato de runtime** para que sea PID 1.
21
22## 2. Qué hace arje-zero al boot (contrato observado)
23
24Fuente: `tawasuyu/03_ukupacha/arje/init/arje-zero/`. Secuencia (PID 1):
25
261. **Guarda de PID 1** (`src/main.rs`): si PID≠1, modo dev; si PID 1, **nunca retorna** (un retorno
27 ⇒ rescue shell en `/dev/console`, no panic del kernel).
282. **Superficie del kernel** (`arje-kernel::bootstrap_kernel_surface`, `init/arje-kernel/src/surface.rs`):
29 remonta `/` RW; monta `/proc`, `/sys`, `/dev` (devtmpfs), `/sys/fs/cgroup` (cgroup2),
30 y tmpfs en `/run`, `/tmp`, `/dev/pts`, `/dev/shm`; crea `/run/lock`. Best-effort e idempotente
31 (un `EBUSY` por ya-montado se ignora). `prctl(PR_SET_CHILD_SUBREAPER)`.
323. **Carga la seed** (`src/seed.rs`): lee `/ente/seed.card.json` (o `/ente/seed.card`); valida con
33 `.validate()`; si falla, no bootea. (`--restore <snapshot>` y un synth de dev son los otros
34 caminos.) La seed es un `Card` con `payload: Virtual` y `genesis: Vec<Card>`.
354. **Runtime tokio current-thread** + `primordial_loop`: levanta el **bus** (`arje-bus`) en
36 `$ENTE_BUS_SOCK` (default `$XDG_RUNTIME_DIR/ente-bus-$USER.sock`, fallback `/tmp/...`), crea el
37 `EnteGraph`, e **instancia los genesis cards** (`graph/lifecycle.rs::authorize_and_spawn`).
385. **Bucle de estado**: `SIGCHLD` → `reap` → `on_death(id, status)` → política de `Supervision`;
39 requests del bus; uevents. **No hay getty propia**: el acceso humano es un genesis card.
40
41### Tipos relevantes (`shared/card/card-core/src/lib.rs`)
42
43- `Payload::Native { exec, argv, envp }` — corre un **ELF nativo** como proceso supervisado
44 (incarnado por `arje-incarnate/src/plain.rs` con `Command::new(exec)`); obtiene PID, se reapea por
45 `SIGCHLD`. **Es como corre hammerd.**
46- `Payload::Wasm { module_sha256, entry }` — módulo WASM resuelto del CAS (`arje-cas`, hoy BLAKE3).
47- `Supervision::Restart { initial, max } | OneShot | Delegate`. `Restart` usa `sandokan-lifecycle::Backoff`
48 (exponencial; resetea si la unidad vivió ≥ `max`). **La muerte de una card `Restart` = el `CRASHED` real.**
49- La seed se serializa JSON (`Card::to_json_pretty` / `from_json`); ejemplo real:
50 `03_ukupacha/arje/seeds/arje-qemu.card.json` (un `agetty` Native con `Restart`).
51
52## 3. Qué debe proveer el rootfs de Stage 1
53
54Derivado del contrato §2. El ensamblado (`hammer-bootstrap::assemble_rootfs`) debe garantizar:
55
56| Necesidad | Detalle | Quién lo pone |
57|---|---|---|
58| **PID 1** | `/usr/bin/arje-zero` (estático) y `/sbin/init` → symlink a él | receta `arje-zero` + assemble |
59| **Mount points vacíos** | `/proc /sys /dev /run /tmp /sys/fs/cgroup /dev/pts /dev/shm` (dirs vacíos) | assemble (esqueleto FHS) |
60| **Consola de rescate** | `/dev/console`, `/dev/kmsg` accesibles | kernel/devtmpfs (arje monta `/dev`) |
61| **Seed card** | `/ente/seed.card.json` válido (§4) | assemble (genera la seed) |
62| **hammerd** | `/usr/bin/hammerd` (ya es componente de Stage 1) | receta `hammerd` |
63| **Shell** | `/bin/sh` (busybox) para rescate y para la getty | receta `busybox` |
64| **CAS** (opcional) | `/var/lib/ente/cas` + `ENTE_CAS_ROOT` — **sólo si** hay payloads Wasm | assemble (sólo si aplica) |
65| **Cards de disco** (opcional) | `/etc/arje/cards.d/*.json` para `SpawnCardFromDisk` | assemble (opcional) |
66
67> arje **monta los pseudo-FS él mismo**; el rootfs no los monta (a diferencia del inittab
68> provisional). Sólo deben existir como directorios. Esto simplifica el ensamblado.
69
70## 4. La seed card de Stage 1
71
72Un `Card` `Virtual` (la semilla, sin proceso) cuyos `genesis` son el userland supervisado. Mínimo
73viable para Stage 1:
74
75```jsonc
76{
77 "schema_version": 1,
78 "id": "<ULID estable de la seed de Stage 1>",
79 "label": "hammer-stage1",
80 "provides": ["Spawn", "Journal"],
81 "payload": "Virtual",
82 "supervision": "OneShot",
83 "genesis": [
84 {
85 "label": "hammerd",
86 "payload": { "Native": {
87 "exec": "/usr/bin/hammerd",
88 "argv": ["--store", "/store", "--journal", "/var/lib/hammer/journal"],
89 "envp": [["RUST_LOG", "info"], ["ENTE_BUS_SOCK", "/run/ente-bus.sock"]]
90 }},
91 "supervision": { "Restart": { "initial": 200, "max": 10000 } }
92 },
93 {
94 "label": "console-getty",
95 "payload": { "Native": {
96 "exec": "/bin/busybox",
97 "argv": ["getty", "-n", "-l", "/bin/sh", "115200", "console"],
98 "envp": []
99 }},
100 "supervision": { "Restart": { "initial": 200, "max": 10000 } }
101 }
102 ]
103}
104```
105
106- **`hammerd` como card `Restart`** ⇒ su muerte dispara `on_death` con el `ExitStatus`: **ese es el
107 `CRASHED` real**, manejado por el init (reinicio con backoff), no por un `respawn` ciego.
108- **`console-getty`** reemplaza el `::respawn:/bin/sh` provisional con una getty supervisada.
109- `ENTE_BUS_SOCK=/run/ente-bus.sock` fija un path estable (en boot no hay `XDG_RUNTIME_DIR`; el
110 default caería en `/tmp/ente-bus-ente.sock`). `/run` es tmpfs montado por arje.
111
112### Cómo se genera la seed (decisión abierta, con recomendación)
113
114| Opción | Cómo | Trade-off |
115|---|---|---|
116| **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 |
117| **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) |
118| **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 |
119
120**Recomendación:** **A** ahora (autocontenido, pin + validación en VM), migrar a **C** cuando A1
121(atestación) entre, porque la seed firmada necesita `arje-packager` de todos modos. **B** se
122descarta: acoplar los grafos de build de los dos repos es justo lo que C.3 evita.
123
124## 5. Del init provisional a arje (mapeo)
125
126| Init provisional (busybox/inittab) | Init real (arje-zero) |
127|---|---|
128| `::sysinit:/bin/mount -t proc …` (×4) | `arje-kernel::bootstrap_kernel_surface` (arje monta solo) |
129| `::respawn:/usr/bin/hammerd` (ciego) | genesis card `hammerd` `Native` `Restart` ⇒ **CRASHED real** + backoff |
130| `::respawn:/bin/sh` | genesis card `console-getty` `Native` `Restart` |
131| `/sbin/init` = busybox | `/sbin/init` → `/usr/bin/arje-zero` |
132| sin observabilidad | `on_death(ExitStatus)`, telemetría por `arje-bus`, brain |
133
134## 6. Fases de implementación (riesgo creciente)
135
136- **I1 — arje-zero construible.** ✅ (`recipes/arje-zero.toml`, receta puente).
137- **I2 — arje es PID 1 de Stage 1.** ✅ `STAGE1_COMPONENTS += arje-zero`; `assemble_rootfs` genera
138 `/ente/seed.card.json` (§4, opción A), provee los mount points (incl. `/ente`, `/var/lib/hammer`,
139 `/sys/fs/cgroup`, `/dev/pts`, `/dev/shm`) + `/sbin/init`→`/usr/bin/arje-zero`, y ya **no** escribe
140 el `/etc/inittab` provisional. arje supervisa hammerd ⇒ **CRASHED real manejado por el init**.
141 La seed embebida se **verificó contra el tipo real** `card_core::Card` (`from_json` + `validate()`
142 pasan), no sólo como JSON válido. El boot completo se valida en VM (no testeable sin kernel) — ver
143 el [runbook de boot en QEMU](runbooks/stage1-vm-boot.md); los unit tests cubren la seed y el
144 ensamblado, como el resto de Stage 1.
145- **I3 — bus único (plan B.2).** hammerd `announce` en `arje-bus` (cambio en hammerd) y **expone el
146 `CRASHED` a la capa de IA** de hammer (`agent.sock` emite `{"t":"crashed",…}`). El `agent.sock`
147 (JSON, API de IA) queda **encima**; `arje-bus` (postcard) es el plano de control del init.
148- **I4 — overlay + atestación.** arje monta el overlay del store (lowerdir RO `/store` / upperdir,
149 plan B.1); A1/A2: la seed gana `attest: Vec<ConcesionCapacidad>` y arje-zero verifica los binarios
150 (BLAKE3, ya alineado por A0) **antes** de incarnar — el nexo del modelo de confianza en capas
151 (el `expected_hash` de un `.swm` de hammer **es** el BLAKE3 que arje atesta).
152
153## 7. Decisiones (fijadas en este SDD)
154
155- **Generación de la seed:** template JSON autocontenido (opción A) en v1; `arje-packager` (C) al
156 llegar A1. ☐ a implementar.
157- **getty:** `busybox getty` (cero recetas nuevas) sobre `console`, no `agetty` de util-linux. ✅ decisión.
158- **PID 1 enganche:** symlink `/sbin/init` → `/usr/bin/arje-zero` (funciona con cualquier cmdline;
159 no exige `init=` en el kernel). ✅ decisión.
160- **CAS:** `ENTE_CAS_ROOT=/var/lib/ente/cas` sólo cuando haya payloads Wasm; con hammerd Native v1
161 no se monta. ✅ decisión.
162- **Bus:** `ENTE_BUS_SOCK=/run/ente-bus.sock` (path estable en boot). ✅ decisión.
163- **CRASHED a la IA:** diferido a I3/B.2 (requiere que hammerd hable `arje-bus`). En I2 el CRASHED ya
164 lo maneja el init (reinicio con backoff); exponerlo aguas arriba es I3. ☐.
165
166## 8. Hecho cuando
167
168`hammer bootstrap stage1` produce un rootfs cuyo PID 1 es arje-zero, que monta los pseudo-FS, carga
169la seed, levanta hammerd y la getty bajo supervisión real, y al matar hammerd lo reinicia con
170backoff registrando el `on_death` — el `CRASHED` real que la Fase 5 difirió, ahora en el init propio.
12-init-real.md
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):
- 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
(un EBUSY por 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 un Card con payload: Virtual y genesis: 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 el
EnteGraph, e instancia los genesis cards (graph/lifecycle.rs::authorize_and_spawn).
- 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:
{
"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; 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.