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 1389Shell 137Markdown 80Rust 802602 Sutichaykuna

⎇ 534014c3ec69daea789a1d0523e1d00a69c6ed65 scripts/mudanza/declarar.py Python · 172 siq'ikuna · 10.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

planear: buscar la receta por el PAQUETE del origen — y el intérprete no es el programa 053e5e7ed · Sergio ·

1#!/usr/bin/env python3
2"""declarar.py — convierte los servicios de un censo en una SEMILLA de arje (SDD 29 etapa 3).
3
4El paso que cierra la mudanza. Copiar un binario no lo levanta al arrancar: hay que DECLARARLO. Esto
5toma lo que el censo leyó de `/proc` —la invocación real— y emite `seed.card.json`.
6
7── POR QUÉ NO ALCANZA CON `arje-absorb` ────────────────────────────────────────────────────────
8`arje-absorb` ya traduce sysvinit/runit/dinit/openrc a una Semilla, y está bien hecho. Pero lee **la
9declaración del init ajeno**, que es exactamente la que miente: en gioser `rc-status` daba `stopped`
10para cinco servicios que estaban vivos, y los que NADIE declara —los `no-declarado`, que son el
11trabajo real— no aparecen en ninguna. Absorber la declaración reproduce el agujero.
12
13Esto lee lo VIVO. Los dos se complementan: absorb para lo declarado, esto para lo que corre.
14
15── LAS TRES REGLAS ─────────────────────────────────────────────────────────────────────────────
161. **Un servicio sin `cmdline` legible NO se emite**, y se dice. Una tarjeta que no puede arrancar
17   es peor que una tarjeta ausente: la ausente falla ruidosamente al primer arranque, la rota
18   arranca el sistema y deja el servicio caído sin que nada avise.
192. **El `cwd` se preserva envolviendo**, porque el payload `Native` de arje acepta `exec`/`argv`/
20   `envp` y **NO `cwd`** (comprobado sobre la semilla real). Un servicio que depende de su
21   directorio —como `puerta-f6e393ff`, que corre con `cwd=/mnt/vvv/tawasuyu/shared/tejido`— arrancaría
22   en `/` y fallaría de un modo difícil de atribuir. Se envuelve en `sh -c 'cd … && exec …'`, que es
23   el idioma que la propia tarjeta de `sshd` ya usa.
243. **Los IDs son DETERMINISTAS**, derivados del nombre. El mismo censo produce la misma semilla byte
25   a byte, así que dos corridas se pueden comparar con `diff` — que es lo que vuelve revisable un
26   fichero generado.
27
28⚠ NO se copia el entorno. El censo no lo lee (trae tokens), así que un servicio que dependa de
29variables de entorno va a necesitar que alguien las escriba. Se avisa por nombre.
30
31Uso:
32    scripts/mudanza/declarar.py --censo censo.toml [--label mi-servidor] --out seed.card.json
33"""
34import argparse, hashlib, json, os, shlex, sys, tomllib
35
36sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
37from planear import origen_binario, alternativas, _REPO   # noqa: E402
38import formatos                                            # noqa: E402
39
40def perfil(censo, label, repo):
41    """Emite un `[perfil.<label>]` para `docs/state/targets.toml`.
42
43    ── POR QUÉ UN PERFIL Y NO UNA LISTA DE `install` ────────────────────────────────────────────
44    Mudar servicio por servicio a una caja viva es lo que uno haría en cualquier distro, y en takana
45    es ir contra el diseño: **el software de una máquina se DECLARA en `targets.toml` y viene en la
46    imagen**, reproducible y firmado. Un servidor armado a fuerza de instalaciones sueltas no se
47    puede volver a construir — que es exactamente el problema que esta mudanza existe para no repetir
48    (el caddy puesto a mano en gioser no tiene dueño ni receta: se pierde con la máquina).
49
50    Además hay un impedimento medido: `takana install` REPRODUCE desde fuente, y eso exige el LAB
51    entero en el cliente (SDD 28 §5.4). Una caja de destino no lo tiene. El perfil esquiva el
52    problema por el lado correcto: el software entra al armar la imagen, en el hub, que sí tiene lab.
53
54    Lo que el perfil NO puede cubrir se lista aparte, con su motivo. Un perfil que se calla lo que le
55    falta sale N/N y describe un servidor incompleto — la lección de `foot` en `escritorio-sway`."""
56    raices, sin_receta, sin_sellar = {}, [], set()
57    for s_ in censo.get("servicio", []):
58        if s_.get("decision") != "muda":
59            continue
60        elegido = s_.get("alternativa") or s_["name"]
61        clase, detalle = origen_binario(dict(s_, name=elegido), repo)
62        if clase == "receta-takana":
63            import re as _re
64            m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
65            # Se agrupa POR RECETA, no por servicio: dos servicios pueden necesitar la misma
66            # (uvicorn y python3 son los dos `python3`), y repetir una raíz en un perfil no la
67            # duplica — sólo ensucia y hace creer que son dos cosas distintas.
68            receta = m.group(1) if m else elegido
69            # ⚠ Sellada y sin sellar no son lo mismo, y el perfil las listaba igual. Una sellada se
70            # INSTALA del repo firmado; una sin sellar hay que CONSTRUIRLA antes, y eso puede ser una
71            # tarde. Medido: `qdrant` entró al catálogo ayer desde otro frente y no tiene artefacto.
72            if "sin sellar" in detalle:
73                sin_sellar.add(receta)
74            raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
75        else:
76            # ⚠ Un servicio de clase `interprete` cuenta las DOS cosas a la vez, y hay que hacer las
77            # dos: su runtime SÍ entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo
78            # que pase— y el servicio sigue listado como NO CUBIERTO, porque el intérprete no es el
79            # programa. Meterlo sólo en las raíces haría salir el perfil N/N con la app faltando;
80            # dejarlo sólo en la lista de faltantes armaría una imagen sin runtime y el programa,
81            # cuando llegue, no arrancaría.
82            if clase == "interprete":
83                import re as _re
84                m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
85                if m:
86                    raices.setdefault(m.group(1), []).append(
87                        (f"{s_['name']} (sólo el runtime)", tuple(s_.get("ports", []))))
88            sin_receta.append((s_["name"], clase, detalle))
89
90    L = [f"[perfil.{label}]",
91         f'descripcion = "servicios mudados desde {censo.get("maquina",{}).get("hostname","?")} '
92         f'— generado por scripts/mudanza/declarar.py"',
93         'hereda = ["base"]',
94         "# Generado desde un CENSO, no escrito a mano: cada raíz está acá porque un servicio que",
95         "# CORRÍA en la máquina vieja la necesita, y takana tiene receta para construirla.",
96         "paquetes = ["]
97    for receta in sorted(raices):
98        quienes = raices[receta]
99        nombres = ", ".join(sorted({q[0] for q in quienes}))
100        puertos = sorted({p for q in quienes for p in q[1]})
101        det = nombres + (f" · puertos {','.join(str(x) for x in puertos)}" if puertos else "")
102        if receta in sin_sellar:
103            det = ("⚠ SIN SELLAR: hay que construirla antes de armar la imagen — " + det).strip(" —")
104        nota = f"  # {det}" if det and det != receta else ""
105        L.append(f'  "{receta}",{nota}')
106    L.append("]")
107    if sin_sellar:
108        L.append(f"# ⚠ {len(sin_sellar)} sin artefacto en el store: {', '.join(sorted(sin_sellar))}.")
109        L.append("#   El perfil es correcto, pero armar la imagen las va a CONSTRUIR, no instalar.")
110    return "\n".join(L), sin_receta
111
112
113def main():
114    ap = argparse.ArgumentParser(description="Genera una Semilla de arje desde un censo decidido.")
115    ap.add_argument("--censo", required=True)
116    ap.add_argument("--label", default="takana-servidor", help="etiqueta de la Semilla raíz")
117    ap.add_argument("--out", help="fichero de salida (default: stdout)")
118    ap.add_argument("--perfil-out", help="además, escribir un [perfil.<label>] para targets.toml")
119    a = ap.parse_args()
120
121    with open(a.censo, "rb") as f:
122        censo = tomllib.load(f)
123
124    # ── las tarjetas las emite el CENTRO, no este guión ─────────────────────────────────────────
125    # Antes esto tenía su propio molde de card, y `formatos/arje.py` el suyo. Dos emisores del mismo
126    # formato es el N×M que el pivote existe para evitar — y no fue teórico: cada copia tenía un
127    # campo mal de la raíz, y ninguno de los dos el mismo. `declarar.py` lee el censo con el lector
128    # `proc` y escribe con el escritor `arje`; lo propio que queda acá es el PERFIL.
129    lectores, escritores = formatos.cargar()
130    sistema = lectores["proc"][0](open(a.censo).read(), a.censo)
131    txt = escritores["arje"][0](sistema, a.label)
132    hijas = json.loads(txt)["genesis"]
133
134    print(f"\n══ SEMILLA '{a.label}' ══")
135    print(f"   {len(hijas)} servicio(s) declarados")
136    por = sistema.acta.por_estado()
137    for e, titulo in (("SIN-TRADUCIR", "⚠ NO se pudo traducir — trabajo a mano"),
138                      ("decision", "◆ traducido con una DECISIÓN — revisalo")):
139        if e in por:
140            print(f"\n   {titulo} ({len(por[e])}):")
141            for x in por[e][:12]:
142                print(f"     {x['directiva'][:58]}")
143                if x["motivo"]:
144                    print(f"        └─ {x['motivo'][:92]}")
145            if len(por[e]) > 12:
146                print(f"     … y {len(por[e])-12} más (usá `traducir.py --from proc --to arje` para verlas)")
147
148    if a.perfil_out:
149        bloque, sin_receta = perfil(censo, a.label, _REPO)
150        open(a.perfil_out, "w").write(bloque + "\n")
151        n_r = len([l for l in bloque.splitlines() if l.strip().startswith('"')])
152        print(f"\n══ PERFIL '{a.label}' ══")
153        print(f"   {n_r} receta(s) que takana YA construye → {a.perfil_out}")
154        print("   Pegalo en docs/state/targets.toml y armá la imagen: el software del servidor nuevo")
155        print("   se DECLARA, no se instala a mano. Además `takana install` reproduce desde fuente y")
156        print("   eso exige el lab entero en el cliente — el perfil lo resuelve en el hub, que sí lo tiene.")
157        if sin_receta:
158            print(f"\n   ⚠ {len(sin_receta)} servicio(s) que el perfil NO puede cubrir:")
159            for nom, cl, det in sin_receta:
160                print(f"     {nom:22} [{cl}] {det[:84]}")
161            print("     Un perfil que se calla lo que le falta sale N/N describiendo un servidor")
162            print("     incompleto. Para cada uno: escribir la receta, elegir un equivalente que sí")
163            print("     esté, o llevar el binario a mano sabiendo que no se reproduce.")
164    if a.out:
165        open(a.out, "w").write(txt)
166        print(f"\n   semilla → {a.out}")
167    else:
168        print(txt)
169
170
171if __name__ == "__main__":
172    main()