`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>
`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.
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>
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>
`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>
`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>
`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>
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.
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.
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.
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.
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.
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.
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.
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.
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>
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.
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.
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.
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.
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`.
Los dos sitios que quedaban con trafico real sirven desde la caja `takana`, con TLS publico y
verificados desde fuera. Los servicios viejos SIGUEN corriendo en gioser a proposito (decision del
usuario: mover el DNS y dejar el origen vivo, para poder volver en un minuto).
· `tawasuyu.net` + `www`: la raiz en gioser era EL MONOREPO ENTERO (145 G) servido por HTTP. El log
de accesos dice que las unicas rutas con trafico son `/`, `/descargas` y `/web`, asi que se
replicaron solo los dos subarboles que el sitio usa (1,2 M + 3,8 G) con la MISMA estructura, para
que cada `rewrite` siga siendo el mismo.
· `gioser.net` + `www`: 428 M de estaticos + `/reencuentro`, que necesita PHP. `/hooks/*` NO se
muda: lo atiende `webhook-deploy.py`, que redespliega aura/sigma/summa/brahman — los cuatro
FOSILES del §6.19 — y tiene cero peticiones. Muere con la caja vieja.
· `sergio` dejo de ser `CNAME -> www` y tiene su A propio (apuntando a gioser, sin cambio visible).
Sin eso, mover `www` se lo llevaba puesto: en esa zona casi todo cuelga de `www`.
PHP no entra al corpus y no hace falta: `php-fpm 8.5.10` corre en la instancia `gioser-php` (ADR
0015), supervisado por arje. Control contra el original en los dos sentidos: POST con la trampa
anti-robots da `{"ok":true}` 200 y GET da 405, igual que en gioser.
EL MURO, que es general y no de PHP: para montar en `/work/www/gioser-web`, bwrap crea `/work` y
`/work/www` —que la imagen no trae— y los crea **0700 root**. Adentro somos root y a mano todo
funciona; pero un servicio que BAJA de privilegio (php-fpm a `http`, o cualquier instancia con
`run_as`) no puede ni atravesarlos, y el sintoma es un «File not found» sobre un fichero que ESTA.
Medido con el control que lo separa de un problema de permisos del anfitrion: como `http`, leer un
fichero de la IMAGEN funciona y leer el directorio CONCEDIDO da Permission denied.
`grants_to_args` crea ahora los ancestros que faltan con `--perms 0755 --dir`, y SOLO los que la
imagen no trae: hacerlo sobre `/etc` o `/home` le cambiaria los modos a la imagen. Con test, y
probado rompiendolo a proposito (sale `["/etc","/etc/php"]` en vez de `["/etc/php"]`).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cuatro secciones discutiendo por qué la ventana de atuq salía de 117x70, y no era la ventana de atuq.
Lo dice el mismo volcado de protocolo que ya se había leído, dos líneas más abajo de donde paró la
lectura:
558 -> xdg_toplevel#58.set_title("Open atuq in Troubleshoot Mode?")
Es safeMode.xhtml, el diálogo de Modo de resolución de problemas. La cadena, cada eslabón con su
fuente y ninguno deducido:
1. el perfil del usuario traía `toolkit.startup.recent_crashes = 16` (prefs.js del 15-Sep 00:36,
ANTES de las corridas del día; leído con debugfs sobre la partición, sin montar y sin root);
2. el umbral de NUESTRO build es 3 (browser/omni.ja -> defaults/preferences/firefox.js:831);
3. BrowserGlue.sys.mjs:389 abre el diálogo MODAL en `_beforeUIStartup()`, que por su propio
comentario corre «before the first window is opened»: no hay navegador detrás esperando;
4. el `set_max_size(348, 16332)` sale del Fluent del diálogo (`max-width: 400px`);
5. y el proceso se va solo con `ATUQ-EXIT=0` porque el único camino de ese diálogo que termina el
programa es `onCancel()` -> `quit(eForceQuit)`. Quién lo cancela no está medido.
El control, en los dos sentidos, mismo artefacto y mismo perfil: con el contador limpio pinta a
+123 s (704.458 px); fijado en 16, nada en 300 s y vuelve el diálogo. Y una honestidad que cambia la
fuerza del argumento: en la corrida limpia el `--crashes 0` fue un NO-OP —Gecko ya lo había
borrado—, así que la que prueba la causa es la que lo pone de vuelta y rompe de vuelta.
Cae también la sospecha del §6.10.duodecies: con `WidgetScreen:5`, Gecko ve la pantalla completa
(1280x800 de workarea) CINCO SEGUNDOS antes de crear la ventana. No había carrera con wl_output.
Y lo más caro: la serie de primera-pintura.json está contaminada por el propio andamiaje. El arnés
mata la VM con el navegador vivo, cada arranque sin terminar es una caída para Gecko y pasados 3 el
sujeto medido deja de ser el navegador. «3 de 13» y «2 de 10» midieron un perfil que se degradaba
corrida a corrida. Lo que cada corrida sume exactamente NO está medido y así queda escrito.
Queda una decisión de producto que no se mete de paso porque re-sella el artefacto: si atuq debe
traer `toolkit.startup.max_resumed_crashes = -1`.
`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.
El primer servicio AJENO que corre sobre takana. 200 con TLS publico desde 2.29.29.217 y
`/api/chat/` contestando con Gemini de verdad, con el backend en una instancia `qorpa` sobre el
rootfs de Arch pineado por sha256, supervisado por arje-zero como el ente `sergioh-api`.
Es el que §6.21 llamo «lo que decide la fecha de borrado de gioser»: su venv trae extensiones
`cpython-314-...-linux-gnu.so`, o sea glibc, y no tiene camino a musl. La coincidencia que lo hizo
barato: el Arch pineado trae python 3.14.7 y el venv de gioser es 3.14.6 — misma serie, asi que el
venv se REHACE adentro con pip en vez de copiarse.
La seccion trae ademas el muro (la jaula no viajaba en ninguna imagen — commit anterior), los tres
tropiezos del camino (el uid 1001 del rsync que cae fuera del userns; una instancia admite UN solo
`run`; `ps` que no ve procesos y `dig` que no existe, dos ausencias que se leen como diagnostico) y
lo que quedo declarado: `instance.toml` con un unico dir concedido, las 84 deps pineadas en
`requirements.lock`, y la tarjeta en cards.d Y en el genesis.
Y una correccion de rumbo en §9: la lista «lo que falta para el cutover» era del 11-09 y sus pasos
1-5 ya estan hechos. Lo que sigue vivo en gioser son TRES vhosts, no quince.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mudando `api.sergio.gioser.net` a la caja de produccion, `takana qorpa provision` aborto:
Error: no encuentro harkaq-exec en /root/.cache/harkaq/harkaq-exec
construilo: gcc -O1 -Wall -static -o ... scripts/harkaq/harkaq-exec.c
El mensaje es bueno y la receta que propone es IMPOSIBLE de seguir: en una caja instalada no hay
gcc, ni `scripts/`, ni arbol de desarrollo. El binario solo existia como un `gcc` a mano en la
cache de `$HOME` del hub, o sea que la jaula del ADR 0015 funcionaba unicamente en la maquina
donde alguien la habia compilado.
Y su companero estaba igual, medido en `build-state.json`: `bwrap` sellado desde hace meses con
`"perfiles": []` — CERO perfiles. Lo invocan por PATH tanto `qorpa` como el sandbox de
`takana build` (`takana-build/src/sandbox.rs`), asi que **ninguna imagen de takana podia enjaular
nada, ni construir**. Es [[subcomando-sin-driver]] un piso mas abajo: el CLI que los llama viaja
en todas las imagenes y sus herramientas en ninguna.
Tres piezas:
· `recipes/harkaq-exec.toml` — nueva. Estatico musl, `b3:cd34954f...`, 269 K. El pin va al commit
que toco la FUENTE (`1d9ddcee`, 2026-09-03) y no a HEAD: el `.c` no se mueve desde entonces y el
repo commitea cada media hora por el cron de la cosecha — pinear HEAD re-hashearia la receta cada
media hora sin que su fuente cambiara. La fuente sigue en `scripts/harkaq/` y no en un arbol
propio porque tres scripts la compilan desde ahi y dos copias divergen en silencio.
· `qorpa.rs` — busca el binario tambien en `/usr/bin`, que es de donde sale en cualquier maquina
que no sea el hub. El orden es HARKAQ_BIN (lo que el operador declara) → arbol de desarrollo →
paquete, para que un cambio en la jaula se pruebe sin instalar nada. Y el error ya nombra las
dos salidas, no solo la del hub.
· `targets.toml` — los dos en `perfil.base`.
Medido en la caja de produccion tras aplicarlos: `bwrap 0.11.0` + harkaq-exec responden, y
`takana qorpa provision sergioh-api` instala python 3.14.7 con pacman DENTRO de la jaula
(`[harkaq] jaula puesta: ABI 9`). El 3.14 no es casualidad: el venv de gioser trae extensiones
`cpython-314-...-gnu.so`, asi que la imagen de Arch pineada da la misma serie.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.