diff --git a/scripts/test-atuq-boveda-coherente.py b/scripts/test-atuq-boveda-coherente.py index 94780aca..eead8d62 100755 --- a/scripts/test-atuq-boveda-coherente.py +++ b/scripts/test-atuq-boveda-coherente.py @@ -5,28 +5,38 @@ python3 scripts/test-atuq-boveda-coherente.py --negative-control ── QUÉ MIRA, Y POR QUÉ ESTE GUARDIÁN EXISTE APARTE ─────────────────────────────────────────── -Una extensión de `atuq` está enchufada en CUATRO lugares distintos, y ninguno da error si falta: +Una extensión de `atuq` está enchufada en CINCO lugares distintos, y ninguno da error si falta: manifest.json el id que la extensión declara distribution/policies.json la política que la instala native-messaging/*.json el permiso para hablarle al host atuq.cfg las preferencias que la acompañan + recipes/puriy-costura.toml el COMMIT del host, que decide si sus verbos existen Si falta la política, la extensión no se instala y el usuario ve un navegador sin la función. Si falta el permiso del host, la extensión se instala y **se conecta a nada** —`connectNative` falla en silencio y la insignia no aparece nunca—. Si faltan las preferencias, la bóveda y el gestor de -Gecko se pelean por el mismo campo. Ninguno de los tres se ve como un error: se ven como «no anda». +Gecko se pelean por el mismo campo. Y si el commit pineado del host es ANTERIOR a los verbos, la +extensión conecta, manda `vault.match` y recibe «verbo desconocido» —que `fondo.js` lee como «no hay +bóveda» y se calla—. Ninguno de los cuatro se ve como un error: se ven como «no anda». Éste NO reemplaza al guardián de metal (servidor HTTP, navegador real, login real, el diálogo de consentimiento a la vista). Lo precede: mide lo que se puede medir sin construir nada, en un segundo, y ahorra descubrir en metal un renglón que faltaba en un JSON. +── LOS DOS CONTROLES NEGATIVOS, UNO POR AFIRMACIÓN ─────────────────────────────────────────── `--negative-control` saca el permiso del host de una copia en memoria y exige que esto lo detecte. -Sin él, un guardián que siempre dice que sí se vería idéntico a uno que funciona. +`--negative-control-verbo` le manda al quinto chequeo un verbo inventado y exige lo mismo. Sin +ellos, un guardián que siempre dice que sí se vería idéntico a uno que funciona. + +Y el quinto chequeo trae además un control POSITIVO —`sct.observe`, que el host pineado tiene que +tener sí o sí—, porque es un `grep` que espera cero y un cero puede ser una ausencia o un sitio +equivocado: las dos se ven igual, y sólo las separa un patrón hermano que TENGA que dar positivo. """ import json import os import re +import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -36,6 +46,98 @@ ID = "boveda@atuq.tawasuyu" # Las preferencias de D7: de fábrica manda la bóveda, pero el usuario puede volver atrás. PREFS = ["signon.rememberSignons", "signon.autofillForms"] +# ── 8. EL QUINTO LUGAR: ¿el host que la RECETA PINEA sabe los verbos que ella manda? ────────── +# Los siete de arriba miran `recipes/atuq/`, que es el lado del navegador. Pero una extensión no +# está enchufada en cuatro lugares sino en CINCO, y el quinto vive en otra receta y en otro repo: +# `recipes/puriy-costura.toml` pinea un commit de tawasuyu, y es ESE commit el que decide si +# `vault.match` es un verbo o una cadena que nadie atiende. +# +# Falla igual de callado que los otros —peor, incluso—: el manifiesto está, el permiso está, la +# política instala, `connectNative` CONECTA, la extensión manda `vault.match` y el host contesta +# `{"ok": false, "error": "verbo desconocido: vault.match"}`. `fondo.js` lee `r.ok !== true`, borra +# la insignia y se calla. Lo que el usuario ve es un navegador sin bóveda; lo que un guardián de +# ficheros ve es todo en orden. +# +# Y no es hipotético: así estaba el 2026-09-15 al escribir esto. La extensión se commiteó con los +# verbos ya escritos en tawasuyu (`bc6903f9e`) y la receta seguía pineada en `e19bb0e5`, que es su +# ANCESTRO. El artefacto sellado lo confirma desde el otro lado, con su control puesto: +# strings del binario sellado → «vault» 0 veces · «cas» 5 veces +# +# ── EL CONTROL POSITIVO NO ES DECORACIÓN ────────────────────────────────────────────────────── +# Este chequeo es un `grep` que espera CERO, y un cero puede ser una ausencia o un sitio +# equivocado: las dos se ven igual. Por eso se pregunta también por `sct.observe`, un verbo que el +# commit pineado TIENE que tener —`sct` es la unidad 6, sellada y medida en metal—. Si el control +# sale cero, lo que está roto es el guardián y se dice así, en vez de acusar a la receta. +VERBOS_CONTROL = "sct.observe" +TAWASUYU = os.environ.get("TAWASUYU", "/mnt/vvv/tawasuyu") +RECETA_HOST = os.path.join(ROOT, "recipes/puriy-costura.toml") + + +def verbos_que_manda_la_extension(): + """Los verbos salen del propio `fondo.js`, no de una lista escrita a mano acá. + + Una lista paralela se desincroniza el día que alguien agrega un verbo y no toca este + fichero — y lo haría en el único sitio donde eso no puede verse.""" + fondo = leer("extensions/boveda/fondo.js") + return sorted(set(re.findall(r'verb:\s*"([a-z]+\.[a-z_]+)"', fondo))) + + +def commit_pineado(): + with open(RECETA_HOST, encoding="utf-8") as f: + m = re.search(r'^commit\s*=\s*"([0-9a-f]{7,40})"', f.read(), re.M) + return m.group(1) if m else None + + +def atiende(commit, verbo): + """¿Hay un brazo de `match` para este verbo en el commit pineado? + + `git grep ` lee del objeto, así que no hace falta hacer checkout ni tocar el árbol + compartido de tawasuyu, que siempre tiene ficheros en vuelo de otras sesiones.""" + r = subprocess.run( + ["git", "-C", TAWASUYU, "grep", "-qF", f'"{verbo}" =>', commit, "--", "*.rs"], + capture_output=True, + ) + return r.returncode == 0 + + +def revisar_el_host(verbo_extra=None): + verbos = verbos_que_manda_la_extension() + if not verbos: + mal("no se pudo leer ningún `verb:` de fondo.js: este chequeo no está midiendo nada") + return + if verbo_extra: + verbos = verbos + [verbo_extra] + + commit = commit_pineado() + if not commit: + mal(f"{os.path.relpath(RECETA_HOST, ROOT)} no pinea ningún commit") + return + if not os.path.isdir(os.path.join(TAWASUYU, ".git")): + # Un ausente falla RUIDOSAMENTE: si el clon no está, esto no se puede comprobar, y + # «no se pudo comprobar» no es «está bien». + mal(f"no hay clon de tawasuyu en {TAWASUYU}: el quinto lugar queda SIN COMPROBAR " + f"(se cambia con TAWASUYU=/ruta)") + return + if subprocess.run(["git", "-C", TAWASUYU, "cat-file", "-e", f"{commit}^{{commit}}"], + capture_output=True).returncode != 0: + mal(f"el clon de tawasuyu no conoce el commit pineado {commit[:9]}: " + f"hace falta un `git fetch` antes de creerle a este guardián") + return + + # El control primero: si éste falla, ningún cero de abajo significa nada. + if not atiende(commit, VERBOS_CONTROL): + mal(f"CONTROL ROTO: {VERBOS_CONTROL} tampoco aparece en {commit[:9]}. El grep está mirando " + f"el sitio equivocado y los ceros de abajo no prueban ninguna ausencia") + return + bien(f"control: {VERBOS_CONTROL} SÍ está en el host pineado ({commit[:9]}) ⇒ el grep mide") + + for v in verbos: + if atiende(commit, v): + bien(f"el host pineado atiende {v}") + else: + mal(f"el host pineado ({commit[:9]}) NO atiende {v}: la extensión lo manda y recibe " + f"«verbo desconocido», que fondo.js lee como «no hay bóveda» y se calla") + fallas = [] @@ -53,7 +155,7 @@ def leer(p): return f.read() -def revisar(permisos_host): +def revisar(permisos_host, verbo_extra=None): # 1. La extensión existe y declara el id que todos los demás nombran. try: m = json.loads(leer("extensions/boveda/manifest.json")) @@ -114,17 +216,27 @@ def revisar(permisos_host): else: bien("la dirección la pone el navegador, no la página") + # 8. Y el quinto lugar, que vive en otra receta y en otro repo. + revisar_el_host(verbo_extra) + def main(): control = "--negative-control" in sys.argv + control_verbo = "--negative-control-verbo" in sys.argv host = json.loads(leer("native-messaging/puriy_costura.json")) permisos = list(host.get("allowed_extensions", [])) + verbo_extra = None if control: print("⚠ control negativo: se le saca el permiso del host") permisos = [x for x in permisos if x != ID] + if control_verbo: + # Un verbo que ningún commit puede atender: si el quinto chequeo no lo ve faltar, no está + # mirando el host sino contestando que sí. + verbo_extra = "vault.este_verbo_no_existe" + print(f"⚠ control negativo: se le agrega el verbo inventado {verbo_extra}") print(f"── la bóveda en atuq ──") - revisar(permisos) + revisar(permisos, verbo_extra) if control: if any("allowed_extensions" in f for f in fallas): @@ -132,6 +244,12 @@ def main(): return 0 print("\n✗ el control negativo NO lo detectó: este guardián no mide nada") return 1 + if control_verbo: + if any(verbo_extra in f for f in fallas): + print("\n✓ el control negativo del verbo lo detectó") + return 0 + print("\n✗ el control del verbo NO lo detectó: el quinto chequeo no mide nada") + return 1 if fallas: print(f"\n✗ {len(fallas)} problema(s)") return 1