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 815Rust 56Shell 37Markdown 291229 Definiciones

⎇ dcb83e1fd380c1ad5b1e49f266c1bd163d771d5f docs/adr/0021-dueno-del-verbo-de-instalar.md Markdown · 404 líneas · 25.6 KB Historia crudo

feat(metal): --keep conserva el /home viejo, y los usuarios se crean con SU uid dcb83e1fd · Sergio ·

1# ADR 0021 — El dueño del verbo de instalar: takana pone el motor, churay pone la cara
2
3- **Estado:** PROPUESTO, con **las dos partes IMPLEMENTADAS y medidas** (2026-09-23): el verbo
4  `takana metal` acá, y la cara en tawasuyu (`churay-core::metal`, la pestaña «Sistema en disco» y
5  `churay-cli metal`).
6- **Fecha:** 2026-09-23
7- **Contesta:** [SDD 28 §8](../28-servidor-de-produccion.md) («`churay` como instalador centralizado
8  — la dirección, no la decisión»), que dejó tres preguntas anotadas para que se **decidieran** en
9  un ADR propio y no se descubrieran a mitad.
10- **Frontera:** `crates/takana-cli/src/metal.rs`, `scripts/takana-install-partition.sh`,
11  `scripts/instalador-initramfs.sh`, y —del otro lado— `02_ruway/churay` en tawasuyu.
12- **Relacionado:** [ADR 0017](0017-ciclo-de-vida-del-kernel.md) (kexec),
13  [ADR 0018](0018-soberania-del-arranque.md) (NVRAM), [ADR 0015](0015-imagenes-ajenas.md) (glibc),
14  [SDD 27](../27-perfiles-instalables-y-el-instalador.md) (perfiles instalables).
15
16## Contexto
17
18El instalador en metal quedó funcionando el 2026-09-23: censo del terreno, elección por PARTUUID,
19formateo de una sola partición, ESP poblada sin montarla, entrada NVRAM con respaldo previo, y el
20camino «sin USB» por `kexec`. Y aun así **no se podía usar**. La forma de invocarlo era ésta:
21
22```sh
23sudo ./takana kernel kexec ./kernel --initrd ./takana-instalador.cpio.gz \
24     --cmdline 'console=ttyS0,115200 console=tty0' --jump
25```
26
27El usuario lo dijo en una línea:
28
29> *pero esos son comandos a lo poweruser.*
30
31Tiene razón, y el diagnóstico es preciso: eso no es un instalador, es **el motor con la tapa
32abierta**. Quien lo teclea tiene que saber que existe `kexec`, que el payload va por `--initrd`, que
33sin `--jump` sólo se carga y qué consola pedirle al kernel nuevo. **Ninguna de esas cuatro cosas es
34una decisión del que instala** — son detalles de implementación que se filtraron a la superficie.
35
36Y la segunda mitad de su observación es la que importa para este ADR:
37
38> *churay era, la herramienta de instalar? ya tiene varias cosas adelantadas.*
39
40Sí. `churay` (quechua: *poner*) existe desde el 2026-06-24 en tawasuyu y está lejos de ser un
41esqueleto: instalador **por grafo** con GUI (`churay-llimphi`) y CLI (`churay-cli`), modo sistema y
42modo local, bundles precompilados firmados con ed25519, repo remoto multi-origen con clave anclada y
43anillo rotable, actualizador con `rollback`, vigía de espejos en cron, y una puerta de entrada desde
44la web (`tawasuyu.net/instalar.sh`). Lo que **no** hace es instalar takana en metal.
45
46Entonces la pregunta no es «¿hay que escribir un instalador?» sino **quién es dueño de qué**.
47
48## Decisión
49
50> **takana es dueño del MOTOR y expone un verbo estable. churay es dueño de la CARA y lo llama.
51> Ninguna cara vuelve a leer discos por su cuenta.**
52
53### 1. El verbo vive en takana, y es `takana metal`
54
55```
56takana metal check     ¿puede esta máquina instalarse sin USB?      (no toca nada)
57takana metal survey    ¿qué hay en los discos y qué se puede usar?  (no toca nada)
58takana metal install   el viaje entero: comprueba, avisa y salta
59```
60
61Tres verbos y el orden en que se usan es el orden en que se leen. `install` encuentra el paquete
62solo —junto al binario, no en el directorio actual, porque el paquete se copia a un pen drive y se
63corre desde cualquier lado—, corre el diagnóstico **antes** de prometer nada, dice en voz alta qué
64va a pasar y pide un «si» escrito. No acepta un *enter* pelado: el enter es lo que se aprieta sin
65leer, y lo que sigue mata la sesión.
66
67**Por qué en takana y no en churay.** El motor necesita root, `kexec_file_load`, dispositivos de
68bloque, las herramientas selladas del store y el rootfs ya construido. Nada de eso se puede mover a
69un instalador de usuario sin arrastrarlo entero. churay ya llegó a esa misma conclusión por su lado
70y lo dejó escrito en su LEEME: *«hammer-build/overlay (bwrap, zig, overlayfs, root) no encajan en un
71instalador de usuario, así que no se usan»*.
72
73### 2. El censo es UNO, y las caras lo consumen
74
75`takana metal survey` **no reimplementa nada**: ejecuta el mismo `takana-install-partition.sh
76--survey` que corre dentro del instalador. Con `--json` emite el contrato:
77
78```json
79{ "version": 1,
80  "raiz_viva": "8:2",
81  "particiones": [
82    {"dev": "/dev/sda2", "partuuid": "6F0B…", "mib": 6144, "fs": "ext4",
83     "etiqueta": "hammer-root", "montaje": "/", "veredicto": "RAIZ_VIVA",
84     "usable": true, "explicacion": "la raíz del sistema que está corriendo AHORA"} ] }
85```
86
87`veredicto` ∈ `CANDIDATA · LIBRE · RAIZ_VIVA · SOLO_ESP · EN_USO · NO_TOCAR`. El campo `usable` es
88la lectura de una sola pregunta —¿se puede instalar acá?— para que una cara **no tenga que
89reimplementar el `case` de los veredictos y equivocarse en el default**: lo que no reconozca no es
90usable.
91
92**Por qué esto es la parte que no se negocia.** El SDD 28 §8 ya nombra el riesgo —«un motor, tres
93frentes»— y este repo lo pagó dos veces (los `hydrate-*.sh` contra `targets.toml`; la unidad systemd
94del worker contra el repo). Acá el coste de divergir es peor que quedar desactualizado: **el censo
95es lo que decide qué NO se toca**. Una cara que clasifique por su cuenta y no conozca, por ejemplo,
96que una partición marcada LUKS en la tabla no se ofrece aunque `blkid` no vea cabecera adentro, no
97muestra de menos: **formatea de más**.
98
99### 3. Lo que churay hace, y lo que no
100
101| | churay | takana |
102|---|---|---|
103| pintar el terreno y preguntar | **sí** (consume `survey --json`) | modo texto, de respaldo |
104| decidir si una partición se puede tocar | **no** | sí, y es el único |
105| formatear, poblar la ESP, NVRAM | **no** | sí, dentro del instalador |
106| llegar a una máquina ajena (web, bundle, firma) | **sí, ya lo tiene** | no, y no lo va a tener |
107
108Lo último es el aporte real de churay y la razón por la que esto no es una capa de más: **el camino
109de la web al escritorio ya existe del lado de churay** —`instalar.sh`, `blobs/<hex>` verificados,
110manifiesto firmado, clave anclada, multi-origen, anillo rotable— y del lado de takana no existe
111ninguno. Hoy el paquete del instalador se copia a mano.
112
113## Las tres preguntas del SDD 28 §8, contestadas
114
115**(1) ¿Hidrata binarios o reproduce desde fuente en una máquina ajena?**
116Para **este** verbo, ninguna de las dos: el instalador **lleva el rootfs ya construido adentro**, en
117RAM, y lo copia. No hay build en la máquina del usuario y no hace falta red. La pregunta seguía
118teniendo sentido para *paquetes sueltos en cualquier distro*, que es otro verbo (`takana install`,
119SDD 27 §4) y **queda abierta ahí**. Confundirlas fue lo que hacía parecer que había una sola
120decisión.
121
122**(2) ¿Quién es dueño del verbo?** Contestado arriba: takana el motor, churay la cara. El verbo
123canónico es inglés (regla 4 de `CLAUDE.md`); lo que se lee, castellano.
124
125**(3) ¿Qué pasa con un host glibc?** **No es un problema, y hay que decir por qué para que nadie lo
126trate como si lo fuera.** Lo que el ADR 0015 aparta del store son *imágenes ajenas* —rootfs que no
127reproducen desde fuente—. Acá no entra nada al store: lo que corre en el host ajeno es **un ELF
128estático** (medido: `statically linked`, sin intérprete), así que el libc del anfitrión no
129participa. El instalador de verdad corre sobre **su propio kernel** después del salto, donde ya no
130hay host.
131
132## El salto, ejecutado de verdad (2026-09-23)
133
134Hasta este día el camino «sin USB» tenía **todo probado menos el salto**: el instalador en RAM se
135había arrancado a mano y hacía su trabajo, pero `kexec_file_load` **no se había ejecutado nunca** —
136ninguna de nuestras máquinas corre un kernel con `CONFIG_KEXEC_FILE`, así que el primer sitio donde
137el salto iba a pasar era la máquina del usuario. Eso convierte al usuario en el banco de pruebas.
138
139`scripts/metal-salto-test.sh` lo cierra: arranca una VM con `linux-metal-kexec` —la variante que sí
140trae el flag—, le da un disco con vecinos de verdad (una ESP con un `bootmgfw.efi` de Microsoft
141dentro, un destino vacío y un hueco sin asignar) y corre ahí **los tres verbos de usuario**. Las
142once comprobaciones en verde, y el veredicto sale del **disco**, no de lo que la pantalla dijo:
143
144```
145✓ el sistema origen arrancó            ✓ EL SALTO OCURRIÓ: el origen no volvió del kexec
146✓ metal check: dice que SÍ             ✓ el instalador arrancó DESPUÉS del salto
147✓ metal survey: el censo salió         ✓ el instalador terminó bien
148✓ metal install: anunció el salto      ✓ la tabla de particiones NO cambió
149✓ el destino quedó ext4 con LABEL=hammer-root
150✓ la ESP tiene \EFI\takana\takanax64.efi
151✓ el vecino de la ESP (bootmgfw.efi de Microsoft) quedó byte a byte igual
152```
153
154El log serial corta exactamente donde tiene que cortar —`⚡ saltando al instalador — esto no
155vuelve.`— y la línea siguiente ya es del otro kernel: `TAKANA-INSTALADOR: arrancado en RAM. El
156sistema viejo YA NO CORRE: su disco está libre.`
157
158⚠ Y el arnés nació con dos comprobaciones que fallaban **sin que el instalador tuviera nada que
159ver**, que es la forma más cara de tener un test: comparaba el listado de `mdir` del vecino contra sí
160mismo —y el listado trae el «bytes free» del volumen, que cambia en cuanto se copia cualquier otra
161cosa— y usaba `cmp`, que **no existe en este anfitrión musl**: `command not found` sale 127, el `if`
162lo toma como falso y el test acusaba «la tabla cambió» sin haber comparado nada. Un test que no
163puede correr tiene que decirlo, no fallar con el mensaje de otra cosa.
164
165## Lo medido (2026-09-23)
166
167| | |
168|---|---|
169| `metal check` como uid 1001 | dice «falta root», **y avisa que el diagnóstico no es concluyente** |
170| `metal check` como root del anfitrión | dice la causa real: el kernel no trae `CONFIG_KEXEC_FILE` |
171| `metal survey --json` en el hub | 6 entradas, JSON válido, una sola `usable` |
172| `metal install` sin paquete | nombra los tres directorios donde buscó |
173| `--unattended` sin destino | aborta: «nadie va a estar ahí para elegirlo» |
174| la cmdline sin respuestas | **idéntica al default**: no hay ninguna máquina horneada (test) |
175
176Dos defectos aparecieron al poner la cara, y los dos eran del motor:
177
178- **`--survey` exigía el kernel y mtools.** En una máquina ajena —sin store y sin kernel del
179  corpus— el censo moría con «no encuentro el kernel EFI-stub» **antes de imprimir una sola línea
180  del terreno**. Mirar no puede costar lo mismo que escribir.
181- **`--survey` salía con 1 habiendo funcionado.** Un `a && b && c` como último comando del bucle de
182  impresión decide el estado de salida del script entero; con `set -e`, `mostrar_censo` moría ahí si
183  la última fila no tenía PARTUUID (un disco sin tabla, un hueco libre). En este hub la última fila
184  es `/dev/sdc`. Un `if` no propaga; el `&&` sí.
185
186Y uno de clasificación, que es el que más se parece a lo que este ADR quiere evitar: el censo
187ofrecía `/dev/sda1` —**2 MiB**, el hueco de arranque— como «✓ se puede usar», porque el veredicto
188miraba **qué** hay dentro y nunca **cuánto** cabe. Elegirla formatea algo real y muere a mitad de la
189copia. El piso es ahora el mismo número con el que se talla un hueco (`mib_necesarios`), así que las
190dos puertas no pueden divergir.
191
192## Lo que este ADR NO decide
193
194- **Windows.** El instalador standalone desde Windows sigue donde lo dejó el runbook: WSL2 no tiene
195  efivarfs y un `.exe` nativo es un port, no una envoltura. churay tiene su propio frente ahí
196  (`PLAN-MULTIPLATAFORMA.md`, F4) y no se mezcla con éste.
197- **Firmar el kernel del instalador.** Con Secure Boot en modo `integrity` el salto no ocurre, y hoy
198  no firmamos. Es la decisión abierta del ADR 0018.
199- **Redimensionar.** Encoger un filesystem pide desmontarlo, y la raíz viva no se desmonta. Eso es
200  física, no una carencia: el redimensionado vive **después** del salto, dentro del instalador, y
201  todavía no está escrito.
202
203## La cara de churay — HECHA el 2026-09-23
204
205Está implementada en tawasuyu: `churay-core::metal` (el envoltorio, con 9 tests), la pestaña
206**«Sistema en disco»** de `churay-llimphi`, y `churay-cli metal` para headless. Once claves en los
207tres idiomas. El binario `takana` que churay necesita es el estático del paquete del instalador;
208para el `survey` no hace falta ni store, ni rootfs, ni root.
209
210**Tres cosas salieron distintas de lo que este ADR había anotado, y las tres a mejor:**
211
2121. **No es un nodo del grafo, es una pestaña.** Un nodo con `Scope::System` habría entrado en el
213   conjunto que `install_unit` recorre, y el instalador de apps habría intentado instalarlo como a
214   una app. La pestaña —al lado de «Actualizaciones»— no toca ningún flujo existente.
2152. **churay NO pasa `--unattended`.** El botón autoriza el **salto**, no el formateo: la máquina
216   salta al instalador en RAM y el instalador vuelve a mostrar el terreno y a preguntar en su propia
217   pantalla, donde ya no hay un sistema montado estorbando. Dos consentimientos para dos cosas
218   distintas, y el segundo se da en el sitio donde el disco ya está libre.
2193. **La raíz viva se pinta `⚡` y no `✓`, y desaparece si la máquina no puede saltar.** Sin `kexec`
220   no hay forma de soltarla; ofrecerla igual sería ofrecer algo que no se puede hacer. Un `✓`
221   idéntico al de una partición libre sería la peor forma de ofrecer la partición del sistema que
222   la persona está usando en ese momento.
223
224Y una que este ADR daba por resuelta y no lo estaba: **`takana metal survey` sólo encontraba el
225censo junto al binario**. Llamado desde churay —que encuentra `takana` en el `PATH`, que es como lo
226va a encontrar siempre— el binario está en `/usr/bin` y el censo no está al lado, así que el censo
227moría. Ahora se busca también en `/usr/libexec/takana`, `/usr/lib/takana` y `/usr/share/takana`.
228El defecto sólo se ve desde el otro lado de la frontera, que es para lo que sirve tener dos lados.
229
230### Lo que churay decide, y que no es reclasificar
231
232Leer el veredicto para pintarlo distinto **no** es recalcularlo. Las tres decisiones de la cara se
233equivocan del lado seguro y están en sus tests:
234
235| decisión | por qué |
236|---|---|
237| un veredicto **desconocido** no se ofrece, aunque venga `usable: true` | si takana publica uno nuevo y ese churay es viejo: perder una opción se nota; ofrecer la equivocada, no |
238| un JSON que no parsea es un **error**, no un censo vacío | un censo vacío se pinta como «esta máquina no tiene discos», que es la mentira tranquila |
239| lo que **no** se puede elegir se muestra igual, con su motivo | una lista que sólo trae lo elegible se lee como «acá no hay nada más», y el que no encuentra su partición no sabe si el instalador no la vio o decidió no ofrecerla |
240
241## Respuesta a la propuesta de churay (2026-09-25)
242
243Del otro lado se escribió `02_ruway/churay/SDD-COORDINACION-TAKANA.md`: una lista de **datos** que la
244cara necesita para dibujar el terreno en vez de listarlo, explícitamente sin mover ni una decisión a
245churay. Se contesta acá porque dos de sus puntos son decisiones y no código.
246
247### Concedido e implementado: el censo pasa a la versión 2
248
249Los cuatro puntos de §2.1–§2.4 del pedido, en `censo()` y `censo_json()`. Por fila: `clase`
250(`particion` · `hueco` · `disco`), `disco`, `inicio_bytes`, `tamano_bytes`, `tipo_guid`. Y un array
251`discos` con `modelo`, `removible`, `tamano_bytes`, `sector_bytes`, `tabla`.
252
253**Ni un campo de decisión.** `veredicto` y `usable` siguen siendo los únicos que deciden y siguen
254saliendo de acá; lo agregado es para pintar y explicar, que es exactamente lo que se pidió.
255
256Dos cosas salieron distintas de lo propuesto, y conviene saber por qué:
257
2581. **El aparato va en un array propio, no como campos de cada fila.** Se pidió `disco_modelo` /
259   `disco_removible` / `disco_tamano_bytes` por partición. Un dato del disco repetido en cada fila se
260   puede **contradecir entre filas** —y entonces hay que decidir a cuál creerle—, así que cada fila
261   trae `disco` y eso es la clave del array. Es un `HashMap` del lado de la cara y un error menos.
2622. **El hueco ya venía con `dev`, y eso era peor que no tenerlo.** El pedido dice que un hueco «no
263   tiene `dev` ni `partuuid`»; en realidad **ya traía `dev` = el disco**, así que dos huecos del
264   mismo disco llegaban con el MISMO `dev` y una cara que los indexe por ahí se queda con uno y
265   pierde el otro **sin que nada falle**. Por eso existe `clase`: la identidad de un hueco es
266   `disco` + `inicio_bytes`, y está dicho en el contrato en vez de quedar implícito.
267
268### Y apareció un defecto que el pedido no buscaba: el sector de 512 estaba horneado
269
270`mib_de_sectores()` dividía por 2048 a ciegas. `sfdisk -d` cuenta en sectores **lógicos**, que en un
271disco 4Kn son 4096 — y `/sys/class/block/<n>/size`, que usa la otra rama, cuenta **siempre** en
272unidades de 512. Las dos conversiones estaban mezcladas en una.
273
274Medido con un loop de sector 4096 (`losetup --sector-size 4096`), censo viejo contra censo nuevo
275sobre el MISMO disco:
276
277| | viejo | nuevo | real |
278|---|---|---|---|
279| partición de 1 GiB | **128 M** | 1024 M | 1024 M |
280| el hueco de al lado | **895 M → ✗ NO TOCAR, «hacen falta 2048 M»** | 7166 M → ✓ instalar al lado | 7166 M |
281
282O sea que en una máquina con discos 4Kn el censo **escondía el único sitio donde instalar no destruye
283nada** y decía que era chico. Se equivocaba del lado seguro —ocultaba, no ofrecía— pero el resultado
284práctico es un instalador que en esa máquina contesta «acá no hay dónde». Es el mismo patrón del
285`/dev/sda1` de 2 MiB que este ADR ya trae: **el veredicto mira lo que hay y se olvida de cuánto
286cabe**, y la unidad en la que se mide es parte de «cuánto».
287
288Comprobado además que el JSON sale **solo** por stdout (la línea de la herramienta resuelta del store
289va por stderr) y que parsea; los huecos, el 4Kn y el disco sin tabla, con discos de verdad.
290
291### Lo que NO se concede todavía, y qué falta para poder
292
293- **§2.5 `usado_bytes`** — de acuerdo con el orden propuesto: es el más caro (montar en sólo lectura
294  para medir) y el que menos cambia una decisión. Queda anotado con su condición, que es del pedido y
295  es correcta: **si no hay dato, ausente y no cero.** Un 0 % dibujado sobre una partición llena
296  miente justo en el número que hace decidir.
297- **§3 conservar `/home`** — `takana metal install --conservar PARTUUID=…:/home` es aceptable como
298  forma, y las tres condiciones que el pedido pone (no formatear, comprobar que monta ANTES de tocar
299  la raíz, declararlo en el plan) son de takana y se asumen. Pero **la trampa 1 no es de la cara y no
300  está decidida**: si el usuario que crea takana no tiene el mismo `uid` que el del sistema viejo,
301  los archivos quedan de otro dueño y el escritorio arranca sin poder escribir en su propia carpeta.
302  Las dos salidas —crear el usuario con el uid que se encuentra, o cambiar dueños y decirlo— son
303  decisiones de takana con consecuencias distintas, y hasta que se elija **el botón no se ofrece**.
304  La instantánea de `pacha-dotfiles` que churay propone para la trampa 2 no le pide nada a takana y
305  no hace falta esperarla.
306- **§7 la raíz del suelo** — hay dos llaves reales y un anillo firmado que hoy **no anclan nada**, y
307  eso es de este repo decidirlo. No están apagadas por diseño: están sin conectar, que se ve igual y
308  no protege lo mismo. Sigue abierto quién es dueño de esa raíz; una llave sin dueño es peor que no
309  tenerla.
310
311### Una corrección menor al pedido
312
313Su §1 lista los veredictos como `CANDIDATA · LIBRE · RAIZ_VIVA · SOLO_ESP · EN_USO · NO_TOCAR` y
314falta **`CRUDA`** (sin identificar, usable con confirmación), que el censo emite desde el principio.
315No es un problema real —`churay-core::metal` sí lo conoce y lo trata—, pero la tabla del documento
316quedó corta y es la que alguien va a leer.
317
318## Las dos decisiones que faltaban, tomadas (2026-09-25)
319
320### 1. Conservar el `/home` viejo: manda el uid de los DATOS
321
322> **takana crea los usuarios con el uid que encuentra en el `/home` conservado —todos los que
323> haya— y el login por defecto es el nombre del directorio.** No se hace `chown` de nada.
324
325El motivo de elegir esto y no lo contrario: recorrer un `/home` de cientos de miles de ficheros es
326lento, es destructivo sobre los metadatos, y **pierde el reparto si había más de un dueño**. El uid
327de los datos es el dato duro; el del sistema nuevo es lo barato de adaptar.
328
329Implementado como `takana metal install --keep PARTUUID=…:/home` → `takana.keep=` en la cmdline →
330`--keep` en `takana-install-partition.sh`. **El verbo es `--keep` y no `--conservar`**: la regla 4 del
331`CLAUDE.md` manda que la superficie de la CLI sea inglés; lo que se LEE sigue en castellano.
332
333Las tres condiciones del pedido se cumplen, y hay una cuarta que apareció al escribirlo:
334
3351. **No se formatea.** La partición conservada no se toca en ningún momento.
3362. **Se comprueba que monta ANTES de tocar la raíz** — en sólo lectura, y si ya estaba montada se lee
337   de donde está y **no se desmonta**: un `umount` de un montaje ajeno puede propagarse al original,
338   que es un accidente que este repo ya midió.
3393. **Se declara en el plan**, junto a lo que sí se borra, y nombrando a cada usuario que va a crear
340   con su uid — antes de pedir el «si».
3414. **Un choque de uid aborta, y aborta ANTES del `mke2fs`.** Si el `/home` viejo trae un uid que en el
342   sistema nuevo ya es de otro (`sshd` es el 74), no hay arreglo automático: la salida es del usuario
343   —renombrar el directorio, o instalar sin `--keep`— y se dice cuál es el choque. Nada se formateó.
344
345Sólo se admite `/home`. Conservar `/usr` o `/etc` de otro sistema no es conservar datos: es arrancar
346un híbrido de dos distros, y se rechaza nombrando el motivo.
347
348#### Dos cosas que se descubrieron al implementarlo, y las dos habrían fallado en silencio
349
350**El `findfs` de busybox NO entiende `PARTUUID`.** Medido: `busybox 1.36.1` dice
351`Usage: findfs LABEL=label or UUID=uuid`, y nada más. El initramfs de pivote es el que monta el
352`/home` conservado, y ahí sólo hay busybox — así que un `findfs PARTUUID=…` **devuelve vacío sin
353error**, el `if` elige la otra rama y la máquina arranca con `/home` vacío diciendo que todo fue
354bien. Por eso el montaje va por `UUID` del filesystem (que tampoco cambia, porque esa partición no se
355formatea nunca) y el PARTUUID se anota igual en `/etc/takana/home.conf` para quien repare a mano.
356
357**El árbol del stage son HARDLINKS del rootfs sellado, así que un `>>` escribe DENTRO DEL STORE.**
358No es teórico: se reprodujo a propósito sobre el artefacto real
359(`5011955a…-product-rootfs/etc/passwd`, un inode con 2 enlaces tras el `cp -al`), y un simple
360`echo … >> arbol/etc/passwd` **cambió el fichero del store** — `662e02d6…` → `e22ddba3…`. Un
361instalador que agregue usuarios al árbol envenena el corpus para todos y nada falla. Por eso
362`keep_aplicar` **copia y hace `mv` encima** de `passwd`/`group`/`shadow` antes de escribir: eso rompe
363el enlace y deja el inode del store intacto. Comprobado en los dos sentidos —con el `mv`, el store
364queda byte a byte igual y el árbol pasa a otro inode; sin él, el store cambia.
365
366⚠ Y una lección del propio experimento: **`sed -i` no repara ese daño.** Al quitar la línea del árbol
367con `sed -i` el store SIGUIÓ envenenado, porque `sed -i` escribe un fichero nuevo y lo renombra —
368rompe el enlace y deja el contenido malo en el inode del store. Hubo que reescribir el fichero *del
369store* para restaurarlo (verificado: vuelve a `662e02d6…`). La herramienta que rompe el hardlink es
370la que no sirve para deshacerlo.
371
372Las contraseñas de los usuarios creados quedan **bloqueadas** (`!` en `shadow`), no vacías: un campo
373vacío es login SIN CLAVE. Y un directorio cuyo nombre no es un login válido, o que es de root, se
374salta **nombrándolo en voz alta** — sus datos no se tocan, pero el que lo busque tiene que enterarse.
375
376La trampa 2 del pedido —los dotfiles de otra versión— no se resuelve acá y se AVISA en el plan. La
377instantánea de `pacha-dotfiles` que churay propone para desarmarla vive del lado de la suite y no le
378pide nada a takana.
379
380### 2. La raíz del suelo es de takana — pero **no hay nada que importar**
381
382Decidido: la raíz del suelo es de este repo. Y al ir a importar las dos llaves que el pedido daba por
383existentes, **no existen**. Medido hoy en esta máquina:
384
385| lo que el pedido dice | lo que hay |
386|---|---|
387| semillas privadas en `~/.takana/seeds/{raiz,release}.seed` (600) | **no existe ni el directorio `~/.takana`** — ni en `/home/sergio` ni en `/root` |
388| anillo serie 1 en `dist/takana/claves.signed.json` | **no existe** ese fichero en ningún sitio de tawasuyu |
389| las dos pubkeys | existen **como texto, en dos ficheros markdown** y en el espejo git del repo |
390
391O sea que de la ceremonia del 2026-09-24 sobrevivieron **dos cadenas hexadecimales en un documento**.
392Una pubkey sin su mitad privada se puede anclar, pero nadie puede firmar con ella: sería una raíz que
393no puede emitir nada, y eso es peor que no tener raíz, porque *parece* que hay una.
394
395⇒ **La decisión se sostiene y el «se importan esas dos» no se puede ejecutar.** Lo que hace falta es
396una ceremonia AQUÍ, y eso genera material criptográfico real: no se corre de paso. Queda como el
397siguiente paso explícito de este frente, y las dos pubkeys del documento de churay hay que tratarlas
398como lo que son —**restos sin dueño y sin uso**— y borrarlas de la lista de cosas que protegen algo.
399
400⚠ Y la lección que vale más que el incidente: **un documento afirmaba la existencia de material
401criptográfico con rutas y permisos concretos (`600, fuera del repo`), y ninguna de las tres rutas
402existía.** El detalle no es evidencia. Es el mismo patrón que este repo ya nombró dos veces —la
403etiqueta no es el hecho, una protección que funciona por accidente se ve igual que una por diseño—:
404acá una protección que **no existe** se veía igual que una que sí, y con más detalle que la real.

0021-dueno-del-verbo-de-instalar.md

ADR 0021 — El dueño del verbo de instalar: takana pone el motor, churay pone la cara

Contexto

El instalador en metal quedó funcionando el 2026-09-23: censo del terreno, elección por PARTUUID, formateo de una sola partición, ESP poblada sin montarla, entrada NVRAM con respaldo previo, y el camino «sin USB» por kexec. Y aun así no se podía usar. La forma de invocarlo era ésta:

sudo ./takana kernel kexec ./kernel --initrd ./takana-instalador.cpio.gz \
     --cmdline 'console=ttyS0,115200 console=tty0' --jump

El usuario lo dijo en una línea:

pero esos son comandos a lo poweruser.

Tiene razón, y el diagnóstico es preciso: eso no es un instalador, es el motor con la tapa abierta. Quien lo teclea tiene que saber que existe kexec, que el payload va por --initrd, que sin --jump sólo se carga y qué consola pedirle al kernel nuevo. Ninguna de esas cuatro cosas es una decisión del que instala — son detalles de implementación que se filtraron a la superficie.

Y la segunda mitad de su observación es la que importa para este ADR:

churay era, la herramienta de instalar? ya tiene varias cosas adelantadas.

Sí. churay (quechua: poner) existe desde el 2026-06-24 en tawasuyu y está lejos de ser un esqueleto: instalador por grafo con GUI (churay-llimphi) y CLI (churay-cli), modo sistema y modo local, bundles precompilados firmados con ed25519, repo remoto multi-origen con clave anclada y anillo rotable, actualizador con rollback, vigía de espejos en cron, y una puerta de entrada desde la web (tawasuyu.net/instalar.sh). Lo que no hace es instalar takana en metal.

Entonces la pregunta no es «¿hay que escribir un instalador?» sino quién es dueño de qué.

Decisión

takana es dueño del MOTOR y expone un verbo estable. churay es dueño de la CARA y lo llama. Ninguna cara vuelve a leer discos por su cuenta.

1. El verbo vive en takana, y es takana metal

takana metal check     ¿puede esta máquina instalarse sin USB?      (no toca nada)
takana metal survey    ¿qué hay en los discos y qué se puede usar?  (no toca nada)
takana metal install   el viaje entero: comprueba, avisa y salta

Tres verbos y el orden en que se usan es el orden en que se leen. install encuentra el paquete solo —junto al binario, no en el directorio actual, porque el paquete se copia a un pen drive y se corre desde cualquier lado—, corre el diagnóstico antes de prometer nada, dice en voz alta qué va a pasar y pide un «si» escrito. No acepta un enter pelado: el enter es lo que se aprieta sin leer, y lo que sigue mata la sesión.

Por qué en takana y no en churay. El motor necesita root, kexec_file_load, dispositivos de bloque, las herramientas selladas del store y el rootfs ya construido. Nada de eso se puede mover a un instalador de usuario sin arrastrarlo entero. churay ya llegó a esa misma conclusión por su lado y lo dejó escrito en su LEEME: «hammer-build/overlay (bwrap, zig, overlayfs, root) no encajan en un instalador de usuario, así que no se usan».

2. El censo es UNO, y las caras lo consumen

takana metal survey no reimplementa nada: ejecuta el mismo takana-install-partition.sh --survey que corre dentro del instalador. Con --json emite el contrato:

{ "version": 1,
  "raiz_viva": "8:2",
  "particiones": [
    {"dev": "/dev/sda2", "partuuid": "6F0B…", "mib": 6144, "fs": "ext4",
     "etiqueta": "hammer-root", "montaje": "/", "veredicto": "RAIZ_VIVA",
     "usable": true, "explicacion": "la raíz del sistema que está corriendo AHORA"} ] }

veredicto ∈ CANDIDATA · LIBRE · RAIZ_VIVA · SOLO_ESP · EN_USO · NO_TOCAR. El campo usable es la lectura de una sola pregunta —¿se puede instalar acá?— para que una cara no tenga que reimplementar el case de los veredictos y equivocarse en el default: lo que no reconozca no es usable.

Por qué esto es la parte que no se negocia. El SDD 28 §8 ya nombra el riesgo —«un motor, tres frentes»— y este repo lo pagó dos veces (los hydrate-*.sh contra targets.toml; la unidad systemd del worker contra el repo). Acá el coste de divergir es peor que quedar desactualizado: el censo es lo que decide qué NO se toca. Una cara que clasifique por su cuenta y no conozca, por ejemplo, que una partición marcada LUKS en la tabla no se ofrece aunque blkid no vea cabecera adentro, no muestra de menos: formatea de más.

3. Lo que churay hace, y lo que no

churaytakana
pintar el terreno y preguntarsí (consume survey --json)modo texto, de respaldo
decidir si una partición se puede tocarnosí, y es el único
formatear, poblar la ESP, NVRAMnosí, dentro del instalador
llegar a una máquina ajena (web, bundle, firma)sí, ya lo tieneno, y no lo va a tener

Lo último es el aporte real de churay y la razón por la que esto no es una capa de más: el camino de la web al escritorio ya existe del lado de churay —instalar.sh, blobs/<hex> verificados, manifiesto firmado, clave anclada, multi-origen, anillo rotable— y del lado de takana no existe ninguno. Hoy el paquete del instalador se copia a mano.

Las tres preguntas del SDD 28 §8, contestadas

(1) ¿Hidrata binarios o reproduce desde fuente en una máquina ajena? Para este verbo, ninguna de las dos: el instalador lleva el rootfs ya construido adentro, en RAM, y lo copia. No hay build en la máquina del usuario y no hace falta red. La pregunta seguía teniendo sentido para paquetes sueltos en cualquier distro, que es otro verbo (takana install, SDD 27 §4) y queda abierta ahí. Confundirlas fue lo que hacía parecer que había una sola decisión.

(2) ¿Quién es dueño del verbo? Contestado arriba: takana el motor, churay la cara. El verbo canónico es inglés (regla 4 de CLAUDE.md); lo que se lee, castellano.

(3) ¿Qué pasa con un host glibc? No es un problema, y hay que decir por qué para que nadie lo trate como si lo fuera. Lo que el ADR 0015 aparta del store son imágenes ajenas —rootfs que no reproducen desde fuente—. Acá no entra nada al store: lo que corre en el host ajeno es un ELF estático (medido: statically linked, sin intérprete), así que el libc del anfitrión no participa. El instalador de verdad corre sobre su propio kernel después del salto, donde ya no hay host.

El salto, ejecutado de verdad (2026-09-23)

Hasta este día el camino «sin USB» tenía todo probado menos el salto: el instalador en RAM se había arrancado a mano y hacía su trabajo, pero kexec_file_load no se había ejecutado nunca — ninguna de nuestras máquinas corre un kernel con CONFIG_KEXEC_FILE, así que el primer sitio donde el salto iba a pasar era la máquina del usuario. Eso convierte al usuario en el banco de pruebas.

scripts/metal-salto-test.sh lo cierra: arranca una VM con linux-metal-kexec —la variante que sí trae el flag—, le da un disco con vecinos de verdad (una ESP con un bootmgfw.efi de Microsoft dentro, un destino vacío y un hueco sin asignar) y corre ahí los tres verbos de usuario. Las once comprobaciones en verde, y el veredicto sale del disco, no de lo que la pantalla dijo:

✓ el sistema origen arrancó            ✓ EL SALTO OCURRIÓ: el origen no volvió del kexec
✓ metal check: dice que SÍ             ✓ el instalador arrancó DESPUÉS del salto
✓ metal survey: el censo salió         ✓ el instalador terminó bien
✓ metal install: anunció el salto      ✓ la tabla de particiones NO cambió
✓ el destino quedó ext4 con LABEL=hammer-root
✓ la ESP tiene \EFI\takana\takanax64.efi
✓ el vecino de la ESP (bootmgfw.efi de Microsoft) quedó byte a byte igual

El log serial corta exactamente donde tiene que cortar —⚡ saltando al instalador — esto no vuelve.— y la línea siguiente ya es del otro kernel: TAKANA-INSTALADOR: arrancado en RAM. El sistema viejo YA NO CORRE: su disco está libre.

⚠ Y el arnés nació con dos comprobaciones que fallaban sin que el instalador tuviera nada que ver, que es la forma más cara de tener un test: comparaba el listado de mdir del vecino contra sí mismo —y el listado trae el «bytes free» del volumen, que cambia en cuanto se copia cualquier otra cosa— y usaba cmp, que no existe en este anfitrión musl: command not found sale 127, el if lo toma como falso y el test acusaba «la tabla cambió» sin haber comparado nada. Un test que no puede correr tiene que decirlo, no fallar con el mensaje de otra cosa.

Lo medido (2026-09-23)

metal check como uid 1001dice «falta root», y avisa que el diagnóstico no es concluyente
metal check como root del anfitrióndice la causa real: el kernel no trae CONFIG_KEXEC_FILE
metal survey --json en el hub6 entradas, JSON válido, una sola usable
metal install sin paquetenombra los tres directorios donde buscó
--unattended sin destinoaborta: «nadie va a estar ahí para elegirlo»
la cmdline sin respuestasidéntica al default: no hay ninguna máquina horneada (test)

Dos defectos aparecieron al poner la cara, y los dos eran del motor:

Y uno de clasificación, que es el que más se parece a lo que este ADR quiere evitar: el censo ofrecía /dev/sda1 —2 MiB, el hueco de arranque— como «✓ se puede usar», porque el veredicto miraba qué hay dentro y nunca cuánto cabe. Elegirla formatea algo real y muere a mitad de la copia. El piso es ahora el mismo número con el que se talla un hueco (mib_necesarios), así que las dos puertas no pueden divergir.

Lo que este ADR NO decide

La cara de churay — HECHA el 2026-09-23

Está implementada en tawasuyu: churay-core::metal (el envoltorio, con 9 tests), la pestaña «Sistema en disco» de churay-llimphi, y churay-cli metal para headless. Once claves en los tres idiomas. El binario takana que churay necesita es el estático del paquete del instalador; para el survey no hace falta ni store, ni rootfs, ni root.

Tres cosas salieron distintas de lo que este ADR había anotado, y las tres a mejor:

  1. No es un nodo del grafo, es una pestaña. Un nodo con Scope::System habría entrado en el conjunto que install_unit recorre, y el instalador de apps habría intentado instalarlo como a una app. La pestaña —al lado de «Actualizaciones»— no toca ningún flujo existente.
  2. churay NO pasa --unattended. El botón autoriza el salto, no el formateo: la máquina salta al instalador en RAM y el instalador vuelve a mostrar el terreno y a preguntar en su propia pantalla, donde ya no hay un sistema montado estorbando. Dos consentimientos para dos cosas distintas, y el segundo se da en el sitio donde el disco ya está libre.
  3. La raíz viva se pinta ⚡ y no ✓, y desaparece si la máquina no puede saltar. Sin kexec no hay forma de soltarla; ofrecerla igual sería ofrecer algo que no se puede hacer. Un ✓ idéntico al de una partición libre sería la peor forma de ofrecer la partición del sistema que la persona está usando en ese momento.

Y una que este ADR daba por resuelta y no lo estaba: takana metal survey sólo encontraba el censo junto al binario. Llamado desde churay —que encuentra takana en el PATH, que es como lo va a encontrar siempre— el binario está en /usr/bin y el censo no está al lado, así que el censo moría. Ahora se busca también en /usr/libexec/takana, /usr/lib/takana y /usr/share/takana. El defecto sólo se ve desde el otro lado de la frontera, que es para lo que sirve tener dos lados.

Lo que churay decide, y que no es reclasificar

Leer el veredicto para pintarlo distinto no es recalcularlo. Las tres decisiones de la cara se equivocan del lado seguro y están en sus tests:

decisiónpor qué
un veredicto desconocido no se ofrece, aunque venga usable: truesi takana publica uno nuevo y ese churay es viejo: perder una opción se nota; ofrecer la equivocada, no
un JSON que no parsea es un error, no un censo vacíoun censo vacío se pinta como «esta máquina no tiene discos», que es la mentira tranquila
lo que no se puede elegir se muestra igual, con su motivouna lista que sólo trae lo elegible se lee como «acá no hay nada más», y el que no encuentra su partición no sabe si el instalador no la vio o decidió no ofrecerla

Respuesta a la propuesta de churay (2026-09-25)

Del otro lado se escribió 02_ruway/churay/SDD-COORDINACION-TAKANA.md: una lista de datos que la cara necesita para dibujar el terreno en vez de listarlo, explícitamente sin mover ni una decisión a churay. Se contesta acá porque dos de sus puntos son decisiones y no código.

Concedido e implementado: el censo pasa a la versión 2

Los cuatro puntos de §2.1–§2.4 del pedido, en censo() y censo_json(). Por fila: clase (particion · hueco · disco), disco, inicio_bytes, tamano_bytes, tipo_guid. Y un array discos con modelo, removible, tamano_bytes, sector_bytes, tabla.

Ni un campo de decisión. veredicto y usable siguen siendo los únicos que deciden y siguen saliendo de acá; lo agregado es para pintar y explicar, que es exactamente lo que se pidió.

Dos cosas salieron distintas de lo propuesto, y conviene saber por qué:

  1. El aparato va en un array propio, no como campos de cada fila. Se pidió disco_modelo / disco_removible / disco_tamano_bytes por partición. Un dato del disco repetido en cada fila se puede contradecir entre filas —y entonces hay que decidir a cuál creerle—, así que cada fila trae disco y eso es la clave del array. Es un HashMap del lado de la cara y un error menos.
  2. El hueco ya venía con dev, y eso era peor que no tenerlo. El pedido dice que un hueco «no tiene dev ni partuuid»; en realidad ya traía dev = el disco, así que dos huecos del mismo disco llegaban con el MISMO dev y una cara que los indexe por ahí se queda con uno y pierde el otro sin que nada falle. Por eso existe clase: la identidad de un hueco es disco + inicio_bytes, y está dicho en el contrato en vez de quedar implícito.

Y apareció un defecto que el pedido no buscaba: el sector de 512 estaba horneado

mib_de_sectores() dividía por 2048 a ciegas. sfdisk -d cuenta en sectores lógicos, que en un disco 4Kn son 4096 — y /sys/class/block/<n>/size, que usa la otra rama, cuenta siempre en unidades de 512. Las dos conversiones estaban mezcladas en una.

Medido con un loop de sector 4096 (losetup --sector-size 4096), censo viejo contra censo nuevo sobre el MISMO disco:

viejonuevoreal
partición de 1 GiB128 M1024 M1024 M
el hueco de al lado895 M → ✗ NO TOCAR, «hacen falta 2048 M»7166 M → ✓ instalar al lado7166 M

O sea que en una máquina con discos 4Kn el censo escondía el único sitio donde instalar no destruye nada y decía que era chico. Se equivocaba del lado seguro —ocultaba, no ofrecía— pero el resultado práctico es un instalador que en esa máquina contesta «acá no hay dónde». Es el mismo patrón del /dev/sda1 de 2 MiB que este ADR ya trae: el veredicto mira lo que hay y se olvida de cuánto cabe, y la unidad en la que se mide es parte de «cuánto».

Comprobado además que el JSON sale solo por stdout (la línea de la herramienta resuelta del store va por stderr) y que parsea; los huecos, el 4Kn y el disco sin tabla, con discos de verdad.

Lo que NO se concede todavía, y qué falta para poder

Una corrección menor al pedido

Su §1 lista los veredictos como CANDIDATA · LIBRE · RAIZ_VIVA · SOLO_ESP · EN_USO · NO_TOCAR y falta CRUDA (sin identificar, usable con confirmación), que el censo emite desde el principio. No es un problema real —churay-core::metal sí lo conoce y lo trata—, pero la tabla del documento quedó corta y es la que alguien va a leer.

Las dos decisiones que faltaban, tomadas (2026-09-25)

1. Conservar el /home viejo: manda el uid de los DATOS

takana crea los usuarios con el uid que encuentra en el /home conservado —todos los que haya— y el login por defecto es el nombre del directorio. No se hace chown de nada.

El motivo de elegir esto y no lo contrario: recorrer un /home de cientos de miles de ficheros es lento, es destructivo sobre los metadatos, y pierde el reparto si había más de un dueño. El uid de los datos es el dato duro; el del sistema nuevo es lo barato de adaptar.

Implementado como takana metal install --keep PARTUUID=…:/home → takana.keep= en la cmdline → --keep en takana-install-partition.sh. El verbo es --keep y no --conservar: la regla 4 del CLAUDE.md manda que la superficie de la CLI sea inglés; lo que se LEE sigue en castellano.

Las tres condiciones del pedido se cumplen, y hay una cuarta que apareció al escribirlo:

  1. No se formatea. La partición conservada no se toca en ningún momento.
  2. Se comprueba que monta ANTES de tocar la raíz — en sólo lectura, y si ya estaba montada se lee de donde está y no se desmonta: un umount de un montaje ajeno puede propagarse al original, que es un accidente que este repo ya midió.
  3. Se declara en el plan, junto a lo que sí se borra, y nombrando a cada usuario que va a crear con su uid — antes de pedir el «si».
  4. Un choque de uid aborta, y aborta ANTES del mke2fs. Si el /home viejo trae un uid que en el sistema nuevo ya es de otro (sshd es el 74), no hay arreglo automático: la salida es del usuario —renombrar el directorio, o instalar sin --keep— y se dice cuál es el choque. Nada se formateó.

Sólo se admite /home. Conservar /usr o /etc de otro sistema no es conservar datos: es arrancar un híbrido de dos distros, y se rechaza nombrando el motivo.

Dos cosas que se descubrieron al implementarlo, y las dos habrían fallado en silencio

El findfs de busybox NO entiende PARTUUID. Medido: busybox 1.36.1 dice Usage: findfs LABEL=label or UUID=uuid, y nada más. El initramfs de pivote es el que monta el /home conservado, y ahí sólo hay busybox — así que un findfs PARTUUID=… devuelve vacío sin error, el if elige la otra rama y la máquina arranca con /home vacío diciendo que todo fue bien. Por eso el montaje va por UUID del filesystem (que tampoco cambia, porque esa partición no se formatea nunca) y el PARTUUID se anota igual en /etc/takana/home.conf para quien repare a mano.

El árbol del stage son HARDLINKS del rootfs sellado, así que un >> escribe DENTRO DEL STORE. No es teórico: se reprodujo a propósito sobre el artefacto real (5011955a…-product-rootfs/etc/passwd, un inode con 2 enlaces tras el cp -al), y un simple echo … >> arbol/etc/passwd cambió el fichero del store — 662e02d6… → e22ddba3…. Un instalador que agregue usuarios al árbol envenena el corpus para todos y nada falla. Por eso keep_aplicar copia y hace mv encima de passwd/group/shadow antes de escribir: eso rompe el enlace y deja el inode del store intacto. Comprobado en los dos sentidos —con el mv, el store queda byte a byte igual y el árbol pasa a otro inode; sin él, el store cambia.

⚠ Y una lección del propio experimento: sed -i no repara ese daño. Al quitar la línea del árbol con sed -i el store SIGUIÓ envenenado, porque sed -i escribe un fichero nuevo y lo renombra — rompe el enlace y deja el contenido malo en el inode del store. Hubo que reescribir el fichero del store para restaurarlo (verificado: vuelve a 662e02d6…). La herramienta que rompe el hardlink es la que no sirve para deshacerlo.

Las contraseñas de los usuarios creados quedan bloqueadas (! en shadow), no vacías: un campo vacío es login SIN CLAVE. Y un directorio cuyo nombre no es un login válido, o que es de root, se salta nombrándolo en voz alta — sus datos no se tocan, pero el que lo busque tiene que enterarse.

La trampa 2 del pedido —los dotfiles de otra versión— no se resuelve acá y se AVISA en el plan. La instantánea de pacha-dotfiles que churay propone para desarmarla vive del lado de la suite y no le pide nada a takana.

2. La raíz del suelo es de takana — pero no hay nada que importar

Decidido: la raíz del suelo es de este repo. Y al ir a importar las dos llaves que el pedido daba por existentes, no existen. Medido hoy en esta máquina:

lo que el pedido dicelo que hay
semillas privadas en ~/.takana/seeds/{raiz,release}.seed (600)no existe ni el directorio ~/.takana — ni en /home/sergio ni en /root
anillo serie 1 en dist/takana/claves.signed.jsonno existe ese fichero en ningún sitio de tawasuyu
las dos pubkeysexisten como texto, en dos ficheros markdown y en el espejo git del repo

O sea que de la ceremonia del 2026-09-24 sobrevivieron dos cadenas hexadecimales en un documento. Una pubkey sin su mitad privada se puede anclar, pero nadie puede firmar con ella: sería una raíz que no puede emitir nada, y eso es peor que no tener raíz, porque parece que hay una.

⇒ La decisión se sostiene y el «se importan esas dos» no se puede ejecutar. Lo que hace falta es una ceremonia AQUÍ, y eso genera material criptográfico real: no se corre de paso. Queda como el siguiente paso explícito de este frente, y las dos pubkeys del documento de churay hay que tratarlas como lo que son —restos sin dueño y sin uso— y borrarlas de la lista de cosas que protegen algo.

⚠ Y la lección que vale más que el incidente: un documento afirmaba la existencia de material criptográfico con rutas y permisos concretos (600, fuera del repo), y ninguna de las tres rutas existía. El detalle no es evidencia. Es el mismo patrón que este repo ya nombró dos veces —la etiqueta no es el hecho, una protección que funciona por accidente se ve igual que una por diseño—: acá una protección que no existe se veía igual que una que sí, y con más detalle que la real.