# 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 <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):

```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/<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.      |

### 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": "<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`:

```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.
