> 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…
atuq §6.5.bis: la extensión `foco` — muestra el foco del sistema y no tiene con qué apagarlo
La mitad del navegador del §6.5, deliberadamente asimétrica: sólo lee. Pregunta `focus.state` (nuevo
en puriy-costura, commit 38815b5f3) y pinta tres estados — `foco`, nada, y `?` cuando nadie escribió
el estado. Ese tercero es el que importa: si «no sé» se redondeara a «apagado», la insignia afirmaría
que no hay foco sin haberlo mirado.
No hay verbo para apagarlo, y es la propiedad y no un pendiente: si el navegador pudiera levantar el
foco, valdría lo que vale un bloqueador de extensión. En tawasuyu hay un test que lo fija; acá el
guardián mide el EFECTO — tras una sesión entera, el fichero de estado quedó igual.
`scripts/test-atuq-foco.py` corre los tres estados sobre el path de PRODUCCIÓN (`/etc/takana/focus`),
no la escotilla de pruebas: una escotilla mide el código, no el contrato con la imagen. Verde sobre
el artefacto vigente (atuq 5d1afc50, puriy-costura 0de6b4ca), y verificado rompiéndolo — con una
extensión parcheada que pinta «foco» siempre (en una COPIA del artefacto, vía ATUQ_DIR), falla
nombrando la insignia.
diff --git a/docs/26-atuq-envoltorio-gecko.md b/docs/26-atuq-envoltorio-gecko.md
index 87b9b60b..b31e3416 100644
--- a/docs/26-atuq-envoltorio-gecko.md
+++ b/docs/26-atuq-envoltorio-gecko.md
@@ -491,7 +491,7 @@ La columna «quién más lo tiene» es lo que evita que nos contemos un cuento.
| 6.2 | **Descargas direccionadas por contenido** | nadie | store CAS de takana, `tejido` | bajo |
| 6.3 | **Archivo personal + RAG local** | Rewind/Recall (nube, Windows); SingleFile (guarda, no busca) | `khipu`, `rag-motor`, `willay-rag` | medio |
| 6.4 | **Historial y perfil sobre `qullqa`** | nadie | `qullqa-core`, `qullqa-pozo` | medio |
-| 6.5 | **Foco por `cortafuegos`, no por extensión** | nadie (nadie es dueño del navegador *y* del sistema) | `cortafuegos`, `pacha` | bajo |
+| 6.5 | **Foco por `cortafuegos`, no por extensión** ✅ **cadena y navegador; falta quién lo aplica** | nadie (nadie es dueño del navegador *y* del sistema) | `cortafuegos`, `nftables` (nuevo en el corpus) | bajo |
| 6.6 | **Medios por fuera del navegador** | extensiones sueltas; de fábrica no | `foreign-ytdlp`, `-platform`, `-dlna` | bajo |
| 6.7 | **IA local en la barra lateral** | Chrome/Edge son nube; Zen no tiene | `rimay`, `iniy` | bajo |
| 6.8 | **Proxy por contenedor** ✅ **v0.5** | nadie de fábrica | — (API de Firefox) | bajo |
@@ -761,6 +761,35 @@ navegador:
Mientras eso no exista, lo que `atuq` puede tener —y tiene, §6.5.bis— es la mitad honesta: **ver** el
estado del foco y **no poder apagarlo**.
+#### 6.5.bis La mitad del navegador: `focus.state`, y la asimetría a propósito (2026-09-10)
+
+La extensión `foco` **sólo lee**. Pregunta `focus.state` al host cada minuto —el estado lo cambia root
+por fuera, no hay evento al que suscribirse— y pinta tres cosas: `foco`, nada, o **`?`**. El tercer
+caso es el que importa: el host contesta `unknown` cuando nadie escribió el estado o cuando el
+contenido no se entiende, y **eso no se redondea a `off`**; una insignia que dijera «sin foco» sin
+haber mirado afirmaría un hecho que no tiene.
+
+**Y no hay verbo para apagarlo.** `focus.stop` cae en «verbo desconocido», igual que `focus.off` y
+`focus.start`. No es una omisión pendiente: es la propiedad entera. Si el navegador pudiera levantar
+el foco, esto valdría exactamente lo que vale un bloqueador de extensión —dos clics— que es el
+problema que el §6.5 venía a resolver. Encenderlo tampoco está: aplicar un reglaset pide root y este
+proceso corre como el usuario. Hay un test en tawasuyu que lo fija por nombre, y se puso rojo a
+propósito agregando un `focus.stop` «por comodidad».
+
+El estado vive en `/etc/takana/focus` (lo escribe root junto con el reglaset). `scripts/test-atuq-foco.py`
+corre **el path de producción, no la escotilla de pruebas** —una escotilla mide el código, no el
+contrato con la imagen— en tres sesiones del navegador, y en cada una comprueba tres cosas: qué dijo
+la extensión, qué pintó, y que el fichero **quedó igual** después de la sesión. Eso último es la
+promesa del §6.5 medida del lado del navegador. Verificado rompiéndolo: con una extensión parcheada
+que pinta «foco» pase lo que pase (dentro de una COPIA del artefacto, vía `ATUQ_DIR`), el guardián
+falla nombrando la insignia.
+
+```
+-- fichero de foco: on → ESTADO on · INSIGNIA "foco" · quedó on
+-- fichero de foco: off → ESTADO off · INSIGNIA "" · quedó off
+-- fichero de foco: (no existe) → ESTADO unknown · INSIGNIA "?" · siguió ausente
+```
+
### 6.6 Medios fuera del navegador — HECHO (2026-09-10)
Una navegación de PRIMER NIVEL a un medio (`Content-Type: video/*` o `audio/*`) se cancela y la URL se
diff --git a/recipes/atuq/README.md b/recipes/atuq/README.md
index ba41c609..bca0ee85 100644
--- a/recipes/atuq/README.md
+++ b/recipes/atuq/README.md
@@ -29,6 +29,7 @@ el vigente (por eso el ciclo es: editar todo → resellar una vez → medir), y
| `extensions/archivo/` | `<appdir>/distribution/extensions/archivo@atuq.tawasuyu.xpi` | **Archivo personal** (SDD 26 §6.3). |
| `extensions/medios/` | `<appdir>/distribution/extensions/medios@atuq.tawasuyu.xpi` | **Medios fuera del navegador** (SDD 26 §6.6). |
| `extensions/torrent/` | `<appdir>/distribution/extensions/torrent@atuq.tawasuyu.xpi` | **Torrent con identidad** (SDD 26 §6.9). |
+| `extensions/foco/` | `<appdir>/distribution/extensions/foco@atuq.tawasuyu.xpi` | **Modo foco: lo muestra, no lo apaga** (SDD 26 §6.5). |
| `native-messaging/*.json` | `/usr/lib/mozilla/native-messaging-hosts/` | El manifiesto del host nativo. **Fuera del appdir** — ver abajo. |
| `bin/puriy-costura-host` | `<appdir>/puriy-costura-host` | El lanzador del host: le pone el `--state` que el manifiesto no puede. |
@@ -240,6 +241,32 @@ sin pestaña es el único resultado inaceptable, y el control negativo del guard
⚠ El binario **no viene del mensaje**: es una constante del host. Lo único que viaja es la URL, sólo
`http(s)`, y detrás de un `--`.
+## `foco` — el navegador MUESTRA el foco, y no tiene con qué apagarlo (§6.5)
+
+El foco no es un bloqueador de sitios: es una regla `nft` de **egress por cgroup** que aplicó root, y
+mientras está puesta el navegador **no sale a la red** — con todo lo local andando (el archivo del
+§6.3 se lee, el CAS del §6.2 está). Medido en una máquina con root: el proceso del cgroup en foco no
+abre la conexión y el del cgroup de al lado sí (`scripts/test-foco-egress.sh`).
+
+Esta extensión es la mitad del navegador, y es **asimétrica a propósito: sólo lee**. Pregunta
+`focus.state` al host cada minuto —el estado lo cambia root por fuera, así que no hay evento al que
+suscribirse— y pinta tres cosas y ninguna más: `foco` si está puesto, nada si no, y **`?` si nadie
+escribió el estado**. Ese tercer caso es el que importa: si «no sé» se redondeara a «apagado», la
+insignia afirmaría que no hay foco sin haberlo mirado.
+
+**No hay verbo para apagarlo, y eso es la propiedad y no un pendiente.** Si desde el navegador se
+pudiera levantar, el modo foco valdría lo que vale un bloqueador de extensión: dos clics. En tawasuyu
+hay un test que lo fija (`focus.stop` → «verbo desconocido»), y acá el guardián mide el efecto:
+después de una sesión entera del navegador, el fichero de estado quedó **igual**.
+
+`scripts/test-atuq-foco.py` corre los tres estados y comprueba, por corrida, qué dijo la extensión,
+qué pintó y que el estado no cambió. Verificado rompiéndolo: con una extensión que pinta «foco» pase
+lo que pase, el guardián falla nombrando la insignia.
+
+Lo que todavía NO existe —y no se inventó de paso— es quién pone a `atuq` en un cgroup y quién aplica
+la política: lo primero pide delegación de un subárbol a la sesión (decisión de arje, SDD 10) y lo
+segundo pide root. Ver el §6.5 del SDD 26.
+
## `torrent` — el magnet lo toma la suite (§6.9)
La extensión registra un handler para `magnet:` (`protocol_handlers`, que existe en nuestro build),
@@ -324,7 +351,6 @@ pestañas están fijadas borra el wrapper y devuelve `null`. `--negative-control
necesita el daemon de embeddings y un backend LLM en la imagen. `willay-rag` es el molde.
- **La IA local en la barra lateral (6.7)**, que está bloqueada por algo medido: el corpus no tiene
ninguna receta de LLM ni de embeddings. Es también la que traería el motor que le falta al archivo.
-- **El foco por `cortafuegos` (6.5)**, el último del §6 que no depende de un modelo ni de un daemon.
- **Que una descarga se pueda COMPARTIR con `tejido`**: hoy el objeto está en el CAS de descargas y
tejido sirve el del sistema. Falta que el índice de descargas sea una raíz que el GC respete, y ahí
las dos raíces pueden volver a ser una.
diff --git a/recipes/atuq/distribution/policies.json b/recipes/atuq/distribution/policies.json
index 631e37f5..f261d356 100644
--- a/recipes/atuq/distribution/policies.json
+++ b/recipes/atuq/distribution/policies.json
@@ -55,6 +55,10 @@
"torrent@atuq.tawasuyu": {
"installation_mode": "normal_installed",
"install_url": "file:///usr/lib/atuq/distribution/extensions/torrent@atuq.tawasuyu.xpi"
+ },
+ "foco@atuq.tawasuyu": {
+ "installation_mode": "normal_installed",
+ "install_url": "file:///usr/lib/atuq/distribution/extensions/foco@atuq.tawasuyu.xpi"
}
}
}
diff --git a/recipes/atuq/extensions/foco/fondo.js b/recipes/atuq/extensions/foco/fondo.js
new file mode 100644
index 00000000..825e84cd
--- /dev/null
+++ b/recipes/atuq/extensions/foco/fondo.js
@@ -0,0 +1,89 @@
+"use strict";
+// foco — el navegador MUESTRA el modo foco; no lo decide y no lo puede apagar (SDD 26 §6.5), v1.
+//
+// ── QUÉ ES EL FOCO, Y POR QUÉ NO ES UN BLOQUEADOR DE SITIOS ───────────────────
+// El foco lo sostiene el cortafuegos del sistema: una regla `nft` de egress por
+// cgroup que aplicó root. Cuando está puesto, el navegador NO SALE A LA RED — y
+// todo lo local sigue: el archivo personal (§6.3) se lee, el CAS (§6.2) está ahí.
+// No es una lista de dominios: una lista se esquiva con un espejo, y un cgroup sin
+// egress no.
+//
+// ── POR QUÉ ESTA EXTENSIÓN SÓLO MIRA ──────────────────────────────────────────
+// Si desde acá se pudiera levantar el foco, valdría exactamente lo que vale un
+// bloqueador de extensión: lo apaga en dos clics la misma persona que no quería
+// distraerse. El host no expone ningún verbo para apagarlo —`focus.stop` contesta
+// «verbo desconocido», y hay un test que lo fija—, así que esta extensión no tiene
+// con qué, y eso es la propiedad. Encenderlo tampoco: aplicar un reglaset pide root.
+//
+// ── Y POR QUÉ «NO SÉ» ES UNA RESPUESTA ────────────────────────────────────────
+// Si nadie escribió el estado, el host contesta `unknown` y acá se muestra tal cual.
+// Pintar «sin foco» cuando no se miró sería afirmar un hecho que no se tiene.
+
+const HOST = "puriy_costura";
+// Cada cuánto se vuelve a preguntar. El estado lo cambia root por fuera del
+// navegador, así que no hay evento al que suscribirse: se sondea, y despacio.
+const CADA_MS = 60000;
+
+let puerto = null;
+let siguiente = 1;
+
+function di(s) {
+ try { dump("FOCO " + s + "\n"); } catch (e) {}
+}
+
+// La insignia es TODA la interfaz: tres estados, tres textos, y ninguno inventado.
+function pintar(estado, fichero) {
+ const texto = estado === "on" ? "foco" : estado === "unknown" ? "?" : "";
+ const titulo =
+ estado === "on"
+ ? "Modo foco: el sistema tiene al navegador sin salida a la red"
+ : estado === "off"
+ ? "Modo foco apagado"
+ : "Modo foco: no se sabe — nadie escribió el estado en " + (fichero || "el sistema");
+ try {
+ browser.browserAction.setBadgeText({ text: texto });
+ browser.browserAction.setTitle({ title: titulo });
+ } catch (e) {
+ di("INSIGNIA falló " + e);
+ }
+ di('INSIGNIA texto="' + texto + '"');
+}
+
+function conectar() {
+ try {
+ puerto = browser.runtime.connectNative(HOST);
+ } catch (e) {
+ di("SIN HOST " + e);
+ return;
+ }
+ di("CONECTADO " + HOST);
+ puerto.onMessage.addListener((r) => {
+ if (!r || r.verb !== "focus.state") return;
+ if (!r.ok) {
+ di("ERROR " + (r.error || "sin causa"));
+ return;
+ }
+ di("ESTADO " + r.focus + " fichero=" + (r.focus_file || "?"));
+ pintar(r.focus, r.focus_file);
+ });
+ puerto.onDisconnect.addListener(() => {
+ const e = puerto && puerto.error ? puerto.error.message : "sin causa";
+ di("DESCONECTADO " + e);
+ puerto = null;
+ });
+}
+
+function preguntar() {
+ if (!puerto) conectar();
+ if (!puerto) return;
+ try {
+ puerto.postMessage({ id: siguiente++, verb: "focus.state" });
+ } catch (e) {
+ di("ENVÍO falló " + e);
+ puerto = null;
+ }
+}
+
+di("LISTA v1 muestra el foco del sistema; no lo apaga (no tiene con qué)");
+preguntar();
+setInterval(preguntar, CADA_MS);
diff --git a/recipes/atuq/extensions/foco/manifest.json b/recipes/atuq/extensions/foco/manifest.json
new file mode 100644
index 00000000..be359b72
--- /dev/null
+++ b/recipes/atuq/extensions/foco/manifest.json
@@ -0,0 +1,25 @@
+{
+ "manifest_version": 2,
+ "name": "atuq — modo foco",
+ "description": "Muestra si el sistema tiene al navegador en modo foco. No lo apaga: no puede (SDD 26 §6.5).",
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "foco@atuq.tawasuyu",
+ "strict_min_version": "115.0"
+ }
+ },
+ "permissions": [
+ "nativeMessaging"
+ ],
+ "background": {
+ "scripts": ["fondo.js"]
+ },
+ "browser_action": {
+ "default_title": "Modo foco",
+ "default_icon": "atuq128.png"
+ },
+ "icons": {
+ "128": "atuq128.png"
+ }
+}
diff --git a/recipes/atuq/native-messaging/puriy_costura.json b/recipes/atuq/native-messaging/puriy_costura.json
index 85df956b..8416e7fc 100644
--- a/recipes/atuq/native-messaging/puriy_costura.json
+++ b/recipes/atuq/native-messaging/puriy_costura.json
@@ -8,6 +8,7 @@
"descargas@atuq.tawasuyu",
"archivo@atuq.tawasuyu",
"medios@atuq.tawasuyu",
- "torrent@atuq.tawasuyu"
+ "torrent@atuq.tawasuyu",
+ "foco@atuq.tawasuyu"
]
}
diff --git a/recipes/puriy-costura.toml b/recipes/puriy-costura.toml
index 24319a6e..70826361 100644
--- a/recipes/puriy-costura.toml
+++ b/recipes/puriy-costura.toml
@@ -43,7 +43,7 @@ license = "MIT OR Apache-2.0"
[source]
repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git"
-commit = "066b3761f7a7a18415b0a59cb671355953bb48a1"
+commit = "38815b5f3625d7d61dd4005195e3f822d11fc380"
# ⚠ SIN ESTO EL BUILD MUERE EN UN SITIO QUE NO NOMBRA LA CAUSA — medido acá el 2026-09-10.
# tawasuyu COMMITEA su propio `vendor/`: adentro tiene un `smithay` parcheado a mano (tearing/flip
# async para el compositor), y lo enchufa con `[patch.crates-io] smithay = { path = "vendor/smithay" }`.
diff --git a/scripts/test-atuq-foco.py b/scripts/test-atuq-foco.py
new file mode 100644
index 00000000..10ff07b8
--- /dev/null
+++ b/scripts/test-atuq-foco.py
@@ -0,0 +1,162 @@
+#!/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())