tawasuyu

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 1534Shell 201Rust 109Markdown 993716 Definitions

declarar: `/proc` pasa a ser un lector del centro — y había DOS emisores de tarjetas, mal los dos

`declarar.py` tenía su propio molde de card de arje y `formatos/arje.py` el suyo: el N×M que el
pivote existe para evitar, adentro de mi propio código. Y no quedó en teoría — CADA COPIA TENÍA UN
CAMPO MAL DE LA RAÍZ, Y NINGUNO DE LOS DOS EL MISMO:

    campo         declarar.py            arje.py        semilla REAL del producto
    provides      ["Spawn","Journal"] ✓  [] ✗           ["Spawn","Journal"]
    supervision   Restart{…} ✗           "OneShot" ✓    "OneShot"

Las consecuencias son concretas: sin `Spawn`/`Journal` las hijas no tienen quién las lance ni dónde
escribir, y una card `Virtual` con `Restart` le pide a arje que respawnee algo que nunca corrió.

Ahora hay un lector `proc` (el censo es un formato de origen, igual que systemd u OpenRC) y el
escritor `arje` es el ÚNICO que emite tarjetas; `declarar.py` queda con lo suyo, el perfil. Tres
lectores en la familia: systemd y openrc leen LO DECLARADO —que es lo que miente, `rc-status` daba
`stopped` para cinco servicios vivos— y `proc` lee LO QUE CORRE, que es donde aparecen los 15
`no-declarado`.

Dos cosas más, las dos sobre no mentir:

· **Una semilla vacía parecería un éxito.** Si ningún servicio tiene `decision = "muda"`, el lector
  lo DICE y sale ≠0 en vez de emitir cero tarjetas en silencio. Mismo modo de fallo que un artefacto
  vacío en el store.
· **El acta se ahogaba en su propio ruido.** Anotaba el `envp` vacío una vez POR SERVICIO: 26 líneas
  idénticas que tapaban los dos hallazgos reales (`shuma-daemon` corre como `sergio`; los que no
  tienen cmdline). La limitación es del lector y vale para todos ⇒ una entrada nombrando a los 26.
  Un acta donde casi todo es la misma línea se deja de leer, y entonces no queda ningún acta. Y al
  revés: las entradas que SÍ son por servicio ahora lo nombran (`gitea: cwd=/var/lib/gitea`).

Controles: la raíz generada coincide campo por campo con la del producto; dos corridas dan el fichero
byte a byte idéntico; 26 tarjetas con 26 ids únicos.

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

d0e3aadb1255b379d22915b77e36ed59e94b89e0 · by Sergio · (1 month ago) · parent 0e5e3f8c8 · browse the tree at this commit

Semantic delta · 5

scripts/mudanza/declarar.py

scripts/mudanza/formatos/arje.py

scripts/mudanza/formatos/proc.py

4 files · +156 −97

diff --git a/docs/29-mudanza.md b/docs/29-mudanza.md
index 2e9186a7..59d094a8 100644
--- a/docs/29-mudanza.md
+++ b/docs/29-mudanza.md
@@ -301,6 +301,49 @@ reconstruyen el servidor desde cero**:
 Ninguno de los tres es un log de lo que se hizo: los tres son entradas que se vuelven a ejecutar.
 Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez.
 
+### 4.0 octies · `/proc` es un formato de origen más — y había DOS emisores de tarjetas
+
+`declarar.py` tenía su propio molde de card de arje, y `formatos/arje.py` el suyo. Dos emisores del
+mismo formato es exactamente el N×M que el pivote existe para evitar, y no quedó en teoría: **cada
+copia tenía un campo mal de la raíz, y ninguno de los dos el mismo.**
+
+| campo de la raíz | `declarar.py` | `formatos/arje.py` | la semilla REAL del producto |
+|---|---|---|---|
+| `provides` | `["Spawn","Journal"]` ✓ | `[]` ✗ | `["Spawn","Journal"]` |
+| `supervision` | `Restart{…}` ✗ | `"OneShot"` ✓ | `"OneShot"` |
+
+Las dos consecuencias son concretas: sin `Spawn`/`Journal` las hijas no tienen quién las lance ni
+dónde escribir; y una card `Virtual` con `Restart` le pide a arje que respawnee algo que nunca corrió.
+
+Ahora hay un lector `proc` —el censo es un formato de origen, igual que systemd u OpenRC— y el
+escritor `arje` es el único que emite tarjetas. `declarar.py` queda con lo suyo: el **perfil**.
+
+```
+familia «servicio»:  systemd ─┐
+                     openrc  ─┼─►  Servicio/Sistema  ──►  arje
+                     proc    ─┘
+```
+
+Los tres se complementan y ninguno solo alcanza: systemd y OpenRC leen **lo declarado**, que es lo
+que miente —`rc-status` daba `stopped` para cinco servicios vivos—, y `proc` lee **lo que corre**,
+que es lo único donde aparecen los 15 `no-declarado`.
+
+#### Una semilla vacía parecería un éxito
+
+Si ningún servicio tiene `decision = "muda"`, el lector `proc` **no devuelve cero tarjetas en
+silencio**: lo dice y sale ≠0. Es el mismo modo de fallo que un artefacto vacío en el store — un
+ausente falla ruidosamente, un vacío llega hasta el final diciendo que todo fue bien.
+
+#### El acta se estaba ahogando en su propio ruido
+
+La primera versión anotaba el `envp` vacío **una vez por servicio**: 26 líneas idénticas que tapaban
+los dos hallazgos que sí eran de un servicio concreto —`shuma-daemon` corre como `sergio`, y los que
+no tienen línea de comando—. La limitación es del *lector* y vale igual para todos, así que va **una
+entrada nombrando a los 26**. Un acta donde casi todo es la misma línea se deja de leer, y entonces
+no queda ningún acta. Lo mismo con la supervisión. Y al revés: las entradas que sí son por servicio
+ahora lo **nombran** (`gitea: cwd=/var/lib/gitea`), porque cuatro `WorkingDirectory=/home/sergio`
+sin nombre obligan a buscar cuál es cuál.
+
 ### 4.0 septies · El censo real de gioser, y dos huecos que destapó
 
 Corrido contra `204.168.193.248` — que es `gioser.net`, identificado **por IP y no por hostname**,
diff --git a/scripts/mudanza/declarar.py b/scripts/mudanza/declarar.py
index bf4ee37a..cd649631 100755
--- a/scripts/mudanza/declarar.py
+++ b/scripts/mudanza/declarar.py
@@ -35,43 +35,7 @@ import argparse, hashlib, json, os, shlex, sys, tomllib
 
 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 from planear import origen_binario, alternativas, _REPO   # noqa: E402
-from ids import ulid_determinista                          # noqa: E402
-
-def tarjeta(nombre, cmdline, cwd, puertos):
-    """Una entrada `genesis`. El molde sale de la semilla real del producto."""
-    argv_all = shlex.split(cmdline)
-    if not argv_all:
-        return None
-    if cwd and cwd not in ("/", ""):
-        # Regla 2: `Native` no tiene `cwd`. Se envuelve, igual que la tarjeta de sshd del producto.
-        exec_, argv = "/bin/sh", ["-c", f"cd {shlex.quote(cwd)} && exec {cmdline}"]
-    else:
-        exec_, argv = argv_all[0], argv_all[1:]
-    return {
-        "flow": {"input": [], "output": []},
-        "genesis": [],
-        "id": ulid_determinista("takana-mudanza", nombre),
-        "label": nombre,
-        "lifecycle": "daemon",
-        "lineage": None,
-        "payload": {"Native": {"argv": argv, "envp": [], "exec": exec_}},
-        "permissions": {"filesystem": "read-write", "ipc": {"allow": []},
-                        "networking": "full", "processes": True},
-        "priority": "normal",
-        "provides": [],
-        "requires": [],
-        "schema_version": 1,
-        "soma": {
-            "cgroup": {"cpu_weight": None, "io_weight": None, "path": f"arje.slice/{nombre}"},
-            "cpu_affinity": None,
-            "namespaces": {k: False for k in
-                           ("cgroup", "ipc", "mount", "net", "pid", "user", "uts")},
-            "rlimits": {"mem_bytes": None, "nofile": None, "nproc": None},
-        },
-        "supervision": {"Restart": {"initial": 500, "max": 20000}},
-        "_mudanza": {"puertos": puertos, "cwd_original": cwd},
-    }
-
+import formatos                                            # noqa: E402
 
 def perfil(censo, label, repo):
     """Emite un `[perfil.<label>]` para `docs/state/targets.toml`.
@@ -135,61 +99,30 @@ def main():
     with open(a.censo, "rb") as f:
         censo = tomllib.load(f)
 
-    hijas, sin_cmdline, envueltos = [], [], []
-    for s in censo.get("servicio", []):
-        if s.get("decision") != "muda":
-            continue
-        cmd = (s.get("cmdline") or "").strip()
-        if not cmd:
-            # Regla 1: no se inventa una invocación.
-            sin_cmdline.append(s["name"])
-            continue
-        t = tarjeta(s["name"], cmd, (s.get("cwd") or "").strip(), s.get("ports", []))
-        if t is None:
-            sin_cmdline.append(s["name"])
-            continue
-        if t["payload"]["Native"]["exec"] == "/bin/sh":
-            envueltos.append(s["name"])
-        hijas.append(t)
-
-    hijas.sort(key=lambda t: t["label"])    # orden estable ⇒ `diff` útil
-    semilla = {
-        "flow": {"input": [], "output": []},
-        "genesis": hijas,
-        "id": ulid_determinista("takana-mudanza-raiz", a.label),
-        "label": a.label,
-        "lifecycle": "daemon",
-        "lineage": None,
-        "payload": "Virtual",
-        "permissions": {"filesystem": "read-write", "ipc": {"allow": []},
-                        "networking": "full", "processes": True},
-        "priority": "normal",
-        "provides": ["Spawn", "Journal"],
-        "requires": [],
-        "schema_version": 1,
-        "soma": {
-            "cgroup": {"cpu_weight": None, "io_weight": None, "path": "arje.slice"},
-            "cpu_affinity": None,
-            "namespaces": {k: False for k in
-                           ("cgroup", "ipc", "mount", "net", "pid", "user", "uts")},
-            "rlimits": {"mem_bytes": None, "nofile": None, "nproc": None},
-        },
-        "supervision": {"Restart": {"initial": 500, "max": 20000}},
-    }
-
-    txt = json.dumps(semilla, indent=1, sort_keys=True) + "\n"
+    # ── las tarjetas las emite el CENTRO, no este guión ─────────────────────────────────────────
+    # Antes esto tenía su propio molde de card, y `formatos/arje.py` el suyo. Dos emisores del mismo
+    # formato es el N×M que el pivote existe para evitar — y no fue teórico: cada copia tenía un
+    # campo mal de la raíz, y ninguno de los dos el mismo. `declarar.py` lee el censo con el lector
+    # `proc` y escribe con el escritor `arje`; lo propio que queda acá es el PERFIL.
+    lectores, escritores = formatos.cargar()
+    sistema = lectores["proc"][0](open(a.censo).read(), a.censo)
+    txt = escritores["arje"][0](sistema, a.label)
+    hijas = json.loads(txt)["genesis"]
+
     print(f"\n══ SEMILLA '{a.label}' ══")
     print(f"   {len(hijas)} servicio(s) declarados")
-    if envueltos:
-        print(f"   {len(envueltos)} envuelto(s) en `sh -c 'cd … && exec …'` porque dependen de su "
-              f"directorio\n     (el payload Native de arje no tiene `cwd`): {' '.join(envueltos)}")
-    if sin_cmdline:
-        print(f"\n   ⚠ {len(sin_cmdline)} servicio(s) NO se declararon: no se pudo leer su cmdline.")
-        print(f"     {' '.join(sin_cmdline)}")
-        print("     Una tarjeta que no puede arrancar es PEOR que una ausente: la ausente falla")
-        print("     al primer arranque, la rota deja el servicio caído sin que nada avise.")
-    print("   ⚠ El entorno (`envp`) va VACÍO: el censo no lee /proc/<pid>/environ porque trae")
-    print("     tokens. Un servicio que dependa de variables va a necesitar que alguien las escriba.")
+    por = sistema.acta.por_estado()
+    for e, titulo in (("SIN-TRADUCIR", "⚠ NO se pudo traducir — trabajo a mano"),
+                      ("decision", "◆ traducido con una DECISIÓN — revisalo")):
+        if e in por:
+            print(f"\n   {titulo} ({len(por[e])}):")
+            for x in por[e][:12]:
+                print(f"     {x['directiva'][:58]}")
+                if x["motivo"]:
+                    print(f"        └─ {x['motivo'][:92]}")
+            if len(por[e]) > 12:
+                print(f"     … y {len(por[e])-12} más (usá `traducir.py --from proc --to arje` para verlas)")
+
     if a.perfil_out:
         bloque, sin_receta = perfil(censo, a.label, _REPO)
         open(a.perfil_out, "w").write(bloque + "\n")
diff --git a/scripts/mudanza/formatos/arje.py b/scripts/mudanza/formatos/arje.py
index 3fb6de8d..8bdeb0b4 100644
--- a/scripts/mudanza/formatos/arje.py
+++ b/scripts/mudanza/formatos/arje.py
@@ -33,18 +33,18 @@ def _tarjeta(srv, acta, desde):
     if srv.cwd and srv.cwd not in ("/", ""):
         # `Native` no tiene `cwd`: se envuelve, que es el idioma que la tarjeta de sshd ya usa.
         exec_, argv = "/bin/sh", ["-c", f"cd {shlex.quote(srv.cwd)} && exec {srv.cmd}"]
-        acta.anota(srv.lineas.get("cwd", srv.origen), "decision", f"WorkingDirectory={srv.cwd}",
+        acta.anota(srv.lineas.get("cwd", srv.origen), "decision", f"{srv.nombre}: cwd={srv.cwd}",
                    "el payload `Native` no tiene `cwd`: se envuelve en `sh -c 'cd … && exec …'`")
     else:
         exec_, argv = argv_all[0], argv_all[1:]
 
     if srv.usuario and srv.usuario != "root":
-        acta.anota(srv.lineas.get("usuario", srv.origen), "SIN-TRADUCIR", f"User={srv.usuario}",
+        acta.anota(srv.lineas.get("usuario", srv.origen), "SIN-TRADUCIR", f"{srv.nombre}: User={srv.usuario}",
                    "el payload `Native` de arje NO tiene campo de usuario ⇒ esta tarjeta corre el "
                    "servicio COMO ROOT. Hay que bajar el privilegio dentro del comando "
                    "(p. ej. `setpriv --reuid`) antes de desplegarla")
     if srv.grupo and srv.grupo != "root":
-        acta.anota(srv.lineas.get("grupo", srv.origen), "SIN-TRADUCIR", f"Group={srv.grupo}",
+        acta.anota(srv.lineas.get("grupo", srv.origen), "SIN-TRADUCIR", f"{srv.nombre}: Group={srv.grupo}",
                    "mismo motivo que `User=`: no hay dónde ponerlo en la tarjeta")
 
     if srv.tipo == "forking":
@@ -109,7 +109,7 @@ def _tarjeta(srv, acta, desde):
 
 
 @escritor("arje", familia="servicio")
-def escribir(sis):
+def escribir(sis, etiqueta="mudanza"):
     acta = sis.acta
     hijas, vistos = [], {}
     for srv in sis.servicios:
@@ -137,18 +137,24 @@ def escribir(sis):
             continue
         hijas.append(t)
 
+    hijas.sort(key=lambda t: t["label"])      # orden estable ⇒ `diff` útil entre corridas
+    # ⚠ La raíz se calca de la semilla REAL del producto (`STAGE1_SEED_CARD`), y el calco importa:
+    # cuando esto vivía en dos sitios, cada copia tenía un campo mal y ninguno de los dos igual.
+    # `provides` debe anunciar Spawn y Journal —si no, las hijas no tienen quién las lance ni dónde
+    # escribir— y la supervisión de una card `Virtual` es `OneShot`: no hay proceso que reiniciar,
+    # así que un `Restart` acá le pide a arje que respawnee algo que nunca corrió.
     raiz = {
         "flow": {"input": [], "output": []},
         "genesis": hijas,
-        "id": ulid_determinista(SEM + "-raiz", "mudanza"),
-        "label": "mudanza",
+        "id": ulid_determinista(SEM + "-raiz", etiqueta),
+        "label": etiqueta,
         "lifecycle": "daemon",
         "lineage": None,
         "payload": "Virtual",
         "permissions": {"filesystem": "read-write", "ipc": {"allow": []},
                         "networking": "full", "processes": True},
         "priority": "normal",
-        "provides": [],
+        "provides": ["Spawn", "Journal"],
         "requires": [],
         "schema_version": 1,
         "soma": {"cgroup": {"cpu_weight": None, "io_weight": None, "path": "arje.slice"},
diff --git a/scripts/mudanza/formatos/proc.py b/scripts/mudanza/formatos/proc.py
new file mode 100644
index 00000000..47358a00
--- /dev/null
+++ b/scripts/mudanza/formatos/proc.py
@@ -0,0 +1,77 @@
+"""Lector del CENSO (lo que `/proc` dice que corre) → modelo pivote «servicio» (SDD 29).
+
+El tercer lector de la familia, y el que no tiene equivalente en ningún init: `systemd` y `openrc`
+leen **lo declarado**, que es exactamente lo que miente — en gioser `rc-status` daba `stopped` para
+cinco servicios que estaban vivos, y los `no-declarado` (15 de 39) no aparecen en ninguna
+declaración porque, por definición, nadie los declaró. Éste lee **lo que corre**.
+
+Los tres se complementan y ninguno solo alcanza. Que los tres desemboquen en el mismo escritor es
+justamente el punto del pivote: la tarjeta de arje se emite en UN lugar, no uno por procedencia.
+
+── LO QUE ESTE LECTOR NO PUEDE ─────────────────────────────────────────────────────────────────
+El censo **no lee `/proc/<pid>/environ`** — trae tokens y contraseñas, y un censo se guarda en un
+fichero que se manda por ahí. Así que el `envp` de toda tarjeta sale VACÍO. Un servicio que dependa
+de variables de entorno va a arrancar y fallar, y eso se dice por su nombre, no en una nota general.
+"""
+import sys, os, tomllib
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+from modelo import Servicio, Sistema                       # noqa: E402
+from formatos import lector                                # noqa: E402
+
+
+@lector("proc", familia="servicio")
+def leer(texto, nombre=""):
+    sis = Sistema(formato_origen="proc")
+    acta = sis.acta
+    censo = tomllib.loads(texto)
+    servicios = censo.get("servicio", [])
+    if not servicios:
+        acta.anota(0, "SIN-TRADUCIR", nombre or "censo",
+                   "el censo no tiene ningún `[[servicio]]`: ¿es el fichero correcto?")
+        return sis
+
+    decididos = [s for s in servicios if s.get("decision") == "muda"]
+    if not decididos:
+        # Salir con cero tarjetas y código 0 sería una semilla VACÍA que parece un éxito. Es el mismo
+        # modo de fallo que un artefacto vacío en el store: un ausente falla ruidosamente, un vacío
+        # llega hasta el final diciendo que todo fue bien.
+        acta.anota(0, "SIN-TRADUCIR", nombre or "censo",
+                   f"ninguno de los {len(servicios)} servicios tiene `decision = \"muda\"`. La "
+                   "semilla saldría VACÍA y parecería un éxito: decidí primero (`planear.py --decide`)")
+        return sis
+
+    for s in decididos:
+        srv = Servicio(nombre=s["name"], tipo="simple")
+        srv.cmd = (s.get("cmdline") or "").strip()
+        srv.cwd = (s.get("cwd") or "").strip()
+        srv.usuario = (s.get("usuario") or "").strip()
+        if srv.usuario:
+            srv.lineas["usuario"] = 0
+        # Está corriendo y se decidió mudarlo ⇒ se quiere arriba. Es una decisión, y se anota como
+        # tal: el censo no dice si estaba en el arranque, dice que está VIVO ahora.
+        srv.arranque_auto = True
+        srv.reinicio = "siempre"
+        if not srv.cmd:
+            acta.anota(0, "SIN-TRADUCIR", s["name"],
+                       "no se pudo leer su línea de comando en el origen: NO se emite tarjeta. Una "
+                       "tarjeta que no arranca es peor que una ausente — la ausente falla ruidosamente "
+                       "al primer arranque, la rota deja el servicio caído sin que nada avise")
+            continue
+        if s.get("ports"):
+            acta.anota(0, "traducido", f"{s['name']} sirve {', '.join(str(p) for p in s['ports'])}")
+        sis.servicios.append(srv)
+
+    # ⚠ UNA entrada, no una por servicio. La limitación es del LECTOR y vale igual para todos, así
+    # que repetirla 26 veces no informa 26 veces: tapa los hallazgos que sí son de un servicio
+    # concreto (el que corre como `sergio`, el que no tiene cmdline). Un acta en la que casi todo es
+    # la misma línea se deja de leer, y entonces no queda ningún acta.
+    if sis.servicios:
+        acta.anota(0, "decision", f"supervisión de las {len(sis.servicios)} tarjetas",
+                   "el censo dice que CORREN, no si el origen las levantaba al arrancar; se declaran "
+                   "con reinicio automático porque se decidió mudarlas. Sacá del `genesis` las que no "
+                   "quieras al boot")
+        acta.anota(0, "SIN-TRADUCIR", f"envp VACÍO en las {len(sis.servicios)} tarjetas",
+                   "el censo NO lee `/proc/<pid>/environ` (trae tokens), así que ninguna tarjeta "
+                   "lleva entorno. El que dependa de variables arranca y falla: " +
+                   " ".join(x.nombre for x in sis.servicios))
+    return sis