El chequeo del commit anterior miraba una extensión. El cable tiene la misma forma para las diez, y
este vigía lo recorre entero: qué verbos manda cada extensión y si el commit que `recipes/
puriy-costura.toml` pinea los atiende. Corre en un segundo y no construye nada.
Medido hoy, 13 verbos sobre 8 extensiones: sólo los TRES de la bóveda están sin dueño. O sea que
esto no se podía deducir mirando cuándo se tocó la receta por última vez — las otras nueve
extensiones conviven bien con el mismo pin viejo.
⚠ Y lo que costó más que el vigía: **el extractor de verbos no veía la mitad de lo que hay**. Estaba
buscando `verb: "…"`, que es como lo escribe la bóveda. Pero la extensión de IA arma el mensaje con
`postMessage({ id, verb: verbo, … })` —el verbo llega por VARIABLE—, así que ese patrón encontraba
cero verbos en `ia` y la declaraba sana sin haber mirado nada: cero verbos encontrados y cero verbos
faltando son el mismo cero con dos causas. Ahora recoge toda cadena con forma `ns.verbo` y descarta
las que son nombres de fichero, que es lo único que se les parece. Con eso aparecen `ai.ask` y
`archive.ask`, que antes no estaban en ninguna cuenta.
De ahí sale el tercer control, que no es negativo ni positivo sino de COBERTURA: una extensión de la
que no se extrae ningún verbo se REPORTA. Y las dos que de verdad no le hablan al host —`inicio` y
`proxy`, que viven enteras dentro del navegador— están nombradas en el código, para que «no manda
verbos» no se pueda confundir con «el extractor se quedó mudo».
Los otros tres controles: `sct.observe` tiene que aparecer en el commit pineado (si no, lo roto es
el vigía y lo dice); `--negative-control` agrega un verbo inventado a cada extensión y exige que lo
vea; y si el clon de tawasuyu no está, o no conoce el commit pineado, esto FALLA — «no se pudo
comprobar» no es «está bien». Los cuatro probados, incluida la rama del commit desconocido.
Pregunta por `git grep '"<verbo>" =>' <commit> -- '*.rs'` leyendo del objeto: sin checkout, porque
el árbol de tawasuyu es compartido y siempre tiene ficheros en vuelo de otras sesiones.
275 lines
13 KiB
Python
Executable File
275 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""¿La bóveda está enchufada ENTERA, o a medias? (SDD-BOVEDA §8, D7)
|
|
|
|
python3 scripts/test-atuq-boveda-coherente.py
|
|
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 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. 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.
|
|
`--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__)))
|
|
ATUQ = os.path.join(ROOT, "recipes/atuq")
|
|
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")
|
|
|
|
|
|
# Lo que parece un verbo y no lo es: los ficheros que el manifiesto nombra.
|
|
NO_ES_VERBO = (".js", ".html", ".css", ".png", ".svg", ".json", ".txt", ".xpi", ".mjs")
|
|
|
|
|
|
def verbos_que_manda_la_extension():
|
|
"""Los verbos salen del propio código de la extensión, 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.
|
|
|
|
⚠ Y NO se busca `verb: "..."`, aunque la bóveda lo escriba así. Medido sobre las diez
|
|
extensiones: la de IA arma el mensaje con `postMessage({ id, verb: verbo, ... })`, o sea que
|
|
el verbo llega por variable y ese patrón no engancha NADA. Cero verbos encontrados y cero
|
|
verbos faltando se ven idénticos, y el guardián habría dicho que sí sin mirar nada. Por eso se
|
|
recogen todas las cadenas con forma de verbo y se descartan las que son nombres de fichero."""
|
|
fuentes = [f for f in sorted(os.listdir(os.path.join(ATUQ, "extensions/boveda")))
|
|
if f.endswith(".js")]
|
|
texto = "".join(leer(f"extensions/boveda/{f}") for f in fuentes)
|
|
crudos = set(re.findall(r'"([a-z][a-z0-9_]*\.[a-z][a-z0-9_]*)"', texto))
|
|
return sorted(v for v in crudos if not v.endswith(NO_ES_VERBO))
|
|
|
|
|
|
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 <commit>` 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 = []
|
|
|
|
|
|
def mal(msg):
|
|
fallas.append(msg)
|
|
print(f" ✗ {msg}")
|
|
|
|
|
|
def bien(msg):
|
|
print(f" ✓ {msg}")
|
|
|
|
|
|
def leer(p):
|
|
with open(os.path.join(ATUQ, p), encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
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"))
|
|
except FileNotFoundError:
|
|
mal("no existe extensions/boveda/manifest.json")
|
|
return
|
|
declarado = m.get("browser_specific_settings", {}).get("gecko", {}).get("id")
|
|
if declarado != ID:
|
|
mal(f"el manifest declara {declarado!r} y el resto nombra {ID!r}")
|
|
else:
|
|
bien(f"la extensión declara {ID}")
|
|
|
|
# 2. Los permisos que necesita para poder hacer su trabajo.
|
|
for p in ("nativeMessaging", "tabs"):
|
|
if p not in m.get("permissions", []):
|
|
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'}")
|
|
if not m.get("content_scripts"):
|
|
mal("sin content script no puede rellenar ningún campo")
|
|
if "background" not in m:
|
|
mal("sin fondo no hay quién hable con el host")
|
|
|
|
# 3. Los archivos que el manifest promete.
|
|
for rel in [m.get("background", {}).get("scripts", []), *[c.get("js", []) for c in m.get("content_scripts", [])]]:
|
|
for f in rel:
|
|
if not os.path.exists(os.path.join(ATUQ, "extensions/boveda", f)):
|
|
mal(f"el manifest nombra {f} y el archivo no está")
|
|
|
|
# 4. La política que la instala.
|
|
pol = json.loads(leer("distribution/policies.json"))
|
|
ext = pol.get("policies", {}).get("ExtensionSettings", {})
|
|
if ID not in ext:
|
|
mal("no está en ExtensionSettings: el navegador no la va a instalar")
|
|
elif ext[ID].get("installation_mode") != "normal_installed":
|
|
mal(f"se instala en modo {ext[ID].get('installation_mode')!r}, que no la deja activada")
|
|
else:
|
|
bien("la política la instala")
|
|
|
|
# 5. El permiso para hablarle al host. Éste es el que falla en SILENCIO.
|
|
if ID not in permisos_host:
|
|
mal("no está en allowed_extensions del host: se instala y no se conecta a nada")
|
|
else:
|
|
bien("el host la deja hablar")
|
|
|
|
# 6. Las preferencias de D7, y que sean `defaultPref` y no `lockPref`.
|
|
cfg = leer("atuq.cfg")
|
|
for p in PREFS:
|
|
if not re.search(rf'defaultPref\("{re.escape(p)}"', cfg):
|
|
mal(f"falta defaultPref({p!r}): el gestor de Gecko va a pelear por el mismo campo")
|
|
if re.search(rf'lockPref\("{re.escape(p)}"', cfg):
|
|
mal(f"{p} está como lockPref: eso decide POR el usuario, y atuq no hace eso")
|
|
if not fallas:
|
|
bien("las preferencias están, y como valores de arranque y no como cárcel")
|
|
|
|
# 7. Y lo que la extensión NO tiene que hacer: decidir ella qué credencial va.
|
|
fondo = leer("extensions/boveda/fondo.js")
|
|
if "sender.tab.url" not in fondo:
|
|
mal("el fondo no toma la dirección del chrome: una página podría decir que es el banco")
|
|
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, verbo_extra)
|
|
|
|
if control:
|
|
if any("allowed_extensions" in f for f in fallas):
|
|
print("\n✓ el control negativo lo detectó")
|
|
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
|
|
print("\n✓ la bóveda está enchufada entera")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|