Files
brahman/docs/arje-boot.md
T
sergio b83d40a833 refactor(naming): A1 — ente→arje, vista→revista, pluma→fana
Rename batch de la Fase A del PLAN_MACRO:
- 25 crates ente-* → arje-* (protocol/init/runtime/compat). El linaje
  arje (init Linux) queda con prefijo coherente.
- vista → revista (revista-core + revista-web).
- pluma → fana (fana-md + fana-md-reader-web). fana absorbe el linaje
  markdown de pluma; será el writer DAG editor (prioridad alta).

Cambios:
- git mv de 29 crate dirs + 2 SDDs
- package/lib/bin names + path refs + imports .rs reescritos
- workspace Cargo.toml + comentarios de sección
- SDDs de init/runtime/compat/protocol actualizados a arje-
- SDD de revista + SDD de fana (reescrito: writer DAG editor)
- docs/STATUS.md, ROADMAP.md, PLAN_MACRO.md, arje-boot.md,
  arje-replace-systemd.md actualizados
- docs/changelog/akasha.md → chasqui.md

scripts/rename-fase-a.py idempotente (--dry-run soportado).
cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:10:14 +00:00

16 KiB

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

  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

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

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

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