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 1430Shell 145Markdown 87Rust 852973 Definitions

⎇ 000a7c8144cce6fc561b41b9b1d4e4a8ae30d54b scripts/test-atuq-instalacion.py Python · 263 lines · 11.8 KB History Who touched what raw

atuq: dos cosas que el README daba por ciertas eran falsas, y las dos se midieron 97c35d67b · Sergio ·

1#!/usr/bin/env python3
2"""¿QUIÉN instala de verdad las extensiones de `atuq`: la política o la carpeta? (SDD 26 §7.bis)
3
4    python3 scripts/test-atuq-instalacion.py                    # los tres escenarios y el veredicto
5    python3 scripts/test-atuq-instalacion.py --scenario as-is    # uno solo, para diagnosticar
6
7── POR QUÉ EXISTE ESTA PRUEBA ────────────────────────────────────────────────────────────────
8Dos documentos nuestros se contradecían sobre el mecanismo del que dependen TODAS las extensiones
9de atuq —hoy `inicio` y `proxy`, mañana la de `sct`—:
10
11  · `recipes/atuq/README.md` decía que el *sideloading* desde `distribution/extensions/` «ya no
12    funciona» y que lo que instala es `ExtensionSettings.install_url` de la política;
13  · el §7.bis del SDD 26 midió lo contrario: que `install_url` con `file://` no instala nada y que
14    quien instala es el escaneo de la carpeta.
15
16Los dos mecanismos APUNTAN A LOS MISMOS FICHEROS, así que ninguna observación del artefacto los
17distingue: hay que romper uno por vez y mirar qué pasa. Eso es lo que hace este guión.
18
19── LOS TRES ESCENARIOS ───────────────────────────────────────────────────────────────────────
20    as-is         el artefacto tal cual                     ⇒ las extensiones tienen que estar
21    no-policy     `policies.json` SIN `ExtensionSettings`   ⇒ si siguen: instala LA CARPETA
22    policy-only   los XPI mudados fuera de la carpeta y la
23                  política apuntando a la ruta nueva        ⇒ si no están: la política NO instala
24
25En los tres se mira lo mismo, y por dos vías independientes:
26
27    el MOTOR      una sonda con permiso `browserSettings` pregunta el `homepageOverride`; si el
28                  override de `inicio` agarró, es `moz-extension://…/inicio.html`
29    el PERFIL     `extensions.json` dice si el complemento ESTÁ y si está `active`
30
31⚠ El `location` de `extensions.json` NO sirve para distinguir el mecanismo, y esto es medido: los
32dos XPI de atuq salen `app-profile` **también cuando los instala el escaneo de la carpeta** y no hay
33ninguna política que los nombre. Se lo imprime igual porque es barato, pero la única señal que
34discrimina es la de arriba: romper un mecanismo por vez y ver si la extensión sigue.
35
36La sonda es además el CONTROL de que la corrida sirvió: vive en `distribution/extensions/` y NO está
37declarada en ninguna política, así que si habla, el escaneo de la carpeta está vivo en esa corrida.
38Una corrida muda no se interpreta como «no instaló»: se declara rota.
39
40REQUISITOS: el rootfs de `scripts/atuq-nested.sh` (que lo hidrata) y `atuq` sellado.
41"""
42import json
43import os
44import shutil
45import subprocess
46import sys
47import tempfile
48import zipfile
49
50ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
51RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs")
52ALPINE = os.path.join(ROOT, ".dev-fs/alpine")
53HAMMER = os.path.join(ROOT, "target/release/takana")
54STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store"))
55
56APPDIR = "/usr/lib/atuq"
57EXTDIR = f"{APPDIR}/distribution/extensions"
58ALTDIR = f"{APPDIR}/xpi-politica"
59IDS = ("inicio@atuq.tawasuyu", "proxy@atuq.tawasuyu")
60
61SONDA = "sonda-instalacion@atuq.tawasuyu"
62MANIFIESTO = {
63    "manifest_version": 2,
64    "name": "sonda-instalacion",
65    "version": "1.0",
66    "browser_specific_settings": {"gecko": {"id": SONDA}},
67    "permissions": ["browserSettings"],
68    "background": {"scripts": ["fondo.js"]},
69}
70FONDO = """
71function di(s) { try { dump("INSTALACION-TEST " + s + "\\n"); } catch (e) {} }
72(async () => {
73  try {
74    const h = await browser.browserSettings.homepageOverride.get({});
75    di("HOMEPAGE " + h.value);
76  } catch (e) {
77    di("EXCEPCIÓN " + e.message);
78  }
79})();
80"""
81
82ESCENARIOS = ("as-is", "no-policy", "policy-only")
83
84
85def fatal(msg):
86    print(f"✗ {msg}", file=sys.stderr)
87    raise SystemExit(1)
88
89
90def artefacto():
91    """El artefacto VIGENTE de atuq, por `takana hash` y nunca por glob.
92
93    ⚠ ESCOTILLA `ATUQ_DIR` con su aviso puesto: el corpus es compartido y otro frente puede
94    re-hashear `firefox` dejando el atuq vigente sin sellar durante las horas del build. Con la
95    escotilla se puede medir contra uno viejo A SABIENDAS, y por eso grita.
96    """
97    forzado = os.environ.get("ATUQ_DIR")
98    if forzado:
99        if not os.path.isdir(forzado):
100            fatal(f"ATUQ_DIR={forzado} no existe")
101        print(f"⚠ ATUQ_DIR: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente")
102        return forzado
103    r = subprocess.run([HAMMER, "--store", STORE, "hash", "recipes/atuq.toml"],
104                       cwd=ROOT, capture_output=True, text=True)
105    if r.returncode != 0:
106        fatal(f"no pude hashear atuq: {r.stderr.strip()}")
107    h = r.stdout.strip().splitlines()[-1].removeprefix("b3:")
108    d = os.path.join(STORE, f"{h}-atuq")
109    if not os.path.isdir(d):
110        fatal(f"atuq b3:{h[:8]} no está sellado — construilo antes")
111    return d
112
113
114def politica_sin_extensionsettings(base):
115    p = json.load(open(base))
116    p["policies"].pop("ExtensionSettings", None)
117    return p
118
119
120def politica_apuntando_a(base, directorio):
121    p = json.load(open(base))
122    for ident, cfg in p["policies"]["ExtensionSettings"].items():
123        cfg["install_url"] = f"file://{directorio}/{ident}.xpi"
124    return p
125
126
127def correr(escenario, atuq):
128    """Una corrida de atuq headless con perfil nuevo. Devuelve (homepage, locations, log)."""
129    tmp = tempfile.mkdtemp(prefix=f"atuq-inst-{escenario}-")
130    salida = os.path.join(tmp, "salida")
131    capa_ext = os.path.join(tmp, "capa", APPDIR.lstrip("/"), "distribution", "extensions")
132    perfil = os.path.join(salida, "perfil")
133    os.makedirs(perfil)
134    os.makedirs(capa_ext)
135    try:
136        with zipfile.ZipFile(os.path.join(capa_ext, f"{SONDA}.xpi"), "w") as z:
137            z.writestr("manifest.json", json.dumps(MANIFIESTO, indent=1))
138            z.writestr("fondo.js", FONDO)
139
140        with open(os.path.join(perfil, "user.js"), "w") as fh:
141            fh.write('user_pref("browser.dom.window.dump.enabled", true);\n')
142            fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n')
143
144        base = os.path.join(atuq, APPDIR.lstrip("/"), "distribution", "policies.json")
145        preparar = "true"
146        if escenario == "no-policy":
147            nueva = politica_sin_extensionsettings(base)
148        elif escenario == "policy-only":
149            nueva = politica_apuntando_a(base, ALTDIR)
150            preparar = (f"mkdir -p {ALTDIR} && "
151                        + " && ".join(f"mv {EXTDIR}/{i}.xpi {ALTDIR}/{i}.xpi" for i in IDS))
152        else:
153            nueva = None
154        if nueva is not None:
155            capa_pol = os.path.join(tmp, "capa", APPDIR.lstrip("/"), "distribution")
156            with open(os.path.join(capa_pol, "policies.json"), "w") as fh:
157                json.dump(nueva, fh, indent=2)
158
159        guion = f"""
160        mkdir -p /root
161        [ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
162        {preparar}
163        timeout 60 /usr/bin/atuq --profile /salida/perfil --no-remote --headless about:blank \
164            > /salida/consola.log 2>&1
165        cp /salida/perfil/extensions.json /salida/extensions.json 2>/dev/null || true
166        exit 0
167        """
168        subprocess.run([
169            "bwrap",
170            "--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq,
171            "--overlay-src", os.path.join(tmp, "capa"), "--tmp-overlay", "/",
172            "--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys",
173            "--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-pid",
174            "--bind", salida, "/salida",
175            "--uid", "0", "--gid", "0",
176            "--setenv", "PATH", "/usr/bin:/bin:/usr/sbin:/sbin",
177            "--setenv", "XDG_RUNTIME_DIR", "/run", "--setenv", "HOME", "/root",
178            "--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1",
179            "--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1",
180            "--setenv", "MOZ_DISABLE_GMP_SANDBOX", "1",
181            "--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1",
182            "--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1",
183            "--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1",
184            "sh", "-c", guion,
185        ], check=False)
186
187        log = open(os.path.join(salida, "consola.log"), errors="replace").read()
188        homepage = None
189        for l in log.splitlines():
190            if l.startswith("INSTALACION-TEST HOMEPAGE "):
191                homepage = l.split(" ", 2)[2]
192        locations = {}
193        ej = os.path.join(salida, "extensions.json")
194        if os.path.exists(ej):
195            try:
196                for a in json.load(open(ej)).get("addons", []):
197                    if a.get("id") in IDS or a.get("id") == SONDA:
198                        locations[a["id"]] = (a.get("location"), bool(a.get("active")))
199            except (ValueError, KeyError) as e:
200                print(f"   ⚠ no pude leer extensions.json: {e}")
201        return homepage, locations, log
202    finally:
203        shutil.rmtree(tmp, ignore_errors=True)
204
205
206def main():
207    argv = sys.argv[1:]
208    pedido = None
209    if "--scenario" in argv:
210        pedido = argv[argv.index("--scenario") + 1]
211        if pedido not in ESCENARIOS:
212            fatal(f"escenario desconocido: {pedido} (hay {', '.join(ESCENARIOS)})")
213    if not os.path.isdir(RFS):
214        fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`, que lo hidrata")
215
216    atuq = artefacto()
217    print(f"artefacto: {os.path.basename(atuq)}")
218    res = {}
219    for e in (ESCENARIOS if pedido is None else (pedido,)):
220        print(f"\n== escenario {e}")
221        homepage, locations, log = correr(e, atuq)
222        nuestra = bool(homepage and "inicio.html" in homepage)
223        print(f"   MOTOR   homepage = {homepage}"
224              f"   ⇒ {'la de atuq' if nuestra else 'la de Firefox'}")
225        if not homepage:
226            destino = os.path.join(ROOT, "work", f"atuq-instalacion-{e}.log")
227            os.makedirs(os.path.dirname(destino), exist_ok=True)
228            open(destino, "w").write(log)
229            fatal(f"la sonda no dijo NADA en «{e}» — corrida rota, no resultado. Log en {destino}")
230        for ident, (loc, activo) in sorted(locations.items()):
231            print(f"   PERFIL  {ident:34s} location={loc} activo={activo}")
232        for ident in IDS:
233            if ident not in locations:
234                print(f"   PERFIL  {ident:34s} NO ESTÁ en extensions.json")
235        res[e] = (nuestra, locations)
236
237    if pedido is not None:
238        return 0
239
240    print()
241    fallos = []
242    if not res["as-is"][0]:
243        fallos.append("tal cual, la página de inicio NO es la de atuq — atuq está roto, "
244                      "no es un problema del mecanismo")
245    if not res["no-policy"][0]:
246        fallos.append("sin `ExtensionSettings` las extensiones NO se instalan ⇒ el escaneo de "
247                      "`distribution/extensions/` YA NO alcanza: hay que revisar el §7.bis y el "
248                      "README, porque todas las extensiones de atuq cuelgan de ahí")
249    if res["policy-only"][0]:
250        fallos.append("con los XPI FUERA de la carpeta y sólo la política apuntándolos, las "
251                      "extensiones SÍ se instalaron ⇒ `install_url` con `file://` ahora funciona "
252                      "y el §7.bis quedó viejo")
253    if fallos:
254        for f in fallos:
255            print(f"✗ {f}", file=sys.stderr)
256        return 1
257    print("✓ quien instala es el ESCANEO de `distribution/extensions/`: sin la política siguen "
258          "puestas, y con la política sola no aparecen.")
259    print("  La política las FIJA y las CONFIGURA; instalarlas, no. (SDD 26 §7.bis)")
260    return 0
261
262
263sys.exit(main())