Files
takana/docs/28-servidor-de-produccion.md
T
SergioandClaude Opus 5 3870ca3566 SDD 28 §3.6-3.8: la caja corre takana puro — y el muro era la puerta de enlace, no el arranque
Puerta 1 pasada: PID 1 `arje-zero`, kernel 7.1.2 propio, `/dev/sda3 -> /store` y
`/dev/sda4 -> /var/lib/hammer` montados por etiqueta, IP y salida por `netup`, y se entra por SSH.
Una caja Hetzner sin una línea de Debian debajo. Escribir los 7,1 G comprimidos: ~17 s.

Queda escrito lo que costó, que es lo reusable:

- **El kernel del corpus no sirve para hcloud** (`linux.toml` apaga SCSI; el disco es virtio-SCSI).
  `linux-generic` sí, y su `CONFIG_SCSI_VIRTIO=y` no está en la receta: lo pone `defconfig` y sólo se
  ve en el `.config` que el artefacto publica. La receta dice lo que se cambió; el `.config` sellado
  dice lo que quedó.
- **La hidratación pisa `/sbin/init`** con el de busybox: la imagen habría arrancado perfecta con el
  init equivocado.
- **La puerta de enlace de Hetzner está FUERA del prefijo** (IP `/32`, gw `172.31.1.1`). Sin ruta
  on-link el default se rechaza y la caja queda con IP y sin salida — arranca, levanta sshd y no se
  puede entrar. Nunca había aparecido porque netup sólo se probaba contra el slirp de QEMU, que da
  un /24 con la puerta dentro: el único entorno de prueba no contenía el caso.
- **Un binario dentro del `product-rootfs` está congelado**: arreglar netup no llegaba a la imagen
  hasta declararlo raíz del perfil.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-10 20:16:43 +00:00

30 KiB

SDD 28 — El servidor de producción: instalar takana puro en una caja remota y mudarse a ella

Escrito 2026-09-09 a pedido del usuario («qué tan lejos estamos de montar un server takana en Hetzner y que sea producción, y mover allá todo lo que tengo acá, y de una vez mantenerlo por repo takana»). Todo número marcado (medido) salió de un comando corrido ese día sobre gioser.

Continúa SDD 19 (lanzamiento público) y SDD 27 (metapaquetes e instalador). No repite el instalador: SDD 27 decide QUÉ se instala y con qué verbo; esto decide DÓNDE, y cómo se llega.


0. La corrección de encuadre: ya estamos en Hetzner

gioser es un servidor Hetzner (hcloud id 126324694, hel1, 4 cores / 7,6 GiB) (medido). La pregunta no era «cómo llegar a Hetzner» sino cómo tener una caja de takana en vez de ser un inquilino más de una máquina compartida que:

  • sirve 19 dominios por Caddy,
  • tiene /mnt/vvv al 89 % (27 G libres) (medido),
  • es fija y sin backup por pedido explícito del usuario (blindaje anti-borrado en deadman.sh y farm-down.sh, ver ADR 0014 y la memoria del proyecto),
  • y no publica nada de takana: cero entradas de takana en el Caddyfile (medido).

0.1 El hallazgo que cambia el tamaño del problema

$ ps -p 1 -o comm=,args=
arje-zero  /usr/local/sbin/arje-zero
$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-linux root=UUID=54de62df-… rw net.ifnames=0 splash \
  init=/usr/local/sbin/arje-zero loglevel=4

El init de takana ya gobierna un servidor de producción real. Caddy, gitea, sshd, qdrant y act-runner cuelgan directo de PID 1 (medido: ps -eo pid,ppid,comm | awk '$2==1').

Pero eso no es takana puro: es Artix con nuestro init inyectado por init= en el cmdline. El kernel, la libc y los 19 servicios son ajenos. Se probó la mitad difícil sobre userland prestado.

0.2 ⚠ La declaración del arranque no existe en ningún sitio legible

rc-status de OpenRC dice stopped para caddy, gitea, sshd, cronie y act-runner, y los cinco están vivos (medido, los dos comandos). /etc/arje/cards.d tiene 4 tarjetas (acpid, openrc-openclaw, squid, zram-swap) y ninguna de ellas los levanta.

La secuencia de arranque de esta caja no está escrita. Es lo menos reproducible de toda la mudanza y lo primero que hay que capturar — ver §4.


1. Inventario de lo que hay que mudar (medido 2026-09-09)

Datos en /mnt/vvv (217 G de 255)

tawasuyu 124 G
takana 91 G — incluye el store de 55 G bind-mounteado desde el otro volumen ⇒ ~36 G propios
rustup 29 G
act-runner · gioserv · _respaldo 6,5 · 5,4 · 4,1 G
~20 dirs *-standalone agora, card, chasqui, cosmos, dominium, khipu, minga, mirada, nakui, nahual, pata, pineal, pluma, shuma, takiy, llimphi…

El volumen harkaq-cosecha (250 G, 148 G usados)

store 55 G, 1327 artefactos — lo irreemplazable
work-sources + cargo-home + gopath ~85 G — caché regenerable, no se muda
work-tarballs · devfs-cache · work-out 3,2 G · 4,5 G · 397 M

El store no se copia por red. Los dos volúmenes están en hel1; un volumen hcloud se desengancha de una caja y se engancha a otra en la misma ubicación. La mudanza del store son dos comandos.

Servicios vivos, todos hijos de arje-zero

caddy :80/:443 · gitea :3002 · sshd :2345 · act_runner (el CI de tawasuyu) · qdrant :6333/:6334 · openclaw · zeroclaw · shuma-gateway :7378 · tejido :4102 y :33097 · puerta-f6e393ff · uvicorn :8771 · python3 :8770 · squid :1137 · fail2ban · glances · php-fpm · metalog · adb :5037.

Lo que no está en ningún repo

/var/lib/gitea (2,2 G: SQLite + los repos de git de todo, incluido el origin de takana) · /var/www (287 M) · /home/sergio/gioser-web/ · /etc/caddy/Caddyfile + 20 ficheros .bak · los certificados de letsencrypt · los crontabs · /etc/arje/cards.d · las 3 credenciales de ~/.config/hammer/*.env · las llaves ~/.ssh/{github5,sergio} · el token hcloud · y la memoria del agente en ~/.claude/.

1.1 La basura, contada — porque una mudanza fiel copia también la basura

De los 19 dominios del Caddyfile (probados uno por uno desde la propia caja):

responden 200 rotos
gioser.net · tawasuyu.net · git.gioser.net · summa.gioser.net · sergio.gioser.net api.gioser.net → 502 (backend caído)
aura · sigma · kosmofono · terapeuta.ec → sin DNS

Y sus raíces no existen en disco: /var/www sólo tiene terapeuta, goaccess, ogisoer, git-tawasuyu, letsencrypt y un .bak — no hay aura_frontend, ni sigma, ni summa/frontend, ni kosmofono. La mitad del Caddyfile describe un servidor que ya no existe.

Regla de la mudanza: no se muda lo que no responde. Lo que no tiene DNS ni ficheros se anota y se deja morir con la caja vieja. La utilidad del §4 tiene que saber decir declarado / vivo / fósil.


2. La caja: qué, cuánto y por qué

Precios consultados a la API de hcloud el 2026-09-09 (hel1, x86, tipos no deprecados) (medido):

tipo recursos €/mes
cx53 16 c / 32 G / 320 G 29,49
cpx41 8 c / 16 G / 240 G 32,49
ccx23 (dedicado) 4 c / 16 G / 160 G 85,99

Elegido cx53: es el mejor punto de la tabla y entra holgado el conjunto tawasuyu + takana + rustup (~244 G) si algún día se muda todo.

Decisiones del usuario (2026-09-09):

  1. La caja PUBLICA; el LXC prestado sigue moliendo. dev.gioser.net cuesta €0 y es la máquina con más RAM (6 c / 16 G + 8 G swap). Separar servir de compilar mantiene la caja chica y barata. La caja nueva puede además compilar, con el LXC enchufado como segundo worker.
  2. «Producción» = operativo pero privado. Sin descarga pública todavía ⇒ no se activan los bloqueantes legales del SDD 19 §2 (espejo de fuentes público por HTTP, marca de terceros). Lo que sí hace falta igual es la clave de release estable (§5).
  3. gioser se BORRA cuando la mudanza esté comprobada. Eso convierte esto en un experimento con fecha de caducidad, y cambia el diseño entero: ver §6.

El blindaje anti-borrado de gioser es deliberado y hay que levantarlo a mano y a propósito. deadman.sh y farm-down.sh lo protegen por lista negra de nombre y por ausencia del label role=hammer-worker. Ninguna automatización debe poder borrarlo: cuando llegue el día, se borra a mano, con la comprobación del §6 en verde y no antes.


3. Instalar takana puro en una caja remota

3.1 Lo que ya está, y por qué está más cerca de lo que parece

El dato que lo destraba: Hetzner Cloud arranca por BIOS, no por EFI. (medido en gioser: no existe /sys/firmware/efi; GPT + GRUB; /boot es vfat de 1 G.) Y scripts/takana-install.sh escribe exactamente ese layout: GPT + GRUB BIOS, SeaBIOS → GRUB → kernel → arje-zero PID1. El camino EFI —que fue el que más trabajo costó (ADR 0010)— es el que Hetzner no necesita.

pieza estado (medido)
kernel con virtio recipes/linux.toml compila VIRTIO, VIRTIO_PCI, VIRTIO_BLK, VIRTIO_NET, EXT4_FS =y ⇒ sin initramfs
red crates/netup = cliente DHCPv4 propio en Rust sobre netlink, autodetecta interfaz y espera carrier. gioser toma su IP por DHCP en eth0 (/32, net.ifnames=0, DNS 185.12.64.2) ⇒ es justo su caso
acceso recipes/openssh.toml sellado; el product-rootfs vigente ya trae sshd; takana-install.sh acepta AUTHKEYS
servir recipes/caddy.toml existe
escritura a disco scripts/takana-install.sh (no interactivo) y scripts/takana-live-install.sh (TUI), probado por scripts/install-tui-test.sh

3.2 El camino remoto: rescue → dd → reboot

Hetzner Cloud tiene un sistema de rescate (Debian por SSH) que se activa desde la API sin tocar la caja. Desde ahí se escribe la imagen al disco y se reinicia. No hace falta ISO, ni consola, ni medio físico — que es justo lo que hace innecesario el 90 % del trabajo de ISO/USB para este caso.

3.3 Los cuatro huecos, nombrados

  1. perfil.servidorHECHO hoy en docs/state/targets.toml. Ver §3.4.
  2. console=ttyS0 en el cmdline — el único salvavidas si el arranque falla y no hay teclado. Hetzner da consola web; sin serial en el kernel, esa consola no muestra nada útil.
  3. IPv6netup hace DHCPv4 y Hetzner entrega un /64 estático. La caja arrancaría con IPv4 sola. Aceptable para empezar, pero hay que saberlo antes, no descubrirlo.
  4. Un .img del perfil servidor que el rescue pueda escribir. Es hydrate-profile.py + install-image.sh, los dos ya existentes — ver §7.

3.4 El perfil, y la deuda que declara (hecho 2026-09-09)

[perfil.servidor] hereda cli (que hereda base, SDD 27 §7.1) y añade 5 raíces que existen (openssh, caddy, curl, wget, tmux) y 5 que NO:

raíz que no resuelve por qué está declarada igual
takana el propio takana no tiene receta. El corpus construye 869 recetas y no la suya ⇒ el binario sólo existe como cargo build sobre un clon. Es EL bloqueante del §5
chrony sin NTP, un reloj que deriva rompe TLS y las firmas — y falla diciendo «firma inválida», no «la hora»
nftables dev.gioser.net está hoy expuesto con fuerza bruta a root en el journal; una caja nueva no debería nacer así
cronie el latido son tres líneas de crontab, y sin systemd no hay timers: o hay cron, o el latido es una tarjeta de arje-zero. Decisión pendiente
logrotate en una caja que sirve, el log de acceso crece hasta llenar el disco

Quedan wanted en el grafo, y wanted no es debtdrenaje.json seguirá diciendo deuda=0. Se declaran igual porque una deuda escrita se ve y una omitida no: sin ellas el perfil saldría N/N —completo y verde— describiendo un servidor sin hora, sin cortafuegos y sin latido. Es la lección de foot en escritorio-sway (121/121 sellado y sin emulador de terminal), pagada por adelantado. Cuenta esperada: 5. Un sexto wanted es un error de tipeo, no una receta nueva.

3.5 La caja, creada (2026-09-10)

nombre / id takana / 165447050
tipo cx33 — 4 c / 7,6 GiB / 76,3 G — €8,49/mes
ubicación hel1 — la misma que los volúmenes vvv y harkaq-cosecha y que el Storage Box
IPv4 / IPv6 2.29.29.217 · 2a01:4f9:c014:2f9c::/64
imagen inicial debian-13desechable: existe sólo para recibir ficheros hasta el dd del §3.2
label role=takana-server
protección de borrado NO, a propósito: es la caja de sacrificio

El label importa: farm-down.sh y deadman.sh borran sólo lo que lleva role=hammer-worker. Esta caja no lo tiene ⇒ ninguna automatización de la granja puede tocarla. Es la misma capa fuerte que protege a gioser, aplicada al revés: no se protege por nombre, se protege por no haber nacido de farm-up.

cx53 no se pudo comprar: sale Available: no en las TRES ubicaciones. Y la disponibilidad cambia por díacx33 salió no en hel1 el 2026-09-09 y yes el 2026-09-10, con la misma consulta. ⇒ el precio de la tabla no dice que la máquina exista: hcloud server-type list devuelve precios para tipos que no se pueden crear. Consultar Available por ubicación, y volver a consultarlo el día que se compra.

Lo que la caja confirma, medido EN ELLA y no inferido de gioser

  1. Arranca por BIOS — no existe /sys/firmware/efiy sin embargo la imagen trae un ESP (sda15, 244 M vfat en /boot/efi). O sea: la presencia de una partición EFI no indica arranque EFI. Si el instalador decidiera la rama mirando particiones en vez de /sys/firmware/efi, elegiría mal en esta caja exacta. takana-live-install.sh mira lo correcto.
  2. console=tty1 console=ttyS0 viene en el cmdline de Debian. Confirma que el serial es lo que Hetzner espera, y que el hueco 2 del §3.3 es real: nuestra imagen tiene que traerlo o la consola web no muestra nada.
  3. eth0 con /32 dinámico por DHCP — idéntico a gioser ⇒ es exactamente el caso de netup.

Dos cosas pendientes en la caja, anotadas

  • 7,6 GiB de RAM y CERO swap — la misma clase de máquina que ya provocó global_oom en gioser. Coherente con la decisión de §2.1 (la caja publica, el LXC muele), pero significa que no se construye pesado acá mientras siga así.
  • sshd trae PasswordAuthentication yes (con PermitRootLogin without-password, así que root por clave igual). El journal del LXC muestra fuerza bruta a root desde IPs random; esta caja está igual de expuesta. Se cierra cuando se instale takana puro, o antes si se queda días con Debian.

3.6 La imagen del perfil.servidor, armada y validada (2026-09-10)

Receta del ensamblado (todo con scripts que ya existían, §7):

# 1. base con init: el product-rootfs sellado (trae /sbin/init -> /usr/bin/arje-zero)
cp -al /mnt/cosecha/store/5011955a…-product-rootfs/. $D/
# 2. userland del perfil encima
scripts/hydrate-profile.py servidor --into $D --store /mnt/cosecha/store --keep
# 3. ⚠ restaurar el init (ver abajo) y meter la clave
ln -sf /usr/bin/arje-zero $D/sbin/init ; cp github5.pub $D/root/.ssh/authorized_keys
# 4. imagen
ROOTFS=$D STAGE=/mnt/cosecha/.install-stage KERNEL=store/23f1cc43…-linux-generic/boot/bzImage \
CMDLINE="console=ttyS0,115200 console=tty0 root=/dev/sda2 rw init=/usr/bin/arje-zero" \
  ./scripts/install-image.sh

Resultado: 86 nodos, 30.500 ficheros, 2,0 G de rootfs → imagen de 7.172 M (2,1 G en disco, sparse).

El kernel correcto es linux-generic, y linux.toml NO sirve para hcloud

El disco de una caja hcloud lo maneja virtio_scsi (driver sd, disco sda) — no virtio_blk. recipes/linux.toml apaga SCSI explícitamente (-d SCSI) ⇒ su kernel arranca y no ve el disco. linux-generic, linux-metal y linux-metal-dual sí sirven.

Y ninguna receta nombra SCSI_VIRTIO. Está encendido igual, porque make defconfig lo trae y -e SCSI -e BLK_DEV_SD lo mantiene. Se supo mirando el .config que linux-generic publica en su artefacto (/boot/config-7.1.2-generic), no leyendo la receta. La receta dice lo que se cambió; sólo el .config sellado dice lo que quedó.

⚠ La hidratación PISA /sbin/init, y el resultado arranca igual

busybox entra en la clausura del perfil y su /sbin/init -> ../bin/busybox gana por llegar después, borrando el -> /usr/bin/arje-zero del product-rootfs. La imagen habría arrancado perfecta con el init de busybox: no takana puro, y sin un solo error. Se tapa por las dos puntas — restaurando el symlink al ensamblar y con init=/usr/bin/arje-zero explícito en el cmdline, que es lo que hace gioser y es autoritativo pase lo que pase con el symlink.

Y la guarda que existía para esto estaba INVERTIDA (arreglada en el mismo commit): [ -e "$ROOTFS/sbin/init" ] sigue el symlink y lo resuelve contra el host, así que el symlink ABSOLUTO a arje-zero daba falso y el rootfs correcto era rechazado, mientras que el relativo a busybox —el equivocado— pasaba. Ver guardian-que-nunca-fallo: una guarda que nunca falló no se sabe si sirve; ésta fallaba al revés.

EXDEV, dos veces en la misma tarde

store y work/out son bind-mounts de /dev/sdb; work/ está en /dev/sdc. Un hardlink no cruza un montaje aunque sea el mismo disco, así que reventaron (1) hydrate-profile.py con el destino fuera del montaje del store —que avisa RUIDOSO y bien— y (2) el staging de install-image.sh, que lo tenía cableado a work/. Ahora STAGE es env. Ver hardlink-cruza-mount-exdev.

La validación: arrancó en QEMU reproduciendo virtio-scsi, y se entró por SSH

SeaBIOS → GRUB → kernel 7.1.2 propio → EXT4-fs (sda2): mounted → Pivoted into new rootfs
→ Run /usr/bin/arje-zero as init process → "arje-zero: despierta como PID 1"
→ netup (DHCP) → ssh-keygen -A → sshd

Y desde fuera: ssh -p 2299 root@127.0.0.1 entra, /proc/1/comm dice arje-zero, ip addr da la IP que negoció netup. Es la puerta 1 del §6 ensayada en local antes de gastar la caja.

El card sshd de /ente/seed.card.json ya hacía el trabajo entero sin que hubiera que tocar nada: sh -c "/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e" — red, claves de host y demonio en una línea. Lo único que hubo que inyectar fue authorized_keys.

Un resto del renombre, encontrado por el arranque

arje-zero: hammer no corre, sin menú de arranque: No such file or directory (os error 2)

arje-zero invoca el binario por el nombre VIEJO (hammer). No es fatal —sigue arrancando— pero el menú de arranque por grafo del ADR 0010 queda muerto. Y viene de tawasuyu, pineado por commit en recipes/arje-zero.toml, así que no se arregla desde este repo: hay que subirlo allá y re-pinear. Se suma a que la imagen tampoco trae takana (§3.4).

3.7 ⚠ El muro real: la puerta de enlace de Hetzner está FUERA del prefijo

La imagen validada en QEMU se escribió a la caja, arrancó… y no se pudo entrar. hcloud decía running, el puerto 22 cerrado, y desde fuera Connection timed out — el modo de fallo más caro de todos, porque no hay consola serie en hcloud para mirar.

La causa, medida entrando al rescue de la propia caja:

inet 2.29.29.217/32 scope global eth0
default via 172.31.1.1 dev eth0
172.31.1.1 dev eth0 scope link      ← ESTA es la que faltaba

La IPv4 viene con prefijo /32 y la puerta (172.31.1.1) no pertenece a ninguna red conectada. netup mandaba el default con RTA_GATEWAY y scope UNIVERSE y nada más, así que el kernel lo rechazaba con ENETUNREACH: la máquina se quedaba con IP y sin salida, contestando el DHCP y levantando sshd, sin nada visiblemente roto por dentro.

Control causal, en un netns con una dummy y una /32:

ip route add default via 172.31.1.1 dev dummy0   ⇒ Error: Nexthop has invalid gateway.
ip route add 172.31.1.1 dev dummy0 scope link    ⇒ ok
ip route add default via 172.31.1.1 dev dummy0   ⇒ ACEPTADO

Arreglado con add_onlink_host_route (dst /32, scope LINK) antes del default.

Por qué no había aparecido nunca: netup se validaba contra el DHCP de QEMU slirp, que entrega un /24 con la puerta dentro de la subred. El escenario que rompe sólo existe en una nube de verdad. Es cache-congela-regresiones en su otra forma: no es que el test estuviera mal, es que el ÚNICO entorno donde se probaba no contenía el caso.

Y destapó un segundo problema de fondo: el binario de netup venía dentro del product-rootfs, o sea congelado en un artefacto sellado del bootstrap ⇒ arreglar netup no llegaba a la imagen. Por eso netup pasa a ser raíz de perfil.servidor: así la hidratación lo proyecta encima y la imagen lleva el vigente. Verificado por sha256 (imagen a23df20e…, product-rootfs a84b0a0d…).

3.8 Puerta 1, pasada (2026-09-10)

PID 1    : arje-zero          kernel : 7.1.2 (linux-generic propio)
IP       : 2.29.29.217        rutas  : default via 172.31.1.1 + 172.31.1.1 scope link
montajes : /dev/sda3 -> /store      /dev/sda4 -> /var/lib/hammer
salida   : ping 1.1.1.1 0% pérdida · curl http://1.1.1.1 -> 301 · resolv.conf escrito por netup

Una caja Hetzner Cloud corriendo takana puro: nuestro kernel, nuestro init, nuestro cliente DHCP, nuestro sshd — sin una línea de Debian debajo. El camino entero es scripts/servidor-image.sh + rescue + dd, y la escritura de los 7,1 G comprimidos tarda ~17 s (gioser y la caja están las dos en hel1).

Lo que la caja todavía no tiene, y es el paso 2: takana (sin receta, §3.4), el store real (el volumen no se engancha hasta acá) y las otras cuatro raíces wanted.


4. La utilidad de migración: absorber lo VIVO, no lo declarado

arje-absorb ya existe, está sellado (recipes/arje-absorb.toml) y su --help dice: lee sysvinit | runit | dinit | openrc | auto y emite una Tarjeta Semilla con cada servicio del init ajeno como hija de arje-zero. Tiene parsers para los cinco (systemd.rs, openrc.rs, runit.rs, dinit.rs, sysvinit.rs).

Pero lee la declaración, y en gioser la declaración miente (§0.2). Un arje-absorb --from openrc sobre esta caja produciría una Semilla que no levanta el servidor, y lo haría en silencio: la Semilla sería sintácticamente perfecta.

⇒ Lo que falta es un segundo lector: absorber del proceso vivo (/proc: ppid==1, cmdline, cwd, entorno, fds en LISTEN) y reconciliar ambas lecturas marcando dónde discrepan. Eso es lo que el usuario nombró como «lo aprendido de matilda» — absorber lo que hay, no lo que está escrito.

La utilidad debe clasificar en tres, no en dos:

clase criterio qué se hace
vivo proceso corriendo y declarado se muda
declarado, muerto en la config, sin proceso ni ficheros (los 4 dominios sin DNS) fósil: se anota y se deja morir
vivo, no declarado proceso con ppid==1 que ningún init declara (caddy, gitea, sshd, act-runner) se escribe la tarjeta que faltaba — es el trabajo real

Se construye al final, estrenándose con la migración gioser → caja nueva, con el caso del rc-status que miente como test de aceptación. Su especificación es el §1 de este documento.


5. Que el servidor sirva sus propios paquetes a su propio host

Existe el camino entero: scripts/build-repo.sh (corpus → .swm firmados + índice), takana install --repo que ya acepta lista HTTP multi-origen y verifica cada paquete contra el digest del índice firmado (ADR 0014), takana upgrade con rollback, y caddy para servir dist/repo con un file_server.

Lo que falta no es servir, son dos cosas:

  1. La clave. build-repo.sh firma hoy con una clave efímera por defecto (keygen release si no se le pasa KEY=) (medido, línea 33). Sin clave estable, «firmado» no significa nada. Es el SDD 19 §3.2 y es requisito aunque el repo sea privado.
  2. La receta de takana (§3.4). El host necesita takana instalado para consumir los paquetes, y hoy no puede instalarlo desde el repo porque takana no está en el repo.

Y una caja que se sirve a sí misma es un solo origen — justo lo que el ADR 0014 existe para no tener. El camino normal puede ser el local, pero la lista de orígenes del host debe incluir también el Storage Box o gioser. Si no, el primer origen roto es el último.

Lo que esto vale: cerrar el bucle es el ensayo de «actualización en sitio probada de verdad» (SDD 19 §5.2), hecho sobre una máquina que importa pero que no es el laptop de nadie.


6. La mudanza como experimento: el criterio de borrado

El usuario decidió que gioser se borra al final. Eso no es un detalle de calendario: es lo que convierte la mudanza en un experimento con criterio de aceptación, y hay que escribirlo antes.

Regla base: un ausente falla ruidosamente; un vacío llega hasta el final diciendo que todo fue bien. Aplicado acá: «lo copié» no es prueba de nada. La prueba es que el servicio responde en la caja nueva y que el dato se verifica por contenido.

6.1 Las puertas, en orden. Ninguna se salta.

# puerta cómo se comprueba
1 la caja nueva arranca takana puro y se entra por SSH ssh a la IP nueva tras el reboot del rescue
2 el store está entero 1327 artefactos y ninguno vacíorespaldo-storagebox.sh --listar separa los vacíos; un directorio vacío no es un artefacto
3 los grafos salen idénticos build-state.py en las dos máquinas ⇒ los cuatro grafos byte a byte. Es la prueba de que un hub está bien montado, no que «funcione»
4 la granja late en la caja nueva una cosecha completa con el LXC enchufado por .fleet y artefactos que vuelven
5 el repo se sirve y el host se actualiza de sí mismo §5 en verde, con la clave estable
6 cada dominio vivo responde 200 desde fuera la tabla del §1.1, repetida contra la IP nueva
7 el respaldo al Storage Box corre desde la caja nueva una corrida completa, no un --seco
8 lo fósil está anotado y decidido lista del §1.1 con «muere» o «revive» al lado de cada uno

Puerta 3 tiene una trampa conocida: sealed_remoto dependía de la máquina (laptop 700, gioser 751 sobre el MISMO corpus) y por eso se sacó del JSON. Si los grafos difieren, mirar primero si la diferencia es «cuánto de esto tengo en disco» y no «qué existe».

6.2 Lo que la mudanza tiene que producir, además de la mudanza

El usuario lo pidió explícito: que este experimento saque recetas y las pruebe. La caja vieja es un catálogo de software real en producción, y cada servicio que hoy corre sobre binarios de Artix es una receta candidata con un caso de uso comprobado detrás:

lo que corre hoy en Artix receta
caddy existe (recipes/caddy.toml)
openssh existe
gitea ✗ — y es la que más peso tiene: aloja el origin de takana
qdrant, squid, fail2ban, php-fpm, metalog, glances
chrony, nftables, cronie, logrotate ✗ — ya declaradas wanted en perfil.servidor

Cada una que se cierre se prueba sola: el servicio o levanta en la caja nueva o no, y eso se ve el mismo día. Es la diferencia entre una receta «sellada» y una receta usada.


7. Reusar los scripts que ya existen, y no escribir de nuevo

Pedido explícito del usuario. El inventario de lo que ya hace el trabajo:

para script existente qué le falta
armar el rootfs de un perfil scripts/hydrate-profile.py nada — es el camino bueno (los hydrate-*.sh por escritorio divergen de targets.toml)
producir el .img scripts/install-image.sh / scripts/product-image.sh apuntar al perfil.servidor
escribir a disco scripts/takana-install.sh el envoltorio de rescue del §3.2
instalador con TUI scripts/takana-live-install.sh ser el mismo motor que el remoto, con las respuestas por fichero
provisionar un worker scripts/farm/vps-setup.sh ya soporta apt y dnf; le falta la rama «takana puro» (sin gestor de paquetes ajeno)
enchufar a la granja scripts/farm/.fleet está en .gitignore ⇒ un hub recién clonado nace con la flota vacía en silencio
latido scripts/farm/cosecha-cron.sh decidir cron vs tarjeta de arje (§3.4)
respaldo scripts/respaldo-storagebox.sh nada — es interrumpible y continuable
absorber el init ajeno arje-absorb el lector de procesos vivos (§4)

Un motor, tres frentes. La tentación es escribir el instalador remoto aparte del TUI. Ahí es donde divergen: ya pasó con los hydrate-*.sh contra targets.toml, y con la unidad systemd del worker que quedó vieja mientras el repo estaba al día. La instalación remota tiene que ser la misma que hace la persona, grabada en un fichero de respuestas.


8. churay como instalador centralizado — la dirección, no la decisión

El usuario lo planteó el 2026-09-09: un instalador único, con CLI y TUI, que sirva para

  • las apps de tawasuyu,
  • paquetes sueltos en cualquier distro, al estilo nix (instalar sin ser root, sin tocar el sistema anfitrión),
  • takana mismo,
  • y llimphi.

Encaja con lo que ya hay y por eso vale escribirlo: takana install ya resuelve por nombre contra un índice firmado y ya acepta múltiples orígenes; qorpa (ADR 0015) ya corre imágenes ajenas enjauladas sobre el mismo kernel; y el vigía de espejos de churay ya corre en cron desde gioser.

No se decide acá. Es un ADR propio, y tiene al menos tres preguntas abiertas antes de escribir una línea: (1) si el instalador «en cualquier distro» hidrata binarios o reproduce desde fuente —la misma pregunta del SDD 27 §4, y la respuesta puede no ser la misma fuera de takana; (2) quién es el dueño del verbo, si takana install crece o si churay lo envuelve; (3) qué pasa con un host glibc, que es exactamente lo que el ADR 0015 aparta del store a propósito. Queda anotado para que se decida, no para que se descubra a mitad.


9. Estado y orden

paso estado
0. perfil.servidor + este documento + el renombre hammer-* de los instaladores 2026-09-09
1a. caja de sacrificio creada — cx33 (no había cx53), hel1, §3.5 2026-09-10
1b. imagen del perfil.servidor + rescue → dd → arranca takana puro (§3) 2026-09-10 — puerta 1 pasada, §3.8
2. el servidor se sirve sus paquetes y se actualiza a sí mismo (§5) pendiente — requiere clave estable + receta de takana
3. mudanza con las 8 puertas del §6, y recetas nuevas del §6.2 pendiente
4. la utilidad de absorción (§4), estrenándose con esta migración pendiente
5. churay como instalador centralizado (§8) ADR propio, sin decidir

El volumen del store NO se engancha a la caja nueva hasta que pase la puerta 1. Una caja de sacrificio con el store dentro deja de ser de sacrificio.