Commit Graph
2895 Commits
Author SHA1 Message Date
Sergio 3f3bb2ed16 estado: cosecha granja 2026-09-16T03:31:40Z — avance del árbol KDE 2026-09-16 03:31:40 +00:00
Sergio ffe26377fb estado: cosecha granja 2026-09-16T03:01:33Z — avance del árbol KDE 2026-09-16 03:01:34 +00:00
Sergio 4f78a80c47 estado: cosecha granja 2026-09-16T02:31:35Z — avance del árbol KDE 2026-09-16 02:31:35 +00:00
Sergio 0ffcf4ed65 estado: cosecha granja 2026-09-16T02:01:47Z — avance del árbol KDE 2026-09-16 02:01:47 +00:00
Sergio 6b72d8d57b estado: cosecha granja 2026-09-16T01:31:45Z — avance del árbol KDE 2026-09-16 01:31:45 +00:00
SergioandClaude Opus 5 a9911785e1 lld21: el enlazador de la distro — porque el bootstrap de rust se NIEGA a construirlo
La vía (a) está CERRADA, y lo dice el propio bootstrap en una línea:

    Cannot enable LLD with `rust.lld = true` when using external llvm-config.

Había leído el paso `Lld` —que compila desde el `src/llvm-project/lld` del tarball y reusa el
`llvm-config` externo— y me faltó mirar la VALIDACIÓN de config, que rechaza la combinación antes de
llegar a ese paso. La guarda tiene sentido: LLD y LLVM comparten ABI de C++, y con un llvm-config
externo el bootstrap no puede garantizar que casen. O sea: mientras usemos el `llvm21` del corpus
—que es lo que queremos— el toolchain NO se va a construir su propio `rust-lld`.

⇒ `recipes/lld21.toml` construye el MISMO `lld`, del mismo `llvm-project` (mismo tarball y mismo
sha256 que `llvm21`, byte por byte), pero SÓLO el subproyecto: `cmake -S lld` contra el LLVM ya
instalado por la dep. Minutos en vez de horas, y sin re-hashear `llvm21` —que encender
`LLVM_ENABLE_PROJECTS=lld` ahí dentro habría arrastrado a `rust` con él.

⚠ Y al revertir el flag aprendí algo que vale para todas las recetas con heredoc: el texto del
`bootstrap.toml` que la fase escribe **es parte de la fase, o sea ENTRADA DE HASH**. Mi comentario
explicando el fallo, puesto ahí adentro, cambiaba el hash del compilador entero: hora y media de
granja por un párrafo. Va fuera, como comentario de la receta, y el hash vuelve a `015a07fb…` — el
artefacto que ya está sellado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 01:02:49 +00:00
Sergio 8a6c928803 estado: cosecha granja 2026-09-16T01:01:51Z — avance del árbol KDE 2026-09-16 01:01:51 +00:00
SergioandClaude Opus 5 b7f70f37a8 rust: lld = true — el compilador que no podía enlazar ahora trae su enlazador
Muro 8, cerrado por la vía (a): el toolchain se construye su propio `rust-lld` en vez de depender de
un `cc` que en una caja takana no existe.

Lo que lo destraba es una línea del log que parece informativa: `skipping llvm-tools (…): external
LLVM`. Al usar un LLVM externo —que es lo que queremos, porque `llvm21` es del corpus— x.py se salta
las herramientas de LLVM, y `rust-lld` es una de ellas. El prebuilt oficial sí la trae, y por eso el
escalón 1 enlazaba y el 2 no.

Comprobado ANTES de encolar dos horas de build, leyendo el bootstrap y midiendo los artefactos:
· el paso `Lld` compila desde `src/llvm-project/lld`, que YA VIENE en el tarball (1,1 G de fuente);
· usa el `llvm-config`/cmake del LLVM EXTERNO y NO dispara un build de LLVM entero;
· `llvm21` publica `lib/cmake/llvm/LLVMConfig.cmake`, que es justo lo que ese paso necesita;
· `dist.rs` copia `rust-lld` al sysroot SÓLO `if builder.config.lld_enabled`;
· y `llvm21` no servía de repuesto: sobre sus 1,6 G no publica NINGÚN `lld`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 00:58:29 +00:00
Sergio 1eff89d870 estado: cosecha granja 2026-09-16T00:31:55Z — avance del árbol KDE 2026-09-16 00:31:56 +00:00
SergioandClaude Opus 5 be95b71449 SDD 31: EL ESCALÓN 2 SELLÓ — rust 1.97.0 desde fuente, b3:015a07fb…
353 M, construido por la granja desde `llvm21` del corpus y el stage0 del lab. Los controles, en la
caja (que sí tiene cargador musl; el worker no):

    rustc --version            → 1.97.0 … (built from a source tarball)   ← NUESTRO
    rustc --print target-list  → x86_64-alpine-linux-musl                 ← EL PARCHE, DENTRO
    cargo --version            → 1.97.0 … (built from a source tarball)

Y compila: `--emit=obj` da un objeto de 3,5 K y `--crate-type=rlib` una biblioteca, sin enlazador.

🧱 MURO 8, YA MEDIDO: el compilador no trae con qué ENLAZAR. En una caja sin `cc` —cualquier takana
que no sea un hub— `-C linker-flavor=ld.lld` da «linker `lld` not found» y el `ld` de binutils muere
con «cannot find -lgcc». La causa estaba en una línea del log que parece informativa:

    skipping llvm-tools (x86_64-alpine-linux-musl): external LLVM

Al usar LLVM externo —que es lo que queremos— x.py se salta las herramientas de LLVM, y `rust-lld`
es una de ellas. El prebuilt oficial sí la trae, y por eso el escalón 1 enlazaba. Y `llvm21` no
sirve de repuesto: NO publica ningún `lld` (medido sobre sus 1,6 G).

Dos salidas: (a) `[rust] lld = true`, que construye rust-lld desde el llvm-project vendoreado y deja
el toolchain autosuficiente; (b) publicar `lld` desde llvm21 y fijar el linker en un
`/etc/cargo/config.toml` del sitio.

Y la lección de método: el artefacto SELLÓ, reproduce y NO ENLAZA. Es [[subcomando-sin-driver]] en
el corazón del toolchain. Un `build ok` no es la prueba: la prueba es el objeto en el disco.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 00:23:46 +00:00
Sergio 953eadc046 estado: cosecha granja 2026-09-16T00:02:19Z — avance del árbol KDE 2026-09-16 00:02:19 +00:00
SergioandClaude Opus 5 ac856501b2 rust: el séptimo muro es el openssl de cargo — y va la variante CON THREADS
El parche del triple FUNCIONÓ: el build pasó el stage 2 —donde moría— y llegó hasta las herramientas.
Ahí cortó otra cosa:

    error: failed to run custom build command for `openssl-sys v0.9.114`
    The system library `openssl` required by crate `openssl-sys` was not found.

`tools = ["cargo"]` arrastra `openssl-sys`, y eso pasa DESPUÉS de compilar las ~400 crates del
compilador, o sea a la hora y media de build.

La dep que entra es `openssl-threads` y NO la canónica, aunque las dos publiquen sólo `.a`: el
`Configure` de openssl APAGA LOS THREADS en cuanto ve `-static` en LDFLAGS, y la canónica se
construye así. Un cargo —masivamente multihilo— enlazado contra un openssl sin soporte de hilos es
la clase de fallo que no se ve al construir ni al arrancar. La variante ya existía por exactamente
lo mismo para `python3`, y el worker la tiene sellada con el mismo hash.

Van también `OPENSSL_STATIC=1` y `OPENSSL_DIR=/usr`: el lab publica `.a` y ningún `.so`, y sin
decírselo `openssl-sys` busca la dinámica y falla con un mensaje que habla de pkg-config.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 23:59:22 +00:00
Sergio c8aa1ddd08 estado: cosecha granja 2026-09-15T23:31:40Z — avance del árbol KDE 2026-09-15 23:31:40 +00:00
Sergio efbb27d548 estado: cosecha granja 2026-09-15T23:01:47Z — avance del árbol KDE 2026-09-15 23:01:47 +00:00
Sergio 3ec12df8db estado: cosecha granja 2026-09-15T22:36:03Z — avance del árbol KDE 2026-09-15 22:36:03 +00:00
Sergio d68fcd84f1 estado: cosecha granja 2026-09-15T22:02:06Z — avance del árbol KDE 2026-09-15 22:02:07 +00:00
SergioandClaude Opus 5 d3dcd739cc sacar el kernel de la cola de la granja: se construyó en la caja
`recipes/incoming/linux-generic.toml` entró a la cola y salió el mismo día. El motivo es concreto:
la cola del worker se muele EN SERIE y `rust` está horas adentro, así que el kernel —que era lo
urgente, porque sin él no hay cortafuegos— habría esperado a que rust terminara. Se construyó en la
propia caja (4 cores, 4,6 G libres, CPU ociosa) en 21 minutos.

Dejarlo encolado no era gratis: el worker no tiene ese artefacto en su store —la cosecha va
worker→hub y nunca al revés— así que lo habría reconstruido entero para llegar al mismo b3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 22:00:10 +00:00
SergioandClaude Opus 5 de16c4ba24 rust: el triple de Alpine, DENTRO del compilador — el sexto muro, atacado
`recipes/rust-alpine-target.patch` registra `x86_64-alpine-linux-musl` en `rustc_target`: un spec
copiado del canónico y una línea en `supported_targets!`. Es lo que hace Alpine en su APKBUILD, y es
la única forma que vale para el triple del HOST (un `.json` por `RUST_TARGET_PATH` no alcanza).

La única diferencia real con el spec canónico es `crt_static_default = false`, que es lo que habilita
dylibs y con ellos los PROC-MACROS: un rustc con crt-static por default no compila `serde_derive` ni
`clap_derive` — ni las 44 recetas `cargo-*` del corpus que usan `derive`.

⚠ PROBADO EN SECO ANTES DE ENCOLAR UNA HORA DE BUILD (`patch -p1 --dry-run` contra el árbol real del
worker), y no de primera: la cabecera `@@ -0,0 +1,38 @@` del fichero nuevo tenía 37 líneas y `patch`
murió con «malformed patch», y el hunk de `mod.rs` lo escribí con números a ojo y falló. El segundo
lo generé con `diff -u` de verdad contra una copia. Un parche se COMPRUEBA, no se redacta.

⚠ Y EL `.patch` HAY QUE ENCOLARLO TAMBIÉN: `source.patches` resuelve contra el directorio de la
receta, así que con la receta enlazada en `recipes/incoming/` el parche se busca AHÍ. Es exactamente
el corolario que `farm-worker-loop.sh` documenta («los `.patch` NO viajan con la receta»), visto
desde el otro lado. Con los dos enlaces, el hash de la cola y el canónico coinciden: `b3:53c7f991…`,
y el worker calcula el mismo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:59:55 +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
SergioandClaude Opus 5 a0b3e27eff SDD 31 §4.bis: el sexto muro de rust, que la granja encontró sola
`rust` en la cola del worker llegó MUCHO más lejos: compiló las 386 crates del stage1 y murió al
empezar `Building stage1 library artifacts (stage1 -> stage1)`:

    error: error loading target specification: could not find specification for target
           "x86_64-alpine-linux-musl"

Es la OTRA MITAD del muro 4 y sólo se ve en el stage 2: usar el triple de Alpine funciona mientras
el compilador que manda es el del LAB —Alpine le PARCHEA esa especificación a su rustc— y deja de
funcionar en cuanto toma el relevo el rustc recién construido, que es upstream puro y no la conoce.
El triple que hace falta para ARRANCAR el build es el que el PRODUCTO del build no tiene.

Tres salidas, con la elegida: parchear `rustc_target` para registrar el triple (lo que hace Alpine;
queda DENTRO del compilador y vale en todos los stages). El `RUST_TARGET_PATH` + JSON es más barato
pero un target por JSON como HOST es terreno que rustc no soporta bien. Y volver al canónico no
tiene candidato a stage0: el prebuilt oficial trae esa std pero no puede compilar proc-macros.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:43:12 +00:00
SergioandClaude Opus 5 70e4c540d3 la caja arrancó con nf_tables — y la Semilla la levantó ENTERA sola
`nft list ruleset` contesta vacío y sin error: nf_tables funciona por primera vez en una máquina
takana. Comprobado con el mecanismo que reemplaza a fail2ban y no con una regla de adorno: un set
`flags dynamic` con `add @ban4 { ip saddr timeout 10s limit rate over 5/minute } drop`, aceptado por
el kernel y releído tal cual.

LO QUE EL REINICIO PROBÓ DE PASO, y vale más que el kernel: la Semilla levantó los OCHO entes sola,
todos con ↻ 0 — incluidos los DOS enjaulados en qorpa, que arrancan desde el genesis con su
newuidmap, su overlay y su harkaq igual que un binario nativo. Todo lo que se editó en
/ente/seed.card.json a lo largo del día era hasta ahora una declaración SIN PROBAR: un reinicio es
el único examen que toma. Desde fuera, al minuto: siete dominios en 200, el proxy en 407 y
`git ls-remote` por SSH:2345 devolviendo el commit. Cero intervención.

⚠ `reboot` NO reinicia una caja con arje-zero: le pide a PID 1 que lo haga y arje-zero no implementa
ese protocolo — el comando vuelve, no dice nada, y la máquina sigue arriba (`up 4 days` después de
«reiniciar»). Funciona `reboot -f`, que llama a reboot(2) saltándose al init. Falta la pieza: un
init que gobierna producción tiene que atender un apagado ordenado.

🧨 Y CASI REINICIO EL HUB POR UN BACKTICK: el aviso decía «no atiende el `reboot` normal» dentro de
comillas DOBLES, y la shell ejecutó lo de entre backticks — `reboot`, en gioser. No pasó nada porque
la sesión no es root y OpenRC contestó «you must be root». Es la TERCERA vez en el día que un
backtick dentro de un texto se ejecuta; las otras dos sólo se comieron un comentario. La regla deja
de ser de estilo: nunca un backtick dentro de comillas dobles en una shell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:41:08 +00:00
SergioandClaude Opus 5 f57c18ab7f kernel con nf_tables: construido y verificado — y NFT_COUNTER ya no existe
`b3:63fdd57c…` sellado EN LA CAJA (gioser tiene 2 G disponibles y SWAP 0; el worker está horas con
rust) en 21 minutos. Los dos controles, sobre el `.config` SELLADO y no sobre la receta:

· lo que se quería encender: NETFILTER_ADVANCED=y, NF_TABLES=y, NF_TABLES_INET=y, NFT_CT=y,
  NFT_LIMIT=y, NFT_LOG=y, NFT_REJECT{,_INET,_IPV4,_IPV6}=y, NFT_SYNPROXY=y.
· lo que NO podía romperse (el muro 1 del §6.4, que dejó una caja sin ver su disco): SCSI_VIRTIO=y,
  VIRTIO_BLK=y, VIRTIO_NET=y, VIRTIO_PCI=y, SCSI=y, EXT4_FS=y. Los seis siguen.

⚠ `NFT_COUNTER` salió AUSENTE — y está bien: en 7.1 ese símbolo **ya no existe** (los contadores son
parte del core de nf_tables), así que no aparece en el `.config` ni como `# … is not set`. La línea
`-e NFT_COUNTER` es inerte y se deja escrita CON LA NOTA, porque sin ella el próximo que compare la
receta con el `.config` va a leer esa ausencia como un fallo. El comentario no re-hashea: el hash
sigue en `63fdd57c…`, comprobado.

El kernel nuevo queda EN `/boot/bzImage.nuevo`, al lado y no en su sitio, con el vigente copiado a
`/boot/bzImage.sin-nftables`. Hasta que alguien decida, la caja arranca lo de siempre — y ese
camino de vuelta es el único que hay: **hcloud no tiene consola serie**, así que un kernel que no
arranca se arregla con rescue + restaurar el bzImage, no apretando una tecla en el menú de grub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:36:37 +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
Sergio 4de3f2b526 estado: cosecha granja 2026-09-15T21:31:50Z — avance del árbol KDE 2026-09-15 21:31:50 +00:00
SergioandClaude Opus 5 743d83d5e9 el kernel genérico enciende nf_tables — y rust entra a la cola de la granja
Dos cosas a moler, las dos en el worker (que tiene 6 cores, 16 G y 94 G libres), encoladas por
ENLACE a la receta canónica y no por copia: `takana hash` da el MISMO b3 por el enlace que por el
fichero real, así que la cola no puede divergir de lo canónico — que es justo el defecto que el
propio `farm-worker-loop.sh` documenta de las colas viejas («su copia aparcada era la versión
ANTERIOR y seguía leyéndose como deuda abierta»).

· `linux-generic` (b3:63fdd57c…, antes 23f1cc43…): `NETFILTER_ADVANCED`, `NF_TABLES`,
  `NF_TABLES_INET`, `NFT_CT`, `NFT_LIMIT`, `NFT_COUNTER`, `NFT_LOG`, `NFT_REJECT{,_INET}` y
  `NETFILTER_SYNPROXY`/`NFT_SYNPROXY`. Es lo que pide el cortafuegos de tawasuyu (SDD-ENTRADA): la
  tabla `inet`, el estado de conexión y el `limit rate` POR ELEMENTO de set, que es el ban por IP
  dentro del kernel y el reemplazo de fail2ban. SYNPROXY va ahora porque encenderlo después cuesta
  otro kernel y otro reinicio de producción.
· `rust` (b3:aa5d81e8…): el escalón 2 del SDD 31, que estaba en `never`. El worker ya tiene `llvm21`
  sellado con el MISMO hash que el hub (95956a16…), así que arranca desde ahí y no lo reconstruye.

Los dos hashes coinciden hub↔worker, comprobado antes de sembrar: el worker va a sellar exactamente
lo que sellaría acá.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:12:12 +00:00
Sergio e3cf54c4b2 estado: cosecha granja 2026-09-15T21:01:50Z — avance del árbol KDE 2026-09-15 21:01:50 +00:00
Sergio a309ab488e estado: cosecha granja 2026-09-15T20:31:56Z — avance del árbol KDE 2026-09-15 20:31:56 +00:00
SergioandClaude Opus 5 4753a991f1 qdrant, act_runner y fail2ban MUEREN — y el sustituto del último no puede correr todavía
Decisión del usuario sobre lo que quedaba del censo:

· `qdrant` (:6333/:6334) — es la base vectorial de `gioser_api`, y ese backend es un FÓSIL
  (api.gioser.net da 502 permanente desde el §6.9). Una base sin consumidor no se muda. La receta
  queda en el catálogo, que no es lo mismo que la imagen, y eso NO es deuda.
· `act_runner` (:41027) — runner de CI, binario suelto que nadie provee. Si mañana hace falta CI,
  entra por receta y no por un binario copiado.
· `fail2ban` — **tawasuyu ya tiene el sustituto y es mejor**: `shared/cortafuegos` (SDD-ENTRADA §1)
  no mira logs con un daemon, usa DOS SETS DINÁMICOS DEL KERNEL (`ban4`/`ban6`, `flags dynamic`,
  `timeout`) con `limit rate over N/minute` POR IP de origen. El ban lo aplica nftables sin proceso
  y sin latencia.

🧨 PERO EL SUSTITUTO NO ARRANCA: `nft list ruleset` da «cache initialization failed: Invalid
argument». `nft 1.1.6` está instalado y sellado; lo que falta está un piso más abajo, en el
`.config` que publica el artefacto del kernel:

    CONFIG_NETFILTER=y · CONFIG_NF_CONNTRACK=y · **# CONFIG_NF_TABLES is not set**
    y la causa: **# CONFIG_NETFILTER_ADVANCED is not set** (NF_TABLES cuelga de ella)

Es la lección del §6.4 muro 1 otra vez: la receta dice lo que se CAMBIÓ; sólo el `.config` sellado
dice lo que QUEDÓ. `linux-generic` sale de `make defconfig` y defconfig apaga NETFILTER_ADVANCED.

⇒ HOY LA CAJA NO TIENE CORTAFUEGOS DE NINGUNA CLASE, con :22022, :2345, :1137, :80 y :443
públicos. Hay que decirlo entero. La cura es un kernel con NETFILTER_ADVANCED + NF_TABLES (trabajo
del SDD 22) y termina en un reinicio de producción.

MITIGACIÓN HECHA HOY, que no depende del kernel: el `sshd` ofrecía `password` y
`keyboard-interactive` —su config eran CUATRO líneas y ninguna hablaba de autenticación, así que
regía el default de OpenSSH— que es justo la puerta que fail2ban cerraba. Ahora es sólo claves, con
cuatro controles: la clave entra, :22022 y :2345 contestan `Permission denied (publickey)` sin
ofrecer contraseña, y el `git ls-remote` por SSH sigue andando.

La lección: una defensa se muda con su CAPACIDAD, no con su nombre. Decidir «fail2ban muere porque
tenemos algo mejor» es correcto Y deja un hueco abierto hasta que ese algo mejor pueda ejecutarse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:28:29 +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
SergioandClaude Opus 5 b927eeffcb squid corre desde el corpus: la jaula duró unas horas y se tiró
`b3:932ba096…` proyectado en la caja y arrancado por arje SIN bwrap: `pid 11548 · uid=968 ·
exe=/usr/sbin/squid`, con la cuenta `proxy` que declara la receta. La instancia `qorpa/squid` se
borró — el manifiesto es descartable por diseño (ADR 0015 D3) y esto es para lo que existía.

El arreglo de esta tanda: **`--disable-log-daemon-helpers` compilaba perfecto y no arrancaba**. El
default de `access_log` es `daemon:`, asi que sin `log_file_daemon` squid muere con `FATAL:
logfile_daemon /usr/lib/squid/log_file_daemon: (2) No such file or directory`. Sellaba, hasheaba y
pasaba `-k parse` — se ve SOLO levantando el servicio con la config real. Familia
[[subcomando-sin-driver]].

El control que lo caza, y que corre en el hub antes de tocar produccion: con la config REAL del
origen y en un puerto aparte, arranque completo + CONNECT a claude.ai y api.anthropic.com con
TCP_TUNNEL/200 + el access.log escrito por el log daemon + `basic_ncsa_auth` leyendo el `passwd` de
verdad (control negativo: clave mala ⇒ `ERR Wrong password`). Los DOS primeros intentos de esta
receta habrian llegado a produccion sin ese control: el binario estaba sellado y el hash era estable
las dos veces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:04:54 +00:00
Sergio eb7bfb19a9 estado: cosecha granja 2026-09-15T20:03:13Z — avance del árbol KDE 2026-09-15 20:03:13 +00:00
SergioandClaude Opus 5 a394dda26a receta: squid 7.7 entra al corpus — y el --export-dynamic que desactivaba el estático
`b3:54070b26…`, 8,8 M, **estático de verdad** (binario y los tres helpers), y probado con la CONFIG
REAL de produccion: parsea sin FATAL, tunela `CONNECT claude.ai` y `CONNECT api.anthropic.com` con
TCP_TUNNEL/200, y su `basic_ncsa_auth` lee el `passwd` de los tres usuarios (control negativo: con
una clave mala contesta `ERR Wrong password`).

Es la respuesta definitiva a lo de ayer: el proxy se levanto primero en una jaula qorpa porque
estaba EN USO y eso lo destrabo en minutos, pero squid es C++ y el lab lo construye. La diferencia
con php-fpm importa: PHP no queremos que entre al corpus, squid si.

⚠ LA FUENTE NO SALE DE squid-cache.org: esa URL devuelve **200 con una pagina HTML de 8985 bytes**,
no el tarball. Pinear su sha256 habria anclado la receta a una pagina de error. Va el release de
GitHub, que ademas trae `configure` ya generado.

🧨 EL MURO, y es del lab: el primer sellado salio con `usr/sbin/squid` **dinamico y `NEEDED
libc.so`** mientras sus helpers salian estaticos — o sea inerte en cualquier imagen sin cargador
([[needed-colgante-libstdcxx]]) pese a `link = "static"`. La cadena: `squid_LDFLAGS` trae
`-export-dynamic` (para modulos eCAP, que estan apagados) ⇒ **el wrapper del lab quita `-static` de
cualquier enlace que mencione `--export-dynamic`**, a proposito, para poder enlazar las `.so`
dlopen-ables de Python y companhia. Y no alcanza con sacarlo de la variable: **libtool lo vuelve a
anhadir solo** en cuanto hay un `-dlopen`. Se vacia `export_dynamic_flag_spec` en el `libtool`
GENERADO (local al build, sin tocar la fuente) y con guarda: si el campo no aparece, aborta — un
`sed` que no acierta deja pasar el problema y el binario sale dinamico sin que nada falle.

⚠⚠ Y `-dlopen force` NO sobra, aunque lo parezca: vaciar `squid_LDFLAGS` entero rompe el enlace con
`undefined symbol: lt__PROGRAM__LTX_preloaded_symbols`, que emite el propio libtool **porque hay un
`-dlopen`**. De los dos flags, el que estorba es uno solo. Distinguirlos costo un build; adivinar
cuesta lo mismo y no ensenha nada.

Declarado en `perfil.servidor` como paquete Y en `servicios`, con su `[[user]] proxy` y su
`[[service]]` (SDD 30) — que comprueba la config del sitio y la cuenta, y sale 78 nombrando cual
falta en vez de dejar un bucle de reinicios. El hash NO cambio al declararlos: `[[user]]` y
`[[service]]` estan fuera de `hash_inputs`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:49:53 +00:00
Sergio 3fda889599 estado: cosecha granja 2026-09-15T19:32:12Z — avance del árbol KDE 2026-09-15 19:32:12 +00:00
Sergio eeaf9833a9 SDD 26: el piso del sello, en el documento — y por qué se comprueba en vez de confiarse
El commit anterior puso la comprobación en el arnés y el párrafo no entró (el ancla del texto no
casó y el commit salió con un solo fichero: `git show --stat` lo dijo en el segundo, que es para lo
que se mira). Va ahora: los 180 s son de ESTA máquina, con el anfitrión cargado el arranque puede no
llegar al gancho, y como el sello se lee antes y después el arnés avisa en vez de envenenar la
corrida siguiente en silencio.
2026-09-15 19:25:07 +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 005f74209c regla 2: recomponer el párrafo que el commit anterior partió en dos
El aviso nuevo se metió entre `aísla de verdad.` y `Vale también para git commit -F -`, dejando la
mitad del párrafo viejo colgando del final del nuevo. Sólo reordena; no cambia una palabra.
2026-09-15 19:07:11 +00:00
Sergio a1781cb8c2 regla 2: el -- no protege la ruta que vos nombrás — con un fichero en MM commitea el ÁRBOL y borra el índice ajeno
El párrafo de arriba dice que `git commit -- <rutas>` es lo único que aísla de verdad, y es cierto
para las OTRAS rutas. Para la ruta que uno nombra es al revés de lo que uno supone, y quedó medido en
repo de juguete: con el fichero en `MM` —stageado y modificado por otro agente— el commit se lleva el
ÁRBOL, no lo stageado, y el índice queda en la versión del árbol. Lo que el otro tenía stageado ahí
se pierde.

Corolario práctico, que es lo que salvó el trabajo de otra sesión hoy: un fichero que aparece `MM` y
que uno no tocó no se commitea ni con pathspec. Se avisa.
2026-09-15 19:06:57 +00:00
Sergio aa4fd151be SDD 26 §7.quinquies: el cable tiene dos puntas en dos repos, y hoy no coinciden
La bóveda se commiteó esta mañana con sus cuatro enchufes en orden y no funciona igual. El motivo no
está en ninguno de los cuatro: cada función de atuq es un VERBO del host, el host vive en tawasuyu, y
`recipes/puriy-costura.toml` lo pinea por commit. Escribir la extensión y subir el pin son dos
unidades distintas y la primera se commitea sin la segunda sin que nada proteste.

Queda escrito con las dos mediciones y sus controles: el binario SELLADO y vigente tiene «vault» 0
veces contra «cas» 5 —el hermano que tenía que dar positivo, sin el cual el cero no prueba nada— y
el pin es ANCESTRO del commit que agregó los verbos. Más la tabla de las diez extensiones: 13 verbos
sobre 8, y los únicos tres sin dueño son los de la bóveda; o sea que «cuándo se tocó la receta» no
contestaba esto.

También queda la trampa del instrumento, que costó más que el vigía: el extractor buscaba `verb: "…"`
y la extensión de IA arma el mensaje con el verbo en una VARIABLE, así que encontraba cero y la
declaraba sana. De ahí el control de cobertura.

Y por qué el pin no subió en el mismo turno, que es lo que hay que leer antes de intentarlo:
el `Cargo.lock` de tawasuyu no cierra para `puriy-costura` —11 deps viejas, sin `pacha-boveda`, y
`pacha-boveda-daemon` ausente del lock—. Reproducido en árbol limpio del commit: `cargo metadata
--locked` muere con «cannot update the lock file» sin nombrar al culpable, tal como la receta
avisaba; y `cargo metadata --offline` lo cierra con 37 líneas sin mover una versión de registry.

Pero el fichero está `MM` en el clon compartido —otra sesión lo tiene stageado Y modificado, y su
árbol YA trae la reparación sin commitear—, así que no se tocó. Con un repo de juguete quedó medido
por qué el `--` no alcanza ahí, que es lo contrario de lo que uno supone: con un fichero en MM,
`git commit -- <ruta>` commitea EL ÁRBOL, no lo stageado, y el índice ajeno de esa ruta desaparece.
El pathspec aísla de lo que el otro dejó en OTRAS rutas; en la ruta que uno nombra, no.

Y la unidad 12 entra al plan con lo que le falta dicho: el lock de allá, el pin, el rebuild y el
guardián de metal.
2026-09-15 19:06:34 +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 2566bdd3c8 estado: cosecha granja 2026-09-15T19:02:16Z — avance del árbol KDE 2026-09-15 19:02:16 +00:00
SergioandClaude Opus 5 a1ccce2b65 squid mudado — y la jaula arrancaba DEGRADADA cuando la arranca un init
El proxy de salida sirve desde la caja: `squid 7.7` en 2.29.29.217:1137 con la config, el `passwd`
de los tres usuarios y las dos ACL de gioser tal cual, dentro de una instancia qorpa y supervisado
por arje. El de gioser sigue vivo e intacto (7.6). Es un servicio con gente encima: el access.log
del origen mostraba CONNECT a api.anthropic.com y a claude.ai en el minuto anterior a mudarlo.

Control en los dos sentidos: desde una IP no autorizada, 407 igual que el original; desde localhost,
que su propia config permite, TCP_TUNNEL/200 a claude.ai y api.anthropic.com.

EL MURO QUE VALE, y es un bug de qorpa: **el rango de subuid se buscaba por `$USER`**. Una shell
interactiva lo trae; un init NO. Bajo arje la tarjeta arranca con PATH y HOME, el nombre salia
vacio, no habia rango, y la jaula caia al userns de UN SOLO ID — con lo que squid moria en
`setgid(15): (22) Invalid argument` y el supervisor lo reintentaba 80 veces. El aviso de qorpa
estaba impreso y era correcto («no hay rango para "" en /etc/subuid»), pero lo que se ve en el bucle
es el error de squid: **la degradacion silenciosa la paga el de mas abajo**. Y el MISMO comando a
mano funcionaba, porque la shell si pone `$USER`: el sintoma dependia de quien lo arrancaba.

⇒ `nombre_de_usuario()` deriva el nombre del UID REAL leyendo /etc/passwd y el entorno queda como
ultimo recurso, con la mitad pura (`nombre_en_passwd`) probada, control negativo incluido: un uid
que no esta NO inventa un nombre — devolver algo ahi haria buscar un rango ajeno.

Los otros tres muros quedan documentados en §6.27: provisionar ANTES de montar la config (pacman
aborta si el paquete no puede escribir sus defaults), `/dev/shm` que bwrap crea 0755 y squid no
puede usar al bajar a `proxy`, y el pidfile que en una jaula con `--unshare-pid` SIEMPRE parece
fresco porque cada corrida tiene su propio PID 2.

Y la pregunta que corresponde —¿no va en una receta?— con su respuesta: si, y sigue pendiente. La
jaula fue la via urgente. La diferencia con php-fpm importa: PHP no queremos que entre al corpus,
squid es C y su sitio natural es una receta como la de gitea.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 18:59:13 +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 721845fb37 estado: cosecha granja 2026-09-15T18:31:47Z — avance del árbol KDE 2026-09-15 18:31:47 +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 cd678efd06 atuq: el contador de caídas es un TALLY — 0 · 1 · 2, y lo suma el arranque siguiente
Lo que el §6.10.terdecies dejaba escrito como «todavía NO medido» quedó medido: tres fases de 60 s en
un mismo arranque, cada una matada con kill -9 antes de pintar, dan contador 0 · 1 · 2. Uno por
arranque interrumpido y sin techo.

Dos cosas que la serie sola no distinguía, y que valen más que el número:

· el incremento lo escribe el arranque SIGUIENTE, no el kill — el «después» de la primera fase sigue
  ausente (el navegador estaba vivo) y el 1 aparece recién en la segunda. Quien cuenta es el que
  arranca y encuentra el anterior sin terminar;
· un arranque que sigue a una corrida SANA no suma. Eso separa «cualquier arranque incrementa» de
  «sólo después de uno interrumpido», que es la diferencia entre culpar al kill y culpar al arranque
  que no llegó a terminar.

Queda dicho qué sigue sin medir: si la comparación con `max_resumed_crashes` va antes o después del
incremento del propio arranque. Las dos lecturas dan «cuatro interrumpidos seguidos» en la práctica;
la diferencia es poder decir el número sin inventarlo, y se está midiendo cruzando el umbral en vivo.

Medido por la otra sesión del frente (work/contador-1.txt), con la predicción escrita antes de mirar.
2026-09-15 18:09:09 +00:00
Sergio 596b27dcad estado: cosecha granja 2026-09-15T18:02:30Z — avance del árbol KDE 2026-09-15 18:02:30 +00:00
Sergio 8880ce7a7b atuq: el navegador SÍ respeta el xulstore — y el debugfs miente si la VM se acaba de matar
Dos cierres del mismo día, los dos medidos por la otra sesión del frente sobre la imagen:

· la #3 queda contestada: con perfil sano el navegador pinta a +195 s y pide
  `set_window_geometry(1332, 852)` —el tamaño que el `xulstore` recuerda— y acata el
  `configure(1280, 692)` del maximizado. Ninguna cifra es 1152×720, o sea que `browser-init.js` no
  calculó nada: es lo correcto en un perfil que ya recuerda. Y de paso sale el discriminador barato
  entre las dos ventanas: el navegador pide `min_size` 638×120 y el diálogo 117×37;
· y una corrección de MÉTODO a lo que este mismo documento recomendaba ayer: leer el perfil con
  `debugfs` sobre el raw es válido con la imagen apagada, NO para leer lo que dejó un kill. Con el
  journal sucio `debugfs` se planta, con `-c` lee el estado pre-journal y ahí `prefs.js` figura de
  0 bytes — indistinguible de «el navegador borró el perfil», que es más grave y falso.

También queda escrito que la corrida que pinta LIMPIA el contador de caídas (el mecanismo visto
desde el otro lado: explica el «se arregla sola» sin hipótesis) y que una corrida sana no ensucia
el `xulstore`.
2026-09-15 18:00:45 +00:00