Commit Graph
8 Commits
Author SHA1 Message Date
SergioandClaude Opus 5 0f58df4795 planear: el preflight medía contra / — y los datos de gioser no entran ahí
La cadena `censar → planear → aplicar` corrió entera contra gioser por primera vez: 59 pasos, 20
ejecutables y 39 manuales, bien separados. El ensayo en seco destapó lo que ninguna prueba de juguete
iba a mostrar: **33,6 G de datos contra una raíz con 3,5 G libres** (el sitio está en `/work`, 66 G).

Dos cosas estaban mal a la vez:

· **El plan copiaba ruta → MISMA ruta**, sin forma de decir dónde cae cada árbol en el destino. Ahora
  `[[datos]]` tiene `destino` (vacío = la misma ruta). El fallo que evita es caro: aparecía a mitad
  de un rsync de 22 G.
· **El preflight sumaba todo y lo comparaba contra `/`.** Acierta POR CASUALIDAD mientras todo caiga
  en `/`, y da un veredicto completamente falso en cuanto una ruta va a otro montaje — en las dos
  direcciones. Ahora mide por sistema de ficheros, agrupa los destinos, nombra qué rutas caen en cada
  uno, y el veredicto lo saca el propio comando en vez de un humano leyendo una columna.

Los dos controles, contra la caja de verdad:

    ✗ /     necesita 34386 MiB · libres  3596  ⇐ /var/www /var/lib /srv /opt /home   exit 1
    ✓ /work necesita 34386 MiB · libres 66192  ⇐ /work/var/www … /work/home          exit 0

Además, el paso `muere` ahora cumple la regla 2 ENTERA («por su nombre Y CON SU TAMAÑO»). Un dominio
fósil no pesa nada por sí mismo: pesa lo que dejó en disco, y `terapeuta.ec` —que no resuelve en
ningún DNS— tiene 279 M en `/var/www/terapeuta`. Que eso se supiera dependía de que alguien se
acordara. Se lista un renglón POR DIRECTORIO (siete dominios `*.gioser.net` reclaman el mismo
`gioser.bak`; repetirlo siete veces convierte el aviso en ruido) y se dice «podría ser de», nunca
«es»: el directorio se llama `terapeuta` y el dominio `terapeuta.ec`, y emparejar «casi» acierta casi
siempre y el resto de las veces manda a borrar lo que no era. Los directorios que no reclama nadie se
listan aparte: son los que nadie recuerda.

⚠ Y un bug que me hice yo y que el control cazó: empalmar por `str.index('# ── 1. datos ───')` cuando
ese marcador existe en DOS funciones. Tomó el corte al revés (j < i) y DUPLICÓ `def pasos` entero;
Python se queda con la última definición, así que el fichero importaba bien y generaba planes con el
preflight viejo. Se vio porque el comando del plan no era el que yo acababa de escribir. Un marcador
de empalme que no es único no es un marcador.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-12 00:45:27 +00:00
SergioandClaude Opus 5 2fa89ce3fa censar: tres nombres equivocados que llegaban al plan, y los 7 «binario desconocido» eran permisos
El censo nombraba los servicios por `comm`, que viene del kernel. De ahí salieron tres errores de
CLASIFICACIÓN — y no son cosméticos: ese nombre es el que el plan mete en `--in /etc/init.d/<n>` y el
que va de `label` en la tarjeta de arje.

· `comm` está capado a 15 caracteres: `willay-crosscheck` llegaba como `willay-crossche` y con ese
  nombre no casaba contra su declaración ⇒ figuraba como `no-declarado` TENIENDO su tarjeta en la
  semilla de arje. La línea de comando trae el nombre entero.
· `supervise-daemon` no es un servicio, es un ENVOLTORIO. Los cinco de gioser se fundían en una
  entrada `supervise-daemo`; y del otro lado `dbus`, `metalog`, `dhcpcd`, `squid` y `shuma-daemon`
  salían como `declarado-muerto` ESTANDO VIVOS — el mismo agujero que este censo existe para tapar,
  entrando por la otra puerta. El nombre real es su argv[1] y el comando real es el último token
  antes del `--` suelto (comprobado contra los cinco).
· `head -15` con ppid==1 era un resto de tubería reparentado, contado como servicio. Va a
  `descartados`, que se imprimen: un huérfano es un hallazgo, no basura.

**Y los 7 «no se pudo leer su binario» eran dos cosas distintas.** Muchos demonios reescriben su
`argv[0]` (`sshd: /usr/bin/sshd [listener]`, `php-fpm: master process (…)`), así que la línea de
comando no dice cuál es el binario — `/proc/<pid>/exe` sí, y necesita ser dueño o root. Medido:

    uid 1001 : desconocido 7 · paquete-ajeno 13 · receta-takana 6 · suelto 13
    root     : desconocido 0 · paquete-ajeno 20 · receta-takana 5 · suelto 14

Ahora el censo DICE cuál de las dos pasó: «repetilo con sudo» y «averigualo a mano» son trabajos muy
distintos, y confundirlos manda a alguien a investigar un permiso.

Desenvolver al supervisor arregló además dos datos que salían falsos: el `exec` de `squid` era
`/usr/bin/supervise-daemon` (⇒ figuraba provisto por el paquete `openrc`), y el `cmdline` guardado
era el del SUPERVISOR — una tarjeta hecha con eso arrancaría `supervise-daemon` dentro de arje, que
ya supervisa. Y se rescata el `--user`: `shuma-daemon` corre como `sergio`, dato que la tarjeta de
arje no puede guardar (no tiene campo de usuario), así que ahora se avisa EN LA REVISIÓN — sin eso a
la vista, un servicio que acá corre sin privilegios termina de root en el destino.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 22:56:47 +00:00
SergioandClaude Opus 5 73f75d45ed censar: sonda DNS-only (--probe-dns) — y los dominios fósiles vuelven a la lista de decisiones
Censo real de gioser (204.168.193.248, identificado por IP y no por hostname, que dice «momento»):
19 vivos, 18 NO-DECLARADOS, 85 declarados-muertos, 29 dominios.

**El HTTP toca al origen; el DNS no.** Sondear los dominios desde la propia máquina disparó su
fail2ban y la dejó incomunicada, así que el censo en local los salteaba — y quedaban 29 de 66 ítems
SIN recomendación por una precaución correcta aplicada de más. Lo que dispara la jaula es el HTTP:
`getent` le pregunta al DNS, no al servidor. `--probe-dns` sondea sólo el nombre, sin UNA SOLA
petición a gioser, y contesta la pregunta que más pesa en una mudanza: cuáles siguen apuntando acá y
cuáles son fósiles. 29/29 clasificados: 12 apuntan acá, 4 ya se mudaron, 13 NO RESUELVEN.

Lo que el DNS solo no puede decir es si el backend contesta, así que ésos quedan en `apunta-aca`,
clase propia y NO `vivo`: decir «vivo» sin haber pedido una página sería la respuesta falsa con forma
de respuesta que este censo existe para evitar.

**Y un hueco de verdad: los `fosil-sin-dns` quedaban fuera de `entradas()`**, con el argumento de que
un dominio sin DNS no necesita ningún paso. Cierto para el DNS, FALSO para lo que arrastra: son 13 de
29, y `terapeuta.ec` tenía 279 M de contenido y su bloque en el servidor web. Fuera de la lista eran
invisibles, así que nadie decidía borrarlos y sus datos viajaban a la caja nueva por omisión — el
«mudar fósiles» que este plan existe para evitar, y contra su propia regla 2 («lo que muere se dice
por su nombre y con su tamaño, ANTES de borrar nada»). Ahora entran, con recomendación `muere` y el
aviso de revisar qué dejaron en disco.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 21:21:03 +00:00
SergioandClaude Opus 5 6f5cc2629a mudanza: de dónde sale cada binario — y la herramienta corrigió un error mío
«Instalá el paquete» es lo que uno ya sabía. La respuesta útil sale de cruzar dos hechos: quién posee
el fichero en el ORIGEN (el censo se lo pregunta al gestor de paquetes de esa máquina, en un lote) y
si el corpus de takana tiene una receta con ese nombre.

  receta-takana  → instalarlo del repo firmado, NO copiar el binario
  paquete-ajeno  → escribir receta, o qorpa (ADR 0015)
  suelto         → nadie lo provee: llevarlo con su entorno o escribirle receta

Medido sobre gioser (37 servicios vivos): 4 receta-takana · 4 paquete-ajeno · **11 SUELTOS** · 7 sin
binario legible. Los sueltos viven en `/usr/local/bin` o en un home (`qdrant`, `matilda`,
`pacha-secretos`, `act_runner`, `shuma-gateway`) y son los que se pierden al apagar el origen.
Sorpresa medida: `/usr/bin/caddy` tampoco tiene dueño — está puesto a mano.

**Y corrigió un error mío**: en el SDD 28 escribí que `gitea` no tenía receta y que «es la que más
peso tiene». Las dos cosas falsas — `recipes/gitea.toml` existe y está sellada. Lo afirmé de memoria;
el programa fue a mirar. Corregido allá con la nota.

**Y un fallo del clasificador, que vale como regla**: calculaba la raíz del repo con un `dirname` de
menos, no encontraba ninguna receta y contestaba `suelto` A TODO, incluido `caddy`. Un clasificador
que contesta siempre lo mismo no clasifica. Ahora falla ruidosamente si no encuentra el catálogo, en
vez de dar una respuesta falsa con forma de respuesta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 17:02:52 +00:00
SergioandClaude Opus 5 1a89592b72 mudanza etapa 3: declarar.py — de «corre y nadie sabe cómo» a una Semilla que arranca
Copiar un binario no lo levanta al arrancar. Esto toma la invocación que el censo leyó de `/proc` y
emite `seed.card.json`. Medido sobre gioser: **37 de 37 servicios vivos declarados, cero fallos**, y
la tarjeta de caddy sale con su invocación real — exactamente lo que faltaba para que no muriera en
el próximo reinicio.

Por qué no alcanza con `arje-absorb`: absorb lee la DECLARACIÓN del init ajeno, que es justo la que
miente (`rc-status` daba `stopped` para cinco servicios vivos), y los `no-declarado` no aparecen en
ninguna declaración por definición. Absorber la declaración reproduce el agujero. Se complementan.

Tres reglas:
1. Un servicio sin `cmdline` legible NO se emite y se dice: una tarjeta que no arranca es peor que
   una ausente — la ausente falla ruidosamente, la rota deja el servicio caído en silencio.
2. El `cwd` se preserva ENVOLVIENDO, porque el payload `Native` acepta `exec`/`argv`/`envp` y no
   `cwd` (comprobado sobre la semilla real). 9 de 37 servicios de gioser dependen de su directorio.
3. IDs deterministas ⇒ misma entrada, misma semilla BYTE A BYTE (verificado con sha256). Un fichero
   generado que cambia en cada corrida no se puede revisar con `diff`.

**Y un bug del censo que costaba el 70 % del dato**: `cmdline` y `cwd` se leían en un solo comando, y
como `readlink /proc/<pid>/cwd` exige permiso de ptrace, en cualquier proceso de root el comando
entero salía ≠0 y se descartaba TAMBIÉN el `cmdline`. 26 de 37 servicios quedaban sin invocación.
Sondas separadas. **Una sonda que falla es un dato; no puede arrastrar a las que funcionaron.**

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 15:13:53 +00:00
SergioandClaude Opus 5 6f12c231fa mudanza etapa 2: el PLAN — exportable, con el comando literal de cada paso
`planear.py` convierte un censo decidido en un plan ejecutable. Cada paso lleva su COMANDO LITERAL y
su verificación, así que el fichero se ejecuta a mano, línea por línea, sin la herramienta y sin este
repo. Eso es lo que el usuario pidió como «pasos exportables».

Las tres reglas, cada una pagada en el SDD 28:

1. **Nada sin decidir se ejecuta**: aborta con código 2 listando qué falta. El silencio no es
   consentimiento — sin decisión, ni mudar ni matar es correcto. Probado en los dos sentidos.
2. **Lo que muere se dice por su nombre y con su tamaño ANTES de borrar**, y el paso no borra nada:
   es una lista para leer antes de apagar el origen.
3. **Cada copia se verifica EN DESTINO**: el `rsync` que llenó el disco devolvió 0 y dejó 1367
   artefactos vacíos; sólo se vio contando del otro lado.

Y el preflight dimensiona con el tamaño de COPIA (hardlinks expandidos, 60 G → 85 G medidos), no con
`du`; si un tamaño resulta ilegible lo dice en vez de contarlo como 0.

**El añadido que cambia el valor: la invocación real.** Decir «este servicio no está declarado»
nombra el problema; lo que hace falta para resolverlo es cómo corre AHORA. El censo lo lee de
`/proc` (`cmdline` + `cwd`, nunca `environ`: el entorno trae tokens) y el plan lo emite:

    #   cmdline: /mnt/vvv/tawasuyu/target/debug/deps/puerta-f6e393ffbef3999e
    #   cwd    : /mnt/vvv/tawasuyu/shared/tejido
    #   puertos: 34221, 44961

Ese servicio de gioser es un binario de `target/debug/deps/` corriendo en producción, con dos
puertos, que ninguna declaración conoce. Apagada la máquina vieja, eso no se reconstruye de memoria.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 15:03:33 +00:00
SergioandClaude Opus 5 5d1120e583 censar: la sonda de dominios dejó a gioser incomunicado — «sólo lee» era falso
Censar gioser en `--local` disparó 29 peticiones HTTPS a sus propios dominios. Varios devuelven 502,
y su `fail2ban` (jaula `caddy-backend-down`) **baneó la propia IP de la máquina**: gioser dejó de
poder hablar con su propio gitea y los `git push` empezaron a fallar con «Could not read from remote
repository». Un censo que deja la máquina incomunicada no es de sólo lectura.

**La frase del script era la mentira**: «sólo lecturas, no se toca nada». No escribe nada, cierto —
pero leer POR RED tiene efectos. Corregida.

Tres arreglos:

1. **La sonda no se hace contra uno mismo.** Por defecto sólo con `--host`; en `--local` se saltea y
   se avisa por qué, con `--probe` para forzarlo. Además es metodológicamente mejor: lo que interesa
   es qué ve EL MUNDO, no qué ve la máquina de sí misma.
2. **Espaciada** medio segundo entre dominios, para no parecerle un escaneo al fail2ban del objetivo.
3. Clase nueva `sin-sondear`, para que un dominio sin datos no se confunda con un fósil.

Desbaneado 204.168.193.248 de `caddy-backend-down`. Y queda la regla: **antes de sondear una máquina,
pensar qué defensa propia le estás disparando.**

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 14:53:05 +00:00
SergioandClaude Opus 5 95c16097e4 SDD 29 + censar.py: la mudanza como PRODUCTO — censo, plan, aplicación. Sin IA.
Corrección de rumbo pedida por el usuario: el SDD 28 derivó hacia «poner ESTA caja a punto» —
instalar caddy, copiar claves, montar discos—, y eso es configurar un servidor, no construir algo.
**caddy no es un hueco de la imagen: es una instalación particular del usuario, y el programa tiene
que DESCUBRIRLA, no traerla.**

Y la consecuencia de método: todo lo que hice a mano en el SDD 28 ES la especificación de este
programa. Cada paso fue determinista; el único juicio fue «¿esto se muda o muere?», que es justo lo
que se le pregunta al usuario. No hace falta IA: hace falta que esté escrito.

**Etapa 1 implementada: `scripts/mudanza/censar.py`.** Sólo lee. Cruza lo DECLARADO contra lo VIVO y
reporta tres clases, cada una justificada por un error medido:

- `rc-status` decía `stopped` de cinco servicios que estaban VIVOS ⇒ la verdad es `ppid==1`, no el init.
- De 29 dominios, 10 vivos: 13 sin DNS, 2 en 502 y **4 que ya resuelven a otra máquina** — por eso se
  compara la IP del DNS contra las de la máquina, o `ya-mudado` se lee como `vivo`.
- `du -sh` no dice cuánto ocupa COPIAR con hardlinks (60 G vs 85 G): se reportan los dos.
- Los nombres no coinciden (`act-runner`/`act_runner`, `crond`/`cronie`, `dbus-daemon`/`dbus`): sin
  normalizar, el mismo servicio sale a la vez como `no-declarado` y `declarado-muerto`.
- Lo descartado se CUENTA: un `ppid==1` que no es servicio suele ser un huérfano, y eso es hallazgo.

Probado contra gioser, donde las respuestas ya se sabían a mano: encuentra MÁS (29 dominios contra
los 19 que probé; apareció `hifas.gioser.net`). Y afinarlo importó: la primera versión daba 28
`no-declarado` con ruido, la segunda da 18 y son reales (`matilda`, `pacha`, `puerta-…`, `adb`).
Un guardián con hallazgos falsos se ignora entero.

El SDD deja escritas las etapas 2 (plan exportable: el fichero ES la interfaz) y 3 (aplicación
idempotente que verifica EN DESTINO y deja el server corriendo), y mide qué ata la imagen a Hetzner:
menos de lo que parece — BIOS vs UEFI y los metadatos de red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 14:51:08 +00:00