1# Booteando arje — initramfs, QEMU y bare metal
2
3`arje` es el init absorbido por brahman: `arje-zero` corre como PID 1, lee
4la **Tarjeta Semilla** (`/ente/seed.card.json`), monta `/proc /sys /dev
5/sys/fs/cgroup`, y encarna recursivamente cada Card declarada en
6`genesis` vía `arje-incarnate::Incarnator` (`clone(2)` + namespaces + cgroup v2).
7
8Este documento describe el ciclo completo:
9
101. **Layout** del initramfs.
112. **Build** del initrd (`scripts/build-arje-initrd.sh`).
123. **Boot en QEMU** (`scripts/run-arje-qemu.sh`).
134. **Boot en bare metal / VM persistente** (entrada de GRUB).
145. **Tarjeta Semilla** — qué pone, qué valida, cómo customizar.
156. **Debugging**: tracing, sockets de introspección, snapshot/restore.
16
17---
18
19## 1. Layout del initramfs
20
21El initrd es un **CPIO + gzip** (formato `newc`, estándar Linux). Su raíz:
22
23```
24/init wrapper sh, ejecuta /sbin/arje-zero
25/sbin/arje-zero PID 1 (musl-static por default)
26/usr/sbin/arje-echo
27/usr/sbin/arje-policy-provider
28/usr/sbin/arje-*-compat 13 shims D-Bus (logind, hostnamed, …)
29/usr/bin/brahman-status CLI: snapshot del broker (sesiones + matches)
30/usr/bin/busctl CLI: bus interno (list-entes, announce, …)
31/usr/bin/brainctl CLI: cerebro (rules, entropy, crystals)
32/ente/seed.card.json Tarjeta Semilla canónica
33/bin/{sh,ls,mount,…} busybox-static
34/etc, /dev, /proc, /sys, /run mountpoints vacíos
35/sys/fs/cgroup mountpoint cgroup v2
36```
37
38Path canónico de la semilla en **prod**: `/ente/seed.card.json` (ver
39`crates/core/arje-zero/src/seed.rs`). En **dev** (no PID 1):
40`./seed.card.json` en el cwd, o se sintetiza una mínima.
41
42## 2. Build del initrd
43
44### 2a. Por qué musl-static
45
46`arje-zero` corre como **PID 1**: si tiene cualquier dependencia dinámica
47(`libgcc_s.so.1`, `libc.so.6`, `ld-linux-x86-64.so.2`) que no está
48presente en el initramfs, el kernel paniquea con
49`error while loading shared libraries: libgcc_s.so.1`.
50
51Solución estándar: compilar contra el target `x86_64-unknown-linux-musl`,
52que produce un ELF **totalmente estático** (no necesita libc ni
53intérprete en runtime). Es el default del script.
54
55### 2b. Requisitos del host
56
57- `rust toolchain` con el target musl:
58 ```bash
59 rustup target add x86_64-unknown-linux-musl
60 ```
61- `musl-gcc` para crates con `build.rs` C (sled, blake3, etc.):
62 ```bash
63 apt install musl-tools # Debian/Ubuntu
64 apk add musl-dev gcc # Alpine
65 pacman -S musl # Arch
66 ```
67- `cpio`, `gzip` para empaquetar.
68- `busybox-static` para el userspace mínimo dentro del initramfs:
69 ```bash
70 apt install busybox-static # Debian/Ubuntu
71 # Alpine ya trae busybox built-in
72 ```
73
74### 2c. Invocación
75
76```bash
77# Default: musl-static, semilla de prod, sale en out/arje.initrd.cpio.gz
78scripts/build-arje-initrd.sh
79
80# Customizar seed/salida:
81scripts/build-arje-initrd.sh seeds/arje-minimal.card.json out/min.cpio.gz
82
83# busybox-static no está en $PATH:
84BUSYBOX_BIN=/path/to/busybox scripts/build-arje-initrd.sh
85
86# Vendorear binarios extra estáticos:
87EXTRA_BINS="/usr/local/bin/strace-static" scripts/build-arje-initrd.sh
88
89# (NO RECOMENDADO) build glibc dinámico — kernel panic asegurado salvo que
90# vendorees libgcc_s + libc + ld-linux a mano:
91ARJE_TARGET=x86_64-unknown-linux-gnu scripts/build-arje-initrd.sh
92```
93
94### 2d. Qué hace el script
95
961. Verifica que el target musl esté instalado; si no, da el comando
97 exacto para instalarlo.
982. `cargo build --release --target x86_64-unknown-linux-musl` para
99 `arje-zero` + los 14 shims compat.
1003. Sanity-check con `file`: aborta si algún binario quedó dinámico.
1014. Valida la seed con `seeds/validate.sh` (parse + `Card::validate`).
1025. Verifica que `busybox` sea estático (también aborta si es dinámico).
1036. Stage en un tmpdir → copia binarios + crea mountpoints + escribe `/init`.
1047. Empaqueta `find . | cpio -o -H newc | gzip -9`.
105
106Tamaño típico (musl): ~10-15 MB descomprimido, 4-6 MB el `.cpio.gz`.
107
108## 3. Boot en QEMU
109
110Requisito: un kernel Linux + qemu-system-x86_64.
111
112### 3a. Con kernel del host
113
114```bash
115# Default: usa /boot/vmlinuz-$(uname -r)
116scripts/run-arje-qemu.sh
117
118# Con kernel explícito:
119scripts/run-arje-qemu.sh out/arje.initrd.cpio.gz /boot/vmlinuz-6.6.0
120```
121
122El script invoca:
123
124```
125qemu-system-x86_64
126 -accel kvm (si /dev/kvm está, fallback tcg)
127 -m 1024 -smp 2
128 -kernel <vmlinuz>
129 -initrd out/arje.initrd.cpio.gz
130 -append "rdinit=/init console=ttyS0,115200 panic=10"
131 -nographic -serial mon:stdio
132 -no-reboot
133```
134
135`rdinit=/init` le dice al kernel que el primer programa a ejecutar es
136nuestro `/init`, no `/sbin/init`. `console=ttyS0,115200` redirige el log
137del kernel + nuestro tracing a la serial → stdio del host. Salida con
138`Ctrl-A X`.
139
140### 3b. Sin kernel del host
141
142Bajar un kernel mínimo (ej. Ubuntu cloud kernel):
143
144```bash
145wget -O /tmp/vmlinuz \
146 https://cloud-images.ubuntu.com/jammy/current/unpacked/jammy-server-cloudimg-amd64-vmlinuz-generic
147scripts/run-arje-qemu.sh out/arje.initrd.cpio.gz /tmp/vmlinuz
148```
149
150O usar el kernel de Alpine (más liviano, ~10 MB):
151
152```bash
153wget -O /tmp/vmlinuz \
154 https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/netboot/vmlinuz-lts
155```
156
157### 3c. Override del cmdline
158
159`KERNEL_CMD="ente.log=debug"` se concatena al cmdline. Cosas útiles:
160
161| flag | efecto |
162| -------------------------- | --------------------------------------------------- |
163| `panic=10` | reboot 10 s tras kernel panic (debug) |
164| `loglevel=7` | log del kernel hasta debug |
165| `quiet` | silencia banner kernel |
166| `RUST_LOG=trace` | (no se interpreta; usar env en `/init`) |
167| `init=/sbin/arje-zero` | salta `/init`, ejecuta directo (no recomendado) |
168
169`arje-zero` lee `RUST_LOG` y `BRAHMAN_*` del env. Para setearlas, editar
170`/init` antes de empaquetar, o agregar `-fw_cfg name=opt/foo,...` a qemu.
171
172### 3d. Smoke test esperado
173
174Con `seeds/arje-prod.card.json`:
175
176```
177arje-zero despierta como PID 1
178Tarjeta Semilla cargada y validada path=/ente/seed.card.json
179bus interno escuchando path=/run/arje-bus.sock
180brahman handshake escuchando (Unix) socket=/run/brahman-init.sock
181brahman admin escuchando socket=/run/brahman-admin.sock
182instanciando genesis seed=arje.seed.prod count=16
183Ente compat-logind encarnado pid=...
184Ente compat-hostnamed encarnado pid=...
185...
186arje# ← shell en tty
187```
188
189Con `seeds/arje-minimal.card.json`: solo el shell.
190
191## 4. Boot en bare metal / VM persistente
192
193El mismo `.cpio.gz` sirve como initramfs estándar de Linux. Entrada
194GRUB típica (`/etc/grub.d/40_custom` + `update-grub`):
195
196```
197menuentry "arje" {
198 linux /boot/vmlinuz-6.6.0 rdinit=/init console=tty1 panic=10
199 initrd /boot/arje.initrd.cpio.gz
200}
201```
202
203Copiar a `/boot/`:
204
205```bash
206sudo cp out/arje.initrd.cpio.gz /boot/arje.initrd.cpio.gz
207sudo update-grub
208```
209
210Para una VM (Proxmox, libvirt, etc.) basta con apuntar el "Direct Kernel
211Boot" al vmlinuz y al initrd. En Proxmox: editar `/etc/pve/qemu-server/<vmid>.conf`:
212
213```
214args: -kernel /boot/vmlinuz-6.6.0 -initrd /boot/arje.initrd.cpio.gz \
215 -append "rdinit=/init console=ttyS0,115200 panic=10"
216serial0: socket
217```
218
219### 4a. Sin pivot_root (initramfs es el rootfs final)
220
221El initrd actual **no hace pivot_root** — se queda como rootfs. Esto es
222intencional: arje no asume nada del disco. Para persistencia, las Cards
223hijas deben montar el FS de disco a demanda (ej. `genesis: [{ label:
224"mount-data", payload: Native("/bin/mount", ["/dev/sda1", "/mnt"]), … }]`).
225
226Cuando necesites pivot_root a un FS real (instalación full-disk), agregar
227un Ente que haga `switch_root` antes de instanciar el resto — pendiente
228de implementar como `Capability::SwitchRoot`.
229
230## 5. Tarjeta Semilla — detalles
231
232### 5a. Seeds estándar
233
234| seed | uso |
235| ----------------------------- | ----------------------------------------------------- |
236| `seeds/arje-minimal.card.json` | PID 1 + 1 shell `/bin/sh`. Smoke test para QEMU. |
237| `seeds/arje-prod.card.json` | Constelación completa: 14 compat shims + getty. |
238| `seeds/arje-host.card.json` | Boot en hardware real: mount/swap/dbus/red/sshd + **carmen-dm** (`mirada-compositor --greeter`) como entrada gráfica. |
239
240### 5b. Validación
241
242```bash
243seeds/validate.sh seeds/arje-prod.card.json
244```
245
246El script carga la Card vía `arje_brain::load_card_file()` (que llama a
247`brahman_card::Card::validate()`) y verifica que `arje-zero` la encarne
248hasta `instanciando genesis`.
249
250### 5c. Customizar
251
252Estructura de un genesis-child:
253
254```json
255{
256 "schema_version": 1,
257 "id": "<ULID Crockford base32 — no I L O U>",
258 "label": "mi-servicio",
259 "provides": ["Journal"],
260 "requires": [],
261 "permissions": {
262 "networking": "loopback",
263 "filesystem": "read-write",
264 "ipc": { "allow": ["wit-v1"] },
265 "processes": false
266 },
267 "soma": {
268 "namespaces": { "mount": true, "pid": true, "net": false, ...},
269 "rlimits": { "mem_bytes": 268435456 },
270 "cgroup": { "path": "ente.slice/mi-servicio", "cpu_weight": 100 }
271 },
272 "payload": { "Native": { "exec": "/usr/sbin/mi-bin", "argv": [], "envp": [] } },
273 "supervision": { "Restart": { "initial": 100, "max": 30000 } },
274 "lifecycle": "daemon",
275 "priority": "normal",
276 "flow": { "input": [], "output": [] },
277 "genesis": []
278}
279```
280
281Validación clave (`brahman_card::Card::validate`):
282
283- `schema_version == 1`.
284- `label` no vacío, ≤ 256 bytes.
285- ULID válido (Crockford base32, **sin `I L O U`**).
286- `provides ∩ requires == ∅`.
287- `Payload::Native.exec` no vacío.
288- `Payload::Wasm.module_sha256` no todo ceros.
289- rlimits: `mem_bytes > 0 && < 1 TiB`, `nproc ∈ [1, 65535]`, `nofile ∈ [1, 1M]`.
290- `cgroup.cpu_weight`, `io_weight ∈ [1, 10000]`.
291- `flow.input` y `flow.output` con nombres únicos.
292- Recursivo sobre `genesis`.
293
294## 6. Debugging
295
296### 6a. Tracing
297
298`arje-zero` usa `tracing-subscriber`. Default: `arje_zero=debug,info`.
299Override con `RUST_LOG`:
300
301```bash
302# En el initrd: editar /init antes de empaquetar
303echo 'export RUST_LOG="trace"' >> wrapper
304```
305
306### 6b. Sockets de introspección (Unix, dentro del initrd)
307
308Sin `XDG_RUNTIME_DIR` ni `TMPDIR` en el env de PID 1 (caso del initrd),
309los sockets caen al fallback `/tmp/`:
310
311| socket | env override | servicio |
312| ---------------------------- | ----------------------- | ----------------------------------------- |
313| `/tmp/arje-bus-*.sock` | `ENTE_BUS_SOCK` | bus interno (Announce/Invoke/ListEntes) |
314| `/tmp/brahman-init.sock` | `BRAHMAN_INIT_SOCKET` | handshake brahman |
315| `/tmp/brahman-admin.sock` | `BRAHMAN_ADMIN_SOCKET` | snapshots de sesiones + matches |
316| `/tmp/arje-brain.sock` | `ENTE_BRAIN_SOCK` | introspección del cerebro (rules, observer) |
317
318El initrd trae 3 CLI clients precompilados en `/usr/bin/`:
319
320```sh
321brahman-status # snapshot completo: sesiones + matches + WIT + flows
322busctl list-entes # entes anunciados en el bus
323busctl announce # smoke test: anuncia una Card desde el shell
324busctl power-off # apaga el fractal
325brainctl list-rules # reglas crystallizadas + manuales
326brainctl entropy # entropía del observer estadístico
327brainctl top 10 # top-10 kinds por frecuencia
328brainctl crystals # patrones detectados con auto-promote
329brainctl crystal-json 0 # exporta un crystal específico como Rule JSON
330```
331
332Ejemplo de sesión típica dentro de la VM:
333
334```sh
335arje# brahman-status
336Init: server=0.1.0 protocol=0.1.0 attached=true
337Context: (sin set)
338
339Sessions (3):
340 [ente] 01HQ5R2K… compat-logind lifecycle=Daemon priority=High
341 [ente] 01HQ5R2K… compat-hostnamed lifecycle=Daemon priority=Normal
342 [ente] 01HQ5R2K… echo-smoke lifecycle=Daemon priority=Low
343
344Matches (0):
345 (ninguno)
346
347arje# busctl list-entes
34814 entes en el bus:
349 01HQ… compat-logind provides=[LegacyLogind]
350 01HQ… compat-journald provides=[Journal]
351 …
352
353arje# brainctl top 5
354spawn 14
355arje_died 0
356…
357```
358
359Si en cambio querés conectar crudo con `socat`/`nc`, agregalo con
360`EXTRA_BINS=/usr/bin/socat-static scripts/build-arje-initrd.sh` y luego:
361
362```sh
363arje# socat - UNIX-CONNECT:/tmp/brahman-admin.sock | jq .
364```
365
366### 6c. Snapshot / restore
367
368```
369arje-zero --checkpoint /ente/checkpoint.json # escribe al cerrar
370arje-zero --restore /ente/checkpoint.json # reconstruye al boot
371```
372
373Snapshot adjunto del cerebro: `/ente/checkpoint.brain.json`.
374
375### 6d. Metrics
376
377```
378arje-zero --metrics-addr 127.0.0.1:9911
379```
380
381Endpoint Prometheus desde dentro de la VM. Para exponerlo al host bajo
382QEMU, agregar `-netdev user,hostfwd=tcp::9911-:9911 -device virtio-net,netdev=…`.
383
384### 6e. Modo DEV en host (sin PID 1)
385
386`arje-zero` detecta si su PID != 1 y entra en **DEV MODE**: no monta
387kernel surface, no se vuelve subreaper, sale tras 4 s. Útil para
388iterar Cards en el host:
389
390```bash
391mkdir /tmp/arje-test && cp seeds/arje-minimal.card.json /tmp/arje-test/seed.card.json
392cd /tmp/arje-test && target/release/arje-zero
393```
394
395## 7. Troubleshooting
396
397### 7a. `error while loading shared libraries: libgcc_s.so.1` al boot
398
399**Causa**: el `/sbin/arje-zero` del initrd se compiló contra glibc
400(`x86_64-unknown-linux-gnu`) y depende dinámicamente de `libgcc_s.so.1`,
401`libc.so.6`, `ld-linux-x86-64.so.2`. Esas libs no están en el initramfs,
402el kernel mata PID 1 → panic.
403
404**Fix**: usar el target musl (default del script desde mayo 2026):
405
406```bash
407rustup target add x86_64-unknown-linux-musl
408apt install musl-tools # Debian/Ubuntu — para crates con build.rs C
409scripts/build-arje-initrd.sh # ya usa musl por default
410```
411
412Verificar manualmente que el binario quedó estático:
413
414```bash
415file target/x86_64-unknown-linux-musl/release/arje-zero
416# → "ELF 64-bit LSB executable, ..., statically linked, ..."
417```
418
419Si decidís quedarte en glibc dinámico, hay que vendorear `libgcc_s.so.1`,
420`libc.so.6` y `ld-linux-x86-64.so.2` (más cualquier dep de `tracing` y
421`tokio`) bajo `/lib64/` y `/lib/x86_64-linux-gnu/` del initrd. No
422recomendado.
423
424### 7b. Kernel panic: `Kernel panic - not syncing: Attempted to kill init!`
425
426**Causa**: `arje-zero` salió con error antes de bind del bus, o `/init`
427crashed antes de exec. PID 1 muriendo = panic kernel.
428
429**Fix**: agregar `panic=10` al cmdline (ya está en el script) para
430auto-reboot tras 10 s y ver el error. Bajo QEMU, `-no-reboot` lo
431convierte en exit limpio. Capturar el log:
432
433```bash
434HEADLESS=1 scripts/run-arje-qemu.sh 2>&1 | tee /tmp/arje-boot.log
435```
436
437Buscar la línea anterior a `Attempted to kill init`.
438
439### 7c. `seed inválida` al correr build-arje-initrd.sh
440
441**Causa**: el JSON tiene un ULID con caracteres prohibidos (`I L O U`
442están **excluidos** de Crockford base32), un `Payload::Native.exec`
443vacío, o un campo del schema rechazado por `brahman_card::Card::validate`.
444
445**Fix**: correr el validador directo para ver el error completo:
446
447```bash
448seeds/validate.sh seeds/mi-seed.card.json
449# Si falla, las primeras 40 líneas del log van a stderr.
450```
451
452### 7d. `cannot find -lcrypt` o `cannot find -lpam` al compilar musl
453
454**Causa**: algún crate del workspace pulled in una dep C que no
455encontró su lib bajo musl.
456
457**Fix**: para los crates de arje no debería pasar (revisamos las deps).
458Si ocurre con un crate nuevo, suele resolverse con `RUSTFLAGS="-C
459target-feature=+crt-static"` o aislando el feature problemático.
460
461---
462
463## Apéndice — checklist pre-deploy
464
465- [ ] `rustup target add x86_64-unknown-linux-musl` instalado en el host build.
466- [ ] `musl-tools` (Debian/Ubuntu) o equivalente disponible.
467- [ ] `busybox-static` disponible para vendorear userspace.
468- [ ] `cargo build --release --target x86_64-unknown-linux-musl -p arje-zero`
469 compila sin warnings.
470- [ ] `file target/x86_64-unknown-linux-musl/release/arje-zero` reporta
471 "statically linked".
472- [ ] `seeds/validate.sh seeds/arje-prod.card.json` → OK.
473- [ ] `scripts/build-arje-initrd.sh` produce `out/arje.initrd.cpio.gz`.
474- [ ] `scripts/run-arje-qemu.sh` arranca y muestra `Tarjeta Semilla cargada
475 y validada` + `instanciando genesis count=16` (o el count que toque).
476- [ ] Si vas a bare metal: tener un kernel `vmlinuz` rescue (ej. Alpine
477 netboot) en `/boot/` por si arje no levanta.
478- [ ] Para VMs Proxmox/libvirt: serial console habilitada para ver el
479 arranque sin display.
arje-boot.md
Booteando arje — initramfs, QEMU y bare metal
arje es el init absorbido por brahman: arje-zero corre como PID 1, lee
la Tarjeta Semilla (/ente/seed.card.json), monta /proc /sys /dev /sys/fs/cgroup, y encarna recursivamente cada Card declarada en
genesis vía arje-incarnate::Incarnator (clone(2) + namespaces + cgroup v2).
Este documento describe el ciclo completo:
- Layout del initramfs.
- Build del initrd (
scripts/build-arje-initrd.sh).
- Boot en QEMU (
scripts/run-arje-qemu.sh).
- Boot en bare metal / VM persistente (entrada de GRUB).
- Tarjeta Semilla — qué pone, qué valida, cómo customizar.
- Debugging: tracing, sockets de introspección, snapshot/restore.
1. Layout del initramfs
El initrd es un CPIO + gzip (formato newc, estándar Linux). Su raíz:
/init wrapper sh, ejecuta /sbin/arje-zero
/sbin/arje-zero PID 1 (musl-static por default)
/usr/sbin/arje-echo
/usr/sbin/arje-policy-provider
/usr/sbin/arje-*-compat 13 shims D-Bus (logind, hostnamed, …)
/usr/bin/brahman-status CLI: snapshot del broker (sesiones + matches)
/usr/bin/busctl CLI: bus interno (list-entes, announce, …)
/usr/bin/brainctl CLI: cerebro (rules, entropy, crystals)
/ente/seed.card.json Tarjeta Semilla canónica
/bin/{sh,ls,mount,…} busybox-static
/etc, /dev, /proc, /sys, /run mountpoints vacíos
/sys/fs/cgroup mountpoint cgroup v2
Path canónico de la semilla en prod: /ente/seed.card.json (ver
crates/core/arje-zero/src/seed.rs). En dev (no PID 1):
./seed.card.json en el cwd, o se sintetiza una mínima.
2. Build del initrd
2a. Por qué musl-static
arje-zero corre como PID 1: si tiene cualquier dependencia dinámica
(libgcc_s.so.1, libc.so.6, ld-linux-x86-64.so.2) que no está
presente en el initramfs, el kernel paniquea con
error while loading shared libraries: libgcc_s.so.1.
Solución estándar: compilar contra el target x86_64-unknown-linux-musl,
que produce un ELF totalmente estático (no necesita libc ni
intérprete en runtime). Es el default del script.
2b. Requisitos del host
rust toolchain con el target musl:
rustup target add x86_64-unknown-linux-musl
musl-gcc para crates con build.rs C (sled, blake3, etc.):
apt install musl-tools # Debian/Ubuntu
apk add musl-dev gcc # Alpine
pacman -S musl # Arch
cpio, gzip para empaquetar.
busybox-static para el userspace mínimo dentro del initramfs:
apt install busybox-static # Debian/Ubuntu
# Alpine ya trae busybox built-in
2c. Invocación
# Default: musl-static, semilla de prod, sale en out/arje.initrd.cpio.gz
scripts/build-arje-initrd.sh
# Customizar seed/salida:
scripts/build-arje-initrd.sh seeds/arje-minimal.card.json out/min.cpio.gz
# busybox-static no está en $PATH:
BUSYBOX_BIN=/path/to/busybox scripts/build-arje-initrd.sh
# Vendorear binarios extra estáticos:
EXTRA_BINS="/usr/local/bin/strace-static" scripts/build-arje-initrd.sh
# (NO RECOMENDADO) build glibc dinámico — kernel panic asegurado salvo que
# vendorees libgcc_s + libc + ld-linux a mano:
ARJE_TARGET=x86_64-unknown-linux-gnu scripts/build-arje-initrd.sh
2d. Qué hace el script
- Verifica que el target musl esté instalado; si no, da el comando
exacto para instalarlo.
cargo build --release --target x86_64-unknown-linux-musl para
arje-zero + los 14 shims compat.
- Sanity-check con
file: aborta si algún binario quedó dinámico.
- Valida la seed con
seeds/validate.sh (parse + Card::validate).
- Verifica que
busybox sea estático (también aborta si es dinámico).
- Stage en un tmpdir → copia binarios + crea mountpoints + escribe
/init.
- Empaqueta
find . | cpio -o -H newc | gzip -9.
Tamaño típico (musl): ~10-15 MB descomprimido, 4-6 MB el .cpio.gz.
3. Boot en QEMU
Requisito: un kernel Linux + qemu-system-x86_64.
3a. Con kernel del host
# Default: usa /boot/vmlinuz-$(uname -r)
scripts/run-arje-qemu.sh
# Con kernel explícito:
scripts/run-arje-qemu.sh out/arje.initrd.cpio.gz /boot/vmlinuz-6.6.0
El script invoca:
qemu-system-x86_64
-accel kvm (si /dev/kvm está, fallback tcg)
-m 1024 -smp 2
-kernel <vmlinuz>
-initrd out/arje.initrd.cpio.gz
-append "rdinit=/init console=ttyS0,115200 panic=10"
-nographic -serial mon:stdio
-no-reboot
rdinit=/init le dice al kernel que el primer programa a ejecutar es
nuestro /init, no /sbin/init. console=ttyS0,115200 redirige el log
del kernel + nuestro tracing a la serial → stdio del host. Salida con
Ctrl-A X.
3b. Sin kernel del host
Bajar un kernel mínimo (ej. Ubuntu cloud kernel):
wget -O /tmp/vmlinuz \
https://cloud-images.ubuntu.com/jammy/current/unpacked/jammy-server-cloudimg-amd64-vmlinuz-generic
scripts/run-arje-qemu.sh out/arje.initrd.cpio.gz /tmp/vmlinuz
O usar el kernel de Alpine (más liviano, ~10 MB):
wget -O /tmp/vmlinuz \
https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/netboot/vmlinuz-lts
3c. Override del cmdline
KERNEL_CMD="ente.log=debug" se concatena al cmdline. Cosas útiles:
| flag | efecto |
panic=10 | reboot 10 s tras kernel panic (debug) |
loglevel=7 | log del kernel hasta debug |
quiet | silencia banner kernel |
RUST_LOG=trace | (no se interpreta; usar env en /init) |
init=/sbin/arje-zero | salta /init, ejecuta directo (no recomendado) |
arje-zero lee RUST_LOG y BRAHMAN_* del env. Para setearlas, editar
/init antes de empaquetar, o agregar -fw_cfg name=opt/foo,... a qemu.
3d. Smoke test esperado
Con seeds/arje-prod.card.json:
arje-zero despierta como PID 1
Tarjeta Semilla cargada y validada path=/ente/seed.card.json
bus interno escuchando path=/run/arje-bus.sock
brahman handshake escuchando (Unix) socket=/run/brahman-init.sock
brahman admin escuchando socket=/run/brahman-admin.sock
instanciando genesis seed=arje.seed.prod count=16
Ente compat-logind encarnado pid=...
Ente compat-hostnamed encarnado pid=...
...
arje# ← shell en tty
Con seeds/arje-minimal.card.json: solo el shell.
4. Boot en bare metal / VM persistente
El mismo .cpio.gz sirve como initramfs estándar de Linux. Entrada
GRUB típica (/etc/grub.d/40_custom + update-grub):
menuentry "arje" {
linux /boot/vmlinuz-6.6.0 rdinit=/init console=tty1 panic=10
initrd /boot/arje.initrd.cpio.gz
}
Copiar a /boot/:
sudo cp out/arje.initrd.cpio.gz /boot/arje.initrd.cpio.gz
sudo update-grub
Para una VM (Proxmox, libvirt, etc.) basta con apuntar el "Direct Kernel
Boot" al vmlinuz y al initrd. En Proxmox: editar /etc/pve/qemu-server/<vmid>.conf:
args: -kernel /boot/vmlinuz-6.6.0 -initrd /boot/arje.initrd.cpio.gz \
-append "rdinit=/init console=ttyS0,115200 panic=10"
serial0: socket
4a. Sin pivot_root (initramfs es el rootfs final)
El initrd actual no hace pivot_root — se queda como rootfs. Esto es
intencional: arje no asume nada del disco. Para persistencia, las Cards
hijas deben montar el FS de disco a demanda (ej. genesis: [{ label: "mount-data", payload: Native("/bin/mount", ["/dev/sda1", "/mnt"]), … }]).
Cuando necesites pivot_root a un FS real (instalación full-disk), agregar
un Ente que haga switch_root antes de instanciar el resto — pendiente
de implementar como Capability::SwitchRoot.
5. Tarjeta Semilla — detalles
5a. Seeds estándar
| seed | uso |
seeds/arje-minimal.card.json | PID 1 + 1 shell /bin/sh. Smoke test para QEMU. |
seeds/arje-prod.card.json | Constelación completa: 14 compat shims + getty. |
seeds/arje-host.card.json | Boot en hardware real: mount/swap/dbus/red/sshd + carmen-dm (mirada-compositor --greeter) como entrada gráfica. |
5b. Validación
seeds/validate.sh seeds/arje-prod.card.json
El script carga la Card vía arje_brain::load_card_file() (que llama a
brahman_card::Card::validate()) y verifica que arje-zero la encarne
hasta instanciando genesis.
5c. Customizar
Estructura de un genesis-child:
{
"schema_version": 1,
"id": "<ULID Crockford base32 — no I L O U>",
"label": "mi-servicio",
"provides": ["Journal"],
"requires": [],
"permissions": {
"networking": "loopback",
"filesystem": "read-write",
"ipc": { "allow": ["wit-v1"] },
"processes": false
},
"soma": {
"namespaces": { "mount": true, "pid": true, "net": false, ...},
"rlimits": { "mem_bytes": 268435456 },
"cgroup": { "path": "ente.slice/mi-servicio", "cpu_weight": 100 }
},
"payload": { "Native": { "exec": "/usr/sbin/mi-bin", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon",
"priority": "normal",
"flow": { "input": [], "output": [] },
"genesis": []
}
Validación clave (brahman_card::Card::validate):
schema_version == 1.
label no vacío, ≤ 256 bytes.
- ULID válido (Crockford base32, sin
I L O U).
provides ∩ requires == ∅.
Payload::Native.exec no vacío.
Payload::Wasm.module_sha256 no todo ceros.
- rlimits:
mem_bytes > 0 && < 1 TiB, nproc ∈ [1, 65535], nofile ∈ [1, 1M].
cgroup.cpu_weight, io_weight ∈ [1, 10000].
flow.input y flow.output con nombres únicos.
- Recursivo sobre
genesis.
6. Debugging
6a. Tracing
arje-zero usa tracing-subscriber. Default: arje_zero=debug,info.
Override con RUST_LOG:
# En el initrd: editar /init antes de empaquetar
echo 'export RUST_LOG="trace"' >> wrapper
6b. Sockets de introspección (Unix, dentro del initrd)
Sin XDG_RUNTIME_DIR ni TMPDIR en el env de PID 1 (caso del initrd),
los sockets caen al fallback /tmp/:
| socket | env override | servicio |
/tmp/arje-bus-*.sock | ENTE_BUS_SOCK | bus interno (Announce/Invoke/ListEntes) |
/tmp/brahman-init.sock | BRAHMAN_INIT_SOCKET | handshake brahman |
/tmp/brahman-admin.sock | BRAHMAN_ADMIN_SOCKET | snapshots de sesiones + matches |
/tmp/arje-brain.sock | ENTE_BRAIN_SOCK | introspección del cerebro (rules, observer) |
El initrd trae 3 CLI clients precompilados en /usr/bin/:
brahman-status # snapshot completo: sesiones + matches + WIT + flows
busctl list-entes # entes anunciados en el bus
busctl announce # smoke test: anuncia una Card desde el shell
busctl power-off # apaga el fractal
brainctl list-rules # reglas crystallizadas + manuales
brainctl entropy # entropía del observer estadístico
brainctl top 10 # top-10 kinds por frecuencia
brainctl crystals # patrones detectados con auto-promote
brainctl crystal-json 0 # exporta un crystal específico como Rule JSON
Ejemplo de sesión típica dentro de la VM:
arje# brahman-status
Init: server=0.1.0 protocol=0.1.0 attached=true
Context: (sin set)
Sessions (3):
[ente] 01HQ5R2K… compat-logind lifecycle=Daemon priority=High
[ente] 01HQ5R2K… compat-hostnamed lifecycle=Daemon priority=Normal
[ente] 01HQ5R2K… echo-smoke lifecycle=Daemon priority=Low
Matches (0):
(ninguno)
arje# busctl list-entes
14 entes en el bus:
01HQ… compat-logind provides=[LegacyLogind]
01HQ… compat-journald provides=[Journal]
…
arje# brainctl top 5
spawn 14
arje_died 0
…
Si en cambio querés conectar crudo con socat/nc, agregalo con
EXTRA_BINS=/usr/bin/socat-static scripts/build-arje-initrd.sh y luego:
arje# socat - UNIX-CONNECT:/tmp/brahman-admin.sock | jq .
6c. Snapshot / restore
arje-zero --checkpoint /ente/checkpoint.json # escribe al cerrar
arje-zero --restore /ente/checkpoint.json # reconstruye al boot
Snapshot adjunto del cerebro: /ente/checkpoint.brain.json.
6d. Metrics
arje-zero --metrics-addr 127.0.0.1:9911
Endpoint Prometheus desde dentro de la VM. Para exponerlo al host bajo
QEMU, agregar -netdev user,hostfwd=tcp::9911-:9911 -device virtio-net,netdev=….
6e. Modo DEV en host (sin PID 1)
arje-zero detecta si su PID != 1 y entra en DEV MODE: no monta
kernel surface, no se vuelve subreaper, sale tras 4 s. Útil para
iterar Cards en el host:
mkdir /tmp/arje-test && cp seeds/arje-minimal.card.json /tmp/arje-test/seed.card.json
cd /tmp/arje-test && target/release/arje-zero
7. Troubleshooting
7a. error while loading shared libraries: libgcc_s.so.1 al boot
Causa: el /sbin/arje-zero del initrd se compiló contra glibc
(x86_64-unknown-linux-gnu) y depende dinámicamente de libgcc_s.so.1,
libc.so.6, ld-linux-x86-64.so.2. Esas libs no están en el initramfs,
el kernel mata PID 1 → panic.
Fix: usar el target musl (default del script desde mayo 2026):
rustup target add x86_64-unknown-linux-musl
apt install musl-tools # Debian/Ubuntu — para crates con build.rs C
scripts/build-arje-initrd.sh # ya usa musl por default
Verificar manualmente que el binario quedó estático:
file target/x86_64-unknown-linux-musl/release/arje-zero
# → "ELF 64-bit LSB executable, ..., statically linked, ..."
Si decidís quedarte en glibc dinámico, hay que vendorear libgcc_s.so.1,
libc.so.6 y ld-linux-x86-64.so.2 (más cualquier dep de tracing y
tokio) bajo /lib64/ y /lib/x86_64-linux-gnu/ del initrd. No
recomendado.
7b. Kernel panic: Kernel panic - not syncing: Attempted to kill init!
Causa: arje-zero salió con error antes de bind del bus, o /init
crashed antes de exec. PID 1 muriendo = panic kernel.
Fix: agregar panic=10 al cmdline (ya está en el script) para
auto-reboot tras 10 s y ver el error. Bajo QEMU, -no-reboot lo
convierte en exit limpio. Capturar el log:
HEADLESS=1 scripts/run-arje-qemu.sh 2>&1 | tee /tmp/arje-boot.log
Buscar la línea anterior a Attempted to kill init.
7c. seed inválida al correr build-arje-initrd.sh
Causa: el JSON tiene un ULID con caracteres prohibidos (I L O U
están excluidos de Crockford base32), un Payload::Native.exec
vacío, o un campo del schema rechazado por brahman_card::Card::validate.
Fix: correr el validador directo para ver el error completo:
seeds/validate.sh seeds/mi-seed.card.json
# Si falla, las primeras 40 líneas del log van a stderr.
7d. cannot find -lcrypt o cannot find -lpam al compilar musl
Causa: algún crate del workspace pulled in una dep C que no
encontró su lib bajo musl.
Fix: para los crates de arje no debería pasar (revisamos las deps).
Si ocurre con un crate nuevo, suele resolverse con RUSTFLAGS="-C target-feature=+crt-static" o aislando el feature problemático.
Apéndice — checklist pre-deploy