gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1389Shell 137Markdown 80Rust 802602 Definiciones

SDD 28: el servidor de producción — y `perfil.servidor` declarando la deuda que no se ve

El frente arranca con el inventario MEDIDO de gioser, no con un plan: PID 1 es `arje-zero` (nuestro
init ya gobierna un servidor real, aunque sobre userland Artix), Hetzner Cloud arranca por **BIOS**
—que es justo el layout que `takana-install.sh` ya escribe— y el kernel propio trae VIRTIO/EXT4 `=y`
con `netup` haciendo DHCPv4, que es exactamente el caso de una caja hcloud.

Dos hallazgos que cambian el trabajo:

- **La secuencia de arranque de gioser no está escrita en ningún lado.** `rc-status` dice `stopped`
  para caddy, gitea, sshd, cronie y act-runner y los cinco están VIVOS colgando de PID 1. Un
  `arje-absorb --from openrc` produciría una Semilla perfecta que no levanta el servidor.
- **La mitad del Caddyfile describe un servidor que ya no existe**: 4 de 19 dominios sin DNS y sin
  ficheros en `/var/www`, uno en 502. Una mudanza fiel copia también la basura ⇒ la regla queda
  escrita: no se muda lo que no responde.

`[perfil.servidor]` hereda `cli` (SDD 27 §7.1) y declara 5 raíces que existen (openssh, caddy, curl,
wget, tmux) y **5 que NO** (takana, chrony, nftables, cronie, logrotate). Quedan `wanted`, y `wanted`
no es `debt` ⇒ `drenaje.json` seguirá diciendo `deuda=0`. Se declaran igual: 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, pagada por adelantado. Cuenta esperada: 5, verificada
resolviendo cada nombre contra el campo `name` de las 869 recetas.

La peor de esas cinco: **takana no tiene receta**. El corpus construye 869 y no la suya, así que el
host no puede instalar takana desde el repo de takana — el bloqueante de que el servidor se sirva a
sí mismo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x

c599daa346b4d294fb6c62bdc6dffb4294cf982c · por Sergio · (hace 1 mes) · padre 3d0fa1c7c · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

2 archivos · +410 −0

diff --git a/docs/28-servidor-de-produccion.md b/docs/28-servidor-de-produccion.md
new file mode 100644
index 00000000..5b627043
--- /dev/null
+++ b/docs/28-servidor-de-produccion.md
@@ -0,0 +1,351 @@
+# 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](19-lanzamiento-publico.md) (lanzamiento público) y
+[SDD 27](27-perfiles-instalables-y-el-instalador.md) (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](adr/0014-distribucion-multiorigen.md) 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](19-lanzamiento-publico.md) (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](adr/0010-arranque-grafo-mirada.md))— 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.servidor`** — **HECHO 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. **IPv6** — `netup` 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](27-perfiles-instalables-y-el-instalador.md))
+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 `debt`** ⇒ `drenaje.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.
+
+---
+
+## 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](adr/0014-distribucion-multiorigen.md)), `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](19-lanzamiento-publico.md) 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](19-lanzamiento-publico.md)), 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ío** — `respaldo-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](adr/0015-imagenes-ajenas.md))
+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](27-perfiles-instalables-y-el-instalador.md), 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** |
+| **1.** caja `cx53` de sacrificio: rescue → `dd` → arranca takana puro (§3) | pendiente |
+| **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.
diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index 81085566..1f75a566 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -864,3 +864,62 @@ paquetes = [
   # pedirlo es promover uno al corpus. Queda escrito para que se elija, no para que se descubra.
   "hicolor-icon-theme",
 ]
+
+[perfil.servidor]
+descripcion = "la caja que SIRVE: sshd, servidor web y las herramientas de operar una máquina remota sin pantalla"
+hereda = ["cli"]
+# Nace el 2026-09-09 del frente «servidor de producción» (SDD 27). Existe porque las cuatro cosas que
+# ese frente quiere —instalar takana puro en una caja remota, servir el repo, que la caja se actualice
+# a sí misma, y migrar gioser— empiezan todas por la misma pregunta: QUÉ se instala. Hasta hoy eso no
+# estaba declarado en ningún lado. El `sshd` del producto, por ejemplo, entra por
+# `product-userland-from-repo.sh` y NO por este manifiesto: dos listas para lo mismo y una sola
+# versionada como destino, que es exactamente lo que este fichero existe para no tener.
+#
+# ⚠ LA LECCIÓN DE `foot`, APLICADA ANTES DE PAGARLA: `escritorio-sway` daba 121/121 sellado y era
+# inusable porque a la declaración le faltaba el emulador de terminal. La métrica mide la clausura de
+# lo DECLARADO y no puede ver lo que falta en la declaración. Un servidor tiene su propia versión de
+# ese agujero, y no es el software de servir: es la HORA (sin reloj, TLS y las firmas fallan), el
+# CORTAFUEGOS y el DISPARADOR PERIÓDICO. Por eso van declarados abajo aunque todavía no existan.
+#
+# ⚠ LAS CUATRO ÚLTIMAS RAÍCES NO RESUELVEN A NINGUNA RECETA — a propósito, y hay que saberlo.
+# Quedan `wanted` en el grafo, y `wanted` NO es `debt`: `drenaje.json` seguirá diciendo `deuda=0`
+# ([[gnome-sin-fuentes]] trampa 1). Se declaran igual porque una deuda escrita y contada se ve, y una
+# omitida no: un `perfil.servidor` sin ellas saldría N/N —completo y verde— describiendo un servidor
+# sin hora, sin cortafuegos y sin latido. Cuenta esperada al escribirlas: **5 `wanted`**. Si al mirar
+# `build-state.json` aparece un sexto, es un error de tipeo en un nombre, no una receta nueva.
+paquetes = [
+  # ── lo que existe y está sellado ──────────────────────────────────────────────────────────────
+  # `openssh`: el acceso. Es la raíz que convierte «la caja arrancó» en «puedo entrar»; sin ella una
+  # instalación remota deja una máquina viva e inalcanzable, que es peor que una que no arrancó.
+  "openssh",
+  # `caddy`: sirve `dist/repo` (índice firmado + los `.tkn`/`.swm`). TLS automático, un binario, cero
+  # dependencias — y es el mismo que ya sirve los 19 dominios de gioser, así que la migración no
+  # cambia de servidor web además de cambiar de sistema.
+  "caddy",
+  # `curl`/`wget`: bajar del mirror y del repo. `takana install --repo https://…` los necesita del
+  # lado del cliente, y son también la única forma de diagnosticar un origen caído desde la caja.
+  "curl", "wget",
+  # `tmux`: un build largo por SSH que muere con la sesión es un build perdido. En una caja sin
+  # pantalla es infraestructura, no comodidad.
+  "tmux",
+
+  # ── declarado y NO existe todavía: la deuda del perfil, contada ───────────────────────────────
+  # `takana`: el propio takana NO tiene receta. El corpus construye 869 recetas y no la suya, así que
+  # hoy el binario sólo existe como `cargo build --release` sobre un clon del repo. Es EL bloqueante
+  # de «que el servidor sirva sus paquetes a su propio host»: el host necesita `takana` instalado para
+  # consumirlos, y no puede instalarlo desde el repo porque no está en el repo.
+  "takana",
+  # `chrony`: la hora. Sin NTP, un reloj que deriva rompe la validación de certificados TLS y la de
+  # firmas con expiración — y falla con errores que no dicen «la hora», dicen «firma inválida».
+  "chrony",
+  # `nftables`: el cortafuegos. `dev.gioser.net` está hoy expuesto a internet con fuerza bruta a root
+  # en el journal; una caja nueva no debería nacer así.
+  "nftables",
+  # `cronie`: el disparador periódico. El latido de la granja son tres líneas de crontab, y en una
+  # distro sin systemd no hay timers: o hay cron, o el latido se vuelve una tarjeta de arje-zero. Es
+  # una decisión pendiente, y se declara para que se decida en vez de descubrirse.
+  "cronie",
+  # `logrotate`: en una caja que sirve, el log de acceso crece hasta llenar el disco. Caddy rota el
+  # suyo por configuración, pero nada más lo hace.
+  "logrotate",
+]