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…
⎇ e6ab5a501601fa54b181ebf2af2e80f3a5594529
scripts/test-atuq-boveda-coherente.py
Historia
Quién tocó qué
crudo
atuq: la bóveda estaba SELLADA y en ninguna imagen — y estar en la imagen no es poder abrirla
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 python3 scripts/test-atuq-boveda-coherente.py --negative-control-perfil
7
8── QUÉ MIRA, Y POR QUÉ ESTE GUARDIÁN EXISTE APARTE ───────────────────────────────────────────
9Una extensión de `atuq` está enchufada en SEIS lugares distintos, y ninguno da error si falta:
10
11 manifest.json el id que la extensión declara
12 distribution/policies.json la política que la instala
13 native-messaging/*.json el permiso para hablarle al host
14 atuq.cfg las preferencias que la acompañan
15 recipes/puriy-costura.toml el COMMIT del host, que decide si sus verbos existen
16 docs/state/targets.toml quién DECLARA al dueño y al diálogo en las imágenes
17
18Si falta la política, la extensión no se instala y el usuario ve un navegador sin la función. Si
19falta el permiso del host, la extensión se instala y **se conecta a nada** —`connectNative` falla
20en silencio y la insignia no aparece nunca—. Si faltan las preferencias, la bóveda y el gestor de
21Gecko se pelean por el mismo campo. Y si el commit pineado del host es ANTERIOR a los verbos, la
22extensión conecta, manda `vault.match` y recibe «verbo desconocido» —que `fondo.js` lee como «no hay
23bóveda» y se calla—. Y si `boveda` y `shuma-pregunta` no están declaradas en el perfil, la
24extensión se instala, conecta, manda `vault.match` y el host contesta `{"ok":true,"locked":true}`
25—«cerrada», que es una respuesta EXITOSA— porque del otro lado del socket no hay nadie: el dueño
26está sellado y en ninguna imagen. Ninguno de los cinco se ve como un error: se ven como «no anda».
27
28Éste NO reemplaza al guardián de metal (servidor HTTP, navegador real, login real, el diálogo de
29consentimiento a la vista). Lo precede: mide lo que se puede medir sin construir nada, en un
30segundo, y ahorra descubrir en metal un renglón que faltaba en un JSON.
31
32── LOS TRES CONTROLES NEGATIVOS, UNO POR AFIRMACIÓN ──────────────────────────────────────────
33`--negative-control` saca el permiso del host de una copia en memoria y exige que esto lo detecte.
34`--negative-control-verbo` le manda al quinto chequeo un verbo inventado y exige lo mismo.
35`--negative-control-perfil` saca a `boveda` de los cuatro perfiles de escritorio, también en
36memoria, y exige que el sexto lo vea. Sin ellos, un guardián que siempre dice que sí se vería
37idéntico a uno que funciona.
38
39Y el quinto chequeo trae además un control POSITIVO —`sct.observe`, que el host pineado tiene que
40tener sí o sí—, porque es un `grep` que espera cero y un cero puede ser una ausencia o un sitio
41equivocado: las dos se ven igual, y sólo las separa un patrón hermano que TENGA que dar positivo.
42"""
43import json
44import os
45import re
46import subprocess
47import sys
48import tomllib
49
50ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
51ATUQ = os.path.join(ROOT, "recipes/atuq")
52ID = "boveda@atuq.tawasuyu"
53
54# Las preferencias de D7: de fábrica manda la bóveda, pero el usuario puede volver atrás.
55PREFS = ["signon.rememberSignons", "signon.autofillForms"]
56
57# ── 8. EL QUINTO LUGAR: ¿el host que la RECETA PINEA sabe los verbos que ella manda? ──────────
58# Los siete de arriba miran `recipes/atuq/`, que es el lado del navegador. Pero una extensión no
59# está enchufada en cuatro lugares sino en CINCO, y el quinto vive en otra receta y en otro repo:
60# `recipes/puriy-costura.toml` pinea un commit de tawasuyu, y es ESE commit el que decide si
61# `vault.match` es un verbo o una cadena que nadie atiende.
62#
63# Falla igual de callado que los otros —peor, incluso—: el manifiesto está, el permiso está, la
64# política instala, `connectNative` CONECTA, la extensión manda `vault.match` y el host contesta
65# `{"ok": false, "error": "verbo desconocido: vault.match"}`. `fondo.js` lee `r.ok !== true`, borra
66# la insignia y se calla. Lo que el usuario ve es un navegador sin bóveda; lo que un guardián de
67# ficheros ve es todo en orden.
68#
69# Y no es hipotético: así estaba el 2026-09-15 al escribir esto. La extensión se commiteó con los
70# verbos ya escritos en tawasuyu (`bc6903f9e`) y la receta seguía pineada en `e19bb0e5`, que es su
71# ANCESTRO. El artefacto sellado lo confirma desde el otro lado, con su control puesto:
72# strings del binario sellado → «vault» 0 veces · «cas» 5 veces
73#
74# ── EL CONTROL POSITIVO NO ES DECORACIÓN ──────────────────────────────────────────────────────
75# Este chequeo es un `grep` que espera CERO, y un cero puede ser una ausencia o un sitio
76# equivocado: las dos se ven igual. Por eso se pregunta también por `sct.observe`, un verbo que el
77# commit pineado TIENE que tener —`sct` es la unidad 6, sellada y medida en metal—. Si el control
78# sale cero, lo que está roto es el guardián y se dice así, en vez de acusar a la receta.
79VERBOS_CONTROL = "sct.observe"
80TAWASUYU = os.environ.get("TAWASUYU", "/mnt/vvv/tawasuyu")
81RECETA_HOST = os.path.join(ROOT, "recipes/puriy-costura.toml")
82
83
84# Lo que parece un verbo y no lo es: los ficheros que el manifiesto nombra.
85NO_ES_VERBO = (".js", ".html", ".css", ".png", ".svg", ".json", ".txt", ".xpi", ".mjs")
86
87
88def verbos_que_manda_la_extension():
89 """Los verbos salen del propio código de la extensión, no de una lista escrita a mano acá.
90
91 Una lista paralela se desincroniza el día que alguien agrega un verbo y no toca este
92 fichero — y lo haría en el único sitio donde eso no puede verse.
93
94 ⚠ Y NO se busca `verb: "..."`, aunque la bóveda lo escriba así. Medido sobre las diez
95 extensiones: la de IA arma el mensaje con `postMessage({ id, verb: verbo, ... })`, o sea que
96 el verbo llega por variable y ese patrón no engancha NADA. Cero verbos encontrados y cero
97 verbos faltando se ven idénticos, y el guardián habría dicho que sí sin mirar nada. Por eso se
98 recogen todas las cadenas con forma de verbo y se descartan las que son nombres de fichero."""
99 fuentes = [f for f in sorted(os.listdir(os.path.join(ATUQ, "extensions/boveda")))
100 if f.endswith(".js")]
101 texto = "".join(leer(f"extensions/boveda/{f}") for f in fuentes)
102 crudos = set(re.findall(r'"([a-z][a-z0-9_]*\.[a-z][a-z0-9_]*)"', texto))
103 return sorted(v for v in crudos if not v.endswith(NO_ES_VERBO))
104
105
106def commit_pineado():
107 with open(RECETA_HOST, encoding="utf-8") as f:
108 m = re.search(r'^commit\s*=\s*"([0-9a-f]{7,40})"', f.read(), re.M)
109 return m.group(1) if m else None
110
111
112def atiende(commit, verbo):
113 """¿Hay un brazo de `match` para este verbo en el commit pineado?
114
115 `git grep <commit>` lee del objeto, así que no hace falta hacer checkout ni tocar el árbol
116 compartido de tawasuyu, que siempre tiene ficheros en vuelo de otras sesiones."""
117 r = subprocess.run(
118 ["git", "-C", TAWASUYU, "grep", "-qF", f'"{verbo}" =>', commit, "--", "*.rs"],
119 capture_output=True,
120 )
121 return r.returncode == 0
122
123
124def revisar_el_host(verbo_extra=None):
125 verbos = verbos_que_manda_la_extension()
126 if not verbos:
127 mal("no se pudo leer ningún `verb:` de fondo.js: este chequeo no está midiendo nada")
128 return
129 if verbo_extra:
130 verbos = verbos + [verbo_extra]
131
132 commit = commit_pineado()
133 if not commit:
134 mal(f"{os.path.relpath(RECETA_HOST, ROOT)} no pinea ningún commit")
135 return
136 if not os.path.isdir(os.path.join(TAWASUYU, ".git")):
137 # Un ausente falla RUIDOSAMENTE: si el clon no está, esto no se puede comprobar, y
138 # «no se pudo comprobar» no es «está bien».
139 mal(f"no hay clon de tawasuyu en {TAWASUYU}: el quinto lugar queda SIN COMPROBAR "
140 f"(se cambia con TAWASUYU=/ruta)")
141 return
142 if subprocess.run(["git", "-C", TAWASUYU, "cat-file", "-e", f"{commit}^{{commit}}"],
143 capture_output=True).returncode != 0:
144 mal(f"el clon de tawasuyu no conoce el commit pineado {commit[:9]}: "
145 f"hace falta un `git fetch` antes de creerle a este guardián")
146 return
147
148 # El control primero: si éste falla, ningún cero de abajo significa nada.
149 if not atiende(commit, VERBOS_CONTROL):
150 mal(f"CONTROL ROTO: {VERBOS_CONTROL} tampoco aparece en {commit[:9]}. El grep está mirando "
151 f"el sitio equivocado y los ceros de abajo no prueban ninguna ausencia")
152 return
153 bien(f"control: {VERBOS_CONTROL} SÍ está en el host pineado ({commit[:9]}) ⇒ el grep mide")
154
155 for v in verbos:
156 if atiende(commit, v):
157 bien(f"el host pineado atiende {v}")
158 else:
159 mal(f"el host pineado ({commit[:9]}) NO atiende {v}: la extensión lo manda y recibe "
160 f"«verbo desconocido», que fondo.js lee como «no hay bóveda» y se calla")
161
162# ── 9. EL SEXTO LUGAR: ¿alguien DECLARA al dueño y al diálogo en las imágenes? ───────────────
163# El quinto lugar pregunta si el host SABE los verbos. Éste pregunta lo de una capa más abajo, que
164# es lo que costó tres semanas de no ver (SDD 26 §7.sexies): quién CONTESTA del otro lado del
165# socket. El host no abre la bóveda nunca, a propósito —`sled` toma un lock exclusivo y el proceso
166# que lanza Gecko muere con cada pestaña—, así que le habla a un DUEÑO; y el 2026-09-18 el dueño
167# estaba SELLADO y en `perfiles: []`, o sea en ninguna imagen. Sellado ≠ instalado.
168#
169# Se mira `targets.toml` —el manifiesto de objetivo— y no `build-state.json`, que es su derivado:
170# lo que decide qué va en la imagen es la declaración, y el estado se recalcula solo.
171PERFILES_ESCRITORIO = ["escritorio-kde", "escritorio-gnome", "escritorio-cosmic", "escritorio-sway"]
172# Las DOS o ninguna: media bóveda es una que niega todo en silencio. Sin el dueño, `vault.match` no
173# ofrece nada; sin el diálogo, `Command::new` falla y TODO `vault.fill` se deniega.
174LA_BOVEDA_EN_LA_IMAGEN = ["boveda", "shuma-pregunta"]
175# El control POSITIVO, por lo mismo que el del §quinto lugar: un `in` que espera encontrar puede
176# fallar porque la raíz falta o porque estoy leyendo el campo equivocado, y las dos se ven igual.
177# `atuq` TIENE que estar en los cuatro — si no está, este chequeo no está midiendo el fichero.
178RAIZ_CONTROL = "atuq"
179TARGETS = os.path.join(ROOT, "docs/state/targets.toml")
180
181
182def revisar_los_perfiles(sin_esta=None):
183 try:
184 with open(TARGETS, "rb") as f:
185 perfiles = tomllib.load(f)["perfil"]
186 except FileNotFoundError:
187 mal(f"no existe {os.path.relpath(TARGETS, ROOT)}: el sexto lugar queda SIN COMPROBAR")
188 return
189 except Exception as e: # TOML roto es un fallo ruidoso, no un «está bien»
190 mal(f"no se pudo leer {os.path.relpath(TARGETS, ROOT)} ({e})")
191 return
192
193 for perfil in PERFILES_ESCRITORIO:
194 if perfil not in perfiles:
195 mal(f"{perfil} no existe en targets.toml: ¿se renombró un perfil de escritorio?")
196 continue
197 raices = list(perfiles[perfil].get("paquetes", []))
198 if sin_esta:
199 raices = [r for r in raices if r != sin_esta]
200 if RAIZ_CONTROL not in raices:
201 mal(f"CONTROL ROTO: {perfil} no declara {RAIZ_CONTROL}. O el navegador se cayó de la "
202 f"imagen, o estoy leyendo el campo equivocado — y entonces lo de abajo no prueba nada")
203 continue
204 faltan = [r for r in LA_BOVEDA_EN_LA_IMAGEN if r not in raices]
205 if faltan:
206 mal(f"{perfil} NO declara {', '.join(faltan)}: la extensión se instala igual y la "
207 f"bóveda contesta `locked:true` con `ok:true` — apagada, y sin un solo error")
208 else:
209 bien(f"{perfil} declara el dueño y el diálogo ({RAIZ_CONTROL} de control)")
210
211
212fallas = []
213
214
215def mal(msg):
216 fallas.append(msg)
217 print(f" ✗ {msg}")
218
219
220def bien(msg):
221 print(f" ✓ {msg}")
222
223
224def leer(p):
225 with open(os.path.join(ATUQ, p), encoding="utf-8") as f:
226 return f.read()
227
228
229def revisar(permisos_host, verbo_extra=None, sin_perfil=None):
230 # 1. La extensión existe y declara el id que todos los demás nombran.
231 try:
232 m = json.loads(leer("extensions/boveda/manifest.json"))
233 except FileNotFoundError:
234 mal("no existe extensions/boveda/manifest.json")
235 return
236 declarado = m.get("browser_specific_settings", {}).get("gecko", {}).get("id")
237 if declarado != ID:
238 mal(f"el manifest declara {declarado!r} y el resto nombra {ID!r}")
239 else:
240 bien(f"la extensión declara {ID}")
241
242 # 2. Los permisos que necesita para poder hacer su trabajo.
243 for p in ("nativeMessaging", "tabs"):
244 if p not in m.get("permissions", []):
245 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'}")
246 if not m.get("content_scripts"):
247 mal("sin content script no puede rellenar ningún campo")
248 if "background" not in m:
249 mal("sin fondo no hay quién hable con el host")
250
251 # 3. Los archivos que el manifest promete.
252 for rel in [m.get("background", {}).get("scripts", []), *[c.get("js", []) for c in m.get("content_scripts", [])]]:
253 for f in rel:
254 if not os.path.exists(os.path.join(ATUQ, "extensions/boveda", f)):
255 mal(f"el manifest nombra {f} y el archivo no está")
256
257 # 4. La política que la instala.
258 pol = json.loads(leer("distribution/policies.json"))
259 ext = pol.get("policies", {}).get("ExtensionSettings", {})
260 if ID not in ext:
261 mal("no está en ExtensionSettings: el navegador no la va a instalar")
262 elif ext[ID].get("installation_mode") != "normal_installed":
263 mal(f"se instala en modo {ext[ID].get('installation_mode')!r}, que no la deja activada")
264 else:
265 bien("la política la instala")
266
267 # 5. El permiso para hablarle al host. Éste es el que falla en SILENCIO.
268 if ID not in permisos_host:
269 mal("no está en allowed_extensions del host: se instala y no se conecta a nada")
270 else:
271 bien("el host la deja hablar")
272
273 # 6. Las preferencias de D7, y que sean `defaultPref` y no `lockPref`.
274 cfg = leer("atuq.cfg")
275 for p in PREFS:
276 if not re.search(rf'defaultPref\("{re.escape(p)}"', cfg):
277 mal(f"falta defaultPref({p!r}): el gestor de Gecko va a pelear por el mismo campo")
278 if re.search(rf'lockPref\("{re.escape(p)}"', cfg):
279 mal(f"{p} está como lockPref: eso decide POR el usuario, y atuq no hace eso")
280 if not fallas:
281 bien("las preferencias están, y como valores de arranque y no como cárcel")
282
283 # 7. Y lo que la extensión NO tiene que hacer: decidir ella qué credencial va.
284 fondo = leer("extensions/boveda/fondo.js")
285 if "sender.tab.url" not in fondo:
286 mal("el fondo no toma la dirección del chrome: una página podría decir que es el banco")
287 else:
288 bien("la dirección la pone el navegador, no la página")
289
290 # 8. Y el quinto lugar, que vive en otra receta y en otro repo.
291 revisar_el_host(verbo_extra)
292
293 # 9. Y el sexto, que vive en el manifiesto de objetivo: quién la pone en la imagen.
294 revisar_los_perfiles(sin_perfil)
295
296
297def main():
298 control = "--negative-control" in sys.argv
299 control_verbo = "--negative-control-verbo" in sys.argv
300 control_perfil = "--negative-control-perfil" in sys.argv
301 host = json.loads(leer("native-messaging/puriy_costura.json"))
302 permisos = list(host.get("allowed_extensions", []))
303 verbo_extra = None
304 if control:
305 print("⚠ control negativo: se le saca el permiso del host")
306 permisos = [x for x in permisos if x != ID]
307 if control_verbo:
308 # Un verbo que ningún commit puede atender: si el quinto chequeo no lo ve faltar, no está
309 # mirando el host sino contestando que sí.
310 verbo_extra = "vault.este_verbo_no_existe"
311 print(f"⚠ control negativo: se le agrega el verbo inventado {verbo_extra}")
312 sin_perfil = None
313 if control_perfil:
314 # El dueño, sacado de una copia en memoria de los cuatro perfiles: si el sexto chequeo no
315 # lo ve faltar, está contestando que sí en vez de mirar `targets.toml`.
316 sin_perfil = "boveda"
317 print(f"⚠ control negativo: se saca {sin_perfil} de los perfiles de escritorio")
318
319 print(f"── la bóveda en atuq ──")
320 revisar(permisos, verbo_extra, sin_perfil)
321
322 if control:
323 if any("allowed_extensions" in f for f in fallas):
324 print("\n✓ el control negativo lo detectó")
325 return 0
326 print("\n✗ el control negativo NO lo detectó: este guardián no mide nada")
327 return 1
328 if control_verbo:
329 if any(verbo_extra in f for f in fallas):
330 print("\n✓ el control negativo del verbo lo detectó")
331 return 0
332 print("\n✗ el control del verbo NO lo detectó: el quinto chequeo no mide nada")
333 return 1
334 if control_perfil:
335 if any(sin_perfil in f and "NO declara" in f for f in fallas):
336 print("\n✓ el control negativo del perfil lo detectó")
337 return 0
338 print("\n✗ el control del perfil NO lo detectó: el sexto chequeo no mide nada")
339 return 1
340 if fallas:
341 print(f"\n✗ {len(fallas)} problema(s)")
342 return 1
343 print("\n✓ la bóveda está enchufada entera")
344 return 0
345
346
347if __name__ == "__main__":
348 raise SystemExit(main())