atuq §7.duodecies: el sembrador entra a las cuatro imágenes — y cifraba la seed de todos con una palabra pública

El §7.undecies dejó la bóveda declarada y a NADIE capaz de abrirla: ninguna
imagen traía un binario que sembrara `pacha_llavero::SEED_IDENTIDAD`. Esta es
esa unidad.

De las dos formas posibles entra `agora-cli`, y el motivo no es que sea mejor:
el wizard `churay-welcome-llimphi` SÍ tiene binario (medido: `src/main.rs` sin
`[[bin]]`, o sea que cargo lo descubre), pero decide además backend de IA,
dotfiles, fondo de pantalla y chasqui — la experiencia de primer arranque
entera, que no se decide dentro de una unidad del navegador.

⚠ Y antes de poder declararlo apareció lo que lo volvía imposible: sin
`AGORA_PASSPHRASE`, `Sesion::abrir()` caía en la frase de desarrollo
"agora-dev" con un aviso por stderr y un ✓ en pantalla. La cadena que eso toca:
frase → Argon2id → ChaCha20-Poly1305 que cifra la seed → la clave con la que
`boveda` descifra su base. O sea, en una imagen de escritorio, la bóveda de
todo el mundo cerrada con una palabra escrita en el fuente, y nada que falle.

Arreglado en tawasuyu (`fd08dc03a`): variable > terminal (se pregunta, sin eco,
y DOS veces en la génesis, donde un error de tipeo no se nota hasta que la seed
ya no se recupera) > desarrollo sólo si no hay a quién preguntarle. La decisión
vive en una función pura con cuatro tests, probada AL REVÉS: con el brazo
`Preguntar` borrado falla con `left: Desarrollo / right: Preguntar`.

Pin `9967b02c` → `da5fb8968` ⇒ `b3:46529e14`, 1,9 M, sellado en el worker con
la guarda PEGADA al build. Mirado por dentro (regla 3) y probado como
artefacto, con control negativo: `identity new` + `unlock` deja
`user pacha:id:default: 32` en `/proc/keys`, y con la frase equivocada contesta
«autenticación fallida» y NO re-siembra.

El muro del `Cargo.lock` por cuarta vez, con la causa cambiada: esta vez no la
puso quien tocó el lock sino otro agente que metió `shuma-taller` en un
`Cargo.toml`. Cerrado en el worker, donde el registro está completo: +1 línea.
Y el lock del árbol compartido traía otra vez el malo (índice y árbol con dos
versiones distintas, las dos rotas), así que el commit se armó con
`commit-tree` sin pasar por el índice.

Corrección al §7.undecies: el verbo es `agora-cli unlock`, no
`agora-cli identity unlock`.

El guardián de coherencia pasa de SEIS lugares a SIETE, con su cuarto control
negativo; los cuatro, en verde.

Queda: la herencia del llavero de SESIÓN entre procesos hermanos (sin medir —
y `/proc/keys` como root no la mide), y `pacha`/`pacha-secretos` en
`perfil.servidor` con el mismo hueco.
This commit is contained in:
Sergio
2026-09-21 16:40:46 +00:00
parent 0e0d8ddf0c
commit c7097e4a91
4 changed files with 426 additions and 8 deletions
File diff suppressed because one or more lines are too long
+132
View File
@@ -352,6 +352,39 @@ paquetes = [
# y el control de contestar que NO (§7.novies)—. Por eso entran hoy y no antes. # y el control de contestar que NO (§7.novies)—. Por eso entran hoy y no antes.
"boveda", "boveda",
"shuma-pregunta", "shuma-pregunta",
# ── …Y QUIÉN SIEMBRA LA IDENTIDAD CON LA QUE ESA BÓVEDA SE ABRE (SDD 26 §7.duodecies — 2026-09-21)
# Declarar al dueño y al diálogo dejó la función ANDANDO y todavía IMPOSIBLE de usar, y esto se
# midió el mismo día. `boveda` abre su base con una clave derivada de la seed de identidad, que
# `pacha-boveda-llimphi` saca del llavero de SESIÓN bajo `pacha_llavero::SEED_IDENTIDAD`. En todo
# tawasuyu esa clave la ESCRIBEN dos binarios —`agora-cli unlock` y el wizard de
# bienvenida `churay-welcome-llimphi`—: el primero estaba sellado con `perfiles: []` y además
# pineado al 2026-06-18, donde el verbo todavía no existe; el segundo no tiene receta. ⇒ en las
# cuatro imágenes `abrir()` daba `Err(boveda-cerrada)` SIEMPRE, y no por falta de desbloqueo:
# por falta de CON QUÉ. Es la misma forma de fallo del renglón de arriba una capa más abajo, y
# por eso los dos bloques viven juntos: la bóveda sin dueño y el dueño sin identidad se ven igual
# desde el navegador — `{"ok":true,"locked":true}`, que es una respuesta exitosa.
# `agora-cli` el sembrador: `identity new --name <nombre>` crea la identidad y `unlock` la
# deja en el llavero de la sesión. **1,9 M** medidos sobre el artefacto sellado
# (`b3:46529e14`), contra los ~43 M de las dos de arriba. Es CLI: no tiene
# lanzador ni lo necesita, así que no hay `.desktop` que comprobar acá.
# Medido contra el ARTEFACTO, no contra el commit (regla 3): en el worker, con el binario que va
# a la imagen, `identity new` + `unlock` deja `user pacha:id:default: 32` en `/proc/keys` — la
# seed, sus 32 bytes. Y con el control negativo puesto: con la frase equivocada contesta
# «autenticación fallida» y NO re-siembra.
# ⚠ NO es el camino de diseño y conviene que quede escrito: el de diseño es el wizard, que pide
# la frase en un campo en vez de en una terminal. Pero ese wizard decide además backend de IA,
# dotfiles, fondo de pantalla y chasqui — traerlo es decidir la experiencia de primer arranque
# entera, y eso no se hace de paso. Ésta hace UNA cosa y es la que el propio tawasuyu nombra como
# «la que siembra el login» (comentario de `pacha-secretos`).
# ⚠ Y queda un eslabón SIN MEDIR, dicho como tal: el llavero es el de SESIÓN
# (`KEY_SPEC_SESSION_KEYRING`), que lo crea el login, y en takana no lo crea nadie —`shadow`
# compila `--without-libpam`, así que el `login` de consola no pasa por PAM y `pam_keyinit` no
# corre, aunque el módulo viaje en la imagen dentro de `linux-pam`—. De ahí se SIGUE que dos
# procesos hermanos puedan no compartir anillo, y entonces desbloquear en una terminal no le
# serviría a la app lanzada desde el menú; el camino bueno sería la misma rama de procesos
# (desbloquear en consola y lanzar el compositor desde ahí). Eso se mide con el guardián de
# metal, no desde acá: `add_key` da EPERM en la jaula y `keyctl` da ENOSYS en el LXC del worker.
"agora-cli",
# ── CAPTURA Y STREAMING ────────────────────────────────────────────────────────────────────── # ── CAPTURA Y STREAMING ──────────────────────────────────────────────────────────────────────
# `obs-studio` (2026-09-04). A diferencia de mpv, ésta vive en ESTA COLA y no en el corpus, y no # `obs-studio` (2026-09-04). A diferencia de mpv, ésta vive en ESTA COLA y no en el corpus, y no
# es preferencia: su frontend es Qt6 y las 13 recetas Qt viven sólo en `incoming-kde`. Un qtbase # es preferencia: su frontend es Qt6 y las 13 recetas Qt viven sólo en `incoming-kde`. Un qtbase
@@ -657,6 +690,39 @@ paquetes = [
# y el control de contestar que NO (§7.novies)—. Por eso entran hoy y no antes. # y el control de contestar que NO (§7.novies)—. Por eso entran hoy y no antes.
"boveda", "boveda",
"shuma-pregunta", "shuma-pregunta",
# ── …Y QUIÉN SIEMBRA LA IDENTIDAD CON LA QUE ESA BÓVEDA SE ABRE (SDD 26 §7.duodecies — 2026-09-21)
# Declarar al dueño y al diálogo dejó la función ANDANDO y todavía IMPOSIBLE de usar, y esto se
# midió el mismo día. `boveda` abre su base con una clave derivada de la seed de identidad, que
# `pacha-boveda-llimphi` saca del llavero de SESIÓN bajo `pacha_llavero::SEED_IDENTIDAD`. En todo
# tawasuyu esa clave la ESCRIBEN dos binarios —`agora-cli unlock` y el wizard de
# bienvenida `churay-welcome-llimphi`—: el primero estaba sellado con `perfiles: []` y además
# pineado al 2026-06-18, donde el verbo todavía no existe; el segundo no tiene receta. ⇒ en las
# cuatro imágenes `abrir()` daba `Err(boveda-cerrada)` SIEMPRE, y no por falta de desbloqueo:
# por falta de CON QUÉ. Es la misma forma de fallo del renglón de arriba una capa más abajo, y
# por eso los dos bloques viven juntos: la bóveda sin dueño y el dueño sin identidad se ven igual
# desde el navegador — `{"ok":true,"locked":true}`, que es una respuesta exitosa.
# `agora-cli` el sembrador: `identity new --name <nombre>` crea la identidad y `unlock` la
# deja en el llavero de la sesión. **1,9 M** medidos sobre el artefacto sellado
# (`b3:46529e14`), contra los ~43 M de las dos de arriba. Es CLI: no tiene
# lanzador ni lo necesita, así que no hay `.desktop` que comprobar acá.
# Medido contra el ARTEFACTO, no contra el commit (regla 3): en el worker, con el binario que va
# a la imagen, `identity new` + `unlock` deja `user pacha:id:default: 32` en `/proc/keys` — la
# seed, sus 32 bytes. Y con el control negativo puesto: con la frase equivocada contesta
# «autenticación fallida» y NO re-siembra.
# ⚠ NO es el camino de diseño y conviene que quede escrito: el de diseño es el wizard, que pide
# la frase en un campo en vez de en una terminal. Pero ese wizard decide además backend de IA,
# dotfiles, fondo de pantalla y chasqui — traerlo es decidir la experiencia de primer arranque
# entera, y eso no se hace de paso. Ésta hace UNA cosa y es la que el propio tawasuyu nombra como
# «la que siembra el login» (comentario de `pacha-secretos`).
# ⚠ Y queda un eslabón SIN MEDIR, dicho como tal: el llavero es el de SESIÓN
# (`KEY_SPEC_SESSION_KEYRING`), que lo crea el login, y en takana no lo crea nadie —`shadow`
# compila `--without-libpam`, así que el `login` de consola no pasa por PAM y `pam_keyinit` no
# corre, aunque el módulo viaje en la imagen dentro de `linux-pam`—. De ahí se SIGUE que dos
# procesos hermanos puedan no compartir anillo, y entonces desbloquear en una terminal no le
# serviría a la app lanzada desde el menú; el camino bueno sería la misma rama de procesos
# (desbloquear en consola y lanzar el compositor desde ahí). Eso se mide con el guardián de
# metal, no desde acá: `add_key` da EPERM en la jaula y `keyctl` da ENOSYS en el LXC del worker.
"agora-cli",
# ── VISOR DE IMÁGENES, TAMBIÉN EN LAS CUATRO ───────────────────────────────────────────────── # ── VISOR DE IMÁGENES, TAMBIÉN EN LAS CUATRO ─────────────────────────────────────────────────
# `swayimg` y no `imv`, que era lo que proponía `docs/plan-apps-usuario-final.md`: imv dibuja con # `swayimg` y no `imv`, que era lo que proponía `docs/plan-apps-usuario-final.md`: imv dibuja con
# OpenGL de función fija (`glBegin`/`glOrtho`) y esta distro NO tiene proveedor de GL de # OpenGL de función fija (`glBegin`/`glOrtho`) y esta distro NO tiene proveedor de GL de
@@ -1056,6 +1122,39 @@ paquetes = [
# y el control de contestar que NO (§7.novies)—. Por eso entran hoy y no antes. # y el control de contestar que NO (§7.novies)—. Por eso entran hoy y no antes.
"boveda", "boveda",
"shuma-pregunta", "shuma-pregunta",
# ── …Y QUIÉN SIEMBRA LA IDENTIDAD CON LA QUE ESA BÓVEDA SE ABRE (SDD 26 §7.duodecies — 2026-09-21)
# Declarar al dueño y al diálogo dejó la función ANDANDO y todavía IMPOSIBLE de usar, y esto se
# midió el mismo día. `boveda` abre su base con una clave derivada de la seed de identidad, que
# `pacha-boveda-llimphi` saca del llavero de SESIÓN bajo `pacha_llavero::SEED_IDENTIDAD`. En todo
# tawasuyu esa clave la ESCRIBEN dos binarios —`agora-cli unlock` y el wizard de
# bienvenida `churay-welcome-llimphi`—: el primero estaba sellado con `perfiles: []` y además
# pineado al 2026-06-18, donde el verbo todavía no existe; el segundo no tiene receta. ⇒ en las
# cuatro imágenes `abrir()` daba `Err(boveda-cerrada)` SIEMPRE, y no por falta de desbloqueo:
# por falta de CON QUÉ. Es la misma forma de fallo del renglón de arriba una capa más abajo, y
# por eso los dos bloques viven juntos: la bóveda sin dueño y el dueño sin identidad se ven igual
# desde el navegador — `{"ok":true,"locked":true}`, que es una respuesta exitosa.
# `agora-cli` el sembrador: `identity new --name <nombre>` crea la identidad y `unlock` la
# deja en el llavero de la sesión. **1,9 M** medidos sobre el artefacto sellado
# (`b3:46529e14`), contra los ~43 M de las dos de arriba. Es CLI: no tiene
# lanzador ni lo necesita, así que no hay `.desktop` que comprobar acá.
# Medido contra el ARTEFACTO, no contra el commit (regla 3): en el worker, con el binario que va
# a la imagen, `identity new` + `unlock` deja `user pacha:id:default: 32` en `/proc/keys` — la
# seed, sus 32 bytes. Y con el control negativo puesto: con la frase equivocada contesta
# «autenticación fallida» y NO re-siembra.
# ⚠ NO es el camino de diseño y conviene que quede escrito: el de diseño es el wizard, que pide
# la frase en un campo en vez de en una terminal. Pero ese wizard decide además backend de IA,
# dotfiles, fondo de pantalla y chasqui — traerlo es decidir la experiencia de primer arranque
# entera, y eso no se hace de paso. Ésta hace UNA cosa y es la que el propio tawasuyu nombra como
# «la que siembra el login» (comentario de `pacha-secretos`).
# ⚠ Y queda un eslabón SIN MEDIR, dicho como tal: el llavero es el de SESIÓN
# (`KEY_SPEC_SESSION_KEYRING`), que lo crea el login, y en takana no lo crea nadie —`shadow`
# compila `--without-libpam`, así que el `login` de consola no pasa por PAM y `pam_keyinit` no
# corre, aunque el módulo viaje en la imagen dentro de `linux-pam`—. De ahí se SIGUE que dos
# procesos hermanos puedan no compartir anillo, y entonces desbloquear en una terminal no le
# serviría a la app lanzada desde el menú; el camino bueno sería la misma rama de procesos
# (desbloquear en consola y lanzar el compositor desde ahí). Eso se mide con el guardián de
# metal, no desde acá: `add_key` da EPERM en la jaula y `keyctl` da ENOSYS en el LXC del worker.
"agora-cli",
# ── VISOR DE IMÁGENES, TAMBIÉN EN LAS CUATRO ───────────────────────────────────────────────── # ── VISOR DE IMÁGENES, TAMBIÉN EN LAS CUATRO ─────────────────────────────────────────────────
# `swayimg` y no `imv`, que era lo que proponía `docs/plan-apps-usuario-final.md`: imv dibuja con # `swayimg` y no `imv`, que era lo que proponía `docs/plan-apps-usuario-final.md`: imv dibuja con
# OpenGL de función fija (`glBegin`/`glOrtho`) y esta distro NO tiene proveedor de GL de # OpenGL de función fija (`glBegin`/`glOrtho`) y esta distro NO tiene proveedor de GL de
@@ -1318,6 +1417,39 @@ paquetes = [
# y el control de contestar que NO (§7.novies)—. Por eso entran hoy y no antes. # y el control de contestar que NO (§7.novies)—. Por eso entran hoy y no antes.
"boveda", "boveda",
"shuma-pregunta", "shuma-pregunta",
# ── …Y QUIÉN SIEMBRA LA IDENTIDAD CON LA QUE ESA BÓVEDA SE ABRE (SDD 26 §7.duodecies — 2026-09-21)
# Declarar al dueño y al diálogo dejó la función ANDANDO y todavía IMPOSIBLE de usar, y esto se
# midió el mismo día. `boveda` abre su base con una clave derivada de la seed de identidad, que
# `pacha-boveda-llimphi` saca del llavero de SESIÓN bajo `pacha_llavero::SEED_IDENTIDAD`. En todo
# tawasuyu esa clave la ESCRIBEN dos binarios —`agora-cli unlock` y el wizard de
# bienvenida `churay-welcome-llimphi`—: el primero estaba sellado con `perfiles: []` y además
# pineado al 2026-06-18, donde el verbo todavía no existe; el segundo no tiene receta. ⇒ en las
# cuatro imágenes `abrir()` daba `Err(boveda-cerrada)` SIEMPRE, y no por falta de desbloqueo:
# por falta de CON QUÉ. Es la misma forma de fallo del renglón de arriba una capa más abajo, y
# por eso los dos bloques viven juntos: la bóveda sin dueño y el dueño sin identidad se ven igual
# desde el navegador — `{"ok":true,"locked":true}`, que es una respuesta exitosa.
# `agora-cli` el sembrador: `identity new --name <nombre>` crea la identidad y `unlock` la
# deja en el llavero de la sesión. **1,9 M** medidos sobre el artefacto sellado
# (`b3:46529e14`), contra los ~43 M de las dos de arriba. Es CLI: no tiene
# lanzador ni lo necesita, así que no hay `.desktop` que comprobar acá.
# Medido contra el ARTEFACTO, no contra el commit (regla 3): en el worker, con el binario que va
# a la imagen, `identity new` + `unlock` deja `user pacha:id:default: 32` en `/proc/keys` — la
# seed, sus 32 bytes. Y con el control negativo puesto: con la frase equivocada contesta
# «autenticación fallida» y NO re-siembra.
# ⚠ NO es el camino de diseño y conviene que quede escrito: el de diseño es el wizard, que pide
# la frase en un campo en vez de en una terminal. Pero ese wizard decide además backend de IA,
# dotfiles, fondo de pantalla y chasqui — traerlo es decidir la experiencia de primer arranque
# entera, y eso no se hace de paso. Ésta hace UNA cosa y es la que el propio tawasuyu nombra como
# «la que siembra el login» (comentario de `pacha-secretos`).
# ⚠ Y queda un eslabón SIN MEDIR, dicho como tal: el llavero es el de SESIÓN
# (`KEY_SPEC_SESSION_KEYRING`), que lo crea el login, y en takana no lo crea nadie —`shadow`
# compila `--without-libpam`, así que el `login` de consola no pasa por PAM y `pam_keyinit` no
# corre, aunque el módulo viaje en la imagen dentro de `linux-pam`—. De ahí se SIGUE que dos
# procesos hermanos puedan no compartir anillo, y entonces desbloquear en una terminal no le
# serviría a la app lanzada desde el menú; el camino bueno sería la misma rama de procesos
# (desbloquear en consola y lanzar el compositor desde ahí). Eso se mide con el guardián de
# metal, no desde acá: `add_key` da EPERM en la jaula y `keyctl` da ENOSYS en el LXC del worker.
"agora-cli",
# `dunst` es LA OTRA MITAD, y sólo hace falta acá: KDE la atiende con plasma-workspace, GNOME con # `dunst` es LA OTRA MITAD, y sólo hace falta acá: KDE la atiende con plasma-workspace, GNOME con
# gnome-shell y COSMIC con cosmic-notifications; sway no tenía a NADIE escuchando # gnome-shell y COSMIC con cosmic-notifications; sway no tenía a NADIE escuchando
# `org.freedesktop.Notifications`, así que una página que pedía notificar mandaba el mensaje al bus # `org.freedesktop.Notifications`, así que una página que pedía notificar mandaba el mensaje al bus
+1 -1
View File
@@ -11,7 +11,7 @@
# #
# `boveda` abre su base con una clave derivada de la seed de identidad # `boveda` abre su base con una clave derivada de la seed de identidad
# └─ que `pacha-boveda-llimphi` saca del llavero de SESIÓN, clave `pacha_llavero::SEED_IDENTIDAD` # └─ que `pacha-boveda-llimphi` saca del llavero de SESIÓN, clave `pacha_llavero::SEED_IDENTIDAD`
# └─ que en TODO tawasuyu escriben dos binarios: `agora-cli identity unlock` # └─ que en TODO tawasuyu escriben dos binarios: `agora-cli unlock`
# y el onboarding `churay-welcome-llimphi` # y el onboarding `churay-welcome-llimphi`
# └─ `agora-cli` estaba en ninguna imagen · `churay-welcome` no tiene receta # └─ `agora-cli` estaba en ninguna imagen · `churay-welcome` no tiene receta
# #
+119 -6
View File
@@ -4,9 +4,10 @@
python3 scripts/test-atuq-boveda-coherente.py python3 scripts/test-atuq-boveda-coherente.py
python3 scripts/test-atuq-boveda-coherente.py --negative-control python3 scripts/test-atuq-boveda-coherente.py --negative-control
python3 scripts/test-atuq-boveda-coherente.py --negative-control-perfil python3 scripts/test-atuq-boveda-coherente.py --negative-control-perfil
python3 scripts/test-atuq-boveda-coherente.py --negative-control-sembrador
── QUÉ MIRA, Y POR QUÉ ESTE GUARDIÁN EXISTE APARTE ─────────────────────────────────────────── ── QUÉ MIRA, Y POR QUÉ ESTE GUARDIÁN EXISTE APARTE ───────────────────────────────────────────
Una extensión de `atuq` está enchufada en SEIS lugares distintos, y ninguno da error si falta: Una extensión de `atuq` está enchufada en SIETE lugares distintos, y ninguno da error si falta:
manifest.json el id que la extensión declara manifest.json el id que la extensión declara
distribution/policies.json la política que la instala distribution/policies.json la política que la instala
@@ -14,6 +15,7 @@ Una extensión de `atuq` está enchufada en SEIS lugares distintos, y ninguno da
atuq.cfg las preferencias que la acompañan atuq.cfg las preferencias que la acompañan
recipes/puriy-costura.toml el COMMIT del host, que decide si sus verbos existen recipes/puriy-costura.toml el COMMIT del host, que decide si sus verbos existen
docs/state/targets.toml quién DECLARA al dueño y al diálogo en las imágenes docs/state/targets.toml quién DECLARA al dueño y al diálogo en las imágenes
recipes/agora-cli.toml quién puede SEMBRAR la identidad con la que la base se abre
Si falta la política, la extensión no se instala y el usuario ve un navegador sin la función. Si Si falta la política, la extensión no se instala y el usuario ve un navegador sin la función. Si
falta el permiso del host, la extensión se instala y **se conecta a nada** —`connectNative` falla falta el permiso del host, la extensión se instala y **se conecta a nada** —`connectNative` falla
@@ -23,17 +25,21 @@ extensión conecta, manda `vault.match` y recibe «verbo desconocido» —que `f
bóveda» y se calla—. Y si `boveda` y `shuma-pregunta` no están declaradas en el perfil, la bóveda» y se calla—. Y si `boveda` y `shuma-pregunta` no están declaradas en el perfil, la
extensión se instala, conecta, manda `vault.match` y el host contesta `{"ok":true,"locked":true}` extensión se instala, conecta, manda `vault.match` y el host contesta `{"ok":true,"locked":true}`
—«cerrada», que es una respuesta EXITOSA— porque del otro lado del socket no hay nadie: el dueño —«cerrada», que es una respuesta EXITOSA— porque del otro lado del socket no hay nadie: el dueño
está sellado y en ninguna imagen. Ninguno de los cinco se ve como un error: se ven como «no anda». está sellado y en ninguna imagen. Y si la imagen no trae un sembrador de identidad, el dueño SÍ
está, abre su ventana, dice por qué no hay bóveda… y el navegador vuelve a ver `locked:true` —
correcto y para siempre, porque nadie tiene con qué desbloquearla (§7.undecies).
Ninguno de los seis se ve como un error: se ven como «no anda».
Éste NO reemplaza al guardián de metal (servidor HTTP, navegador real, login real, el diálogo de Éste NO reemplaza al guardián de metal (servidor HTTP, navegador real, login real, el diálogo de
consentimiento a la vista). Lo precede: mide lo que se puede medir sin construir nada, en un consentimiento a la vista). Lo precede: mide lo que se puede medir sin construir nada, en un
segundo, y ahorra descubrir en metal un renglón que faltaba en un JSON. segundo, y ahorra descubrir en metal un renglón que faltaba en un JSON.
── LOS TRES CONTROLES NEGATIVOS, UNO POR AFIRMACIÓN ────────────────────────────────────────── ── LOS CUATRO CONTROLES NEGATIVOS, UNO POR AFIRMACIÓN ────────────────────────────────────────
`--negative-control` saca el permiso del host de una copia en memoria y exige que esto lo detecte. `--negative-control` saca el permiso del host de una copia en memoria y exige que esto lo detecte.
`--negative-control-verbo` le manda al quinto chequeo un verbo inventado y exige lo mismo. `--negative-control-verbo` le manda al quinto chequeo un verbo inventado y exige lo mismo.
`--negative-control-perfil` saca a `boveda` de los cuatro perfiles de escritorio, también en `--negative-control-perfil` saca a `boveda` de los cuatro perfiles de escritorio, también en
memoria, y exige que el sexto lo vea. Sin ellos, un guardián que siempre dice que sí se vería memoria, y exige que el sexto lo vea. `--negative-control-sembrador` hace lo mismo con `agora-cli`
y exige que el séptimo lo vea. Sin ellos, un guardián que siempre dice que sí se vería
idéntico a uno que funciona. idéntico a uno que funciona.
Y el quinto chequeo trae además un control POSITIVO —`sct.observe`, que el host pineado tiene que Y el quinto chequeo trae además un control POSITIVO —`sct.observe`, que el host pineado tiene que
@@ -209,6 +215,97 @@ def revisar_los_perfiles(sin_esta=None):
bien(f"{perfil} declara el dueño y el diálogo ({RAIZ_CONTROL} de control)") bien(f"{perfil} declara el dueño y el diálogo ({RAIZ_CONTROL} de control)")
# ── 10. EL SÉPTIMO LUGAR: ¿hay en la imagen alguien capaz de SEMBRAR la identidad? ────────────
# El sexto lugar pregunta si el DUEÑO de la base está en la imagen. Éste pregunta lo que queda
# debajo y es lo que costó el §7.undecies: el dueño abre la bóveda con una clave derivada de la
# seed de identidad, que saca del llavero de sesión (`pacha_llavero::SEED_IDENTIDAD`). Si nadie
# la sembró, `abrir()` da `Err(boveda-cerrada)` SIEMPRE — no «todavía no la desbloqueaste»: nunca.
#
# Y el 2026-09-21, en las cuatro imágenes, no había con qué: en todo tawasuyu sólo DOS binarios
# escriben esa clave —`agora-cli identity unlock` y el onboarding `churay-welcome-runner`—, y
# `agora-cli` estaba sellado con `perfiles: []` (en ninguna imagen) y además pineado a un commit
# de junio donde el verbo todavía no existía, mientras que `churay-welcome` no tiene receta.
#
# Se comprueba en los dos planos, porque fallan distinto y los dos en silencio:
# a) el COMMIT que la receta pinea sabe sembrar (si no: «unknown subcommand», o peor, un binario
# que hace otra cosa con el mismo nombre);
# b) algún perfil de escritorio lo DECLARA (si no: sellado ≠ instalado, la lección de `foot`).
SEMBRADORES = {"agora-cli": os.path.join(ROOT, "recipes/agora-cli.toml")}
# Lo que un sembrador TIENE que hacer, leído del commit pineado y no de la memoria: guardar la
# seed bajo la constante canónica. Un `identity unlock` que no llame a esto no siembra nada.
SIEMBRA = "guardar(pacha_llavero::SEED_IDENTIDAD"
# El control POSITIVO del grep, por lo mismo de siempre: este chequeo espera ENCONTRAR, y un
# «no encontré» puede ser una ausencia o un patrón equivocado. La constante se define en
# `pacha-llavero` y tiene que estar en cualquier commit del monorepo.
SIEMBRA_CONTROL = 'SEED_IDENTIDAD: &str'
def pin_de(receta):
with open(receta, encoding="utf-8") as f:
m = re.search(r'^commit\s*=\s*"([0-9a-f]{7,40})"', f.read(), re.M)
return m.group(1) if m else None
def hay_en(commit, patron):
r = subprocess.run(
["git", "-C", TAWASUYU, "grep", "-qF", patron, commit, "--", "*.rs"],
capture_output=True,
)
return r.returncode == 0
def revisar_el_sembrador(sin_sembrador=None):
if not os.path.isdir(os.path.join(TAWASUYU, ".git")):
mal(f"no hay clon de tawasuyu en {TAWASUYU}: el séptimo lugar queda SIN COMPROBAR "
f"(se cambia con TAWASUYU=/ruta)")
return
# a) el commit pineado sabe sembrar
for nombre, receta in SEMBRADORES.items():
if not os.path.exists(receta):
mal(f"no existe {os.path.relpath(receta, ROOT)}: {nombre} no está en el corpus")
continue
commit = pin_de(receta)
if not commit:
mal(f"{os.path.relpath(receta, ROOT)} no pinea ningún commit")
continue
if subprocess.run(["git", "-C", TAWASUYU, "cat-file", "-e", f"{commit}^{{commit}}"],
capture_output=True).returncode != 0:
mal(f"el clon de tawasuyu no conoce el pin {commit[:9]} de {nombre}: "
f"hace falta un `git fetch` antes de creerle a este guardián")
continue
if not hay_en(commit, SIEMBRA_CONTROL):
mal(f"CONTROL ROTO: {SIEMBRA_CONTROL!r} tampoco aparece en {commit[:9]}. El grep mira "
f"el sitio equivocado y su «no está» no prueba nada")
continue
if hay_en(commit, SIEMBRA):
bien(f"{nombre} pineado ({commit[:9]}) SÍ siembra {SIEMBRA[:-1]}…)")
else:
mal(f"{nombre} pineado ({commit[:9]}) NO siembra la seed de identidad: la bóveda "
f"contesta `boveda-cerrada` siempre y nadie tiene con qué abrirla")
# b) alguien lo declara en cada perfil de escritorio
try:
with open(TARGETS, "rb") as f:
perfiles = tomllib.load(f)["perfil"]
except Exception as e:
mal(f"no se pudo leer {os.path.relpath(TARGETS, ROOT)} ({e})")
return
for perfil in PERFILES_ESCRITORIO:
raices = list(perfiles.get(perfil, {}).get("paquetes", []))
if sin_sembrador:
raices = [r for r in raices if r != sin_sembrador]
if RAIZ_CONTROL not in raices:
mal(f"CONTROL ROTO: {perfil} no declara {RAIZ_CONTROL}, así que lo de abajo no mide")
continue
presentes = [n for n in SEMBRADORES if n in raices]
if presentes:
bien(f"{perfil} trae un sembrador de identidad ({', '.join(presentes)})")
else:
mal(f"{perfil} NO trae ningún sembrador de identidad ({'/'.join(SEMBRADORES)}): la "
f"bóveda está declarada y NADIE puede abrirla — `locked:true` para siempre")
fallas = [] fallas = []
@@ -226,7 +323,7 @@ def leer(p):
return f.read() return f.read()
def revisar(permisos_host, verbo_extra=None, sin_perfil=None): def revisar(permisos_host, verbo_extra=None, sin_perfil=None, sin_sembrador=None):
# 1. La extensión existe y declara el id que todos los demás nombran. # 1. La extensión existe y declara el id que todos los demás nombran.
try: try:
m = json.loads(leer("extensions/boveda/manifest.json")) m = json.loads(leer("extensions/boveda/manifest.json"))
@@ -293,11 +390,15 @@ def revisar(permisos_host, verbo_extra=None, sin_perfil=None):
# 9. Y el sexto, que vive en el manifiesto de objetivo: quién la pone en la imagen. # 9. Y el sexto, que vive en el manifiesto de objetivo: quién la pone en la imagen.
revisar_los_perfiles(sin_perfil) revisar_los_perfiles(sin_perfil)
# 10. Y el séptimo: quién puede sembrar la identidad con la que esa bóveda se abre.
revisar_el_sembrador(sin_sembrador)
def main(): def main():
control = "--negative-control" in sys.argv control = "--negative-control" in sys.argv
control_verbo = "--negative-control-verbo" in sys.argv control_verbo = "--negative-control-verbo" in sys.argv
control_perfil = "--negative-control-perfil" in sys.argv control_perfil = "--negative-control-perfil" in sys.argv
control_sembrador = "--negative-control-sembrador" in sys.argv
host = json.loads(leer("native-messaging/puriy_costura.json")) host = json.loads(leer("native-messaging/puriy_costura.json"))
permisos = list(host.get("allowed_extensions", [])) permisos = list(host.get("allowed_extensions", []))
verbo_extra = None verbo_extra = None
@@ -315,9 +416,15 @@ def main():
# lo ve faltar, está contestando que sí en vez de mirar `targets.toml`. # lo ve faltar, está contestando que sí en vez de mirar `targets.toml`.
sin_perfil = "boveda" sin_perfil = "boveda"
print(f"⚠ control negativo: se saca {sin_perfil} de los perfiles de escritorio") print(f"⚠ control negativo: se saca {sin_perfil} de los perfiles de escritorio")
sin_sembrador = None
if control_sembrador:
# El sembrador, sacado de la misma copia en memoria: si el séptimo chequeo no lo ve
# faltar, está contestando que sí en vez de mirar quién puede abrir la bóveda.
sin_sembrador = "agora-cli"
print(f"⚠ control negativo: se saca {sin_sembrador} de los perfiles de escritorio")
print(f"── la bóveda en atuq ──") print(f"── la bóveda en atuq ──")
revisar(permisos, verbo_extra, sin_perfil) revisar(permisos, verbo_extra, sin_perfil, sin_sembrador)
if control: if control:
if any("allowed_extensions" in f for f in fallas): if any("allowed_extensions" in f for f in fallas):
@@ -337,6 +444,12 @@ def main():
return 0 return 0
print("\n✗ el control del perfil NO lo detectó: el sexto chequeo no mide nada") print("\n✗ el control del perfil NO lo detectó: el sexto chequeo no mide nada")
return 1 return 1
if control_sembrador:
if any("sembrador de identidad" in f and "NO trae" in f for f in fallas):
print("\n✓ el control negativo del sembrador lo detectó")
return 0
print("\n✗ el control del sembrador NO lo detectó: el séptimo chequeo no mide nada")
return 1
if fallas: if fallas:
print(f"\n{len(fallas)} problema(s)") print(f"\n{len(fallas)} problema(s)")
return 1 return 1