tawasuyu

tawasuyu/brahman

Rust 492TOML 194Markdown 53JSON 176939 Definitions

feat(arje): reemplaza systemd en máquina real con coexistencia GRUB

Flujo seguro de adopción: arje se instala como entrada GRUB
alternativa, no toca systemd ni /sbin/init. Booteás arje cuando
querés, volvés a systemd si rompe (rollback instantáneo desde el
menú).

Artefactos nuevos:

- scripts/install-arje-as-init.sh: instala binarios musl-static a
  /usr/sbin/ y /usr/bin/, copia seed a /ente/seed.card.json, agrega
  menuentry "arje" a /etc/grub.d/40_custom usando init=/sbin/ente-zero
  con kernel + initrd nativos. NO cambia GRUB_DEFAULT. Idempotente
  (regenera el bloque ARJE-MENUENTRY si existe).

- scripts/uninstall-arje.sh: revierte binarios + menuentry. Conserva
  /ente/seed.card.json por si la editaste.

- seeds/arje-host.card.json: seed para máquina real con 15 cards:
  tmpfiles + mount-fstab + swap-on + dbus-system + 11 compat shims +
  dhcpcd + sshd + agetty. Validada.

- docs/arje-replace-systemd.md: filosofía vs systemd ("no acapara
  porque no genera, sólo arranca lo declarado"), lista exhaustiva de
  servicios systemd que NO deben migrarse (ModemManager, snapd, cups,
  unattended-upgrades, etc.), tabla diferencial de UX vs systemd
  (systemctl restart → kill PID, systemctl enable → editar seed),
  checklist pre-primer-boot, instrucciones de rollback y cómo hacer
  arje default sólo cuando estés seguro.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

ca5dd0417635750c768b64a23d97a251f678c50c · by sergio · (5 months ago) · parent e4b1d41b6 · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

4 files · +693 −0

diff --git a/docs/arje-replace-systemd.md b/docs/arje-replace-systemd.md
new file mode 100644
index 0000000..0fa22c1
--- /dev/null
+++ b/docs/arje-replace-systemd.md
@@ -0,0 +1,221 @@
+# Reemplazar systemd con arje en una máquina real
+
+> **Importante**: este flujo deja **systemd intacto** y agrega arje como
+> entrada GRUB alternativa. No es "destruir y reemplazar"; es "convivir
+> hasta confiar". Booteás arje cuando querés, volvés a systemd si rompe.
+
+## 1. Estrategia
+
+| componente            | qué hacemos                                              |
+| --------------------- | -------------------------------------------------------- |
+| `/sbin/init`          | NO se toca (sigue siendo systemd).                       |
+| `/lib/systemd/*`      | NO se toca.                                              |
+| `kernel + initramfs`  | reusamos los del host (firmware, módulos, root FS).      |
+| `init=` en GRUB       | nueva entrada apunta a `/sbin/ente-zero`.                |
+| `GRUB_DEFAULT`        | NO se cambia. Sigue arrancando systemd por default.      |
+| `/ente/seed.card.json`| nueva — define qué arranca arje.                         |
+| `/usr/sbin/ente-*`    | binarios musl-static del fractal.                        |
+
+Booteo del kernel → initramfs nativo hace su trabajo (mountea root,
+carga módulos) → cuando llega el `switch_root`, en lugar de exec
+`/lib/systemd/systemd`, exec `/sbin/ente-zero`. Esto deja firmware y
+udev fuera del scope de arje (todavía).
+
+## 2. Instalación
+
+```bash
+# 1. (opcional) revisá la seed default que se va a copiar a /ente/
+$EDITOR seeds/arje-host.card.json
+
+# 2. instalar
+sudo scripts/install-arje-as-init.sh
+
+# 3. (opcional) bootear arje SÓLO en el próximo reboot (no cambia default)
+sudo grub-reboot "arje (init=/sbin/ente-zero) — kernel $(uname -r)"
+sudo reboot
+```
+
+Si algo rompe en arje: reset/poweroff → en el menú GRUB elegir la
+entrada default (systemd) → volvés a un sistema funcional. `arje` no
+toca configuración de systemd, así que el rollback es instantáneo.
+
+Para **desinstalar**:
+
+```bash
+sudo scripts/uninstall-arje.sh
+```
+
+## 3. Filosofía vs systemd — y por qué arje no acapara
+
+systemd arranca, por default, **todo lo que tiene un `.service` con
+`WantedBy=multi-user.target`**, recursivamente vía dependencias. Eso es
+opaco y crece con cada paquete instalado.
+
+arje no tiene generadores, ni `WantedBy`, ni `Wants`, ni target trees.
+**Arranca exactamente lo declarado en `genesis: [...]` de la
+`seed.card.json`**, en el orden declarativo, con la `supervision` que
+declarás vos. Si no lo declaraste, no corre. Punto.
+
+### 3a. Servicios que systemd arranca y arje NO debería migrar
+
+Estos son los sospechosos típicos en Debian/Ubuntu/Arch. **No agregues
+ninguno a tu seed** salvo que sepas que lo usás:
+
+| systemd unit                         | por qué saltearlo                        |
+| ------------------------------------ | ---------------------------------------- |
+| `ModemManager.service`               | no tenés módem 3G/4G.                    |
+| `bluetooth.service`                  | si no usás bluetooth.                    |
+| `cups.service`, `cups-browsed`       | si no imprimís.                          |
+| `avahi-daemon.service`               | mDNS local. Ruido si no lo necesitás.    |
+| `snapd.service`                      | si no usás snaps.                        |
+| `apt-daily.service`, `apt-daily-upgrade` | autoupdates — no son init.            |
+| `unattended-upgrades.service`        | corre tras boot, no init.                |
+| `accounts-daemon.service`            | UI helpers GNOME; no init.               |
+| `colord.service`                     | calibración de color. No init.           |
+| `geoclue.service`                    | localización. No init.                   |
+| `gdm.service`, `lightdm.service`     | DM gráfico — agregalo SÓLO si querés GUI.|
+| `NetworkManager-wait-online.service` | espera red. arje no necesita "esperar".  |
+| `systemd-timesyncd.service`          | reemplaza con cron-like + ntpd one-shot. |
+| `systemd-machined.service`           | reemplazado por `ente-machined-compat`.  |
+| `systemd-logind.service`             | reemplazado por `ente-logind-compat`.    |
+| `systemd-resolved.service`           | reemplazado por `ente-resolved-compat`.  |
+| `systemd-hostnamed.service`          | reemplazado por `ente-hostnamed-compat`. |
+| `systemd-timedated.service`          | reemplazado por `ente-timedated-compat`. |
+| `systemd-localed.service`            | reemplazado por `ente-localed-compat`.   |
+| `systemd-journald.service`           | reemplazado por `ente-journald-compat`.  |
+| `systemd-tmpfiles-*.service`         | reemplazado por `ente-tmpfiles-compat`.  |
+| `systemd-binfmt.service`             | reemplazado por `ente-binfmt-compat`.    |
+| `polkitd.service`                    | reemplazado por `ente-polkit-compat`.    |
+
+Las últimas 12 ya están como Cards en `seeds/arje-host.card.json`. Las
+de la mitad de arriba **no** — no las agregues.
+
+### 3b. Servicios que SÍ necesitás declarar (y que ya están en `arje-host.card.json`)
+
+| Card label         | reemplaza                  | binario                                      |
+| ------------------ | -------------------------- | -------------------------------------------- |
+| `tmpfiles-boot`    | `systemd-tmpfiles-setup`   | `/usr/sbin/ente-tmpfiles-compat --boot`      |
+| `mount-fstab`      | `local-fs.target`          | `/bin/mount -a` (one-shot)                   |
+| `swap-on`          | `swap.target`              | `/sbin/swapon -a` (one-shot)                 |
+| `dbus-system`      | `dbus.service`             | `/usr/bin/dbus-daemon --system --nofork`     |
+| `compat-*` (×11)   | shims D-Bus de systemd     | `/usr/sbin/ente-*-compat`                    |
+| `network-dhcpcd`   | `NetworkManager` / `networkd` | `/usr/sbin/dhcpcd -B`                     |
+| `sshd`             | `ssh.service`              | `/usr/sbin/sshd -D`                          |
+| `getty-tty1`       | `getty@tty1.service`       | `/sbin/agetty --noclear tty1 linux`          |
+
+15 entes. Compará: una instalación Debian fresh con GNOME desktop tiene
+**>180 unidades activas** tras `systemctl list-units --state=running`.
+
+### 3c. Cómo agregar un servicio que falta
+
+1. Encontrá el binario y los argumentos. Mirá el `.service` original:
+   ```
+   systemctl cat <unit>
+   ```
+   Mirá `ExecStart=` y reproducí.
+
+2. Agregá una Card al `genesis` de `/ente/seed.card.json`:
+   ```json
+   {
+     "schema_version": 1,
+     "id": "<ULID nuevo, Crockford base32, no I L O U>",
+     "label": "mi-servicio",
+     "provides": [], "requires": [],
+     "permissions": { "networking": "outbound", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
+     "soma": { ... idéntico al patrón de los otros ... },
+     "payload": { "Native": { "exec": "/usr/sbin/mi-bin", "argv": ["--foreground"], "envp": [] } },
+     "supervision": { "Restart": { "initial": 500, "max": 30000 } },
+     "lifecycle": "daemon",
+     "priority": "normal",
+     "flow": { "input": [], "output": [] },
+     "genesis": []
+   }
+   ```
+   **Importante**: `argv` tiene que tener el equivalente a `--foreground` o
+   `-D` o `--no-fork`. Si el binario se daemoniza solo (fork+exit), arje
+   no lo puede supervisar y caerá en loop de restart.
+
+3. Validar:
+   ```bash
+   seeds/validate.sh /ente/seed.card.json
+   ```
+
+4. Reboot a arje. Verificar:
+   ```sh
+   arje# brahman-status
+   ```
+
+## 4. Diferencias de UX con systemd
+
+| acción                       | systemd                              | arje                                                    |
+| ---------------------------- | ------------------------------------ | ------------------------------------------------------- |
+| Ver servicios activos        | `systemctl list-units --state=running` | `brahman-status` + `busctl list-entes`               |
+| Ver logs                     | `journalctl -u foo`                  | `ente-journalctl` (TODO en CLI; por ahora `dmesg`)      |
+| Restart un servicio          | `systemctl restart foo`              | matar el PID (`brain` lo reanuda por `Supervision`)     |
+| Habilitar/Deshabilitar       | `systemctl enable/disable`           | editar `/ente/seed.card.json` + reboot                  |
+| Ver dependencias             | `systemctl list-dependencies foo`    | `brahman-status` muestra references y flow matches      |
+| Detectar fallas reincidentes | `systemctl status foo` → "failed"    | `brainctl crystals` muestra patrones `EnteDied → …`     |
+| socket activation            | `.socket` unit                       | (no implementado todavía; ver flow + broker matching)   |
+| user services                | `--user`                             | (no implementado; arje es system-wide por ahora)        |
+
+## 5. Cosas que arje todavía NO hace que sí hace systemd
+
+- **udev / hotplug**: arje captura uevents (vía `spawn_uevent_stream`),
+  pero no hay reglas declarativas todavía. Para boot inicial, el
+  initramfs nativo ya hizo el cold-plug, así que no es bloqueante.
+- **Timers** (cron-like): `ente-timer-compat` existe pero no está en la
+  seed host por default. Agregalo si querés `*.timer` units migradas.
+- **Service generators**: no hay equivalente a `systemd-fstab-generator`
+  ni `systemd-getty-generator`. Tu seed los declara explícitamente.
+- **Socket activation**: parcialmente — el broker matchea flow
+  consumer↔producer, pero no hay aún listen-on-fd-handoff como
+  `sd_listen_fds()`.
+- **User instances**: no hay todavía `arje --user`. Los procesos de
+  usuario los lanza un getty/login estándar.
+
+## 6. Checklist pre-primer-boot
+
+- [ ] `seeds/arje-host.card.json` ajustada a tu hardware/red/users.
+- [ ] `seeds/validate.sh` → OK.
+- [ ] `/usr/bin/dbus-daemon` existe (de lo contrario los shims no se
+      anuncian — `apt install dbus`).
+- [ ] `/usr/sbin/dhcpcd` o tu network stack preferido está instalado.
+- [ ] `/usr/sbin/sshd` instalado y `/etc/ssh/sshd_config` ok si querés
+      poder entrar remoto si la consola falla.
+- [ ] `/sbin/agetty` instalado (`util-linux`) para tty1.
+- [ ] `sudo scripts/install-arje-as-init.sh` ejecutado.
+- [ ] `grub-reboot` apuntado a la entrada arje **sólo para el próximo
+      boot** — no `grub-set-default`.
+- [ ] Listo para revertir vía menú GRUB si algo falla.
+
+## 7. Cuando arje sea estable: hacerlo default
+
+Cuando ya hayas booteado arje N veces sin problemas y querés que sea
+default:
+
+```bash
+# Listar entradas
+sudo grep -E "^\s*menuentry " /boot/grub/grub.cfg | nl
+
+# Ponerlo como default (usa el índice del menú o el title exacto)
+sudo grub-set-default "arje (init=/sbin/ente-zero) — kernel $(uname -r)"
+```
+
+Mantené la entrada systemd como rescue. Si querés acortar la lista de
+units que systemd arrancaría si bootea de nuevo: `systemctl disable
+ModemManager bluetooth cups …` — los nombres de §3a son una guía.
+
+## Apéndice — depurar el primer boot fallido
+
+Si arje no levanta el primer boot:
+
+1. Volver a GRUB → entrada systemd → bootea normal.
+2. `journalctl --boot=-1` (logs del intento previo).
+3. Buscar `ente-zero` o `Tarjeta Semilla`. Si nada → el ejecutable no
+   se lanzó (kernel panic temprano, falta `init=` correcto, etc.).
+4. Si arrancó pero falló alguna Card: la próxima vez, antes de reboot:
+   ```bash
+   sudo sed -i 's/console=tty1/console=tty1 ente.log=trace/' /etc/grub.d/40_custom
+   sudo update-grub
+   ```
+   y revisá `dmesg` desde systemd tras el reintento.
diff --git a/scripts/install-arje-as-init.sh b/scripts/install-arje-as-init.sh
new file mode 100755
index 0000000..b446634
--- /dev/null
+++ b/scripts/install-arje-as-init.sh
@@ -0,0 +1,178 @@
+#!/usr/bin/env bash
+# install-arje-as-init.sh — instala arje como init alternativo en una
+# máquina real con systemd ya instalado, en coexistencia.
+#
+# ESTRATEGIA: arje NO reemplaza al systemd default. Se instala en
+# paralelo con su propia entrada GRUB. Booteás arje cuando querés,
+# volvés a systemd si algo no anda.
+#
+# QUÉ HACE:
+#   1. Compila musl-static todos los binarios del fractal.
+#   2. Copia binarios a /usr/sbin/ (init/shims) y /usr/bin/ (CLIs admin)
+#      del rootfs vivo.
+#   3. Copia la seed a /ente/seed.card.json (default: arje-host.card.json).
+#   4. Crea menuentry "arje" en /etc/grub.d/40_custom usando init=/sbin/ente-zero
+#      (conserva initramfs nativo → firmware, módulos kernel, root FS,
+#      siguen funcionando exactamente igual).
+#   5. Ejecuta update-grub o grub-mkconfig según distro.
+#
+# QUÉ NO HACE:
+#   - Tocar /sbin/init, /usr/lib/systemd/* (systemd queda intacto y default).
+#   - Cambiar GRUB_DEFAULT (seguís booteando systemd por default).
+#   - Cargar arje ahora. Lo elegís manualmente en el próximo boot.
+#   - Habilitar servicios systemd que arje no necesita (eso es responsabilidad
+#     tuya, ver docs/arje-replace-systemd.md §4).
+#
+# REVERTIR:  sudo scripts/uninstall-arje.sh
+#
+# Uso:
+#   sudo scripts/install-arje-as-init.sh [seed.card.json]
+#
+#   seed   default: seeds/arje-host.card.json
+
+set -euo pipefail
+
+SEED="${1:-seeds/arje-host.card.json}"
+TARGET="${ARJE_TARGET:-x86_64-unknown-linux-musl}"
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$REPO_DIR"
+
+if [ "$(id -u)" -ne 0 ]; then
+  echo "[install-arje] hace falta root (modifica /usr/sbin, /etc/grub.d, /ente)" >&2
+  exit 1
+fi
+
+if [ ! -f "$SEED" ]; then
+  echo "[install-arje] seed no encontrada: $SEED" >&2
+  exit 2
+fi
+
+# Verificar que el systemd default sigue siendo el init — querés saber
+# de qué te volvés capaz de regresar.
+if [ ! -e /sbin/init ] && [ ! -e /lib/systemd/systemd ] && [ ! -e /usr/lib/systemd/systemd ]; then
+  echo "[install-arje] WARN: no detecto systemd en esta máquina." >&2
+  echo "  Si esta máquina ya corre arje, este script no es lo que querés." >&2
+  echo "  Continuá sólo si sabés lo que hacés." >&2
+  read -r -p "  ¿continuar? [y/N] " ans
+  [ "$ans" = "y" ] || exit 3
+fi
+
+echo "[install-arje] paso 1/5: compilar binarios musl-static"
+"$SCRIPT_DIR/build-arje-initrd.sh" "$SEED" /tmp/arje-noop.cpio.gz >/dev/null
+rm -f /tmp/arje-noop.cpio.gz
+BIN_DIR="$REPO_DIR/target/$TARGET/release"
+EX_DIR="$BIN_DIR/examples"
+
+echo "[install-arje] paso 2/5: instalar binarios en /usr/sbin/ y /usr/bin/"
+install -m 0755 "$BIN_DIR/ente-zero" /usr/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 "$BIN_DIR/$b" "/usr/sbin/$b"
+done
+for e in brahman-status busctl brainctl; do
+  install -m 0755 "$EX_DIR/$e" "/usr/bin/$e"
+done
+
+echo "[install-arje] paso 3/5: instalar seed en /ente/seed.card.json"
+mkdir -p /ente
+# Backup del seed previo (si lo hay) — nunca sobrescribimos sin guardar.
+if [ -f /ente/seed.card.json ]; then
+  cp /ente/seed.card.json "/ente/seed.card.json.bak-$(date +%s)"
+fi
+install -m 0644 "$SEED" /ente/seed.card.json
+
+echo "[install-arje] paso 4/5: detectar kernel para menuentry GRUB"
+# Reusamos el kernel del host (mismo vmlinuz que usa systemd). Detectamos
+# el current; si tenés varios, se elige el del uname.
+KVER="$(uname -r)"
+VMLINUZ=""
+INITRD=""
+for cand in "/boot/vmlinuz-$KVER" "/boot/vmlinuz" /boot/vmlinuz-*; do
+  if [ -f "$cand" ]; then VMLINUZ="$cand"; break; fi
+done
+for cand in "/boot/initrd.img-$KVER" "/boot/initramfs-$KVER.img" \
+            "/boot/initrd.img" /boot/initrd.img-* /boot/initramfs-*.img; do
+  if [ -f "$cand" ]; then INITRD="$cand"; break; fi
+done
+
+if [ -z "$VMLINUZ" ] || [ -z "$INITRD" ]; then
+  echo "[install-arje] ERROR: no encontré vmlinuz o initrd nativo en /boot." >&2
+  echo "  Sin esos no se puede armar la entrada GRUB." >&2
+  exit 4
+fi
+echo "  kernel:  $VMLINUZ"
+echo "  initrd:  $INITRD"
+
+# El root device — leemos /proc/cmdline para reusarlo exactamente como
+# está en la entrada systemd actual.
+ROOT_OPT="$(awk -v RS=' ' '/^root=/' /proc/cmdline | tr -d '\n')"
+if [ -z "$ROOT_OPT" ]; then
+  echo "[install-arje] WARN: no extraje root= de /proc/cmdline." >&2
+  echo "  Configurá manualmente la entrada GRUB con tu root device." >&2
+  ROOT_OPT="root=/dev/sda1  # AJUSTAR"
+fi
+
+echo "[install-arje] paso 5/5: agregar menuentry GRUB"
+CUSTOM=/etc/grub.d/40_custom
+# Si ya existe nuestro menuentry, lo reemplazamos (idempotente).
+if grep -q "BEGIN ARJE-MENUENTRY" "$CUSTOM"; then
+  # Borrar el bloque previo.
+  awk '/BEGIN ARJE-MENUENTRY/{f=1} /END ARJE-MENUENTRY/{f=0; next} !f' "$CUSTOM" \
+    > "$CUSTOM.tmp" && mv "$CUSTOM.tmp" "$CUSTOM"
+fi
+
+cat >> "$CUSTOM" <<EOF
+# BEGIN ARJE-MENUENTRY (generated by install-arje-as-init.sh)
+menuentry "arje (init=/sbin/ente-zero) — kernel $KVER" {
+    insmod gzio
+    insmod part_msdos
+    insmod ext2
+    linux  $VMLINUZ $ROOT_OPT ro init=/sbin/ente-zero console=tty1 console=ttyS0,115200 panic=10
+    initrd $INITRD
+}
+# END ARJE-MENUENTRY
+EOF
+chmod 0755 "$CUSTOM"
+
+# Regenerar grub.cfg.
+if command -v update-grub >/dev/null 2>&1; then
+  echo "  ejecutando update-grub"
+  update-grub
+elif command -v grub-mkconfig >/dev/null 2>&1; then
+  echo "  ejecutando grub-mkconfig"
+  grub-mkconfig -o /boot/grub/grub.cfg
+else
+  echo "[install-arje] WARN: no encontré update-grub ni grub-mkconfig." >&2
+  echo "  Regenerá grub.cfg manualmente." >&2
+fi
+
+cat <<EOF
+
+[install-arje] HECHO.
+
+systemd sigue siendo tu init default — esta máquina arranca systemd
+salvo que elijas explícitamente "arje" en GRUB.
+
+  - binarios:     /usr/sbin/ente-* y /usr/bin/{brahman-status,busctl,brainctl}
+  - seed activa:  /ente/seed.card.json (origen: $SEED)
+  - menuentry:    /etc/grub.d/40_custom  (regenerá grub.cfg si lo editás)
+
+Para arrancar arje al próximo boot UNA SOLA VEZ (sin cambiar default):
+
+  sudo grub-reboot "arje (init=/sbin/ente-zero) — kernel $KVER"
+  sudo reboot
+
+Para revertir (quita binarios + menuentry):
+
+  sudo scripts/uninstall-arje.sh
+
+Antes del primer boot real, leé docs/arje-replace-systemd.md §3
+("Servicios systemd que arje NO arranca") para que NO te quedes sin
+red, sin sshd o sin filesystems montados.
+EOF
diff --git a/scripts/uninstall-arje.sh b/scripts/uninstall-arje.sh
new file mode 100755
index 0000000..5afe645
--- /dev/null
+++ b/scripts/uninstall-arje.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# uninstall-arje.sh — revierte lo que install-arje-as-init.sh hizo.
+#
+# - Borra /usr/sbin/ente-* y /usr/bin/{brahman-status,busctl,brainctl}.
+# - Borra el bloque ARJE-MENUENTRY de /etc/grub.d/40_custom.
+# - Regenera grub.cfg.
+# - NO toca /ente/seed.card.json (querés conservar customización).
+# - NO toca /sbin/init ni systemd.
+
+set -euo pipefail
+
+if [ "$(id -u)" -ne 0 ]; then
+  echo "[uninstall-arje] hace falta root" >&2
+  exit 1
+fi
+
+echo "[uninstall-arje] removiendo binarios"
+rm -f /usr/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
+  rm -f "/usr/sbin/$b"
+done
+for e in brahman-status busctl brainctl; do
+  rm -f "/usr/bin/$e"
+done
+
+echo "[uninstall-arje] limpiando menuentry de /etc/grub.d/40_custom"
+CUSTOM=/etc/grub.d/40_custom
+if [ -f "$CUSTOM" ] && grep -q "BEGIN ARJE-MENUENTRY" "$CUSTOM"; then
+  awk '/BEGIN ARJE-MENUENTRY/{f=1} /END ARJE-MENUENTRY/{f=0; next} !f' "$CUSTOM" \
+    > "$CUSTOM.tmp" && mv "$CUSTOM.tmp" "$CUSTOM"
+  chmod 0755 "$CUSTOM"
+fi
+
+if command -v update-grub >/dev/null 2>&1; then
+  update-grub
+elif command -v grub-mkconfig >/dev/null 2>&1; then
+  grub-mkconfig -o /boot/grub/grub.cfg
+fi
+
+echo "[uninstall-arje] hecho. /ente/seed.card.json conservado (borralo a mano si querés)."
diff --git a/seeds/arje-host.card.json b/seeds/arje-host.card.json
new file mode 100644
index 0000000..ffa001d
--- /dev/null
+++ b/seeds/arje-host.card.json
@@ -0,0 +1,249 @@
+{
+  "schema_version": 1,
+  "id": "01J8YVKZH0M0M0M0M0M0M0M0M0",
+  "lineage": null,
+  "label": "arje.seed.host",
+  "provides": ["Spawn", "Journal"],
+  "requires": [],
+  "permissions": {
+    "networking": "full",
+    "filesystem": "read-write",
+    "ipc": { "allow": ["wit-v1", "dbus-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": "01J8YVKZH0M0M0M0M0M0M0M0T1",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0F1",
+      "label": "mount-fstab",
+      "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/boot","cpu_weight":null,"io_weight":null},
+                "cpu_affinity": null },
+      "payload": { "Native": { "exec": "/bin/mount", "argv": ["-a"], "envp": [] } },
+      "supervision": "OneShot", "lifecycle": "oneshot", "priority": "critical",
+      "flow": { "input": [], "output": [] }, "genesis": []
+    },
+    {
+      "schema_version": 1,
+      "id": "01J8YVKZH0M0M0M0M0M0M0M0S1",
+      "label": "swap-on",
+      "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/boot","cpu_weight":null,"io_weight":null},
+                "cpu_affinity": null },
+      "payload": { "Native": { "exec": "/sbin/swapon", "argv": ["-a"], "envp": [] } },
+      "supervision": "OneShot", "lifecycle": "oneshot", "priority": "normal",
+      "flow": { "input": [], "output": [] }, "genesis": []
+    },
+    {
+      "schema_version": 1,
+      "id": "01J8YVKZH0M0M0M0M0M0M0M0DB",
+      "label": "dbus-system",
+      "provides": [], "requires": [],
+      "permissions": { "networking": "loopback", "filesystem": "read-write", "ipc": { "allow": ["dbus-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/bus","cpu_weight":null,"io_weight":null},
+                "cpu_affinity": null },
+      "payload": { "Native": { "exec": "/usr/bin/dbus-daemon", "argv": ["--system", "--nofork"], "envp": [] } },
+      "supervision": { "Restart": { "initial": 200, "max": 30000 } },
+      "lifecycle": "daemon", "priority": "critical",
+      "flow": { "input": [], "output": [] }, "genesis": []
+    },
+    {
+      "schema_version": 1,
+      "id": "01J8YVKZH0M0M0M0M0M0M0M0H1",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0T2",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0J1",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0Y1",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0R1",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0P1",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0PP",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0SY",
+      "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": "01J8YVKZH0M0M0M0M0M0M0M0NW",
+      "label": "network-dhcpcd",
+      "provides": [], "requires": [],
+      "permissions": { "networking": "full", "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/net","cpu_weight":null,"io_weight":null},
+                "cpu_affinity": null },
+      "payload": { "Native": { "exec": "/usr/sbin/dhcpcd", "argv": ["-B"], "envp": [] } },
+      "supervision": { "Restart": { "initial": 500, "max": 60000 } },
+      "lifecycle": "daemon", "priority": "high",
+      "flow": { "input": [], "output": [] }, "genesis": []
+    },
+    {
+      "schema_version": 1,
+      "id": "01J8YVKZH0M0M0M0M0M0M0M0SS",
+      "label": "sshd",
+      "provides": [], "requires": [],
+      "permissions": { "networking": "full", "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/ssh","cpu_weight":null,"io_weight":null},
+                "cpu_affinity": null },
+      "payload": { "Native": { "exec": "/usr/sbin/sshd", "argv": ["-D"], "envp": [] } },
+      "supervision": { "Restart": { "initial": 200, "max": 30000 } },
+      "lifecycle": "daemon", "priority": "high",
+      "flow": { "input": [], "output": [] }, "genesis": []
+    },
+    {
+      "schema_version": 1,
+      "id": "01J8YVKZH0M0M0M0M0M0M0M0G1",
+      "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": "/sbin/agetty", "argv": ["--noclear", "tty1", "linux"], "envp": [] } },
+      "supervision": { "Restart": { "initial": 100, "max": 30000 } },
+      "lifecycle": "daemon", "priority": "high",
+      "flow": { "input": [], "output": [] }, "genesis": []
+    }
+  ]
+}