docs(arje): organiza core/ + seeds canónicas + boot reproducible

- crates/core/README.md: agrupamiento lógico de los 31 crates absorbidos
  de arje (ente-*) y del protocolo brahman (brahman-*) en 6 grupos —
  Init/PID 1, contratos, discovery, IPC+CAS, cerebro, 14 shims compat
  systemd. No se movieron crates físicamente (rompería paths
  cross-workspace).

- seeds/arje-minimal.card.json: PID1 + /bin/sh, smoke test QEMU.
- seeds/arje-prod.card.json: PID1 + 14 shims compat + tmpfiles/binfmt
  one-shots + echo + getty (16 children). Validados con
  brahman_card::Card::validate.
- seeds/validate.sh: carga la seed vía ente-zero en dev mode.

- scripts/build-arje-initrd.sh: empaqueta CPIO+gzip newc layout
  /init→/sbin/ente-zero, /usr/sbin/ente-*-compat, /ente/seed.card.json,
  /bin/{sh,...} (busybox o glibc+ldd). Tested: produce 20 MB initrd OK.
- scripts/run-arje-qemu.sh: qemu-system-x86_64 con KVM auto-detect,
  -kernel/-initrd/-append "rdinit=/init console=ttyS0,115200 panic=10".

- docs/arje-boot.md: doc end-to-end — layout initramfs, QEMU (con kernel
  del host o externo), GRUB bare metal, Proxmox/libvirt args:, schema
  de Card con todas las validaciones, debugging (sockets de
  introspección, snapshot/restore, metrics), checklist pre-deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-18 18:40:05 +00:00
parent a92fa15777
commit 8592bab19e
8 changed files with 1046 additions and 0 deletions
+1
View File
@@ -3,3 +3,4 @@
Cargo.lock.bak
.DS_Store
.claude/
/out/
+100
View File
@@ -0,0 +1,100 @@
# `crates/core/` — Init Arje (absorbido) + Protocolo Brahman
El directorio agrupa **dos linajes** que se fusionaron al absorberse arje
dentro del workspace de brahman:
| linaje | prefijo | función |
| ----------- | ------------ | ------------------------------------------------ |
| `arje` | `ente-*` | Init (PID 1), encarnación Linux, compat systemd |
| `brahman` | `brahman-*` | Tarjeta canónica, handshake, broker, admin |
No están en sub-carpetas físicas porque el workspace declara los paths uno
a uno en `Cargo.toml` raíz y muchos `Cargo.toml` hijos usan `path =
"../ente-X"`. El agrupamiento siguiente es **lógico**: cada crate se
encuentra como `crates/core/<nombre>`.
---
## 1. Init / PID 1
| crate | tipo | resumen |
| --------------- | ---------- | ------------------------------------------------------------------ |
| `ente-zero` | binario | PID 1 del fractal. Bucle primordial (reap + bus + handshake). |
| `ente-kernel` | lib | `bootstrap_kernel_surface()`, subreaper, SIGCHLD/uevent streams. |
| `ente-soma` | lib (shim) | Re-export sobre `crates/shared/ente-incarnate` (clone+ns+cgroup). |
| `ente-snapshot` | lib | `FractalSnapshot` JSON — checkpoint/restore del grafo de Cards. |
## 2. Contratos canónicos
| crate | resumen |
| ------------------ | -------------------------------------------------------------------- |
| `brahman-card` | `Card { soma, payload, flow, permissions, supervision, genesis }`. |
| `brahman-card-wit` | Extracción de interfaces WIT de componentes WASM. |
| `brahman-cards` | Helpers para construir Cards típicas (consumer/producer/broker). |
| `ente-card` | Alias histórico — re-export de `brahman-card` con nombres legacy. |
## 3. Discovery / Routing
| crate | resumen |
| -------------------- | ------------------------------------------------------------------ |
| `brahman-handshake` | Protocolo Init↔módulo (Hello, Ping, ListSessions) postcard/Unix. |
| `brahman-broker` | Service locator: empareja `flow.input``flow.output` por tipo. |
| `brahman-admin` | Socket separado para snapshots de sesiones + matches. |
## 4. IPC interno + Storage
| crate | resumen |
| ------------- | ----------------------------------------------------------------------- |
| `ente-bus` | Unix SOCK_STREAM con framing postcard. `Announce`/`Invoke`/`ListEntes`. |
| `ente-cas` | Content-addressed storage SHA-256 (blobs Wasm, audit log). |
| `ente-wasm` | Encarna `Payload::Wasm` vía `wasmi` en thread dedicado. |
## 5. Cerebro / Observabilidad
| crate | resumen |
| ------------- | ------------------------------------------------------------------------ |
| `ente-brain` | Rule engine + observer estadístico + audit log con hash chain a CAS. |
| `ente-echo` | Ente de prueba — provee `Capability::Endpoint(echo)` para smoke tests. |
## 6. Compat systemd (shims D-Bus)
Cada shim es un binario que se anuncia con un nombre well-known
`org.freedesktop.X1` y traduce las llamadas al bus interno. Esto permite
que GNOME/KDE/aplicaciones legacy arranquen sobre arje sin systemd:
| binario | reemplaza | nombre D-Bus |
| --------------------------- | --------------------- | ----------------------------------- |
| `ente-logind-compat` | `systemd-logind` | `org.freedesktop.login1` |
| `ente-hostnamed-compat` | `systemd-hostnamed` | `org.freedesktop.hostname1` |
| `ente-timedated-compat` | `systemd-timedated` | `org.freedesktop.timedate1` |
| `ente-localed-compat` | `systemd-localed` | `org.freedesktop.locale1` |
| `ente-journald-compat` | `systemd-journald` | `org.freedesktop.LogControl1` |
| `ente-resolved-compat` | `systemd-resolved` | `org.freedesktop.resolve1` |
| `ente-polkit-compat` | `polkitd` | `org.freedesktop.PolicyKit1` |
| `ente-machined-compat` | `systemd-machined` | `org.freedesktop.machine1` |
| `ente-systemd1-compat` | `systemd` (Manager) | `org.freedesktop.systemd1` |
| `ente-notify-compat` | `sd_notify` socket | `/run/systemd/notify` (datagram) |
| `ente-timer-compat` | `systemd-timer` | (cron-like, sin D-Bus) |
| `ente-tmpfiles-compat` | `systemd-tmpfiles` | (aplica tmpfiles.d al boot) |
| `ente-binfmt-compat` | `systemd-binfmt` | (registra binfmt_misc handlers) |
| `ente-policy-provider` | (interno) | proveedor de decisiones polkit |
---
## Crates relacionados fuera de `core/`
Dependen del Init pero viven en `crates/shared/`:
- `ente-incarnate` — rutina pura de `clone(2) + namespaces + cgroup +
rlimits + cpu_affinity`. Reusable por shipote y supervisores no-PID-1.
- `brahman-net` — malla P2P opcional (libp2p) que extiende el handshake.
- `brahman-sidecar` — helper `spawn(card)` para que las apps se presenten
al Init sin reimplementar el cliente del handshake.
## Convención de uso
Para arrancar el Init y ejecutar Cards, ver:
- **Seeds estándar** en `seeds/`.
- **Build de initramfs** con `scripts/build-arje-initrd.sh`.
- **Boot en QEMU / bare metal** documentado en `docs/arje-boot.md`.
+317
View File
@@ -0,0 +1,317 @@
# Booteando arje — initramfs, QEMU y bare metal
`arje` es el init absorbido por brahman: `ente-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 `ente-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/ente-zero
/sbin/ente-zero PID 1
/usr/sbin/ente-echo
/usr/sbin/ente-policy-provider
/usr/sbin/ente-*-compat 13 shims D-Bus (logind, hostnamed, …)
/ente/seed.card.json Tarjeta Semilla canónica
/bin/{sh,ls,mount,…} busybox-static (o glibc dinámica + libs)
/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/ente-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
Requisitos host: `cargo`, `cpio`, `gzip`, y opcionalmente `busybox-static`
(en Debian/Ubuntu: `apt install busybox-static`).
```bash
# Default: semilla de prod, sale en out/arje.initrd.cpio.gz
scripts/build-arje-initrd.sh
# Customizar:
scripts/build-arje-initrd.sh seeds/arje-minimal.card.json out/min.cpio.gz
# Sin busybox del sistema, apuntar a uno propio:
BUSYBOX_BIN=/path/to/busybox scripts/build-arje-initrd.sh
# Vendorear binarios extra (separados por espacio):
EXTRA_BINS="/usr/bin/strace /usr/bin/gdb" scripts/build-arje-initrd.sh
```
El script:
1. Corre `cargo build --release` para `ente-zero` + los 14 shims compat.
2. Valida la seed con `seeds/validate.sh` (parse + `Card::validate`).
3. Stage en un tmpdir → copia binarios + crea mountpoints + escribe `/init`.
4. Empaqueta `find . | cpio -o -H newc | gzip -9`.
Tamaño típico: ~15-25 MB descomprimido, 5-8 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/ente-zero` | salta `/init`, ejecuta directo (no recomendado) |
`ente-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`:
```
ente-zero despierta como PID 1
Tarjeta Semilla cargada y validada path=/ente/seed.card.json
bus interno escuchando path=/run/ente-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 `ente_brain::load_card_file()` (que llama a
`brahman_card::Card::validate()`) y verifica que `ente-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
`ente-zero` usa `tracing-subscriber`. Default: `ente_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)
| socket | servicio |
| ---------------------------- | ------------------------------------------------- |
| `/run/ente-bus.sock` | bus interno (postcard, `BusRequest::Invoke/...`) |
| `/run/brahman-init.sock` | handshake brahman |
| `/run/brahman-admin.sock` | snapshots de sesiones + matches |
| `/run/ente-brain.sock` | introspección del cerebro |
Conectar desde un Ente hijo: `socat - UNIX-CONNECT:/run/brahman-admin.sock`.
(El initrd debe tener `socat` o equivalente; agregalo con `EXTRA_BINS`.)
### 6c. Snapshot / restore
```
ente-zero --checkpoint /ente/checkpoint.json # escribe al cerrar
ente-zero --restore /ente/checkpoint.json # reconstruye al boot
```
Snapshot adjunto del cerebro: `/ente/checkpoint.brain.json`.
### 6d. Metrics
```
ente-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)
`ente-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/ente-zero
```
---
## Apéndice — checklist pre-deploy
- [ ] `cargo build --release -p ente-zero` sin warnings.
- [ ] `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.
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# build-arje-initrd.sh — empaqueta ente-zero + shims compat + Tarjeta Semilla
# en un initramfs CPIO+gzip listo para arrancar bajo QEMU o como /init real.
#
# Layout del initrd resultante:
# /init → wrapper sh que exec /sbin/ente-zero
# /sbin/ente-zero → PID 1
# /usr/sbin/ente-*-compat → shims systemd
# /usr/sbin/ente-echo, ente-policy-provider
# /ente/seed.card.json → Tarjeta Semilla
# /bin/{sh,ls,cat,...} → busybox o glibc-static (depende del flag)
# /dev, /proc, /sys, /run → puntos de montaje (ente-zero los monta)
#
# Uso:
# scripts/build-arje-initrd.sh [seed.card.json] [out.cpio.gz]
#
# seed default: seeds/arje-prod.card.json
# out default: out/arje.initrd.cpio.gz
#
# Env:
# BUSYBOX_BIN path a un busybox-static (default: $(which busybox))
# EXTRA_BINS binarios extra a copiar, separados por espacio
#
# Requisitos: cpio, gzip, ldd (sólo si no usás busybox-static).
set -euo pipefail
SEED="${1:-seeds/arje-prod.card.json}"
OUT="${2:-out/arje.initrd.cpio.gz}"
BUSYBOX_BIN="${BUSYBOX_BIN:-$(command -v busybox 2>/dev/null || true)}"
EXTRA_BINS="${EXTRA_BINS:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_DIR"
if [ ! -f "$SEED" ]; then
echo "[build-initrd] seed no encontrada: $SEED" >&2
exit 2
fi
# 1. Build release de ente-zero y todos los compat shims.
echo "[build-initrd] cargo build --release de ente-zero + shims"
cargo build --release \
-p ente-zero \
-p ente-echo \
-p ente-logind-compat \
-p ente-hostnamed-compat \
-p ente-timedated-compat \
-p ente-localed-compat \
-p ente-journald-compat \
-p ente-resolved-compat \
-p ente-polkit-compat \
-p ente-machined-compat \
-p ente-systemd1-compat \
-p ente-notify-compat \
-p ente-timer-compat \
-p ente-tmpfiles-compat \
-p ente-binfmt-compat \
-p ente-policy-provider
# 2. Validar la seed.
if ! seeds/validate.sh "$SEED" >/dev/null 2>&1; then
echo "[build-initrd] seed inválida: $SEED" >&2
exit 3
fi
# 3. Stage root del initrd.
STAGE="$(mktemp -d -t arje-initrd.XXXXXX)"
trap 'rm -rf "$STAGE"' EXIT
mkdir -p "$STAGE"/{bin,sbin,usr/sbin,etc,ente,proc,sys,dev,run,tmp,sys/fs/cgroup}
# 4. Copiar binarios arje.
install -m 0755 target/release/ente-zero "$STAGE/sbin/ente-zero"
for b in ente-echo ente-policy-provider \
ente-logind-compat ente-hostnamed-compat ente-timedated-compat \
ente-localed-compat ente-journald-compat ente-resolved-compat \
ente-polkit-compat ente-machined-compat ente-systemd1-compat \
ente-notify-compat ente-timer-compat ente-tmpfiles-compat \
ente-binfmt-compat; do
install -m 0755 "target/release/$b" "$STAGE/usr/sbin/$b"
done
# 5. Userspace mínimo (sh, ls, mount, mkdir, ...). Dos rutas:
# (a) busybox-static apuntado por $BUSYBOX_BIN → 1 binario, todo simlink.
# (b) sin busybox → copiar /bin/sh + deps con ldd (libc dinámica).
if [ -n "$BUSYBOX_BIN" ] && [ -x "$BUSYBOX_BIN" ]; then
echo "[build-initrd] usando busybox-static: $BUSYBOX_BIN"
install -m 0755 "$BUSYBOX_BIN" "$STAGE/bin/busybox"
( cd "$STAGE/bin" && for app in sh ls cat mount umount mkdir cp mv \
echo grep sed awk ps kill sleep insmod modprobe poweroff reboot \
sysctl dmesg ip ifconfig; do
ln -sf busybox "$app"
done )
else
echo "[build-initrd] sin busybox — copiando /bin/sh + deps via ldd"
install -m 0755 /bin/sh "$STAGE/bin/sh"
copy_lib() {
local lib="$1"
[ -f "$lib" ] || return 0
local dest="$STAGE${lib}"
mkdir -p "$(dirname "$dest")"
cp -L "$lib" "$dest"
}
for b in /bin/sh /bin/ls /bin/cat /bin/mount /bin/umount /bin/mkdir; do
[ -x "$b" ] || continue
cp -L "$b" "$STAGE${b}"
while read -r lib; do copy_lib "$lib"; done < <(
ldd "$b" 2>/dev/null | awk '{ for (i=1;i<=NF;i++) if ($i ~ /^\//) print $i }'
)
done
fi
# 6. Tarjeta Semilla. Path canónico en prod: /ente/seed.card.json
install -m 0644 "$SEED" "$STAGE/ente/seed.card.json"
# 7. /init wrapper. El kernel pasa control a /init; nosotros invocamos
# ente-zero como PID 1 real con su env mínimo.
cat > "$STAGE/init" <<'EOF'
#!/bin/sh
# arje /init — kernel → este script → ente-zero (PID 1 lo hereda via exec)
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
export RUST_LOG="${RUST_LOG:-ente_zero=info,brahman_handshake=info,info}"
# ente-zero monta /proc /sys /dev /sys/fs/cgroup él mismo.
exec /sbin/ente-zero
EOF
chmod 0755 "$STAGE/init"
# 8. Binarios extra a vendorear.
if [ -n "$EXTRA_BINS" ]; then
for b in $EXTRA_BINS; do
[ -x "$b" ] || { echo "[build-initrd] EXTRA_BINS: $b no existe"; exit 4; }
install -m 0755 "$b" "$STAGE/usr/sbin/$(basename "$b")"
done
fi
# 9. Empaquetar CPIO + gzip. Formato newc (estándar para Linux initramfs).
mkdir -p "$(dirname "$OUT")"
( cd "$STAGE" && find . -print0 | cpio -o -H newc --null --quiet ) | gzip -9 > "$OUT"
echo "[build-initrd] generado: $OUT ($(du -h "$OUT" | cut -f1))"
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# run-arje-qemu.sh — bootea el initrd de arje bajo qemu-system-x86_64.
#
# Uso:
# scripts/run-arje-qemu.sh [initrd] [kernel]
#
# Defaults:
# initrd out/arje.initrd.cpio.gz
# kernel $(uname -r) o /boot/vmlinuz-* (primer match)
#
# Env:
# QEMU binario de qemu (default: qemu-system-x86_64)
# KERNEL_CMD extra cmdline kernel (se concatena al fijo)
# MEM RAM en MB (default: 1024)
# SMP CPUs virtuales (default: 2)
# ACCEL kvm|tcg (default: kvm si /dev/kvm existe)
# HEADLESS 1 → sin display (consola en stdio)
#
# El initrd contiene su propio /init → ente-zero corre como PID 1 real.
set -euo pipefail
INITRD="${1:-out/arje.initrd.cpio.gz}"
KERNEL="${2:-}"
QEMU="${QEMU:-qemu-system-x86_64}"
MEM="${MEM:-1024}"
SMP="${SMP:-2}"
KERNEL_CMD="${KERNEL_CMD:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_DIR"
if [ ! -f "$INITRD" ]; then
echo "[run-qemu] initrd no encontrado: $INITRD" >&2
echo " Generalo primero con: scripts/build-arje-initrd.sh" >&2
exit 2
fi
if [ -z "$KERNEL" ]; then
# En orden: /boot/vmlinuz-$(uname -r), primer vmlinuz-*, /boot/bzImage.
for cand in "/boot/vmlinuz-$(uname -r)" /boot/vmlinuz-* /boot/bzImage*; do
if [ -f "$cand" ]; then KERNEL="$cand"; break; fi
done
fi
if [ -z "$KERNEL" ] || [ ! -f "$KERNEL" ]; then
echo "[run-qemu] kernel no encontrado." >&2
echo " Pasalo como 2do arg o instalá linux-image-amd64." >&2
echo " También podés bajar uno: " >&2
echo " wget https://kernel.ubuntu.com/...vmlinuz" >&2
exit 3
fi
# KVM si está disponible.
ACCEL="${ACCEL:-}"
if [ -z "$ACCEL" ]; then
if [ -e /dev/kvm ] && [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
ACCEL="kvm"
else
ACCEL="tcg"
fi
fi
DISPLAY_ARGS=()
if [ "${HEADLESS:-0}" = "1" ]; then
DISPLAY_ARGS+=( -nographic -serial mon:stdio )
KERNEL_CMD="console=ttyS0,115200 $KERNEL_CMD"
else
DISPLAY_ARGS+=( -nographic -serial mon:stdio )
KERNEL_CMD="console=ttyS0,115200 $KERNEL_CMD"
fi
CMDLINE="rdinit=/init panic=10 loglevel=4 $KERNEL_CMD"
echo "[run-qemu] kernel: $KERNEL"
echo "[run-qemu] initrd: $INITRD ($(du -h "$INITRD" | cut -f1))"
echo "[run-qemu] accel: $ACCEL"
echo "[run-qemu] mem: ${MEM}M, smp: $SMP"
echo "[run-qemu] cmdline: $CMDLINE"
echo "[run-qemu] (Ctrl-A X en stdio mode para salir)"
exec "$QEMU" \
-accel "$ACCEL" \
-m "$MEM" -smp "$SMP" \
-kernel "$KERNEL" \
-initrd "$INITRD" \
-append "$CMDLINE" \
"${DISPLAY_ARGS[@]}" \
-no-reboot
+62
View File
@@ -0,0 +1,62 @@
{
"schema_version": 1,
"id": "01J8YVKZP0M0M0M0M0M0M0M0M1",
"lineage": null,
"label": "arje.seed.minimal",
"provides": ["Spawn", "Journal"],
"requires": [],
"permissions": {
"networking": "none",
"filesystem": "read-write",
"ipc": { "allow": [] },
"processes": true
},
"soma": {
"namespaces": {
"mount": false, "pid": false, "net": false,
"uts": false, "ipc": false, "user": false, "cgroup": false
},
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
"cgroup": { "path": "ente.slice/zero", "cpu_weight": null, "io_weight": null },
"cpu_affinity": null
},
"payload": "Virtual",
"supervision": "OneShot",
"lifecycle": "daemon",
"priority": "critical",
"flow": { "input": [], "output": [] },
"genesis": [
{
"schema_version": 1,
"id": "01J8YVKZP0M0M0M0M0M0M0M0M2",
"lineage": null,
"label": "shell",
"provides": [],
"requires": [],
"permissions": {
"networking": "none",
"filesystem": "read-write",
"ipc": { "allow": [] },
"processes": true
},
"soma": {
"namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
"cgroup": { "path": "ente.slice/shell", "cpu_weight": null, "io_weight": null },
"cpu_affinity": null
},
"payload": {
"Native": {
"exec": "/bin/sh",
"argv": ["-i"],
"envp": [["PATH", "/bin:/usr/bin"], ["TERM", "linux"], ["PS1", "arje# "]]
}
},
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon",
"priority": "high",
"flow": { "input": [], "output": [] },
"genesis": []
}
]
}
+296
View File
@@ -0,0 +1,296 @@
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0M0",
"lineage": null,
"label": "arje.seed.prod",
"provides": ["Spawn", "Journal"],
"requires": [],
"permissions": {
"networking": "loopback",
"filesystem": "read-write",
"ipc": { "allow": ["wit-v1"] },
"processes": true
},
"soma": {
"namespaces": {
"mount": false, "pid": false, "net": false,
"uts": false, "ipc": false, "user": false, "cgroup": false
},
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
"cgroup": { "path": "ente.slice/zero", "cpu_weight": null, "io_weight": null },
"cpu_affinity": null
},
"payload": "Virtual",
"supervision": "OneShot",
"lifecycle": "daemon",
"priority": "critical",
"flow": { "input": [], "output": [] },
"genesis": [
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0M1",
"lineage": null,
"label": "tmpfiles-boot",
"provides": [],
"requires": [],
"permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-tmpfiles-compat", "argv": ["--boot"], "envp": [] } },
"supervision": "OneShot",
"lifecycle": "oneshot",
"priority": "critical",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0M2",
"lineage": null,
"label": "binfmt-boot",
"provides": [],
"requires": [],
"permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-binfmt-compat", "argv": [], "envp": [] } },
"supervision": "OneShot",
"lifecycle": "oneshot",
"priority": "high",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0H0",
"lineage": null,
"label": "compat-hostnamed",
"provides": [], "requires": [],
"permissions": { "networking": "loopback", "filesystem": "read-only", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-hostnamed-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "normal",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0T0",
"lineage": null,
"label": "compat-timedated",
"provides": [], "requires": [],
"permissions": { "networking": "loopback", "filesystem": "read-only", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-timedated-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "normal",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0X0",
"lineage": null,
"label": "compat-localed",
"provides": [], "requires": [],
"permissions": { "networking": "loopback", "filesystem": "read-only", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-localed-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "normal",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0J0",
"lineage": null,
"label": "compat-journald",
"provides": ["Journal"], "requires": [],
"permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-journald-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "high",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0R0",
"lineage": null,
"label": "compat-resolved",
"provides": [], "requires": [],
"permissions": { "networking": "outbound", "filesystem": "read-write", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-resolved-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "normal",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0P0",
"lineage": null,
"label": "compat-polkit",
"provides": [], "requires": [],
"permissions": { "networking": "loopback", "filesystem": "read-only", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-polkit-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "normal",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0Q0",
"lineage": null,
"label": "policy-provider",
"provides": [], "requires": [],
"permissions": { "networking": "none", "filesystem": "read-only", "ipc": { "allow": ["wit-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-policy-provider", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "normal",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0M0",
"lineage": null,
"label": "compat-machined",
"provides": [], "requires": [],
"permissions": { "networking": "loopback", "filesystem": "read-only", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-machined-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "normal",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0S0",
"lineage": null,
"label": "compat-systemd1",
"provides": [], "requires": [],
"permissions": { "networking": "loopback", "filesystem": "read-write", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-systemd1-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "high",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0N0",
"lineage": null,
"label": "compat-notify",
"provides": [], "requires": [],
"permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-notify-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "normal",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0Y0",
"lineage": null,
"label": "compat-logind",
"provides": ["LegacyLogind"], "requires": [],
"permissions": { "networking": "loopback", "filesystem": "read-write", "ipc": { "allow": ["dbus-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-logind-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "high",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0Z0",
"lineage": null,
"label": "compat-timer",
"provides": [], "requires": [],
"permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/compat","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-timer-compat", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 1000, "max": 60000 } },
"lifecycle": "daemon", "priority": "low",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0E0",
"lineage": null,
"label": "echo-smoke",
"provides": [], "requires": [],
"permissions": { "networking": "none", "filesystem": "read-only", "ipc": { "allow": ["wit-v1"] }, "processes": false },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/test","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": { "Native": { "exec": "/usr/sbin/ente-echo", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 200, "max": 30000 } },
"lifecycle": "daemon", "priority": "low",
"flow": { "input": [], "output": [] }, "genesis": []
},
{
"schema_version": 1,
"id": "01J8YVKZQ0M0M0M0M0M0M0M0G0",
"lineage": null,
"label": "getty-tty1",
"provides": [], "requires": [],
"permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
"soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
"cgroup": {"path":"ente.slice/getty","cpu_weight":null,"io_weight":null},
"cpu_affinity": null },
"payload": {
"Native": {
"exec": "/bin/sh",
"argv": ["-i"],
"envp": [["PATH", "/usr/sbin:/usr/bin:/sbin:/bin"], ["TERM", "linux"], ["PS1", "arje# "]]
}
},
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon", "priority": "high",
"flow": { "input": [], "output": [] }, "genesis": []
}
]
}
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Valida una Tarjeta Semilla pasándola por el parser+validate de brahman-card.
# Uso: seeds/validate.sh seeds/arje-prod.card.json
#
# Método: copiamos la Card como `seed.card.json` en un cwd vacío y corremos
# ente-zero en dev-mode 4 s. Si carga, valida, e instancia genesis, la
# Card es estructuralmente correcta — los binarios de los hijos pueden
# faltar en el host (vivirán en /usr/sbin del initrd).
set -euo pipefail
if [ "$#" -ne 1 ]; then
echo "uso: $0 <ruta/seed.card.json>" >&2
exit 2
fi
SEED="$(realpath "$1")"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BIN="$REPO_DIR/target/release/ente-zero"
if [ ! -x "$BIN" ]; then
echo "[validate] compilando ente-zero (release)…"
(cd "$REPO_DIR" && cargo build --quiet --release -p ente-zero)
fi
SCRATCH="$(mktemp -d -t arje-validate.XXXXXX)"
trap 'rm -rf "$SCRATCH"' EXIT
cp "$SEED" "$SCRATCH/seed.card.json"
cd "$SCRATCH"
timeout 5 "$BIN" 2>&1 | tee /tmp/arje-validate.log | \
grep -E "Tarjeta Semilla cargada|semilla inválida|JSON no contiene|Caused by" | head -5
if grep -q "Tarjeta Semilla cargada y validada" /tmp/arje-validate.log; then
echo "[validate] OK: $SEED"
exit 0
fi
echo "[validate] FALLÓ: $SEED" >&2
exit 1