Commit Graph
622 Commits
Author SHA1 Message Date
SergioandClaude Opus 5 9db1845f22 claude abre DONDE ESTÁS PARADO, no en /opt/takana
Reportado tal cual: «entro al repo y ejecuto claude, pero claude sigue estando en /opt/takana y no me
agarra el repo». Era literal: el lanzador tenía el destino cableado a /opt/takana, así que desde
/work/sergio/takana abría el árbol del root — y encima con la memoria de ESE proyecto, porque la
memoria del agente se indexa por ruta de trabajo.

Ahora el defecto es $PWD; PROY sigue ganando cuando se lo pasa a propósito, y si la ruta no existe
cae a /opt/takana. Probado con un doble del binario en los tres casos.

Y el `cd` de adentro ahora falla ruidosamente: la jaula sólo monta lo concedido, así que desde una
ruta no concedida no hay nada que abrir. Antes eso se lo tragaba un `&&` y el agente arrancaba en `/`
sin decir por qué.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:33:54 +00:00
SergioandClaude Opus 5 f32f2f3543 el mapeo por rango: se intentó cerrar y NO alcanzó — queda dicho dónde para
Se hicieron las dos cosas obvias: rango para sergio en /etc/subuid y /etc/subgid, y los ayudantes
newuidmap/newgidmap en setuid root y además con capacidades (cap_setuid=ep). El error SE MOVIÓ —ya no
dice «no hay rango para sergio»— pero sigue sin mapear: «newuidmap: open of uid_map failed:
Permission denied».

Lo descartado, medido: el setuid SÍ eleva en esta caja (sonda en C compilada con el cc nuevo:
uid=1001 euid=0), / no está nosuid, y el uid_map del proceso es suyo (-rw-r--r-- sergio:sergio). O
sea que no es ni el rango que faltaba ni el bit que no tomaba. La causa de fondo queda SIN
DETERMINAR, y se escribe así en vez de inventarla.

De paso, un detalle que cuesta un rato si no se sabe: `chown` BORRA el bit setuid, así que hay que
chownear primero y ponerlo después. La primera sonda salió sin bit por ese orden y parecía que el
setuid no funcionaba en la caja.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:25:54 +00:00
SergioandClaude Opus 5 ff871f4a07 dar de alta una persona: casi es adduser, y lo que falta es lo que tumbó la caja
Respuesta medida a «¿agregar un usuario es más complicado que un simple adduser?». `adduser` está —es
el de busybox— y crea la cuenta. Lo que no alcanza es DÓNDE queda la contraseña: esta caja NO USA
/etc/shadow, el hash vive en el campo 2 de /etc/passwd, y el shadow que hubo está aparcado como
/etc/shadow.el-que-bloqueo-root. El nombre no es broma: crear un /etc/shadow con las cuentas
bloqueadas dejó a sshd sin aceptar ni la clave de root, y la caja se quedó sin administración con los
ocho dominios caídos ~20 minutos. Un adduser a secas puede crear ese fichero sin avisar.

Así que el guion crea la cuenta, deja el hash donde el login lo lee, y APARTA cualquier /etc/shadow
que el alta haya creado donde no había. El control es el que importa: fotografía quién podía entrar
ANTES y falla si alguien perdió su contraseña.

Y hace lo que hace falta para tener poder de verdad: sudoers.d (el sudo de acá sí es setuid; doas no
funciona), rango en /etc/sub{u,g}id con los ayudantes newuidmap/newgidmap en setuid —sin eso un
userns anidado no mapea uid 0 y takana build no corre dentro de la jaula—, ssh, y la instancia
claude-<u> con `root = false`, porque si no el CLI se ve uid 0 y rechaza --dangerously-skip-permissions.

Probado de punta a punta con un usuario desechable y borrado después sin dejar residuo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:20:55 +00:00
SergioandClaude Opus 5 3dc4424597 la jaula que te mapea a root rompe al propio agente
`claude --dangerously-skip-permissions` moría en la caja con «cannot be used with root/sudo
privileges»: la instancia de sergio traía `root = true`, que mapea el uid del invocante a 0 ADENTRO
(qorpa.rs:1533), así que el CLI se veía a sí mismo como root. Con `root = false` queda uid 1001 y la
bandera se acepta — comprobado: `claude --dangerously-skip-permissions --version` contesta.

De paso, a esa instancia le faltaba `/store` en `rw` —la de root ya lo tenía—: sin eso el agente lee
el store y no puede sellar nada. Ahora escribe (probado y limpiado).

Y se corrige el aviso sobre los builds, que ahora falla por otro motivo: con root=false el bwrap
anidado SÍ se levanta y /proc monta, pero no puede mapear el uid 0 que takana build pide, porque
sergio no tiene rango en /etc/subuid y newuidmap/newgidmap no son setuid en esta caja. Cerrarlo es
una decisión con precio (un setuid más, y el /usr/bin de acá está hidratado del store), no un
trámite; mientras tanto el build se le pide al anfitrión por ssh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:03:17 +00:00
SergioandClaude Opus 5 984ebdf58f la subpágina de humanoid se publica con rsync, y queda dicho por qué
El resto de los sitios se publican con `git pull` porque Caddy sirve de los clones. Con humanoid no
se puede: su repo es el proyecto Android y lo que se sirve son APK ya construidos más su índice —
salida de release que no vive en ningún repo, y que para regenerarse pide Android SDK + Java, que la
caja no tiene. Hasta que la caja construya Android, esto es un puente declarado, no el modelo.

`--checksum` a propósito: los APK se reconstruyen con el mismo tamaño y otra fecha, y un rsync por
fecha subiría 91 M cada vez; medido hoy, lo que de verdad cambió fueron tres ficheros (index.html,
en/index.html y rizoma-launcher.apk) y los otros nueve sólo tenían la marca de tiempo distinta. Y no
lleva --delete: borrar a ciegas en lo que se está sirviendo no es publicar, es perder.

El control pregunta al SITIO, no al rsync —que "salió bien" y una página vieja conviven sin ruido— y
comprueba además un APK, porque el índice puede estar bien y la descarga rota: es lo que la gente se
lleva.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 14:57:48 +00:00
SergioandClaude Opus 5 bd8f9fbbb2 atuq: la bóveda ANDA hasta la etapa D — y con el navegador abierto quedaba muda por un tercer fallo
Con el arreglo de llimghi sellado, el guardián de metal pasó de la A a la D:

    A ✓ sin la app dueña, el host contesta locked:true (y con ok:true, que es la trampa)
    B ✓ con la app, vault.status dice ABIERTA — el socket sube en 1 s
    C ✓ el diálogo ABRE en el compositor y se lo CONTESTA: «1» ⇒ yes, «2» ⇒ no (los dos sentidos)
    D ✓ vault.save con consentimiento real guarda, y vault.match la encuentra SIN contraseña
    E ✗ el navegador pide la contraseña y no llega nunca

La E destapó un fallo que no es de atuq ni de llimphi: el dueño de la bóveda atendía de a UN cliente
—`atender_cliente` no vuelve hasta que el cliente se va, y se lo llamaba en el hilo del accept—, así
que la primera conexión se quedaba con él mientras viviera. Y el caso normal es ése: Gecko lanza un
`puriy-costura` por PUERTO, ocho en atuq, y todos conectan al arrancar. La extensión de la bóveda
mandaba `vault.match` y no volvía nunca: sin insignia, sin log y sin error, indistinguible de «este
sitio no tiene contraseñas».

Control sin navegador, en los dos sentidos: un solo cliente contesta en milisegundos; con otro host
conectado y quieto, la misma pregunta queda colgada y la mata el timeout a los 30 s.

Arreglado en tawasuyu (`cf3540460`, un hilo por conexión; los diálogos los serializa ahora el Mutex
de la bóveda) con su test de regresión, que además cazó que el arreglo obvio —clonar el Dueno— borra
el socket en el Drop del primer hilo que termina. Los siete tests que ya había no podían ver el fallo:
abrían un cliente por vez, que es justo lo que producción nunca hace.

⚠ Y subir el pin volvió a chocar con el `Cargo.lock` abierto de tawasuyu. Lo que importa para la
próxima es CUÁL operación lo cierra: `cargo metadata` sin `--locked` da **1 línea** de diff y cero
checksums movidos; `cargo generate-lockfile` da 6.305 líneas y 750 checksums, e invalidaría el
vendoreo de todos. Publicado en `b80f7567c` con índice temporal (estaba MM).

Del lado del guardián, tres cosas que salieron de fallar:
· el contestador INSISTE hasta que la ventana se va — con el mismo `wtype rc=0`, una corrida
  contestaba y otra volvía `denied`: la ventana entra al árbol del compositor antes de que llimphi
  tenga el teclado enganchado, y un sleep más largo sólo mueve el borde;
· el contestador busca la ventana POR PATRÓN: con el navegador abierto, teclear «la enfocada» le
  contestaría a la página, y eso es un sí que nadie dio;
· la sonda del chrome mira `isShownForTab` y la insignia ANTES de apretar — `triggerClickOrPopup`
  sale por la puerta de atrás si la acción no está mostrada, y un click que no llega se ve igual que
  una bóveda que no contesta. (Y `WebExtensionPolicy` no es global en el scope de una ventana: sale
  del módulo. Eso costó una corrida.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 14:52:50 +00:00
SergioandClaude Opus 5 69a93a9d0d publicar los sitios pasa a ser git pull: Caddy sirve de los clones, no de copias
La mudanza dejó los sitios como copias a mano en /work/www —4,2 G del monorepo sólo para
tawasuyu.net— sin ninguna forma de refrescarlas. El contenido estaba bien y el modelo era frágil: el
día que cambia el repo la web no se entera, y eso no se nota, porque un sitio viejo responde 200
igual que uno nuevo.

Ahora Caddy sirve directo de los clones: tawasuyu.net del monorepo y gioser.net/takana/hifas del
clon de gioser-web. Los medios que NO están en git —los APK de humanoid, los 265 M de muestras,
vivo, ende— siguen en /work/www con su propio `root`, que es la separación correcta. Liberados 4,2 G.

El guion hace `pull --ff-only` (no pisa cambios locales de un árbol que está sirviendo), regenera las
dos páginas hermanas —hermanas.py trae el destino cableado a una ruta de gioser, así que se le pasa
el del clon sin tocar el fichero de ese repo— y después COMPRUEBA: que lo servido sea byte a byte lo
que hay en disco, que los medios sigan en 200, y que /.git/config dé 404, porque servir desde un clon
no puede exponer el repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 14:49:04 +00:00
SergioandClaude Opus 5 41b21755b4 en la caja /etc/profile.d no lo leía nadie: /etc/profile no existía
El guion dejaba las RUSTFLAGS en /etc/profile.d/rust-enlazador.sh y ninguna shell de login las
recogía, porque en esta caja /etc/profile NO EXISTE. O sea que el fichero habría sido exactamente la
avería que este repo persigue: presente, con contenido, y sin ningún efecto. Lo descubrió el control
nuevo, que pregunta por la VARIABLE en una shell de login en vez de por el fichero.

Se crea /etc/profile con el bucle estándar sobre profile.d —de paso revive el bash_completion.sh que
ya estaba ahí, igual de mudo— y no se toca el PATH, que lo fija el init.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 13:13:46 +00:00
SergioandClaude Opus 5 69ed6e53c0 la caja no tenía NINGÚN driver de C: se lo damos con el zig que ya está en su store
`command -v cc gcc clang zig` en la caja no devolvía nada. Eso no es sólo incómodo: es lo que hacía
que rustc, con el compilador propio ya sellado, no pudiera enlazar una sola dylib —y ahí entran todas
las proc-macros—. El `-L /usr/lib` obvio empeora las cosas: rompe los estáticos, porque rustc enlaza
musl static-PIE con su copia self-contained y ese camino le hace preferir la libc.a del sistema, que
no es PIC. El camino de librerías no puede ser global; quien sabe cuál es cuál es un driver de C.

NO introduce nada nuevo en el corpus: envuelve el `seed-zig` que ya está en el store —producto del
bootstrap, como stage1-rootfs—. Volver a zig el compilador de C OFICIAL de la distro sería otra cosa,
y es decisión de ADR. La caché de zig va a /work y no a la raíz de 5,9 G, que ya nos costó una caída.

Controles que corre el propio guion, sobre el HECHO y no sobre la intención: compila y ejecuta un
hola mundo en C; enlaza una proc-macro, compila el programa que la usa y lo ejecuta; y —el que evita
el remedio peor que la enfermedad— comprueba que el estático SIN banderas siga enlazando.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 13:11:57 +00:00
SergioandClaude Opus 5 2fafb3a589 atuq: el vigésimo guardián entra a la suite, y entra TRUNCADO — con el motivo escrito al lado
`test-atuq-boveda-metal --hasta A` pasa a correr con los otros diecinueve. Corre truncado a propósito:
las etapas B..F chocan contra el muro del §7.septies —ninguna app llimphi de escritorio abre ventana
sin Vulkan— que no es de la bóveda, y un rojo permanente por una causa ajena se aprende a ignorar en
tres días. Lo que la A afirma es la medición que costó tres semanas de no ver: **sin la app dueña, el
host contesta `locked:true` con `ok:true`**, o sea que «cerrada» es una respuesta exitosa.

La cabecera del runner dice ahora 20 y avisa que un verde de éste NO dice que la bóveda ande. Es la
misma advertencia que ese fichero ya se aplicó dos veces: un número escrito como predicción envejece
callado, y un cuadro entero en verde puede ser un producto sano o un runner que no mide nada.

Probado en los dos sentidos, que es lo único que distingue un guardián de una decoración:
· en verde, 3 s, dentro del runner y sobre el sello vigente (`b3:e556024b`);
· ROJO con una rotura a propósito —invertir la aserción de la etapa A— ⇒ salida 1.

Y el tamaño que faltaba para la decisión del §7.sexies: `boveda` 22 M y `shuma-pregunta` 21 M, ~43 M
por perfil. Hoy la respuesta a «en qué imágenes se declaran» es NO, y no por el tamaño: declararlas
antes de que llimphi se destrabe sería instalar una bóveda que niega todo sin un error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 10:50:18 +00:00
SergioandClaude Opus 5 f611751f86 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>
2026-09-18 10:48:06 +00:00
SergioandClaude Opus 5 fca1de2e55 yupana: radio mezclaba las aristas de build con las de runtime y sobreestimaba el coste
`radio gcc-libs` contestaba «9 dependientes, 9 sellados caen a deuda», y con ese número avisé de una
cascada de horas —firefox con PGO dos veces, waterfox, atuq—. La respuesta correcta es CERO: los
cinco declaran gcc-libs en `deps.runtime`, y **`runtime` no entra en `hash_inputs`**, así que
re-sellarla no re-hashea a nadie. Se vio al mirar el store después del cambio: los cinco seguían al
día.

El grafo de `_deps` une build+runtime a propósito —eso es el CIERRE de una imagen, y esa unión
arregló en su día que firefox arrancara sin libstdc++—, pero `radio` responde otra pregunta: «¿a
quién obligo a reconstruir?». Ahora usa dos grafos: la cascada sale del de BUILD, y las imágenes del
de la unión, que es lo correcto para cada uno. Los que dependen sólo por runtime se siguen
informando, nombrados, diciendo que NO se reconstruyen.

Controles: gcc-libs ⇒ 0 de cascada y 9 sólo-runtime; zlib ⇒ 126 directos y 391 transitivos (sigue
viendo las de verdad); zsh ⇒ 0. Y `test-yupana-radio.py` pasa: libdrm sigue cruzando las 5 colas.

Un número que sobreestima frena arreglos correctos, que es justo lo que casi pasa acá.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:48:35 +00:00
SergioandClaude Opus 5 54283b8a94 la cosecha decía «reintenta próximo ciclo» y no reintentaba nada
Si el push del estado se rechaza porque otro agente empujó primero, el ciclo siguiente se rechaza
IGUAL —nadie rebasa nunca— y el commit del cron se queda local para siempre. Se encontró en la caja:
rama divergente con un `estado: cosecha granja` atascado y la cosecha diciendo «reintenta» cada media
hora sin que nada cambiase. El mensaje tranquilizaba y no era cierto.

Ahora, cuando lo rechazan, llama a `git-sincronizar.sh`, que es la regla 2 ter automatizada: rebasa,
COMPRUEBA por parche que el commit sobrevivió, lo recupera del reflog si no, y empuja. Y si tampoco
puede, lo dice y deja el comando para verlo, en vez de prometer un reintento que no existe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 21:33:58 +00:00
SergioandClaude Opus 5 5d16b55e14 el visor del grafo reventaba con un estado que no conocía
`build-state-view.py` traía los cuatro estados del corpus cableados (sealed/debt/never/unhashable) y
contaba TODOS los nodos contra ese diccionario. `build-state.py` emite dos más —`wanted` (hueco
declarado en targets.toml, sin receta) y `ajeno` (imagen de qorpa, que no se construye desde
fuente)—, así que el visor moría con `KeyError: 'ajeno'` y el tablero HTML llevaba sin poder
generarse. O sea: agregar un estado nuevo rompía el tablero entero, en silencio para quien no lo
corriera.

Ahora los de fuera del corpus se cuentan aparte y se NOMBRAN en la leyenda —no se cuelan a la barra,
que es sobre `totals["recipes"]` y pasaría del 100 %—, y cualquier estado que aparezca mañana cae en
esa misma rama en vez de tumbar el visor.

De paso, el grafo regenerado: 930 selladas, 2 en deuda, 5 nunca, 3 ajenas; git ya figura sellado en
su hash nuevo (b3:215742cb…) y no arrastró a nadie, como decía el radio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 21:00:40 +00:00
SergioandClaude Opus 5 d4896c2999 pagada la deuda: el git del corpus vuelve a clonar por HTTPS
El §6.50 quinquies dejó la caja trayendo fuentes por SSH porque su git no tenía remote-https. Eso era
el síntoma; la causa estaba un paso antes de donde mirábamos —incluida la nota que yo mismo había
escrito, que culpaba al Makefile—: el `configure` de git prueba libcurl con AC_CHECK_LIB, que enlaza
`conftest.c -lcurl` y nada más, y con libcurl estática faltan sus privadas (-lssl -lcrypto -lz).

El test falla, config.mak.autogen se lleva NO_CURL=YesPlease, el Makefile excluye los tres helpers y
conserva git-http-backend —esa asimetría en el artefacto era la firma, y estuvo a la vista desde el
14 de septiembre—. El build termina en verde.

Medido sobre el artefacto: b3:215742cb… trae git-remote-http, -https, git-http-fetch y los ftp, y
CLONA por HTTPS (clone, no ls-remote). En la caja, hidratado y con la reescritura apagada, funcionan
tanto `git clone` como `git clone --mirror`, que es el comando exacto del fetch de takana.

Y el radio, medido en vez de temido: la nota vieja decía «raíz de perfil.base ⇒ radio grande»;
`yupana radio git` dice cero dependientes directos y cero transitivos. El miedo escrito a mano había
sobrestimado el costo de arreglarlo, y eso lo mantuvo roto tres días.

Las reescrituras url.insteadOf quedan retiradas de la caja; el guion se conserva como salida de
emergencia y así lo dice su cabecera.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:52:47 +00:00
SergioandClaude Opus 5 0bb40af496 la caja trae fuentes git por SSH: el arreglo, en un guion idempotente
Lo que hasta ahora era estado a mano en la caja queda reproducible. Hace las cinco piezas medidas:
reescritura de URLs con url.insteadOf, la clave del host reexpresada SIN puerto (cargo la busca así),
la huella de github.com PINEADA contra la publicada —si no coincide, sale con error en vez de
aceptarla—, la IdentityFile por host, y `net.git-fetch-with-cli = true` para que cargo delegue en el
git de verdad. Además mueve CARGO_HOME fuera de la raíz de 5,9 G, que es lo que tiró la caja.

No mueve ningún hash: la URL es un localizador y no entra en hash_inputs (ADR 0013). Termina con un
control que CLONA de los dos orígenes; si alguno no llega, sale distinto de cero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:36:03 +00:00
SergioandClaude Opus 5 e7b77ff657 la cuenta de sergio en la caja, su jaula propia, y shuma corriendo como él
Pedido: «crea mi cuenta sergio con la clave tawasuyu, desde ahí abrir los claudes como lo estoy
haciendo acá con shuma sobre los proyectos».

  · cuenta `sergio` uid 1001 (el mismo que en gioser), home /home/sergio, clave verificada
  · su propia jaula `claude-sergio`, que concede SU home —su `.claude`: credenciales y memoria— y
    NO el de root. Con la instancia de root, sergio moría en «bwrap: Can't mkdir /root/.claude»
  · `claude` elige instancia por usuario; `PROY=/otra/ruta claude` abre el agente sobre otro proyecto
  · shuma-daemon y shuma-gateway pasan a correr como `sergio`, no como la cuenta de servicio `shuma`:
    shuma es su CONSOLA, y cada pestaña que abre tiene que ser una sesión suya — si no, `claude`
    buscaría una jaula `claude-shuma` que no existe

⚠ NO se eleva con doas/sudo: en esta caja el setuid NO TOMA («doas: Operation not permitted», con
NoNewPrivs 0 y `/` sin nosuid — queda como deuda entender por qué). No hace falta: `qorpa run`
levanta la jaula con user namespaces y anda sin privilegio, siempre que el directorio de la
instancia sea del usuario.

⚠ Y un detalle que costó una vuelta: sin `shift 2`, el HOME y el proyecto que el lanzador pasa como
posicionales se cuelan COMO ARGUMENTOS del agente — la primera sesión se abrió con el prompt
«/home/sergio», y el agente contestó, con razón, que eso es una ruta y no un comando.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:31:30 +00:00
SergioandClaude Opus 5 d6ba65e23c el lanzador arranca EN el repo: sin ese cd, el agente empieza sin memoria
La memoria se indexa por ruta de trabajo. `qorpa run` deja el shell en `/`, así que las primeras
pruebas usaron el proyecto `-` y el agente arrancaba en blanco aunque sus 163 ficheros estuvieran en
disco. Con `cd /opt/takana` usa `-opt-takana`, que es donde se renombró la memoria traída de gioser
(allá el repo era /mnt/vvv/takana).

Comprobado en la caja, con dos preguntas que sólo puede contestar leyendo:

  ¿el logo?    → «martillo 7×7, cuatro colores exactos» (está en su memoria)
  ¿la regla 1? → «todo takana build va envuelto en flock, y con -o», con el matiz del lock heredado

O sea: el agente de la caja arranca sabiendo lo que sabe éste.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 17:36:41 +00:00
SergioandClaude Opus 5 eb96c9219e el agente corre en la caja y puede construir — la condición que el usuario puso para borrar gioser
$ claude -p "Decí exactamente: CLAUDE CORRIENDO EN LA CAJA"
  CLAUDE CORRIENDO EN LA CAJA

Turno real —API, credenciales, respuesta— desde 2.29.29.217. El CLI es un ELF glibc de 360 M y la
caja es musl, así que va enjaulado (ADR 0015, el caso de sergioh-api). La instancia declara red,
nesting y SEIS directorios: /opt/takana, /work/dev-fs, /store, /root/.claude, /root/.ssh (ro) y el
CLI (ro). Es ANCHA y está dicho: un agente que construye, commitea y empuja necesita lo mismo que un
humano; lo que la jaula compra es que el alcance esté escrito y acotado.

⚠ El `takana` que construye NO se instala adentro: sale del store, que ya está concedido — es
estático musl y corre igual dentro de una imagen glibc.

LA CAJA CONSTRUYE: sellados hoy `intel-ucode` (156 signatures) y `sof-firmware` (2 firmware + 8
topologías). Faltaba una pieza que el lab no traía: `.dev-fs/tools` (zig 0.13/0.16 y go, 1 G) — la
imagen del lab empaqueta `alpine/` y nada más, y el primer intento murió con «zig no encontrado».

🧱 Y lo que NO anda, con su rodeo: `takana build` dentro de la jaula muere con «bwrap: Can't mount
proc on /proc» aunque `nesting = true`. No es el anidamiento —un `bwrap --proc /proc` a mano SÍ
funciona adentro— sino el userns anidado del sandbox del build; probado también con `root = false`,
que es la sospecha que el propio código documenta, y falla igual. El rodeo que sí anda, escrito en el
lanzador: pedirle el build al ANFITRIÓN por ssh a 127.0.0.1:22022. Así se selló sof-firmware desde
dentro de la jaula.

Manifiesto y lanzador versionados en scripts/servidor/: una instancia que sólo existe en la caja se
pierde con la caja.

⚠ Pendiente menor: la memoria del agente está en `-mnt-vvv-takana` y allá el repo vive en
/opt/takana ⇒ el índice POR RUTA no coincide y arranca sin memoria del proyecto aunque los 733
ficheros estén en disco.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 17:34:56 +00:00
SergioandClaude Opus 5 597406ad70 git-sincronizar: gritaba «el commit desapareció» SIEMPRE — era pipefail + SIGPIPE, no el rebase
El guión avisaba en cada corrida de que el commit se había perdido, iba a recuperarlo, y el
cherry-pick le contestaba que el contenido ya estaba. Un guardián que grita siempre se ignora, así
que valía la pena encontrar por qué.

No era git: era la comprobación. El guión corre con `set -o pipefail` y comprobaba con
`git log … | grep -qF "$MSG"`. **`grep -q` sale en cuanto encuentra**, eso le manda SIGPIPE a
`git log`, que termina ≠0, y con `pipefail` la tubería entera se lee como FALLO **aunque el grep haya
encontrado**. Demostrado en una línea:

  $ bash -c 'set -o pipefail; seq 1 100000 | grep -q "^5$" && echo OK || echo FALLO'
  FALLO

Arreglado capturando la salida antes de comparar: sin tubería, sin SIGPIPE, sin falso positivo.

⇒ Y de paso desmiente lo que el propio guión daba por hecho: de las cinco «pérdidas» de commit que
motivaron escribirlo, al menos las últimas dos fueron ESTE falso positivo, no el rebase. Las
primeras sí están en el reflog con su `(start)`/`(finish)` sin `pick`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:55:42 +00:00
SergioandClaude Opus 5 b985570788 respaldo: un parcial de SÓLO LECTURA envenena esa ruta para siempre — y el bucle lo llamaba «red»
La primera corrida completa desde la caja (puerta 7) se cayó 40 veces seguidas con el MISMO fichero:

  open ".../fonts/dejavu/.rsync-partial/DejaVuSans-Oblique.ttf" failed: No such file… (2)
  ..  store: corte de red (rsync 23). Intento 39; reanudo en 60s.
  !!  store: 40 intentos y sigue cayéndose. Dejo lo subido y paro.

No era red. Una transferencia interrumpida el 11-sep dejó ese PARCIAL en el destino con modo
-r--r--r-- (el del origen: el store es de sólo lectura por diseño), y ningún intento posterior puede
reescribirlo. El fallo es ETERNO, y el bucle reintentaba cada 60 s contra esa pared — exactamente lo
que la cabecera de la función de reintento dice que no hay que hacer.

Dos arreglos:

1. `--chmod=Fu+w`: los ficheros del respaldo quedan escribibles por su dueño, así que un corte a
   mitad se RETOMA en vez de trabarse. El precio es no conservar el bit de sólo-lectura en la copia;
   es bajo: el contenido es lo que se respalda y los permisos los reconstruye el store.

2. Al rsync 23 se le dan TRES intentos, no cuarenta, y al tercero se NOMBRA la causa probable (un
   parcial de sólo lectura en el destino) en vez de llamarlo corte de red. El 23 es «some files were
   not transferred», que tanto puede ser un cable como una pared.

Comprobado: borrado el parcial envenenado, ese fichero sube a la primera.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:55:01 +00:00
SergioandClaude Opus 5 615cc2f839 git-sincronizar: un cherry-pick VACÍO no es un fallo, es que el contenido ya estaba
Estrenando el guión con su propio commit saltó el caso: la comprobación por mensaje dio negativo,
el cherry-pick de recuperación salió con «the previous cherry-pick is now empty» y el guión abortó
con error… teniendo el contenido ya en el árbol.

Ahora distingue los dos casos: vacío ⇒ falso positivo de la comprobación, se aborta el cherry-pick y
se sigue; cualquier otro error ⇒ se aborta, se imprime el reflog y se sale ≠0 para que lo mire un
humano. Lo que importa es que el CONTENIDO esté, no que el sha coincida.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:07:25 +00:00
SergioandClaude Opus 5 17b6ea1a4f git-sincronizar.sh: la regla 2 ter automatizada — porque hoy hizo falta cinco veces
En dos días, `git pull --rebase` se llevó por delante CINCO commits propios en este árbol
compartido, siempre con el mismo reflog: `commit:` mío, `(start): checkout <otro>`, `(finish):
returning to main`, y ni un `pick` en el medio. El comando no es el culpable —en un repo de juguete
reaplica bien— sino que alguien mueve `refs/heads/main` mientras el rebase está en vuelo.

La receta manual (CLAUDE.md regla 2 ter) funciona: mirar el log y recuperar con reflog +
cherry-pick. Esto es esa receta automatizada, porque **una regla que hay que acordarse de aplicar en
cada push es una regla que un día no se aplica**.

Hace, en orden: anota su HEAD · rebasa · COMPRUEBA que el commit sigue en la rama —por su mensaje,
no por el sha, que el rebase reescribe— · si no está lo recupera del reflog · empuja · verifica que
gitea quedó donde tiene que quedar · y si el espejo de GitHub quedó divergente lo realinea con
`--force-with-lease` (nunca `--force` a secas) y sólo tras comprobar que su contenido ya está en
nuestra historia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:06:44 +00:00
SergioandClaude Opus 5 80e04872d7 poda-fuentes: no corría en takana — y la causa era que la caja NO TIENE /dev/fd
El latido, corrido desde la caja, moría así:

  poda-fuentes.sh: line 72: /dev/fd/63: No such file or directory
  poda-fuentes.sh: line 73: viejos: unbound variable
  ⚠ poda de fuentes falló (rc=1) — sigo

El segundo error es consecuencia del primero (el `mapfile` nunca corrió), y el primero no nombra la
causa: **la sustitución de procesos de bash necesita `/dev/fd`, y una caja takana no lo tiene**.
gioser sí: `/dev/fd -> /proc/self/fd`. O sea que NINGÚN `<(...)` de NINGÚN script del hub funcionaba
allá — esto era sólo el primero en toparse.

Dos arreglos, y los dos hacían falta:

1. En el script: `<(...)` fuera (fichero temporal, que anda en cualquier shell y con cualquier /dev)
   y `df -B1 --output=avail`, que es GNU, por `df -k` + awk. Es la poda de `work/sources`, justo lo
   que un hub necesita para no llenarse.

2. En la caja: `/dev/fd -> /proc/self/fd`, con una Card OneShot en el genesis para que sobreviva al
   reinicio (como la de `hostname`). Comprobado después: `bash -c "cat <(echo funciona)"` responde.
   Lo correcto a futuro es que lo haga el init o el product-rootfs, no una Card — queda anotado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:56:04 +00:00
SergioandClaude Opus 5 1ca210a311 store-gc: xargs -d tampoco existe en busybox — el arreglo anterior imprimía «huérfanos ?»
El primer intento cambió `du --files0-from=-` (GNU) por `xargs -d '\n' du -sk`… y `-d` también es
extensión GNU. En la caja quedó «espacio: superados 0 · huérfanos ?». Va con `tr '\n' '\0' | xargs
-0`, que busybox sí tiene, y se probó la función suelta en las dos máquinas.

Lo que SÍ funcionó del intento anterior es el `?`: cuando el total no se puede calcular, la función
lo DICE en vez de imprimir 0. Un cero inventado habría dicho «no hay nada que ganar» justo cuando
había 150 huérfanos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:19:59 +00:00
SergioandClaude Opus 5 4fe6046200 store-gc: decía «espacio: superados ·» y «libres: Available → Available» — dos bugs que salen en takana
Corrido por primera vez en la caja (una máquina busybox, no GNU), el recolector funcionó —337
artefactos borrados y verificados, 26,8 G liberados— pero sus dos NÚMEROS salieron vacíos:

  ==> espacio: superados  · huérfanos
  ==> 337 artefactos borrados y VERIFICADOS (0 sobrevivientes) · libres: Available → Available

1. `du -sch --files0-from=-` es de GNU coreutils y busybox NO lo tiene ⇒ `espacio()` devolvía vacío.
   Un número que falta se lee como un número chico: sin él nadie puede decidir si vale la pena
   correrlo, que es justo para lo que está el dry-run. Ahora `xargs du -sk` + awk, que anda en los
   dos mundos.

2. `df -h /home` estaba CABLEADO, y el store casi nunca vive ahí: en gioser es un bind-mount del
   volumen y en una caja takana es `/store`, su propia partición. En la caja no hay `/home`, así que
   `tail -1` se quedó con la CABECERA y el resultado fue «Available → Available». Se mide `$STORE`.

Medido de verdad con `df` a mano: /store pasó de 84,7 G usados (91 %) a 57,9 G (62 %) — 26,8 G
liberados, 1657 → 1320 artefactos. Y el control que importa después de un gc: los 11 enlaces de
`/usr/bin` que apuntan DENTRO del store siguen resolviendo y los 21 entes siguen corriendo.

⚠ Vale anotar la advertencia que el propio gc imprime y que en esa caja no se puede satisfacer: «sin
.config legible ⇒ NO se protege ningún kernel por esta vía». El default (sólo superados) no toca lo
vigente, pero `--huerfanos` en una máquina que arranca de su store hay que pensarlo dos veces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:19:22 +00:00
SergioandClaude Opus 5 2271cc3c10 el latido se pone al día antes de commitear — si no, late y no publica, para siempre
`cosecha-cron.sh` commitea el estado y lo empuja, pero nunca hacía `pull`. En el hub no se notaba:
el árbol lo mantiene al día el agente que trabaja ahí. Al mover el latido a una caja donde NO hay
nadie trabajando —que es justo lo que pide la puerta 4— el repo se queda atrás en el primer push
ajeno y a partir de ahí TODOS los ciclos fallan el push, cada uno anotando «reintenta próximo
ciclo». Un latido que late y no publica.

`git pull --ff-only`, y nunca `--rebase`: este script corre en un árbol COMPARTIDO con otros agentes
y ahí un `pull --rebase` ya se llevó un commit por delante (regla 2 ter). Fast-forward no reescribe
nada: o adelanta limpio, o falla sin tocar el árbol y se anota en el log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:52:00 +00:00
SergioandClaude Opus 5 1c237f9da2 mudanza: decidir SIN teclas unitarias, /mnt/vvv entra al censo, y 25 rutas de secretos más
Tres cosas que salieron de usar la herramienta de verdad.

1. `--decidir <fichero>`: decidir EN LOTE desde `<ruta-o-nombre> <decision>` por línea.

   El usuario: «no sé cómo activar o desactivar cosas aquí desde shuma llimphi remoto, que las
   opciones son con teclas unitarias». El modo interactivo lee una tecla por entrada, y hay
   terminales donde eso no se puede usar — una herramienta cuya única forma de decidir exige un
   tipo de terminal no es una herramienta, es una herramienta PARA ESA TERMINAL. El fichero anda en
   cualquiera, se revisa antes de aplicarlo, se versiona y se vuelve a correr.

   Una clave que no empareja con nada, o que empareja con varias, es ERROR RUIDOSO y no se escribe
   NADA: un lote a medias deja decidido lo que nadie revisó.

2. `/mnt/vvv` entra a las raíces del censo. No estaba, y ahí vive el trabajo: los repos de la
   persona, el monorepo, el store, work/sources. El agujero se vio preguntando por `humanoid`: el
   censo sólo conocía `/home/sergio/humanoid` —200 M de `build/` de junio— mientras el proyecto de
   verdad, con su `.git`, estaba en `/mnt/vvv/humanoid`, fuera de toda raíz censada. `repos_git` SÍ
   lo miraba: una parte del censo conocía el árbol y la otra no.

3. 25 rutas más en `rutas-fuera-de-git.txt`, del barrido que hizo el frente tawasuyu sobre su home:
   `~/.wawa/seeds` (sin él ninguna app nueva del génesis nace), `~/.tejido` (la identidad de esta
   máquina en la flota), `~/.local/share/agora`, `~/.config/{wawa,minga,thasnuna,shuma,mirada,hcloud}`,
   `~/keys` y `~/fdroid` (⚠ firma de apps Android), `~/.gnupg`, `~/.pgpass`, `/etc/wireguard`,
   `/etc/{shuma,sandokan,tawasuyu/agente,local.d}`. Son identidades y semillas: no se «vuelven a
   generar», porque generar otras significa ser OTRA máquina para el resto de la flota.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 18:01:16 +00:00
SergioandClaude Opus 5 5ec4e2b213 mudanza: el tercer camino — mudar, RESPALDAR o abandonar, cosa por cosa y con su descripción
Pedido del usuario: «una lista de cada cosa con una corta descripción y la opción de yo elegir mudar,
mover a un directorio para respaldar ese directorio, o abandonarlo». Eran tres huecos distintos.

1. «CADA COSA». El censo medía RAÍCES: `/home` era UNA entrada de 25 G y UNA decisión, con repos,
   SDKs, 4,3 G de caché y trabajo irrepetible adentro. Ahora emite un renglón por cosa —169 en
   gioser contra 5—, un nivel hacia adentro de cada raíz y DOS en `/home`, porque su primer nivel
   son usuarios y la decisión no es por usuario. `--solo-totales` conserva la vista vieja, que es la
   que dimensiona la mudanza; las dos viajan en el censo (`datos` y `datos_raiz`).

   ⚠ El primer intento mandaba las ~110 sondas en UNA llamada, se pasaba del timeout y devolvía
   lista VACÍA: «no hay datos» en vez de «no pude medirlos». Va en tandas, y una tanda que falla se
   nombra.

2. «UNA CORTA DESCRIPCIÓN», y son hechos: lo sirve el servidor web · lo usa un proceso vivo · es
   repo git y a dónde apunta · tiene cambios sin commitear · adentro hay node_modules/target/venv ·
   lo más nuevo que hay dentro. «Sin señales» también es un hecho, y es el que dice dónde mirar.

3. «ELEGIR ENTRE TRES»: DECISIONES pasa de (muda, muere) a (muda, RESPALDA, muere). Faltaba el
   camino que se usa de verdad —no lo quiero corriendo allá, tampoco lo quiero perder—; con dos
   opciones, todo lo dudoso se marcaba `muda` y la mudanza engordaba.

   La regla de fondo no cambió (qué datos VALEN no lo dice la máquina), pero cuatro hechos sí los
   sabe: servido/usado ⇒ muda · caché ⇒ muere · repo limpio con remoto ⇒ muere · repo sin remoto o
   sucio ⇒ respalda. Y el cruce que evita el peor error: un remoto que apunta a ESTA MISMA máquina
   no es un respaldo, es el mismo disco (medido: /mnt/vvv/humanoid → ssh://git@127.0.0.1:2345).

4. LAS DECISIONES SE GUARDAN. `--decide` las usaba sólo para el plan de esa corrida: cerrabas la
   terminal y se perdían las 169. Ahora reescribe el censo (temporal+fsync+rename, `.bak`, y releído
   antes de pisar nada; aborta si perdió alguna). Para eso hubo que hacer el emisor IDEMPOTENTE:
   escribía `decision = ""` fijo, así que re-emitir un censo decidido duplicaba la clave y el TOML
   dejaba de parsear — la herramienta no podía reescribir su propio fichero.

5. EL PASO `respaldo`: corral por defecto leído de respaldo-storagebox.sh (no una segunda dirección
   a mano), nombre aplanado, y NO se instala en el destino. Sin corral, aborta: un rsync a ninguna
   parte devuelve 0 y se lee como un respaldo. Probarlo con rotura a propósito Y con el control que
   tiene que pasar destapó dos bugs: `--mkpath` es obligatorio (el primer respaldo siempre estrena
   un nivel) y un FICHERO no lleva barra final — `rsync fichero/ dst/` es un error, y el paso de
   `datos` tenía el mismo defecto latente. La verificación es rsync en seco contra el destino real,
   no `du` ni `find`: el Storage Box no tiene find, no acepta tuberías, y su `du` comprime.

6. De paso: `aplicar.py --dry-run` decía « todos los pasos ejecutados y verificados» sin haber
   ejecutado nada. Ahora dice EN SECO: N mostrados, NINGUNO ejecutado y NINGUNO verificado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 16:20:32 +00:00
Sergio 2098e86ba4 atuq: la boveda ANDA — 19 guardianes en verde, y el bloqueo de tres semanas era un fichero sin empujar
El pin de `puriy-costura` subio a `23a292863` y con eso se apagaron los dos rojos del cable. La suite
entera sobre `b3:e556024b`: **19 verdes, 0 rojos, 32,1 min**. No se toco ningun guardian — que la
suite existiera ANTES del arreglo es lo que permite afirmarlo: el cuadro de ayer y el de hoy son el
mismo instrumento.

La medicion que cierra es la que abrio el §7.quinquies, leida al reves: `strings` sobre el binario
sellado da `vault` **10** veces donde daba 0, con `cas`/`sct` de control; y `vigia-atuq-verbos` pasa
de 3 verbos sin dueno a cero, con su control positivo intacto.

Lo destrabo publicar el `Cargo.lock` de tawasuyu (`23a292863` alla), regenerado en un arbol limpio:
215 lineas, todas de contabilidad de deps por ruta, cero `checksum` y cero `source` movidos. Y el
remate, que es la leccion: **ese lock era byte a byte el que la otra sesion ya tenia en su arbol sin
commitear**. Tres semanas de bloqueo y el fichero correcto estaba escrito, sin empujar, a un
directorio de distancia. Antes de decir «bloqueado por otro repo», mirar si lo que falta no esta ya
hecho y sin publicar.

Commiteado alla con indice temporal (`GIT_INDEX_FILE` + `commit-tree`), que es la unica forma de
publicar una ruta en `MM` sin llevarse por delante lo del otro agente: su arbol quedo byte a byte
igual, comprobado con `cmp` antes y despues.

⚠ Y la cabecera del runner se desmintio SOLA dos veces: anuncio «un rojo» cuando eran dos, y «dos»
cuando ya no quedaba ninguno. Un numero escrito como prediccion envejece callado. Con todo en verde
el numero ya no distingue un producto sano de un runner que no corre nada, asi que lo que sostiene la
corrida queda escrito: la puerta del sello (verificada contra un store vacio) y el control interno de
cada guardian.
2026-09-16 15:55:30 +00:00
SergioandClaude Opus 5 e2bb4f9b89 censar: «no existe» y «no puedo mirar» se confundían — 572 M invisibles, y el censo de hoy
El censo ya tenía tres estados a propósito (`ausente` / `sin_permiso` / `ok`), pero el tercero sólo
se detectaba sobre la PROPIA ruta: cuando el que no deja pasar es un ANCESTRO, `[ -e "$p" ]`
contesta que no existe y la ruta caía en `ausente`, que el censo descarta sin decir nada.

Medido en gioser: `/root/.local` es 0700 ⇒ un censo corrido como `sergio` daba `/root/.local/bin`
por AUSENTE. Como root son 572 M de binarios puestos a mano que ningún paquete provee — justo la
clase que la mudanza no puede reconstruir.

Arreglado: si un ancestro existe y no es atravesable, el estado es `sin_permiso`. Probado en los dos
sentidos, que es lo que separa un guardián de un adorno:

  ancestro 0700 de otro dueño   →  sin_permiso   (sale en el ⚠ del resumen)
  ruta que NO existe de verdad  →  ausente       (no se reporta)   ← el control que tiene que pasar

Con el arreglo, el censo sin privilegio pasa de 2 rutas ciegas a 13, entre ellas el home entero de
`artix`, que antes no figuraba de ninguna manera.

Y el §6.34 con el estado MEDIDO de la mudanza hoy: de los 29 dominios queda UNO sirviéndose desde
gioser (`sergio.gioser.net`); los otros seis vivos ya contestan 200 contra 2.29.29.217. El rescate
del §6.20 sigue coincidiendo bit a bit con cuatro de los seis procesos que corren desde un inodo
borrado, y los otros dos tienen en disco una versión más nueva. Lo que queda abierto son los pasos 6
y 7: el montón de daemons propios (shuma, willay, pacha, tejido, tupu, matilda…) y los datos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:11:16 +00:00
Sergio fb7dba530d atuq: el que estaba «nombrado como hueco» entra a la suite — 17 verdes y 2 rojos en 32 min
La primera version dejaba afuera al guardian de notificaciones (`dunst-headless.sh --via-atuq`) con
la excusa de que vive en el arnes de sway, y lo escribia en la cabecera «para que su ausencia no se
lea como cobertura». Eso dura tres dias: al cuarto la lista ES la cobertura y el parrafo no lo lee
nadie. Corrido aparte: verde en 180 s. Entra.

Es ademas el que mide lo que ningun auditor de ELF puede ver: una pagina llama `new
Notification(...)`, `libxul` hace `dlopen("libnotify.so.4")` —que no es NEEDED de nada—, el bus
activa dunst y dunst DIBUJA. Cinco piezas y la unica forma de saber que estan las cinco es verlo.

Para que entrara, la lista pasa a llevar el COMANDO de cada guardian en vez de su nombre de fichero:
lo que lo tenia afuera era el despacho por extension, no una decision.

Corrida entera de los diecinueve sobre `b3:e556024b`: **17 verdes, 2 rojos, 32,2 min**, que es
exactamente lo que la cabecera predice — y la cabecera es el control, no un adorno.

Los caros: sct 242 s, archivo semantico 238 s, ia 182 s, instalacion 180 s, notificaciones 180 s.
Los cuatro primeros salen en 5 s y no tocan el navegador.
2026-09-16 14:05:51 +00:00
SergioandClaude Opus 5 eaa7d02725 el corte, con hora: 93 minutos — y en un servicio que ya autoriza no va límite por tasa
Del `access.log`, sin ambigüedad. La IP del usuario (`45.234.61.160`, autenticada como `sergio`,
2709 peticiones) tuvo tres silencios: **93,2 min (11:59:37 → 13:32:49 UTC)**, 50,1 min (10:44:58 →
11:35:06) y 10,0 min (11:45:06 → 11:55:05). Los tres son el `timeout` de 10 minutos del ban
disparándose una y otra vez: cada vez que volvía, su primera ráfaga lo baneaba de nuevo. El corte
grande terminó exactamente cuando vacié los sets.

🧨 Y LA EXENCIÓN QUE TENÍA NO SERVÍA: la ACL histórica de squid exime `45.234.60.0/24` y él entra
desde `45.234.61.160` — **una red de al lado**. Parecía cubierto y no lo estaba. La pertenencia se
MIDE (`ip in red`), no se deduce del parecido del prefijo.

DECISIÓN DE FONDO: en un servicio que YA AUTORIZA no va límite por tasa. El uso de esta caja es
INTENSIVO —varias sesiones de agente en paralelo, cada una abriendo túneles a la vez— y un límite
calibrado para «una persona normal navegando» no protege de nada: el atacante ajusta su ritmo, el
usuario no puede trabajar más despacio. Squid ya pide usuario y contraseña y tiene su ACL. El ban del
puerto queda apagado (60000/min, ráfaga 1000) y contra un flood queda el tope GLOBAL
`syn_nuevas_por_seg`, que no distingue usuarios ni banea a nadie por ser rápido.

Control tras aplicar: su túnel pasando —`45.234.61.160 … CONNECT api.anthropic.com:443 sergio`— y
`45.234.61.160 ∈ 45.234.60.0/23` comprobado con aritmética de redes, no a ojo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:38:54 +00:00
SergioandClaude Opus 5 618dcfd851 🧨 el cortafuegos baneaba a los usuarios del proxy — y squid nunca se cayó
Preguntaron si el proxy estaba caído. NO lo estaba: el ente llevaba horas corriendo con ↻ 0 y
contestando 407. Lo caído era el ACCESO — el cortafuegos de ayer baneaba a los autorizados.

La prueba en una línea: **154.197.1.2, una IP que está en la ACL de squid (gente con contraseña),
apareció en `@ban4`**, con el contador del ban en 38.246 paquetes tirados.

LA CAUSA NO ES LA TASA, ES LA RÁFAGA: `limit rate over 300/minute burst 5 packets` tolera cinco
conexiones por encima del promedio, y un navegador o un agente detrás de un proxy abre DECENAS de
CONNECT en el mismo instante aunque su promedio sea bajísimo. Y como @ban4 es UN SOLO set consultado
antes que todo, caer por el puerto del proxy te tira también el web y el git.

⚠ El tipo ya traía el campo `rafaga` (default 5) con un comentario que dice «para un servicio web va
alto (100-200)». Otra vez leí el .ron de EJEMPLO y no el TIPO.

Hecho, en orden de urgencia: flush de los sets (servicio restaurado en el acto) · los 21 IPs y 5
redes de la ACL de squid a `confiables` · `rafaga` por servicio según su clientela (ssh 5, git 20,
web 100, proxy 200) · proxy a 1200/min con ban de 60 s, que es red de contención contra un flood y
no control de uso. Control después: CERO autorizados en el set, 24 baneados (escáneres) y tráfico
real pasando —`154.194.14.37 … CONNECT api.deepseek.com:443 sigma`, 298 K tunelados—.

⚠⚠ Y un fallo de método que casi lo tapa: **`nft -f` SUMA a la tabla existente**. Cargué el reglaset
corregido encima del viejo y quedaron 30 reglas con las VIEJAS ADELANTE, así que los `confiables`
nuevos no se evaluaban nunca. Por eso `cortafuegos apply-input` borra la tabla antes de cargar.

La lección: un cortafuegos correcto y uno usable no son lo mismo, y la diferencia NO se ve al
aplicarlo. El control que lo habría cazado es el que no corrí: una ráfaga de conexiones desde una IP
que no esté en `confiables`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:33:50 +00:00
SergioandClaude Opus 5 e903f486d9 el enlazador va DENTRO del compilador — /etc/cargo/config.toml es una ruta que cargo no lee
La prueba de la caja recién instalada —`cargo build` en un proyecto nuevo, sin flags ni rutas a
mano— falló con `linker `cc` not found`, y destapó que la pieza que había empaquetado era falsa:
**cargo NO LEE `/etc/cargo/config.toml`**. Sólo lee `$CARGO_HOME/config.toml`, los
`.cargo/config.toml` del proyecto y sus padres, y lo que se le pase con `--config` — que es lo que yo
venía pasando a mano en cada prueba, y por eso «funcionaba».

Y el arreglo no era mover el fichero: **el que enlaza es `rustc`, y rustc no lee la config de
cargo**. Las flags van DENTRO del compilador, en el spec del triple:

· `linker = "ld.lld"` + `linker_flavor = Gnu(Cc::No, Lld::Yes)` — sin esto rustc busca `cc`, que una
  caja takana no tiene.
· `crt_static_default = true` — sin esto enlaza dinámico y pide `-lgcc`, que no existe (la distro se
  construye con zig, que trae compiler-rt).
· `crt_static_allows_dylibs = true` — su pareja obligatoria: sin ella `crt-static` apaga los dylibs
  y con ellos los PROC-MACROS (serde_derive, clap_derive, las 44 recetas `cargo-*` con `derive`).
  El compilador en sí sigue dinámico: el `bootstrap.toml` fija `crt-static = false` para el build, y
  eso gana sobre el default del spec.

⇒ `cargo-config` (receta, árbol y copia en scripts/) se van enteros: un paquete que instala un
fichero que nadie abre es peor que no tenerlo.

Y de paso, `lld21` instala los cuatro alias como SYMLINKS y no como copias: cmake los duplicaba, 5 ×
96 M = 478 M de artefacto para 97 M de enlazador. Ahora 108 M, y `ld.lld` sigue despachando por
`argv[0]`, que es como upstream lo diseñó.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:07:56 +00:00
SergioandClaude Opus 5 72ba86c290 la cadena rehecha: llvm21 arreglado, lld enlaza, rust reconstruido — los seis controles pasan
Un cambio de dos líneas en `llvm21` re-selló tres artefactos y los molió la granja sola, en orden,
por dependencia: llvm21 95956a16→bd4a4094, lld21 27be53c1→95a4794b, rust 015a07fb→702094a8.

    llc --version        Stack dump: + segfault   →   LLVM 21.1.2, Optimized build
    opt --version        ídem                     →   LLVM 21.1.2
    ld.lld               «not built with zlib»    →   ENLAZA
    rustc --version                               →   1.97.0 (built from a source tarball)
    --print target-list                           →   x86_64-alpine-linux-musl
    cargo build + correr                          →   0,16 s y el binario habla

La config del sitio pasa a usar `lld` (más rápido); el `ld` de binutils queda documentado como
alternativa que también funciona.

LA REGLA QUE SE GANÓ SU LUGAR: **`link = "static"` en el encabezado decide cosas que la receta no
dice**. Van tres, todas medidas: apagó los threads de openssl, le mintió a libtool, y acá rompió los
registros estáticos de LLVM (`cl::opt`, `TargetRegistry`) porque el `static-pie` descarta sus
constructores globales. Con C++ grande la postura por defecto pasa a ser `dynamic` y medirlo — y el
control no es que selle: es CORRER UNA HERRAMIENTA del artefacto.

Y el corolario de por qué nadie lo vio en meses: el único consumidor de llvm21 era rust, que usa
`llvm-config` y las `.a`, nunca una herramienta. **Un artefacto puede estar roto en todo lo que nadie
usa** — como los 44 `cargo-*` sin cargo y los 23 binarios sin cargador.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 11:19:18 +00:00
SergioandClaude Opus 5 ef1f9252a1 EL TOOLCHAIN COMPILA, ENLAZA Y CORRE — en una caja sin compilador de C
$ cargo build --offline
       Compiling prueba v0.1.0 … Finished in 0.44s
    $ ./target/debug/prueba
    cargo, rustc y ld: los tres nuestros

`cargo` y `rustc` son los que construyó la granja; el enlazador es el `ld` de nuestro `binutils`.

EL MURO 8 SE CERRÓ SIN `lld`. La cadena, que es lo que vale: `-C linker-flavor=ld.lld` → «linker
`lld` not found» (el build con LLVM externo no genera rust-lld); `rust.lld = true` → el bootstrap lo
RECHAZA; `lld21` propio → crasheaba con cualquier entrada; `lld21` con zlib → **no se puede encender
desde ahí**, porque `LLVMConfig.cmake` dice `LLVM_ENABLE_ZLIB 0` y el build standalone lo hereda.

Lo que faltaba NO era un enlazador: era decirle a rustc que enlace en ESTÁTICO. Con `+crt-static`,
rustc usa su propio libunwind/compiler_builtins y deja de pedir `-lgcc` — que es lo que rompía el
intento con `ld`, porque la distro se construye con zig (compiler-rt) y libgcc no existe. Y estático
es el modo natural de musl.

⇒ `scripts/servidor/cargo-config.toml` (instalado en `/etc/cargo/config.toml`) cierra el pendiente
«config del sitio» del §5, con las tres flags y el porqué de cada una.

🧨 Y `lld21` destapó algo que nadie había visto: **las 79 herramientas de `llvm21` están ROTAS**.
`llc --version` crashea igual —también en el worker— porque salen `static-pie` y el enlace estático
descarta los constructores globales de los que dependen `cl::opt` y `TargetRegistry`. Funciona lo
que no los necesita (`llvm-ar`, `llvm-config`) y revienta lo que sí; nadie lo notó porque `rust` usa
`llvm-config` y las `.a`, nunca una herramienta. Queda como deuda DECLARADA: arreglarlo (o encender
zlib) re-hashea `llvm21` y con él `rust`. Hoy no bloquea nada.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 04:48:25 +00:00
SergioandClaude Opus 5 9f325a3c05 🐛 la granja molía UNA receta por cola y por ciclo — el xargs -I{} recibía la cola entera junta
Medido hoy al encolar `lld21` junto a `rust`: el ciclo decía «2 recetas en recipes/incoming» y
construía UNA. La causa está en el filtro de «ya sellado en el hub»: acumula en `filtrada` uniendo
con ESPACIOS, y más abajo la cola se le pasa a `xargs -I{}`, que trata **una línea = un elemento**.
Unidas por espacios, N recetas llegan como UN SOLO argumento:

    printf '%s\n' " a.toml b.toml c.toml" | xargs -I{} sh -c 'echo [$1]' _ {}
      recibe: [a.toml b.toml c.toml]          ← uno solo

⇒ la fase paralela fallaba SIEMPRE con «No such file», y la fase 1b —el reintento serial— salvaba
exactamente UNA receta: la última, porque `basename` de esa ristra da el último nombre. El log
parecía normal, con su ✗ seguido de un ✓ (reintento serial), y ese patrón está en TODAS las colas
(yambar, spidermonkey, rust…). O sea que el reintento serial, que existe para conciliar colisiones
de concurrencia, venía tapando el bug desde que se escribió el filtro.

El arreglo son dos líneas: acumular con salto de línea y descartar la línea vacía con la que arranca
el acumulador (una línea vacía también es un elemento para `xargs -I{}`). Probado en chiquito, en
los dos sentidos: antes 1 elemento, después 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 04:00:08 +00:00
SergioandClaude Opus 5 24a6dd3b2f el cortafuegos puesto en la caja — y el primer baneado fui yo
`table inet tawasuyu_input` con policy drop, 15 reglas de puerto, y el set `ban4` llenandose solo:
a los pocos minutos, 27 IPs baneadas y paquetes tirados por el contador. El reemplazo de fail2ban
funcionando sobre trafico real, dentro del kernel y sin daemon.

🧨 EL PRIMER BANEADO FUI YO, EN SEGUNDOS. Aplicada la primera version: ssh y https bien, y `http`,
proxy y git-ssh muertos; al minuto la caja entera dejo de contestarme. La causa no es una regla mal
escrita: **el set @ban4 es UNO SOLO para todos los servicios** y `ip saddr @ban4 drop` esta antes
que todo, asi que pasarse de tasa en UN puerto te tira en TODOS — y el `nuevas_por_min: 5` del
puerto de administracion es exactamente lo que me pasa a mi, porque cada comando remoto es una
conexion nueva. Lo devolvio el INTERRUPTOR DE HOMBRE MUERTO (un `nft flush ruleset` programado a
180 s que sólo se desarma si la verificacion externa sale bien). Sin consola serie, esa es la
diferencia entre un susto y un rescue.

⚠ Y el tipo ya lo avisaba: `PoliticaEntrada` tiene un campo `confiables` cuyo comentario describe
palabra por palabra lo que me paso. Copie el .ron de EJEMPLO en vez de leer el TIPO. La cura fue una
linea con el hub y el worker, que se aceptan ANTES de la logica de tasa.

⚠ `nft -c` rechazo 5 reglas antes de aplicar nada: `ct count over N` es CONFIG_NFT_CONNLIMIT y este
kernel no lo trae. Quedan COMENTADAS y no borradas en el fichero que se aplica — lo que la politica
declara y el kernel no puede tiene que verse. El simbolo ya entro a la receta del kernel.

Y para que sobreviva al reinicio, `nftables` declara su [[service]] con `lifecycle = "oneshot"`:
cargar un reglaset no es un daemon, es un acto. Va PRIMERO en el genesis — entre que la red esta y
que las reglas cargan, la caja esta abierta. Probado con `nft flush ruleset` + `arjectl start`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:56:51 +00:00
Sergio ba202d7826 atuq: los dieciocho guardianes en UN comando — y el sello llevaba nueve horas atrás
`atuq` es `source.dir` entero, asi que el commit de la boveda de esta manana (`6f0ba974`) movio el
ArtifactHash y dejo el artefacto vigente SIN SELLAR. Durante esas nueve horas ningun guardian podia
medir: se niegan a correr contra uno que no sea el vigente, que es lo correcto. Pero un guardian que
se niega solo grita CUANDO alguien lo corre, y nadie lo corria — el repo se veia sano y la unica
senal era un `hash --check` que nadie tenia motivo para teclear.

Resellado en 3,7 s (es derivado y el firefox vigente estaba en el store) y corridos los dieciocho
sobre `b3:e556024b`: 16 en verde y 2 en ROJO, los dos por la MISMA causa —los tres verbos de la
boveda del §7.quinquies—. O sea que la unidad 12 no rompio nada de al lado, que es lo que habia que
saber antes de seguir: toco `atuq.cfg`, la politica y el empaquetado de extensiones.

`scripts/test-atuq-suite.sh` deja eso en un comando, en serie (cuatro nucleos sin swap; dos
navegadores a la vez es un OOM) y cada guardian a SU fichero, porque dos corridas sobre el mismo log
se mezclan y el resultado parece un pase. Lo primero que mira es el sello y NO corre nada si falta:
verificado contra un store vacio, sale por ahi y devuelve 1.

Dos cosas quedan escritas como control y no como adorno:

· el numero de rojos esperados va en la cabecera. Un cuadro entero en verde puede ser un producto
  sano o un runner que no corre nada, y se ven igual;
· esa cabecera decia «un rojo» —prediccion escrita antes de correr— y la primera corrida dijo dos:
  el guardian de coherencia de la boveda lleva el mismo chequeo del cable adentro.

Coste medido en gioser: ~29 min. Los cuatro primeros salen en 10 s y no tocan el navegador.
2026-09-15 21:33:05 +00:00
SergioandClaude Opus 5 0a7afebe9c la caja tiene HORA, LATIDO y ROTACIÓN — y el respaldo del gitea dejó de ser a mano
`planear.py --revisar` sobre el censo ordena lo que queda, y lo primero no eran los servicios del
usuario sino TRES CAPACIDADES que ninguna metrica reclama: el disparador periodico, la hora y la
rotacion de logs. Los tres paquetes ya estaban sellados y declarados en `perfil.servidor` desde que
ese perfil nacio — lo que faltaba era el eslabon que los ARRANCA, igual que con squid.

· `chronyd`: la caja iba **15 s atrasada** y nadie la corregia. Ahora stratum 3 contra los NTP de
  Hetzner, 0,000005 s de NTP. Sin hora no hay TLS ni firmas, y el sintoma no se parece a la causa.
· `crond`: no existia el latido. Verificado con una entrada `* * * * *` en el crontab REAL.
· `logrotate`: `/var/log/squid` sin rotar. Diario, con `squid -k rotate` (squid mantiene los
  ficheros abiertos: un rename a secas lo deja escribiendo en un inode que nadie puede leer).
· `scripts/respaldo-gitea.sh` + `17 3 * * *`: cierra el hueco del §6.15 — los 44 repos vivian en UNA
  copia y el respaldo se hacia a mano. Sube 23+21 repos (1,5 G) y un `gitea.db` de 332 M tomado con
  `.backup`, consistente con el servidor vivo. El control no es que el script salga 0: es CONTAR los
  repos de los dos lados.

`chrony` y `cronie` NO declaraban `[[service]]` — el paquete llegaba a la imagen y no lo arrancaba
nadie. Ahora lo declaran (fuera de `hash_inputs`: los hashes no se movieron) y el perfil los habilita.

TRES MEDICIONES que valen mas que el resultado:
1. **La deriva era de gioser, no de la caja**: despues de sincronizar, el HUB quedo 3 s adelantado.
   Medir «contra el otro» sin un tercero no dice quien esta mal.
2. **El spool de cronie es `/var/spool/cron/<usuario>`**, no `…/crontabs/<usuario>`: `crontab -l`
   mostraba las entradas y `crontabs/` estaba vacio. Un crontab restaurado en el sitio equivocado es
   un fichero perfecto que nadie lee.
3. 🧨 **`sqlite3` seguia INERTE en la caja** (`compressBound: symbol not found`): el ultimo de los 23
   del §6.4. Le faltaba `zlib-shared`, sellado y declarado en `perfil.base` desde el 11-09 — pero la
   caja se instalo antes. Sin el no habia snapshot consistente. Un paquete declarado no es un
   paquete instalado: en una caja viva vale lo que el `upgrade` proyecto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:17:20 +00:00
Sergio db24d206dc arnés de la imagen: el piso del sello se COMPRUEBA — un piso que falla callado no existe
Los 180 s de `PISO_SELLO` son de esta máquina (TCG sin KVM y con la carga que tenga el anfitrión,
igual que los +97/+120/+137 de la pintura). Con el anfitrión cargado el mismo arranque puede no
llegar al gancho que sella el éxito, y entonces la corrida suma al contador de caídas y envenena la
siguiente EN SILENCIO — que es exactamente como empezó todo esto.

Ahora que `toolkit.startup.last_success` se lee antes y después, eso se puede decir, así que se dice:

  ✓ el arranque SELLÓ el éxito (last_success X → Y): esta corrida no envenena la siguiente
  ⚠⚠ el sello NO se movió: este arranque no terminó ⇒ la corrida SUMA al contador. Subí PISO_SELLO

Es la diferencia entre un piso confiado y un piso comprobado, y cuesta cuatro líneas porque el dato
ya estaba medido y guardado.
2026-09-15 19:24:50 +00:00
Sergio 5b47cd596c atuq: el reloj del sello es el del LANZAMIENTO, no el de la pintura — y el piso va en el arnés
Leyendo el perfil en cada captura, el sello (`toolkit.startup.last_success`) y la limpieza del
contador llegan al disco entre +100 s y +137 s DESDE EL LANZAMIENTO, no desde el primer cuadro. Y es
UN gancho y no dos: el mismo arranque que limpia escribe el sello, con su propia hora de arranque.

Las dos cotas salen de corridas independientes, que es lo que hace al intervalo creíble:

· a los +100 s NO había sellado — la corrida S1 vivió 100 s desde el lanzamiento (primer cuadro a
  +97 s, fin del proceso tres segundos después, medido con los tiempos de sus propios ficheros);
· a los +137 s YA había sellado — la corrida con `--watch-prefs` lo ve aparecer en esa captura.

S1 parecía la excepción del modelo y resulta ser su cota inferior: si el reloj fuera el de la pintura
tendría que haber sellado, y no selló.

La consecuencia es contraintuitiva y es la que muerde: cuanto MÁS RÁPIDO pinta, más fácil es
envenenar el perfil, porque `--until-paint` corta antes. La serie de ayer no se rompió por ser larga
sino por ser rápida. Por eso la regla va en el arnés y no en un runbook: `PISO_SELLO = 180`, margen
sobre la cota alta, y `--until-paint` no corta antes de ese piso. El comentario del piso lleva los
dos números medidos al lado — un piso que se lee como elegido lo baja el próximo que lo vea.

Las corridas `cierre-1`/`cuanto-1`, el mando `--watch-prefs` y el piso los midió y escribió la otra
sesión del frente; acá van con la cota de S1 que los cierra por abajo.
2026-09-15 19:23:26 +00:00
Sergio 1de1fc31f6 atuq: lo que limpia el contador es SOBREVIVIR un rato después de pintar, no pintar
El par aísla la variable —mismo pin, mismo perfil, misma imagen, y la única diferencia es cuánto vive
el navegador después del primer cuadro—:

  S1       +97 s,  muerta en el acto (`--until-paint`)   ⇒ contador después: 1
  cierre-1 +120 s, ~360 s de vida después de pintar      ⇒ contador después: AUSENTE

Con eso queda contestada la pregunta que dos secciones atrás este documento daba por cerrada y no lo
estaba, y A deja de ser una anomalía: también siguió viva minutos después del primer cuadro.

Se dice además lo que la tabla NO prueba: si ese arranque además SELLÓ el éxito. El volcado del
«después» sólo grepeaba el contador; ahora `toolkit.startup.last_success` va en los DOS volcados y se
guarda en la medición (`last_success_antes` / `_despues`), que es lo que faltaba para no repetir la
corrida. Si el sello se mueve, es un gancho que sella y limpia a la vez; si no, son dos.

Y queda escrito que el 16 sigue sin explicación, ahora con una contradicción medida encima:
desapareció en una corrida diálogo + ATUQ-EXIT=0, y las fases k2/k3 del umbral fueron diálogo +
ATUQ-EXIT=0 y no limpiaron nada. La respuesta buena no tapa la sucia.

La corrida `cierre-1` y el mando `--watch-prefs` son de la otra sesión del frente.
2026-09-15 19:09:09 +00:00
Sergio c30905e2b9 atuq: vigía de las dos puntas del cable — y el extractor de verbos que no veía la mitad
El chequeo del commit anterior miraba una extensión. El cable tiene la misma forma para las diez, y
este vigía lo recorre entero: qué verbos manda cada extensión y si el commit que `recipes/
puriy-costura.toml` pinea los atiende. Corre en un segundo y no construye nada.

Medido hoy, 13 verbos sobre 8 extensiones: sólo los TRES de la bóveda están sin dueño. O sea que
esto no se podía deducir mirando cuándo se tocó la receta por última vez — las otras nueve
extensiones conviven bien con el mismo pin viejo.

⚠ Y lo que costó más que el vigía: **el extractor de verbos no veía la mitad de lo que hay**. Estaba
buscando `verb: "…"`, que es como lo escribe la bóveda. Pero la extensión de IA arma el mensaje con
`postMessage({ id, verb: verbo, … })` —el verbo llega por VARIABLE—, así que ese patrón encontraba
cero verbos en `ia` y la declaraba sana sin haber mirado nada: cero verbos encontrados y cero verbos
faltando son el mismo cero con dos causas. Ahora recoge toda cadena con forma `ns.verbo` y descarta
las que son nombres de fichero, que es lo único que se les parece. Con eso aparecen `ai.ask` y
`archive.ask`, que antes no estaban en ninguna cuenta.

De ahí sale el tercer control, que no es negativo ni positivo sino de COBERTURA: una extensión de la
que no se extrae ningún verbo se REPORTA. Y las dos que de verdad no le hablan al host —`inicio` y
`proxy`, que viven enteras dentro del navegador— están nombradas en el código, para que «no manda
verbos» no se pueda confundir con «el extractor se quedó mudo».

Los otros tres controles: `sct.observe` tiene que aparecer en el commit pineado (si no, lo roto es
el vigía y lo dice); `--negative-control` agrega un verbo inventado a cada extensión y exige que lo
vea; y si el clon de tawasuyu no está, o no conoce el commit pineado, esto FALLA — «no se pudo
comprobar» no es «está bien». Los cuatro probados, incluida la rama del commit desconocido.

Pregunta por `git grep '"<verbo>" =>' <commit> -- '*.rs'` leyendo del objeto: sin checkout, porque
el árbol de tawasuyu es compartido y siempre tiene ficheros en vuelo de otras sesiones.
2026-09-15 19:04:50 +00:00
Sergio e7369efe82 atuq: la bóveda le habla a un host que no sabe sus verbos — y eran CINCO lugares, no cuatro
Este guardián nació esta mañana diciendo que una extensión está enchufada en CUATRO sitios y que
ninguno da error si falta. Los cuatro estaban bien. El quinto no, y es el que hoy está roto.

El quinto lugar vive en otra receta y en otro repo: `recipes/puriy-costura.toml` pinea un commit de
tawasuyu, y es ESE commit el que decide si `vault.match` es un verbo o una cadena que nadie atiende.
Medido, con su control al lado, sobre el artefacto SELLADO y vigente (`hash --check` dice SELLADO
para `b3:1eb2b692`):

    strings del binario sellado → «vault» 0 veces   ·   «cas» 5 veces   ⇐ EL CONTROL

El cero solo no probaba nada —un grep que devuelve cero puede ser una ausencia o un sitio
equivocado, y las dos se ven igual—; lo que lo convierte en evidencia es el «cas» que TENÍA que dar
positivo en el mismo binario. Y del lado de la fuente sale lo mismo: la receta pinea `e19bb0e5`, que
es ANCESTRO de `bc6903f9e` —el commit que agregó los verbos—, así que el host sellado es anterior a
la bóveda por construcción.

Cómo se ve ese fallo desde la silla del usuario, que es por qué esto merece guardián: el manifiesto
está, el permiso está, la política instala, `connectNative` CONECTA, la extensión manda
`vault.match` y el host contesta `verbo desconocido`. `fondo.js` lee `r.ok !== true`, borra la
insignia y se calla. Un navegador sin bóveda, y todos los ficheros en orden.

El chequeo pregunta `git grep '"<verbo>" =>' <commit> -- '*.rs'` sobre el clon, sin checkout: el
árbol de tawasuyu es compartido y siempre tiene ficheros en vuelo de otras sesiones. Los verbos los
saca de `fondo.js` y no de una lista escrita acá, que se desincronizaría justo en el único sitio
donde eso no se ve.

Tres controles, porque el chequeo es un grep que espera cero:
- POSITIVO: `sct.observe` tiene que aparecer en el commit pineado. Si no aparece, lo roto es el
  guardián y lo dice así, en vez de acusar a la receta;
- `--negative-control-verbo`: le agrega un verbo inventado y exige que lo detecte;
- y si el clon no está, «no se pudo comprobar» NO es «está bien»: falla ruidosamente.

Lo que este commit NO hace: subir el pin. Eso es su propia unidad y tiene un muro medido delante —el
`Cargo.lock` de tawasuyu en `main` todavía describe a `puriy-costura` con sus once deps viejas, sin
`pacha-boveda` ni `pacha-cifrador`, y `pacha-boveda-daemon` no está en el lock en absoluto. Con eso,
`cargo vendor --locked` muere sin nombrar al culpable, que es exactamente la trampa que la propia
receta dejó escrita.
2026-09-15 18:59:02 +00:00
Sergio 98c9e4daa6 atuq: la tasa con perfil sano es +97 y +98 s — y PINTAR NO ES TERMINAR el arranque
Cuatro arranques fríos en serie, el perfil partiendo del 5 que dejó la corrida del umbral:

  S1  pin 0   antes 5  después 1   el navegador   ✓ +97 s
  S2  sin pin antes 1  después 2   el navegador   ✓ +98 s
  S3  sin pin antes 2  después 3   (matada a los 60 s)
  S4  sin pin antes 3  después 4   EL DIÁLOGO     ✗ en 420 s

Tres cosas:

· la tasa, por fin con perfil sano: dos corridas independientes y pegadas, +97 y +98 s, contra los
  +123/+195/+197/+204 de antes. Con el anfitrión descargado el mismo artefacto pinta en la mitad del
  tiempo ⇒ el número es de la máquina y la carga tanto como del producto, y se publica con sus
  condiciones;
· el umbral se reprodujo solo y en otra secuencia: S4 arrancó con 3, sumó a 4 y abrió el diálogo —
  segunda medición independiente del mismo borde, esta vez identificando la ventana por `min_size`
  y no por el título (que está traducido, así que un grep por «Troubleshoot Mode» en una imagen en
  castellano no engancha y su ausencia se lee como la conclusión contraria);
· ⚠ y una corrida que PINTA no limpia el contador: S2 pintó y dejó 2. Más todavía,
  `toolkit.startup.last_success` vale lo mismo en las CUATRO (1789494795, las 17:53) ⇒ el gancho de
  cierre no corrió ni una vez, ni en las que pintaron. Pintar no es terminar de arrancar.

Entonces «quién limpia el contador» SIGUE SIN MEDIR, y el propio documento decía que esta serie lo
cerraría: no lo cierra, y queda escrito así. Hace falta una corrida que deje al navegador terminar
de arrancar y salir limpio.

Lo más caro es para el instrumento: con `--until-paint` cada corrida suma uno, pinte o no, así que
una serie se rompe sola a la cuarta — que es lo que pasó ayer sin que nadie lo viera. El arnés ahora
AVISA cuando el contador está en 3 («esta corrida va a abrir el diálogo, no el navegador») y la
receta para una serie queda escrita: pinnear `--crashes 0` en cada corrida, que resetea la base
(S1 lo muestra partiendo de 5).

También cae una candidata mía, con un cero que viene con control (medido por la otra sesión): el pref
no tiene default en este build, así que fijarlo en 0 SÍ se persiste y el «ausente» no era el pin.
2026-09-15 18:52:48 +00:00
Sergio 7997464337 atuq: el umbral son 4 — y mostrar el diálogo NO limpia el contador, como este documento decía
Medido en los dos lados, partiendo del perfil en 2 y con tres fases más en un arranque: el que
encuentra 2 guardado PINTA (y queda en 3); el que encuentra 3 lo sube a 4, compara 4 > 3 y abre el
diálogo (y queda en 4); el siguiente queda en 5 y vuelve a abrir el diálogo. O sea que la
comparación va DESPUÉS del incremento del propio arranque: contando desde la última corrida que
terminó bien, son cuatro arranques interrumpidos seguidos y el quinto lanzamiento ya no abre el
navegador.

`toolkit.startup.last_success` vale lo mismo en las tres fases (1789494795, el arranque de la última
corrida sana): el contador sube mientras esa marca no se mueve. Con las dos cifras juntas, un
contador que no cambió deja de ser ambiguo entre «no subió» y «subió y se limpió».

⚠ Y eso refuta una atribución que yo había escrito ayer y que estaba en tres sitios: mostrar el
diálogo NO limpia el contador (k2 lo dejó en 4, k3 en 5). Queda retirada del SDD y del comentario
del arnés. La desaparición del 16 entre las 17:12 y las 17:20 pasa a ser un hecho SIN explicación
medida, con dos candidatas escritas y ninguna dada por buena — y una de ellas es mía y fea: todas
las corridas que pintaron llevaban `--crashes 0` pinneado, que es el valor por DEFECTO, y un pref
igual al default no se persiste. El «ausente» puede ser eso y no una limpieza. Queda dicho cómo se
cierra: dejar el contador en 2 SIN pinnearlo, correr hasta que pinte y leerlo después.

Un discriminador que salió gratis: `ATUQ-EXIT=137` es «la matamos» (128+9) y `ATUQ-EXIT=0` es «se
fue sola» por el diálogo. El código de salida contesta lo que una captura no podía.

Medido por la otra sesión del frente (work/umbral-1.txt), con la predicción escrita antes de mirar.
2026-09-15 18:20:23 +00:00
Sergio 2f90d8eb40 arnés de la imagen: el contador de caídas viaja con la medición, y las envenenadas se apartan
`atuq-en-imagen.py` ahora lee `toolkit.startup.recent_crashes` del perfil ANTES y DESPUÉS de cada
corrida y lo anota junto al tiempo de primera pintura (`recent_crashes_antes` / `_despues` /
`_fijado`). Los dos volcados, no uno: Gecko BORRA el contador al mostrar el diálogo de Modo de
resolución de problemas, así que un perfil envenenado se ve limpio si se lo mira después — y la
corrida siguiente parece arreglarse sola mientras la anterior parece intermitente.

Con eso, `vigia-imagen.py` saca de la tasa las corridas con el contador > 3 (ahí el sujeto no es el
navegador sino un diálogo modal) y para las 21 anteriores al campo dice que NO SE PUEDEN CLASIFICAR
en vez de contarlas como buenas. Un denominador se marca, no se borra.

Mandos y guardas nuevas:

· `--crashes N` fija el contador en el `user.js` del perfil: 0 limpia, >3 reproduce. Es el control en
  los dos sentidos, que es lo único que distingue causa de correlación con suerte;
· `WidgetScreen:5` en el MOZ_LOG y una sonda `ATUQ-DIAG` en la propia página (screen/outer/inner por
  `dump()`), que es lo que refutó la carrera con `wl_output`;
· la lista ordenada del protocolo ya no se ahoga en los ~60 modos que anuncia QEMU —el `head`
  cortaba antes del `set_window_geometry`— y trae `set_title`/`set_app_id`, que es lo que identificó
  la ventana;
· si QEMU no arranca, se dice en el primer segundo leyendo `qemu.log`. El fallo real es «Failed to
  get write lock» con otra VM sobre la misma imagen, y sin esta guarda se veía como diez minutos
  esperando una marca del serial: el socket queda creado y el `connect()` funciona.

Las fases (`--xulstore`), el `ATUQ-EXIT=$?`, la copia del log del arranque anterior y el `find` del
perfil sobre la raíz entera los escribió la otra sesión que trabaja este frente; conviven acá porque
el fichero es compartido.
2026-09-15 17:46:30 +00:00
sergioandsergio 6f0ba974c7 feat(atuq): la boveda de la suite guarda las contrasenas, y el gestor de Gecko se aparta
La decima extension de atuq. Ofrece la credencial que corresponde al sitio
abierto y la pone en el formulario, pero NO puede sacar una contrasena de la
boveda por su cuenta: pregunta cuales corresponden —y eso contesta titulo y
usuario, nada mas— y la contrasena sale por otro verbo que pregunta en el
escritorio antes de contestar. Si la extension queda comprometida, o si una
pagina consigue hablarle, lo que obtiene es la lista de titulos de los sitios
que coinciden con su propia direccion.

La direccion siempre la pone el chrome y nunca la pagina. Lo unico que el
trozo que corre dentro del sitio aporta es que HAY un formulario y lo que el
usuario tipeo; si pudiera decir de quien es la pagina, podria decir que es el
banco.

Y la extension no decide nada: quien sabe que credencial va en que sitio es
la funcion del otro lado del cable. Ponerlo en JS seria reimplementar el
original y, peor, poner en la pagina la decision de a quien se le entrega una
contrasena.

EL GESTOR DE GECKO SE APAGA, pero como valor de arranque y no como politica.
Dos gestores peleando por el mismo campo es la peor experiencia que puede
tener alguien que solo quiere entrar a su correo: el sitio se rellena dos
veces, o ninguna, y no hay forma de saber cual tiene la buena. De fabrica
manda la boveda; el que prefiera el de Gecko lo prende y listo. Prohibirselo
seria decidir por el, que es lo que este fichero dice en su cabecera que atuq
no quiere ser.

Y un guardian nuevo, que corre en un segundo y sin construir nada. Una
extension esta enchufada en CUATRO lugares distintos y ninguno da error si
falta: el identificador que ella declara, la politica que la instala, el
permiso para hablarle al host, y las preferencias que la acompanan. Si falta
la politica no se instala. Si falta el permiso se instala y se conecta a
nada, en silencio, y la insignia no aparece nunca. Si faltan las preferencias
los dos gestores se pelean. Ninguno de los tres se ve como un error: se ven
como "no anda". El guardian los mira todos y trae su control negativo, que le
saca el permiso del host y exige que lo detecte.

No reemplaza al guardian de metal —servidor, navegador real, login real, el
dialogo a la vista—: lo precede, y ahorra descubrir construyendo que faltaba
un renglon en un JSON.

Los cinco verbos del otro lado ya estan en tawasuyu (bc6903f9e), con trece
tests que leen el cable.
2026-09-15 10:08:20 -04:00