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 188Rust 55Shell 30Markdown 281211 Definitions

⎇ e7369efe824aed48bb1b0752bd128d37c9b7b791 scripts/test-atuq-boveda-coherente.py Python · 261 lines · 12.3 KB History Who touched what raw

atuq: la bóveda le habla a un host que no sabe sus verbos — y eran CINCO lugares, no cuatro e7369efe8 · Sergio ·

1#!/usr/bin/env python3
2"""¿La bóveda está enchufada ENTERA, o a medias? (SDD-BOVEDA §8, D7)
3
4    python3 scripts/test-atuq-boveda-coherente.py
5    python3 scripts/test-atuq-boveda-coherente.py --negative-control
6
7── QUÉ MIRA, Y POR QUÉ ESTE GUARDIÁN EXISTE APARTE ───────────────────────────────────────────
8Una extensión de `atuq` está enchufada en CINCO lugares distintos, y ninguno da error si falta:
9
10    manifest.json                     el id que la extensión declara
11    distribution/policies.json        la política que la instala
12    native-messaging/*.json           el permiso para hablarle al host
13    atuq.cfg                          las preferencias que la acompañan
14    recipes/puriy-costura.toml        el COMMIT del host, que decide si sus verbos existen
15
16Si falta la política, la extensión no se instala y el usuario ve un navegador sin la función. Si
17falta el permiso del host, la extensión se instala y **se conecta a nada** —`connectNative` falla
18en silencio y la insignia no aparece nunca—. Si faltan las preferencias, la bóveda y el gestor de
19Gecko se pelean por el mismo campo. Y si el commit pineado del host es ANTERIOR a los verbos, la
20extensión conecta, manda `vault.match` y recibe «verbo desconocido» —que `fondo.js` lee como «no hay
21bóveda» y se calla—. Ninguno de los cuatro se ve como un error: se ven como «no anda».
22
23Éste NO reemplaza al guardián de metal (servidor HTTP, navegador real, login real, el diálogo de
24consentimiento a la vista). Lo precede: mide lo que se puede medir sin construir nada, en un
25segundo, y ahorra descubrir en metal un renglón que faltaba en un JSON.
26
27── LOS DOS CONTROLES NEGATIVOS, UNO POR AFIRMACIÓN ───────────────────────────────────────────
28`--negative-control` saca el permiso del host de una copia en memoria y exige que esto lo detecte.
29`--negative-control-verbo` le manda al quinto chequeo un verbo inventado y exige lo mismo. Sin
30ellos, un guardián que siempre dice que sí se vería idéntico a uno que funciona.
31
32Y el quinto chequeo trae además un control POSITIVO —`sct.observe`, que el host pineado tiene que
33tener sí o sí—, porque es un `grep` que espera cero y un cero puede ser una ausencia o un sitio
34equivocado: las dos se ven igual, y sólo las separa un patrón hermano que TENGA que dar positivo.
35"""
36import json
37import os
38import re
39import subprocess
40import sys
41
42ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
43ATUQ = os.path.join(ROOT, "recipes/atuq")
44ID = "boveda@atuq.tawasuyu"
45
46# Las preferencias de D7: de fábrica manda la bóveda, pero el usuario puede volver atrás.
47PREFS = ["signon.rememberSignons", "signon.autofillForms"]
48
49# ── 8. EL QUINTO LUGAR: ¿el host que la RECETA PINEA sabe los verbos que ella manda? ──────────
50# Los siete de arriba miran `recipes/atuq/`, que es el lado del navegador. Pero una extensión no
51# está enchufada en cuatro lugares sino en CINCO, y el quinto vive en otra receta y en otro repo:
52# `recipes/puriy-costura.toml` pinea un commit de tawasuyu, y es ESE commit el que decide si
53# `vault.match` es un verbo o una cadena que nadie atiende.
54#
55# Falla igual de callado que los otros —peor, incluso—: el manifiesto está, el permiso está, la
56# política instala, `connectNative` CONECTA, la extensión manda `vault.match` y el host contesta
57# `{"ok": false, "error": "verbo desconocido: vault.match"}`. `fondo.js` lee `r.ok !== true`, borra
58# la insignia y se calla. Lo que el usuario ve es un navegador sin bóveda; lo que un guardián de
59# ficheros ve es todo en orden.
60#
61# Y no es hipotético: así estaba el 2026-09-15 al escribir esto. La extensión se commiteó con los
62# verbos ya escritos en tawasuyu (`bc6903f9e`) y la receta seguía pineada en `e19bb0e5`, que es su
63# ANCESTRO. El artefacto sellado lo confirma desde el otro lado, con su control puesto:
64#     strings del binario sellado → «vault» 0 veces · «cas» 5 veces
65#
66# ── EL CONTROL POSITIVO NO ES DECORACIÓN ──────────────────────────────────────────────────────
67# Este chequeo es un `grep` que espera CERO, y un cero puede ser una ausencia o un sitio
68# equivocado: las dos se ven igual. Por eso se pregunta también por `sct.observe`, un verbo que el
69# commit pineado TIENE que tener —`sct` es la unidad 6, sellada y medida en metal—. Si el control
70# sale cero, lo que está roto es el guardián y se dice así, en vez de acusar a la receta.
71VERBOS_CONTROL = "sct.observe"
72TAWASUYU = os.environ.get("TAWASUYU", "/mnt/vvv/tawasuyu")
73RECETA_HOST = os.path.join(ROOT, "recipes/puriy-costura.toml")
74
75
76def verbos_que_manda_la_extension():
77    """Los verbos salen del propio `fondo.js`, no de una lista escrita a mano acá.
78
79    Una lista paralela se desincroniza el día que alguien agrega un verbo y no toca este
80    fichero — y lo haría en el único sitio donde eso no puede verse."""
81    fondo = leer("extensions/boveda/fondo.js")
82    return sorted(set(re.findall(r'verb:\s*"([a-z]+\.[a-z_]+)"', fondo)))
83
84
85def commit_pineado():
86    with open(RECETA_HOST, encoding="utf-8") as f:
87        m = re.search(r'^commit\s*=\s*"([0-9a-f]{7,40})"', f.read(), re.M)
88    return m.group(1) if m else None
89
90
91def atiende(commit, verbo):
92    """¿Hay un brazo de `match` para este verbo en el commit pineado?
93
94    `git grep <commit>` lee del objeto, así que no hace falta hacer checkout ni tocar el árbol
95    compartido de tawasuyu, que siempre tiene ficheros en vuelo de otras sesiones."""
96    r = subprocess.run(
97        ["git", "-C", TAWASUYU, "grep", "-qF", f'"{verbo}" =>', commit, "--", "*.rs"],
98        capture_output=True,
99    )
100    return r.returncode == 0
101
102
103def revisar_el_host(verbo_extra=None):
104    verbos = verbos_que_manda_la_extension()
105    if not verbos:
106        mal("no se pudo leer ningún `verb:` de fondo.js: este chequeo no está midiendo nada")
107        return
108    if verbo_extra:
109        verbos = verbos + [verbo_extra]
110
111    commit = commit_pineado()
112    if not commit:
113        mal(f"{os.path.relpath(RECETA_HOST, ROOT)} no pinea ningún commit")
114        return
115    if not os.path.isdir(os.path.join(TAWASUYU, ".git")):
116        # Un ausente falla RUIDOSAMENTE: si el clon no está, esto no se puede comprobar, y
117        # «no se pudo comprobar» no es «está bien».
118        mal(f"no hay clon de tawasuyu en {TAWASUYU}: el quinto lugar queda SIN COMPROBAR "
119            f"(se cambia con TAWASUYU=/ruta)")
120        return
121    if subprocess.run(["git", "-C", TAWASUYU, "cat-file", "-e", f"{commit}^{{commit}}"],
122                      capture_output=True).returncode != 0:
123        mal(f"el clon de tawasuyu no conoce el commit pineado {commit[:9]}: "
124            f"hace falta un `git fetch` antes de creerle a este guardián")
125        return
126
127    # El control primero: si éste falla, ningún cero de abajo significa nada.
128    if not atiende(commit, VERBOS_CONTROL):
129        mal(f"CONTROL ROTO: {VERBOS_CONTROL} tampoco aparece en {commit[:9]}. El grep está mirando "
130            f"el sitio equivocado y los ceros de abajo no prueban ninguna ausencia")
131        return
132    bien(f"control: {VERBOS_CONTROL} SÍ está en el host pineado ({commit[:9]}) ⇒ el grep mide")
133
134    for v in verbos:
135        if atiende(commit, v):
136            bien(f"el host pineado atiende {v}")
137        else:
138            mal(f"el host pineado ({commit[:9]}) NO atiende {v}: la extensión lo manda y recibe "
139                f"«verbo desconocido», que fondo.js lee como «no hay bóveda» y se calla")
140
141fallas = []
142
143
144def mal(msg):
145    fallas.append(msg)
146    print(f"  ✗ {msg}")
147
148
149def bien(msg):
150    print(f"  ✓ {msg}")
151
152
153def leer(p):
154    with open(os.path.join(ATUQ, p), encoding="utf-8") as f:
155        return f.read()
156
157
158def revisar(permisos_host, verbo_extra=None):
159    # 1. La extensión existe y declara el id que todos los demás nombran.
160    try:
161        m = json.loads(leer("extensions/boveda/manifest.json"))
162    except FileNotFoundError:
163        mal("no existe extensions/boveda/manifest.json")
164        return
165    declarado = m.get("browser_specific_settings", {}).get("gecko", {}).get("id")
166    if declarado != ID:
167        mal(f"el manifest declara {declarado!r} y el resto nombra {ID!r}")
168    else:
169        bien(f"la extensión declara {ID}")
170
171    # 2. Los permisos que necesita para poder hacer su trabajo.
172    for p in ("nativeMessaging", "tabs"):
173        if p not in m.get("permissions", []):
174            mal(f"le falta el permiso {p!r}: sin eso no puede {'hablarle al host' if p == 'nativeMessaging' else 'saber qué página mira el usuario'}")
175    if not m.get("content_scripts"):
176        mal("sin content script no puede rellenar ningún campo")
177    if "background" not in m:
178        mal("sin fondo no hay quién hable con el host")
179
180    # 3. Los archivos que el manifest promete.
181    for rel in [m.get("background", {}).get("scripts", []), *[c.get("js", []) for c in m.get("content_scripts", [])]]:
182        for f in rel:
183            if not os.path.exists(os.path.join(ATUQ, "extensions/boveda", f)):
184                mal(f"el manifest nombra {f} y el archivo no está")
185
186    # 4. La política que la instala.
187    pol = json.loads(leer("distribution/policies.json"))
188    ext = pol.get("policies", {}).get("ExtensionSettings", {})
189    if ID not in ext:
190        mal("no está en ExtensionSettings: el navegador no la va a instalar")
191    elif ext[ID].get("installation_mode") != "normal_installed":
192        mal(f"se instala en modo {ext[ID].get('installation_mode')!r}, que no la deja activada")
193    else:
194        bien("la política la instala")
195
196    # 5. El permiso para hablarle al host. Éste es el que falla en SILENCIO.
197    if ID not in permisos_host:
198        mal("no está en allowed_extensions del host: se instala y no se conecta a nada")
199    else:
200        bien("el host la deja hablar")
201
202    # 6. Las preferencias de D7, y que sean `defaultPref` y no `lockPref`.
203    cfg = leer("atuq.cfg")
204    for p in PREFS:
205        if not re.search(rf'defaultPref\("{re.escape(p)}"', cfg):
206            mal(f"falta defaultPref({p!r}): el gestor de Gecko va a pelear por el mismo campo")
207        if re.search(rf'lockPref\("{re.escape(p)}"', cfg):
208            mal(f"{p} está como lockPref: eso decide POR el usuario, y atuq no hace eso")
209    if not fallas:
210        bien("las preferencias están, y como valores de arranque y no como cárcel")
211
212    # 7. Y lo que la extensión NO tiene que hacer: decidir ella qué credencial va.
213    fondo = leer("extensions/boveda/fondo.js")
214    if "sender.tab.url" not in fondo:
215        mal("el fondo no toma la dirección del chrome: una página podría decir que es el banco")
216    else:
217        bien("la dirección la pone el navegador, no la página")
218
219    # 8. Y el quinto lugar, que vive en otra receta y en otro repo.
220    revisar_el_host(verbo_extra)
221
222
223def main():
224    control = "--negative-control" in sys.argv
225    control_verbo = "--negative-control-verbo" in sys.argv
226    host = json.loads(leer("native-messaging/puriy_costura.json"))
227    permisos = list(host.get("allowed_extensions", []))
228    verbo_extra = None
229    if control:
230        print("⚠ control negativo: se le saca el permiso del host")
231        permisos = [x for x in permisos if x != ID]
232    if control_verbo:
233        # Un verbo que ningún commit puede atender: si el quinto chequeo no lo ve faltar, no está
234        # mirando el host sino contestando que sí.
235        verbo_extra = "vault.este_verbo_no_existe"
236        print(f"⚠ control negativo: se le agrega el verbo inventado {verbo_extra}")
237
238    print(f"── la bóveda en atuq ──")
239    revisar(permisos, verbo_extra)
240
241    if control:
242        if any("allowed_extensions" in f for f in fallas):
243            print("\n✓ el control negativo lo detectó")
244            return 0
245        print("\n✗ el control negativo NO lo detectó: este guardián no mide nada")
246        return 1
247    if control_verbo:
248        if any(verbo_extra in f for f in fallas):
249            print("\n✓ el control negativo del verbo lo detectó")
250            return 0
251        print("\n✗ el control del verbo NO lo detectó: el quinto chequeo no mide nada")
252        return 1
253    if fallas:
254        print(f"\n✗ {len(fallas)} problema(s)")
255        return 1
256    print("\n✓ la bóveda está enchufada entera")
257    return 0
258
259
260if __name__ == "__main__":
261    raise SystemExit(main())