# 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: 1. **Layout** del initramfs. 2. **Build** del initrd (`scripts/build-arje-initrd.sh`). 3. **Boot en QEMU** (`scripts/run-arje-qemu.sh`). 4. **Boot en bare metal / VM persistente** (entrada de GRUB). 5. **Tarjeta Semilla** — qué pone, qué valida, cómo customizar. 6. **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: ```bash rustup target add x86_64-unknown-linux-musl ``` - `musl-gcc` para crates con `build.rs` C (sled, blake3, etc.): ```bash 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: ```bash apt install busybox-static # Debian/Ubuntu # Alpine ya trae busybox built-in ``` ### 2c. Invocación ```bash # 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 1. Verifica que el target musl esté instalado; si no, da el comando exacto para instalarlo. 2. `cargo build --release --target x86_64-unknown-linux-musl` para `arje-zero` + los 14 shims compat. 3. Sanity-check con `file`: aborta si algún binario quedó dinámico. 4. Valida la seed con `seeds/validate.sh` (parse + `Card::validate`). 5. Verifica que `busybox` sea estático (también aborta si es dinámico). 6. Stage en un tmpdir → copia binarios + crea mountpoints + escribe `/init`. 7. 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 ```bash # 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 -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): ```bash 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): ```bash 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/`: ```bash 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/.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. | ### 5b. Validación ```bash 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: ```json { "schema_version": 1, "id": "", "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`: ```bash # 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/`: ```sh 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: ```sh 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: ```sh 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: ```bash 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): ```bash 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: ```bash 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: ```bash 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: ```bash 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 - [ ] `rustup target add x86_64-unknown-linux-musl` instalado en el host build. - [ ] `musl-tools` (Debian/Ubuntu) o equivalente disponible. - [ ] `busybox-static` disponible para vendorear userspace. - [ ] `cargo build --release --target x86_64-unknown-linux-musl -p arje-zero` compila sin warnings. - [ ] `file target/x86_64-unknown-linux-musl/release/arje-zero` reporta "statically linked". - [ ] `seeds/validate.sh seeds/arje-prod.card.json` → OK. - [ ] `scripts/build-arje-initrd.sh` produce `out/arje.initrd.cpio.gz`. - [ ] `scripts/run-arje-qemu.sh` arranca y muestra `Tarjeta Semilla cargada y validada` + `instanciando genesis count=16` (o el count que toque). - [ ] Si vas a bare metal: tener un kernel `vmlinuz` rescue (ej. Alpine netboot) en `/boot/` por si arje no levanta. - [ ] Para VMs Proxmox/libvirt: serial console habilitada para ver el arranque sin display.