atuq: el diálogo de la bóveda no abre, y el muro no es de la bóveda — ninguna ventana llimphi pinta
`boveda` (b3:59ffd74b) y `shuma-pregunta` (b3:99763eca) sellaron en el worker. Ninguno de los dos abre
ventana, y el error no habla de contraseñas:
panicked at llimphi-hal/src/lib.rs:1032: index out of bounds: the len is 0 but the index is 0
Esa línea es `caps.formats[0]`: la surface no tiene NI UN formato. La cadena, medida eslabón por
eslabón con RUST_LOG=wgpu_hal=debug:
No (or unknown) windowing system ((None, Some(..))) present. Using surfaceless platform
Trying native-render → No config found! · Trying presentation → No config found!
El `None` es `raw_display_handle`, y no es del arnés: es lo que llimphi hace A PROPÓSITO en su camino
de escritorio, con el comentario al lado («Sin display: este camino no tiene ventana todavía»). Sin
display handle wgpu abre el display EGL surfaceless, que no publica configs con WINDOW_BIT ⇒ la
surface no es presentable ⇒ cero formatos ⇒ panic.
No se nota en una máquina con Vulkan, porque ese camino elige Backends::PRIMARY y a Vulkan el display
no le hace falta. Y **ninguna imagen de takana tiene Vulkan**: las tres recetas de mesa se compilan
con `-Dvulkan-drivers=` vacío, y `vulkan-loader` sólo existe en incoming-kde sin que ningún perfil lo
declare (y un loader sin driver no es un driver). ⇒ toda app llimphi de escritorio cae al camino GL y
no puede abrir ventana. Reproducido en TRES binarios y dos versiones de wgpu: shuma-pregunta y boveda
(29) y llimphi-counter (27).
Los controles, porque la conclusión es fuerte: pedir Vulkan explícito da NoAdapter y no hay ICD ni
libvulkan en ninguna capa (la premisa está medida); con softpipe el fallo es OTRO y anterior —wgpu
pide compute shaders y softpipe se queda en GL 3.3—, así que llegar hasta acá ya exige llvmpipe; y el
compositor está levantado con su socket y su WAYLAND_DISPLAY.
Lo que esto dice del producto: el consentimiento de la bóveda no puede pedir permiso en ninguna imagen
de hoy, y como un lanzamiento fallido se traduce a «no» (Command::new falla ⇒ return false), el usuario
vería una bóveda que niega todo sin un solo error.
Se arregla en llimphi (que pase el display handle; la función `instancia_con` ya existe ahí al lado) o
con un driver Vulkan por software en el corpus. No se arregla en takana ni en el arnés.
Entra igual lo que sí se puede afirmar:
· `scripts/test-atuq-boveda-metal.py`, seis etapas declaradas, la A en VERDE — sin la app dueña el
host contesta locked:true, que es la medición del §7.sexies ahora vigilada. Cada etapa dice qué
afirma y la B nombra el muro en vez de dejar que se rediagnostique;
· `recipes/wtype.toml` — el corpus no tenía NINGUNA forma de meter una tecla en un compositor: todos
los arneses de scripts/wlr miran, ninguno toca. Sellado y en ningún perfil: es instrumento.
⚠ Y una trampa del arnés, mía, que costó tres corridas: exportaba GALLIUM_DRIVER=softpipe, o sea que
yo mismo lo clavaba al driver débil. mesa-swrast y mesa-llvmpipe se ven como dos nombres de lo mismo
—«mesa por software»— y la diferencia decide si el arnés existe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3050,6 +3050,89 @@ dos y recién ahí escribirlo. Lo que sí queda escrito desde hoy es la pregunta
|
||||
tiene que empezar —`vault.status` contra el host— porque es la que separa «la bóveda dijo que no» de
|
||||
«no hay bóveda», que se ven idénticas desde la extensión.
|
||||
|
||||
#### 7.septies El diálogo NO ABRE, y el muro no es de la bóveda: **ninguna app llimphi de escritorio puede pintar en esta distro** (2026-09-18)
|
||||
|
||||
Con `boveda` y `shuma-pregunta` sellados, la etapa B del guardián de metal falla, y falla en un sitio
|
||||
que no tiene nada que ver con las contraseñas:
|
||||
|
||||
```
|
||||
thread 'main' panicked at 02_ruway/llimphi/llimphi-hal/src/lib.rs:1032:83:
|
||||
index out of bounds: the len is 0 but the index is 0
|
||||
```
|
||||
|
||||
Esa línea es `caps.formats[0]` sobre las capacidades de la surface. O sea: **la surface no tiene NI UN
|
||||
formato**. La cadena entera, medida eslabón por eslabón con `RUST_LOG=wgpu_hal=debug`:
|
||||
|
||||
```
|
||||
wgpu_hal::gles::egl: No (or unknown) windowing system ((None, Some(...))) present.
|
||||
Using surfaceless platform
|
||||
wgpu_hal::gles::egl: Trying native-render → No config found!
|
||||
wgpu_hal::gles::egl: Trying presentation → No config found!
|
||||
wgpu_hal::gles::egl: Trying off-screen → (ésta sí)
|
||||
```
|
||||
|
||||
El `None` de ese par es `desc.raw_display_handle`, y **no es un accidente del arnés: es lo que el
|
||||
propio llimphi hace a propósito**, con su comentario al lado (`llimphi-hal/src/lib.rs`, camino de
|
||||
escritorio `new_inner`):
|
||||
|
||||
> «Sin display: este camino no tiene ventana todavía (la surface se crea después, contra esta misma
|
||||
> instancia). Los caminos que SÍ la tienen —`new_for_raw_surface`, `recreate_for_window`— la pasan.»
|
||||
|
||||
Sin display handle, wgpu abre el display EGL por la plataforma **surfaceless**, que por definición no
|
||||
publica configs con `WINDOW_BIT`; sin config de ventana, `presentable` es falso; sin `presentable`,
|
||||
`surface_capabilities` devuelve `None` y la lista de formatos sale vacía. Cada paso está en el log.
|
||||
|
||||
**Por qué esto no se nota en la máquina de quien escribió llimphi, y sí acá.** Ese camino elige
|
||||
`Backends::PRIMARY` —Vulkan/Metal/DX12— y **a Vulkan el display handle no le hace falta**. Sólo cae al
|
||||
backend GL cuando no hay ninguno de los tres… que es exactamente el caso de esta distro:
|
||||
|
||||
| | Vulkan | efecto |
|
||||
|---|---|---|
|
||||
| `recipes/mesa.toml` (iris, la de las imágenes) | `-Dvulkan-drivers=` **vacío** | no hay ICD |
|
||||
| `recipes/mesa-swrast.toml` · `recipes/mesa-llvmpipe.toml` | `-Dvulkan-drivers=` **vacío** | no hay ICD |
|
||||
| `vulkan-loader` | sólo en `incoming-kde`, y **ningún perfil lo declara** | y el loader sin driver no es un driver |
|
||||
|
||||
⇒ **En ninguna imagen de takana hay un driver Vulkan.** Con lo cual toda app llimphi de escritorio
|
||||
—no sólo el diálogo de la bóveda— toma el camino GL, y el camino GL sin display handle no puede abrir
|
||||
ventana. Comprobado en TRES binarios distintos y con dos versiones de wgpu: `shuma-pregunta` (wgpu 29),
|
||||
`boveda` (wgpu 29) y `llimphi-counter` (wgpu 27, pineado a otro repo).
|
||||
|
||||
**Los controles, porque la conclusión es fuerte:**
|
||||
|
||||
· pedir Vulkan explícito (`LLIMPHI_WGPU_BACKEND=vulkan`) ⇒ **`NoAdapter`**, y `ls /usr/share/vulkan/icd.d`
|
||||
y `ls /usr/lib/libvulkan*` no existen en ninguna capa. La premisa «no hay Vulkan» está medida, no
|
||||
supuesta;
|
||||
· con **softpipe** el fallo es OTRO y anterior —`RequestDevice("Parent device is lost")`, con
|
||||
`indirect-validation error: ComputePipeline(… COMPUTE_SHADER …)` una línea antes, porque softpipe se
|
||||
queda en GL 3.3 y wgpu pide compute—. O sea que llegar hasta el panic de los formatos ya exige
|
||||
`llvmpipe` (GL 4.5), que es lo que el arnés monta: `mesa-llvmpipe` + `llvm18`;
|
||||
· y el mismo arnés levanta `sway`, el socket wayland aparece y `WAYLAND_DISPLAY` está puesto — el
|
||||
compositor no es el que falta.
|
||||
|
||||
⚠ **Lo que esto dice del producto, y es lo que importa:** la bóveda de `atuq` no está «casi lista». Su
|
||||
consentimiento —`PorDialogo`, que lanza `shuma-pregunta`— **no puede pedir permiso en ninguna imagen
|
||||
de hoy**, y como el fallo del lanzamiento se traduce a «no» (`Command::new` falla ⇒ `return false`),
|
||||
lo que un usuario vería es una bóveda que niega todo, sin un error. Y no es sólo la bóveda: es
|
||||
cualquier ventana llimphi que la distro quiera abrir.
|
||||
|
||||
**Dónde se arregla, y dónde NO.** No se arregla en takana: ni en la receta, ni en el arnés, ni
|
||||
poniendo otro mesa. Las dos salidas son de otro repo o de otra receta, y conviene decir cuál es cuál:
|
||||
|
||||
1. **llimphi** (tawasuyu): que el camino de escritorio pase el display handle cuando lo tiene, o que
|
||||
al caer al backend GL reconstruya la instancia con él. La función que hace falta **ya existe ahí
|
||||
al lado** (`instancia_con`, que usan `new_for_raw_surface` y `recreate_for_window`);
|
||||
2. **un driver Vulkan por software** en el corpus (lavapipe: `-Dvulkan-drivers=swrast`), que además
|
||||
destrabaría el muro de ScreenCast que el runbook de COSMIC documenta por otro camino. Es una
|
||||
receta nueva y una decisión de tamaño, no un arreglo de paso.
|
||||
|
||||
La 1 es la correcta: un navegador que pide permiso no debería depender de que la máquina tenga
|
||||
Vulkan. La 2 es la que, además, le sirve a otros frentes.
|
||||
|
||||
**Lo que queda escrito y corriendo mientras tanto** es `scripts/test-atuq-boveda-metal.py`, con sus
|
||||
seis etapas declaradas y la A en verde: sin la app dueña, el host contesta `locked:true`. Es poco, y
|
||||
es exactamente lo que se puede afirmar hoy — que es mejor que un guardián que no existe y que uno que
|
||||
diera verde midiendo nada.
|
||||
|
||||
## 8. Plan, por unidades de trabajo
|
||||
|
||||
Cada una cierra sola, se commitea y se pushea. El orden no es preferencia: cada una destraba a la
|
||||
@@ -3077,7 +3160,7 @@ siguiente.
|
||||
| 9 | **Medios (6.6) ✅ y torrent (6.9) ✅ — 2026-09-10.** El medio lo abre `mpv`; el torrent lo toma `puriy-costura-torrent`, un daemon **propio, configurable y perezoso** que sobrevive al navegador y cosecha al CAS. Medido antes de decidir: la pila de librqbit compila para musl (226 crates), así que la decisión fue «no ahí» y no «no se puede». **Faltan** el foco (6.5) y la IA local (6.7), ésta bloqueada porque el corpus no tiene modelo ni embeddings | 6.5–6.7, 6.9 | 5 ✅ |
|
||||
| 10 | **Foco (6.5) — 2026-09-10.** Entra al corpus la cadena `nftables` (libmnl + libnftnl + nft 1.1.6), que el grafo pedía y nadie había puesto, con dos arreglos de fábrica: el sello de tiempo que rompía la reproducción y un bashismo. Medido con root: el cgroup en foco no sale y el de al lado sí. Y del lado del navegador, la extensión `foco` MUESTRA el estado y **no tiene verbo para apagarlo**. **Falta** quién pone a `atuq` en un cgroup (delegación, decisión de arje) y quién aplica la política (root) | 6.5 | 6 ✅ |
|
||||
| 11 | **Motor de inferencia local (6.7) — 2026-09-11.** Entra `recipes/llama-cpp.toml` (b10901, estática, 199 M, **REPRODUCE**), que resulta ser **un solo muro para dos pendientes**: el §6.7 y la mitad semántica del §6.3 no eran dos problemas, era que el corpus no tenía con qué correr un modelo (`pluma-llm` sólo tiene backends de nube; `rimay-verbo-fastembed` DESCARGA onnxruntime glibc + el modelo). ⚠ Y dejó medido lo que no se podía deducir: **`SOURCE_DATE_EPOCH` —la variable que nos da reproducibilidad— apagaba las SEIS perillas de ISA de ggml**, y el artefacto sellaba y corría con 0 `%ymm`. Ahora van declaradas y el `install` las comprueba. Guardián `scripts/test-llama-cpp.py` con control negativo vivo. **Faltan** el modelo (fuente pineada, no receta), los verbos del host y quién levanta el servidor | 6.7, y la mitad semántica de 6.3 | 5 ✅ |
|
||||
| 12 | **La bóveda (SDD-BOVEDA §8) — a medias, 2026-09-15.** La décima extensión: ofrece la credencial del sitio abierto y no puede sacar una contraseña por su cuenta —`vault.match` contesta títulos y usuarios; la contraseña sale por `vault.fill`, que **pregunta en el escritorio**—. La dirección la pone el chrome, nunca la página. El gestor de Gecko se aparta como VALOR DE ARRANQUE y no como política. **Destrabada el 2026-09-16** (§7.quinquies.bis): el `Cargo.lock` de tawasuyu publicado —y resultó ser byte a byte el que la otra sesión ya tenía sin commitear—, el pin subido a `23a292863` ⇒ `b3:7d63655a`, construido en el worker (el vendoreo son 2,4 G y el hub estaba al 99%) y medido con el mismo `strings` que había diagnosticado el hueco: `vault` de **0 a 10**, con `cas`/`sct` de control. **Falta**, y no era lo que este renglón decía (§7.sexies, 2026-09-18): el host sellado contesta `vault.status → locked:true` porque **el dueño de la bóveda y el diálogo de consentimiento no estaban en el corpus** — la función está apagada de fábrica en las cuatro imágenes. Entran `recipes/boveda.toml` y `recipes/shuma-pregunta.toml`; después de eso, el guardián de METAL | el gestor de contraseñas de la suite dentro del navegador | 5 ✅ |
|
||||
| 12 | **La bóveda (SDD-BOVEDA §8) — a medias, 2026-09-15.** La décima extensión: ofrece la credencial del sitio abierto y no puede sacar una contraseña por su cuenta —`vault.match` contesta títulos y usuarios; la contraseña sale por `vault.fill`, que **pregunta en el escritorio**—. La dirección la pone el chrome, nunca la página. El gestor de Gecko se aparta como VALOR DE ARRANQUE y no como política. **Destrabada el 2026-09-16** (§7.quinquies.bis): el `Cargo.lock` de tawasuyu publicado —y resultó ser byte a byte el que la otra sesión ya tenía sin commitear—, el pin subido a `23a292863` ⇒ `b3:7d63655a`, construido en el worker (el vendoreo son 2,4 G y el hub estaba al 99%) y medido con el mismo `strings` que había diagnosticado el hueco: `vault` de **0 a 10**, con `cas`/`sct` de control. **Falta**, y no era lo que este renglón decía (§7.sexies, 2026-09-18): el host sellado contesta `vault.status → locked:true` porque **el dueño de la bóveda y el diálogo de consentimiento no estaban en el corpus** — la función está apagada de fábrica en las cuatro imágenes. Entran `recipes/boveda.toml` y `recipes/shuma-pregunta.toml`, **las dos SELLADAS el 2026-09-18** (`b3:59ffd74b` y `b3:99763eca`, construidas en el worker). ⚠ Y con eso apareció el muro de verdad, que no es de la bóveda: **ninguna app llimphi de escritorio abre ventana en esta distro** porque no hay Vulkan en ninguna imagen y el camino GL de llimphi arma la instancia sin display handle (§7.septies). El guardián de metal existe —`scripts/test-atuq-boveda-metal.py`, seis etapas— y hoy llega hasta la A. La B se destraba en llimphi, no acá | el gestor de contraseñas de la suite dentro del navegador | 5 ✅ |
|
||||
| 9.a | **Proxy por contenedor ✅ v0.5** — contenedores por política + extensión con `proxy.onRequest` | 6.8, y NO dependía de 5: es API de Firefox | — |
|
||||
|
||||
Las unidades 2 y 4 son **paralelizables**: la toolchain no toca el chrome y el chrome no toca la
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# wtype 0.4 — teclear en Wayland desde un script: `wtype "hola"`, `wtype -k Return`. Es xdotool para
|
||||
# Wayland, por el protocolo `zwp_virtual_keyboard_v1` (que el XML vendorea en `protocol/`).
|
||||
#
|
||||
# ── POR QUÉ ENTRA (2026-09-18): sin esto no se puede CONTESTAR un diálogo ───────────────────────
|
||||
# El corpus no tiene **ninguna** forma de meter una tecla en un compositor: ni `wtype`, ni `ydotool`,
|
||||
# ni nada que hable el protocolo de teclado virtual (comprobado sobre `recipes/` y sobre
|
||||
# `scripts/`). Mientras lo único que había que medir era «¿la ventana pinta?», daba igual: las
|
||||
# capturas y el log del protocolo contestan eso sin tocar nada. La bóveda de `atuq` rompe esa
|
||||
# comodidad, porque su promesa **es** una pregunta: `vault.fill` no entrega la contraseña hasta que
|
||||
# una persona diga que sí en `shuma-pregunta` (SDD 26 §7.sexies). Un guardián que no puede decir que
|
||||
# sí sólo puede medir la mitad de la función — que la puerta existe—, y nunca que la puerta se abre.
|
||||
#
|
||||
# No es sólo para la bóveda: es la pieza que le faltaba a **cualquier** arnés de escritorio de este
|
||||
# repo. Todos los de `scripts/wlr/` miran; ninguno toca.
|
||||
#
|
||||
# ⚠ Y NO va declarado en ningún perfil, a propósito: es un INSTRUMENTO, no parte del producto. Mismo
|
||||
# criterio que `llama-cpp` cuando entró sin imagen que la declarara — una imagen no crece por algo
|
||||
# que sólo usa el que mide. Los arneses lo montan como capa overlay desde el store.
|
||||
#
|
||||
# ── LA REPRODUCIBILIDAD, QUE ACÁ TIENE UNA TRAMPA A LA VISTA ───────────────────────────────────
|
||||
# `meson.build` arma la cadena de versión con `__DATE__` cuando encuentra `git`. Un macro de fecha
|
||||
# de compilación es exactamente lo que rompe un binario reproducible… salvo que el compilador
|
||||
# respete `SOURCE_DATE_EPOCH`, que es lo que hacen gcc y clang (y por lo tanto `zig cc`) desde hace
|
||||
# años: con la variable puesta —y takana la pone siempre— `__DATE__` deja de ser el reloj de pared.
|
||||
# Si algún día esta receta apareciera en `why-differs`, éste es el primer sospechoso y está escrito
|
||||
# acá para no volver a buscarlo.
|
||||
#
|
||||
# ⚠ El tag `v0.4` es ANOTADO: `git ls-remote --tags` devuelve DOS shas y el bueno es el `^{}`
|
||||
# (d71be3a7…), que es el commit. Pinear el sha del objeto tag valida y después no encuentra el árbol.
|
||||
name = "wtype"
|
||||
version = "0.4"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
repo = "https://github.com/atx/wtype.git"
|
||||
commit = "d71be3a7b3f93b534a2823fd68cabd7ac2a02359" # v0.4 PELADO (^{commit})
|
||||
|
||||
[build]
|
||||
compiler = "zig-cc"
|
||||
target = "x86_64-linux-musl"
|
||||
link = "dynamic"
|
||||
zig_version = "0.13.0"
|
||||
|
||||
[build.phases]
|
||||
configure = "meson setup output --prefix=/usr --buildtype=release --wrap-mode=nodownload"
|
||||
compile = "ninja -C output"
|
||||
install = '''
|
||||
set -e
|
||||
DESTDIR=/out ninja -C output install
|
||||
test -x /out/usr/bin/wtype || { echo "no quedó /out/usr/bin/wtype — ¿cambió el layout de meson?" >&2; exit 1; }
|
||||
'''
|
||||
|
||||
[deps]
|
||||
# `wayland-cursor` y `wayland-client` salen los dos del paquete `wayland`; `xkbcommon` es
|
||||
# `libxkbcommon`. `rt` lo resuelve musl dentro de la libc y no hace falta declarar nada.
|
||||
build = ["meson", "samurai", "python3", "pkgconf", "wayland", "libxkbcommon", "expat", "libffi", "zlib"]
|
||||
run = ["wayland", "libxkbcommon"]
|
||||
@@ -0,0 +1,598 @@
|
||||
#!/usr/bin/env python3
|
||||
"""¿La bóveda de `atuq` entrega una contraseña, y sólo si una persona dice que sí? (SDD 26 §7.sexies)
|
||||
|
||||
python3 scripts/test-atuq-boveda-metal.py # las seis etapas, en orden
|
||||
python3 scripts/test-atuq-boveda-metal.py --hasta C # sólo hasta la etapa C (el diálogo)
|
||||
python3 scripts/test-atuq-boveda-metal.py --list # qué afirma cada etapa, sin correr nada
|
||||
|
||||
── POR QUÉ ESTE GUARDIÁN, Y POR QUÉ NO ALCANZA CON EL DE COHERENCIA ──────────────────────────
|
||||
`test-atuq-boveda-coherente.py` mira los CINCO lugares donde una extensión se enchufa y tarda un
|
||||
segundo. Es lo que hay que correr primero y no reemplaza a nada. Lo que no puede contestar es si del
|
||||
otro lado del cable hay alguien: el 2026-09-18 los cinco lugares estaban en orden —manifiesto,
|
||||
política, permiso, prefs y el commit del host con sus verbos— y la bóveda contestaba **cerrada** en
|
||||
las cuatro imágenes, porque el DUEÑO de la base y el DIÁLOGO de consentimiento no estaban en el
|
||||
corpus. Ningún guardián de ficheros podía verlo; éste empieza justamente por ahí.
|
||||
|
||||
── LAS SEIS ETAPAS, Y QUÉ AFIRMA CADA UNA ────────────────────────────────────────────────────
|
||||
A sin la app `boveda`, el host contesta `locked:true` ← el control, y es el bug del §7.sexies
|
||||
B con la app corriendo, `vault.status` dice ABIERTA ← el dueño existe y atiende el socket
|
||||
C `vault.save` abre el DIÁLOGO y se lo puede contestar ← el consentimiento es real y se ve
|
||||
D el navegador, sobre una página servida por HTTP, encuentra la credencial (`vault.match`)
|
||||
E `vault.fill` contestado que SÍ ⇒ la contraseña llega al campo del formulario
|
||||
F `vault.fill` contestado que NO ⇒ `denied`, y el campo queda VACÍO ← el control de E
|
||||
|
||||
La etapa A no es decoración: **«cerrada» es una respuesta exitosa** (`ok:true`), así que sin ese
|
||||
control un guardián que midiera sólo B no distinguiría «la bóveda abrió» de «nadie preguntó bien».
|
||||
Y la F es la que hace que la E signifique algo: una bóveda que entrega siempre se ve idéntica a una
|
||||
que entrega con permiso, y la diferencia es la única razón por la que esto existe.
|
||||
|
||||
── LO QUE HIZO FALTA PARA PODER CONTESTAR UNA PREGUNTA ───────────────────────────────────────
|
||||
El corpus no tenía **ninguna** forma de meter una tecla en un compositor. Todos los arneses de
|
||||
`scripts/wlr/` miran; ninguno toca. Entra `wtype` (protocolo `zwp_virtual_keyboard_v1`) como
|
||||
INSTRUMENTO —no va en ninguna imagen— y con él se contesta el diálogo igual que lo haría una
|
||||
persona: flechas y `Enter`.
|
||||
|
||||
⚠ **Y el render es por SOFTWARE a propósito, con `llvmpipe` y no con `softpipe`.** `shuma-pregunta`
|
||||
y `boveda` son llimphi/wgpu, o sea clientes de GL; el `mesa` de las imágenes es **iris-only** (Intel)
|
||||
y esta máquina tiene una virtio-gpu, así que EGL no encontraría driver. Se montan `mesa-llvmpipe` +
|
||||
`llvm18` como últimas capas. Con `mesa-swrast` (softpipe, GL 3.3) el diálogo **no abre**: wgpu pide
|
||||
compute shaders y muere con `RequestDevice("Parent device is lost")`, un mensaje que no nombra la
|
||||
causa. Es el entorno de prueba, no el artefacto: en la imagen de verdad el driver lo pone el metal.
|
||||
|
||||
⚠⚠ **DÓNDE ESTÁ HOY (2026-09-18): la etapa A pasa y la B choca contra un muro que no es de la
|
||||
bóveda.** `boveda` y `shuma-pregunta` están sellados, y ninguno de los dos abre ventana: llimphi
|
||||
construye la instancia de wgpu **sin display handle** —su propio comentario lo dice: «este camino no
|
||||
tiene ventana todavía»—, wgpu cae entonces a la plataforma EGL **surfaceless**, que por definición no
|
||||
tiene configs de ventana, y `surface.get_capabilities()` devuelve **cero formatos**; llimphi hace
|
||||
`caps.formats[0]` y panica. En una máquina con Vulkan no se nota, porque el backend PRIMARY no
|
||||
necesita el display — y **ninguna imagen de takana tiene Vulkan**: las tres recetas de mesa se
|
||||
compilan con `-Dvulkan-drivers=` vacío. Ver SDD 26 §7.septies. Este guardián queda escrito y
|
||||
CORRIENDO hasta donde se puede: la A en verde es la medición del §7.sexies, vigilada.
|
||||
|
||||
REQUISITOS: el rootfs de sway (`scripts/hydrate-profile.py escritorio-sway --into …`) y, sellados,
|
||||
`atuq`, `puriy-costura`, `boveda`, `shuma-pregunta`, `wtype`, `llvm18` y `mesa-llvmpipe`.
|
||||
"""
|
||||
import argparse
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/sway-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"))
|
||||
|
||||
# La raíz de la bóveda DE PRUEBA. Con esta variable puesta, la app deriva sus claves de esta cadena
|
||||
# en vez de la seed de identidad del llavero del kernel —que en ninguna máquina de desarrollo está
|
||||
# desbloqueada— y abre `…/tawasuyu/boveda-de-prueba`, que es OTRA bóveda, vacía, y lo dice en
|
||||
# pantalla. Sin esto este guardián no se podría encender en ningún lado.
|
||||
RAIZ_DE_PRUEBA = "metal-de-atuq"
|
||||
|
||||
USUARIO = "sergio"
|
||||
CLAVE = "Zx7#qLm2pVnR"
|
||||
TITULO = "Banco de prueba"
|
||||
|
||||
ETAPAS = [
|
||||
("A", "sin la app `boveda`, el host contesta locked:true (el bug del §7.sexies, vigilado)"),
|
||||
("B", "con la app corriendo, vault.status dice ABIERTA: el dueño atiende el socket"),
|
||||
("C", "el DIÁLOGO abre en el compositor y se lo puede CONTESTAR (el instrumento: wtype)"),
|
||||
("D", "vault.save contestado que SÍ guarda, y vault.match la encuentra después"),
|
||||
("E", "el navegador real: vault.fill contestado que SÍ ⇒ la contraseña llega al campo"),
|
||||
("F", "vault.fill contestado que NO ⇒ denied y el campo queda VACÍO (el control de E)"),
|
||||
]
|
||||
|
||||
# El atajo del diálogo: el dígito contesta con esa opción (`atajo_de_digito` en shuma-pregunta),
|
||||
# así que «1» es la primera opción y «2» la segunda. Se teclea con `wtype`, que es la única forma
|
||||
# que tiene este corpus de meter una tecla en un compositor.
|
||||
TECLA_SI = "1"
|
||||
TECLA_NO = "2"
|
||||
|
||||
PAGINA = """<!doctype html><html><head><meta charset="utf-8"><title>Banco de prueba</title></head>
|
||||
<body><h1>Banco de prueba</h1>
|
||||
<form id="f" method="POST" action="/entrar">
|
||||
<input id="u" name="username" type="text" autocomplete="username">
|
||||
<input id="p" name="password" type="password" autocomplete="current-password">
|
||||
<button id="b" type="submit">Entrar</button>
|
||||
</form></body></html>
|
||||
"""
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
print(f"✗ {msg}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def artefacto(receta, nombre, escotilla=None):
|
||||
"""El artefacto VIGENTE, por `takana hash` y nunca por glob.
|
||||
|
||||
⚠ La escotilla grita al usarse: el corpus es compartido y otro frente puede re-hashear una dep
|
||||
dejando lo de acá sin sellar durante las horas de su build. Un resultado obtenido con la
|
||||
escotilla NO se cita como «el de hoy pasa».
|
||||
"""
|
||||
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:\n"
|
||||
f" flock -o work/.farm-build.lock {HAMMER} --store {STORE} build recipes/{receta}.toml")
|
||||
return d
|
||||
|
||||
|
||||
class Servidor(http.server.SimpleHTTPRequestHandler):
|
||||
"""La página de login, servida de verdad. `atuq` la carga por la red como cualquier otra."""
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
datos = PAGINA.encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(datos)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(datos)
|
||||
|
||||
def do_POST(self):
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
self.rfile.read(n)
|
||||
datos = b"<!doctype html><html><body>adentro</body></html>"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(datos)))
|
||||
self.end_headers()
|
||||
self.wfile.write(datos)
|
||||
|
||||
|
||||
def puerto_libre():
|
||||
s = socket.socket()
|
||||
s.bind(("0.0.0.0", 0))
|
||||
p = s.getsockname()[1]
|
||||
s.close()
|
||||
return p
|
||||
|
||||
|
||||
# ── El cliente del cable, en Python ───────────────────────────────────────────────────────────
|
||||
# Marco de 4 bytes little-endian + JSON, que es lo que habla native messaging y lo que
|
||||
# `shared/foreign-webext` implementa del otro lado. Se escribe acá —y no se llama a la extensión—
|
||||
# porque las etapas A/B/C miden la CADENA host↔dueño↔diálogo, donde el navegador no participa:
|
||||
# meterlo sólo agregaría diez minutos y un sospechoso más a cada fallo.
|
||||
CLIENTE = r'''
|
||||
import json, os, struct, subprocess, sys
|
||||
def hablar(binario, estado, mensajes):
|
||||
p = subprocess.Popen([binario, "--state", estado],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
entrada = b""
|
||||
for m in mensajes:
|
||||
b = json.dumps(m).encode()
|
||||
entrada += struct.pack("<I", len(b)) + b
|
||||
fuera, err = p.communicate(entrada, timeout=180)
|
||||
salida, i = [], 0
|
||||
while i + 4 <= len(fuera):
|
||||
n = struct.unpack("<I", fuera[i:i+4])[0]; i += 4
|
||||
salida.append(json.loads(fuera[i:i+n])); i += n
|
||||
return salida, err.decode(errors="replace")
|
||||
'''
|
||||
|
||||
|
||||
|
||||
# ── El contestador: quien dice que sí (o que no) donde diría una persona ──────────────────────
|
||||
# ⚠ Espera a que la ventana esté en el ÁRBOL DEL COMPOSITOR y con el foco, **no** a que el proceso
|
||||
# exista. Que `shuma-pregunta` esté corriendo no dice que su superficie esté mapeada ni enfocada, y
|
||||
# una tecla mandada antes no la recibe nadie: el diálogo se queda abierto y el resultado se lee como
|
||||
# «el consentimiento no contesta», que es la conclusión contraria a la verdadera. El compositor es
|
||||
# el único que sabe quién tiene el foco, así que se le pregunta a él.
|
||||
CONTESTADOR = r"""
|
||||
import json, subprocess, sys, time
|
||||
|
||||
TECLA = sys.argv[1]
|
||||
ESPERA = int(sys.argv[2]) if len(sys.argv) > 2 else 120
|
||||
|
||||
|
||||
def arbol():
|
||||
r = subprocess.run(["/usr/bin/swaymsg", "-t", "get_tree", "-r"],
|
||||
capture_output=True, text=True)
|
||||
if r.returncode != 0 or not r.stdout.strip():
|
||||
return None
|
||||
try:
|
||||
return json.loads(r.stdout)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def ventanas(n, acc=None):
|
||||
acc = [] if acc is None else acc
|
||||
if n.get("pid") and n.get("type") in ("con", "floating_con"):
|
||||
acc.append(n)
|
||||
for hijo in (n.get("nodes") or []) + (n.get("floating_nodes") or []):
|
||||
ventanas(hijo, acc)
|
||||
return acc
|
||||
|
||||
|
||||
t0 = time.time()
|
||||
visto = None
|
||||
while time.time() - t0 < ESPERA:
|
||||
a = arbol()
|
||||
if a:
|
||||
vs = ventanas(a)
|
||||
if vs:
|
||||
visto = vs
|
||||
enfocada = [v for v in vs if v.get("focused")]
|
||||
print("VENTANAS " + json.dumps(
|
||||
[{"app_id": v.get("app_id"), "name": v.get("name"),
|
||||
"focused": bool(v.get("focused")),
|
||||
"rect": v.get("rect")} for v in vs], ensure_ascii=False))
|
||||
if enfocada:
|
||||
# Un respiro después del foco: el primer frame puede no estar pintado todavía y
|
||||
# la tecla llegaría a una ventana que aún no instaló su manejador.
|
||||
time.sleep(2)
|
||||
r = subprocess.run(["/usr/bin/wtype", TECLA], capture_output=True, text=True)
|
||||
print("WTYPE %s rc=%s %s" % (TECLA, r.returncode, r.stderr.strip()))
|
||||
raise SystemExit(0)
|
||||
time.sleep(1)
|
||||
print("SIN VENTANA tras %ss; lo último que vi: %s" % (ESPERA, json.dumps(visto, ensure_ascii=False)[:2000]))
|
||||
raise SystemExit(1)
|
||||
"""
|
||||
|
||||
|
||||
def guion_dentro(cuerpo, con_boveda, log="pasos.log", salida_rel="/salida"):
|
||||
"""El guión que corre DENTRO de la jaula, con sway headless levantado.
|
||||
|
||||
El compositor se levanta siempre —aunque la etapa no lo necesite— porque el diálogo es un
|
||||
cliente wayland y `boveda` también: sin compositor no se distingue «la app no abrió la bóveda»
|
||||
de «la app no encontró dónde pintar», que son dos causas muy distintas con el mismo síntoma.
|
||||
"""
|
||||
arranque_boveda = ""
|
||||
if con_boveda:
|
||||
arranque_boveda = f"""
|
||||
# El DUEÑO de la bóveda. Se lanza con la raíz de prueba puesta, así que abre
|
||||
# `…/tawasuyu/boveda-de-prueba` y no la de nadie. Levanta el socket del navegador en un hilo.
|
||||
BOVEDA_TEST_ROOT={RAIZ_DE_PRUEBA} /usr/bin/boveda >> {salida_rel}/boveda.log 2>&1 &
|
||||
echo "@@ boveda lanzada pid $!" >> {salida_rel}/{log}
|
||||
# Se espera al SOCKET, no a un `sleep`: un sleep que alcanza hoy no alcanza en una máquina
|
||||
# cargada, y el fallo aparece como «la bóveda estaba cerrada» — la conclusión contraria.
|
||||
i=0
|
||||
while [ ! -S "$XDG_RUNTIME_DIR/boveda.sock" ] && [ $i -lt 60 ]; do sleep 1; i=$((i+1)); done
|
||||
[ -S "$XDG_RUNTIME_DIR/boveda.sock" ] \\
|
||||
&& echo "@@ socket de la boveda LISTO en ${{i}}s" >> {salida_rel}/{log} \\
|
||||
|| echo "@@ SIN socket de la boveda tras ${{i}}s" >> {salida_rel}/{log}
|
||||
"""
|
||||
return f"""#!/bin/sh
|
||||
export XDG_RUNTIME_DIR=/run/user/0 HOME={salida_rel}/hogar LANG=C XDG_DATA_DIRS=/usr/share
|
||||
export XDG_STATE_HOME={salida_rel}/hogar/.local/state
|
||||
export WLR_BACKENDS=headless WLR_HEADLESS_OUTPUTS=1 WLR_RENDERER=pixman
|
||||
# El cliente de GL va por software: ver la cabecera. `LIBGL_ALWAYS_SOFTWARE` hace que EGL elija
|
||||
# `swrast` en vez de buscar un driver de la GPU que esta máquina no tiene, y `GALLIUM_DRIVER`
|
||||
# elige CUÁL de los dos software.
|
||||
#
|
||||
# ⚠ **Y tiene que ser `llvmpipe`, no `softpipe`, y no es una preferencia.** Medido el 2026-09-18:
|
||||
# con softpipe el diálogo no abre nunca y muere con `RequestDevice("Parent device is lost")`, que no
|
||||
# nombra la causa. La causa está una línea más arriba en el log, y sólo aparece con
|
||||
# `RUST_LOG=wgpu_core=info`:
|
||||
#
|
||||
# Renderer: softpipe · Version: 3.3 (Core Profile)
|
||||
# indirect-validation error: ComputePipeline(Internal("The selected version doesn't support
|
||||
# Features(BUFFER_STORAGE | COMPUTE_SHADER | DYNAMIC_ARRAY_SIZE)"))
|
||||
#
|
||||
# O sea: **wgpu exige compute shaders** y softpipe se queda en GL 3.3. `llvmpipe` da GL 4.5 y pasa.
|
||||
# Las dos son «mesa por software» y en el store son dos recetas hermanas (`mesa-swrast` es softpipe,
|
||||
# `mesa-llvmpipe` es llvmpipe + `llvm18`), así que la diferencia se ve como un detalle de nombre y
|
||||
# decide si el arnés existe o no.
|
||||
export LIBGL_ALWAYS_SOFTWARE=1 LIBGL_DRIVERS_PATH=/usr/lib/dri GALLIUM_DRIVER=llvmpipe
|
||||
mkdir -p "$XDG_RUNTIME_DIR" {salida_rel}/hogar "$XDG_STATE_HOME"; chmod 700 "$XDG_RUNTIME_DIR"
|
||||
(
|
||||
i=0
|
||||
while [ ! -S /run/user/0/wayland-1 ] && [ $i -lt 40 ]; do sleep 1; i=$((i+1)); done
|
||||
[ -S /run/user/0/wayland-1 ] || {{ echo "@@ sin socket wayland" >> {salida_rel}/{log}; exit 1; }}
|
||||
export WAYLAND_DISPLAY=wayland-1
|
||||
export SWAYSOCK=$(ls /run/user/0/sway-ipc.* 2>/dev/null | head -1)
|
||||
echo "@@ wayland listo" >> {salida_rel}/{log}
|
||||
{arranque_boveda}
|
||||
{cuerpo}
|
||||
sleep 1
|
||||
/usr/bin/swaymsg exit 2>/dev/null
|
||||
) &
|
||||
exec /usr/bin/sway -d > {salida_rel}/sway.log 2>&1
|
||||
"""
|
||||
|
||||
|
||||
def correr_jaula(capas, salida, guion, segundos=420):
|
||||
"""Una corrida de bwrap con las capas dadas. La ÚLTIMA capa gana."""
|
||||
ruta = os.path.join(salida, "dentro.sh")
|
||||
with open(ruta, "w") as fh:
|
||||
fh.write(guion)
|
||||
orden = ["bwrap"]
|
||||
for c in capas:
|
||||
orden += ["--overlay-src", c]
|
||||
orden += [
|
||||
"--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",
|
||||
"/bin/sh", "/salida/dentro.sh",
|
||||
]
|
||||
try:
|
||||
subprocess.run(orden, check=False, timeout=segundos)
|
||||
return True
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f" ⚠ la jaula no se cerró sola; se la cortó a los {segundos} s")
|
||||
return False
|
||||
|
||||
|
||||
def leer(salida, nombre):
|
||||
r = os.path.join(salida, nombre)
|
||||
return open(r, errors="replace").read() if os.path.exists(r) else ""
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(add_help=False)
|
||||
ap.add_argument("--hasta", default="F")
|
||||
ap.add_argument("--list", action="store_true")
|
||||
ap.add_argument("-h", "--help", action="store_true")
|
||||
args, _ = ap.parse_known_args()
|
||||
if args.help:
|
||||
print(__doc__)
|
||||
return
|
||||
if args.list:
|
||||
for clave, que in ETAPAS:
|
||||
print(f" {clave} {que}")
|
||||
return
|
||||
|
||||
hasta = args.hasta.upper()
|
||||
if hasta not in [c for c, _ in ETAPAS]:
|
||||
fatal(f"--hasta {hasta}: las etapas son " + ", ".join(c for c, _ in ETAPAS))
|
||||
|
||||
if not os.path.isdir(RFS):
|
||||
fatal(f"no está el rootfs de sway en {RFS} — hidratalo con "
|
||||
"`scripts/hydrate-profile.py escritorio-sway --into …`")
|
||||
|
||||
# Los artefactos se exigen POR ETAPA y no todos de entrada. No es comodidad: pedir los seis para
|
||||
# correr la A haría que este guardián no se pudiera encender mientras se construye una de las
|
||||
# piezas que él mismo destrabó —y la A es justamente la que mide el estado SIN esas piezas—.
|
||||
necesita = {"A": [("puriy-costura", "puriy-costura", None)]}
|
||||
necesita["B"] = necesita["A"] + [("boveda", "boveda", None),
|
||||
("llvm18", "llvm18", None),
|
||||
("mesa-llvmpipe", "mesa-llvmpipe", None)]
|
||||
necesita["C"] = necesita["B"] + [("shuma-pregunta", "shuma-pregunta", None), ("wtype", "wtype", None)]
|
||||
necesita["D"] = necesita["C"] + [("atuq", "atuq", "ATUQ_DIR")]
|
||||
necesita["E"] = necesita["D"]
|
||||
necesita["F"] = necesita["D"]
|
||||
|
||||
dirs = {}
|
||||
for receta, nombre, escotilla in necesita[hasta]:
|
||||
dirs[nombre] = artefacto(receta, nombre, escotilla)
|
||||
print(f"artefacto: {os.path.basename(dirs[nombre])}")
|
||||
|
||||
# El orden de las capas ES la semántica: **la última gana**. El mesa por software va al final
|
||||
# para pisar el `iris`-only del rootfs; si fuera antes, el rootfs le devolvería el driver de una
|
||||
# GPU Intel que esta máquina no tiene. `llvm18` va antes porque `swrast_dri.so` lo NECESITA:
|
||||
# sin él, el loader dice «failed to open swrast: libLLVM.so.18.1» y wgpu reporta «NoAdapter»,
|
||||
# que es el mismo síntoma que no tener driver.
|
||||
capas_base = [ALPINE, RFS]
|
||||
for n in ("atuq", "puriy-costura", "boveda", "shuma-pregunta", "wtype", "llvm18", "mesa-llvmpipe"):
|
||||
if n in dirs:
|
||||
capas_base.append(dirs[n])
|
||||
|
||||
# El servidor de la página de login. Se levanta aunque la etapa no lo use: su PUERTO es lo que
|
||||
# define la URL con la que se guarda la credencial, y esa URL tiene que ser la MISMA en la
|
||||
# etapa que guarda y en la que el navegador visita — si no, `vault.match` no encontraría nada
|
||||
# y el fallo hablaría de la bóveda cuando el que cambió fue el puerto.
|
||||
puerto = puerto_libre()
|
||||
servidor = http.server.ThreadingHTTPServer(("0.0.0.0", puerto), Servidor)
|
||||
threading.Thread(target=servidor.serve_forever, daemon=True).start()
|
||||
url = f"http://127.0.0.1:{puerto}/"
|
||||
print(f"la página de login se sirve en {url}")
|
||||
|
||||
tmp = tempfile.mkdtemp(prefix="atuq-boveda-metal-")
|
||||
salida = os.path.join(tmp, "salida")
|
||||
os.makedirs(salida)
|
||||
fallos = []
|
||||
try:
|
||||
correr_etapas(capas_base, salida, hasta, url, fallos)
|
||||
finally:
|
||||
guardado = os.path.join(ROOT, "work", "atuq-boveda-metal")
|
||||
os.makedirs(guardado, exist_ok=True)
|
||||
for n in os.listdir(salida):
|
||||
o = os.path.join(salida, n)
|
||||
if os.path.isfile(o):
|
||||
shutil.copy(o, os.path.join(guardado, n))
|
||||
print(f"\nlogs en {guardado}")
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
if fallos:
|
||||
for f in fallos:
|
||||
print(f"✗ {f}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
print("\n✓ todo lo que se corrió, en verde")
|
||||
|
||||
|
||||
def respuesta(salida, fichero, verbo):
|
||||
"""La respuesta a un verbo, leída del JSON que dejó el preguntador dentro de la jaula."""
|
||||
crudo = leer(salida, fichero)
|
||||
if not crudo.strip():
|
||||
return None
|
||||
for r in json.loads(crudo):
|
||||
if r.get("verb") == verbo:
|
||||
return r
|
||||
return None
|
||||
|
||||
|
||||
def correr_etapas(capas, salida, hasta, url, fallos):
|
||||
"""Las etapas, en orden y cada una en su propia jaula.
|
||||
|
||||
Cada etapa levanta su propio compositor. Es más lento que reusar uno y es a propósito: el
|
||||
estado que importa —la bóveda de prueba, el registro del host— vive en `/salida`, que se
|
||||
comparte, y así una etapa no hereda procesos de la anterior. Dos corridas sobre el mismo
|
||||
log se MEZCLAN y el resultado parece un pase; eso ya pasó en este frente (§6.1.bis).
|
||||
"""
|
||||
orden = [c for c, _ in ETAPAS]
|
||||
tope = orden.index(hasta)
|
||||
escribir_preguntador(salida, url)
|
||||
with open(os.path.join(salida, "contestar.py"), "w") as fh:
|
||||
fh.write(CONTESTADOR)
|
||||
|
||||
# ── A: SIN la app. Es el estado en que estuvo el corpus entero hasta el 2026-09-18. ────────
|
||||
print("\n== A · sin la app `boveda`, ¿qué contesta el host?")
|
||||
cuerpo = " python3 /salida/preguntar.py sin-boveda >> /salida/pasos-A.log 2>&1\n"
|
||||
correr_jaula(capas, salida, guion_dentro(cuerpo, con_boveda=False, log="pasos-A.log"), segundos=240)
|
||||
print(indentar(leer(salida, "pasos-A.log")))
|
||||
estado = respuesta(salida, "sin-boveda.json", "vault.status")
|
||||
if estado is None:
|
||||
fallos.append("A: el preguntador no dejó respuesta — mirá pasos-A.log")
|
||||
elif estado.get("locked") is not True:
|
||||
fallos.append(f"A: sin la app, vault.status tenía que decir locked:true y dijo {estado}")
|
||||
else:
|
||||
print(" ✓ A: sin dueño, la bóveda contesta CERRADA (y con ok:true, que es la trampa)")
|
||||
if tope < orden.index("B"):
|
||||
return
|
||||
|
||||
# ── B: CON la app. El dueño abre la base y atiende el socket. ──────────────────────────────
|
||||
print("\n== B · con la app `boveda` corriendo, ¿la bóveda está abierta?")
|
||||
cuerpo = " python3 /salida/preguntar.py con-boveda >> /salida/pasos-B.log 2>&1\n"
|
||||
correr_jaula(capas, salida, guion_dentro(cuerpo, con_boveda=True, log="pasos-B.log"), segundos=300)
|
||||
print(indentar(leer(salida, "pasos-B.log")))
|
||||
estado = respuesta(salida, "con-boveda.json", "vault.status")
|
||||
if estado is None:
|
||||
fallos.append("B: el preguntador no dejó respuesta — mirá pasos-B.log y boveda.log")
|
||||
elif estado.get("locked") is not False:
|
||||
fallos.append(f"B: con la app corriendo, vault.status tenía que decir locked:false y dijo "
|
||||
f"{estado}.\n"
|
||||
" Mirá `boveda.log`. Si dice «index out of bounds: the len is 0 but the index "
|
||||
"is 0» en `llimphi-hal/src/lib.rs`, NO es de la bóveda: es el muro del §7.septies "
|
||||
"—llimphi construye la instancia de wgpu SIN display handle, wgpu cae a la "
|
||||
"plataforma EGL surfaceless, esa plataforma no tiene configs de ventana, y la "
|
||||
"lista de formatos de la surface sale vacía—. Se destraba en llimphi, no acá, y "
|
||||
"hasta entonces la app no abre ventana ni levanta el socket del navegador.")
|
||||
else:
|
||||
print(" ✓ B: el dueño atiende — la bóveda de prueba está ABIERTA")
|
||||
if tope < orden.index("C"):
|
||||
return
|
||||
|
||||
# ── C: el instrumento. Antes de pedirle nada a la bóveda, ¿se puede contestar un diálogo? ──
|
||||
# Va ANTES de D a propósito: si el diálogo no se puede contestar, `vault.save` va a decir
|
||||
# `denied` — que es la respuesta CORRECTA a un consentimiento que no llegó— y sin esta etapa
|
||||
# eso se leería como «la bóveda no guarda». Separar el instrumento del producto es lo que
|
||||
# hace que el fallo diga cuál de los dos se rompió.
|
||||
print("\n== C · ¿abre el diálogo en el compositor, y se lo puede contestar?")
|
||||
cuerpo = """
|
||||
for par in si:%s:1 no:%s:2; do
|
||||
quien=${par%%%%:*}; resto=${par#*:}; tecla=${resto%%%%:*}; n=${resto#*:}
|
||||
( python3 /salida/contestar.py "$tecla" > /salida/contestador-C-$quien.log 2>&1 ) &
|
||||
/usr/bin/shuma-pregunta --title "Prueba del instrumento" --detail "esto no toca la boveda" \
|
||||
--option "yes=Si, contesta" --option "no=No" \
|
||||
> /salida/dialogo-C-$quien.json 2> /salida/dialogo-C-$quien.err
|
||||
echo "@@ dialogo $quien salio con $? y dijo: $(cat /salida/dialogo-C-$quien.json)" >> /salida/pasos-C.log
|
||||
wait
|
||||
done
|
||||
""" % (TECLA_SI, TECLA_NO)
|
||||
correr_jaula(capas, salida, guion_dentro(cuerpo, con_boveda=False, log="pasos-C.log"), segundos=420)
|
||||
print(indentar(leer(salida, "pasos-C.log")))
|
||||
for quien in ("si", "no"):
|
||||
print(indentar(leer(salida, f"contestador-C-{quien}.log")))
|
||||
dijo_si = leer(salida, "dialogo-C-si.json").strip()
|
||||
dijo_no = leer(salida, "dialogo-C-no.json").strip()
|
||||
if '"choice":"yes"' not in dijo_si.replace(" ", ""):
|
||||
fallos.append(f"C: al teclear «{TECLA_SI}» el diálogo tenía que contestar yes y dijo {dijo_si!r} "
|
||||
"— si no hay VENTANAS en el log del contestador, el cliente de GL no pintó "
|
||||
"(mirá dialogo-C-si.err: es donde llimphi dice por qué)")
|
||||
elif '"choice":"no"' not in dijo_no.replace(" ", ""):
|
||||
# El control: sin él, un diálogo que contestara «yes» pase lo que pase se vería igual.
|
||||
fallos.append(f"C: el control falló — al teclear «{TECLA_NO}» tenía que contestar no y dijo {dijo_no!r}")
|
||||
else:
|
||||
print(" ✓ C: el diálogo abre, la tecla llega, y contesta lo que se le tecleó (sí Y no)")
|
||||
if tope < orden.index("D"):
|
||||
return
|
||||
|
||||
# ── D: la bóveda guarda, y sólo con permiso. ───────────────────────────────────────────────
|
||||
print("\n== D · vault.save con el consentimiento dado, ¿guarda? ¿y después la encuentra?")
|
||||
cuerpo = """
|
||||
( python3 /salida/contestar.py "%s" > /salida/contestador-D.log 2>&1 ) &
|
||||
python3 /salida/preguntar.py guardar >> /salida/pasos-D.log 2>&1
|
||||
wait
|
||||
""" % TECLA_SI
|
||||
correr_jaula(capas, salida, guion_dentro(cuerpo, con_boveda=True, log="pasos-D.log"), segundos=420)
|
||||
print(indentar(leer(salida, "pasos-D.log")))
|
||||
print(indentar(leer(salida, "contestador-D.log")))
|
||||
guardo = respuesta(salida, "guardar.json", "vault.save")
|
||||
encontro = respuesta(salida, "guardar.json", "vault.match")
|
||||
if guardo is None:
|
||||
fallos.append("D: el preguntador no dejó respuesta de vault.save — mirá pasos-D.log")
|
||||
elif guardo.get("denied") is True:
|
||||
fallos.append("D: vault.save salió DENEGADO. Si la etapa C está en verde, el diálogo se puede "
|
||||
"contestar y esto es del producto; si C falló, es el instrumento y no la bóveda")
|
||||
elif not guardo.get("item"):
|
||||
fallos.append(f"D: vault.save no devolvió el identificador de lo guardado: {guardo}")
|
||||
elif not (encontro or {}).get("items"):
|
||||
fallos.append(f"D: guardó pero vault.match no la encuentra para {url}: {encontro}")
|
||||
else:
|
||||
item = encontro["items"][0]
|
||||
if item.get("username") != USUARIO:
|
||||
fallos.append(f"D: la credencial encontrada no es la guardada: {item}")
|
||||
else:
|
||||
print(f" ✓ D: guardada con permiso y encontrada de nuevo ({item.get('title')!r}, "
|
||||
f"usuario {item.get('username')!r})")
|
||||
# Y la mitad que importa: `vault.match` NO puede traer la contraseña. El tipo del
|
||||
# otro lado no tiene dónde ponerla, y esto lo comprueba desde afuera.
|
||||
if any(k in item for k in ("password", "clave")):
|
||||
fallos.append(f"D: ⚠ vault.match trajo una CONTRASEÑA, y no debe: {item}")
|
||||
|
||||
|
||||
def indentar(t):
|
||||
return "\n".join(" " + l for l in t.splitlines() if l.strip())
|
||||
|
||||
|
||||
def escribir_preguntador(salida, url):
|
||||
"""El cliente del cable que corre DENTRO de la jaula.
|
||||
|
||||
Habla con el host EXACTAMENTE como lo hace la extensión —marco de 4 bytes + JSON— y no con la
|
||||
bóveda directamente: lo que este guardián tiene que medir es la cadena entera, y el host es el
|
||||
único que sabe encontrar al dueño.
|
||||
"""
|
||||
with open(os.path.join(salida, "preguntar.py"), "w") as fh:
|
||||
fh.write(CLIENTE)
|
||||
fh.write(f'''
|
||||
QUE = sys.argv[1]
|
||||
URL = {url!r}
|
||||
BIN = "/usr/bin/puriy-costura"
|
||||
ESTADO = "/salida/hogar/estado-costura"
|
||||
os.makedirs(ESTADO, exist_ok=True)
|
||||
# `ping` va SIEMPRE y es el control de la corrida: separa «el host no contestó» de «el host
|
||||
# contestó que la bóveda está cerrada», que es la distinción que costó tres semanas.
|
||||
mensajes = [
|
||||
{{"id": 1, "verb": "ping"}},
|
||||
{{"id": 2, "verb": "vault.status"}},
|
||||
]
|
||||
if QUE == "guardar":
|
||||
mensajes.append({{"id": 3, "verb": "vault.save", "url": URL,
|
||||
"username": {USUARIO!r}, "password": {CLAVE!r}, "title": {TITULO!r}}})
|
||||
if QUE in ("match", "guardar"):
|
||||
mensajes.append({{"id": 4, "verb": "vault.match", "url": URL}})
|
||||
respuestas, err = hablar(BIN, ESTADO, mensajes)
|
||||
open("/salida/%s.json" % QUE, "w").write(json.dumps(respuestas, ensure_ascii=False, indent=1))
|
||||
open("/salida/%s.err" % QUE, "w").write(err)
|
||||
for r in respuestas:
|
||||
print(" <-", json.dumps(r, ensure_ascii=False))
|
||||
''')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user