gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 815Rust 56Shell 37Markdown 291229 Sutichaykuna

arjectl en la imagen: relanzar en caliente — y el genesis NO alcanzaba para eso

El servicio que falla y agota su backoff sólo se podía recuperar reiniciando la máquina entera.
Ya no. Y no hubo que escribir nada: **el cliente existía en tawasuyu y el crate se llama `arje-ctl`**
(el binario, `arjectl`). Buscarlo por `arjectl` no lo encontraba, y de ahí salió mi conclusión falsa
de que había que implementarlo — el protocolo ya traía ListEntes, SpawnCardFromDisk,
StopCardFromDisk, KillEnte y EnteStatus.

`recipes/arjectl.toml` lo construye del MISMO commit que `arje-zero` (98db584f), y no por comodidad:
el bus es un protocolo entre dos binarios y un cliente de otro árbol puede conectar sin entenderse
con el init. Publica sólo `arjectl`; el crate también produce un `systemctl` de camuflaje que acá no
se instala — en una distro sin systemd, ese nombre en el PATH invita a escribir runbooks con el
verbo ajeno.

⚠ EL HUECO QUE SÓLO SE VE USÁNDOLO: el genesis de la seed dice qué arranca AL BOOT, pero
`start`/`restart` usan `SpawnCardFromDisk`, que lee `/etc/arje/cards.d/<label>.json` — y el armado
no lo escribía:

    $ arjectl start gitea
    Error: arje-zero rechazó: card gitea: No such file or directory
           (buscada en /etc/arje/cards.d/gitea.json)

Son dos preguntas distintas —qué arranca solo, y qué se puede encarnar a pedido— y arje las responde
desde sitios distintos. `inyectar-cards.py` escribe ahora los dos árboles, y en `cards.d` escribe
TODAS las cards, no sólo las nuevas: `sshd` viene del product-rootfs y tampoco era relanzable.

Medido con la VM arrancada UNA sola vez: la imagen trae `cards.d/{gitea,sshd}.json` · `list-units`
da PID/CPU/MEM/HILOS/reinicios · poner el `app.ini` + `arjectl start gitea` ⇒ **GET / 200 sin
reiniciar** (uptime 6 min) · `arjectl restart gitea` cambia el PID (118 → 192) y sigue en 200.

⚠ Y un aviso que costó un HTTP intermitente: **`arjectl start` sobre un Ente YA VIVO lo DUPLICA** —
`SpawnCardFromDisk` no deduplica por label y arje le da un ULID nuevo. En gitea el síntoma fue
`unable to lock level db … resource temporarily unavailable` y un `[F]`: dos servidores peleando por
el mismo estado. Para relanzar se usa `restart`, o se mira `list-units` antes. Un `start` idempotente
es trabajo de arje, no de esta imagen.

⚠ Deuda anotada, no barrida: `arjectl` va en `perfil.servidor` porque este frente es el que lo pagó.
TODA imagen de takana corre arje-zero como PID 1 y ninguna se puede operar sin él ⇒ el argumento
para subirlo a `base` es fuerte, y es una línea. Se deja como decisión.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn

fd09ca77b0291d1a2ac7230c663c4ed1c7a9665b · ruwaq Sergio · (1 killa ñawpaq) · tayta 805411942 · kay commitpi sach'ata qhaway

Semántico sayk'uy

Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.

5 qillqakuna · +122 −1

diff --git a/docs/28-servidor-de-produccion.md b/docs/28-servidor-de-produccion.md
index 93f14b73..116f0695 100644
--- a/docs/28-servidor-de-produccion.md
+++ b/docs/28-servidor-de-produccion.md
@@ -1042,6 +1042,52 @@ Y **la imagen no trae `arjectl`**: tras poner la config hubo que REINICIAR para
 intentarlo. El backoff de `restart` se agota y no hay forma de pedirle a PID 1 que relance un ente en
 caliente. Para una caja de producción eso es una pieza que falta, no una comodidad.
 
+### 6.12 🎛️ `arjectl` en la imagen: control en caliente *(2026-09-14)*
+
+El §6.11 cerró con «la imagen no trae `arjectl`, hubo que reiniciar la máquina para arrancar un
+servicio». Ya lo trae, y **no hubo que escribir nada**: el cliente existía en tawasuyu y se llama
+**`arje-ctl`** (el crate; el binario es `arjectl`). Buscarlo por `arjectl` no lo encontraba, y de ahí
+salió la conclusión falsa de que faltaba implementarlo — el protocolo (`arje-bus::BusRequest`) ya
+traía `ListEntes`, `SpawnCardFromDisk`, `StopCardFromDisk`, `KillEnte` y `EnteStatus`.
+
+`recipes/arjectl.toml` lo construye **del mismo commit que `arje-zero`** (`98db584f`), y eso no es
+comodidad: el bus es un protocolo entre dos binarios, y un cliente de otro árbol puede conectar y no
+entenderse con el init. Publica **sólo `arjectl`**; el crate también produce un `systemctl` de
+camuflaje que acá no se instala — un `systemctl` en el PATH de una distro sin systemd invita a
+escribir runbooks con el verbo ajeno.
+
+#### El hueco que sólo se ve usándolo: el genesis no alcanza
+
+Con `arjectl` en la imagen, el primer intento falló con un mensaje que nombra la causa:
+
+    $ arjectl start gitea
+    Error: arje-zero rechazó: card gitea: No such file or directory
+           (buscada en /etc/arje/cards.d/gitea.json)
+
+`start`/`restart` usan `SpawnCardFromDisk`, que lee **`/etc/arje/cards.d/<label>.json`** — y el
+armado sólo escribía el `genesis` de la seed. Son dos preguntas distintas: *qué arranca solo* y *qué
+se puede encarnar a pedido*. Con sólo la primera, un servicio que agota su backoff **no se puede
+relanzar sin reiniciar la máquina**. `inyectar-cards.py` escribe ahora los dos árboles (y en
+`cards.d` escribe TODAS, no sólo las nuevas: `sshd` viene del product-rootfs y tampoco era
+relanzable).
+
+#### Medido, con la VM arrancada UNA sola vez
+
+| | |
+|---|---|
+| la imagen trae | `/etc/arje/cards.d/{gitea,sshd}.json` |
+| `arjectl list-units` | tabla con PID, CPU%, MEM, HILOS y reinicios de cada Ente |
+| poner `/etc/gitea/app.ini` + `arjectl start gitea` | **`GET /` → 200**, sin reiniciar (`uptime` 6 min) |
+| `arjectl restart gitea` | el PID cambia (118 → 192) y sigue sirviendo 200 |
+
+#### ⚠ `arjectl start` sobre un Ente YA VIVO lo DUPLICA
+
+`SpawnCardFromDisk` no deduplica por label: encarna otra instancia y arje le da un ULID nuevo. En
+`gitea` el síntoma fue `unable to lock level db … resource temporarily unavailable` seguido de `[F]`
+— dos servidores peleando por el mismo estado, con el HTTP intermitente mientras duraba. **Para
+relanzar se usa `restart`**, o se mira `list-units` antes. Un `start` idempotente es trabajo de arje,
+no de esta imagen; queda anotado río arriba.
+
 ## 7. Reusar los scripts que ya existen, y no escribir de nuevo
 
 Pedido explícito del usuario. El inventario de lo que ya hace el trabajo:
diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index 727909df..1a80fe28 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -1300,6 +1300,14 @@ paquetes = [
   # `tmux`: un build largo por SSH que muere con la sesión es un build perdido. En una caja sin
   # pantalla es infraestructura, no comodidad.
   "tmux",
+  # `arjectl` (2026-09-14): el control en caliente del init. Sin esto una caja sólo se administra
+  # REINICIÁNDOLA, y se descubrió pagándolo: en la primera imagen con gitea, la card falló por falta
+  # de config, el backoff de `restart` se agotó y no había forma de pedirle a PID 1 que reintentara
+  # (SDD 28 §6.11). `status`, `list-units`, `start`, `stop`, `restart` sobre el bus de arje.
+  # ⚠ Va acá y NO en `base` sólo porque este frente es el que lo pagó; el argumento para subirlo es
+  # fuerte —TODA imagen de takana corre arje-zero como PID 1 y ninguna se puede operar sin él— y es
+  # una línea. Se deja como decisión a tomar, no como olvido.
+  "arjectl",
   # `netup` (2026-09-10): el cliente DHCP propio que levanta la red al arrancar. Entra como RAÍZ
   # aunque su binario YA venga dentro del `product-rootfs`, y por una razón concreta: el del producto
   # está congelado en el artefacto sellado del bootstrap, así que un arreglo de netup no llega nunca
diff --git a/recipes/arjectl.toml b/recipes/arjectl.toml
new file mode 100644
index 00000000..dcf67093
--- /dev/null
+++ b/recipes/arjectl.toml
@@ -0,0 +1,40 @@
+# arjectl — el control en caliente del init. Sin esto, una caja de takana sólo se administra
+# REINICIÁNDOLA, y eso se descubrió del peor modo: la primera imagen del perfil `servidor` levantó
+# gitea, la card falló por falta de config, el backoff de `restart` se agotó y **no había forma de
+# pedirle a PID 1 que volviera a intentarlo**. Hubo que reiniciar la máquina entera para arrancar un
+# servicio (SDD 28 §6.11).
+#
+# ══ NO SE ESCRIBIÓ NADA: YA EXISTÍA, Y SE LLAMA `arje-ctl` ═════════════════════════════════════
+# El protocolo del bus (`arje-bus::BusRequest`) ya trae `ListEntes`, `SpawnCardFromDisk`,
+# `StopCardFromDisk`, `KillEnte` y `EnteStatus`; lo que faltaba en la imagen era el CLIENTE, que vive
+# en `03_ukupacha/arje/runtime/arje-ctl` y da `status`, `list-units`, `start`, `stop` y `restart`.
+# Buscarlo por `arjectl` no lo encontraba —el crate es `arje-ctl`, el binario `arjectl`— y de ahí
+# salió la conclusión falsa de que había que escribirlo.
+#
+# ══ EL COMMIT ES EL MISMO QUE `arje-zero`, Y NO ES POR COMODIDAD ═══════════════════════════════
+# El bus es un PROTOCOLO entre dos binarios: un cliente de otro commit puede hablarle a un init que
+# ya no entiende lo mismo, y el síntoma sería un `arjectl` que conecta y no obtiene respuesta. Que
+# `arje-zero`, `arje-polkit-compat` y éste salgan del mismo árbol es la garantía de que el cliente y
+# el init fueron compilados contra la MISMA definición. De paso reusa el árbol de fuentes ya
+# fetcheado (ADR 0012).
+#
+# ⚠ El crate publica DOS binarios: `arjectl` (soberano) y `systemctl` (drop-in de camuflaje, que
+# reenvía al systemd real si systemd es el init vivo). Acá se publica **sólo `arjectl`**: la distro
+# no tiene systemd y un `systemctl` en el PATH invitaría a escribir runbooks con el verbo ajeno.
+# Ponerlo sería una decisión de compatibilidad, no un efecto colateral de la receta.
+name    = "arjectl"
+version = "0.0.1"
+license = "MIT"
+
+[source]
+repo   = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git"
+commit = "98db584fd28d9ff2bf34c9cf7d7db2e1de0b3fd0"
+# Este árbol COMMITEA su propio `vendor/` y el `cargo vendor` de takana lo pisaría — igual que en
+# `arje-zero.toml`. Ver [[takana-cargo-vendor-clobbers-project-vendor]].
+cargo_vendor_dir = ".hammer-cargo-vendor"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+flags    = ["-p", "arje-ctl", "--bin", "arjectl"]
diff --git a/scripts/gnome/inyectar-cards.py b/scripts/gnome/inyectar-cards.py
index dd43e314..624962ae 100755
--- a/scripts/gnome/inyectar-cards.py
+++ b/scripts/gnome/inyectar-cards.py
@@ -7,9 +7,23 @@
 #
 # NO genera las Cards: las recibe ya emitidas por `takana service-cards`, que es la única
 # implementación de la traducción `[[service]]` → Card. Acá sólo se COMPONE la seed.
+#
+# ── LAS CARDS VAN A DOS SITIOS, Y HACEN FALTA LOS DOS (2026-09-14) ──────────────────────────────
+# El `genesis` de la seed dice qué arranca AL BOOT. `arjectl start|restart <label>` no lee de ahí:
+# usa `SpawnCardFromDisk`, que carga `/etc/arje/cards.d/<label>.json`. Medido en la imagen del
+# perfil `servidor`:
+#
+#     $ arjectl start gitea
+#     Error: arje-zero rechazó: card gitea: No such file or directory
+#            (buscada en /etc/arje/cards.d/gitea.json)
+#
+# O sea: con sólo el genesis, un servicio que muere tras agotar su backoff **no se puede relanzar
+# sin reiniciar la máquina**. Escribir las dos no es duplicar: son dos preguntas distintas —qué
+# arranca solo, y qué se puede encarnar a pedido— y arje las responde desde sitios distintos.
 import json, os, sys
 
 seed_p, cards_p = sys.argv[1], sys.argv[2]
+cards_dir = sys.argv[3] if len(sys.argv) > 3 else None
 seed = json.load(open(seed_p))
 cards = json.load(open(cards_p))
 
@@ -28,3 +42,16 @@ os.chmod(tmp, 0o644)
 # read-only al artefacto del store, y escribirle encima mutaría el store.
 os.replace(tmp, seed_p)
 print("   cards en el genesis:", ", ".join(puestos) or "(ninguna nueva)")
+
+# El store en disco que `arjectl` consulta. Se escriben TODAS, no sólo las nuevas: una card que ya
+# estaba en el genesis (p. ej. `sshd`, que viene del product-rootfs) tampoco es relanzable sin esto.
+if cards_dir:
+    os.makedirs(cards_dir, exist_ok=True)
+    for c in cards:
+        dst = os.path.join(cards_dir, f"{c['label']}.json")
+        tmp = dst + ".new"
+        with open(tmp, "w") as f:
+            json.dump(c, f, indent=2)
+        os.chmod(tmp, 0o644)
+        os.replace(tmp, dst)   # `replace`, no `write`: el árbol fundido hardlinkea al store
+    print("   cards en", cards_dir + ":", ", ".join(c["label"] for c in cards))
diff --git a/scripts/servidor-image.sh b/scripts/servidor-image.sh
index b44cfa2c..e5fdaadb 100755
--- a/scripts/servidor-image.sh
+++ b/scripts/servidor-image.sh
@@ -114,7 +114,7 @@ if [ -n "$SVCPATHS" ]; then
   # shellcheck disable=SC2086
   "$TAKANA" --store "$STORE" service-cards $SVCPATHS > "$WORKDIR/cards.json"
   echo "==> cards   : $(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))))' "$WORKDIR/cards.json") card(s) de $(echo "$SVCPATHS" | wc -l) receta(s)"
-  python3 scripts/gnome/inyectar-cards.py "$RFS/ente/seed.card.json" "$WORKDIR/cards.json"
+  python3 scripts/gnome/inyectar-cards.py "$RFS/ente/seed.card.json" "$WORKDIR/cards.json" "$RFS/etc/arje/cards.d"
 else
   echo "==> cards   : el perfil no arranca ningún servicio de paquete"
 fi