gioser

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…

TOML 188Rust 55Shell 30Markdown 281211 Definiciones

atuq: el camino de native messaging EXISTE — sonda, y tres cosas que no eran obvias

Todo el §6 del SDD 26 —sct, descargas al CAS, archivo con RAG, torrent— pasa por
un solo mecanismo: un proceso Rust hablando native messaging con la extensión.
Antes de escribir ese crate en tawasuyu conviene saber si el camino existe en
NUESTRO build, que es propio, rebrandeado y con MOZ_REQUIRE_SIGNING vacío.

Existe. Una extensión de diez líneas y un host de tres:

    positivo  MENSAJE {"ok":true}
    control   DESCONECTADO No such native application puente_atuq

El control no sólo falla: NOMBRA la causa, que es lo que confirma que la ruta del
manifiesto es la que se probó.

TRES COSAS MEDIDAS QUE NO ERAN OBVIAS:

1. El manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/`, NO en el
   appdir. Gecko lo busca por `XRESysNativeManifests`, que en Linux sale de un
   `/usr/lib/mozilla` compilado, y el rebranding a atuq no lo mueve.

2. Un host que escribe y SALE pierde el mensaje. La primera sonda hacía printf y
   terminaba: el puerto llegaba a onDisconnect «sin error y sin mensaje», o sea
   el peor informe posible — parece que el camino no existe. Con un sleep detrás
   del printf, el mensaje aparece. El host de verdad es un proceso largo, así que
   en producción no se nota; en una prueba, sí.

3. `ExtensionSettings.install_url` con `file://` NO instala nada, y sin una línea
   de log. Probado con normal_installed y con force_installed, y con el XPI dentro
   y fuera del appdir: ninguna instala. Lo que instala las extensiones de atuq es
   el ESCANEO de `distribution/extensions/`. La política sirve para fijarlas y
   configurarlas; leerla como «esto es lo que las instala» es un error fácil,
   porque los dos mecanismos apuntan a los mismos ficheros y se tapan uno al otro.

`sendNativeMessage` no sirve para diagnosticar: devuelve «An unexpected error
occurred» para todo. El error del puerto de `connectNative` es el único que
nombra la causa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs

c13be792abd617f328dbf738e6bb481bf6639e94 · por Sergio · (hace 1 mes) · padre 8a0496dbb · ver el árbol en este commit

Delta semántico · 3

scripts/test-atuq-nativo.py

1 archivos · +202 −0

diff --git a/scripts/test-atuq-nativo.py b/scripts/test-atuq-nativo.py
new file mode 100644
index 00000000..ead25aab
--- /dev/null
+++ b/scripts/test-atuq-nativo.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+"""¿Puede una extensión de `atuq` hablar con un proceso NATIVO? (SDD 26 §7)
+
+    python3 scripts/test-atuq-nativo.py                     # sale 0 si el mensaje va y vuelve
+    python3 scripts/test-atuq-nativo.py --negative-control  # sin manifiesto: exige que NO vuelva
+
+Todo el §6 del SDD 26 —`sct`, las descargas al CAS, el archivo con RAG, el torrent— pasa por UN solo
+mecanismo: un proceso Rust que habla *native messaging* con la extensión. Antes de escribir ese
+crate en tawasuyu conviene saber si el camino EXISTE en nuestro build, porque es un build propio,
+rebrandeado y con `MOZ_REQUIRE_SIGNING` vacío: nada garantiza que Gecko encuentre el manifiesto ni
+que pueda lanzar el proceso dentro de la jaula.
+
+Esta prueba no escribe el host de verdad. Pone una **sonda**: una extensión de diez líneas con
+permiso `nativeMessaging` y un host de tres que contesta `{"ok":true}`. Si el mensaje vuelve, el
+contrato está y el crate se puede escribir contra algo comprobado.
+
+── LO QUE MIDIÓ, Y QUE NO ES OBVIO (2026-09-07) ───────────────────────────────────────────────
+
+1. **El manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/`, NO en el appdir.** Gecko lo
+   busca por `XRESysNativeManifests`, que en Linux sale de un `/usr/lib/mozilla` COMPILADO, y el
+   rebranding a `atuq` no lo mueve: la ruta no depende del nombre de la aplicación. Está en las
+   cadenas de `libxul.so` y se comprueba corriendo esto.
+
+2. **Un host que escribe y sale PIERDE el mensaje.** La primera sonda hacía `printf` de la respuesta
+   y terminaba; el puerto llegaba a `onDisconnect` **sin error y sin mensaje**, que es el peor
+   informe posible: parece que el camino no existe. Con un `sleep` detrás del `printf` el mensaje
+   aparece. El host de tawasuyu es un proceso LARGO —vive mientras el navegador lo tenga abierto—,
+   así que en producción no se nota; en una prueba, sí.
+
+3. **`ExtensionSettings.install_url` con `file://` NO instala nada, y sin una línea de log.** Se
+   probó con `normal_installed` y con `force_installed`, y con el XPI dentro y fuera del appdir:
+   ninguna instala. Lo que instala las extensiones de atuq es el ESCANEO de
+   `distribution/extensions/`. La política sirve para fijarlas y configurarlas —eso ya lo prueba
+   `test-atuq-politica.py`—, pero leerla como «esto es lo que las instala» es un error fácil,
+   porque los dos mecanismos apuntan a los mismos ficheros y se tapan uno al otro.
+
+REQUISITOS: el rootfs de `scripts/atuq-nested.sh` (que lo hidrata) y `atuq` sellado.
+"""
+import json
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import zipfile
+
+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/hammer")
+STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store"))
+
+EXT_ID = "nativa@atuq.tawasuyu"
+HOST = "puente_atuq"
+
+MANIFIESTO = {
+    "manifest_version": 2,
+    "name": "sonda-nativa",
+    "version": "1.0",
+    "browser_specific_settings": {"gecko": {"id": EXT_ID}},
+    "permissions": ["nativeMessaging"],
+    "background": {"scripts": ["fondo.js"]},
+}
+
+FONDO = """
+function di(s) { try { dump("NATIVA-TEST " + s + "\\n"); } catch (e) {} }
+di("arranca la sonda");
+try {
+  const p = browser.runtime.connectNative("%s");
+  p.onMessage.addListener(m => di("MENSAJE " + JSON.stringify(m)));
+  // El error del puerto es lo ÚNICO que nombra la causa: sin manifiesto dice «No such native
+  // application», y si el host muere raro lo dice ahí. `sendNativeMessage` sólo devuelve un
+  // «An unexpected error occurred» que no distingue nada.
+  p.onDisconnect.addListener(pp => di("DESCONECTADO " + (pp.error ? pp.error.message : "sin error")));
+  p.postMessage({hola: 1});
+} catch (e) {
+  di("EXCEPCIÓN " + e.message);
+}
+""" % HOST
+
+# `\\013` = 11, el largo de `{"ok":true}`, en 4 bytes little-endian. Y el `sleep`: ver el punto 2 de
+# la cabecera — sin él Gecko cierra el puerto sin entregar lo que el host ya había escrito.
+SONDA_HOST = """#!/bin/sh
+echo "host: arrancó" >> /salida/host.log
+printf '\\013\\000\\000\\000{"ok":true}'
+echo "host: respondió" >> /salida/host.log
+sleep 8
+"""
+
+
+def fatal(msg):
+    print(f"✗ {msg}", file=sys.stderr)
+    raise SystemExit(1)
+
+
+def artefacto(receta, nombre):
+    """El artefacto VIGENTE, por `hammer hash` y nunca por glob: con dos en el store, `ls | head`
+    es una ruleta y se puede estar probando el de ayer."""
+    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")
+    return d
+
+
+def main():
+    negativo = "--negative-control" in sys.argv[1:]
+    if negativo:
+        print("== MODO CONTROL NEGATIVO: sin manifiesto, se exige que el mensaje NO vuelva")
+    if not os.path.isdir(RFS):
+        fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`, que lo hidrata")
+    atuq = artefacto("atuq", "atuq")
+    print(f"artefacto: {os.path.basename(atuq)}")
+
+    tmp = tempfile.mkdtemp(prefix="atuq-nativo-")
+    salida = os.path.join(tmp, "salida")
+    # La extensión se instala por el ESCANEO de `distribution/extensions/` (ver punto 3 de la
+    # cabecera), así que la sonda entra como una capa de overlay sobre el appdir del artefacto.
+    extdir = os.path.join(tmp, "capa", "usr", "lib", "atuq", "distribution", "extensions")
+    manifdir = os.path.join(tmp, "capa", "usr", "lib", "mozilla", "native-messaging-hosts")
+    os.makedirs(os.path.join(salida, "perfil"))
+    os.makedirs(extdir)
+    os.makedirs(manifdir)
+    try:
+        with zipfile.ZipFile(os.path.join(extdir, f"{EXT_ID}.xpi"), "w") as z:
+            z.writestr("manifest.json", json.dumps(MANIFIESTO, indent=1))
+            z.writestr("fondo.js", FONDO)
+
+        with open(os.path.join(salida, "host.sh"), "w") as fh:
+            fh.write(SONDA_HOST)
+        os.chmod(os.path.join(salida, "host.sh"), 0o755)
+
+        if not negativo:
+            json.dump({"name": HOST, "description": "sonda de native messaging",
+                       "path": "/salida/host.sh", "type": "stdio",
+                       "allowed_extensions": [EXT_ID]},
+                      open(os.path.join(manifdir, f"{HOST}.json"), "w"), indent=1)
+
+        with open(os.path.join(salida, "perfil", "user.js"), "w") as fh:
+            # `dump()` es el canal por el que la sonda habla: va al stdout del proceso, sin red.
+            fh.write('user_pref("browser.dom.window.dump.enabled", true);\n')
+            fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n')
+
+        guion = """
+        mkdir -p /root
+        [ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
+        timeout 50 /usr/bin/atuq --profile /salida/perfil --no-remote --headless about:blank \
+            > /salida/consola.log 2>&1
+        exit 0
+        """
+        subprocess.run([
+            "bwrap",
+            "--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq,
+            "--overlay-src", os.path.join(tmp, "capa"), "--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", "HOME", "/root",
+            "--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1",
+            # Dentro de bwrap no se puede crear otro user namespace (`uid_map: EPERM`) ⇒ el sandbox
+            # de contenido de Gecko no arranca y cada proceso hijo muere en bucle. Acá se mide el
+            # camino nativo, no el sandbox del motor.
+            "--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1",
+            "--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1",
+            "sh", "-c", guion,
+        ], check=False)
+
+        log = open(os.path.join(salida, "consola.log"), errors="replace").read()
+        for linea in [l for l in log.splitlines() if l.startswith("NATIVA-TEST")]:
+            print("   " + linea)
+        arrancó = "NATIVA-TEST arranca la sonda" in log
+        volvió = 'NATIVA-TEST MENSAJE {"ok":true}' in log
+        corrió = os.path.exists(os.path.join(salida, "host.log"))
+
+        if not arrancó:
+            shutil.copy(os.path.join(salida, "consola.log"),
+                        os.path.join(ROOT, "work", "atuq-nativo.log"))
+            fatal("la sonda ni siquiera arrancó — ¿no se instaló el XPI? log en work/atuq-nativo.log")
+
+        if negativo:
+            if volvió:
+                fatal("SIN manifiesto el mensaje volvió igual — esta prueba no distingue nada")
+            print("✓ control negativo: sin manifiesto no hay mensaje (la prueba SÍ lo vería)")
+            return 0
+        if not volvió:
+            shutil.copy(os.path.join(salida, "consola.log"),
+                        os.path.join(ROOT, "work", "atuq-nativo.log"))
+            fatal(f"el mensaje no volvió (el host {'sí' if corrió else 'NO'} llegó a correr)"
+                  " — log en work/atuq-nativo.log")
+        print("✓ una extensión de atuq habló con un proceso nativo y le volvió la respuesta")
+        return 0
+    finally:
+        shutil.rmtree(tmp, ignore_errors=True)
+
+
+sys.exit(main())