#!/usr/bin/env python3 """¿El navegador MUESTRA el modo foco del sistema, y lo deja como estaba? (SDD 26 §6.5) python3 scripts/test-atuq-foco.py ── QUÉ SE EJERCITA ─────────────────────────────────────────────────────────────────────────── Tres corridas, una por estado del fichero que escribe root (`/etc/takana/focus`): `on`, `off` y **ausente**. En cada una se mide lo que la extensión DICE y lo que pinta en su insignia, leído del `dump` del navegador — no lo que el host contestó, que es la mitad que ya cubren los tests de `puriy-costura`. ── Y LA PROMESA DEL §6.5, MEDIDA DEL LADO DEL NAVEGADOR ────────────────────────────────────── Después de cada corrida se vuelve a leer el fichero DENTRO de la jaula: tiene que estar **igual**. El foco lo sostiene un reglaset `nft` que aplicó root, y la única forma de que eso valga algo es que el navegador no pueda deshacerlo. Que no exista un verbo para apagarlo está fijado en tawasuyu (`no_hay_verbo_para_apagar_el_foco_y_eso_es_la_propiedad`); acá se comprueba el efecto: el estado sobrevive intacto a una sesión entera del navegador. `unknown` es el caso que más importa de los tres: si el host redondeara «no sé» a «apagado», la insignia diría «sin foco» sin que nadie lo haya mirado. REQUISITOS: el rootfs mínimo de `atuq-nested.sh`, y `atuq` + `puriy-costura` sellados. """ import os import shutil import subprocess import sys import tempfile ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs") ALPINE = os.path.join(ROOT, ".dev-fs/alpine") HAMMER = os.path.join(ROOT, "target/release/takana") STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store")) # El path de producción, el mismo que la constante del host. Se mide ÉSTE y no la escotilla de # pruebas: una escotilla mide el código, no el contrato con la imagen. FOCO = "/etc/takana/focus" def fatal(msg): print(f"✗ {msg}", file=sys.stderr) raise SystemExit(1) def artefacto(receta, nombre, escotilla=None): forzado = os.environ.get(escotilla) if escotilla else None if forzado: if not os.path.isdir(forzado): fatal(f"{escotilla}={forzado} no existe") print(f"⚠ {escotilla}: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente") return forzado r = subprocess.run([HAMMER, "--store", STORE, "hash", f"recipes/{receta}.toml"], cwd=ROOT, capture_output=True, text=True) if r.returncode != 0: fatal(f"no pude hashear {receta}: {r.stderr.strip()}") h = r.stdout.strip().splitlines()[-1].removeprefix("b3:") d = os.path.join(STORE, f"{h}-{nombre}") if not os.path.isdir(d): fatal(f"{nombre} b3:{h[:8]} no está sellado — construilo antes") if not os.listdir(d): fatal(f"{nombre} b3:{h[:8]} está VACÍO: eso no es un artefacto, es un nombre") return d def correr(atuq, costura, salida, estado): """Una sesión del navegador con el fichero de foco en `estado` (None = no existe).""" poner = "rm -f " + FOCO if estado is None else ( f"mkdir -p {os.path.dirname(FOCO)} && printf '%s\\n' '{estado}' > {FOCO}") guion = f""" mkdir -p /root /salida/hogar [ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id export HOME=/salida/hogar {poner} timeout 45 /usr/bin/atuq --profile /salida/perfil --no-remote --headless 'about:blank' \ > /salida/consola.log 2>&1 # El estado, RELEÍDO después de que el navegador terminó: es la prueba de que no lo tocó. if [ -f {FOCO} ]; then cp {FOCO} /salida/focus-final; else echo AUSENTE > /salida/focus-final; fi exit 0 """ subprocess.run([ "bwrap", "--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq, "--overlay-src", costura, "--tmp-overlay", "/", "--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys", "--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-pid", "--bind", salida, "/salida", "--uid", "0", "--gid", "0", "--setenv", "PATH", "/usr/bin:/bin:/usr/sbin:/sbin", "--setenv", "XDG_RUNTIME_DIR", "/run", "--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1", "--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1", "--setenv", "MOZ_DISABLE_GMP_SANDBOX", "1", "--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1", "--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1", "--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1", "sh", "-c", guion, ], check=False) log = open(os.path.join(salida, "consola.log"), errors="replace").read() lineas = [l[len("FOCO "):] for l in log.splitlines() if l.startswith("FOCO ")] final = "" try: final = open(os.path.join(salida, "focus-final"), errors="replace").read().strip() except OSError: pass return lineas, final, log def main(): atuq = artefacto("atuq", "atuq", "ATUQ_DIR") costura = artefacto("puriy-costura", "puriy-costura") print(f"artefactos: {os.path.basename(atuq)}\n {os.path.basename(costura)}") if not os.path.isdir(RFS): fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`") # estado puesto → (lo que la extensión tiene que decir, la insignia, el fichero tras la sesión) casos = [ ("on", "on", "foco", "on"), ("off", "off", "", "off"), (None, "unknown", "?", "AUSENTE"), ] tmp = tempfile.mkdtemp(prefix="atuq-foco-") try: for puesto, espera, insignia, tras in casos: salida = os.path.join(tmp, f"caso-{puesto or 'ausente'}") os.makedirs(os.path.join(salida, "perfil")) with open(os.path.join(salida, "perfil", "user.js"), "w") as fh: fh.write('user_pref("browser.dom.window.dump.enabled", true);\n') fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n') print(f"\n-- fichero de foco: {puesto if puesto else '(no existe)'}") lineas, final, log = correr(atuq, costura, salida, puesto) for l in lineas: print(" " + l) if not lineas: destino = os.path.join(ROOT, "work", f"atuq-foco-{puesto or 'ausente'}.log") os.makedirs(os.path.dirname(destino), exist_ok=True) shutil.copy(os.path.join(salida, "consola.log"), destino) fatal(f"la extensión no dijo nada — log en {destino}") estados = [l for l in lineas if l.startswith("ESTADO ")] if not estados: fatal(f"la extensión no informó ningún estado: {lineas}") if not estados[-1].startswith(f"ESTADO {espera} "): fatal(f"con el fichero en {puesto!r} dijo «{estados[-1]}» y tenía que decir «{espera}»") if f'INSIGNIA texto="{insignia}"' not in lineas: fatal(f"la insignia no quedó en «{insignia}»: {[l for l in lineas if 'INSIGNIA' in l]}") # Y la promesa: el navegador no cambió el estado. if final != tras: fatal(f"el fichero del foco quedó en {final!r} y tenía que quedar en {tras!r}" " — el navegador NO debería poder tocarlo") print(f" ✓ dijo {espera}, pintó «{insignia}», y dejó el estado igual ({final})") print("\n✓ los tres estados llegan al navegador tal como los dejó el sistema") print("✓ «no sé» es una respuesta: sin fichero, la insignia dice «?» y no «sin foco»") print("✓ y tras una sesión entera el estado del foco quedó intacto: el navegador no lo apaga") return 0 finally: shutil.rmtree(tmp, ignore_errors=True) sys.exit(main())